fix(android): interpose pthread_create to bypass libstd's broken static stub
Some checks failed
Mirror to GitHub / mirror (push) Failing after 36s
Build Release Binaries / build-amd64 (push) Failing after 3m52s

Builds #7, #8 and #9 all crashed at launch with the same SIGSEGV inside
__init_tcb(bionic_tcb*, pthread_internal_t*)+4 called via pthread_create
from std::sys::thread::unix::Thread::new.

Digging further: the problem is NOT the final linker we pass to cargo.
It's that rustup ships a PRE-COMPILED libstd for aarch64-linux-android
which was built statically against an old NDK libc archive. That archive
has a pthread_create stub which calls a static __init_tcb stub that
assumes libc's static init path has set up the TCB — which never happens
in a .so loaded via dlopen. Bumping minSdk to 26 or forcing the
android26-clang linker (903a07c) doesn't rebuild libstd and therefore
doesn't fix the bundled broken stub.

The legacy wzp-android crate dodged this with a getauxval_fix.c shim that
interposes getauxval via RTLD_NEXT. The same trick works for pthread_create
here: define our own `int pthread_create(...)` in cpp/pthread_shim.c that
forwards to `dlsym(RTLD_NEXT, "pthread_create")` — the real, fully working
version exported from libc.so. The linker processes our static lib before
libstd.rlib, so libstd's unresolved pthread_create reference binds to our
symbol, and the broken libc.a stub inside libstd is never pulled in.

build.rs compiles cpp/pthread_shim.c right after cpp/getauxval_fix.c so
both symbol overrides are in place before any Rust code gets linked.

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

View File

@@ -38,6 +38,14 @@ fn build_oboe_android(target: &str) {
.file("cpp/getauxval_fix.c") .file("cpp/getauxval_fix.c")
.compile("getauxval_fix"); .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(); let oboe_dir = fetch_oboe();
match oboe_dir { match oboe_dir {
Some(oboe_path) => { Some(oboe_path) => {

View File

@@ -0,0 +1,45 @@
/* pthread_shim.c
*
* Interpose pthread_create to bypass the broken static stub that Rust's
* pre-compiled libstd for aarch64-linux-android drags in.
*
* The stub (from an old NDK libc.a) calls __init_tcb(bionic_tcb*, ...)+4,
* which SIGSEGVs in .so libraries because __init_tcb expects TCB state that
* only the static-libc init path sets up. In a dlopen-loaded shared lib
* nothing ever initialises that state.
*
* By providing our own pthread_create at link time (which takes priority
* over the one dragged in by libstd) and forwarding it via dlsym(RTLD_NEXT)
* to the REAL pthread_create in libc.so, we completely sidestep the static
* stub — libc.so's pthread_create is the fully working runtime version.
*
* The same trick handles `getauxval` via getauxval_fix.c.
*/
#ifdef __ANDROID__
#define _GNU_SOURCE
#include <dlfcn.h>
#include <pthread.h>
#include <stddef.h>
typedef int (*pthread_create_fn)(pthread_t *, const pthread_attr_t *,
void *(*)(void *), void *);
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg) {
static pthread_create_fn real_pthread_create = NULL;
if (real_pthread_create == NULL) {
/* RTLD_NEXT: skip the symbol we're currently defining and return
* the next one in the search order — which is the real pthread_create
* exported from libc.so. */
real_pthread_create =
(pthread_create_fn)dlsym(RTLD_NEXT, "pthread_create");
if (real_pthread_create == NULL) {
return -1;
}
}
return real_pthread_create(thread, attr, start_routine, arg);
}
#endif /* __ANDROID__ */