Skip to content

Latest commit

 

History

History
116 lines (95 loc) · 5.68 KB

README.md

File metadata and controls

116 lines (95 loc) · 5.68 KB

Fuzzing

Coverage-guided fuzz targets for the highest-value untrusted-input surfaces. Linux + clang only (the system toolchain on macOS does not bundle the fuzzing runtime).

Targets

Target Entry point Surface
fuzz_parse ray_parse Rayfall source parser / lexer
fuzz_numparse ray_parse_i64/f64/u64_hex numeric-literal scanners (pure, no runtime)
fuzz_de ray_de / ray_de_raw binary wire-frame / journal-entry decoder
fuzz_eval ray_eval_str full parse → eval pipeline (sandboxed)
fuzz_csv ray_read_csv* CSV reader (input via memfd)
fuzz_journal ray_journal_validate / ray_journal_replay journal framing walk + replay
fuzz_parquet ray_parquet_open / ray_parquet_next footer, page, encoding and optional index parsers
fuzz_select_merge nested select vs materialized select two/three-layer queries, nullable numeric/SYM/STR columns, serialized results and errors

The journal log is a sequence of IPC frames verbatim, so the de corpus also hardens journal replay's decode step.

fuzz_eval runs untrusted programs in-process, so the escape hatches that would compromise the fuzzer — shell (.sys.exec/.sys.cmd), exit/quit, network listen/.ipc.open, and file writes (write-csv, .log.open) — are compiled out under -DRAY_FUZZING (guards in syscmd.c / system.c / builtins.c / journal.c). fuzz_csv and fuzz_journal expose their input as an anonymous in-memory file (memfd → /proc/self/fd), so there is no disk I/O.

Two accepted fuzz_eval limitations: global state (set) leaks across iterations, so a rare crash may not reproduce standalone; and loop/recursion constructs can run long, so a timeout-* artifact is only a finding when the input has no such construct.

Running

make fuzz-parse                 # 60s (default) run of one target
make fuzz-parse FUZZ_RUNTIME=0  # run until a crash / Ctrl-C
make fuzz-smoke                 # short pass over the fast targets (PR CI gate)
make fuzz-select_merge FUZZ_RUNTIME=600 # structured nested-query differential check

select_merge generates two/three-layer queries and compares them with the same queries wrapped in do to force inner materialization. It checks serialized results and error codes, allowing relative/absolute tolerance 1e-12 only for F64 aggregate values (fusion changes addition order). Row expressions, schema, order and null metadata still compare exactly. The generator also covers extreme F64 values and tables of 8,191, 8,192, 16,385 and 131,073 rows. Run RAY_FUZZ_CORES=4 make fuzz-select_merge to exercise parallel execution; the default remains serial for deterministic coverage. RAY_FUZZ_CORES is read only by this fuzz driver. The generator includes products of two independent F64 columns, squares, nested products, and scalar/grouped reductions. PR CI runs this target for one minute each in serial and four-worker modes, each with a fresh corpus and the committed seeds. This leaves time for mutations instead of spending the whole short run replaying the growing cache. Its nightly campaign uses four workers and a cached corpus.

fuzz-smoke covers parse, numparse, and de (the fast, stateless targets); eval, csv, journal, and select_merge run in the nightly fuzz-long job (.github/workflows/nightly.yml), 15 minutes each over a cached corpus.

Grown corpora live in fuzz/corpus/<target>/ (gitignored); override the root with FUZZ_CORPUS_ROOT=/path/to/corpus to use a separate working corpus. Committed starter inputs in fuzz/seeds/<target>/ are included with either root. Regenerate the working corpora from the test suite before a session:

scripts/fuzz-seed-parse.sh      # parse/eval inputs from test/rfl
scripts/fuzz-seed-frames.sh     # binary frames captured from a journaling server

Local note: AddressSanitizer + high-entropy ASLR

On some newer kernels the sanitizer runtime intermittently faults at process exit under the default ASLR entropy — unrelated to any target code (it also happens on an empty input). If you see nondeterministic SEGVs at teardown, run under reduced ASLR:

setarch -R make fuzz-parse

CI runners are unaffected. If clang auto-selects a gcc toolchain directory without libstdc++ (a partially-installed newer gcc shadowing the real one), pass the real one:

make fuzz-smoke FUZZ_LDEXTRA=-L/usr/lib/gcc/x86_64-linux-gnu/<ver>

Crash triage workflow

crash-XXXX appears (CI artifact or local run)
  1. confirm:   ./build_fuzz/fuzz_<t> crash-XXXX
  2. minimize:  ./build_fuzz/fuzz_<t> -minimize_crash=1 -runs=20000 crash-XXXX
  3. fix on the dev branch (normal PR flow)
  4. regression test:
       language-expressible bug  -> test/rfl/regress/<name>.rfl   (expr !- code)
       binary-input bug          -> keep the minimized input in fuzz/seeds/<t>/
  5. retain the minimized input under fuzz/seeds/<t>/ so the corpus never
     forgets the case

Error assertions in .rfl regression tests match the 7-byte error code (e.g. !- parse, !- domain), not the detailed message — the message is not part of the value rendered for comparison.

make fuzz-parquet FUZZ_RUNTIME=600 exercises the native Parquet footer, page, dictionary, definition-level and optional index parsers under ASan/UBSan. Its seeds are ordinary small Parquet files, including both page versions, Snappy, checksums, malformed optional indexes and fidelity edge cases. The driver consumes at most 256 batches of 257 rows per input to bound work from forged row counts. It covers decoded/pruned reader paths; it does not certify import publication, parallel scheduling or every supported producer.