llvm-nm on the crashing .so confirmed the research's smoking gun theory:
000000000130c1f0 t _Z10__init_tcbP10bionic_tcbP18pthread_internal_t
0000000000000000 a pthread_create.cpp
0000000001331108 t pthread_create
All lowercase 't' (= LOCAL text symbols), zero UND dynamic references
for pthread_create. So rustc's link step is pulling bionic's own
pthread_create.cpp compilation unit out of libc.a as a whole-archive
inclusion and binding those symbols locally inside our .so, instead
of letting them stay UND and resolved against libc.so at dlopen time.
Rust's libstd thread::spawn then calls the LOCAL (broken) pthread_create
which calls the LOCAL __init_tcb with arguments set up for bionic's
static-executable layout — crashes at __init_tcb+4 with SEGV_ACCERR.
`-Wl,--exclude-libs,ALL` tells the linker to make symbols from static
archives NOT appear in the dynamic symbol table of the output .so.
`-Wl,--no-whole-archive` tells it to only pull archive objects that
satisfy undefined references, not include the whole archive blindly.
If this works, the symbol table should show pthread_create as UND
(or at least not locally bound) and the app should launch. If it
doesn't, the remaining fallback is the research's action #3 —
extract the C++ into its own upstream cdylib crate built with
cargo-ndk, and dlopen it from the Tauri cdylib at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>