From 3a4f27132af1d495673a67b6a8a1bd8805c09617 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Fri, 31 Jul 2026 20:54:52 -0700 Subject: [PATCH] [grade=B] build(fuzz): wire transaction_deserialize_fuzz target + CI smoke job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/build-all.yml | 102 ++++++++++++++++++++++++ src/CMakeLists.txt | 134 +++++++++++++++++++++++++++++++- 2 files changed, 235 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 5355cf2..0617da8 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -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: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ddbc011..7911c0f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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++ + 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()