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.
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.TryParsecrashed withNotSupportedExceptionon restore text resembling no wordlist (NBitcoin'sWordlist.AutoDetectthrows for its internal "Unknown" language).ElectrumApi.ParsePeerscrashed withInvalidOperationExceptionon a peer list containing a JSON string with invalid UTF-8 bytes (parses fine, fails atGetString()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.