Skip to content

fix(guard): bound JSON depth before the renderer serializes it - #431

Open
PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/guard-json-depth-20261107
Open

PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/guard-json-depth-20261107

Conversation

@PerryLink

Copy link
Copy Markdown
Contributor

Problem

A json node accepts any value (src/client/guard.ts, repair case json). A value nested a few thousand levels deep therefore passes validation with no errors, and the first serialization at render time overflows the stack:

  • src/client/fence-render.tsx:194 — the fence state key calls JSON.stringify(spec)
  • src/client/GenuiBlock.tsx:86 — the streaming memo fingerprint stringifies the spec
  • src/client/blocks/advanced.tsx:102 — the JSON tree recurses over the value

Repro on the base commit (~30 KB body, 5 000 levels):

processGenuiSpec: errors=0, renderable=true
JSON.stringify(spec): RangeError: Maximum call stack size exceeded

In jsdom the whole fence then fails to render ([dsh-genui] render failed: RangeError) and the reader gets the error boundary instead of a code block. The same pattern exists in repairStrings: a deep object used as a select/radio/segmented-control option falls back to JSON.stringify(item) and throws out of processGenuiSpec itself.

Fix

GENUI_LIMITS.maxJsonDepth = 32, checked by an iterative jsonValueTooDeep helper (a recursive check would just move the overflow into the guard):

32 is far inside what the JSON tree can draw and what V8 can stringify (a few thousand levels), and well above a readable JSON payload. Values at 32 still round-trip verbatim; 33 is the first rejected level.

Tests

tests/genui-guard.spec.ts (+3):

  1. 5 000-level value: validation error, node dropped, JSON.stringify(processed.spec) does not throw; 32-level value round-trips;
  2. direct validateGenuiSpec produces the diagnostic and repairGenuiSpec returns {items: []}; shallow payloads keep their exact shape;
  3. a 5 000-level object option no longer throws out of processGenuiSpec, and labelled object options still become labels.

On the base commit all 3 fail (two with RangeError); with the patch npx vitest run tests/genui-guard.spec.ts is 52 passed, full suite 2109 passed / 152 files, npx tsc -p tsconfig.json --noEmit clean.

Not covered here: the render_ui tool wrapper-unwrapping depth (src/plugin/tool.ts), which is a separate path.

A `json` node accepts any value, so a ~30 KB / ~5k-level payload passed
validation with no errors; the block fingerprints (`JSON.stringify`) and
the JSON tree recursion then overflowed the stack at render time.

Check the value iteratively (a recursive depth check would just move the
overflow into the guard) and cap it at GENUI_LIMITS.maxJsonDepth = 32,
well inside what the JSON tree can draw. Over-deep values fail validation
with a teaching diagnostic and repair drops the node, so nothing
unserializable reaches the renderer. The same helper guards the
object-option label fallback in repairStrings, which had the identical
unbounded JSON.stringify and threw out of processGenuiSpec itself.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant