Part of #8040. First failure found when actually driving a real production Next.js App Route through Perry.
Summary
perry compile crashes with SIGBUS (EXC_BAD_ACCESS / KERN_PROTECTION_FAILURE) during LLVM codegen of next/dist/compiled/jsonwebtoken/index.js. The composed production App Route never reaches the runtime — the failure is at the compile boundary, before any request can be executed.
The dylib link blocker (#8064 / fixed by #8078) is genuinely behind us: codegen reached 100 of 104 modules before dying.
Environment
- Perry
506f4ab11a5d2a1b7a498e965c79d11e2fa63043 (perry 0.5.1510), cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
- macOS arm64, in-process LLVM backend (LLVM 22.1.4)
- Fixture:
tests/release/packages/next-app-route (Next.js 16.3.0, React 19.2.4), production webpack build
Node oracle: PASSES
Both oracles are green, so the fixture and route implementation are correct:
next start + verify.mjs → PASS: 21 production App Route requests
node perry-host.js + verify.mjs → PASS: 21 production App Route requests (twice, cold + warm)
The routeModule.handle bypass guard was armed and sabotage-verified: removing only the wrapper install makes it fire 21/21 times; with the wrapper installed it fires 0 times. So the guard is a discriminating detector, not a vacuous one. Note its signal appears in the host log, not in verify.mjs's exit code — the assertion throws inside a .then() that is caught and logged after the response is already sent.
The crash
Last codegen activity before death:
[perry] codegen: modules finished 100/104 (96%; 119.0 min elapsed; ETA ~4.8 min)
[perry] codegen: node_modules_next_dist_compiled_jsonwebtoken_index_js: freezing 2 codegen units for worker threads
[perry] codegen: node_modules_next_dist_compiled_jsonwebtoken_index_js: froze 2/2 units (100%; 9.6s elapsed; ETA ~0.0s)
perry: module IR is 14.0 MB (> 6.0 MB), 468 functions (~31 KB/fn); compiling at -Os instead of -O3 ...
No error is printed — the process dies under signal. Crash report:
exception: EXC_BAD_ACCESS (SIGBUS), KERN_PROTECTION_FAILURE at 0x0000001b413c6d08
termination: SIGNAL code 10, "Bus error: 10"
faulting thread: 15 (an LLVM codegen worker), depth 24 frames
This is a wild pointer dereference, not a truncated-mmap SIGBUS. The crash report's own VM region info places the fault address in reserved, never-mapped address space:
0x1b413c6d08 is in 0x1000000000-0x7000000000
---> GPU Carveout (reserved) 1000000000-7000000000 [384.0G] ---/--- SM=NUL reserved VM address space (unallocated)
Nothing is or ever was mapped there, so this is a corrupted/garbage pointer being dereferenced — it rules out the "object file truncated by a full disk" explanation. The faulting stack is only 24 frames deep, which also rules out stack overflow. The process had 15 codegen worker threads live under the in-process LLVM backend.
Minimal reproduction
Reduced from 104 modules to 2. Outside a Next project directory (important: Perry auto-discovers .next/server — "Next.js standalone: discovered 17 runtime module(s)" — so compiling inside the fixture pulls the whole 104-module graph in regardless of what you require):
mkdir /tmp/minjwt && cd /tmp/minjwt
cp <fixture>/node_modules/next/dist/compiled/jsonwebtoken/index.js ./jsonwebtoken-index.js
printf 'const jwt = require("./jsonwebtoken-index.js");\nconsole.log("jwt typeof:", typeof jwt);\n' > main.js
perry compile main.js --output-type dylib --no-auto-optimize --no-cache -o min.dylib
jsonwebtoken-index.js is 118 KB, sha256 056c2dddba96faa35f4dc0740228a241990561290ff3bd5456c7c7dbc5a7a6b9 (from next@16.3.0). It lowers to a 14.5 MB IR module, 468 functions, split into 2 codegen units.
This reproduction reaches the exact crash state and samples into the same code region as the original crash. I was not able to run it to completion — see the compile-time blowup below — so I have confirmed the repro reaches the failing path but have not yet confirmed the SIGBUS is deterministic. That is the first thing to check.
Secondary finding: codegen compile-time blowup
Independent of the crash, single LLVM codegen units on this graph take pathological time:
.next/server/chunks/430.js unit 2/6 took 6704.3 s (112 minutes) while its five sibling units took 80–319 s each.
- The 2-module
jsonwebtoken repro spent >90 minutes on unit 1/2 while unit 2/2 finished in 142 s.
Sampling shows it genuinely progressing through different LLVM code (the hot leaf moves between samples), so it is slow, not hung. Total wall time for the 104-module compile was ~2 h before it crashed. This alone makes the fixture impractical as a CI gate.
Suggested next steps
- Confirm determinism of the SIGBUS by running the 2-module repro to completion.
- A/B
PERRY_CODEGEN_UNIT_JOBS=1 vs default. 15 concurrent workers under an in-process LLVM backend makes a codegen concurrency bug (e.g. a shared LLVMContext/Module across threads) a prime suspect for a wild pointer. I started this A/B but had to stop it on disk pressure.
- Build
perry with debug symbols — the release binary is stripped (431 symbols), so the 24-frame backtrace could not be symbolicated and the responsible pass is still unidentified.
- Investigate why one unit of a 14 MB IR module costs 100× its siblings.
Part of #8040. First failure found when actually driving a real production Next.js App Route through Perry.
Summary
perry compilecrashes with SIGBUS (EXC_BAD_ACCESS / KERN_PROTECTION_FAILURE) during LLVM codegen ofnext/dist/compiled/jsonwebtoken/index.js. The composed production App Route never reaches the runtime — the failure is at the compile boundary, before any request can be executed.The dylib link blocker (#8064 / fixed by #8078) is genuinely behind us: codegen reached 100 of 104 modules before dying.
Environment
506f4ab11a5d2a1b7a498e965c79d11e2fa63043(perry 0.5.1510),cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-statictests/release/packages/next-app-route(Next.js16.3.0, React19.2.4), production webpack buildNode oracle: PASSES
Both oracles are green, so the fixture and route implementation are correct:
next start+verify.mjs→PASS: 21 production App Route requestsnode perry-host.js+verify.mjs→PASS: 21 production App Route requests(twice, cold + warm)The
routeModule.handlebypass guard was armed and sabotage-verified: removing only the wrapper install makes it fire 21/21 times; with the wrapper installed it fires 0 times. So the guard is a discriminating detector, not a vacuous one. Note its signal appears in the host log, not inverify.mjs's exit code — the assertion throws inside a.then()that is caught and logged after the response is already sent.The crash
Last codegen activity before death:
No error is printed — the process dies under signal. Crash report:
This is a wild pointer dereference, not a truncated-mmap SIGBUS. The crash report's own VM region info places the fault address in reserved, never-mapped address space:
Nothing is or ever was mapped there, so this is a corrupted/garbage pointer being dereferenced — it rules out the "object file truncated by a full disk" explanation. The faulting stack is only 24 frames deep, which also rules out stack overflow. The process had 15 codegen worker threads live under the in-process LLVM backend.
Minimal reproduction
Reduced from 104 modules to 2. Outside a Next project directory (important: Perry auto-discovers
.next/server— "Next.js standalone: discovered 17 runtime module(s)" — so compiling inside the fixture pulls the whole 104-module graph in regardless of what yourequire):jsonwebtoken-index.jsis 118 KB, sha256056c2dddba96faa35f4dc0740228a241990561290ff3bd5456c7c7dbc5a7a6b9(fromnext@16.3.0). It lowers to a 14.5 MB IR module, 468 functions, split into 2 codegen units.This reproduction reaches the exact crash state and
samples into the same code region as the original crash. I was not able to run it to completion — see the compile-time blowup below — so I have confirmed the repro reaches the failing path but have not yet confirmed the SIGBUS is deterministic. That is the first thing to check.Secondary finding: codegen compile-time blowup
Independent of the crash, single LLVM codegen units on this graph take pathological time:
.next/server/chunks/430.jsunit 2/6 took 6704.3 s (112 minutes) while its five sibling units took 80–319 s each.jsonwebtokenrepro spent >90 minutes on unit 1/2 while unit 2/2 finished in 142 s.Sampling shows it genuinely progressing through different LLVM code (the hot leaf moves between samples), so it is slow, not hung. Total wall time for the 104-module compile was ~2 h before it crashed. This alone makes the fixture impractical as a CI gate.
Suggested next steps
PERRY_CODEGEN_UNIT_JOBS=1vs default. 15 concurrent workers under an in-process LLVM backend makes a codegen concurrency bug (e.g. a sharedLLVMContext/Moduleacross threads) a prime suspect for a wild pointer. I started this A/B but had to stop it on disk pressure.perrywith debug symbols — the release binary is stripped (431 symbols), so the 24-frame backtrace could not be symbolicated and the responsible pass is still unidentified.