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
10 changes: 10 additions & 0 deletions .github/workflows/gc-native-roots.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# #7173: native-frame GC root verification.
#
# Runs on an ARM64 runner deliberately. The backend is aarch64-only: on x86-64
# every root is `Indirect [RSP + off]` (DWARF register 7), which the runtime
# cannot resolve — `_Unwind_GetGR` does not reliably return the stack pointer —
# so the collector segfaults. The compiler now refuses that combination
# outright, which would make an x86-64 run of this gate test nothing but the
# refusal. ARM64 exercises the configuration that is actually supported, and
# still answers the question this gate exists for: whether the compact map
# survives ELF linking.
#
#
# Runs the gc-ratchet probe matrix in every native-root mode under forced
# evacuation + evacuation verification, byte-diffed against the pinned Node
# oracle. Each arm carries a liveness assert, because CLAUDE.md's fourth way a
Expand Down
24 changes: 24 additions & 0 deletions changelog.d/7321-statepoints-aarch64-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
### Native GC roots: refuse non-aarch64, and stop the ELF map forcing DT_TEXTREL

The `gc-native-roots` gate went red on `main` with a **SIGSEGV**, not a missing
section — `SHF_GNU_RETAIN` did keep `.perry_gcmap` through `--gc-sections`, and
`.llvm_stackmaps` was gone as intended. Two separate defects behind the crash.

**The backend is aarch64-only and did not say so.** Measured by cross-compiling
a probe to `x86_64-unknown-linux-gnu` and decoding the emitted stack map: every
one of its 178 root slots is `Indirect [RSP + off]`, DWARF register 7. The
runtime's `chain_walkable` admits only aarch64's FP/SP (29 and 31), so every
frame falls back to the unwinder, which resolves the base with
`_Unwind_GetGR(ctx, 7)` — and that does not reliably return the stack pointer
(`_Unwind_GetCFA` is the supported way). The walker therefore computed wild
addresses and the collector segfaulted writing through them. The mode is
opt-in, so the compiler now refuses the combination outright rather than
emitting a binary that crashes under collection.

**The ELF section was read-only but holds relocated addresses.** `ld` reported
`relocation against 'main' in read-only section '.perry_gcmap'` and created a
DT_TEXTREL in a PIE. It is now `"awR"` (SHF_ALLOC | SHF_WRITE | SHF_GNU_RETAIN).

The gate moves to an ARM64 Linux runner. On x86-64 it would now exercise only
the refusal; on ARM64 it tests the supported configuration and still answers
the question it exists for — whether the compact map survives ELF linking.
25 changes: 24 additions & 1 deletion crates/perry-codegen/src/gc_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,18 @@ const GC_MAP_VERSION: u8 = 3;
/// Section the compact map is emitted into, and the label it is given.
const GC_MAP_LABEL: &str = "_perry_gc_map";
const MACHO_SECTION: &str = "__PERRY_GCMAP,__perry_gcmap";
/// `w` because the section holds **relocated function addresses**: without
/// SHF_WRITE the linker reports `relocation against \`main\` in read-only
/// section \`.perry_gcmap\`` and creates a DT_TEXTREL in a PIE, which is both
/// a hardening regression and a portability hazard.
///
/// `R` is SHF_GNU_RETAIN, the ELF analogue of Mach-O's `.no_dead_strip`.
/// Perry links with `-Wl,--gc-sections`, and nothing in the program
/// references this section — the collector finds it by name at runtime — so
/// without RETAIN the linker discards it and the binary ships with no GC map
/// at all. Measured: the section is present in the object (PROGBITS, SHF_ALLOC,
/// with relocations) and absent from the linked binary.
const ELF_SECTION: &str = ".perry_gcmap,\"aR\",@progbits";
const ELF_SECTION: &str = ".perry_gcmap,\"awR\",@progbits";

/// LLVM stack-map v3 location kinds. Only these two describe a frame slot;
/// `Constant`/`ConstIndex` carry the statepoint preamble and `Register` cannot
Expand Down Expand Up @@ -834,6 +839,24 @@ pub fn compact_and_assemble(
// the collector finds no native roots at all — the exact outcome the hard
// error below exists to prevent, reached with no diagnostic. The mode is
// opt-in, so refusing loudly costs nothing.
// The runtime can only resolve aarch64 frame bases. Measured on x86-64:
// every root is `Indirect [RSP + off]` (DWARF register 7), so
// `chain_walkable` is false — it admits only aarch64's FP/SP, 29 and 31 —
// and every frame falls back to `_Unwind_GetGR(ctx, 7)`. That call does not
// reliably return the stack pointer (`_Unwind_GetCFA` is the supported way
// to obtain it), so the walker computes wild addresses and the collector
// segfaults writing through them. Observed exactly that on the Linux gate.
//
// The mode is opt-in, so refusing here is free; emitting a binary that
// crashes under collection is not.
if !target.starts_with("aarch64") && !target.starts_with("arm64") {
return Err(anyhow!(
"perry: native GC roots (PERRY_STATEPOINTS / PERRY_RS4GC) are \
aarch64-only — target `{target}` records roots against frame \
bases this runtime cannot resolve, and the collector would \
segfault rather than report anything. Tracked for #7173."
));
}
let macho = target.contains("apple") || target.contains("darwin");
let elf = !macho && !target.contains("windows") && !target.contains("msvc");
if !macho && !elf {
Expand Down
Loading