Repository navigation
fix(parser): reject over-deep and oversized input instead of overflowing the stack - #295
Merged
Merged
Conversation
…wing the stack A 20 KB program of 4,000 nested calls overflowed the parser's stack and aborted the whole server. Recursion over client input is now bounded: - Terms and type expressions nest at most storage.performance.max_nesting_depth levels (default 128, capped at 1024); deeper input is a validation error. - Rule and query bodies hold at most 512 predicates plus atom arguments, since the query plan and every pass over it nest one level per element. - Stratification's Tarjan walk is iterative, so long rule chains no longer recurse once per link. - Server runtime threads and the incremental worker get 16 MiB stacks, the budget the limits above are measured against. - Persisted rules nest past serde_json's 128-level limit, which made the WAL unreadable and the engine unable to start; deep records now decode on an engine-sized stack, bounded by the parser's ceiling.
jsam
force-pushed
the
fm/fix-parser-depth
branch
from
October 5, 2026 07:16
cbac248 to
54f5173
Compare
jsam
marked this pull request as ready for review
October 5, 2026 08:06
This was referenced Oct 5, 2026
jsam
added a commit
that referenced
this pull request
Oct 6, 2026
Three targets run the server's own code through a test-support seam (src/fuzzing.rs) on an engine-sized stack at the nesting ceiling: iql_statement, iql_program (split, parse, bind params, authorize) and ws_client_frame (pre-auth decode, id probe, request classification). Beyond crashes they check that a persistent rule reloads unchanged and that a request run read-only holds only queries. fuzz/seeds.py seeds them from the example programs, every /ws frame type and the #295 deep-nesting and oversized-body cases; fuzz/run.sh (make fuzz) runs a campaign that keeps going past findings.
jsam
added a commit
that referenced
this pull request
Oct 6, 2026
…n-finite float literals (#301, #397) (#405) * test: cargo-fuzz targets for the IQL parser and /ws client frames (#301) Three targets run the server's own code through a test-support seam (src/fuzzing.rs) on an engine-sized stack at the nesting ceiling: iql_statement, iql_program (split, parse, bind params, authorize) and ws_client_frame (pre-auth decode, id probe, request classification). Beyond crashes they check that a persistent rule reloads unchanged and that a request run read-only holds only queries. fuzz/seeds.py seeds them from the example programs, every /ws frame type and the #295 deep-nesting and oversized-body cases; fuzz/run.sh (make fuzz) runs a campaign that keeps going past findings. * docs(storage_engine): unlink the private revisions module from public docs make doc-check failed on main since #390: rustdoc denies a public doc linking to a private item. * fix(parser): refuse float literals beyond f64's range (#397) A persistent rule holding 1e400 (or inf, NaN) was stored with a JSON null, which failed to decode on the next start: WalUnreadable, and the engine would not start. Every float literal now goes through float_literal: decimal syntax only, and a literal beyond f64's range is a validation error. Rust's inf/NaN spellings are identifiers again. Negating i64::MIN in a literal no longer overflows. Found by the iql_statement fuzz target; its inputs are kept in fuzz/regressions/.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An authenticated client of any role could abort the whole server with one frame. A 20 KB program of 4,000 nested calls (
+nr(abs(abs(...abs(1)...)))) overflowed the tokio worker's 2 MiB stack in the recursive term parser. Rust aborts the process on a stack overflow, so every connection and subscription in the cell went down with it.The audit of other recursion over client input found more of the same:
list[list[...]]had no depth limit.serde_json's 128-level limit on reload, soStorageEngine::newfailed withWalUnreadableand the engine would not start.Fix
storage.performance.max_nesting_depthlevels. The default is 128 and the cap is 1024; 0 resets to the default. Each call, arithmetic operator, parenthesized group,list[...]and record adds a level.validationerror:Expression nesting exceeds the limit of 128 levels (storage.performance.max_nesting_depth).Rule body is too large: N elements (body predicates plus body-atom arguments); the limit is 4096). The widest rule in the repo's examples is about 21.ENGINE_THREAD_STACK_BYTES). This is the budget the limits were measured against. The extra space is virtual and only committed when used. The server's startup, including WAL and catalog replay, also runs as a runtime task, so it gets the same stack.storage::nested_json. JSON within serde_json's limit takes the old path. Deeper records, up to a bound derived from the nesting ceiling, decode on an engine-sized stack using serde_json'sunbounded_depthfeature.Evidence
Live, release build, over
/ws:+nr(abs^4000(1))(the audit's repro)has overflowed its stack,fatal runtime error: stack overflow, abortingvalidationin 3 ms, server uplist[nesting 100,000validation, 13-560 ms each, server upCheap query
?r(X), three alternating rounds of 300 queries: p50 was 0.137-0.145 ms on this branch and 0.138-0.151 ms on main. That is the same within noise.Tests:
tests/nesting_depth_tests.rschecks deep input on every statement form, that each limit is inclusive (at the limit evaluates, one past is refused), the configured limit up to the ceiling, type expressions, and a restart with rules at both limits. It runs on a runtime set up like the server's.deep_nesting_is_refused_not_overflowed, generates nested calls, groups and operator chains (seeded around the limit, at 4,000, and up to 20,000) in five statement forms.nested_jsonbound tests.Follow-ups filed while landing: #301 (fuzz harness) and #302 (HNSW recall test flake). The CI sharing-test flake is tracked in #289.