Skip to content

[Next.js/dylib] perry compile SIGBUSes in LLVM codegen of next/dist/compiled/jsonwebtoken (100/104 modules in) #8121

Description

@proggeramlug

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.mjsPASS: 21 production App Route requests
  • node perry-host.js + verify.mjsPASS: 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

  1. Confirm determinism of the SIGBUS by running the 2-module repro to completion.
  2. 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.
  3. 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.
  4. Investigate why one unit of a 14 MB IR module costs 100× its siblings.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions