From 0bf19e9c52f39c52697f13127845f33240194d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 4 Aug 2026 07:53:24 +0200 Subject: [PATCH] ci(gc): let the statepoint arm pass, and put ELF back under test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate has been red on main since it landed, five runs, and neither cause was a code regression. It could not pass. native-roots-aarch64 runs on macos-14, whose image ships Apple clang 15 — which does not register the statepoint-example GC strategy — so every run died with 'error in backend: unsupported GC: statepoint-example' before a probe executed. A gate that cannot pass is as useless as one that cannot fail, and this one read as a real regression for a day. It now pins Homebrew LLVM the way the RS4GC job beside it already did, and asserts the pinned toolchain registers the strategy so a future image change fails with a clear message instead of deep inside a probe compile. Newer Apple clang does register it (21 does locally), so this is the runner image, not Apple clang. It stopped testing ELF. Both GC arms are on macOS, so nothing exercised the object format the compact map was reworked for — and every bug that reached main there was ELF-only and invisible on Mach-O: SHF_GNU_RETAIN or --gc-sections drops the section; SHF_WRITE or the relocated addresses force a DT_TEXTREL in a PIE; eh_walker's asm used the Mach-O underscore convention so aarch64-Linux could not link. native-roots-elf-aarch64 runs the same matrix on ubuntu-24.04-arm with both liveness asserts. No toolchain install needed — stock Ubuntu clang registers the strategy, verified on 18.1.3 on a real host. --- .github/workflows/gc-native-roots.yml | 111 +++++++++++++++++- .../7341-gate-toolchain-and-elf-arm.md | 26 ++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7341-gate-toolchain-and-elf-arm.md diff --git a/.github/workflows/gc-native-roots.yml b/.github/workflows/gc-native-roots.yml index 94c59c4277..8cd1e4dfbf 100644 --- a/.github/workflows/gc-native-roots.yml +++ b/.github/workflows/gc-native-roots.yml @@ -111,6 +111,35 @@ jobs: set -euo pipefail export PERRY_RUNTIME_DIR="$PWD/target/perry-dev" export PERRY_NO_AUTO_OPTIMIZE=1 + + # Pin an LLVM that registers the `statepoint-example` GC strategy. + # Without this the job cannot pass at all: the macos-14 image ships + # Apple clang 15, which does not register it, so every run died with + # fatal error: error in backend: unsupported GC: statepoint-example + # before a single probe executed. That is CLAUDE.md's failure mode + # inverted — a gate that cannot PASS is as useless as one that cannot + # fail, and it stayed red across five consecutive main runs while + # looking like a real regression. + # + # Newer Apple clang does register it (21 does, locally), so this is a + # property of the runner image rather than of Apple clang forever. + # Pin explicitly rather than depend on the image. The RS4GC job below + # already does this for its own reasons. + brew list llvm >/dev/null 2>&1 || brew install llvm + llvm_bin="$(brew --prefix llvm)/bin" + if [ ! -x "$llvm_bin/clang" ]; then + echo "::error::no clang under $llvm_bin — cannot run the statepoint arm, and skipping it silently is the gate that cannot fail" + exit 1 + fi + export PERRY_LLVM_CLANG="$llvm_bin/clang" + "$PERRY_LLVM_CLANG" --version | head -2 + # Assert the pinned toolchain actually supports the strategy, so a + # future image change fails here with a clear message instead of + # deep inside a probe compile. + printf 'define void @f() gc "statepoint-example" {\n ret void\n}\n' > /tmp/gcstrategy.ll + "$PERRY_LLVM_CLANG" -c /tmp/gcstrategy.ll -o /dev/null \ + || { echo "::error::pinned clang does not support gc \"statepoint-example\""; exit 1; } + pass=0 total=0 errs="" @@ -470,6 +499,80 @@ jobs: # REFUSAL (never a silently rootless binary), and goes red the day x86-64 # starts working, which is the prompt to widen the aarch64 matrix above (#7321). # Deliberately cheap: one probe, no runtime, no oracle. + # ELF coverage. The two GC arms above run on macos-14, so without this job + # nothing exercises the object format the whole compact-map design had to be + # rewritten for. Every bug that reached main in that area was ELF-only and + # invisible on Mach-O: + # * the section needed SHF_GNU_RETAIN or `--gc-sections` dropped it, since + # nothing references it (Mach-O has `.no_dead_strip`); + # * it needed SHF_WRITE too, or the relocated function addresses forced a + # DT_TEXTREL in a PIE; + # * `eh_walker`'s asm defined `_perry_eh_capture_context` with the Mach-O + # underscore convention, so aarch64-Linux could not even link. + # + # Runs on ARM64 because the backend is aarch64-only (#7324); on x86-64 it + # would exercise only the refusal, which `statepoints-refuse-x86` covers. + # Stock Ubuntu clang registers `statepoint-example` (verified on 18.1.3), so + # unlike macOS this needs no toolchain install. + native-roots-elf-aarch64: + runs-on: ubuntu-24.04-arm + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: gc-native-roots-elf + - name: Build compiler and static runtime (perry-dev profile) + run: | + export RUSTFLAGS="-Cforce-frame-pointers=yes" + cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static + - name: Assert the toolchain registers the GC strategy + run: | + set -euo pipefail + printf 'define void @f() gc "statepoint-example" {\n ret void\n}\n' > /tmp/gcstrategy.ll + clang -c /tmp/gcstrategy.ll -o /dev/null \ + || { echo "::error::this clang does not support gc \"statepoint-example\" — pin one that does"; exit 1; } + - name: Probe matrix, statepoint mode, forced evacuation + if: ${{ !cancelled() }} + run: | + set -euo pipefail + export PERRY_RUNTIME_DIR="$PWD/target/perry-dev" + export PERRY_NO_AUTO_OPTIMIZE=1 + pass=0 + total=0 + errs="" + for probe in benchmarks/gc_ratchet/probes/*.ts; do + # Same exclusion as the macOS bridge arm: the explicit bridge + # refuses a try-carrying probe rather than emit a frame with no + # roots (#7330/#7335). RS4GC covers 09. + [ "$(basename "$probe")" = "09_try_catch_roots.ts" ] && continue + total=$((total+1)) + name=$(basename "$probe" .ts) + node --expose-gc --experimental-strip-types "$probe" > "/tmp/$name.oracle" + PERRY_STATEPOINTS=1 ./target/perry-dev/perry "$probe" -o "/tmp/$name" + # Liveness: the compact section must be present AND LLVM's gone, + # so a run where the rewrite silently stopped cannot read as green. + readelf -S "/tmp/$name" | grep -q "\.perry_gcmap" \ + || { echo "::error::$name has no .perry_gcmap — the statepoint arm was not live"; exit 1; } + readelf -S "/tmp/$name" | grep -q "\.llvm_stackmaps" \ + && { echo "::error::$name still carries .llvm_stackmaps — the compact rewrite did not run"; exit 1; } + PERRY_STATEPOINTS=1 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 \ + "/tmp/$name" > "/tmp/$name.out" 2> "/tmp/$name.err" + diff "/tmp/$name.oracle" "/tmp/$name.out" \ + || { echo "::error::$name output diverged from the pinned oracle"; exit 1; } + errs="$errs /tmp/$name.err" + pass=$((pass+1)) + done + echo "ELF statepoint matrix: $pass/$total" + [ "$total" -gt 0 ] || { echo "::error::no probes matched — the matrix ran on nothing"; exit 1; } + [ "$pass" -eq "$total" ] + grep -l "#gcmetric" $errs >/dev/null \ + || { echo "::error::no probe emitted gc metrics — the collector never ran"; exit 1; } + statepoints-refuse-x86: runs-on: ubuntu-latest timeout-minutes: 45 @@ -512,7 +615,13 @@ jobs: # branch protection to require, so adding an arm never needs a protection edit # and a red arm cannot hide behind a green sibling. gc-native-roots-complete: - needs: [native-roots-aarch64, native-roots-rs4gc-aarch64, statepoints-refuse-x86] + needs: + [ + native-roots-aarch64, + native-roots-rs4gc-aarch64, + native-roots-elf-aarch64, + statepoints-refuse-x86, + ] if: always() runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/changelog.d/7341-gate-toolchain-and-elf-arm.md b/changelog.d/7341-gate-toolchain-and-elf-arm.md new file mode 100644 index 0000000000..3721fb721a --- /dev/null +++ b/changelog.d/7341-gate-toolchain-and-elf-arm.md @@ -0,0 +1,26 @@ +### `gc-native-roots`: make the statepoint arm able to pass, and restore ELF coverage + +The gate has been red on `main` since it landed — five consecutive runs — and +neither cause was a code regression. + +**It could not pass.** `native-roots-aarch64` runs on `macos-14`, whose image +ships **Apple clang 15**, which does not register the `statepoint-example` GC +strategy. Every run died with `fatal error: error in backend: unsupported GC: +statepoint-example` before a single probe executed. That is CLAUDE.md's +failure mode inverted: a gate that cannot *pass* is as useless as one that +cannot fail, and it looked like a real regression for a day. The job now pins +Homebrew LLVM, as the RS4GC job beside it already did, and asserts the pinned +toolchain registers the strategy so a future image change fails with a clear +message rather than inside a probe compile. Newer Apple clang does register it +(21 does), so this is a property of the runner image, not of Apple clang. + +**It stopped testing ELF.** Both GC arms run on macOS, so nothing exercised the +object format the compact map had to be reworked for. Every bug that reached +`main` in that area was ELF-only and invisible on Mach-O: the section needed +`SHF_GNU_RETAIN` or `--gc-sections` discarded it; it needed `SHF_WRITE` too, or +the relocated addresses forced a `DT_TEXTREL` in a PIE; and `eh_walker`'s asm +used the Mach-O underscore convention, so aarch64-Linux could not link at all. +A `native-roots-elf-aarch64` arm runs the same matrix on `ubuntu-24.04-arm` +with both liveness asserts (`.perry_gcmap` present **and** `.llvm_stackmaps` +absent). It needs no toolchain install — stock Ubuntu clang registers the +strategy, verified on 18.1.3.