Skip to content

gc: PERRY_GC_TRACE=1 SIGSEGVs under the evacuating arms — the liveness instrument crashes on the corpus file that most needs it #7018

Closed as not planned

Description

@proggeramlug

Summary

PERRY_GC_TRACE=1 crashes the program it is supposed to be observing. Under the evacuating arms, test_gap_repsel_scalar_replaced_locals SIGSEGVs with the flag set and passes without it.

This matters more than an ordinary flag bug: PERRY_GC_TRACE=1 is the tool this whole campaign relies on to prove a GC arm was live. PERRY_GC_DIAG under-reports (it only prints on some paths), so GC_TRACE's one-marker-per-completed-cycle output is the primary evidence that a green cell means anything. A green cell from an inert arm is exactly the false confidence that invalidated months of this project's earlier results (#6942, #6946, #6950).

So the instrument crashes on the case that most needs instrumenting.

Reproduction

PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \
PERRY_GC_TRACE=1 ./test_gap_repsel_scalar_replaced_locals
  • with PERRY_GC_TRACE=1 → SIGSEGV
  • without it → passes, byte-identical to the pinned Node 26.5.0 oracle, with copied_objects in the 261 251 – 379 072 range

Not a regression from the hoist

Confirmed pre-existing: reproduced identically on a rebuilt main without PR #7013, same arms, same failure. Reported during #7013's validation and deliberately not folded into that PR.

Status of this report

Measured by the agent that landed #7013; I have not independently re-verified it. First step for whoever picks this up is to reproduce both arms above before doing anything else.

Why it is plausible

The tracer runs inside a collection, walking structures the collector is mid-way through mutating — during an evacuating cycle, objects are being copied and forwarded while the trace reads them. A tracer that dereferences a forwarded-but-not-yet-rewritten pointer, or reads a header on an object already evacuated, would fault exactly here. That is a hypothesis, not a diagnosis; the stack will say.

Note the interaction with #6998 and #6995: both concern root words that are not what the decoder expects, and is_plausible_heap_addr is the shared predicate. If the tracer classifies raw words the same way, it may share their root cause.

Why it should be fixed rather than worked around

Every liveness claim in this campaign — "the arm moved N objects", "no green cell came from an inert arm" — is downstream of this flag. If it cannot run on the evacuating arms for some corpus files, then for those files we have no liveness evidence at all, and cells we currently call verified are only presumed verified.

Activity

  1. proggeramlug commented on Aug 2, 2026

    @proggeramlug
    ContributorAuthor

    Re-tested on main = d4ac65f21 and deliberately NOT closed — same reason as #6982.

    This issue's own status line asks whoever picks it up to "reproduce both arms above before doing anything else". Doing that:

    PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \
    PERRY_GC_TRACE=1 ./test_gap_repsel_scalar_replaced_locals
    
    • with PERRY_GC_TRACE=1: rc=0, 5/5. Adding PERRY_GC_FORCE_EVACUATE=1: rc=0, 5/5.
    • without it: rc=0, 2/2.

    No SIGSEGV on either arm. But the liveness numbers say the comparison is void:

    copy-minors copied_objects
    this issue's cited range, no trace — 261 251 – 379 072
    d4ac65f21, PERRY_GC_TRACE=1 0 0
    d4ac65f21, no trace 0 0

    The file performs zero copying minors on this build in either arm. The tracer is documented here as crashing because "it runs inside a collection, walking structures the collector is mid-way through mutating" — with no evacuating collection running, it never gets the chance. Green is unfalsifiable.

    Blocked on #7255 (the requires=scavenge arms went inert), same as #6982.

    Worth noting the second-order cost, which is this issue's own argument turned on itself: PERRY_GC_TRACE=1 is the instrument every liveness claim in this campaign is downstream of, and right now it reports honestly that there is nothing to observe. That is the correct behaviour and it is also why this issue cannot be retired from a green run.

    Part of the sweep in #7262.

  2. proggeramlug commented on Aug 2, 2026

    @proggeramlug
    ContributorAuthor

    Judgeable again, and it does not reproduce

    #7255 was blocking this: the sweep that found "0 copying minors even with
    PERRY_GC_FORCE_EVACUATE=1" was measuring the default/verify_evac family,
    which #7161 took to copy-minor 0/50 corpus-wide. This issue's own arm was
    never in that family — it is the matrix's evac_minor cell, and that one is
    demonstrably live.

    Measured on 61410a222 + #7255's branch, release, macOS arm64, node 26.5.1 (the pin):

    Liveness of the arm first, because a green run under an inert arm asserts
    nothing. test_gap_repsel_scalar_replaced_locals x evac_minor in the matrix:
    cycles=61 evacuated=0 scavenged=1577269. The copying minor runs, and it copies
    1.58M objects — in the same order of magnitude as the 261 251–379 072 this issue
    cites, on a corpus that has grown since.

    The exact repro, 10 runs:

    PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \
    PERRY_GC_TRACE=1 ./test_gap_repsel_scalar_replaced_locals
    
    rc GC cycles output vs node
    runs 1–10, with PERRY_GC_TRACE=1 0 61 each byte-exact
    runs 1–10, control without it 0 — byte-exact

    10/10 clean with the tracer on, 61 completed cycles per run, byte-identical to
    the pinned oracle. The instrument does not crash the program it observes on this
    file at this commit.

    I have not bisected to find what fixed it, and the original report was itself
    second-hand ("I have not independently re-verified it"). But the two things that
    made this unjudgeable — an inert arm, and no way to tell an inert arm from a
    clean one — are both gone: the matrix now fails when an arm satisfies its
    requires= on zero cells, so a future green on this cell means the copying minor
    ran.

    Recommend closing as not-reproducing, with the liveness evidence above as the
    reason it is safe to.

  3. proggeramlug commented on Aug 9, 2026

    @proggeramlug
    ContributorAuthor

    Closing: does not reproduce, and the hypothesis is structurally refuted as well as empirically. Re-verified at HEAD a853135aa (v0.5.1384), both link modes, as the issue's own "first step for whoever picks this up" asked.

    The reported repro, 20 runs, 0 crashes

    (a) Matrix-faithful link — PERRY_NO_AUTO_OPTIMIZE unset, the way gc_repsel_matrix.sh deliberately compiles.

    arm runs rc
    PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_TRACE=1 5/5 0
    control, no trace 5/5 0

    That arm alone is not evidence, and is reported as such. Auto-optimize relinks the runtime --no-default-features, so the diagnostics feature is off and GcCycleTrace::emit falls to the #[cfg(not(feature = "diagnostics"))] stub (gc/telemetry.rs). All 113 [gc] cycle lines read "diagnostics feature disabled" — the real tracer never ran. A green run there says nothing about the tracer, which is the same shape as #6942/#6946.

    (b) Non-vacuous link — PERRY_NO_AUTO_OPTIMIZE=1 against an archive that carries diagnostics.

    arm runs rc liveness
    full repro env + PERRY_GC_TRACE=1 5/5 0 110 real "event":"gc_cycle" objects (95 minor + 15 full), copied_objects sum 466,424
    control, no trace 5/5 0 stdout byte-identical to the trace arm

    So the arm is demonstrably live — the copying minor ran and moved 466k objects — the tracer emitted its real output, and nothing crashed. That is the combination this issue says should SIGSEGV.

    13 .ips crash reports were produced during the investigation; every one is EXC_CRASH/SIGABRT from an unrelated instrument (see #7677). Zero SIGSEGV, and nothing at all was written during the window the pure #7018 arms ran.

    Not file-specific

    Four other corpus files, same arms, diagnostics-linked, 3 runs each — all rc=0, stdout identical between arms:

    file cycles copied
    test_gap_repsel_gc_stress 57 479,504
    test_gap_repsel_ptr_shape_elements 1 6,154
    test_gap_gc_pointer_publish 8 31,323
    test_gap_repsel_element_shape_loop_clone 1 6,121

    A 13-configuration env sweep is also clean, including +PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1, +PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800 (257 cycles, 1,385,127 copied) and +PERRY_GC_ZEAL=1. Of the three env vars in the repro, only PERRY_GC_HEAP_LIMIT=8 changes anything at all (13 → 110 cycles); PERRY_GC_INCREMENTAL=0 and PERRY_CONSERVATIVE_STACK_SCAN=off change nothing on this file.

    The hypothesis cannot hold — a name collision, not a defect

    PERRY_GC_TRACE is read in exactly one production place: gc_trace_enabled() (gc/policy.rs:571, OnceLock-cached). It has exactly two production call sites:

    • gc/policy.rs:760 — steps_before: gc_trace_enabled().then(GcStepSnapshot::current), a scalar counter snapshot;
    • gc/barrier.rs:931 — bump_write_barrier_trace_counter, incrementing u64 fields of a thread-local Cell.

    Emission is GcCycleTrace::emit, which serde-serialises already-accumulated scalars and eprintln!s one line at cycle end. Nothing on the PERRY_GC_TRACE path dereferences a heap object, walks the object graph, or reads a GcHeader. There is no pointer decode at all, so it cannot dereference a forwarded-but-not-yet-rewritten pointer, and is_plausible_heap_addr is not involved — the suggested kinship with #6998/#6995 does not hold either.

    The issue's phrasing — "the tracer runs inside a collection, walking structures the collector is mid-way through mutating" — accurately describes gc/trace.rs, the marking tracer and home of ValidPointerSet. That one runs on every collection regardless of PERRY_GC_TRACE. Two different things called "the tracer"; the flag gates the one that only counts.

    What this changes for the campaign

    The issue's real worry — "every liveness claim in this campaign is downstream of this flag" — stands, but the failure mode is the opposite of a crash. It is (a): under the shipped auto-optimize link the flag emits a stub marker and the JSON trace is unavailable, so a [gc] cycle count is a count of stubs. gc_repsel_matrix.sh already handles both formats for counting cycles, and #7676 extends the same both-spellings treatment to the reclamation counters, because the malloc sweep's bytes exist only in the JSON form. Anyone reaching for PERRY_GC_TRACE as evidence should check which of the two lines they are getting first.

    Filed separately from this investigation: #7677 (PERRY_GC_FROMSPACE_SCAN_ABORT=1 aborts deterministically on two corpus files — a genuine and unrelated finding).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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