Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Klaus Landsdorf <kl@iniationware.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
5.6 KiB
Fuzz crash corpus
The corpus checked in under fuzzing/*.Fuzz.Corpus and fuzzing/*/Assets/Repo runs on every pull
request. The full crash corpus holds about 22k crash, timeout and slow inputs that earlier
fuzzing campaigns found. It is too large to replay on every pull request, so it lives on the orphan
branch fuzz-corpus of this repository and the nightly workflow replays it.
History: the Azure DevOps secure file
Until the move to GitHub Actions, the corpus was the Azure DevOps secure file
FuzzingArtifacts.zip (Pipelines → Library → Secure files).
The since-removed Azure test template (.azurepipelines/test.yml) downloaded it with DownloadSecureFile@1,
but only for the Opc.Ua.Encoders.Fuzz.Tests matrix entry. It extracted the file over
fuzzing/Opc.Ua.Encoders.Fuzz.Tests/ and re-published it as the drop artifact whenever the job
failed.
It was treated as private, but it was not private in practice:
- the
dropartifacts could be downloaded anonymously; FuzzCrashAssetsprinted a base64REPRODUCERline into anonymously readable build logs for every input that still threw.
All inputs date from April to June 2024, and current master replays every one of them without a
test failure. The archive was therefore published unchanged as the first commit of fuzz-corpus.
Its SHA-256 is 85198e46518217241de5dc1eefcb5a4e40b18e62df1a2da1f333c3a39fe1d61b, and it was
recovered from the drop artifact of Azure build 18397.
The master378 and 1.x release pipelines keep their own copy of test.yml and still use the
secure file, so do not delete it from the Azure Library.
Branch layout
fuzz-corpus shares no history with master and is never merged into it. Each top-level folder
mirrors a fuzzing/<project> folder and is copied over it unchanged:
fuzz-corpus
├── .gitattributes # "* binary": inputs are raw bytes, never normalized
├── README.md
└── Opc.Ua.Encoders.Fuzz.Tests/ # overlays fuzzing/Opc.Ua.Encoders.Fuzz.Tests/
└── Assets/
├── crash-<sha1> # 22,267
├── slow-unit-<sha1> # 25
└── timeout-<sha1> # 1
FuzzTargetTestsBase enumerates Assets/crash*.*,
timeout*.* and slow*.* and replays each input through every fuzz target:
- A crash input must not throw any more. The one exception is an encoding-fidelity finding on an
input outside
Assets/Repo, which is reported but does not fail the test. - Timeout and slow inputs must finish within the watchdog.
- Every input also runs under the resource oracles. An allocation,
time or limit finding always fails the test and, for an input outside
Assets/Repo, prints noREPRODUCERbytes, so a resource-abuse regression in a private input stays out of the public log.
Other areas, such as Opc.Ua.Network.Fuzz.Tests/Assets/…, can be added as further top-level
folders. The nightly matrix must then also run those projects (see below).
How CI replays it
The crash-corpus job in .github/workflows/nightly.yml runs
on every scheduled and manual nightly run. It:
- checks out the commit pinned in the job's
FUZZ_CORPUS_COMMITinto.fuzz-crash-corpus. It is a plainactions/checkoutof this repository, with no secrets or environment involved; - verifies the checked-out SHA, copies each top-level folder over
fuzzing/<folder>and records the commit and file count in the job summary; - runs
Opc.Ua.Encoders.Fuzz.Testson every non-macOS profile through the sharedrun-dotnet-testsaction and uploadscrash-corpus-results-*.
The nightly summary fails if the job was skipped. The job narrows its matrix with
-OnlyProject 'fuzzing/Opc.Ua.Encoders.Fuzz.Tests/Opc.Ua.Encoders.Fuzz.Tests.csproj' in the
discover job, so widen that when the corpus gains other areas.
The pin is the point: pushing to fuzz-corpus changes nothing until a reviewed pull request to
master moves FUZZ_CORPUS_COMMIT.
Replaying it locally
git fetch origin fuzz-corpus
git worktree add ../fuzz-corpus origin/fuzz-corpus
Copy-Item ../fuzz-corpus/Opc.Ua.Encoders.Fuzz.Tests/* fuzzing/Opc.Ua.Encoders.Fuzz.Tests/ -Recurse -Force
dotnet test fuzzing/Opc.Ua.Encoders.Fuzz.Tests -c Release -p:CustomTestTarget=net10.0
git clean -fdx fuzzing/Opc.Ua.Encoders.Fuzz.Tests/Assets
.gitignore does not exclude the overlaid files, so the final git clean is what keeps them out
of a commit. Assets/Repo and Assets/ForkMessages are tracked and are not touched.
Adding inputs
- Unfixed crashes are not added here. An input that still crashes the stack is a potential vulnerability. Report it privately as described in SECURITY.md and fix it first.
- Once the fix has shipped, open a pull request against
fuzz-corpusthat adds the input as<project>/Assets/crash-<sha1>,timeout-<sha1>orslow-<sha1>, keeping libFuzzer's content-hash name. - After it merges, open a pull request against
masterthat setsFUZZ_CORPUS_COMMITinnightly.ymlto the new commit. That run is the first to replay the new inputs. - A minimized input that should gate every pull request also belongs in
fuzzing/<project>/Assets/Repo/onmaster, where it is checked strictly.
fuzz-corpus should be covered by a branch ruleset that requires pull requests and blocks force
pushes and deletion. Force pushes are the risk: a pinned commit that is no longer reachable from
any branch may eventually be garbage-collected, and the nightly checkout would then fail.