---
name: main-thread-tls-destructors-never-run
description: "process::exit skips main-thread TLS destructors — moving work onto a spawned thread makes Drop impls RUN that the binary had never once executed, a whether-change disguised as a timing-shift"
metadata: 
  node_type: memory
  type: project
  originSessionId: 3b6f6e5f-c7fa-447c-b4bc-bf7a1df9707a
  modified: 2026-08-04T18:16:07.667Z
---

2026-08-04, USHER #150 golden red RCA. `crates/spt/src/main.rs` ended `let code = cli::run();
std::process::exit(code);`. U3's stack wrapper (`ea9f43b`) changed it to run `cli::run` on a named
16 MiB spawned thread and `join()` it, because clap's derive-built `Command` tree overflowed the
Windows 1 MiB main-thread stack at ~3 more arguments. `process::exit` is unchanged and still on the
main thread.

**The teardown consequence is not "TLS teardown moves" — it is "TLS teardown starts HAPPENING."**
Main-thread thread-local destructors do NOT run under `std::process::exit`; the process is torn down
without unwinding. So every `Drop` reachable from `cli::run`'s thread-locals had NEVER executed in
this binary's life. Once the CLI runs on a spawned thread, that thread's TLS destructors DO run at
thread exit — before `join` returns, before `main` reaches `process::exit`, and crucially AFTER all
of the command's normal output has already been emitted.

**Why:** ⭐⭐ it predicts a specific and otherwise-puzzling signature: a child that prints its full
normal output (here `BOUND:` then `READY:`), then exits NONZERO with no Rust panic anywhere in its
captured stderr. Not a panic (that maps to 101 and still prints through the hook), not an ordinary
error return (the code path was an unconditional `return 0`), not a deliberate exit (the reachable
`process::exit` population had no nonzero site on that path) — the death is in teardown, after the
value path already produced 0. Diagnosing "where does teardown differ" as a timing question misses
that code which never ran now runs; that is a strictly stronger and much more testable claim.

**How to apply:** when a wrapper moves work off the main thread, the review question is "which `Drop`
impls execute now that never did?" — enumerate `thread_local!` in the moved code's reachable set and
check for non-trivial `Drop`, especially anything touching handles a background thread may still hold
or that the runtime has already torn down. Same question for any change that replaces `process::exit`
with a normal return, which turns exit-skipped destructors into executed ones across the board.
Related: [[assert-gates-exit-status-but-prints-stderr]] is why the exit CODE was unavailable to
discriminate this at all — the rig printed stderr and asserted on `status.success()`.
