Zinc Next: fuzzing

ZN-147 (and ZN-433 for fuzz_zpk). Five libFuzzer targets (tests/fuzz), built with AddressSanitizer and UndefinedBehaviorSanitizer in their own build tree (build-fuzz).

TargetWhat it feedsWhat must hold
fuzz_parsearbitrary bytes to the lexer and parserno crash, hang or stack overflow; diagnostics are the only refusal
fuzz_checkarbitrary text as main.ts through loading, checker, lowering, optimizer, reference counting, the IR verifier, ZBC emission and its verifierno crash or hang; a program the checker accepts lowers to IR and ZBC that verify (a failed verify aborts the run)
fuzz_zbcarbitrary bytes to zbc::decode and zbc::verifyno crash; a file that decodes and verifies encodes back to the same bytes (one spelling per module)
fuzz_nativean export signature (first line) and argument bytesthe signature parser refuses what it does not know, the registry refuses a malformed export, a registered export is called with arguments decoded from the bytes
fuzz_zpkarbitrary bytes as a ZPK pack (src/zpk); the entries’ checksums are set to their mutated bytes before reading, since anyone can compute them, so the lz4 and zstd decoders and the zstd dictionary see the fuzzer’s bytesan index that opens finds each of its names at its own row; every entry reads to its stated size or is refused with a message (rows over 16 MB are skipped: the asset manager checks sizes against its budget first)

Running

tools/fuzz <parse|check|zbc|native|all> [seconds=60]

The tool configures build-fuzz with Homebrew’s clang (brew install llvm; set LLVM=<prefix> to use another) or, on a Mac without it, with Apple’s clang, which instruments (-fsanitize=fuzzer-no-link) but ships no libFuzzer runtime: tools/libfuzzer builds one once from the pinned compiler-rt 19.1.7 source (checksum checked) into ~/.zinc/cache/libfuzzer-19.1.7, and CMake links it through -DZN_LIBFUZZER=<archive>. It builds the targets, seeds build-fuzz/corpus/<target> from tests/golden, the checker’s programs, tests/conformance, the compatibility ZBC files and the packs zpk_test seeds writes, and runs the target. New corpus entries stay in build-fuzz/corpus. The T2 gate tests/t2/fuzz.sh runs 60 seconds per target and is skipped without a libFuzzer clang (never on macOS since ZN-433).

Long run: tools/fuzz all 3600 (or one target for a night with tools/fuzz check 28800; -- -jobs=4 -workers=4 after the seconds runs several processes). A crash leaves tests/fuzz/crashes/<target>-<kind>-<hash>.

When a crash is found

  1. Reproduce: build-fuzz/fuzz_<target> tests/fuzz/crashes/<file> (the report names the frames).
  2. Fix the cause where all callers go through (the checker, the decoder), not in the harness.
  3. Copy the input to tests/fuzz/regress/<target>-<what>.<ts|zbc>: tests/t0/fuzz_regress.sh runs every file there through zinc and fails on a crash, a signal or a hang (timeout 20 s). A file that must be refused rather than merely survived gets a line at the end of that script.
  4. Delete the artifact from tests/fuzz/crashes.

Found and fixed (first runs)

TargetInputCauseFix
fuzz_zbczbc-class-flags.zbc: a class with flag bits the encoder never writesthe decoder accepted any flag byte and dropped the unknown bits, so two files meant one moduledecoding refuses flags above 3 (invalid class flags)
fuzz_checkcheck-verify-quadratic-{1,2,3}.ts: regular expression tests, 1.6 to 4 KB, 1 to 3 s in a release build, a minute with sanitizersthe IR verifier computed dominators as an n x n bit matrix to a fixed point: quadratic in the blocks of a functionCooper, Harvey and Kennedy over a reverse postorder, dominance by the intervals of a walk of the dominator tree; 3.0 s becomes 0.14 s
fuzz_checkcheck-lvalue-untyped.ts: static { C.n$ = 5 } and a name with a stray byteassigning to a member of an object the checker had not typed read the type table at the “no type” indexlvalue and primMember leave an untyped object alone

Runs (10 minutes each, ASan and UBSan, seeded corpus)

TargetResult
fuzz_parse1.28 M runs, clean
fuzz_zbc1.00 M runs, clean
fuzz_native54 M runs, clean
fuzz_checkone failure after the three fixes above: regress/check-dup-names-invalid-ir.ts, a program with nested and top-level functions and classes of the same names (early, later, LIMIT, Late, make) is accepted by the checker and lowered to invalid IR (setfield value type differs from the field type). Open, not fixed yet: ZN-147 stays in Review. The compiler reports an internal error and exits 3 (no crash), which is what tests/t0/fuzz_regress.sh accepts for now.

Edit this page on GitHub ↗