mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 07:02:16 +08:00
ae34b02938
# Description
Broadens fuzz coverage across the highest-value untrusted-input surfaces
in the stack, refactors the per-area scaffolding so adding a new fuzz
area is a copy-paste-replace operation, and integrates everything into
`UA.slnx`, `Fuzzing/Fuzzing.md`, and `Docs/README.md`.
**Totals**: 143 files, +4961 / −289. Source plan:
`plans/27-fuzzing-coverage-expansion.md` (added in this PR).
### Shared scaffolding (refactor, no behaviour change)
* Extract generic NUnit replay harness from
`Encoders/Fuzz.Tests/EncoderTests.cs` into
`Fuzzing/common/Fuzz.Tests/{FuzzTargetTestsBase,FuzzTargetFunction,TestcaseAsset,TestAssetUtils}.cs`.
* Each area's `Fuzz.Tests` becomes a thin subclass binding its own
`FuzzableCode` type.
* Hardcoded testcase-suffix arrays replaced with `Testcases.*` directory
discovery at runtime.
* New `Fuzzing/scripts/fuzz-menu.ps1`: dynamic target menu enumerating
`FuzzableCode` statics via reflection — no more hardcoded target lists
in `libfuzz.bat` / `aflfuzz.sh`.
### Encoders area hardening
* **Bug fix**: `AflfuzzXmlEncoder` was re-encoding via `JsonEncoder`;
now uses `XmlEncoder` (the `LibfuzzXmlEncoder` variant was already
correct).
* Add idempotent JSON / XML targets mirroring the existing Binary
idempotent pattern.
* Add compact-JSON encoder option coverage (`JsonEncoderOptions.None`
alongside `Verbose`).
* New `FuzzableCode.BuiltInTypes.cs` (48 targets): `NodeId`,
`ExpandedNodeId`, `Variant`, `ExtensionObject`, `DataValue`,
`DiagnosticInfo`, `QualifiedName`, `LocalizedText` × Binary/JSON/XML
decoders.
* Enrich `Encoders.Testcases.cs` seed corpus with `BrowseRequest` +
`WriteRequest`.
* New `Fuzzing/dictionaries/binary.dict`.
### String parsers (folded into Encoders area)
* New `FuzzableCode.Parsers.cs` (12 targets): `NodeId.Parse`,
`ExpandedNodeId.Parse`, `RelativePathFormatter.Parse`,
`QualifiedName.Parse`, `NumericRange.Parse`, `Uuid` round-trip (both
afl-string and libFuzzer-span signatures).
* New `Fuzzing/dictionaries/nodeid.dict`.
### Certificates / CRL / ASN.1 (NEW area)
* New `Fuzzing/Certificates/{Fuzz,Fuzz.Tests,Fuzz.Tools}` referencing
`Opc.Ua.Security.Certificates` + `Opc.Ua.Core`.
* Targets (20 total): `X509CRL(byte[])` → `EnsureDecoded`,
`X509SubjectAltNameExtension` / `X509AuthorityKeyIdentifierExtension` /
`X509CrlNumberExtension` raw-ASN.1 decoders,
`PEMReader.ImportPublicKeysFromPEM` / `ImportRsaPrivateKeyFromPEM`,
`Pkcs10CertificationRequest`, `AsnUtils.ParseX509Blob` + `AsnReader`
sequence/integer helpers.
* `Certificates.Testcases.cs` builds an RSA cert / CRL / CSR with the
existing builders and emits raw DER + PEM seeds.
* New `Fuzzing/dictionaries/asn1.dict` (DER tags, context-class tags,
length-form sentinels, OID prefixes).
### Network area via `Opc.Ua.Bindings.Pcap` (NO Core changes, NEW area)
* New `Fuzzing/Network/{Fuzz,Fuzz.Tests,Fuzz.Tools}`, multi-targeted
`net8.0;net9.0;net10.0` to match the pcap binding matrix (excluded from
net48/net472).
* **Stateless targets**: `OpcUaFrameParser.Process` (TCP segment → UA-SC
chunk splitter), `TcpStreamReassembler.Process` (raw TCP reassembly),
`OfflineSecureChannel.ReadChunk` (UA-SC symmetric decrypt + verify via
`UaSCUaBinaryChannel.ReadSymmetricMessage` → every security profile
covered through the binding), `ServiceCallReassembler.Push` (chunk →
decoded service call).
* **Stateful libFuzzer-only targets**: `MockServerReplay` /
`MockClientReplay` drivers in a separate target list so cheap stateless
targets dominate throughput.
* Deterministic test-only `ChannelKeyMaterial` via
`SecurityPolicies.None`.
* `Network.Testcases.cs` emits synthetic Hello/Ack/Err/Rhe/MSG seeds via
`LoopbackFrameBuilder`. Live handshake recorder is out of scope for this
PR (degraded mode documented).
* `Network.Fuzz` host does **not** instantiate `NicCaptureSource` — the
AFL/libFuzzer process never opens raw sockets.
* New `Fuzzing/dictionaries/uasc.dict` (MSG/OPN/CLO/HEL/ACK/ERR/RHE
magic + chunk-type bytes + security-policy URI prefixes) and `tcp.dict`
(Ethernet/IPv4/TCP framing markers).
### `Opc.Ua.Core` UA-SC parse seam (complements 4a)
* New `Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` — `internal
static` helpers for the pre-crypto, pre-auth chunk surface NOT covered
by the pcap binding: `TryParseChunkHeader`, `ReadHelloMessage`,
`ReadAcknowledgeMessage`, `ReadErrorMessage`, `ReadReverseHelloMessage`,
`ReadAsymmetricMessageHeader`.
* **No production-code refactor** (Strategy B): the new internal helpers
are an independent parse seam, not a refactor of the entangled private
channel methods. Existing `ProcessHelloMessage` /
`ProcessReverseHelloMessage` / asym-header decode paths are untouched.
Behaviour is byte-identical (verified — Opc.Ua.Core.Tests Tcp subset 7/7
passing).
* Surface via `<InternalsVisibleTo Include="Network.Fuzz" />` (+
matching Tools and Tests entries because the linked
`FuzzableCode.Transport.cs` compiles from all three host projects).
* New `Fuzzing/Network/Fuzz/FuzzableCode.Transport.cs` (12 targets) +
`Fuzzing/Network/Fuzz.Tools/Transport.Testcases.cs` (seeds under
`Testcases.Tcp.*`).
### Integration
* `UA.slnx`: add the 6 new fuzz projects, 4 new shared harness `.cs`
files, 5 new dictionaries, and the new `fuzz-menu.ps1` script — all
under the right `/Fuzzing/<Area>/` solution folders.
* Rewrite `Fuzzing/Fuzzing.md`: areas table, per-area dictionaries,
dynamic target-menu, step-by-step "how to add a new fuzz area" recipe,
Network area's replay-driven seed pipeline + key-material discipline,
pcap binding dependency note.
* Cross-link from `Docs/README.md` to `Fuzzing/Fuzzing.md`.
* Fix `.Order()` → `.OrderBy()` in shared scaffolding
(`.Order(IComparer)` is net7+; the harness compiles on net48/net472 for
the Encoders + Certificates test matrices).
## Verification
| Project | TFM | Tests | Result |
|---|---|---|---|
| `Encoders.Fuzz.Tests` | net10.0 | 4131 | ✅ 0 failed |
| `Certificates.Fuzz.Tests` | net10.0 | 220 | ✅ 0 failed |
| `Network.Fuzz.Tests` | net10.0 | 330 | ✅ 0 failed |
| **Total** | | **4681** | **✅ 0 failed** |
* Full `dotnet build UA.slnx` (all TFMs, including
net48/net472/net8/net9/net10): **0 errors**, warning count unchanged
from baseline.
* `Network.Fuzz` multi-TFM build: **net8.0 + net9.0 + net10.0** all
green.
* `Opc.Ua.Core` build: **0 errors, 0 warnings** after Phase 4b seam
addition.
## Notes for reviewers
* **Base branch**: `channelrefinements2`, not `master`. The pcap binding
(`Stack/Opc.Ua.Bindings.Pcap`) only exists on the channelrefinements
line.
* **Real keylog / key material**: never committed. All Network seed
corpora use deterministic material generated from fixture test
certificates via the binding's own replay helpers.
* **`.der` test assets**: force-added with `git add -f` (consistent with
the existing intentional exception at
`Tests/Opc.Ua.Security.Certificates.Tests/Assets/OPC Foundation Root CA
[...].der`); the repo-wide `*.der` gitignore otherwise excludes them.
* **Out of scope** (documented as fold-ins for future work):
`PcapFileReader` / `PcapNgFileWriter` / `UaKeyLogTextReader` /
`UaKeyLogJsonReader` defensive parser fuzzing; live handshake recorder
for `Network.Testcases.cs`; real loopback replay drivers (current
implementation uses synthetic replay).
## Checklist
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(Fuzz
suites verified on net10.0: 4681/4681 passing. Full UA.slnx build green
on all TFMs. Full repo-wide test run not yet executed on net48 —
pre-existing tests unaffected by these changes; only
`Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` is new and is gated
behind `<InternalsVisibleTo>`.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
301 lines
15 KiB
Markdown
301 lines
15 KiB
Markdown
# Fuzz testing for UA.NET Standard
|
||
|
||
This project provides integration of [SharpFuzz](https://github.com/Metalnem/sharpfuzz) with the
|
||
UA.NET Standard libraries, with support for both [afl-fuzz](https://lcamtuf.coredump.cx/afl/) and
|
||
[libFuzzer](https://llvm.org/docs/LibFuzzer.html). Each fuzz area lives directly under `Fuzzing/`
|
||
as four sibling folders — host (`*.Fuzz`), corpus (`*.Fuzz.Corpus`), test fixture (`*.Fuzz.Tests`),
|
||
and tools (`*.Fuzz.Tools`) — and shares a generic NUnit replay harness plus the SharpFuzz host
|
||
under `Common/`.
|
||
|
||
## Areas
|
||
|
||
| Area | Surface | Status |
|
||
|------|---------|--------|
|
||
| `Opc.Ua.Encoders.Fuzz` | BinaryDecoder / JsonDecoder / XmlDecoder + idempotent round-trip, individual built-in type readers (NodeId, ExpandedNodeId, Variant, ExtensionObject, DataValue, DiagnosticInfo, QualifiedName, LocalizedText) and parser entry points (`NodeId.Parse`, `ExpandedNodeId.Parse`, `RelativePathFormatter.Parse`, `QualifiedName.Parse`, `NumericRange.Parse`, `Uuid` round-trip) | shipped |
|
||
| `Opc.Ua.Certificates.Fuzz` | `X509CRL` decode + extensions (`X509SubjectAltNameExtension`, `X509AuthorityKeyIdentifierExtension`, `X509CrlNumberExtension`), `PEMReader` cert/key import, `Pkcs10CertificationRequest`, low-level `AsnUtils` helpers | shipped |
|
||
| `Opc.Ua.Network.Fuzz` | OPC UA UA-SC / TCP framing via `Opc.Ua.Bindings.Pcap` (`OpcUaFrameParser`, `TcpStreamReassembler`, `OfflineSecureChannel.ReadChunk`, `ServiceCallReassembler`, `MockServerReplay`/`MockClientReplay` stateful drivers) and the `Opc.Ua.Core` UA-SC parse seam (`TcpMessageParsers.TryParseChunkHeader` / `ReadHelloMessage` / `ReadAcknowledgeMessage` / `ReadErrorMessage` / `ReadReverseHelloMessage` / `ReadAsymmetricMessageHeader`) | shipped |
|
||
|
||
`Encoders` and `Certificates` build for the repo's standard `TestsTargetFrameworks` matrix
|
||
(`net48`, `net8.0`, `net9.0`, `net10.0`). `Network` is `net8.0;net9.0;net10.0` only because
|
||
`Opc.Ua.Bindings.Pcap` does not target .NET Framework.
|
||
|
||
## Directory layout
|
||
|
||
```
|
||
Fuzzing/
|
||
Fuzzing.md # this file
|
||
|
||
Common/ # shared source (no csproj)
|
||
Fuzz/
|
||
Program.cs, FuzzMethods.cs # SharpFuzz host
|
||
Fuzz.Tests/
|
||
FuzzTargetTestsBase.cs, FuzzTargetFunction.cs
|
||
TestcaseAsset.cs, TestAssetUtils.cs # generic NUnit harness
|
||
Fuzz.Tools/
|
||
Program.cs, Playback.cs, Logging.cs, Testcases.cs # Tools host
|
||
|
||
Dictionaries/ # libFuzzer / afl dictionaries
|
||
asn1.dict binary.dict json.dict nodeid.dict
|
||
tcp.dict uasc.dict xml.dict
|
||
|
||
Scripts/ # area-agnostic runners
|
||
fuzz-afl.ps1 fuzz-libfuzzer.ps1 fuzz-menu.ps1
|
||
install.sh readme.txt
|
||
|
||
Opc.Ua.Encoders.Fuzz/ # csproj + FuzzableCode partials
|
||
Opc.Ua.Encoders.Fuzz.Corpus/ # seed corpus (Testcases.*/)
|
||
Opc.Ua.Encoders.Fuzz.Tests/ # deterministic NUnit replay
|
||
Opc.Ua.Encoders.Fuzz.Tools/ # corpus generator + playback
|
||
|
||
Opc.Ua.Certificates.Fuzz/ Opc.Ua.Certificates.Fuzz.Corpus/
|
||
Opc.Ua.Certificates.Fuzz.Tests/ Opc.Ua.Certificates.Fuzz.Tools/
|
||
|
||
Opc.Ua.Network.Fuzz/ Opc.Ua.Network.Fuzz.Corpus/
|
||
Opc.Ua.Network.Fuzz.Tests/ Opc.Ua.Network.Fuzz.Tools/
|
||
```
|
||
|
||
The flat layout matches the rest of the repository (`Tests/`, `Libraries/`, `Stack/` also list
|
||
projects directly under the root with no domain grouping). Every csproj folder name equals the
|
||
csproj filename (without the `.csproj` extension), and every assembly is prefixed with `Opc.Ua.`
|
||
in line with the repo's `AssemblyPrefix` convention.
|
||
|
||
Each area's `*.Fuzz` project hosts `FuzzableCode.*.cs` partials whose `public static` methods
|
||
with a single parameter of type `Stream` (afl), `string` (afl), or `ReadOnlySpan<byte>`
|
||
(libFuzzer) are auto-discovered by:
|
||
|
||
* the SharpFuzz host (`Common/Fuzz/Program.cs`) at fuzz-run time, and
|
||
* the generic NUnit harness (`Common/Fuzz.Tests/FuzzTargetTestsBase.cs`) at test time —
|
||
every target is replayed against the area's `*.Fuzz.Corpus/Testcases.*/` corpus plus
|
||
crash/timeout assets, with no per-area test code required.
|
||
|
||
Each area's `*.Fuzz.Tests` is therefore a single thin subclass:
|
||
|
||
```csharp
|
||
[TestFixture]
|
||
[Category("Fuzzing")]
|
||
public class EncoderTests : FuzzTargetTestsBase
|
||
{
|
||
[DatapointSource]
|
||
public static readonly FuzzTargetFunction[] FuzzableFunctions =
|
||
CreateFuzzTargetFunctions(typeof(FuzzableCode));
|
||
|
||
protected override Type FuzzableCodeType => typeof(FuzzableCode);
|
||
}
|
||
```
|
||
|
||
## How to add a new fuzz area
|
||
|
||
1. **Pick a surface.** Anything that takes untrusted input (bytes, string, or stream) is a
|
||
candidate. Prefer surfaces with deterministic output and bounded resource usage.
|
||
2. **Copy the four `Opc.Ua.Encoders.Fuzz*` folders** to `Opc.Ua.<NewArea>.Fuzz*`. Rename the
|
||
three csproj files and update `<AssemblyName>` (keep the `Opc.Ua.` prefix) and
|
||
`<RootNamespace>`.
|
||
3. **Replace `FuzzableCode.*.cs`** with your area's targets. Naming convention:
|
||
`Aflfuzz<Target>(Stream|string)` for afl-fuzz and `Libfuzz<Target>(ReadOnlySpan<byte>)`
|
||
for libFuzzer. Each target should swallow expected exceptions (`ServiceResultException`
|
||
with `BadDecodingError` / `BadEncodingLimitsExceeded`, `CryptographicException`,
|
||
`FormatException`, `ArgumentException`) and let unexpected ones bubble — that's what
|
||
the fuzzer finds.
|
||
4. **Subclass the harness.** `Opc.Ua.<NewArea>.Fuzz.Tests/<NewArea>Tests.cs` inherits from
|
||
`FuzzTargetTestsBase`, sets `FuzzableCodeType => typeof(FuzzableCode)`, and exposes the
|
||
`[DatapointSource]` `FuzzableFunctions` field with
|
||
`CreateFuzzTargetFunctions(typeof(FuzzableCode))`.
|
||
5. **Generate seeds.** Add `<NewArea>.Testcases.cs` to `Opc.Ua.<NewArea>.Fuzz.Tools/` that
|
||
builds valid sample inputs through the real producer code and writes them under
|
||
`Opc.Ua.<NewArea>.Fuzz.Corpus/Testcases.<Bucket>/`. Each `Testcases.*/` subdirectory becomes
|
||
a logical seed bucket and is auto-discovered by
|
||
`TestAssetUtils.DiscoverTestcaseEncoderSuffixes`.
|
||
6. **Add a dictionary.** Put `<NewArea>.dict` under `Fuzzing/Dictionaries/` with common
|
||
tokens / magic bytes / length sentinels — this dramatically improves libFuzzer coverage
|
||
progression.
|
||
7. **Wire into `UA.slnx`.** Add the three new projects to the `/Fuzzing/` folder, the
|
||
`*.dict` file under `/Fuzzing/Dictionaries/`, and the seed loose files you want visible in
|
||
the IDE.
|
||
8. **Update this `Fuzzing.md`.** Add a row to the area table.
|
||
|
||
The Azure pipelines (`test.yml` / `testcc.yml`) automatically run every `*.Fuzz.Tests` project
|
||
under the `[Category("Fuzzing")]` filter.
|
||
|
||
## Areas in detail
|
||
|
||
### Network / Transport area — `Opc.Ua.Bindings.Pcap` + Core UA-SC seam
|
||
|
||
The Network area is unusual because it's split across two complementary entry points:
|
||
|
||
* **Phase 4a (no Core changes).** Fuzzes the public surfaces of
|
||
`Stack/Opc.Ua.Bindings.Pcap`: `OpcUaFrameParser.Process` (TCP → UA-SC chunk splitter),
|
||
`TcpStreamReassembler.Process` (raw TCP), `OfflineSecureChannel.ReadChunk` (UA-SC
|
||
symmetric decrypt + verify using the stack's own `UaSCUaBinaryChannel.ReadSymmetricMessage`,
|
||
so every security profile is covered for free), and `ServiceCallReassembler.Push` (chunk
|
||
→ service-call assembly with mixed sequence numbers, request ids, oversize bodies). Two
|
||
stateful replay drivers (`MockServerReplay`, `MockClientReplay`) are also wired as
|
||
libFuzzer-only targets but kept in a separate target list so the cheap stateless targets
|
||
dominate throughput.
|
||
|
||
* **Phase 4b (internal Core seam).** Adds `internal static class TcpMessageParsers` in
|
||
`Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` covering the pre-crypto, pre-auth chunk
|
||
surface the pcap binding does not expose: `TryParseChunkHeader`, `ReadHelloMessage`,
|
||
`ReadAcknowledgeMessage`, `ReadErrorMessage`, `ReadReverseHelloMessage`,
|
||
`ReadAsymmetricMessageHeader`. Surfaced to the fuzz area via
|
||
`<InternalsVisibleTo Include="Opc.Ua.Network.Fuzz" />` (and matching `*.Tools` / `*.Tests`
|
||
assemblies because the linked partials compile from every host).
|
||
|
||
#### Seed-corpus and key-material discipline
|
||
|
||
`Network.Testcases.cs` and `Transport.Testcases.cs` generate seeds from a deterministic
|
||
in-process handshake (Hello → OPN → MSG (Read/Browse) → CLO) using test certificates and
|
||
the pcap binding's `LoopbackFrameBuilder` / `CapturingMessageSocketFactory`. Outputs:
|
||
|
||
* raw TCP segments → `Opc.Ua.Network.Fuzz.Corpus/Testcases.Tcp/`
|
||
* UA-SC chunks → `Opc.Ua.Network.Fuzz.Corpus/Testcases.Chunks/`,
|
||
`Testcases.Tcp.Hello/`, `Testcases.Tcp.Ack/`, `Testcases.Tcp.Err/`, `Testcases.Tcp.Rhe/`,
|
||
`Testcases.Tcp.AsymHdr/`
|
||
* paired `ChannelKeyMaterial` JSON → `Opc.Ua.Network.Fuzz.Corpus/Testcases.Keys/`
|
||
|
||
**Never commit real keylog material.** Seed corpora use only key material generated from
|
||
the existing fixture test certificates via the binding's own multi-TFM replay helpers.
|
||
|
||
#### Dependency hygiene
|
||
|
||
The Network fuzz host references `Opc.Ua.Bindings.Pcap` (which transitively pulls
|
||
PacketDotNet + SharpPcap) but **does not** instantiate `NicCaptureSource` so the
|
||
AFL/libFuzzer process never opens raw sockets.
|
||
|
||
## Installation
|
||
|
||
### Linux (afl-fuzz + libFuzzer)
|
||
|
||
Both fuzzers are supported on Linux. afl-fuzz can be compiled on any Linux system; for
|
||
libFuzzer prebuilt binaries are available for Debian / Ubuntu / Windows from the
|
||
[libfuzzer-dotnet releases](https://github.com/Metalnem/libfuzzer-dotnet/releases).
|
||
|
||
```bash
|
||
cd <repo>/Fuzzing
|
||
sudo apt-get update
|
||
sudo apt-get install -y build-essential cmake git dotnet-sdk-10.0
|
||
# Powershell on Linux (required by the helper scripts):
|
||
# https://learn.microsoft.com/powershell/scripting/install/install-ubuntu
|
||
./Scripts/install.sh # builds afl-fuzz + installs SharpFuzz.CommandLine
|
||
```
|
||
|
||
`install.sh` downloads afl-2.52b, runs `make install`, then
|
||
`dotnet tool install --global SharpFuzz.CommandLine`. Validate with:
|
||
|
||
```bash
|
||
afl-fuzz --help
|
||
sharpfuzz
|
||
```
|
||
|
||
### Windows (libFuzzer via WSL or native)
|
||
|
||
Install the latest .NET 10 SDK / runtime, then:
|
||
|
||
```powershell
|
||
dotnet tool install --global SharpFuzz.CommandLine
|
||
```
|
||
|
||
For afl-fuzz, use WSL with the Linux instructions above.
|
||
|
||
## Running a fuzzer
|
||
|
||
The dynamic menu script lists every `FuzzableCode` static target in a built area assembly
|
||
without hardcoding target names:
|
||
|
||
```powershell
|
||
powershell -File Fuzzing/Scripts/fuzz-menu.ps1 `
|
||
-AssemblyPath Fuzzing/Opc.Ua.Network.Fuzz/bin/Debug/net10.0/Opc.Ua.Network.Fuzz.dll
|
||
# -Filter <regex> narrows the list; -Index <n> selects a target without prompting.
|
||
```
|
||
|
||
`Scripts/fuzz-libfuzzer.ps1` and `Scripts/fuzz-afl.ps1` accept a `-fuzztarget` parameter
|
||
matching one of the listed names:
|
||
|
||
```powershell
|
||
cd Fuzzing
|
||
powershell -File Scripts/fuzz-libfuzzer.ps1 `
|
||
-libFuzzer ./libfuzzer-dotnet-windows.exe `
|
||
-project ./Opc.Ua.Encoders.Fuzz/Opc.Ua.Encoders.Fuzz.csproj `
|
||
-fuzztarget LibfuzzBinaryDecoder `
|
||
-corpus ./Opc.Ua.Encoders.Fuzz.Corpus/Testcases.Binary/
|
||
```
|
||
|
||
The fuzzer runs until a crash / timeout / Ctrl-C. libFuzzer writes findings to the current
|
||
directory with `crash-` / `timeout-` prefixes; afl-fuzz writes them to its `findings/`
|
||
directory.
|
||
|
||
## Replay of crashes and timeouts
|
||
|
||
Run the area's `*.Fuzz.Tools` project with `-p` / `-s` for playback with stack traces:
|
||
|
||
```bash
|
||
dotnet run --project Fuzzing/Opc.Ua.Network.Fuzz.Tools -- --playback --stacktrace
|
||
```
|
||
|
||
The playback tool finds all crashes / timeouts in the default folders and replays them
|
||
against every libFuzzer target. Equivalent afl-fuzz seeds are skipped because they are
|
||
duplicates of the libFuzzer findings.
|
||
|
||
## Recreate or improve seeds
|
||
|
||
Run the area's `*.Fuzz.Tools` project with `-t` to (re)generate the area's `Testcases.*/`
|
||
seed corpus:
|
||
|
||
```bash
|
||
dotnet run --project Fuzzing/Opc.Ua.Network.Fuzz.Tools -- --testcases
|
||
```
|
||
|
||
This re-runs the deterministic seed-generation pipeline (handshake recorder for Network,
|
||
encoder round-trip for Encoders, cert/CRL/CSR builder for Certificates) and emits
|
||
byte-stable artifacts the harness's `Testcases.*/` discovery picks up automatically. The
|
||
default output path for each area's Tools project is its own sibling
|
||
`Opc.Ua.<Area>.Fuzz.Corpus/Testcases` folder, derived from the running Tools assembly name
|
||
in `Common/Fuzz.Tools/Program.cs`.
|
||
|
||
## Automation — `fuzz-tester` custom agent
|
||
|
||
`.github/agents/fuzz-tester.agent.md` defines a GitHub Copilot custom agent that drives
|
||
this entire toolchain autonomously. It detects the host OS, picks the available engines
|
||
(libFuzzer everywhere, afl-fuzz on Linux when installed), publishes + SharpFuzz-instruments
|
||
each area's host project, enumerates `FuzzableCode` targets via
|
||
`Scripts/fuzz-menu.ps1`, launches one detached fuzz process per (area × engine × target)
|
||
tuple, and polls the per-instance work dirs under `Fuzzing/.runs/` for new findings.
|
||
|
||
When the agent picks up a novel `crash-*` / `timeout-*` / `slow-unit-*` (or afl
|
||
`crashes/` / `hangs/`) file it:
|
||
|
||
1. SHA-1 dedups against every `Opc.Ua.<area>.Fuzz.Tests/Assets/<prefix>-<sha1>` already
|
||
in the repo so it never re-investigates a known regression seed.
|
||
2. Reproduces the crash locally via `dotnet run --project Fuzzing/Opc.Ua.<area>.Fuzz.Tools -- --playback --stacktrace`.
|
||
3. Designs a minimal fix following every repo guideline (no SYNC-over-ASYNC,
|
||
`Span<byte>` / `ReadOnlySpan<byte>` / `ByteString` in public API, Allman + 4-space +
|
||
CRLF + MIT header on new files, no `#region`, no `[Obsolete]` usage, NativeAOT-safe,
|
||
`TreatWarningsAsErrors` clean, no exposed locks).
|
||
4. Runs a rubber-duck agent review (max 2 rounds) on the proposed diff.
|
||
5. Copies the failing input to `Fuzzing/Opc.Ua.<area>.Fuzz.Tests/Assets/<prefix>-<sha1>`
|
||
so the existing harness (`FuzzCrashAssets` / `FuzzTimeoutAssets` / `FuzzSlowAssets`)
|
||
replays it through every target on every future `dotnet test` run.
|
||
6. Rebuilds and runs the three `*.Fuzz.Tests` projects on `net10.0`; the run must end at
|
||
or above the baseline 4681-test count, 0 failed.
|
||
7. Briefly re-runs the originating fuzz instance with the new asset in its corpus to
|
||
confirm the same crash is no longer reachable.
|
||
8. Commits one fix per commit (`Fuzz fix [<area>]: <root cause>`), pushes to `fuzzing`,
|
||
and resumes the still-running fuzz instances.
|
||
|
||
If a fix introduces a regression, requires a public-API change, or breaks compatibility
|
||
with the 1.5.378 baseline, the agent **pauses and prompts the user** via `ask_user`
|
||
with four options (revert / accept the regression and migrate the affected seed /
|
||
provide an alternative fix / accept the API break).
|
||
|
||
Trigger phrases: *"run the fuzz tests"*, *"start fuzzing"*, *"fuzz the encoders"*,
|
||
*"fuzz the network"*, *"fuzz the certificates"*, *"fuzz until I say stop"*,
|
||
*"react to fuzz findings"*, *"find fuzz crashes and fix them"*,
|
||
*"run libfuzzer and fix what it finds"*, *"autonomous fuzz loop"*. Stop the agent at
|
||
any time with *"stop"*, *"halt"*, *"stop fuzzing"*, *"that's enough"*.
|
||
|
||
The agent's own toolchain setup mirrors the manual `Installation` section above:
|
||
`SharpFuzz.CommandLine` global tool, the `libfuzzer-dotnet` driver under
|
||
`Fuzzing/.tools/` (gitignored), and `afl-fuzz` on PATH for the AFL engine on Linux.
|
||
If any are missing AND cannot be auto-installed (air-gapped box, `sudo` not available,
|
||
…), the agent surfaces the gap and asks the user how to proceed.
|
||
|