fix(codegen): keep the personality slot's .hidden/.weak through the compact GC-map rewrite (Linux throw/catch GPF across modules) - #8948
Conversation
…ompact GC-map rewrite `compact_stack_map_asm` treats every line from the `.llvm_stackmaps` section switch to the next section switch as the stack map, and replaces it wholesale. LLVM's `AsmPrinter` finalization prints the ELF personality slot's attributes — `.hidden DW.ref.perry_eh_personality` and `.weak DW.ref.perry_eh_personality` — right after the stack map and BEFORE switching to the slot's `.data.DW.ref.perry_eh_personality,"awG",…,comdat` section, so the rewrite swallowed both lines and the assembler defined the COMDAT slot as a LOCAL symbol. Every multi-object link then breaks the unwind tables: `ld -r` (split codegen units) and the final exe/dylib link keep one COMDAT group and resolve nothing for the other objects' CIE personality relocations — `.eh_frame` is exempt from the discarded-section complaint, so the drop is silent. The first caught `throw` whose unwind crosses a frame from any other module or unit calls a garbage personality pointer, and the process dies with a GPF at `_Unwind_RaiseException`'s `call *%rax`. A two-module program with a `try` in each module is enough; Coop's Next.js dylib hit it on Linux during module init. Mach-O is unaffected (no DW.ref/COMDAT slot). Zero-width lines inside the block that do not name `__LLVM_StackMaps` are now carried through the rewrite verbatim, in their original position: symbol attributes LLVM printed ahead of a section switch, and the -O3 absolute-symbol assignments that were parsed as zero bytes and then lost. Claude-Session: https://claude.ai/code/session_01UZJbhb2FTuakurTHPAKQgd
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThe GC-map rewrite now preserves foreign zero-width assembly lines inside stack-map blocks. It keeps ELF personality-slot attributes and absolute-symbol assignments, drops attributes for the map label, and adds tests for these cases. The changelog records the Linux crash and verification results. ChangesGC-map assembly preservation
Merge Risk: 🟡 Moderate · up to The compiler now preserves assembly directives across a size-changing metadata rewrite. The targeted personality-symbol case is covered, but other accepted directives could change relative or unwind metadata and affect exception handling or garbage collection in linked binaries; this PR is not merge-ready until that bounded correctness risk is addressed or explicitly accepted. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 1 files. (1 skipped: 1 unsupported.) Full details: Description checkExplanation The description gives detailed information about the symptom, mechanism, fix, verification, and known limitations. It omits the template's explicit headings and checklist, but it provides equivalent content for most required sections.
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Merged. One fix pushed: this takes Validated batched with #8949/#8950/#8951: codegen 1337/0, runtime 2773/0, evac failing set 16 (the pre-existing one), |
…cap (#8957) #8948 took gc_map.rs to 2047 lines and `check_file_size` has been red on main since it merged. I had prepared this split during that PR's review but pushed it to the wrong remote — #8948 is same-repo, and the push went to a fork the PR does not track — so the unfixed head is what merged. Moved the cfg(test) module out verbatim via the #[path] pattern the tree already uses (1332 + 712 lines). Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Symptom
On x86-64 Linux, Coop's Next.js App Route dylib (a 75 MB
--output-type dylibimage, split codegen units merged withld -r) dies during module init on the first caughtthrow:The same fixture at the same commit initialises and serves on macOS. It is not dylib- or Coop-specific: a two-module executable with one
tryin each module segfaults the same way (see reproducer).Mechanism (gdb evidence)
The faulting instruction is the personality call in phase 1 of the unwinder, with a garbage function pointer:
cur_context(x/28gx $rbx) shows the frame being processed is frame #4 (ra=0x7fffa6c89e18,func=perry_closure_…require_hook_js,lsdain.gcc_except_table), so the FDE/LSDA lookup worked and the CIE's personality pointer decoded to junk.readelf --debug-dump=frames app.soconfirms it — 60 CIEs, one per merged object, and only the first carries a realDW_EH_PE_indirect|pcrel|sdata4pointer toDW.ref.perry_eh_personality:Walking that back through the objects: every Perry-emitted
.o(object cache) defines the slot aswhereas
clang-22 -Son the same.lldefines itOBJECT WEAK HIDDEN. A LOCAL symbol inside a COMDAT group is fatal at any multi-object link: GNU ld keeps one group per program (also in theld -rmerge of split codegen units) and only redirects references to global symbols of a discarded group to the kept copy; references to a local one are dropped — silently, because.eh_frameis exempt from the "defined in discarded section" complaint (_bfd_elf_default_action_discarded). The dropped relocation leaves the pcrel field with whatever bytes were there, which the unwinder then decodes as the personality address.The reason the symbol is LOCAL is Perry's own assembly rewrite in
crates/perry-codegen/src/gc_map.rs(compact_stack_map_asm, the statepoint compact GC-map pass, on by default with RS4GC). It treats every line from the.llvm_stackmapssection switch to the next section switch as the stack map and replaces the range. LLVM'sAsmPrinterfinalization prints the personality slot's attributes before switching to the slot's section:Captured from Perry's in-process pipeline (
.o.sscratch file, kept with a smallunlinkshim): the emitted text has the.section/.type/.size/label lines and no.hidden/.weak. Without them the assembler binds the group signature symbol locally. Mach-O has noDW.refslot and no COMDAT groups (the personality goes through a GOT entry), so macOS never sees it; single-file programs never link two groups, so the Linux gap suite never sees it either.Minimal linker-level demonstration of the same effect, no Perry involved:
Minimal reproducer (Perry)
perry compile main.ts -o main_exe && ./main_exeon Ubuntu 24.04 x86-64 (LLVM 22.1.8, binutils 2.42):Segmentation fault (core dumped), gdb:call *%raxwithrax = 0x52ef040000006fff;readelf --debug-dump=framesshows a9b 18 00 00 00CIE.init ok 6, exit 0; the two module CIEs merge into one with a valid pointer; the cached objects carryOBJECT WEAK HIDDEN DW.ref.perry_eh_personality.PERRY_CODEGEN_UNITS=2 perry compile main.ts --output-type dylib(forces theld -rsplit-unit merge) likewise goes from a junk CIE to a single validzPLRCIE.Fix
parse_blocknow records every zero-width line inside the block that does not name__LLVM_StackMaps(symbol attributes printed ahead of a section switch, and the-O3absolute-symbol assignmentsperry_null_guard_zero = …that were parsed as zero bytes and then lost the same way), andcompact_stack_map_asmre-emits them verbatim after the replacement map, before the section switch that ended the block — i.e. exactly where LLVM had them. Lines about the map's own label stay dropped (the replacement declares its own). No change to the map encoding, the personality routine, or the link lines.Verification
cargo test --release -p perry-codegen --lib gc_map(server, ec5a0ac): 23 passed, including the three new tests (elf_personality_slot_attributes_survive_the_rewrite,the_map_labels_own_attributes_are_not_carried,symbol_assignments_inside_the_block_are_re_emitted).cargo test --release -p perry-codegen --lib(server): 1337 passed, 0 failed, 1 ignored.cargo fmt --check -p perry-codegen: clean.cargo clippy -p perry-codegen --all-targets(macOS, LLVM 22): exit 0, no new warnings (thegc_map.rswarnings it prints are pre-existingmanual_is_multiple_ofhits on untouched lines).readelfon the newapp.so: exactly onezPLRCIE with a valid personality. Single-app harness (COOP_BENCH_APP_COUNTS=1 … measure_in_process_startup_and_rss):warm bench-000: 200 OK {"runtime":"next","iterations":100,"checksum":3726872593}on all three trials. Three-app harness (COOP_BENCH_APP_COUNTS=3 COOP_BENCH_PRELOAD_CONCURRENCY=1):bench-000/001/002: 200 OKon all three trials (RESULT apps=3 restart_startup_median_ms=1655 ready_rss_median_mib=489.7).Not covered
trywould close that on the Linux arms.perry-main.lock; it needs a bump to a commit containing this fix (the server's uncommitted lock points at ec5a0ac for the verification above).https://claude.ai/code/session_01UZJbhb2FTuakurTHPAKQgd