-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(gc): the aarch64 fast stack-map walker misread every SVE-shaped prologue (#7984) #7997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5d9aeaf
55a3ed6
91c56b9
967dd6b
969c596
28ec164
47031cb
e2f7a92
b33110a
53e046f
dcee6a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -119,11 +119,24 @@ | |||||||||||||||||||||||||||||||||||||||||||
| # objects copied, and the whole step passes. The collector was | ||||||||||||||||||||||||||||||||||||||||||||
| # never the problem. | ||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||
| # ubuntu-24.04-arm REAL DEFECT, filed as #7984. `PERRY_STACKMAP_WALKER=verify` | ||||||||||||||||||||||||||||||||||||||||||||
| # caught the fast fp-chain walker and the unwinder resolving the | ||||||||||||||||||||||||||||||||||||||||||||
| # same root to addresses 96 bytes apart. This arm is red because | ||||||||||||||||||||||||||||||||||||||||||||
| # it found the bug it was built to find; it must STAY red until | ||||||||||||||||||||||||||||||||||||||||||||
| # #7984 is fixed. Do not skip it. | ||||||||||||||||||||||||||||||||||||||||||||
| # ubuntu-24.04-arm REAL DEFECT, filed as #7984 and FIXED (#7997). | ||||||||||||||||||||||||||||||||||||||||||||
| # `PERRY_STACKMAP_WALKER=verify` caught the fast fp-chain | ||||||||||||||||||||||||||||||||||||||||||||
| # walker and the unwinder resolving the same root 96 bytes | ||||||||||||||||||||||||||||||||||||||||||||
| # apart, and the fast walker — the one that runs when `verify` | ||||||||||||||||||||||||||||||||||||||||||||
| # is off — was the wrong one. Two blind spots in | ||||||||||||||||||||||||||||||||||||||||||||
| # `fp_to_sp_offset`, both only reachable with SVE on, which is | ||||||||||||||||||||||||||||||||||||||||||||
| # what `-mcpu=native` turns on for a Neoverse-class core and | ||||||||||||||||||||||||||||||||||||||||||||
| # nothing on macOS or x86-64 ever does: a callee-save store | ||||||||||||||||||||||||||||||||||||||||||||
| # ended the prologue's stack-adjustment run, and | ||||||||||||||||||||||||||||||||||||||||||||
| # `addvl sp, sp, #-N` was not decoded at all. The 96 was never | ||||||||||||||||||||||||||||||||||||||||||||
| # a constant — it is that frame's missed tail, and it scales | ||||||||||||||||||||||||||||||||||||||||||||
| # with the vector length (208 at VL = 64 B). | ||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||
| # This arm should now be GREEN. If it goes red again, read the | ||||||||||||||||||||||||||||||||||||||||||||
| # report `verify` prints before assuming a regression: it names | ||||||||||||||||||||||||||||||||||||||||||||
| # the frame, the base register, both resolved bases and the | ||||||||||||||||||||||||||||||||||||||||||||
| # prologue words, which is enough to say which walker is wrong | ||||||||||||||||||||||||||||||||||||||||||||
| # without a second run. | ||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||
| # windows-latest REAL DEFECT, filed as #7985 (`perry.exe` cannot link | ||||||||||||||||||||||||||||||||||||||||||||
| # against the official LLVM 22 release: /MT-vs-/MD CRT mismatch, | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -133,11 +146,11 @@ | |||||||||||||||||||||||||||||||||||||||||||
| # `D:\a\_temp` as a remote host — was a workflow bug and is | ||||||||||||||||||||||||||||||||||||||||||||
| # fixed here with `--force-local`. | ||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||
| # So: after #7970 the macOS and ubuntu-latest arms should pass and the other two | ||||||||||||||||||||||||||||||||||||||||||||
| # should remain red on their filed defects. This workflow is therefore NOT a | ||||||||||||||||||||||||||||||||||||||||||||
| # promotion candidate yet — promoting it while #7984/#7985 are open would block | ||||||||||||||||||||||||||||||||||||||||||||
| # every PR. Promote only once all four arms are green, and per CLAUDE.md, run it | ||||||||||||||||||||||||||||||||||||||||||||
| # green once BEFORE adding it to branch protection. | ||||||||||||||||||||||||||||||||||||||||||||
| # So: after #7970 and #7997 three of the four arms should pass and only | ||||||||||||||||||||||||||||||||||||||||||||
| # windows-latest should remain red, on #7985. This workflow is therefore still | ||||||||||||||||||||||||||||||||||||||||||||
| # NOT a promotion candidate — promoting it while #7985 is open would block every | ||||||||||||||||||||||||||||||||||||||||||||
| # PR. Promote only once all four arms are green, and per CLAUDE.md, run it green | ||||||||||||||||||||||||||||||||||||||||||||
| # once BEFORE adding it to branch protection. | ||||||||||||||||||||||||||||||||||||||||||||
| name: gc-native-roots | ||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||
| # Must run where it can actually gate something. Branch-scoped triggers were | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -262,6 +275,42 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # The two aarch64 walkers, over a frame this repository wrote, on the host | ||||||||||||||||||||||||||||||||||||||||||||
| # that has to walk it. | ||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||
| # Until this step, NOTHING `cargo test` runs ever called `fp_chain::visit` | ||||||||||||||||||||||||||||||||||||||||||||
| # or `unwind::visit` — their only exercise anywhere was `verify` mode in | ||||||||||||||||||||||||||||||||||||||||||||
| # the step further down, inside a workflow that has never had a successful | ||||||||||||||||||||||||||||||||||||||||||||
| # run on any branch (#7970). The unit tests cover the decoder and the | ||||||||||||||||||||||||||||||||||||||||||||
| # matcher; the step that turns a `(register, offset)` pair into a stack | ||||||||||||||||||||||||||||||||||||||||||||
| # address had none, which is why #7984 could only ever be found by | ||||||||||||||||||||||||||||||||||||||||||||
| # compiling TypeScript and collecting. | ||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||
| # This asks the same question in seconds and without a compiler: given a | ||||||||||||||||||||||||||||||||||||||||||||
| # frame whose layout is known — the aarch64-ELF one, where LLVM puts the | ||||||||||||||||||||||||||||||||||||||||||||
| # `x29,x30` pair below the other callee-saves, and the Mach-O one, where | ||||||||||||||||||||||||||||||||||||||||||||
| # it sits at the top — do both walkers land on the word the record names? | ||||||||||||||||||||||||||||||||||||||||||||
| # It runs BEFORE the probe matrix so a walker defect is reported as a | ||||||||||||||||||||||||||||||||||||||||||||
| # walker defect rather than as an oracle diff twenty minutes later. | ||||||||||||||||||||||||||||||||||||||||||||
| - name: Walker agreement (aarch64 hosts) | ||||||||||||||||||||||||||||||||||||||||||||
| if: ${{ !cancelled() && matrix.arch == 'aarch64' && runner.os != 'Windows' }} | ||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||||||||||||||||
| export RUSTFLAGS="-C force-frame-pointers=yes -C force-unwind-tables=yes" | ||||||||||||||||||||||||||||||||||||||||||||
| cargo test --profile perry-dev -p perry-runtime --lib gc::roots::stack_maps \ | ||||||||||||||||||||||||||||||||||||||||||||
| -- --test-threads=1 | tee /tmp/walker-agreement.log | ||||||||||||||||||||||||||||||||||||||||||||
| # `--lib <filter>` is a substring match, so a rename makes it select | ||||||||||||||||||||||||||||||||||||||||||||
| # nothing and `cargo test` still exits 0 having run zero tests — a | ||||||||||||||||||||||||||||||||||||||||||||
| # gate that cannot fail. Require each test by name, including the | ||||||||||||||||||||||||||||||||||||||||||||
| # sabotage arm that proves the sentinel check discriminates. | ||||||||||||||||||||||||||||||||||||||||||||
| for name in both_walkers_resolve_an_sp_root_in_an_elf_shaped_frame \ | ||||||||||||||||||||||||||||||||||||||||||||
| both_walkers_resolve_an_sp_root_in_a_darwin_shaped_frame \ | ||||||||||||||||||||||||||||||||||||||||||||
| a_wrong_frame_offset_is_caught; do | ||||||||||||||||||||||||||||||||||||||||||||
| grep -q "walker_agreement::$name ... ok" /tmp/walker-agreement.log \ | ||||||||||||||||||||||||||||||||||||||||||||
| || { echo "::error::$name did not run — the filter matched nothing"; exit 1; } | ||||||||||||||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+303
to
+312
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Require the fallback test by name as well. The loop exists so that a rename cannot leave the gate green with zero tests selected. 🛠️ Proposed fix for name in both_walkers_resolve_an_sp_root_in_an_elf_shaped_frame \
both_walkers_resolve_an_sp_root_in_a_darwin_shaped_frame \
- a_wrong_frame_offset_is_caught; do
+ a_wrong_frame_offset_is_caught \
+ an_undecodable_prologue_declines_the_fast_walk_and_the_unwinder_still_answers; do📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| - name: Probe matrix, RS4GC mode, forced evacuation | ||||||||||||||||||||||||||||||||||||||||||||
| if: ${{ !cancelled() }} | ||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -515,7 +564,7 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||
| "/tmp/rs4gc-$name" > "/tmp/walker-$name-$mode.out" \ | ||||||||||||||||||||||||||||||||||||||||||||
| 2> "/tmp/walker-$name-$mode.err" \ | ||||||||||||||||||||||||||||||||||||||||||||
| || { echo "::error::$name crashed under PERRY_STACKMAP_WALKER=$mode"; \ | ||||||||||||||||||||||||||||||||||||||||||||
| tail -20 "/tmp/walker-$name-$mode.err"; exit 1; } | ||||||||||||||||||||||||||||||||||||||||||||
| tail -120 "/tmp/walker-$name-$mode.err"; exit 1; } | ||||||||||||||||||||||||||||||||||||||||||||
| diff "/tmp/rs4gc-$name.oracle" "/tmp/walker-$name-$mode.out" \ | ||||||||||||||||||||||||||||||||||||||||||||
| || { echo "::error::$name diverged from the pinned oracle under PERRY_STACKMAP_WALKER=$mode"; exit 1; } | ||||||||||||||||||||||||||||||||||||||||||||
| checked=$((checked+1)) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| ### aarch64: the fast GC stack-map walker misread every SVE-shaped prologue (#7984) | ||
|
|
||
| `PERRY_STACKMAP_WALKER=verify` caught the frame-pointer-chain walker and the | ||
| Itanium unwinder resolving the same GC root 96 bytes apart on | ||
| `ubuntu-24.04-arm`. The fp-chain walker is the wrong one — it is also the one | ||
| that runs when `verify` is off — and it was blind twice in the same prologue. | ||
|
|
||
| Reproduced end to end on aarch64 Linux. The trigger is the tuning, not the | ||
| distro: Perry builds a host binary with `-mcpu=native`, and on a Neoverse-class | ||
| core that turns SVE on, which changes the shape LLVM emits. The same probe built | ||
| `-mcpu=neoverse-n1` passes; no Apple arm can see it at all. Measured on `main` | ||
| — the module body, 100 stack-map records — built `-mcpu=neoverse-n2`: | ||
|
|
||
| ``` | ||
| 12479c: add x29, sp, #0x20 <- fp established; the decoder read 32 | ||
| 1247a0: stp x28, x27, [sp, #48] <- not a `sub sp`, so the run ended HERE | ||
| ... four more callee-save pairs | ||
| 1247b4: sub sp, sp, #0x50 <- 80 bytes, dropped | ||
| 1247b8: addvl sp, sp, #-2 <- and two vector lengths more, dropped | ||
| ``` | ||
|
|
||
| 1. **A callee-save store ended the accumulation run.** It does not move sp, so | ||
| it says nothing about whether the prologue's stack adjustments are finished, | ||
| but the rule was "the first instruction that is not a `sub sp` ends the run". | ||
| Stores through sp with no writeback are now transparent to it, enumerated by | ||
| opcode so an unrecognised instruction still ends the run — the safe | ||
| direction. | ||
| 2. **`addvl`/`addpl` writing sp was not decoded at all.** Its multiplier is in | ||
| the instruction; its unit is the runtime SVE vector length, which is not. | ||
| That is now read once via `prctl(PR_SVE_GET_VL)` — a syscall rather than | ||
| `rdvl`, which faults on a core without SVE — and cached. Where it cannot be | ||
| read the whole decode fails and the frame goes to the platform unwinder, | ||
| which reads DWARF CFI and needs no VG for an fp-based frame. | ||
|
|
||
| **96 was never a constant.** It is that frame's missed tail and it scales with | ||
| the vector length: 96 on the runner (`0x50 + 1 x 16`), 208 under `qemu -cpu max` | ||
| at VL = 64 (`0x50 + 2 x 64`). A fix keyed on 96 would have been wrong on every | ||
| other vector length. | ||
|
|
||
| Validated at two vector lengths so the scaling is pinned rather than one | ||
| machine's answer: 28 `verify` runs (14 probes x VL 16 B and 64 B) under | ||
| `qemu-aarch64 -cpu max`, all byte-exact against the pinned Node oracle. The fast | ||
| walker is live there — `11_collect_at_depth` reports `fp_walks 12`, | ||
| `fallback_walks 0`, `records_matched 1338`, `locations_visited 2678` with | ||
| `verify` green, so 2678 slots were cross-checked against the unwinder and | ||
| agreed. | ||
|
|
||
| ### `PERRY_STACKMAP_WALKER=verify` now names the disagreement it finds | ||
|
|
||
| All the gate could report was `fast walk visited 1 unique slots, unwinder | ||
| visited 1` and two addresses in decimal — not the frame, not the base register, | ||
| not the function whose prologue was decoded, and therefore not which walker was | ||
| wrong. Every candidate explanation predicts exactly that output. | ||
|
|
||
| Both walkers now hand back a `ResolvedRoot` rather than a bare | ||
| `MutableRootSlot`: the address, plus the frame return address it was matched on, | ||
| the record's function, the map's base register and frame offset, and the base | ||
| that walker resolved that register to. `visit_stack_map_root_slots` projects it | ||
| straight back, so the collector's view is unchanged. On a mismatch `verify` | ||
| prints every root from both walks, states that an equal slot count means a | ||
| *base* disagreement rather than a missed frame (with the per-slot delta), prints | ||
| each walk's frame and record counts so an early-terminating walk is | ||
| distinguishable, and on aarch64 dumps `fp_to_sp_offset`'s decode together with | ||
| the prologue words it read. That report is what identified #7984 in one run. | ||
|
|
||
| The prologue dump is gated on the parsed map vouching for the function address. | ||
| The first draft was not, and a unit test with a synthetic address turned the | ||
| diagnostic into a SIGSEGV with no output — which is what would happen in the | ||
| field for the one failure mode where a report matters most, a map whose | ||
| addresses are wrong. | ||
|
|
||
| ### The two aarch64 walkers now have unit coverage at all | ||
|
|
||
| Nothing `cargo test` runs had ever called `fp_chain::visit` or `unwind::visit`; | ||
| the decoder and the matcher were covered, the step that turns a | ||
| `(register, offset)` pair into a stack address was not. | ||
| `stack_maps_walker_agreement.rs` drives both over `global_asm!` probe frames in | ||
| the two real layouts — the aarch64-ELF one with the frame record in the middle | ||
| of the frame, and the Mach-O one with it at the top — and requires each walker | ||
| to land on a word **holding a sentinel** the probe wrote into the slot its | ||
| record names. Set equality is satisfied by two empty sets and by two identically | ||
| wrong ones; the slot's contents are the discriminating quantity. | ||
| `a_wrong_frame_offset_is_caught` is the sabotage arm, and a third test pins that | ||
| an undecodable prologue declines the fast walk while the unwinder still | ||
| resolves the root — the fallback the SVE fix rests on. | ||
|
|
||
| `gc-native-roots.yml` runs them on both aarch64 arms, before the probe matrix, | ||
| requiring each test by name (`--lib <filter>` is a substring match, so a rename | ||
| would select nothing and cargo would still exit 0). Its crash path tailed 20 | ||
| lines of the failing run's stderr, which truncates the report; now 120. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 154
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 3708
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 10804
Do not mark
ubuntu-24.04-armasGREENyet.The current commit's workflow run is still pending. Recent completed runs on the real arm runner ended in failure or cancellation. Record the QEMU evidence separately and update the ledger only after a real-arm run passes.
🤖 Prompt for AI Agents