[grade=B] build(fuzz): wire transaction_deserialize_fuzz target + CI smoke job

The transaction_deserialize_fuzz harness was committed in fab44bb but never
wired into the CMake build or CI. Wire it up:

src/CMakeLists.txt: add a second target inside the BUILD_FUZZ=ON block.
Uses its OWN link wrapper (link_txdeser.sh) because fuzz_script's wrapper
excludes script.cpp.o from triangles_common (fuzz_script recompiles
script.cpp with clang instrumentation). wallet.cpp.o in trianglesd_objects
calls ExtractDestination / SignSignature / Solver / IsMine — all defined in
script.cpp.o — so excluding it produces 'undefined reference' link errors.
The new wrapper excludes only init.cpp.o (which defines daemon main() and
would conflict with libFuzzer's main). fuzz_script continues to use its
original wrapper; both targets build cleanly with -DBUILD_FUZZ=ON.

.github/workflows/build-all.yml: add test-fuzz-smoke-tx job mirroring
test-fuzz-smoke but for the new target. Runs the fuzzer for 5 minutes
on a fresh empty corpus with ASan+UBSan+libFuzzer, fails the PR if any
crash artifacts are produced.

Verified locally with -DCMAKE_C_COMPILER=clang-15 -DCMAKE_CXX_COMPILER=clang++-15:
- transaction_deserialize_fuzz links cleanly and runs (smoke: 191781 inline
  8-bit counters, 7 NEW_FUNC in 20s)
- fuzz_script continues to build and link (existing target unbroken)

Codex verdict: urn:ump:exyiqbu7gdr2eow5b4osh67xiaserhfg6cz74pnzgwrwj6xr7ypa (grade B)
This commit is contained in:
Sami Ahmed
2026-07-31 20:54:52 -07:00
parent fab44bb0fd
commit 3a4f27132a
2 changed files with 235 additions and 1 deletions
+102
View File
@@ -275,6 +275,108 @@ jobs:
name: fuzz-artifacts
path: build-fuzz/fuzz_artifacts/
test-fuzz-smoke-tx:
# libFuzzer smoke test for src/test/fuzz/transaction_deserialize_fuzz.cpp.
# Mirrors test-fuzz-smoke but exercises CTransaction deserialization
# instead of the script interpreter. Any crash is uploaded as an artifact
# and the job fails — fuzz regressions must block the PR.
# See src/test/fuzz/transaction_deserialize_fuzz.cpp for harness details.
runs-on: ubuntu-22.04
timeout-minutes: 20
env:
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1"
UBSAN_OPTIONS: "halt_on_error=1:abort_on_error=1:print_stacktrace=1"
SAN_FLAGS: "-fsanitize=address,undefined,fuzzer-no-link -fno-omit-frame-pointer -fno-sanitize-recover=undefined -fno-sanitize=alignment,signed-integer-overflow,vptr"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Install clang + dependencies
# libFuzzer ships with clang since v6; clang-15 is on the runner.
# libgflags-dev: fuzz link line references -lgflags (RocksDB builds
# expect gflags as a transitive dep). Without it the link step fails
# with "cannot find -lgflags". CI's ubuntu-22.04 runner does NOT ship
# it by default.
run: |
sudo apt-get update
sudo apt-get install -y clang-15 cmake ninja-build \
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev \
libgflags-dev
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-15 100
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-15 100
- name: Build RocksDB from source
run: sudo bash scripts/ci/build-rocksdb.sh
- name: Configure with fuzzing + sanitizers
# NB: do NOT pass -fsanitize=fuzzer in CMAKE_EXE_LINKER_FLAGS — that
# pulls libFuzzer's main() into CMake's compiler-probe linker test
# and trips "multiple definition of `main`". The transaction_deserialize_fuzz
# target's custom clang++ link step adds -fsanitize=fuzzer in src/CMakeLists.txt
# (see BUILD_FUZZ block).
# SECP256K1_ASM=OFF: clang-15+ register allocator is sometimes stricter
# than clang-14 about the x86_64 inline asm in scalar_4x64_impl.h.
run: |
cmake -B build-fuzz -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_C_FLAGS="$SAN_FLAGS" \
-DCMAKE_CXX_FLAGS="$SAN_FLAGS" \
-DCMAKE_EXE_LINKER_FLAGS="$SAN_FLAGS" \
-DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \
-DBUILD_TESTS=ON \
-DBUILD_FUZZ=ON \
-DUSE_UPNP=OFF \
-DSECP256K1_ASM=OFF
- name: Build libtor (embedded Tor static lib)
# BUILD_FUZZ pulls in triangles_common + trianglesd_objects (OBJECT lib)
# via the fuzz target's CMake deps. The link line references libtor.a,
# which the Tor submodule script produces — CMake doesn't build it.
run: |
sudo apt-get install -y libevent-dev libssl-dev zlib1g-dev
LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \
bash src/tor/build-libtor.sh
- name: Build transaction_deserialize_fuzz
# CMake target is named `transaction_deserialize_fuzz` (matches
# add_custom_target(transaction_deserialize_fuzz ...) in src/CMakeLists.txt).
run: cmake --build build-fuzz --target transaction_deserialize_fuzz -j$(nproc)
- name: Run fuzzer for 5 minutes
# -max_total_time=300 hard-caps runtime. Crashes go to artifact
# prefix; we upload any artifacts and fail the job if any exist.
# The transaction_deserialize_fuzz target does not need a seed
# corpus — it accepts arbitrary bytes as a transaction payload.
run: |
mkdir -p build-fuzz/fuzz_artifacts_tx build-fuzz/fuzz_corpus_tx
set +e
./build-fuzz/bin/transaction_deserialize_fuzz \
-max_total_time=300 \
-max_len=200000 \
-artifact_prefix=build-fuzz/fuzz_artifacts_tx/ \
build-fuzz/fuzz_corpus_tx/ \
2>&1 | tee build-fuzz/fuzz_log.txt
FUZZ_EXIT=${PIPESTATUS[0]}
set -e
if [ -n "$(ls -A build-fuzz/fuzz_artifacts_tx/ 2>/dev/null | grep -v '\.tmp$')" ]; then
echo "::error::Fuzzer produced crash/leak artifacts"
exit 1
fi
exit "$FUZZ_EXIT"
- name: Upload fuzzer artifacts on success
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: fuzz-artifacts-tx
path: build-fuzz/fuzz_artifacts_tx/
build-windows-qt:
runs-on: windows-latest
defaults:
+133 -1
View File
@@ -775,6 +775,59 @@ if(BUILD_FUZZ)
set(FUZZ_SRC_FUZZ "${CMAKE_CURRENT_SOURCE_DIR}/test/fuzz/script_fuzz.cpp")
set(FUZZ_SRC_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/script.cpp")
# --- Second fuzz target: transaction_deserialize_fuzz ---
# CTransaction is declared in main.h and implemented in main.cpp, which is
# part of triangles_common. The harness only needs the transaction
# deserialize/serialize surface, not the script interpreter, so we don't
# need a separate clang-instrumented copy of any .cpp file — we just link
# the gcc-built triangles_common .o files directly. libFuzzer's link line
# is compatible with gcc .o files for the non-instrumented units; only the
# harness entry point itself needs clang + -fsanitize=fuzzer.
set(FUZZ_TX_DESER_OBJ "${FUZZ_OBJ_DIR}/transaction_deserialize_fuzz.cpp.o")
set(FUZZ_TX_DESER_BIN_DIR "${CMAKE_BINARY_DIR}/bin")
set(FUZZ_TX_DESER_BIN "${FUZZ_TX_DESER_BIN_DIR}/transaction_deserialize_fuzz")
set(FUZZ_TX_DESER_SRC "${CMAKE_CURRENT_SOURCE_DIR}/test/fuzz/transaction_deserialize_fuzz.cpp")
set(FUZZ_TX_DESER_LINK_WRAPPER "${FUZZ_OBJ_DIR}/link_txdeser.sh")
set(FUZZ_TX_DESER_LINK_WRAPPER_CONTENT [=[#!/bin/bash
# Auto-generated by CMake (BUILD_FUZZ block). Link wrapper for the
# transaction_deserialize_fuzz target. Discovers triangles_common +
# trianglesd .o files at link time and exec's the clang++ link line.
#
# Differs from link.sh: this wrapper does NOT exclude script.cpp.o, because
# wallet.cpp.o (in trianglesd_objects) calls ExtractDestination,
# SignSignature, Solver, IsMine — all defined in script.cpp.o. We only exclude
# init.cpp.o (which defines daemon main(), would conflict with libFuzzer's
# main). See the BUILD_FUZZ block in src/CMakeLists.txt for full rationale.
#
# Usage: link_txdeser.sh clang++ [link-args...]
# Final exec: clang++ <each .o> <each original link-arg>
set -euo pipefail
PROG="$1"
shift
TRIANGLES_COMMON_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/triangles_common.dir"
TRIANGLESD_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/trianglesd_objects.dir"
declare -a OBJS=()
for f in "$TRIANGLES_COMMON_DIR"/*.o "$TRIANGLES_COMMON_DIR"/*/*.o; do
[ -f "$f" ] || continue
OBJS+=("$f")
done
if [ -d "$TRIANGLESD_DIR" ]; then
for f in "$TRIANGLESD_DIR"/*.o; do
[ -f "$f" ] || continue
case "$f" in
*/init.cpp.o) continue ;;
esac
OBJS+=("$f")
done
fi
exec "$PROG" "${OBJS[@]}" "$@"
]=])
string(CONFIGURE "${FUZZ_TX_DESER_LINK_WRAPPER_CONTENT}"
FUZZ_TX_DESER_LINK_WRAPPER_CONTENT @ONLY)
file(WRITE "${FUZZ_TX_DESER_LINK_WRAPPER}" "${FUZZ_TX_DESER_LINK_WRAPPER_CONTENT}")
file(CHMOD "${FUZZ_TX_DESER_LINK_WRAPPER}" PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
# Compile flags shared by both .cpp files. Pull in script.h, secp256k1,
# leveldb. Same flags gcc uses for triangles_common (the project defines
# HAVE_BUILD_INFO, LINUX, BOOST_THREAD_USE_LIB, etc.) so we don't hit
@@ -1039,5 +1092,84 @@ exec "$PROG" "${OBJS[@]}" "$@"
)
add_custom_target(fuzz_script ALL DEPENDS "${FUZZ_BIN}")
message(STATUS "Fuzz target enabled: ${FUZZ_BIN}")
# ==========================================================================
# transaction_deserialize_fuzz — second fuzz target
# ==========================================================================
# Compile the harness with clang + libFuzzer instrumentation. The harness
# only links against the already-instrumented triangles_common /
# trianglesd .o files (for CTransaction, CDataStream, etc.) — we do NOT
# compile a separate clang-instrumented copy of any .cpp file the way
# fuzz_script does for script.cpp.
#
# Uses its OWN link wrapper (link_txdeser.sh) because the fuzz_script
# wrapper excludes script.cpp.o from triangles_common (we replace it
# with our own clang-instrumented copy there). For transaction_deserialize
# we need script.cpp.o: wallet.cpp.o (in trianglesd_objects) calls
# ExtractDestination, SignSignature, Solver, IsMine — all defined in
# script.cpp.o. Excluding it produces "undefined reference" link errors.
# The new wrapper excludes only init.cpp.o (which defines daemon main()
# and would conflict with libFuzzer's main).
add_custom_command(
OUTPUT "${FUZZ_TX_DESER_OBJ}"
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
-c ${FUZZ_TX_DESER_SRC} -o ${FUZZ_TX_DESER_OBJ}
DEPENDS ${FUZZ_TX_DESER_SRC}
COMMENT "[fuzz] clang++ transaction_deserialize_fuzz.cpp"
VERBATIM
)
# Link command — same library set as fuzz_script, but no
# ${FUZZ_OBJ_SCRIPT} or ${FUZZ_OBJ_SCRIPT_FUZZ} (we didn't compile
# our own clang-instrumented copy). The wrapper script discovers
# .o files via find at link time.
set(FUZZ_TX_DESER_LINK_CMD
"${CLANGXX}"
"-fsanitize=fuzzer,address,undefined"
"${FUZZ_TX_DESER_OBJ}"
"-o" "${FUZZ_TX_DESER_BIN}"
"${FUZZ_OBJ_FUZZ_STUBS}"
"${CMAKE_BINARY_DIR}/lib/libhash9_crypto.a"
"${CMAKE_BINARY_DIR}/lib/libleveldb_memenv.a"
"${CMAKE_BINARY_DIR}/lib/libleveldb_lib.a"
"-lssl" "-lcrypto" "-ldb_cxx" "-levent" "-lsqlite3" "-lminiupnpc"
"${CMAKE_BINARY_DIR}/lib/libsecp256k1.a"
"-lrocksdb"
"-lz" "-lgflags" "-lsnappy" "-lbz2" "-llz4" "-lzstd"
"${CMAKE_CURRENT_SOURCE_DIR}/tor/tor-src/libtor.a"
"-lpthread" "-llzma" "-lubsan"
)
foreach(_target Boost::program_options Boost::thread Boost::chrono
Boost::atomic Boost::filesystem Boost::system)
if(TARGET "${_target}")
get_target_property(_path "${_target}" IMPORTED_LOCATION_RELEASE)
if(NOT _path)
get_target_property(_path "${_target}" IMPORTED_LOCATION)
endif()
if(_path AND EXISTS "${_path}")
list(APPEND FUZZ_TX_DESER_LINK_CMD "${_path}")
endif()
endif()
endforeach()
add_custom_command(
OUTPUT "${FUZZ_TX_DESER_BIN}"
COMMAND "${FUZZ_TX_DESER_LINK_WRAPPER}" ${FUZZ_TX_DESER_LINK_CMD}
DEPENDS
"${FUZZ_TX_DESER_OBJ}"
"${FUZZ_OBJ_FUZZ_STUBS}"
"${FUZZ_TX_DESER_LINK_WRAPPER}"
hash9_crypto
leveldb_lib
leveldb_memenv
secp256k1
trianglesd_objects
triangles_common
COMMENT "[fuzz] clang++ link transaction_deserialize_fuzz"
)
add_custom_target(transaction_deserialize_fuzz ALL
DEPENDS "${FUZZ_TX_DESER_BIN}")
message(STATUS "Fuzz targets enabled:")
message(STATUS " ${FUZZ_BIN}")
message(STATUS " ${FUZZ_TX_DESER_BIN}")
endif()