Files
triangles_v5/src/test/fuzz
Sami Ahmed 2de9a9da20 docs(fuzz): document transaction_deserialize_fuzz target and link wrapper difference
The README only covered script_fuzz. Add the second target, the build
invocation (CC=clang CXX=clang++ is required — gcc doesn't support
-fsanitize=fuzzer-no-link), and explain why transaction_deserialize_fuzz
needs its own link wrapper (link_txdeser.sh keeps script.cpp.o because
wallet.cpp.o references ExtractDestination/SignSignature/Solver/IsMine).

Matches the committed CMakeLists.txt wiring at 3a4f271.
2026-07-31 20:57:34 -07:00
..

Triangles Fuzz Targets

Two libFuzzer-based harnesses, both gated on -DBUILD_FUZZ=ON so default builds (and CI) don't pull in libFuzzer.

Build

cd build
CC=clang CXX=clang++ cmake -G Ninja -DBUILD_TESTS=ON -DBUILD_FUZZ=ON ..
ninja

Both targets (fuzz_script and transaction_deserialize_fuzz) are part of the default ALL target once BUILD_FUZZ=ON.

Requires clang++ (libFuzzer is built in since clang-6; clang-18 is current on DNS2). gcc does NOT support -fsanitize=fuzzer-no-link, so the entire triangles_common and trianglesd_objects libraries must be compiled with clang under BUILD_FUZZ=ON.

Targets

fuzz_script — script interpreter

libFuzzer harness for EvalScript in src/script.cpp. Mutations find bugs in opcode dispatch, stack handling, push-data edge cases, and the multisig stack walk.

./bin/fuzz_script -max_total_time=300 -max_len=10000 corpus/
./bin/fuzz_script crash-deadbeef.bin  # reproduce a crash

Seed corpus: start with src/test/data/script_valid.json and script_invalid.json — extract the scriptPubKey fields and prefix each with uint8_t(scriptLen). A small starter set lives in corpus/ (generated by scripts/seed-from-tests.sh).

What it finds: every historical script interpreter bug has been in this surface (sigcache asymmetry, CHECKMULTISIG stack walk ordering, combineSigs size trap, push-data encoding, numeric overflow).

transaction_deserialize_fuzz — P2P tx parser

libFuzzer harness for CTransaction deserialization. Reads raw attacker-controlled bytes into a CDataStream and calls Unserialize on a CTransaction, then exercises hash determinism, round-trip serialize/parse, and CheckTransaction bounds. Mirrors the Bitcoin Core deserialize-fuzz pattern.

./bin/transaction_deserialize_fuzz -max_total_time=300 -max_len=200000 corpus/
./bin/transaction_deserialize_fuzz crash-deadbeef.bin

What it covers:

  • ReadCompactSize varint decoder — every overflow / truncation / non-canonical encoding path.
  • Vector<T> Unserialize_impl — recursive expansion when T is itself a structured type (CTxIn, CTxOut). Known to do unbounded std::vector::resize(nSize) before reading; historical DoS surface for "send a tx claiming nSize=0xFFFFFFFF".
  • CScript deserialization (downstream EvalScript is covered by fuzz_script).
  • CTransaction::CheckTransaction bounds — max size, negative value, out-of-range totals.
  • Hash determinism — GetHash() must produce the same uint256 for the same bytes, regardless of intermediate state mutations.

What it does NOT cover: signature verification (covered by script_tests.cpp / keystore_tests.cpp), block-level validation, P2P message framing (the fuzz input is the raw tx payload, not the wire envelope).

Both targets use a wrapper script (fuzz_objs/link.sh and fuzz_objs/link_txdeser.sh) that discovers .o files at link time. The difference:

  • link.sh excludes script.cpp.o from triangles_common because fuzz_script provides its own clang-instrumented copy.
  • link_txdeser.sh keeps script.cpp.o (needed by wallet.cpp.o symbols like ExtractDestination, SignSignature, Solver, IsMine) and excludes only init.cpp.o (daemon main() would conflict with libFuzzer's).

CI integration

Both jobs run under the Build All Platforms workflow on every PR. The CI script build script lives in .github/workflows/build-all.yml under the fuzz job.