Code change:
fn expect_aborted(status: ExitStatus) { dbg!(status); let signal = status.signal().expect("expected child process to die of signal"); <ins> #[cfg(not(target_os = "android"))] </ins> assert!(signal == libc::SIGABRT || signal == libc::SIGILL || signal == libc::SIGTRAP); <ins> #[cfg(target_os = "android")] { // Android signals an abort() call with SIGSEGV at address 0xdeadbaad // See e.g. https://groups.google.com/g/android-ndk/c/laW1CJc7Icc assert!(signal == libc::SIGSEGV); // Additional checks performed: // 1. Find last tombstone (similar to coredump but in text format) from the //    same executable (path) as we are (must be because of usage of fork): //    This ensures that we look into the correct tombstone. // 2. Cause of crash is a SIGSEGV with address 0xdeadbaad. // 3. libc::abort call is in one of top two functions on callstack. // The last two steps distinguish between a normal SIGSEGV and one caused // by libc::abort. let this_exe = std::env::current_exe().unwrap().into_os_string().into_string().unwrap(); let exe_string = format!(">>> {this_exe} <<<"); let tombstone = (0..100) .map(|n| format!("/data/tombstones/tombstone_{n:02}")) .filter(|f| std::path::Path::new(&f).exists()) .map(|f| std::fs::read_to_string(&f).expect("Cannot read tombstone file")) .filter(|f| f.contains(&exe_string)) .last() .expect("no tombstone found"); println!("Content of tombstone:n{tombstone}"); assert!( tombstone.contains("signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr deadbaad") ); let abort_on_top = tombstone .lines() .skip_while(|l| !l.contains("backtrace:")) .skip(1) .take_while(|l| l.starts_with("    #")) .take(2) .any(|f| f.contains("/system/lib/libc.so (abort")); assert!(abort_on_top); } </ins> } fn main() {

Related discussion:
text: Since is stored in which is not a thread-local, maybe the program could abort before using any thread-local.
It can't do that and also print the payload, because printing the payload might itself panic, and the recursive panic needs to be detected.
Maybe instead there should be a which can return None to mean "this thread-local variable has not been initialised".
I think it might be possible to use a to avoid the allocation.
Do thread-local always avoid the allocation? I think they fall back to regular thread-locals on platforms where avoiding allocations is not (easily) possible.
The version of looks basically same as the non- version when using , which will likely allocate here: Maybe the panic count could be stored directly in rather than on the heap. But is itself safe after ? If it tries to hold a lock or something it's possible to deadlock.
Should this only occur on Android? It happened on QNX as well (not yet upstreamed, work in progress so I don't trust it yet), but I can also reproduce it on with a simple patch (disables thread local storage): Would this be a target spec where we would expect to succeed?
Interesting. To be honest, I'm not sure. I do know that modern libc doctrine is that allocation after fork is UB. The test case is checking that we can safely panic after fork, and abort, without UB. This ought to work due to . I think that every failure of this test case is a real bug. Solving those bugs is not so easy because the machinery inside panic uses thread-local state which apparently is really super hard to make work without allocating or taking locks.
Yes, allocating memory is UB, same as working with mutexes. In case our setups are okay (i.e. having and using fork etc.), I also see this is a bug. Maybe we don't need to fix the issue with the thread-local state (if at all possible): calls to get the number recursive panics and if we . : determines using --works determines number recursive panics using TLS --allocates after --UB first: If it is true, it will always call -- the process will be killed very very soon by the OS In case of a recursion, a hardcoded-string is printed; otherwise additionally Do we really need to correctly determine a recursion, or can we allow "false positives"?