fix(android): drop pthread_shim — clang shim makes it unnecessary (and harmful)
Some checks failed
Mirror to GitHub / mirror (push) Failing after 37s
Build Release Binaries / build-amd64 (push) Failing after 3m49s

Once the Dockerfile rewrites every android24-clang to exec android26-clang,
the linker uses the API-26 NDK sysroot and libstd's pthread_create reference
resolves directly against libc.so's real runtime symbol — no interposition
needed.

The pthread_shim.c approach was actually fighting its own solution: our
shim's dlsym() call bound at link time to libdl.a's STUB dlsym (a
five-line function inside libdl_static.o that just returns NULL and sets
dlerror to "libdl.a is a stub --- use libdl.so instead"). NDK r19 and
glibc 2.34 both replaced libdl.a with empty stubs because dynamic loading
is now part of the main libc/bionic — so no amount of link-order
tinkering can make a static libdl.a dlsym actually work.

Remove pthread_shim.c, the cc::Build::new().file("cpp/pthread_shim.c")
step in build.rs, and the -Wl,--wrap=pthread_create rustc-link-arg. Keep
getauxval_fix.c because that one DOES work at link time (the symbol
override is for a function compiler-rt defines statically, not one that
would depend on the stub libdl.a/libc.a).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-09 13:52:53 +04:00
parent 5df9d418c9
commit e2e023d2bc
2 changed files with 0 additions and 94 deletions

View File

@@ -38,14 +38,6 @@ fn build_oboe_android(target: &str) {
.file("cpp/getauxval_fix.c")
.compile("getauxval_fix");
// pthread_shim: interpose pthread_create so Rust libstd can't use the
// broken static pthread_create stub (which calls __init_tcb, crashing
// in a .so). Our shim forwards to libc.so's real one via RTLD_NEXT.
// Compiled as its own static lib so the linker links it ahead of libstd.
cc::Build::new()
.file("cpp/pthread_shim.c")
.compile("pthread_shim");
let oboe_dir = fetch_oboe();
match oboe_dir {
Some(oboe_path) => {
@@ -112,12 +104,6 @@ fn build_oboe_android(target: &str) {
// Oboe requires Android log + OpenSLES backends
println!("cargo:rustc-link-lib=log");
println!("cargo:rustc-link-lib=OpenSLES");
// Wrap pthread_create: redirect every `pthread_create` reference to our
// `__wrap_pthread_create` in pthread_shim.c, which forwards to the real
// libc.so symbol via dlsym. Without this the linker binds to libstd's
// bundled broken static pthread_create stub (see pthread_shim.c).
println!("cargo:rustc-link-arg=-Wl,--wrap=pthread_create");
}
/// Recursively add all .cpp files from a directory to a cc::Build.

View File

@@ -1,80 +0,0 @@
/* pthread_shim.c
*
* Interpose pthread_create via `--wrap=pthread_create` to bypass Rust libstd's
* broken static pthread_create stub. The stub (from an old bundled libc.a)
* calls __init_tcb+4 and SIGSEGVs in dlopen-loaded .so libraries.
*
* Approach:
* 1. The linker rewrites every `pthread_create` reference inside libstd
* (and everywhere else) into `__wrap_pthread_create` — our function.
* 2. Inside __wrap_pthread_create we call libc.so's real pthread_create
* via dlsym(RTLD_DEFAULT). Because --wrap redirects only references,
* the symbol `pthread_create` itself is NOT defined in our .so, so
* RTLD_DEFAULT finds the one exported by libc.so (always loaded).
* 3. We log every step via __android_log_print so we can diagnose any
* lookup failure from logcat (tag: WZP_pthread_shim).
*/
#ifdef __ANDROID__
#define _GNU_SOURCE
#include <android/log.h>
#include <dlfcn.h>
#include <pthread.h>
#include <stddef.h>
#include <stdio.h>
#define TAG "WZP_pthread_shim"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
typedef int (*pthread_create_fn)(pthread_t *, const pthread_attr_t *,
void *(*)(void *), void *);
static pthread_create_fn resolve_real_pthread_create(void) {
/* RTLD_DEFAULT: search the global symbol table starting with the main
* executable (app_process64), then every shared library loaded into the
* process in load order. libc.so is always loaded first, so its
* pthread_create export is the first match — and since we never define
* a symbol literally named "pthread_create" (only __wrap_pthread_create),
* there's no ambiguity. */
pthread_create_fn fn = (pthread_create_fn)dlsym(RTLD_DEFAULT, "pthread_create");
if (fn != NULL) {
LOGI("resolved pthread_create via RTLD_DEFAULT at %p", (void *)fn);
return fn;
}
LOGE("dlsym(RTLD_DEFAULT, pthread_create) returned NULL: %s", dlerror());
/* Fall back to explicit dlopen of libc.so — bionic is normally happy
* to hand back the already-loaded mapping. */
void *libc = dlopen("libc.so", RTLD_LAZY | RTLD_NOLOAD);
if (libc == NULL) {
libc = dlopen("libc.so", RTLD_LAZY);
}
if (libc == NULL) {
LOGE("dlopen(libc.so) failed: %s", dlerror());
return NULL;
}
fn = (pthread_create_fn)dlsym(libc, "pthread_create");
if (fn == NULL) {
LOGE("dlsym(libc, pthread_create) returned NULL: %s", dlerror());
return NULL;
}
LOGI("resolved pthread_create via dlopen(libc.so) at %p", (void *)fn);
return fn;
}
int __wrap_pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg) {
static pthread_create_fn real = NULL;
if (real == NULL) {
real = resolve_real_pthread_create();
if (real == NULL) {
LOGE("__wrap_pthread_create: no real pthread_create found, returning EAGAIN");
return 11; /* EAGAIN — best we can do */
}
}
return real(thread, attr, start_routine, arg);
}
#endif /* __ANDROID__ */