ADR-0311: libFuzzer harness expansion — fuzz_yuv_input + fuzz_cli_parse¶
- Status: Accepted
- Date: 2026-05-05
- Deciders: lusoris, Claude
- Tags: security, build, ci, docs, fork-local
Context¶
ADR-0270 landed the libFuzzer scaffold under core/test/fuzz/ with one initial harness (fuzz_y4m_input). That harness already paid for itself: a 60-second smoke run surfaced a heap- buffer-overflow in y4m_convert_411_422jpeg (fixed in PR #357). The scaffold was always intended as the first harness — the meson plumbing, the nightly workflow, and the corpus-management convention are deliberately generic so additional parsers can land in the same shape.
Two sibling parsers in core/tools/ are the obvious next targets:
yuv_input.c— the headerless raw-YUV reader. Where Y4M parses a header, YUV is unparsed and the caller supplies dimensions; the interesting fuzz surface is the chroma-subsampling arithmetic inyuv_input_fetch_frame(the same shape that surfaced the 411 bug in the Y4M path) plus the truncated-fread / short-read branch indst_buf_sz-sized reads. Cannot be reached through the existing Y4M harness becausevideo_input_openonly registers the Y4M vtbl.cli_parse.c— the CLI argument parser. Attacker-reachable whenever a host script wrapsvmafand forwards untrusted argv (filenames,--featurepayloads,--modelcolon-delimited strings). The colon-delimited sub-parsers (parse_model_config/parse_feature_config) runstrsepchains over heap-duplicated argv strings — classic format-string / overrun shape.
Both are pure parser surfaces, both are zero-GPU / zero-DNN, and both fit the existing scaffold's LLVMFuzzerTestOneInput(uint8_t *, size_t) contract. The Research-0083 digest enumerates the wider candidate set and ranks complexity / risk.
Decision¶
We will land two additional harnesses (fuzz_yuv_input.c, fuzz_cli_parse.c) under core/test/fuzz/, register them in the existing meson.build opt-in (-Dfuzz=true), seed each with 6 hand- crafted inputs covering branch-significant shapes, and add both to the nightly .github/workflows/fuzz.yml matrix at 60 s/harness/night. fuzz_cli_parse uses -Wl,--wrap=exit to intercept the usage() / exit(1) path via a setjmp/longjmp shim so a single bad input does not terminate the fuzzer process. ADR-0270 stays the parent ADR for the scaffold itself; this ADR is a strict expansion.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
yuv_input + cli_parse (chosen) | Both reachable from the public CLI, both are pure C, both fit the scaffold verbatim. | The cli_parse harness needs a __wrap_exit linker shim because usage() calls exit(1); that is a small but non-trivial bit of harness machinery. | Highest risk-weighted coverage delta for the LOC budget — cli_parse is the actual attacker entry point on a wrapped CLI; yuv_input is the one parser surface Y4M does not reach. |
output.c (XML / JSON / CSV / SUB writer) | Output formatting routes through user-controllable model / feature names. | The writer is fed libvmaf-internal state, not raw bytes; harness has to fabricate a valid VmafFeatureCollector first — more harness scaffolding than fuzz target. | Harness would mostly exercise the printf-format paths the unit tests already cover at higher fidelity. Defer until the unit-test gate finds gaps. |
dnn/ ONNX model-load path | Tiny-AI surface is a real attacker-reachable parser (loads .onnx from disk). | ORT does its own internal fuzzing (Microsoft / Google fuzz ORT continuously); duplicating that gives diminishing returns and pulls a heavyweight dep into the build. | Not worth the build-time cost on the fork's CI; revisit once we ship a fork-specific ONNX preflight (allowlist sniff, sidecar verifier). |
vmaf_per_shot / vmaf_roi JSONL parsers | Real fork-local file readers, no upstream coverage. | Both are line-oriented and short — the existing unit tests cover the parser branches; coverage delta is small. | Tracked as a follow-up if the nightly job catches regressions there. Not worth the new-harness LOC for the current sweep. |
| Defer expansion until OSS-Fuzz onboards | Frees engineering time. | Scorecard Fuzzing stays at one-target tier; cli_parse and raw-YUV bugs remain undetected. | Rejected — the scaffold is reusable, the per-target cost is ~150 LOC, and the YUV path mirrors the same chroma-subsampling shape that already produced one real bug in the Y4M path. |
Consequences¶
- Positive:
- Coverage of every pure parser surface in
core/tools/— Y4M, raw YUV, and CLI argv. Future Y4M / YUV chroma-conversion bugs (the same shape as the 411 OOB write) get caught in the raw path too, not only the Y4M front door. - The
cli_parseharness exercisesparse_model_config/parse_feature_configcolon-tokenisation, which currently has no unit-test coverage. - The 60-second smoke run on the seed corpus already surfaced a real
assert(long_opts[n].name)failure inerror()(core/tools/cli_parse.c:250): handlers for the long-only optionsARG_THREADS/ARG_SUBSAMPLE/ARG_CPUMASKcallparse_unsigned(optarg, 't' / 's' / 'c', argv[0])with a hardcoded short-option char that is not registered inlong_opts[]. Any abbreviated--threads/--subsample/--cpumaskinvocation with a non-numeric argument trips the assertion. Captured reproducer parked undercore/test/fuzz/cli_parse_known_crashes/cli_threads_abbrev_assert.argv; the fuzzer harness carries an early-reject filter for the--th*/--s*/--c*token prefixes so the nightly job stays green until the fix lands. Tracked as a follow-up bug; the fix is a one-line change at each call site (replace the hardcoded char withARG_THREADS/ARG_SUBSAMPLE/ARG_CPUMASK). - Scorecard
Fuzzingcheck moves from "≥ 1 target" toward the "≥ 3 targets" tier. - Negative:
- Three parallel matrix legs in the nightly workflow instead of one (≈3 minutes of runner time per night vs ≈5 before; the per-harness budget drops from 5 min to 60 s to keep the total bounded, see
.github/workflows/fuzz.yml). fuzz_cli_parse's__wrap_exitlinker shim is GNU-ld / lld-specific; documented in the harness comment block. Macs shipping Apple ld would need an-Wl,-undefined,dynamic_lookupfallback, but the fuzz CI lane runs only onubuntu-latest.- Neutral / follow-ups:
- A pre-commit hook that requires every new
tools/parser to be matched by a fuzz harness is not in scope here; tracked as a future automation when at least 5 parsers ship harnesses. - The Research-0083 digest queues
output.cand the tiny-AI preflight as the next two candidates if the current sweep finds bugs.
References¶
- ADR-0270 — parent scaffold ADR.
- Research-0083 — surface survey, complexity ranking, and Scorecard delta.
- docs/development/fuzzing.md — operator runbook (updated with the two new harnesses).
- libFuzzer (LLVM).
- OSSF Scorecard
Fuzzingcheck. - Source:
req— paraphrased from the user's 2026-05-05 request to expand the libFuzzer scaffold to coveryuv_inputandcli_parseas the natural next harnesses after the Y4M wedge target landed.