Skip to content

gc: forcing POINTER_FREE on a pointer-bearing object strands nothing — our zeal/protect instruments do not discriminate the layout-state hazard #7635

Description

@proggeramlug

The problem

While auditing #7633 I sabotaged layout_finish_deferred_boxed_object(ptr, saw_pointer)(ptr, false) — i.e. every JSON-parsed record claims
POINTER_FREE while holding heap pointers
. That is the stranded-live-child
hazard #7630's own soundness note names, and the exact failure the layout state
exists to prevent.

It produces byte-identical correct output under every instrument we have:

probe plain ZEAL=1 PROTECT_FROMSPACE=1 FORCE_EVACUATE=1
children also held in a separate array identical identical identical
children reachable ONLY via the record's slot identical identical identical

The second probe was built specifically because the first was vacuous: 4,000
records × 2 pointer fields, 40 rounds of churn to force promotion, children read
back only after the churn, so the record's slot is the sole path to each string.
8 retired quarantine sets and 16 copying minors observed live — the
instruments were armed and the collector was moving.

Why this is not "the branch is safe"

The obvious benign explanation is refuted. Parse value strings are not
longlived or interned: string_storage_alloc (string/mod.rs:504) uses
arena_alloc_gc, the ordinary nursery arena — arena_alloc_gc_longlived is the
other function. My probe's strings are 6–10 chars, above
SHORT_STRING_MAX_LEN = 5, so they are real heap StringHeaders in the
nursery. They are collectable and movable, and a POINTER_FREE record should
therefore lose them.

So either the tracer does not honour POINTER_FREE on this path (in which case
the state's trace-skip is not buying what it claims anywhere), or something
re-marks these objects that neither I nor the PR author identified.

Why it matters

Every future change to layout-state bookkeeping — and #7630's family has more
queued — will cite "zeal + from-space protect: output identical" as evidence of
no stranded children. That sentence currently discriminates nothing: a
mutation guaranteed to strand children passes it. This is CLAUDE.md's hazard 4
(a gate that runs but whose subject never did), applied to the instrument rather
than the job.

#7633 merged on the strength of its argument — the materialiser owns each
object end-to-end, finalize is reached on every path (verified: zero early exits
between store and finalize in both parse functions), and the pointer case is
conservative — plus a clean gap suite. That was the right call. But the argument
is now the only thing holding it up, and the next such change may not have one
as tight.

What would close this

A probe where forcing POINTER_FREE on a pointer-bearing object faults or
corrupts
. If one cannot be constructed, that is the finding: it means our
POINTER_FREE trace-skip is unobservable, and we should understand why before
building more optimizations on top of it. Either outcome is worth knowing, and
the probe becomes the regression test the whole family has been missing.

Starting points: check whether the full mark-sweep's conservative stack scan
(#7558) reaches these objects independently of their layout state; check whether
old-gen tracing consults _reserved layout bits at all after promotion; and try
a shape where the record is never promoted (small parse, forced minor only).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions