Skip to content

fix(fuzz): retain encrypted private campaign evidence - #47

Merged
YUZHEthefool merged 1 commit into
mainfrom
fix/fuzz-private-evidence
Oct 30, 2026
Merged

YUZHEthefool merged 1 commit into
mainfrom
fix/fuzz-private-evidence

Conversation

@YUZHEthefool

Copy link
Copy Markdown
Member

Problem

Scheduled fuzzing publishes only an opaque HMAC candidate ID. Raw libFuzzer inputs, stack traces and QEMU evidence were kept in RUNNER_TEMP and discarded with the job, so the open candidates in #40 and #41 cannot be reproduced or root-caused even by maintainers.

Outcome

Retain failure-only plaintext evidence inside the runner, seal it with age, and publish only ciphertext. The public candidate schema, issue format and HMAC IDs are unchanged.

Closes #46.

Design and scope

  • scripts/fuzz/private_evidence.py run records the exact command, revision, source hashes, source patch, guest-image hashes, tool versions, exit status and raw libFuzzer output in an exclusive private directory.
  • seal streams a tar/gzip archive directly into age; only campaign.tar.gz.age is uploaded, under fuzz-evidence-<job-index>. Plaintext never enters a cache or artifact.
  • QEMU failures retain the submitted program, private guest image, disk, serial output, QEMU stderr and outcome.json. The receipt distinguishes crash, boot_timeout, guest_hang and execution_error; an execution error alone is not a kernel defect.
  • crash-triage --private-index writes a mode-0600 index mapping public candidate IDs to retained inputs. The public report remains exactly two fields and never embeds paths or payloads; the index cannot be written into the public output directory.
  • The campaign fails closed when FUZZ_EVIDENCE_RECIPIENTS is missing or malformed, or when encryption/upload fails, so the reporting job cannot create a pointer without evidence.
  • The QEMU libFuzzer per-input timeout is now -timeout=1200, above the executor's 900-second guest bound, in both the workflow runner and the local Makefile campaign targets.
  • Documentation covers recipient generation, the repository variable, decryption and the fact that candidate IDs deduplicate inputs, not root causes.

Security properties

  • The plaintext evidence tree is created mode 0700 with an exclusive-create check; the triage index is 0600.
  • seal refuses symlinks, hard links, special files, incomplete runs, an existing output, and archives over 1 GiB.
  • Encryption failure or a non-age output is rejected before os.replace.
  • The HMAC fingerprint key is removed from the environment before the fuzzer or dependency build scripts run.
  • Repository variable FUZZ_EVIDENCE_RECIPIENTS is already configured on this repository and preflight succeeds.

Validation

Check Result
python3 -m pytest scripts/tests -q PASS: 198 passed (9 new evidence tests)
bash scripts/ci/entrypoint.sh tools PASS: fuzzer-tests, fuzzer-clippy, fuzz-features, executor
make lint PASS (including ABI oracle)
userspace/fuzzer tests PASS: 15 unit + 1 smoke + 7 executor + 4 scaffold
userspace/fuzzer Clippy -D warnings + release crash-triage build PASS
userspace/nilix-syz-fuzzer tests PASS: 34
fuzz qemu_executor::tests PASS: 5, including failure receipt retention
New evidence tests PASS: real age round-trip, tamper rejection, symlink/hardlink refusal, size limit, fail-closed encryption, non-zero campaign exit
FUZZ_EVIDENCE_RECIPIENTS preflight PASS against the configured repository variable
git diff --check (cr-at-eol) PASS

The real-guest QEMU smoke/campaign was not run locally; the PR relies on the existing qemu-fuzz CI gate for the guest path.

Compatibility and limitations

Run scheduled campaigns through a private evidence wrapper that records
the exact input, source identity, tool versions and raw libFuzzer output,
then streams the archive into age. Upload only ciphertext; keep the
public candidate schema unchanged.

Retain failure-only QEMU execution evidence with an outcome receipt and
map public candidate IDs to inputs in a private crash-triage index. Fail
the campaign closed when recipients, encryption or upload fail.

Set the QEMU libFuzzer per-input timeout above the 900-second guest bound
in both the runner and local Makefile campaigns.

Refs #46
@github-actions github-actions Bot added documentation Improvements or additions to documentation area: userspace Ring-3 programs, libc compatibility, and userspace support area: fuzzing Fuzz targets, corpora, executors, triage, and KCOV workflows area: tests Unit, hosted, runtime, QEMU, SMP, stress, and conformance tests area: tooling/CI Build system, scripts, hooks, automation, and repository tooling dependencies Dependency or toolchain updates size: L 500-999 review lines, excluding generated/binary artifacts bug Something isn't working labels Oct 30, 2026
@YUZHEthefool
YUZHEthefool merged commit ffaf9f8 into main Oct 30, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: fuzzing Fuzz targets, corpora, executors, triage, and KCOV workflows area: tests Unit, hosted, runtime, QEMU, SMP, stress, and conformance tests area: tooling/CI Build system, scripts, hooks, automation, and repository tooling area: userspace Ring-3 programs, libc compatibility, and userspace support bug Something isn't working dependencies Dependency or toolchain updates documentation Improvements or additions to documentation size: L 500-999 review lines, excluding generated/binary artifacts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Tracking]: retain encrypted private fuzz evidence for candidate diagnosis

1 participant