From 79e876126cd5fbde8b4b18ff395d908c4b395293 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Thu, 9 Apr 2026 13:04:18 +0400 Subject: [PATCH] fix(android): interpose pthread_create to bypass libstd's broken static stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- desktop/src-tauri/build.rs | 8 +++++ desktop/src-tauri/cpp/pthread_shim.c | 45 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 desktop/src-tauri/cpp/pthread_shim.c diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index e17495c..caf85a4 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -38,6 +38,14 @@ 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) => { diff --git a/desktop/src-tauri/cpp/pthread_shim.c b/desktop/src-tauri/cpp/pthread_shim.c new file mode 100644 index 0000000..f56d748 --- /dev/null +++ b/desktop/src-tauri/cpp/pthread_shim.c @@ -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 +#include +#include + +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__ */