Skip to content

Bound input size in validate_expression_tool / validate_style_tool to prevent heap OOM#126

Merged
mattpodwysocki merged 1 commit into
mainfrom
fix/oom-size-limits-followup
Jul 15, 2026
Merged

Bound input size in validate_expression_tool / validate_style_tool to prevent heap OOM#126
mattpodwysocki merged 1 commit into
mainfrom
fix/oom-size-limits-followup

Conversation

@mattpodwysocki

@mattpodwysocki mattpodwysocki commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #125, addressing review feedback from @Valiunia about unbounded recursion in the new style-spec-delegating validators.

The depth cap added in #125 only protects against deep adversarial input (a long linear chain). It does not protect against wide-but-shallow input - e.g. a single match expression with millions of branches, at depth ~1 - which sails past the cap untouched and gets handed whole to createPropertyExpression/validate().

Deep (stack overflow) Wide (heap OOM)
Triggered by linear nesting depth total array/object size
Caught by our try/catch? Yes - RangeError is a normal JS exception No - V8 aborts the process before JS code can run
Blast radius that one request fails entire process dies, killing every concurrent MCP session
Stopped by the #125 depth-64 cap? Yes No - depth never exceeds ~1
Stopped by this PR? Yes (already, via #125) Yes

Reproduced the wide case under a memory-constrained process (node --max-old-space-size=150) and confirmed it's worse than the stack-overflow case: heap exhaustion is not a catchable JS exception - V8 aborts the process directly (FATAL ERROR: Reached heap limit, SIGABRT), which would take down every concurrent MCP session, not just the one request.

Fixed with two layers of defense:

  • Schema-level size caps on the JSON-string form of expression (256 KB) and style (10 MB) in their Zod schemas, rejected before JSON.parse ever runs. Note: for validate_expression_tool, z.union([z.string().max(N), z.any()]) does not work, since the sibling z.any() branch matches everything, including oversized strings, and swallows them right past the length check - had to use a .refine() instead.
  • O(1) array-length / object-key-count guard, checked via .length before ever iterating into or copying the collection, alongside the existing depth guard. This covers the already-parsed object-input path too (not just JSON strings), and rejects an oversized array/object anywhere in the tree without visiting its elements.

Test plan

  • npm test - all 596 tests pass, including new regression tests for both the wide-payload and oversized-string cases in both tools (asserting fast rejection, not just non-crash)
  • npm run lint - 0 errors
  • npm run build - succeeds
  • Manually reproduced the OOM crash pre-fix (node --max-old-space-size=150 + a 2,000,000-branch match expression → SIGABRT/FATAL ERROR: Reached heap limit) and confirmed post-fix it's rejected in ~1ms with a clear error instead

🤖 Generated with Claude Code

The depth cap from the previous commit only protects against deeply-nested
input; a shallow-but-huge array (e.g. a "match" expression with millions of
branches) sails past it untouched. Confirmed by reproducing under a
constrained heap: unlike the stack-overflow case, exhausting the heap is not
a catchable JS error - it aborts the whole process (SIGABRT, "FATAL ERROR:
Reached heap limit"), taking down every concurrent MCP session, not just the
one request.

- Cap the JSON-string form of `expression`/`style` in their Zod schemas, so
  oversized payloads are rejected before JSON.parse ever runs. For
  validate_expression_tool this has to be a `.refine()` rather than `.max()`
  on the union member directly, since the sibling `z.any()` branch would
  otherwise swallow oversized strings right past the length check.
- Add an O(1) array-length/object-key-count guard (checked before iterating
  or copying) alongside the existing depth guard, so the already-parsed
  object-input path is covered too, and so a single oversized array/object
  anywhere in the tree is rejected without visiting its elements.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner July 15, 2026 15:34
@mattpodwysocki
mattpodwysocki merged commit 0306e2b into main Jul 15, 2026
2 checks passed
@mattpodwysocki
mattpodwysocki deleted the fix/oom-size-limits-followup branch July 15, 2026 15:39
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.

2 participants