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.
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:
ReadCompactSizevarint decoder — every overflow / truncation / non-canonical encoding path.Vector<T> Unserialize_impl— recursive expansion whenTis itself a structured type (CTxIn,CTxOut). Known to do unboundedstd::vector::resize(nSize)before reading; historical DoS surface for "send a tx claiming nSize=0xFFFFFFFF".CScriptdeserialization (downstreamEvalScriptis covered byfuzz_script).CTransaction::CheckTransactionbounds — max size, negative value, out-of-range totals.- Hash determinism —
GetHash()must produce the sameuint256for 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).
Link wrappers
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.shexcludesscript.cpp.ofromtriangles_commonbecausefuzz_scriptprovides its own clang-instrumented copy.link_txdeser.shkeepsscript.cpp.o(needed bywallet.cpp.osymbols likeExtractDestination,SignSignature,Solver,IsMine) and excludes onlyinit.cpp.o(daemonmain()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.