mirror of
https://github.com/ruby/ruby.git
synced 2026-10-29 08:55:35 +08:00
The retire GC at notify_exit could not reclaim the terminating thread's scaffolding -- the Thread and Fiber wrappers, the ThreadGroup, the Ractor-local stdio, whatever the machine stack still references -- because at that point they were all still rooted. Those ~11 objects kept ~2 heap pages alive per dead Ractor, merged into the inheritor to wait for its next major GC, so both a spawn-and-join loop and a fire-and-forget loop grew the heap by two pages per Ractor with nothing reclaiming them. Instead, run the final collection at the tail of thread_start_func_2, just before the scheduler handoff. The thread still holds the GVL, so the collection runs lock-free like any local GC (a concurrent global GC waits for its safepoint), and vm_remove_ractor keeps its light lock-only section. r->postmortem makes rb_ractor_mark_local_roots root only what must survive the death -- the join value (r->sync.legacy) and the rb_gc_register_mark_object pins -- and the zombie_objspaces entry recorded later measures exactly the pages the joiner will inherit. While the Ractor is still in vm->ractor.set, rb_vm_mark and ractor_mark keep the Ractor object itself and its default port alive as before; everything they mark is foreign to or pinned by this collection. Two of the swept wrappers are the dying thread's own: its Thread (the rest of the collection resolves GET_EC through ec->thread_ptr) and its root Fiber (whose struct embeds that ec). thread_free and fiber_free recognize "the struct I am standing on" (ec == the current ec), keep the struct, and leave an in-band mark (th->self = 0; cont.self = 0, which already means "no wrapper" on the never-wrapped path). Found as a 1/30 heap corruption, pinned by ASAN. The collecting thread captures those two pointers right after the collection, while its GVL still excludes any global GC, and frees exactly the captured structs at its very last step -- after vm_remove_ractor unlinks the Ractor, anything it did not sweep may already be freed by a concurrent global GC, so the epilogue reads no shared memory at all. Those frees would resolve their objspace through the TLS ec -- which sits inside the fiber struct being freed and leads to the retired Ractor -- so rb_ractor_postmortem_free severs the TLS ec first (the MN epilogue already had) and they fall back to the main objspace. While a thread-event hook is registered the postmortem flag stays off and the collection keeps the ordinary retire roots: callbacks may retain any Ractor's Thread object (rb_internal_thread_event_hook_t), and such references are invisible to the reduced roots. The check takes the hook rwlock, so it happens-after any completed registration; a hook registered after it gets no event from this thread -- rb_thread_execute_hooks skips a thread whose wrapper is gone (th->self == 0) -- so it never receives what the collection swept. On platforms without hook support (win32, none) the check reports false. The collection runs after thread_cleanup_func, so the dying thread's machine stack is already torn down: the root scan skips its machine context (rb_gc_impl_during_postmortem_p), which holds nothing the reduced roots do not already cover and faults when scanned on some platforms (windows-arm). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>