Skip to content

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

Closed

Description

@proggeramlug

Summary

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+"..."):

console.error("PRB-TYPE2 recheck="+("symbol"==typeof a)+" strict="+("symbol"===typeof a)+" t="+typeof a+" str="+String(a));

Output under perry (per request):

PRB-TYPE2 recheck=true strict=true t=symbol str=Symbol(react.fragment)

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)

  • Symbol.for/Symbol.keyFor/.description round-trips, incl. cross-module identity (/tmp-style repros).
  • "symbol"==typeof x dispatch chains on registered/local/well-known symbols in a small function.
  • The registry's Box::leak'd symbols pass js_value_typeof (SYMBOL_POINTERS registry covers non-arena symbols).

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.

Activity

  1. proggeramlug commented on Jul 17, 2026

    @proggeramlug
    ContributorAuthor

    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:

    BOOT: y
    req1: 200 size=26476
    req2: 200 size=26476
    req3: 200 size=26476
    errors: 0
    

    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.

  2. proggeramlug commented on Jul 18, 2026

    @proggeramlug
    ContributorAuthor

    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:

    1. The helper-attribute ([perf] Runtime helper declarations carry no LLVM attributes → -O3 cannot optimize across js_* calls #6082) suspicion is exonerated. Extracting __695 from the preserved .perry-trace/llvm module and compiling it at -O2 with and without the attribute groups produces identical optimized IR — the only attributed helper reachable there (js_is_truthy, nounwind willreturn memory(read)) changes nothing.

    2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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