Summary
Budgeted GC performs its one-time FinalRootRemark, then permits mutator windows while weak processing is still incomplete. During such a window, WeakRef.deref() / WeakMap.get() can turn an unmarked weak target into a strong compiled-code local through a read. The incremental write barrier and allocate-black births do not observe that transition. A later weak-processing slice can therefore tombstone the target and sweep it while generated code still holds the returned pointer.
This is a correctness problem, not only a pause-quality tradeoff.
Interleaving
With two registered weak holders and a one-holder assist budget:
- A budgeted minor/full completes marking and runs
FinalRootRemark once.
WeakProcessing snapshots the holder registry and processes holder A. Because the subphase is classified as sliced, it returns to the mutator with holder B still pending.
- B's target
T is white: weak edges are deliberately excluded from the strong trace.
- The mutator executes
B.deref() (or weakMap.get(key)) and receives T into a generated stack/register local. This is a read, so neither the heap-store barrier nor allocate-black birth accounting shades T.
- The next assist processes B, sees
T unmarked, clears the weak slot, and later sweep reclaims T.
- Compiled code still has the strong local returned in step 4.
The native/statepoint lowering makes the local discoverable when roots are scanned, but no root scan occurs after this mutator window: FinalRootRemark has already completed.
The full path also permits a sliced remembered-set rebuild between FinalRootRemark and weak decisions, so the underlying contract needs to cover every post-remark mutator window, not just the newly sliced holder loop.
Evidence
Required contract / acceptance criteria
Choose and document one sound design, for example:
- keep the interval from the final root remark through completed weak decisions and the sweep snapshot atomic; or
- add a weak-read barrier/epoch that shades a returned target while weak processing is pending, drain those shades, and make already-issued weak decisions consistent; or
- move/repeat the final root remark after the last mutator window, then complete weak decisions through sweep without exposing the mutator.
And add coverage:
Audit context
Found while re-auditing the GC/performance delta through v0.5.1481. #7892 correctly removed the O(total-heap) weak-holder search, but its resumable post-remark execution changes the liveness synchronization contract.
Summary
Budgeted GC performs its one-time
FinalRootRemark, then permits mutator windows while weak processing is still incomplete. During such a window,WeakRef.deref()/WeakMap.get()can turn an unmarked weak target into a strong compiled-code local through a read. The incremental write barrier and allocate-black births do not observe that transition. A later weak-processing slice can therefore tombstone the target and sweep it while generated code still holds the returned pointer.This is a correctness problem, not only a pause-quality tradeoff.
Interleaving
With two registered weak holders and a one-holder assist budget:
FinalRootRemarkonce.WeakProcessingsnapshots the holder registry and processes holder A. Because the subphase is classified as sliced, it returns to the mutator with holder B still pending.Tis white: weak edges are deliberately excluded from the strong trace.B.deref()(orweakMap.get(key)) and receivesTinto a generated stack/register local. This is a read, so neither the heap-store barrier nor allocate-black birth accounting shadesT.Tunmarked, clears the weak slot, and later sweep reclaimsT.The native/statepoint lowering makes the local discoverable when roots are scanned, but no root scan occurs after this mutator window:
FinalRootRemarkhas already completed.The full path also permits a sliced remembered-set rebuild between
FinalRootRemarkand weak decisions, so the underlying contract needs to cover every post-remark mutator window, not just the newly sliced holder loop.Evidence
FinalRootRemarkperforms an unbounded one-time root scan and drain, then advances onward.WeakProcessingis explicitly classified as sliced and may return to the mutator.budgetholders and leaves the remaining cursor for later.Required contract / acceptance criteria
Choose and document one sound design, for example:
And add coverage:
Audit context
Found while re-auditing the GC/performance delta through v0.5.1481. #7892 correctly removed the O(total-heap) weak-holder search, but its resumable post-remark execution changes the liveness synchronization contract.