diff --git a/changelog.d/8121-rs4gc-inline-asm-statepoint.md b/changelog.d/8121-rs4gc-inline-asm-statepoint.md new file mode 100644 index 0000000000..9946882fed --- /dev/null +++ b/changelog.d/8121-rs4gc-inline-asm-statepoint.md @@ -0,0 +1,9 @@ +`perry compile` no longer SIGBUSes on production Next.js bundles that contain a preserved loop. + +Perry emits `call void asm sideeffect "", ""()` as the issue-#74 loop-preservation barrier. `rewrite-statepoints-for-gc` rewrites every non-leaf call in a `gc "statepoint"` function into a `gc.statepoint`, and for inline asm that means using the `InlineAsm` itself as the statepoint's callee operand — invalid IR, rejected by the verifier with `Cannot take the address of an inline asm!`. + +`optimize_and_emit` verified the module *before* the rewrite but not after, so the broken module went straight to SelectionDAG, which dereferenced the bogus callee and killed the compiler with `SIGBUS` inside `AArch64TargetLowering::LowerCall`. Compiling next@16.3.0's bundled `jsonwebtoken` died this way 100 modules into a 104-module production App Route, blocking #8040. + +The barrier now carries `"gc-leaf-function"` on both the native and text emission paths, which is the documented way to tell RS4GC a call cannot trigger a collection — true by construction for a barrier that emits no instructions. `optimize_and_emit` additionally verifies *after* RS4GC, so this class of bug is a clean compile error naming the pass instead of a crash that takes the compiler process down. + +Fixes #8121. Refs #8040. diff --git a/crates/perry-codegen/src/dialect/mod.rs b/crates/perry-codegen/src/dialect/mod.rs index 2082a56410..17e9ee28ba 100644 --- a/crates/perry-codegen/src/dialect/mod.rs +++ b/crates/perry-codegen/src/dialect/mod.rs @@ -861,9 +861,11 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> { let ptr = self.ctx .create_inline_asm(void_fn, asm, constraints, sideeffect, false, None, false); - self.builder + let site = self + .builder .build_indirect_call(void_fn, ptr, &[], "") .map_err(be)?; + mark_gc_leaf(self.ctx, site); Ok(()) } @@ -1542,9 +1544,11 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> { None, false, ); - self.builder + let site = self + .builder .build_indirect_call(void_fn, ptr, &[], "") .map_err(be)?; + mark_gc_leaf(self.ctx, site); Ok(()) } I::Br { label } => { @@ -1818,3 +1822,28 @@ fn apply_flags(inst: Option>, flags: &[&str]) { unsafe { llvm_sys::core::LLVMSetFastMathFlags(inst.as_value_ref(), fmf) }; } } + +/// #8121: tell `rewrite-statepoints-for-gc` to leave an inline-asm call alone. +/// +/// Perry emits `call void asm sideeffect "", ""()` as the issue-#74 loop +/// preservation barrier. RS4GC rewrites every non-leaf call in a `gc +/// "statepoint"` function into a `gc.statepoint` whose callee operand is the +/// original callee — and for inline asm that means taking the address of an +/// `InlineAsm`, which is not a value. The result fails the verifier with +/// "Cannot take the address of an inline asm!", and because production only +/// verifies BEFORE the rewrite, the broken module reached SelectionDAG and +/// SIGBUS'd in `AArch64TargetLowering::LowerCall` while lowering the bogus +/// statepoint callee. +/// +/// `"gc-leaf-function"` is the documented way to say "this call cannot trigger +/// a collection", which is exactly true of an empty barrier: it emits no +/// instructions and cannot call back into the runtime. +fn mark_gc_leaf<'ctx>( + ctx: &'ctx inkwell::context::Context, + site: inkwell::values::CallSiteValue<'ctx>, +) { + site.add_attribute( + inkwell::attributes::AttributeLoc::Function, + ctx.create_string_attribute("gc-leaf-function", ""), + ); +} diff --git a/crates/perry-codegen/src/inprocess.rs b/crates/perry-codegen/src/inprocess.rs index 7d726a630a..0ef993d923 100644 --- a/crates/perry-codegen/src/inprocess.rs +++ b/crates/perry-codegen/src/inprocess.rs @@ -412,6 +412,20 @@ fn optimize_and_emit( e.to_string() ) })?; + // #8121: verify AFTER the rewrite, not only before it. RS4GC can turn + // a module the verifier accepted into one it rejects (it rewrote an + // inline-asm barrier into a statepoint whose callee is an InlineAsm). + // Production previously verified only the input, so the broken module + // went straight to SelectionDAG and took the whole compiler down with + // a SIGBUS instead of reporting anything. A crash inside this process + // is exactly what the funclet refusal above exists to avoid. + module.verify().map_err(|e| { + anyhow!( + "rewrite-statepoints-for-gc produced a module the LLVM verifier \ + rejects; refusing to hand it to codegen (#8121):\n{}", + e.to_string() + ) + })?; } let pipeline = match opt { @@ -441,6 +455,70 @@ fn optimize_and_emit( mod tests { use super::*; + /// #8121: Perry emits `call void asm sideeffect "", ""()` as the issue-#74 + /// loop-preservation barrier. RS4GC rewrites every non-leaf call in a + /// `gc "statepoint-example"` function into a `gc.statepoint`, and for inline + /// asm that means using the `InlineAsm` itself as the callee operand — + /// invalid IR ("Cannot take the address of an inline asm!"). Production + /// verified only BEFORE the rewrite, so the broken module reached + /// SelectionDAG and killed the compiler with a SIGBUS in + /// `AArch64TargetLowering::LowerCall`. + /// + /// `%p` stays live across `@may_collect`, so RS4GC has real work to do and + /// a fixture that rewrote nothing cannot pass either arm silently. + fn asm_barrier_fixture(gc_leaf: bool) -> String { + let barrier_attr = if gc_leaf { " #5" } else { "" }; + format!( + "declare i64 @may_collect()\n\ + \n\ + define ptr addrspace(1) @barrier_fn(ptr addrspace(1) %p) gc \"statepoint-example\" {{\n\ + entry:\n\ + \x20 call void asm sideeffect \"\", \"\"(){barrier_attr}\n\ + \x20 %r = call i64 @may_collect()\n\ + \x20 ret ptr addrspace(1) %p\n\ + }}\n\ + \n\ + attributes #5 = {{ \"gc-leaf-function\" }}\n" + ) + } + + /// The bug itself, pinned. If this ever stops failing, the `gc-leaf-function` + /// marking has become unnecessary and the sibling test below is vacuous. + #[test] + fn rs4gc_breaks_an_unmarked_inline_asm_barrier() { + let target = crate::codegen::default_target_triple(); + let err = + statepoint_rewritten_ir(&asm_barrier_fixture(false), &target, "asm_barrier_unmarked") + .expect_err("RS4GC must reject an unmarked inline-asm barrier (#8121)"); + let text = format!("{err:#}"); + assert!( + text.contains("inline asm"), + "expected the inline-asm verifier rejection, got: {text}" + ); + } + + /// The fix: marked `gc-leaf-function`, the barrier is left alone and the + /// rewrite still happens for the genuinely collecting call. + #[test] + fn a_gc_leaf_inline_asm_barrier_survives_rs4gc() { + let target = crate::codegen::default_target_triple(); + let rewritten = + statepoint_rewritten_ir(&asm_barrier_fixture(true), &target, "asm_barrier_gc_leaf") + .expect("a gc-leaf inline-asm barrier must survive RS4GC (#8121)"); + assert!( + rewritten.contains("gc.statepoint"), + "RS4GC rewrote nothing, so this fixture proves nothing:\n{rewritten}" + ); + assert!( + !rewritten.contains("elementtype(void ()) asm"), + "the inline-asm barrier was used as a statepoint callee:\n{rewritten}" + ); + assert!( + rewritten.contains("asm sideeffect"), + "the barrier vanished instead of being left alone:\n{rewritten}" + ); + } + fn constant_fold_order_fixture(folded: bool) -> String { let mut ir = String::from( "declare i64 @may_collect()\n\ndefine i64 @f(i64 %d0, i64 %d1, i64 %d2, i64 %d3, i64 %d4, i64 %d5, i64 %d6, i64 %d7) gc \"statepoint-example\" {\nentry:\n", diff --git a/crates/perry-codegen/src/inst.rs b/crates/perry-codegen/src/inst.rs index 89b9ce0e21..e377c5e292 100644 --- a/crates/perry-codegen/src/inst.rs +++ b/crates/perry-codegen/src/inst.rs @@ -277,7 +277,10 @@ impl LlInst { out.push(')'); } LlInst::AsmBarrier => { - out.push_str(" call void asm sideeffect \"\", \"\"()"); + // #8121: `#5` is `{ "gc-leaf-function" }`, which stops + // rewrite-statepoints-for-gc from rewriting this barrier into + // a statepoint whose callee is an InlineAsm (invalid IR). + out.push_str(" call void asm sideeffect \"\", \"\"() #5"); } LlInst::Br { label } => { let _ = write!(out, " br label %{label}"); diff --git a/crates/perry-codegen/src/module.rs b/crates/perry-codegen/src/module.rs index 4f05e3ad0a..a8426a0295 100644 --- a/crates/perry-codegen/src/module.rs +++ b/crates/perry-codegen/src/module.rs @@ -799,6 +799,12 @@ impl LlModule { if used_nounwind_willreturn { ir.push_str("\nattributes #4 = { nounwind willreturn }\n"); } + // #8121: the issue-#74 loop barrier carries `#5` so RS4GC treats it as + // a GC leaf. Emitted only when a barrier actually rendered, so modules + // without one keep byte-identical IR. + if ir.contains("call void asm sideeffect \"\", \"\"() #5") { + ir.push_str("\nattributes #5 = { \"gc-leaf-function\" }\n"); + } } fn push_attrs_and_metadata(&self, ir: &mut String) { diff --git a/crates/perry-codegen/src/native_emit.rs b/crates/perry-codegen/src/native_emit.rs index fe41b97e27..cdc51ca42e 100644 --- a/crates/perry-codegen/src/native_emit.rs +++ b/crates/perry-codegen/src/native_emit.rs @@ -679,6 +679,48 @@ mod tests { } } + /// #8121, emission half. The sibling pair in `inprocess::tests` proves the + /// LLVM mechanism (RS4GC breaks an unmarked inline-asm barrier, and + /// `gc-leaf-function` stops it) using hand-written IR, so it would still + /// pass if Perry stopped emitting the attribute. This asserts the emission + /// itself, on both paths. + #[test] + fn perry_emits_the_loop_barrier_as_a_gc_leaf() { + let mut module = LlModule::new(crate::codegen::default_target_triple()); + let function = module.define_function("barrier_emission_fixture", VOID, vec![]); + let entry = function.create_block("entry"); + entry.asm_sideeffect_barrier(); + entry.ret_void(); + + let text_ir = module.to_ir(); + assert!( + text_ir.contains("asm sideeffect"), + "fixture emitted no barrier, so this proves nothing:\n{text_ir}" + ); + assert!( + text_ir.contains(r#"attributes #5 = { "gc-leaf-function" }"#), + "text path lost the gc-leaf attribute group (#8121):\n{text_ir}" + ); + assert!( + text_ir.contains(r#"call void asm sideeffect "", ""() #5"#), + "text path barrier is not marked #5 (#8121):\n{text_ir}" + ); + + let context = Context::create(); + let native_ir = build_native_module(&context, &module) + .expect("barrier emission fixture constructs") + .print_to_string() + .to_string(); + assert!( + native_ir.contains("asm sideeffect"), + "native arm emitted no barrier, so this proves nothing:\n{native_ir}" + ); + assert!( + native_ir.contains("gc-leaf-function"), + "native path lost the gc-leaf attribute on the barrier (#8121):\n{native_ir}" + ); + } + fn compact_gc_map_section_name() -> &'static [u8] { if cfg!(target_os = "macos") { b"__perry_gcmap"