Files
Koichi SasadaandClaude Fable 5 e768a87a17 Ractor: the dying thread collects its own objspace post-mortem
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>
2026-08-11 03:33:40 +09:00
..
2025-11-19 16:05:12 +09:00
2026-03-06 14:02:00 +09:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00
2026-07-15 15:21:01 +09:00
2026-05-13 17:37:48 +09:00
2026-05-29 17:02:23 +09:00
2026-05-31 08:05:05 +09:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00
2024-01-11 10:09:53 -05:00
2026-07-22 23:52:01 +02:00
2023-05-21 23:00:27 +09:00
2023-12-07 15:52:35 -05:00
2021-09-10 20:00:06 +09:00
2026-03-16 15:04:38 +01:00
2021-09-10 20:00:06 +09:00
2023-12-07 15:52:35 -05:00
2021-09-10 20:00:06 +09:00
2021-09-10 20:00:06 +09:00