Skip to content
Closed
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
9 changes: 9 additions & 0 deletions changelog.d/8121-rs4gc-inline-asm-statepoint.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 31 additions & 2 deletions crates/perry-codegen/src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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 } => {
Expand Down Expand Up @@ -1818,3 +1822,28 @@ fn apply_flags(inst: Option<InstructionValue<'_>>, 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", ""),
);
}
78 changes: 78 additions & 0 deletions crates/perry-codegen/src/inprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-codegen/src/inst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
42 changes: 42 additions & 0 deletions crates/perry-codegen/src/native_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading