Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo - #37358

Closed
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup
Closed

Fix unbounded debugInfo growth in Flight transferReferencedDebugInfo#37358
sundeep8967 wants to merge 2 commits into
react:mainfrom
sundeep8967:fix/flight-debug-info-dedup

Conversation

@sundeep8967

Copy link
Copy Markdown
Contributor

Closes#37343

Problem

In React Flight client DEV mode, transferReferencedDebugInfo copies name == null entries from a referenced chunk's _debugInfo into the parent chunk's _debugInfo. In large, deeply nested, or heavily deduplicated RSC model graphs:

  1. In fulfillReference, the element props branch (case '3') and default branch called transferReferencedDebugInfo without checking !reference.isDebug (unlike the general reference branch).
  2. transferReferencedDebugInfo unconditionally pushed entries without checking if the parent _debugInfo already contained that entry reference.
  3. This caused exponential duplicate accumulation of debug info entries across deep graph traversals, eventually exceeding V8's pointer-element array limit (2^27 = 134,217,728) and throwing RangeError: Invalid array length.

Solution

  1. Added !reference.isDebug guards to the element case '3' and default branches in fulfillReference to ensure debug chunks do not recursively re-transfer debug info during resolution.
  2. Deduplicated entries in transferReferencedDebugInfo before pushing to parentDebugInfo.

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Hi @sundeep8967!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against e8beac1

@meta-cla

meta-claBot commented Aug 24, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.
The rest of react#37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. react#37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
Alternatives Considered
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and
that is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixesreact#37343Closesreact#37358Closesreact#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
github-actionsBot pushed a commit that referenced this pull request Sep 2, 2026
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.
The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.
The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.
This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.
The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.
**Alternatives Considered**
- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.
Fixes#37343Closes#37358Closes#37359
Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
DiffTrain build for [8f00437](8f00437)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [Flight] Unbounded _debugInfo growth in transferReferencedDebugInfo throws RangeError: Invalid array length

1 participant

@sundeep8967