From f015be63ecee6d1dc19fb78df88b42ba9c8af200 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Thu, 9 Apr 2026 13:08:41 +0400 Subject: [PATCH] fix(android): use --wrap=pthread_create instead of raw symbol override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build #10 failed with: ld.lld: error: duplicate symbol: pthread_create >>> defined at pthread_shim.c:30 >>> ... in archive libpthread_shim.a (the other definition coming from libstd's bundled libc.a stub) The raw-symbol-override approach was naive: when two static archives both define the same symbol the linker refuses instead of picking one. Switch to GNU-ld's `--wrap=pthread_create` mechanism: - All `pthread_create` references get rewritten to `__wrap_pthread_create` - Our shim now defines `__wrap_pthread_create` (no symbol clash) - Inside the shim we `dlopen("libc.so")` + `dlsym("pthread_create")` to get the real runtime symbol directly, bypassing BOTH the broken static stub (libstd's libc.a copy) AND libstd's own pthread_create path - `--real_pthread_create` is deliberately NOT used — it would alias the same broken stub the wrap exists to avoid The wrap flag is emitted via `cargo:rustc-link-arg` in build.rs so it only affects the Android target (the Android-branch of build.rs is the only place that emits it). Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src-tauri/build.rs | 6 ++++ desktop/src-tauri/cpp/pthread_shim.c | 53 ++++++++++++++++------------ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index caf85a4..3584776 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -112,6 +112,12 @@ 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. diff --git a/desktop/src-tauri/cpp/pthread_shim.c b/desktop/src-tauri/cpp/pthread_shim.c index f56d748..3670dd7 100644 --- a/desktop/src-tauri/cpp/pthread_shim.c +++ b/desktop/src-tauri/cpp/pthread_shim.c @@ -1,19 +1,19 @@ /* pthread_shim.c * - * Interpose pthread_create to bypass the broken static stub that Rust's - * pre-compiled libstd for aarch64-linux-android drags in. + * Interpose pthread_create via linker --wrap to bypass Rust libstd's broken + * static pthread_create stub (pulled in from an old NDK libc.a), which + * transitively calls __init_tcb+4 and SIGSEGVs in any .so loaded via dlopen. * - * 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. + * Link flags (see build.rs): + * -Wl,--wrap=pthread_create * - * 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 wrap flag makes the linker redirect every unresolved reference to + * `pthread_create` → `__wrap_pthread_create` (below). Inside the shim we + * explicitly open libc.so and look up the real, fully working runtime + * pthread_create, bypassing libstd's bundled archive entirely. * - * The same trick handles `getauxval` via getauxval_fix.c. + * We deliberately do NOT call `__real_pthread_create` — that alias is the + * SAME broken stub the wrap is designed to get around. */ #ifdef __ANDROID__ @@ -26,20 +26,29 @@ 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) { +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) { + /* Explicitly open libc.so and fetch the runtime pthread_create. + * RTLD_NOLOAD would be wrong here — we want the mapping even if + * bionic already loaded libc. The standard filename "libc.so" is + * safe on Android; bionic's dynamic linker resolves it to + * /apex/com.android.runtime/lib64/bionic/libc.so at runtime. */ + void *libc = dlopen("libc.so", RTLD_LAZY | RTLD_GLOBAL); + if (libc != NULL) { + real = (pthread_create_fn)dlsym(libc, "pthread_create"); + } + if (real == NULL) { + /* Fallback: try RTLD_DEFAULT — may or may not work depending on + * link order but it's better than segfaulting. */ + real = (pthread_create_fn)dlsym(RTLD_DEFAULT, "pthread_create"); + } + if (real == NULL) { return -1; } } - return real_pthread_create(thread, attr, start_routine, arg); + return real(thread, attr, start_routine, arg); } #endif /* __ANDROID__ */