Skip to content

gc: sliced weak processing exposes post-remark weak-to-strong read race #7900

Description

@proggeramlug

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:

  1. A budgeted minor/full completes marking and runs FinalRootRemark once.
  2. 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.
  3. B's target T is white: weak edges are deliberately excluded from the strong trace.
  4. 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.
  5. The next assist processes B, sees T unmarked, clears the weak slot, and later sweep reclaims T.
  6. 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:

  • Deterministic state-machine test with at least two weak holders and a one-unit assist budget: pause after one holder, strongly acquire the other target through the public weak read, resume, and prove the target remains valid.
  • Exercise both budgeted minor and full ordering (the full path has sliced remembered-set rebuild before weak processing).
  • Exercise the native/statepoint lowering, where the newly strong value can exist only in a generated local; do not make the witness rely solely on a root-store barrier that would mask the bug.
  • Add an invariant/telemetry assertion that no mutator window exists after the last root observation unless weak reads participate in marking.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions