Skip to content

fix: ExitProcess hang on Windows (2.2.7) - #56

Merged
Ttimmahlax merged 2 commits into
release/2.2.xfrom
fix/exit-hang-2.2.7
Oct 24, 2026
Merged

Ttimmahlax merged 2 commits into
release/2.2.xfrom
fix/exit-hang-2.2.7

Conversation

@Ttimmahlax

Copy link
Copy Markdown
Contributor

Problem

On Windows, a multithreaded process could hang forever inside ExitProcess, with one thread spinning at 100 % of a core in thread_done_cb. TerminateProcess cannot end it, because the process is already terminating. ArtCraft hit this 2 times in about 1,060 runs of DesignCraft's CLI (a rayon renderer, pinned to one core, on a loaded box), and never with the system allocator.

Cause

ExitProcess terminates every other thread wherever it stands, then runs the exiting thread's FLS callbacks. One of them is thread_done_cb, which abandons the thread's heap.

In the binary that hung, thread_done_cb+0x6e2 is the pause; lock cmpxchgb loop of heaps_lock(), reached from heaps_unregister at the end of thread_done_one. A pool worker was killed while it held HEAPS_LOCK during its own teardown, and nothing would ever release it.

The same exposure applies to every other wait on that path: abandoned_lock, the arena locks, and a page left in XFLAG_FREEING.

Fix

thread_done_cb (Windows) returns at once when RtlDllShutdownInProgress() is true. Nothing can adopt an abandoned heap at that point, and the address space is about to be discarded.

  • The ntdll import is raw-dylib, because windows-sys does not bind it.
  • On x86 the import name is undecorated.
  • Ordinary thread exits are unchanged: the flag is only set inside LdrShutdownProcess.

Evidence

  • New tests/exit_hang.rs: a child process parks a thread inside init::for_each_heap (which holds HEAPS_LOCK) and calls process::exit from a thread that owns a heap.
    • On 2.2.6: the child hung, at 6 h 44 min of CPU when it was found, and only TerminateThread could end it.
    • With this change: it exits in under a second.
  • On this branch: cargo test -p rusty_alloc -p rusty_alloc-api passes, including abandon_rss, teardown_reclaim and double_free::abandoned_double_free_aborts, which need normal thread exits to still abandon their heaps. Clippy reports no warnings, and cargo fmt --check is clean.
  • Not run: the x86_64-pc-windows-gnu and i686 Windows targets, so their raw-dylib link is untested.

Not covered

Code that allocates on the exiting thread after the other threads are killed, such as a DLL detach routine, can still meet a lock a killed thread held. That is much rarer. Bounding or yielding those waits is separate work.

Release

The base is release/2.2.x, a new branch at the 2.2.6 release commit (61ae839). Like 2.2.6, this release carries only the fix and not the unreleased speed work on main.

  • 8956990 is the fix, the test and the rusty_alloc changelog.
  • b1211b1 bumps the version to 2.2.7, the same shape as 61ae839.
  • Tagging (rusty_alloc-v2.2.7, rusty_alloc-api-v2.2.7) and publishing are left to the maintainer.
  • main needs the same fix under Unreleased, as a separate PR.

🤖 Generated with Claude Code

tim-almond-house and others added 2 commits October 24, 2026 07:04
ExitProcess terminates every other thread wherever it stands, then runs
the exiting thread's FLS callbacks, thread_done_cb among them. A thread
killed while holding HEAPS_LOCK (a pool worker in its own teardown) left
heaps_unregister spinning forever, in a process TerminateProcess cannot
end. ArtCraft hit it 2 times in ~1,060 runs; thread_done_cb+0x6e2 in
their binary is the heaps_lock spin. thread_done_cb now returns when
RtlDllShutdownInProgress() is true: nothing can adopt the heap and the
address space is about to go.

tests/exit_hang.rs parks a thread inside for_each_heap (HEAPS_LOCK held)
and exits the process; it hung on 2.2.6 and exits in under a second now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut from the 2.2.6 tag so the unreleased speed work on main does not ship
with it, the same way 2.2.6 was cut from 2.2.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ttimmahlax
Ttimmahlax merged commit 3e95fa4 into release/2.2.x Oct 24, 2026
12 of 13 checks passed
Ttimmahlax pushed a commit that referenced this pull request Oct 24, 2026
ExitProcess terminates every other thread wherever it stands, then runs
the exiting thread's FLS callbacks, thread_done_cb among them. A thread
killed while holding HEAPS_LOCK (a pool worker in its own teardown) left
heaps_unregister spinning forever, in a process TerminateProcess cannot
end. ArtCraft hit it 2 times in ~1,060 runs; thread_done_cb+0x6e2 in
their binary is the heaps_lock spin. thread_done_cb now returns when
RtlDllShutdownInProgress() is true: nothing can adopt the heap and the
address space is about to go.

tests/exit_hang.rs parks a thread inside for_each_heap (HEAPS_LOCK held)
and exits the process; it hung on 2.2.6 and exits in under a second now.
Shipped alone as 2.2.7 from release/2.2.x (#56).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants