Skip to content

fix(genui-runtime): bound the pre-validation walks at the depth limits - #429

Open
PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/genui-runtime-depth-guards-20261105
Open

PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/genui-runtime-depth-guards-20261105

Conversation

@PerryLink

Copy link
Copy Markdown
Contributor

Problem

Three walks run on a freshly parsed fence before validation/repair:

  • normalizeGenuiSpec (src/client/genui-runtime/normalize.ts)
  • defaultFileTreeDirectories, the file-tree type pass (same file)
  • diagnoseUnknownGenuiFields (src/client/genui-runtime/diagnostics.ts)

All three recurse over the component tree with no depth bound. validateNode, repairNodeFields and walkTree do enforce GENUI_LIMITS.maxDepth = 8 / maxTreeDepth = 6, but they run after these walks, so a deeply nested fence overflows the stack before the spec can be rejected:

input crash site
~1.1k-level / ~27 KB {"type":"col","items":[…]} chain normalizeGenuiSpec
~2.5k-level file-tree children defaultFileTreeDirectories
~2k-level nested tree diagnoseUnknownGenuiFields

All three fail with RangeError: Maximum call stack size exceeded. Fence bodies are model output, so the input is untrusted and ~27 KB is not a large fence.

Fix

Thread the node depth through the three walks and stop at the same limits the rest of the pipeline already uses:

  • root item = depth 0; depth 8 is accepted, depth 9 is the first rejected level (unchanged exceeds max depth 8 diagnostic);
  • file-tree keeps maxTreeDepth + 1 = 7 levels, matching walkTree and the renderer's own depth > maxTreeDepth check (blocks/advanced.tsx);
  • the over-deep subtree is returned as-is instead of walked. That is invisible to readers: validation rejects the fence and repair drops the tail anyway.

No renderer, schema or limit changes.

Tests

Four new tests in tests/genui-schema.spec.ts:

  1. normalize normalizes exactly the rendered levels (alias at depth 8 works) and leaves depth 9 untouched; 4k-level input does not throw.
  2. The full pipeline accepts depth 8 and rejects depth 9 with the usual diagnostic; 4k-level input does not throw.
  3. Unknown-field diagnostics stop at maxDepth (reported at depth 8, not at depth 9).
  4. The file-tree directory walk stops at maxTreeDepth; the deepest kept directory keeps its inferred dir marker.

On the base commit all four fail with RangeError; with the patch npx vitest run is 2110 passed / 152 files and npx tsc -p tsconfig.json --noEmit is clean. Specs within the documented depth limit are byte-identical before/after (26-spec equivalence set covering empty/single roots, every container, aliases, unknown fields and file-trees of 6/7/8 levels).

normalizeGenuiSpec, the file-tree directory pass, and unknown-field
diagnostics all walk the freshly parsed fence before validateNode and
repairNodeFields apply GENUI_LIMITS.maxDepth / maxTreeDepth. A deeply
nested fence overflowed the stack before the guard could reject it.

Thread the walk depth through those three functions and stop at the same
limits the validator and repair already use: the root item is depth 0,
depth 9 is the first rejected level, and a file-tree keeps
maxTreeDepth + 1 levels. The over-deep subtree is returned untouched;
validation rejects the fence and repair drops that tail either way, so
readers never see it.

Reproduced on the base commit with "RangeError: Maximum call stack size
exceeded" for a ~1.1k-level / ~27 KB col chain (normalizeGenuiSpec), a
~2.5k-level file-tree (defaultFileTreeDirectories) and a ~2k-level tree
(diagnoseUnknownGenuiFields). 4k-level inputs now process in <10 ms, and
specs within the documented depth limit are byte-identical to before.

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