You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
React Flight serializer: symbol falls through the whole typeof chain, re-check at throw site is true — Next.js App Router renders all 500 #6546
In Next.js 16's compiled React Flight serializer (next/dist/compiled/next-server/app-page-turbo.runtime.prod.js, the ~318KB outlined-model writer closure), a Symbol.for("react.fragment") value falls through the entire typeof dispatch chain to the tail throw — even though re-evaluating the exact same comparison at the throw site returns true.
Probe installed immediately before the tail throw Error("Type "+typeof a+"..."):
i.e. control passed the function's if("symbol"==typeof a){...} arm without taking it, then the probe — adjacent statements, same variable — sees the comparison as TRUE. The compiled arm evaluated FALSE for the same value the probe sees as a symbol.
Impact
This is the current blocker for Next.js 16 standalone serving (with #6522, #6527, #6537, #6545 and the fetch-super fix applied, the server boots and stays alive): every App Router render 500s with either Error: Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(undefined) cannot be found… or Error: Type symbol is not supported in Client Component props. (which of the two Flight sites fires varies per build), plus paired Unhandled Rejection: undefined.
What was ruled out (all byte-identical to node in isolation)
The failing function's IR shows the symbol check compiled generically (js_value_typeof + js_string_equals against the interned "symbol"), which is sound — so either the executed check reads a stale/desynced slot for the reassigned minified local a (the function reassigns a heavily; possibly the boxed-vs-direct dual-slot family, cf. #6328/#6345), or codegen emitted a duplicated/specialized variant of the typeof chain (typed-feedback fast path) whose symbol classification diverges, and that variant is the one executing.
Note js_value_typeof returns per-thread cached StringHeaders for its results while the compiled comparison goes through js_string_equals (content compare) — identity vs content shouldn't matter here, but the executed-variant question is open.
Repro
perry compile the gscmaster Next.js 16 standalone app (or any App Router app) at main + PR #6545, boot, curl /<any-page>. The probe text above can be inserted at the single "Type "+typeof a+" is not supported in Client Component props" site in app-page-turbo.runtime.prod.js.
Verified at main 7279998 + #6545 + the fetch-super fix.
gscmaster serves complete, correct HTML pages. At the default opt level, the same build 500s on every render with the symbol falling through the Flight typeof chain (and the throw-site re-check returning true).
So codegen's emitted IR is semantically correct (consistent with my hand-audit of __695: every typeof in the chain reads the same slot via js_value_typeof + js_string_equals), and clang's -O2/-O3 on that IR breaks it. Prime suspect per prior art (#6082): unsound attributes on js_* helper declarations (memory(argmem: ...) / readonly / willreturn on helpers that actually read/write NaN-boxed heap state), licensing LLVM to CSE or reorder a js_value_typeof/box-read across a mutation of a's slot — matching the observed "check false, adjacent re-check true".
A bisect over the helper-attribute surface (or PERRY_LL_SIZE_OPT / opt-level per-unit) against this repro should pin the exact declaration. Until fixed, PERRY_LL_O0_THRESHOLD_BYTES=1 is a full workaround for Next.js App Router serving.
Update (2026-07-18 evening): no longer reproduces at default opt — the PERRY_LL_O0_THRESHOLD_BYTES=1 workaround is obsolete.
Retested on current main (b389a58): a default-opt build of the gscmaster standalone serves /de with 200s and output byte-identical to the workaround build (40/40 soak). A pristine b389a58 toolchain (without the PR #6599 stream-band fix) also serves 200s at default opt, so the fix landed in the merge window cf79c3f90..b389a58e1 — most likely #6594 (“gate .errors fast-path to error receivers”, the same ungated-receiver-fast-path disease family), possibly #6592/#6596.
Two corrections to the original analysis while closing this out:
The workaround never meant -O0, and __695 was never the miscompiled function.PERRY_LL_O0_THRESHOLD_BYTES=1 routes normal-density oversized modules to -Os (only >256 KB/fn monoliths get -O0), and app_page_turbo (96 MB IR, far above the default 6 MB threshold) compiles at -Os in both configurations. The real delta was the sub-6 MB modules at -O3 vs -Os, so the guilty code was in one of those modules — consistent with an input-dependent/UB-flavored fast-path rather than a clang miscompile of the serializer.
Closing as fixed-by another change; can re-open with a fresh bisect if it resurfaces.
Activity
proggeramlug commentedon Jul 17, 2026
DECISIVE: this is an LLVM-optimization-level miscompile
Forcing every module to
-O0(PERRY_LL_O0_THRESHOLD_BYTES=1 perry compile server.js) with the identical compiler and identical sources:gscmaster serves complete, correct HTML pages. At the default opt level, the same build 500s on every render with the symbol falling through the Flight typeof chain (and the throw-site re-check returning
true).So codegen's emitted IR is semantically correct (consistent with my hand-audit of
__695: everytypeofin the chain reads the same slot viajs_value_typeof+js_string_equals), and clang's-O2/-O3on that IR breaks it. Prime suspect per prior art (#6082): unsound attributes onjs_*helper declarations (memory(argmem: ...)/readonly/willreturnon helpers that actually read/write NaN-boxed heap state), licensing LLVM to CSE or reorder ajs_value_typeof/box-read across a mutation ofa's slot — matching the observed "check false, adjacent re-check true".A bisect over the helper-attribute surface (or
PERRY_LL_SIZE_OPT/ opt-level per-unit) against this repro should pin the exact declaration. Until fixed,PERRY_LL_O0_THRESHOLD_BYTES=1is a full workaround for Next.js App Router serving.proggeramlug commentedon Jul 18, 2026
Update (2026-07-18 evening): no longer reproduces at default opt — the
PERRY_LL_O0_THRESHOLD_BYTES=1workaround is obsolete.Retested on current
main(b389a58): a default-opt build of the gscmaster standalone serves/dewith 200s and output byte-identical to the workaround build (40/40 soak). A pristine b389a58 toolchain (without the PR #6599 stream-band fix) also serves 200s at default opt, so the fix landed in the merge windowcf79c3f90..b389a58e1— most likely #6594 (“gate.errorsfast-path to error receivers”, the same ungated-receiver-fast-path disease family), possibly #6592/#6596.Two corrections to the original analysis while closing this out:
The helper-attribute ([perf] Runtime helper declarations carry no LLVM attributes → -O3 cannot optimize across js_* calls #6082) suspicion is exonerated. Extracting
__695from the preserved.perry-trace/llvmmodule and compiling it at-O2with and without the attribute groups produces identical optimized IR — the only attributed helper reachable there (js_is_truthy,nounwind willreturn memory(read)) changes nothing.The workaround never meant
-O0, and__695was never the miscompiled function.PERRY_LL_O0_THRESHOLD_BYTES=1routes normal-density oversized modules to-Os(only >256 KB/fn monoliths get-O0), andapp_page_turbo(96 MB IR, far above the default 6 MB threshold) compiles at-Osin both configurations. The real delta was the sub-6 MB modules at-O3vs-Os, so the guilty code was in one of those modules — consistent with an input-dependent/UB-flavored fast-path rather than a clang miscompile of the serializer.Closing as fixed-by another change; can re-open with a fresh bisect if it resurfaces.