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.
This commit is contained in:
Sami Ahmed
2026-07-31 20:57:34 -07:00
parent 3a4f27132a
commit 2de9a9da20
+75 -35
View File
@@ -1,54 +1,94 @@
# Triangles Script Fuzzer
# Triangles Fuzz Targets
libFuzzer-based harness for `EvalScript` in `src/script.cpp`. Mutations
find bugs in opcode dispatch, stack handling, push-data edge cases, and
the multisig stack walk.
Two libFuzzer-based harnesses, both gated on `-DBUILD_FUZZ=ON` so default
builds (and CI) don't pull in libFuzzer.
## Build
The fuzz target is gated on `-DBUILD_FUZZ=ON` so default builds (and CI)
don't pull in libFuzzer. To build:
```bash
cd build
cmake -G Ninja -DBUILD_TESTS=ON -DBUILD_FUZZ=ON ..
ninja script_fuzz
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).
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`.
## Run
## 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.
```bash
# Fuzz for 5 minutes
./bin/script_fuzz -max_total_time=300 -max_len=10000 corpus/
# Reproduce a crash
./bin/script_fuzz crash-deadbeef.bin
# Run a single corpus file (when built without -fsanitize=fuzzer)
./bin/script_fuzz corpus/script_001.bin
./bin/fuzz_script -max_total_time=300 -max_len=10000 corpus/
./bin/fuzz_script crash-deadbeef.bin # reproduce a crash
```
## Seed corpus
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`).
Start with the existing `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).
## What it finds
### `transaction_deserialize_fuzz` — P2P tx parser
Every historical script interpreter bug has been in this surface:
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.
- sigcache Set/Get asymmetry (silent no-op)
- CHECKMULTISIG stack walk ordering
- combineSigs size trap
- PushData encoding edge cases
- Numeric overflow on the stack
```bash
./bin/transaction_deserialize_fuzz -max_total_time=300 -max_len=200000 corpus/
./bin/transaction_deserialize_fuzz crash-deadbeef.bin
```
The harness is intentionally minimal — it calls `EvalScript` against
a default-constructed `CTransaction`, so signature verification is
not exercised. That surface is covered by BOOST tests in
`src/test/script_tests.cpp`. The fuzz target is for everything else.
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).
## 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.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.