Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/gc-native-roots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,60 @@ 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.
# TEMPORARY (#7321 investigation): dump the stack-map block the x86-64
# rewriter refuses, so the cause is read off a log instead of guessed. Removed
# once the fix lands.
x86-diagnose:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: gc-native-roots-x86
- name: Build compiler and static runtime (perry-dev profile)
run: |
export RUSTFLAGS="-C force-frame-pointers=yes -C force-unwind-tables=yes"
cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static
- name: Toolchain
run: |
uname -m
lscpu | head -20 || true
which clang || true
clang --version || true
ls /usr/lib/ | grep -i llvm || true
- name: Compile and dump the block
run: |
set -uo pipefail
export PERRY_RUNTIME_DIR="$PWD/target/perry-dev"
export PERRY_NO_AUTO_OPTIMIZE=1
for probe in benchmarks/gc_ratchet/probes/*.ts; do
name=$(basename "$probe" .ts)
set +e
PERRY_STATEPOINTS=1 ./target/perry-dev/perry "$probe" -o "/tmp/x86-$name" > "/tmp/x86-$name.log" 2>&1
rc=$?
set -e
if [ "$rc" -eq 0 ]; then
echo "== $name: COMPILED"
readelf -S "/tmp/x86-$name" | grep -E "perry_gcmap|llvm_stackmaps" || echo " (no gc map section!)"
continue
fi
echo "== $name: FAILED"
grep -E "reason:|target:|assembly left at:|cannot yet express" "/tmp/x86-$name.log" | head -5
asm=$(grep -o '/tmp/[^ ]*\.o\.s' "/tmp/x86-$name.log" | head -1)
[ -n "$asm" ] && [ -f "$asm" ] || continue
start=$(grep -n 'llvm_stackmaps' "$asm" | head -1 | cut -d: -f1)
[ -n "$start" ] || continue
echo " stackmaps at line $start of $(wc -l < "$asm")"
echo " --- first 40 lines of the block:"
sed -n "${start},$((start+40))p" "$asm"
echo " --- directive census from the block onward:"
tail -n "+${start}" "$asm" | awk '{print $1}' | sort | uniq -c | sort -rn | head -25
echo " --- last 15 lines of the file:"
tail -15 "$asm"
done

Comment on lines +379 to +429

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make x86-diagnose a checked fan-in arm.

gc-native-roots-complete does not depend on x86-diagnose. A diagnostic-job failure can therefore leave the documented required context green.

The loop also has no final assertion that it matched a probe and captured a refused stack-map block. Track both counts and fail when either count is zero. Add x86-diagnose to the fan-in needs list and result checks.

Proposed fix
+          probes=0
+          blocks=0
           for probe in benchmarks/gc_ratchet/probes/*.ts; do
+            [ -f "$probe" ] || continue
+            probes=$((probes+1))
             name=$(basename "$probe" .ts)
             ...
             [ -n "$start" ] || continue
+            blocks=$((blocks+1))
             ...
           done
+          [ "$probes" -gt 0 ]
+          [ "$blocks" -gt 0 ]

-    needs: [native-roots-aarch64, native-roots-rs4gc-aarch64, statepoints-refuse-x86]
+    needs: [native-roots-aarch64, native-roots-rs4gc-aarch64, x86-diagnose, statepoints-refuse-x86]
             "native-roots-rs4gc-aarch64=${{ needs.native-roots-rs4gc-aarch64.result }}" \
+            "x86-diagnose=${{ needs.x86-diagnose.result }}" \
             "statepoints-refuse-x86=${{ needs.statepoints-refuse-x86.result }}"; do

As per coding guidelines, “A CI gate must … assert that the behavior it measures actually executed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/gc-native-roots.yml around lines 379 - 429, Update the
x86-diagnose job’s probe loop to count matched probes and captured refused
stack-map blocks, then fail the job if either count is zero. Add x86-diagnose to
the gc-native-roots-complete fan-in job’s needs list and result checks so its
failure cannot leave the required context green.

Source: Coding guidelines

statepoints-refuse-x86:
runs-on: ubuntu-latest
timeout-minutes: 45
Expand Down
49 changes: 49 additions & 0 deletions changelog.d/7331-elf-stack-map-word-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
The compact-map rewriter could not read the stack maps **ELF** backends emit,
because `.word` is not a fixed size.

LLVM chooses each field's spelling per target through
`MCAsmInfo::Data32bitsDirective`, and the AArch64 **ELF** backend chooses
`.word`. GNU `as` defines `.word` as the target's natural machine word — 4 bytes
on AArch64, ARM, PowerPC, MIPS, SPARC and RISC-V, but **2 on x86**. The rewriter
carried one fixed table (`".long" | ".word" => 4`) written against the
Mach-O/AArch64 spelling it was developed on, so an `aarch64-unknown-linux-gnu`
map — where every 32-bit field is `.word`, every 16-bit field `.hword` and every
64-bit field `.xword` — was read at the wrong widths. The width is load-bearing
for the whole block: two bytes of drift per field relocates every root after it.

`.word` is now resolved against the target, and the other spellings an
`MCAsmInfo` can pick (`.1byte`/`.2byte`/`.4byte`/`.8byte`/`.dc.*`) are handled.

A directive inside the block whose width is not modelled is now a **refusal that
names it**, rather than being skipped. Skipping was the unsound branch: the
block is decoded by structural offset, so one ignored directive that emits bytes
shifts everything after it, and the decode then either fails somewhere unrelated
or succeeds against the wrong bytes. Naming it is what turned an opaque refusal
into a one-line diagnosis.

Every refusal now carries a reason — which directive, which record, which byte
offset, whether the per-function record counts disagreed with the header — and
the target. Previously every parse failure collapsed to `None`, so the message
could only repeat that it had failed, which is why #7321 took an issue to
localise.

The re-encode is now verified against the map it came from, on every target:
`verify_roundtrip` decodes the emitted varint stream exactly as
`perry-runtime`'s `parse_gc_map` does and asserts it reproduces every record's
live set. Unlike `PERRY_STACKMAP_WALKER=verify` this needs no
architecture-specific stack walker, so it holds where that check cannot run, and
it is sabotage-tested (dropped root, relocated root, truncation, trailing bytes)
so a pass means the detector works rather than that nothing was tried.

Also fixes an aarch64-ELF link failure the above uncovered: `eh_walker`'s
`global_asm!` defined `perry_eh_capture_context` / `perry_eh_install_context`
with Mach-O's leading underscore unconditionally under
`target_arch = "aarch64"`, so on aarch64 ELF the definitions and the
`extern "C"` declarations were different symbols and `perry-runtime` could not
link at all.

This does **not** yet close #7321. The defect is an ELF defect but specifically
an AArch64-ELF one; x86 ELF spells these fields `.byte`/`.short`/`.long`/`.quad`,
which the old table already handled, and the x86-64 refusal could not be
reproduced under Apple clang 21, Homebrew clang 19/20/22 or Ubuntu clang 18,
across twelve `-march` settings, from either host, over all nine probes.
Loading
Loading