fix(test): C host + two-level namespace for the Next App Route dylib gate (#8205) - #8209
Conversation
|
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 (5)
💤 Files with no reviewable changes (1)
Included review availability: Your plan includes up to 8 reviews per rolling hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe Next App Route dylib gate replaces its Rust host with a C host, removes Darwin flat-namespace linking, and updates the test harness to compile and run the host with explicit process control. ChangesNext App Route dylib gate
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This test-only change replaces the host and linker setup to prevent allocator binding failures, fixes the gate’s working-directory and process-cleanup behavior, and preserves the existing ABI checks. No actionable merge-blocking risk remains beyond normal checks and review. Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 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 |
…gate (#8205) The gate aborted on its first request: the Rust provider-host exported rustc's System-allocator shim, and the stdlib provider image was linked with -flat_namespace, so its __rust_dealloc import bound to the host's shim while the runtime image's shim is mimalloc. The first cross-image Vec drop in js_node_http_server_process_pending freed a mimalloc pointer with libsystem free() and aborted. Replace provider-host.rs with a C host (same load order, flags, probe check and event loop; no Rust allocator shim in the executable) and drop -flat_namespace -interposable from provider-linker.sh so the stdlib image binds its runtime imports two-level to libperry_runtime.dylib.
…/server With the abort gone, every request 500'd: Next opens .next/routes-manifest.json relative to cwd, which does not exist under .next/server. The chunk-require reason for that cwd is obsolete since #8146; the release fixture serves from the package root and is the layout with production evidence.
Each cold start launched the host inside a (cd; host) & subshell and killed the subshell pid; the host survived orphaned, kept serving, and held the port, so a second cold start could never bind. Observed directly: a leaked provider-host with ppid 1 still listening after the gate exited.
dc10383 to
f259dbd
Compare
|
State at ready-for-review (2026-08-16)
|
Fixes #8205.
What was wrong
tests/test_next_app_route_dylib.sh(the #8161 gate behindnext-app-route.yml) aborted on cold start 1, first request — Node oracle PASS, app dylib compiled, providers linked, ABI check clean,PERRY_NEXT_APP_ROUTE_READYprinted, thenAbort trap: 6on the readinesscurl:It is an allocator-shim binding problem, not a route or compiler problem (the same commit/compiler/app served 500 verifier batches under
tests/release/packages/next-app-route/fixture.shthe same day). Thenmfacts from #8205:libperry_runtime.dylibdefines rustc's allocator shim and it is mimalloc:__RNv…7___rustc14___rust_dealloc: b _mi_free.libperry_stdlib.dylibimports__rust_alloc/__rust_dealloc/__rust_alloc_zeroed(U) — correct, it must use the runtime image's allocator.provider-host(built fromprovider-host.rs, a Rust executable) also defines__RNv…7___rustc14___rust_dealloc— the System-allocator shim every Rust binary carries.provider-linker.shlinked the stdlib image with-Wl,-flat_namespace -Wl,-interposable. In a flat namespace an undefined symbol binds at load time to the first image defining it, and the main executable is first — so the stdlib image's__rust_deallocbound to the host's System shim while the buffers it drops were allocated by the runtime image's mimalloc. First cross-imageVec<i64>drop (draining the pending-request list) → libsystemfree()on a mimalloc pointer →abort().The fix (test-only,
tests/only, nocrates/**)provider-host.rs→provider-host.c. Same load order and flags (runtimeRTLD_NOW|RTLD_GLOBAL, stdlibRTLD_NOW|RTLD_GLOBAL, appRTLD_NOW|RTLD_LOCAL), samenext_app_route_provider_runtime_probe() == js_gc_init"one runtime image" check, samejs_gc_init → perry_module_init → {microtasks, 3 timer ticks, stdlib pump, wait}loop. The only behavioural difference is what is absent: a C executable carries no Rust allocator shim, so the only__rust_*definitions in the process are the runtime image's. This is the release-tier layout (tests/release/packages/next-app-route/provider-host.c), which has the production evidence. (On Linux, where the workflow actually runs, a Rust executable's shim is not in.dynsymunless exported, so the flat ELF lookup would most likely have skipped it — I have not run the gate on Linux and do not claim it was red there for this reason; the C host simply leaves no shim to find on either platform.)-flat_namespace -interposablefromprovider-linker.sh. With the default two-level namespace every undefined symbol in the stdlib image is bound at link time to the image that defines it, so__rust_deallocresolves to@rpath/libperry_runtime.dylibregardless of what else the process defines. The exported-symbols list, the-Wl,-uretention of the app's required ABI, the runtime-dylib substitution on the link line and the@loader_pathrpath are unchanged.tests/test_next_app_route_dylib.shbuilds the host with"$real_cc" -O2 … -ldlinstead ofrustc. Everything else the script guarantees is intact: therequired_symbols/missing_symbolsABI check, theforbidden_diagnosticsgrep on every host log (including therouteModule.handlebypass guard), and theProvider ABI hashline..github/workflows/next-app-route.ymldoes not name the host file, so it needs no change.Two more gate-only defects the fix exposed (both were invisible behind the abort)
Fixing the abort let the gate serve for the first time, and it immediately hit two more reasons it could never have gone green — both fixed here, both in the gate script only:
$fixture/.next/serverso the webpack runtime could resolve./chunks/*.js— a workaround that fix(next): make computed relative chunk requires resolve in a compiled App Route #8146 made obsolete (computed relative chunk requires now resolve against the route bundle). Meanwhile Next opens.next/routes-manifest.jsonagainst the working directory on every request, so from.next/servereach request 500'd with ENOENT (measured: 1,043 ENOENT lines, readiness never reached). The host now runs from the fixture root — the release-tier layout with production evidence.( cd …; "$host" … ) &subshell and killed$!; the host survived orphaned (observed directly: aprovider-hostwith ppid 1 still serving after the gate exited) and held the port, so cold start 2 could never bind. The launch nowexecs the host so$host_pidis the host itself. (The node-oracle script has the same shape aroundnpm start— a leakednext-serverwas observed on the oracle port — but that leaks once per gate run, not per cold start, and only bites a second run on the same machine; noted, not fixed here.)What this does and does not make green
This makes the host survive its first request and serve; it does not make the 100-batch loop reliably green on today's
main. #8163 ("Reproduces under DEFAULT GC") shows ~2% of warm batches lose one response after a default-mode copying minor (TypeError: value is not a functionin the host log right after a[gc-copy-minor] ranline, then an empty body inverify.mjs). With 10 verifier passes per cold start this gate can hit that. That is a different defect (a stale closure held outside the GC heap), it is tracked in #8163, and this PR does not paper over it.Validation (bench mini, macOS arm64,
perry-dev@ this branch, LLVM 22.1.8,PERRY_GC_DIAG=1)tests/test_next_app_route_dylib.shend to end (defaults: 10 cold starts x 10 verifiers),PERRY_NEXT_PORT=3400,PERRY_NEXT_CARGO_JOBS=3,PERRY_MODULE_JOBS=2,PERRY_CODEGEN_UNIT_JOBS=2:missing-symbolsempty);Provider ABI hashprinted.verify.mjsUnexpected end of JSON input(empty body), host logTypeError: value is not a functionimmediately after a[gc-copy-minor] ran/nursery_minorsafepoint drain. Copying minors per cold start: 3/3/3/3/3/3;TypeErrorcount: 0/0/0/0/0/1; bypass-guard fires: 0/0/0/0/0/0. That is precisely [Next.js/dylib] Forced-evacuation App Route arm: stale closure from a holder outside the GC heap #8163's default-GC signature ("Reproduces under DEFAULT GC", [Next.js/dylib] Forced-evacuation App Route arm: stale closure from a holder outside the GC heap #8163's comment), and the script'sset -estops at the first failed verifier, so cold starts 7–10 did not run. Since this measurement, fix(gc): root the two holders outside the GC heap behind the forced-evacuation App Route arm (#8163) #8211 landed and fixed the two forced-evacuation-arm holders, but [Next.js/dylib] Forced-evacuation App Route arm: stale closure from a holder outside the GC heap #8163 is reopened on exactly this default-GC residual — so a full 10x10 green run is still not expected onmainuntil that residual lands.routes-manifest.jsonENOENT lines after the cwd fix (1,043 before it); zero orphaned hosts after theexecfix (one observed before it).Mini log paths: gate run
~/perry-bench.noindex/tmp-8205/gate2.log, kept scratch with per-cold-start host logs~/perry-bench.noindex/tmp-8205/perry-next-app-route.hp7c6V/host-{1..6}.log, first (pre-cwd-fix) run~/perry-bench.noindex/tmp-8205/gate.log+perry-next-app-route.KVazL1/.What I could NOT validate, and why
A clean 100/100 gate run is not reachable on
maintoday, for two reasons that are both other people's bugs:js_headers_for_each's hoisted closure pointer (crates/perry-stdlib/src/fetch/headers.rs:460) plus eight sibling sites, measured 8 from-space faults → 0 and 3 failures/300 warm passes → 0. Once that lands the gate's 10x10 should be reachable; I have not re-run it on top of fix(gc): root the Headers/FormData iteration frame slots (#8163, #8217) #8220.mainoutright (filed from this work): the in-process LLVM backend — the default whenPERRY_LLVM_INPROCESSis unset — fails 5 of the 104 modules, root-caused since filing toinsertelement <2 x i64>from perf(gc, codegen): recover the instruction cost of the 56 B → 48 B header shrink (#8122) #8204's module-init header-image compose having no case inperry-codegen/src/dialect/mod.rs(bailsbad binary op).app-route.runtime.prod.jsis one of the failures, soPERRY_ALLOW_PARTIAL_CODEGEN=1is not a workaround. This gate is unaffected in practice —tests/test_next_app_route_dylib.shdrives the external LLVM 22 clang/opt path, which compiles all 104 modules clean, and that is how the run above was produced. The release-tierfixture.sh(in-process by default) is the one blocked.Neither is introduced or worked around by this PR.
Summary by CodeRabbit
Bug Fixes
Tests