Skip to content

test: cargo-fuzz harness for the IQL parser and /ws frames; refuse non-finite float literals (#301, #397) - #405

Merged
jsam merged 3 commits into
mainfrom
fm/fuzz-harness
Oct 6, 2026
Merged

jsam merged 3 commits into
mainfrom
fm/fuzz-harness

Conversation

@jsam

@jsam jsam commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Closes #301. Closes #397.

What

Fuzz harness (#301). fuzz/ is a cargo-fuzz crate with its own workspace, like perf-gate. It has three targets. Each runs the server's own code through a test-support seam (src/fuzzing.rs), on a thread with the server's 64 MiB engine stack, at the highest nesting limit an operator may configure (1024).

target input checked beyond "no panic, no overflow, no abort"
iql_statement one statement through parse_statement a persistent rule passes validate_rule and reloads unchanged from its catalog/WAL JSON (nested_json)
iql_program an execute program, optionally \0 + params JSON split, parse, $param binding and authorize_statement for every role, as the handler does; a program the connection runs read-only (is_query_program) holds only queries; the rule-program parser
ws_client_frame one /ws text frame the pre-auth ClientFrame decode agrees with probe_request_id; a decoded frame re-encodes to itself; Request::from_text's reply echoes the frame's id; an immediate reply encodes; a request classified Access::Shared holds only queries
  • Seeds. fuzz/seeds.py generates them; they are not committed. They cover all 1,170 example programs and their statements, one frame of every client type (with params and expect_*), the fix(parser): reject over-deep and oversized input instead of overflowing the stack #295 cases, and past findings from fuzz/regressions/. The fix(parser): reject over-deep and oversized input instead of overflowing the stack #295 cases are nested calls, groups, operator chains, list[...] and records in every statement form at depths 1, 127-129, 1023-1025 and 4,000, plus bodies of 4,095-4,097 elements. iql.dict and ws_frame.dict are libFuzzer dictionaries.
  • Running. fuzz/run.sh SECONDS [TARGET...] (make fuzz) builds with ASan and debug assertions, then runs the targets in parallel with FUZZ_FORKS workers each. Workers keep going past a crash, timeout (10 s) or OOM (2 GiB) and list every finding at the end. The script uses nightly when installed, otherwise stable with RUSTC_BOOTSTRAP=1.
  • Docs. fuzz/README.md, plus a Fuzzing section and a make fuzz row in TESTING.md. make fmt and make fmt-check now cover fuzz/.

P0 found and fixed (#397). In the first local campaign, iql_statement found that a float literal beyond f64's range (1e400), or Rust's inf/NaN spellings, parses into a non-finite float. JSON has no form for those, so a persistent rule holding one was written to the WAL as null. On the next start StorageEngine::new failed with WalUnreadable and the engine did not start. Any editor could cause this with one statement. All float literals in parser, statement::parser (terms, negatives, vectors, arithmetic) and type refinements now go through one function, parser::float_literal. It accepts decimal syntax only and refuses an out-of-range literal with validation. While there, negating i64::MIN in a literal no longer overflows (checked_neg). The four crash inputs are kept in fuzz/regressions/iql_statement/. tests/float_literal_tests.rs reproduces the restart failure on main and passes here. The syntax reference says float literals must be finite (docs bundle regenerated). The SDKs already refuse non-finite floats before writing IQL.

Also: make doc-check was red on main. Since #390, a public doc in storage_engine/snapshot.rs links the private revisions module. This PR unlinks it.

Campaigns

where duration workers result
devbox, before the fix 15 min 2 per target iql_statement: 4 crashes, all #397 (907k execs, cov 6227). iql_program: 590k execs, cov 6740, none. ws_client_frame: 730k execs, cov 5918, none.
devbox, with the fix 20 min 2 per target running
benchmark host 1 h 8 per target queued behind the host's shared lock (8 perf-gate jobs ahead)

Local checks (GitHub CI is paused, #396)

Run on the devbox under MemoryMax=16G, CARGO_BUILD_JOBS=8, after rebasing on d5df1ba:

check result
make fmt-check (engine, perf-gate, fuzz) pass
make lint (clippy, all targets, all features, -D warnings), plus clippy on the fuzz crate pass
make doc-check pass (failed on main before this PR, see above)
make test-budget: make unit-test (every workspace test, including scenarios and oracle) 4,468 passed, 0 failed; only the known XFAILs (#314-#320)
make test-budget: run_snapshot_tests.sh --debug --skip-build 1,148 passed, 0 failed, 7 skipped
make test-budget: budget check unit, scenarios and oracle_pr ok; component 329 s and specs 79 s over. These match the devbox numbers recorded when #403 calibrated the budgets (component 330-370 s, specs ~92 s on a loaded devbox; CI 147-185 s). The new test binary takes 0.14 s.
cargo fuzz build -O + replay of the 4 crash inputs pass
SDK suites not run: no SDK or wire change, and both SDKs already refuse NaN and infinities

jsam added 3 commits October 6, 2026 06:15
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

make doc-check failed on main since #390: rustdoc denies a public doc
linking to a private item.
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/.
@jsam
jsam merged commit 5d599c5 into main Oct 6, 2026
@jsam
jsam deleted the fm/fuzz-harness branch October 6, 2026 06:29
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.

fix(parser): a float literal beyond f64's range in a persistent rule stops the engine from starting Add a fuzz harness for the parser and WebSocket frames

1 participant