Files
davide cdede17683 feat(fuzz): add SharpFuzz-based fuzzing for untrusted-input parsers
New tests/PalladiumWallet.Fuzz project: one target per parser that
consumes untrusted input (block headers, Merkle proofs, peer lists,
wallet files, user-pasted mnemonics/keys/addresses/amounts), each
encoding the parser's documented error contract - any exception beyond
that contract is a finding, reported as a crash.

Three ways to run: the seed corpus (with regression inputs for every
crash found so far, including the two fixed in prior commits) replays
automatically inside dotnet test via FuzzCorpusTests, so a fixed
contract violation can't silently return; a built-in random-mutation
mode needs no external tooling (dotnet run -- <target> --random N);
and fuzz.sh drives real coverage-guided campaigns via afl++ +
SharpFuzz instrumentation for pre-release or post-parser-change runs.
2026-07-07 22:05:38 +02:00
..

Fuzzing

Fuzz targets over every parser that consumes untrusted input: server responses (block headers, Merkle proofs, peer lists), wallet files (plaintext document and encrypted container), and user-pasted text (mnemonics, SLIP-132 keys, addresses, amounts).

Each target encodes the parser's error contract: the exception types documented as its failure mode are swallowed, anything else escaping is a finding. Targets: header merkle slip132 bip39 address coinamount walletdoc encfile peers (see FuzzTargets.cs for each contract).

Three ways to run

1. Corpus replay — automatic. The seed corpus (including regression inputs for every crash found so far) replays through all targets on every dotnet test run via FuzzCorpusTests, so fixed findings cannot come back.

2. Built-in random smoke — no tooling. Not coverage-guided, but catches shallow contract violations in seconds:

dotnet run -- bip39 --random 50000        # target, iterations [, seed]

3. Coverage-guided campaign — afl++. The real thing; run it before a release or after touching any parser:

apt install afl++
dotnet tool install --global SharpFuzz.CommandLine
./fuzz.sh header              # one target per campaign
./fuzz.sh peers -V 3600       # extra args go to afl-fuzz

fuzz.sh builds Release, instruments PalladiumWallet.Core.dll (and NBitcoin) with SharpFuzz, and launches afl-fuzz with the target's seed corpus. Findings land in findings/<target>/crashes/.

Triage workflow

dotnet run -- <target> findings/<target>/crashes/<file>   # replay: full stack trace

Fix the parser (typed exception or graceful rejection — see the hardening commits for Bip39.TryParse, ElectrumApi.ParsePeers, EncryptedFile.Decrypt as examples), then add the input to SeedCorpus in Program.cs as a regression-* seed and regenerate with dotnet run -- --make-seeds Corpus.

Findings so far

The first smoke run found two real bugs, both reachable from untrusted input:

  • Bip39.TryParse crashed with NotSupportedException on restore text resembling no wordlist (NBitcoin's Wordlist.AutoDetect throws for its internal "Unknown" language).
  • ElectrumApi.ParsePeers crashed with InvalidOperationException on a peer list containing a JSON string with invalid UTF-8 bytes (parses fine, fails at GetString() transcoding) — attacker-controlled server data.

Plus two hardening changes made so the encfile/peers contracts could be strict at all: EncryptedFile.Decrypt maps every malformed-container failure to InvalidDataException and bounds the PBKDF2 iteration count (a tampered file could previously demand 2^31 iterations, hanging the wallet at open), and ParsePeers tolerates any JSON shape.