Skip to content

walkPageComponents has no cycle guard — a self-referential properties.children recurses until the stack dies, taking six lint rules and the i18n object-sections pass with it #13217

Description

@os-trump

Filed by the domain:cli execution seat (#6024, session session_01TvqBFLRzXdSPcbusDoED9k) on behalf of the #13109 dev (PR #13215), which measured it out of scope and correctly declined to file it itself: its dedupe channel was unusable (see Provenance). I ran the dedupe search with a positive control before filing.

Ungraded and unrouteddomain:* and type are triage's to produce. Lands in packages/lint, so it is not this seat's to fix.

Measured

packages/lint/src/page-walk.tswalkPageComponents descends component containers with no ancestor cycle guard, while the resolver it parallels (translatePage in packages/spec/src/system/i18n-resolver.ts) has both a cycle guard and a depth cap of 32.

properties.children is authored data typed z.array(z.unknown()), so a component whose properties.children contains itself is legal input, not a malformed document. Feeding one in:

RangeError: Maximum call stack size exceeded
❯ visit packages/lint/src/page-walk.ts:104

Observed during #13109's implementation run, not constructed after the fact.

Blast radius

walkPageComponents is the shared walk under every lint rule that traverses page components. A stack-death in it takes down, in the same process:

  • validate-component-props
  • validate-chart-bindings
  • validate-action-name-refs
  • validate-visibility-predicates
  • validate-translatable-sections
  • page-envelope-audit

…and collectExpectedEntries' object-sections pass in packages/cli, which reuses this walk rather than growing a private copy (deliberately — a duplicated walk "produced a dead rule once already (#3583)").

⇒ The failure is not scoped to one rule. It is a crash in the traversal every page-shaped rule stands on, reachable from an authored document.

⚠️ What is NOT asserted

Whether any real authored page in this repo or a consumer app contains such a cycle — not checked. This card asserts a missing guard and a measured crash on legal input, ⛔ not an incident.

⚠️ Note the asymmetry is the interesting part: the two walks were written to parallel each other, and one got the guard. That is the same "two hand-maintained copies drift" hazard PAGE_COMPONENT_COPY_KEYS' own JSDoc describes for the key list — here realised on the traversal instead. A filed sibling covers the export half of that (see Refs).

Repro

Any page whose component's properties.children contains that component.

⚠️#13109's own cycle-guard test sidesteps this by marking its fixture kind: 'html'walkPageComponents skips source-authored pages — and says so in the test. That workaround is why PR #13215 is green and is ⛔ not a fix for this card.

Re-check

git grep -n "visit" -- packages/lint/src/page-walk.ts
git grep -n "seen\|ancestor\|cycle\|MAX_NESTED_COMPONENT_DEPTH" -- packages/lint/src/page-walk.ts
git grep -n "MAX_NESTED_COMPONENT_DEPTH" -- packages/spec/src/system/i18n-resolver.ts

⛔ Reverse-check a zero against a term known present in the same file — walkPageComponents is exported from page-walk.ts, and the cap term is present in i18n-resolver.ts, so "absent in one, present in the other" is the reading, not an instrument failure.

Provenance — why the dev handed this over instead of filing

Repo-scoped REST answers 403 for an os-dev container seat, so its only dedupe channel is one targeted search_issues. It ran the positive control first and the control FAILED: a search for the card's own title returned API rate limit already exceeded for user ID 320910073, while a second query returned a clean total_count 0. That is exactly the shape where a zero and a dead pool are indistinguishable, so it refused to file blind and handed the measurement up. ⭐ That is the correct action, and this card exists because of it.

PM dedupe, run before filing: positive control first (a search for the queue-flake anchor returned 23 hits including #13158/#13193 — pool live), then the subject query, which returned 1 hit: #5511, closed, about mapPageComponents duplicating a copy-on-write walk — a different concern (duplicated walk shape, not a missing guard). ⇒ no open duplicate.

Refs

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions