From 8945b2a42edd934e2a97271c8ce6a3522ebdaea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 5 Aug 2026 07:21:31 +0200 Subject: [PATCH 1/2] ci(gc): the macOS in-process RS4GC arm could never pass `RS4GC works on a stock toolchain via the in-process backend` asserts that a copying minor actually moved objects, by counting `[gc-copy-minor] ran copied_objects=` lines in the probe's stderr. Both of those prints are gated on PERRY_GC_DIAG (gc/copying.rs:993 and :1246), and this step never set it -- the sibling walker step does. So the trace held nothing but the probe's own #gcmetric lines, the assert read 0 copying minors / 0 objects copied off an effectively empty file, and the step failed regardless of how the collector behaved. The inverse of the usual hazard: not a gate that cannot fail, but one that cannot PASS. It shipped with the step in #7339 and had never executed, because three of the four arms in this matrix were permanently queued until #7393 added a concurrency group. Also makes the assert say what actually happened. A trace with no [gc-*] diagnostics at all is indistinguishable, by counts alone, from a collector that moved nothing, and the old message asserted the latter. That misdiagnosis is what made this cost a build to identify. Reproduced on macOS aarch64 against current main (cd29706c0, which contains #7398 and #7400, so it was not already fixed): the step as written reproduces the CI error byte-for-byte with a 3-line stderr; the same binary with PERRY_GC_DIAG=1 reports 2 copying minors / 10892 objects copied and the full step exits 0, stdout unchanged so the control diff still holds. The new branch is capable of failing: it fires on the pre-fix trace, passes on the post-fix one, and two negative controls (diagnostics present but zero copies; a synthetic manual_collect trace) still fail with the original message. This does not make the workflow green -- the other three arms fail earlier in "Probe matrix" for unrelated reasons. --- .github/workflows/gc-native-roots.yml | 15 +++++++++++++-- scripts/gc_evacuation_liveness_assert.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gc-native-roots.yml b/.github/workflows/gc-native-roots.yml index a0c8bf679b..e187683f7a 100644 --- a/.github/workflows/gc-native-roots.yml +++ b/.github/workflows/gc-native-roots.yml @@ -535,15 +535,26 @@ jobs: # Same answer as the shadow stack, and a collection that actually # moved something. Without the movement assert this passes with the # conservative scan doing all the rooting (#7336, #7338). + # + # PERRY_GC_DIAG=1 is what makes the evacuation assert able to see + # anything: `[gc-copy-minor] ran copied_objects=...` is printed only + # under that variable (`gc/copying.rs`). Without it the trace holds + # nothing but the probe's own `#gcmetric` lines, the assert reads + # 0 copying minors / 0 objects copied off an empty file, and the step + # fails no matter how the collector behaved — which is how this arm + # read on its first-ever execution (three of four arms in this matrix + # were permanently queued until #7393). Diagnostics go to stderr only, + # so the control diff below is unaffected. ./target/perry-dev/perry "$probe" -o /tmp/inproc-09-control PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ /tmp/inproc-09-control > /tmp/inproc-09.control.out 2>/dev/null - PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + PERRY_GC_DIAG=1 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off \ /tmp/inproc-09 > /tmp/inproc-09.out 2> /tmp/inproc-09.err diff /tmp/inproc-09.control.out /tmp/inproc-09.out \ || { echo "::error::in-process RS4GC diverged from the shadow-stack control"; exit 1; } - python3 scripts/gc_evacuation_liveness_assert.py /tmp/inproc-09.err + python3 scripts/gc_evacuation_liveness_assert.py /tmp/inproc-09.err \ + --probe "09_try_catch_roots (in-process)" # And it must be RS4GC doing the lowering, not a per-function bail to # the bridge -- which would make this arm green while testing the diff --git a/scripts/gc_evacuation_liveness_assert.py b/scripts/gc_evacuation_liveness_assert.py index 5501bf25ef..0525534027 100755 --- a/scripts/gc_evacuation_liveness_assert.py +++ b/scripts/gc_evacuation_liveness_assert.py @@ -26,6 +26,13 @@ COPIED = re.compile(r"copied_objects=(\d+)") ELIGIBLE = re.compile(r"\[gc-copy-minor\] eligible=(\w+)(?: fallback=(\S+))?") MANUAL = re.compile(r"\[gc-scan-fallback\] site=manual_collect") +# Any collector diagnostic at all. Every one of them is printed behind +# `PERRY_GC_DIAG`, so a trace with none of them was produced by a run that did +# not set it — which is indistinguishable, by counts alone, from a collector +# that moved nothing. Told apart below, because guessing wrong costs a build: +# the `gc-native-roots` in-process arm read as "evacuated NOTHING" on its +# first-ever execution purely for want of the variable. +ANY_DIAG = re.compile(r"^\[gc-[a-z-]+\]", re.MULTILINE) def main() -> int: @@ -44,6 +51,14 @@ def main() -> int: print(f"{args.probe}: evacuation live — {ran} copying minor(s), {copied} objects copied") return 0 + if not ANY_DIAG.search(text): + print(f"::error::{args.probe}: {args.trace} carries no collector diagnostics at all, " + "so this assert measured nothing about the collector. Every line it reads is " + "printed behind PERRY_GC_DIAG; re-run the binary with PERRY_GC_DIAG=1 set " + "alongside PERRY_GC_FORCE_EVACUATE=1. (Diagnostics go to stderr, so this does " + "not disturb an stdout oracle diff.)") + return 1 + print(f"::error::{args.probe}: the forced-evacuation arm evacuated NOTHING " f"({ran} copying minors, {copied} objects copied). The arm is vacuous: " f"it proves the program ran, not that a moving collector did (#7336).") From 013083cccaf418e958955614d6c577f4a057996b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 5 Aug 2026 07:21:36 +0200 Subject: [PATCH 2/2] docs: changelog fragment for #7414 --- changelog.d/7414-macos-rs4gc-inprocess-gc-diag.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 changelog.d/7414-macos-rs4gc-inprocess-gc-diag.md diff --git a/changelog.d/7414-macos-rs4gc-inprocess-gc-diag.md b/changelog.d/7414-macos-rs4gc-inprocess-gc-diag.md new file mode 100644 index 0000000000..3290451409 --- /dev/null +++ b/changelog.d/7414-macos-rs4gc-inprocess-gc-diag.md @@ -0,0 +1,12 @@ +**Fixed** the macOS `native-roots-rs4gc` arm's in-process step could never pass. + +It asserts that a copying minor moved objects by counting +`[gc-copy-minor] ran copied_objects=` lines, but both prints are gated on +`PERRY_GC_DIAG` and the step never set it. The trace held only the probe's own +`#gcmetric` lines, so the assert read 0/0 off an effectively empty file and +failed regardless of collector behaviour — the inverse of a gate that cannot +fail. It shipped with the step in #7339 and had never run, because three of four +arms in this matrix were permanently queued until #7393. + +The liveness assert now distinguishes "no collector diagnostics at all" from +"the collector moved nothing", which the old message conflated.