Repository navigation
aarch64-linux: the fast stack-map walker and the unwinder disagree by 96 bytes on the same root (PERRY_STACKMAP_WALKER=verify) #7984
Description
Activity
- added 7 commits that reference this issue
on Aug 12, 2026 Answered, and fixed in #7997
The fp-chain walker is the wrong one — the bad case, since it is what runs when
verifyis off.Reproduced end to end on aarch64 Linux (Ubuntu 24.04 arm64 container, LLVM 22.1.8 from apt.llvm.org, this issue's own RUSTFLAGS). The trigger is the tuning, not the distro. Perry builds a host binary with
-mcpu=native; on a Neoverse-class core that enables SVE, and SVE changes the prologue LLVM emits:PERRY_TARGET_CPU=neoverse-n2 -> diverges PERRY_TARGET_CPU=neoverse-n1 -> passesThe container's own
-mcpu=native(an Apple core, no SVE) passes all 14 probes under all three walkers, which is why this was never visible off that runner — and no macOS arm could ever have seen it.The frame
main, the module body, 100 stack-map records, built-mcpu=neoverse-n2, read withobjdump -d:12479c: 910083fd add x29, sp, #0x20 <- fp established; decoder reads 32 1247a0: a9036ffc stp x28, x27, [sp, #48] <- NOT a `sub sp`: the run ENDED here ... four more callee-save pairs 1247b4: d10143ff sub sp, sp, #0x50 <- 80 bytes, DROPPED 1247b8: 043f57df addvl sp, sp, #-2 <- 2 x VL more, DROPPEDTwo independent blind spots in
fp_to_sp_offset, in one prologue:- A callee-save store ended the accumulation run. It does not move sp, so it says nothing about whether the stack adjustments are finished — but the rule was "the first instruction that is not a
sub spends the run", and LLVM interleaves those stores with the frame-pointer setup whenever SVE is on. addvl sp, sp, #-Nwas not decoded at all. Its multiplier is in the instruction; its unit is the runtime vector length, which is not.
Q1, which walker is right — established arithmetically, not by presumption
Under
qemu -cpu max(VL = 64 B), from the report #7997 adds toverify:fp-chain minus unwinder = [208, 208] byte(s) slot 0x400000800af0 = base 0x400000800ab0 +64 | ip … (fn 0xaaaaaabc4790 + 0x22c) fp_to_sp_offset(fn) = Some(32), prologue words: fc180fea 6d0123e9 a9027bfd 910083fd a9036ffc a90467fa a9055ff8 a90657f6 a9074ff4 d10143ffSame function, same ip, same record, same offsets — a base disagreement:
caller_fp = fast base + decoded = 0x…ab0 + 32 = 0x…ad0 true x29-body_sp = 32 + 0x50 + 2*64 = 240 true body_sp = 0x…ad0 - 240 = 0x…9e0 <- the unwinder's baseThe unwinder's base is the frame's real body SP, derived independently from the prologue the fast walker misread.
Q2, is 96 constant — no
It is that frame's missed tail and it scales with the vector length:
host delta = ubuntu-24.04-arm(VL = 16 B)96 0x50 + 1 x 16qemu -cpu max(VL = 64 B)208 0x50 + 2 x 64A fix keyed on 96 would have been wrong on every other vector length. (Not the
.word-vs-.longhazard the issue suspected: the compact map decodes fine, and a parse error would move both walkers identically and produce no divergence at all.)Q3, does the fast walker run in production on this target — yes
rs4gc_enabled()defaults toNATIVE_ROOTS_TARGET_OK, which is true on aarch64 Linux, andWalkerMode::Fastis the default. So this was a live correctness bug on aarch64 Linux with SVE, not only a CI failure: the collector marked and rewrote a stack word 96 bytes away from the root.The fix
Stores through sp with no writeback are transparent to the accumulation run (enumerated by opcode, so an unrecognised instruction still ends it).
addvl/addplwriting sp is decoded — multiplier fromimm6, unit fromprctl(PR_SVE_GET_VL), cached; where the length cannot be read the whole decode fails and the frame goes to the platform unwinder.Failing closed only was the first cut and was not enough: on an SVE host that frame is the module body, so the fast walker would decline at every collection and
verifyturns a decline into a panic — this arm would have stayed red on its own fix.Validated at two vector lengths: 28
verifyruns (14 probes x VL 16 B and 64 B), all byte-exact, withfp_walks 12 / fallback_walks 0 / locations_visited 2678proving the fast walker is the one doing the work.- A callee-save store ended the accumulation run. It does not move sp, so it says nothing about whether the stack adjustments are finished — but the rule was "the first instruction that is not a
- added a commit that references this issue
on Aug 12, 2026
Summary
On aarch64 Linux (ELF),
PERRY_STACKMAP_WALKER=verifycatches the fastframe-pointer-chain walker and the unwinder resolving the same root to two
different addresses. The gate is working; the code is wrong.
This is the
ubuntu-24.04-armarm ofgc-native-roots(#7970). It is a rootmisidentification on a supported target, not a harness problem — unlike the
macos-14arm of the same run, which was a missingPERRY_GC_DIAG=1and isfixed separately.
Evidence
Run
31405431962, jobnative-roots-rs4gc (ubuntu-24.04-arm, aarch64, ELF),sha
bf8af0e0f, probe01_nursery_churn:Both walkers found one slot, so this is not a missed or invented frame. They
disagree on where that slot is:
verify_visit(gc/roots/stack_maps.rs) runsfp_chain::visitnon-mutating,then
unwind::visitfor the real visitation, and asserts the address sets areequal. A 96-byte constant offset between them is the signature of a
frame-base/CFA disagreement, not of a parse failure.
Why this matters more than a red CI arm
fp_chainis the fast walker — the one that runs whenverifyis off. If itresolves a root 96 bytes away from the real slot, then on this target the
collector marks and rewrites the wrong stack word. That is precisely CLAUDE.md's
"root-store dominance" failure class: nothing is visibly wrong at collection
time, and it surfaces cycles later as
TypeError: value is not a function.verifymode exists to catch exactly this, and it did — the first time anyoneran it end-to-end on aarch64 ELF. The workflow's own header predicted the shape:
Reproduction
x86-64 ELF (
ubuntu-latest) passes the same step, so this is aarch64-specific.Open questions for whoever picks this up
the presumption is
fp_chainis 96 bytes off — but that should be confirmedagainst the emitted stack map rather than assumed.
whole probe matrix under
verifyon this target and checking whether thedelta is always 96 (a fixed frame-layout term) or varies (an offset-scaling
bug, e.g. the
.word-vs-.longfield-width hazardword_width_forwarnsabout, which is aarch64-ELF-specific).
live correctness bug on aarch64 Linux, not only a CI failure.
Found while repairing #7970. Sibling: the macOS arm of the same run was a gate
defect (missing
PERRY_GC_DIAG=1), and the Windows arm is a separate linkfailure.