Parent: #482 Related: #474, #480, #493, #885, #886
Problem
A completed Thread’s return value is not published into ThreadRecord.result until after several GC-capable operations. Because a running peer’s native stack is not conservatively scanned by the parallel collector, threadMain tries to bridge that interval with:
machine.gc_temp_roots.append(machine.arena, out) catch {};
Its own comment identifies this as the return value’s only precise root. If the append fails, microtask drains, waiter settlement, timers, or task pumping can advance collection and reclaim/relocate the value while it is still used.
After publishThreadCompletion, rec.result and rec.settling_joins are already traced under join_mutex; the later ignored temp-root appends are therefore redundant, not the only owners. However, the corresponding promise.resolve/reject calls and final drains also use catch {}, and finishThreadJoinSettlement publishes joins_settled = true even if a pending asyncJoin Promise was never settled or its required reaction publication failed.
#885 owns general Promise state-to-microtask publication. #886 owns already-existing queue transfer/restoration. This issue owns Thread completion’s pre-publication result root and its completion-state transaction.
Required implementation
- Publish the Thread result into a traced, join-mutex-protected ThreadRecord slot before any post-call GC-capable drain, without making
doneobservable early. - Keep
rec.resultandrec.settling_joinsas the authoritative post-publication roots; remove redundant best-effort temp-root appends or turn them into proved non-fallible optimization-only storage. - Publish
doneandjoins_settledonly at states that accurately describe the result, pending join capability settlement, and rerouted reaction ownership. - Propagate or materialize OOM through each asyncJoin result exactly once; never silently leave it pending.
- Compose with Thread termination, nested asyncJoin, thenable assimilation, local-to-realm queue closure, concurrent marking/moving GC, and teardown.
- Preserve the current synchronous join rule: it waits for
done && joins_settled && exited.
No-workaround rules
- No swallowed required root/settlement error, reliance on an unscanned running stack, disabling parallel GC, global GIL, leaking Thread records, capping valid pending joins, retrying user callbacks, or marking completion early.
- A barrier is not a substitute for durable rooting across later collection progress.
- Do not retain redundant fallible root copies as a false safety claim when the ThreadRecord already owns the post-publication roots.
Acceptance
- A deterministic allocator-fault witness proves the pre-publication result remains rooted when the temp-root allocator is exhausted.
- Forced concurrent GC between function return, each drain/waiter step, completion publication, join settlement, and Thread exit cannot reclaim or relocate an unpublished result or Promise.
- Every pre-existing asyncJoin Promise fulfills/rejects exactly once, or receives the exact engine OOM completion, before
joins_settledbecomes observable. - Root tracing/relocation tests prove
rec.result,pending_joins, andsettling_joinsownership at each state transition. - Nested joins, thenables, throwing Threads, termination, waitAsync/property-async rerouting, and late local-queue closure preserve order and liveness.
- Bounded heap, moving/parallel GC, lifecycle/mid-GC/threadfuzz, full units, and suppression-free TSan pass without leaks or suppressions.
- Exact Promise/async/Thread Test262 and PR-249 coverage has zero regressions; documentation makes no unsupported recovery guarantee.
Security boundary
Thread return values and pending join counts are script-controlled, and allocation pressure can coincide with concurrent marking. Required precise roots must be fail-closed engine invariants, not best-effort bookkeeping.