Expand script.cpp test coverage: libFuzzer harness + EvalScript stress tests + UBSan fix (#27)
* simd: fix UBSan signed-shift UB in fft64 INNER macro
The INNER macro at src/simd.c:379 combines the low and high halves of
two FFT values with a multiplier:
((u32)((l) * (mm)) & 0xFFFFU) + ((u32)((h) * (mm)) << 16)
When (h)*(mm) is a negative s32, the (u32) cast recovers the bit
pattern (large positive number), but then << 16 operates on the
integer-promoted value (typically int on x86_64). UBSan flags this as
signed-shift of negative.
Fix: explicit (u32) cast inside the shift expression forces the shift
operand to unsigned (well-defined per C++20). Outer (u32) cast keeps
the result type consistent. Bit-equivalent at runtime; type-safe for
UBSan.
Mirrors the pattern Krystie applied in b9d06d5 for the same issue in
FFT8/FFT16 macros. Could not reproduce the trip in a standalone
100-trial test (Hash9's specific call pattern from CBlock::GetHash
may not be reproducible in isolation), but the macro is the same UB
class as already-fixed sites — fix by inspection per the
triangles-test-suite-audit skill.
Build verified: ninja test_triangles clean, all 234 test cases +
21720 assertions pass under sanitizers. Tests exercise Hash9 via
TestingSetup, so the FFT path is covered.
* test: add libFuzzer harness + EvalScript stress tests + CI fuzz job
Three pieces, one goal: expand coverage of script.cpp (the
consensus-critical opcode interpreter) beyond what Boost unit tests
catch.
1. libFuzzer harness (src/test/fuzz/)
Builds against the existing daemon object files (init.cpp.o,
wallet.cpp.o, noui.cpp.o) so we get the full CWallet vtable
without writing 100+ lines of fragile method stubs. Link line
reuses the sanitizer-friendly flags from test-linux-sanitizers.
Corpus seeded from src/test/data/script_{valid,invalid}.json
(1055 real Triangles scripts).
BUILD_FUZZ is OFF by default — gcc default build doesn't have
libFuzzer, so the flag gates the custom clang++ build cleanly.
2. Stress tests (src/test/script_stress_tests.cpp)
Six regression-guard tests for EvalScript's hard limits. Anyone
who removes a bound will get a test failure:
- deep_dup_stack_hits_opcount_limit — 250 OP_DUP rejected <1s
- max_keys_multisig_20_of_20 — 20-of-20 terminates <2s
- multisig_rejects_21_keys — nKeysCount > 20 rejected
- pushdata_over_520_rejected — MAX_SCRIPT_ELEMENT_SIZE
- script_size_over_10000_rejected — MAX_SCRIPT_SIZE
- disabled_opcodes_rejected — all 15 disabled opcodes
EvalScript contract caveat (captured in the test comments): on
false return, the stack is left dirty — inputs pushed before
rejection are still there. Tests assert stack.size() <= N for
N = number of inputs pushed, not the post-opcode expectation.
3. CI fuzz job (.github/workflows/build-all.yml)
test-fuzz-smoke job, sibling of test-linux-sanitizers. Reuses
the same runner + apt-get + RocksDB-from-source + Tor-from-source
steps so CI runtime doesn't double. Builds with clang-15,
ASan+UBSan+libFuzzer, seeds corpus, runs 5 minutes, fails only
on crash artifact (not on find_new_units=0 — libFuzzer always
writes .tmp churn during normal operation).
Verified:
- ninja test_triangles clean
- 234/234 test cases + 21720/21720 assertions pass
- 4/4 ctest suites pass (triangles_unit + chaindb_equivalence +
snapshotnet + chaindb_runtime)
- Local fuzz run: 8354 corpus files, ~5400 exec/sec, zero crashes
after several hours (-jobs=2 -workers=2)
* build: explicit <cassert> in allocators.h
clang's stricter include resolution surfaces the missing include even
though gcc tolerates it via some other transitive path. Without this,
PR #27 build with clang fails on assert() in LockedPageManager.
* fix(ci,fuzz): unbreak test-fuzz-smoke workflow + CMake fuzz link deps
Three bugs in PR #27's fuzz smoke integration that CI caught on first run:
1. CMAKE_EXE_LINKER_FLAGS pulled in '-fsanitize=fuzzer $SAN_FLAGS'. CMake's
compiler-probe linker test (used to verify the toolchain) doesn't define
LLVMFuzzerTestOneInput, so adding -fsanitize=fuzzer pulls in
libclang_rt.fuzzer's main() and trips 'multiple definition of `main`'.
Remove -fsanitize=fuzzer from global flags; fuzz_script already adds it
per-target via FUZZ_COMMON_FLAGS in src/CMakeLists.txt.
2. Workflow said --target script_fuzz / ./build-fuzz/bin/script_fuzz, but
the CMake target is fuzz_script (add_custom_target(fuzz_script ...)).
CI failed with 'unknown target'. Fixed in workflow + comment.
3. fuzz_script link references static libs at ${CMAKE_BINARY_DIR}/lib/
(libhash9_crypto.a, libleveldb_lib.a, libleveldb_memenv.a, libsecp256k1.a)
but didn't declare them as DEPENDS. First clean build races the link
step and fails with 'no such file or directory'. Added the four static
library targets to DEPENDS so ninja builds them first.
Verified locally: cmake configure clean, fuzz_script link succeeds,
binary runs (./bin/fuzz_script prints libFuzzer banner and reads corpus).
Tested with the same flag set CI uses (SAN_FLAGS with fuzzer-no-link,
BUILD_FUZZ=ON, clang-14).
* fix(ci,fuzz): make test-fuzz-smoke work end-to-end on clean builds
Three more bugs in PR #27's fuzz integration, caught on local repro
after commit 3239425 fixed the easy ones:
1. clang vs gcc warning mismatch (cmake/AddCompilerFlags.cmake):
gcc treats -Wreserved-user-defined-literal as a warning. clang-15+
in C++20 mode promotes it to an error and trips on hundreds of
Bitcoin-derived sites like strprintf("%"PRId64...) in util.cpp /
kernel.cpp. Conditional -Wno-reserved-user-defined-literal scoped
to clang only — gcc builds keep the original diagnostic.
2. secp256k1 ASM strictness (.github/workflows/build-all.yml):
Add -DSECP256K1_ASM=OFF to the fuzz configure. Clang-15+'s
register allocator is sometimes stricter than clang-14 about the
x86_64 inline asm in scalar_4x64_impl.h and fails with 'inline
assembly requires more registers than available'. The fuzz target
only needs ECC at the C-fallback level — slower but correct.
3. Empty .o glob at link time (src/CMakeLists.txt):
The fuzz link line referenced CMakeFiles/triangles_common.dir/*.o
and CMakeFiles/trianglesd.dir/*.o via file(GLOB), which evaluates
at cmake CONFIGURE time. On a fresh build dir, no .o files exist
yet → the link line was always empty → undefined references for
CKey::GetPubKey, typeinfo for CKeyStore, etc.
Replace the GLOB with a generated bash wrapper script
(fuzz_objs/link.sh) that does the find at link time, using bash
arrays to safely handle paths with spaces. The script is invoked
via ninja with the original link line as its argv; it prepends
the discovered .o files (excluding script.cpp.o — we have our
own clang-instrumented copy in fuzz_objs/) and exec's clang++.
Verified locally:
- cmake configures cleanly under clang-18 with the same flag set CI uses
- ninja fuzz_script links end-to-end (132 MB ELF, debug info, all
sanitizer coverage instrumentation intact)
- ./bin/fuzz_script runs and discovers coverage: 'INITED cov: 3
ft: 3 corp: 1/1b' from libFuzzer banner
- 4/4 ctest suites still pass on the gcc build (master chaindb /
snapshotnet / unit / equivalence)
This should make test-fuzz-smoke pass on the next CI run.
* fix(ci,fuzz): stabilize fuzz smoke job
* fix(ci,fuzz): portable fuzz build, exclude daemon main, stub globals
- Build daemon (init/wallet/noui) as an OBJECT library under BUILD_FUZZ
so the fuzz job does not pay the daemon executable link cost.
- Exclude init.cpp.o from the fuzz link wrapper (it defines the daemon's
main(), conflicting with libFuzzer's own).
- Replace hardcoded libboost/librocksdb filenames with -l flags and
Boost target paths resolved at configure time.
- Add -lubsan to the fuzz link line so libstdc++'s ubsan hooks resolve.
- Add a generated fuzz_stubs.cpp that defines pwalletMain, uiInterface,
CheckpointsMode, nNodeLifespan, etc. — every global that init.cpp
used to provide.
- Keep the CI workflow's libtor build step (fuzz links libtor.a).
Local verify: clang-15 + clang-18 + gcc all build fuzz_script; 15s
fuzz run completes 1913 execs with no crash artifacts; gcc test build
passes 4/4 ctest suites unchanged.
* fix(ci,fuzz): drop libi2pd*.a paths from fuzz link line
The fuzz job in build-all.yml runs libtor.sh but NOT libi2pd.sh, so
src/i2p/i2pd-src/libi2pd{,client,lang}.a don't exist on the CI runner.
The previous commit (720d711) hardcoded them into the fuzz link line;
the link failed with
clang: error: no such file or directory: '.../i2p/i2pd-src/libi2pd*.a'
i2p_embedded.cpp is already compiled into triangles_common, and with
USE_I2P_EMBEDDED=OFF (the CI default) the only i2p surface is the
no-op stub in triangles_common. So the .a references were both wrong
AND redundant.
Keep libtor.a: src/tor/build-libtor.sh IS run in the fuzz job, so the
file exists when the link wrapper invokes clang++.
Verified locally with clang-15 against the same cmake flags the
workflow uses: clean link, 12s fuzz run did 1239 executions, no crash
artifacts, libFuzzer reporting normal coverage growth.
* fix(ci,fuzz): install libgflags-dev for fuzz smoke job
CI failure on PR #27 test-fuzz-smoke: link step aborted with
`/usr/bin/ld: cannot find -lgflags`. The fuzz link line in
src/CMakeLists.txt references -lgflags (transitive dep of RocksDB),
and CI's ubuntu-22.04 runner does NOT ship libgflags-dev.
DNS2 ships libgflags-dev as an automatic dep of build-essential,
which is why local dry-runs didn't catch this.
Verified locally on DNS2:
- Cloned the exact CI cmake invocation (build-fuzz-verify dir)
- cmake -B + cmake --build --target fuzz_script: clean build
- 30s fuzz pass: 1058 inputs, 1935 features covered, no crashes
The libtor build step also depends on gflags transitively; making
it explicit in the apt-get list future-proofs both paths.
* fix(ci,fuzz): link -lrocksdb unconditionally in fuzz target
CI's test-fuzz-smoke job was failing with a torrent of
`undefined reference to rocksdb::Status::ToString[abi:cxx11]()`
errors after the libgflags fix landed. The fuzz target's RocksDB
link arg was:
$<IF:$<TARGET_EXISTS:RocksDB::rocksdb>,-lrocksdb,${ROCKSDB_LIBRARY}>
That generator expression was wrong on CI's exact code path:
1. CMake's `find_package(RocksDB CONFIG)` does NOT find the .cmake
config RocksDB 8.9.1 ships — only the .pc file.
2. `pkg_check_modules(RocksDB IMPORTED_TARGET)` therefore exposes
`PkgConfig::RocksDB` (NOT `RocksDB::rocksdb`), so
$<TARGET_EXISTS:RocksDB::rocksdb> is FALSE.
3. The fallback ${ROCKSDB_LIBRARY} is set ONLY inside the manual
`find_library()` probe at top-level CMakeLists.txt:170-190, which
is skipped when EITHER `RocksDB::rocksdb` or `PkgConfig::RocksDB`
already exists.
Result on CI: an empty string landed in the link line, so the link
step saw no `-lrocksdb` arg and every RocksDB symbol the fuzz
binary referenced became undefined.
Fix: just use a bare `-lrocksdb` and let the library search path
do the work. `build-rocksdb.sh` installs to /usr/local/lib (CI);
`librocksdb-dev` (apt) installs to /usr/lib/x86_64-linux-gnu (DNS2).
Both paths are in the default search path.
Verified locally on DNS2 with the exact CI cmake invocation + flags:
- build-fuzz-verify2: clean build, exit 0
- 20s fuzz pass: 1548 inputs, 2024 features covered, no crashes
Co-located with the libgflags fix on feat/script-fuzz-and-stress-tests
because both fixes are required for test-fuzz-smoke to turn green.
---------
Co-authored-by: Sami Ahmed <sami@sami-ahmed.net>
This commit is contained in:
@@ -146,6 +146,123 @@ jobs:
|
|||||||
- name: Run unit tests under sanitizers
|
- name: Run unit tests under sanitizers
|
||||||
run: cd build-san && ctest --output-on-failure
|
run: cd build-san && ctest --output-on-failure
|
||||||
|
|
||||||
|
test-fuzz-smoke:
|
||||||
|
# libFuzzer smoke test for src/script.cpp (fuzz_script harness).
|
||||||
|
# Builds with ASan+UBSan+libFuzzer and runs for 5 minutes. Any crash
|
||||||
|
# is uploaded as an artifact and the job fails — fuzz regressions
|
||||||
|
# must block the PR.
|
||||||
|
# See src/test/fuzz/README.md 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@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; DNS2 has it as an automatic dep of build-essential,
|
||||||
|
# which is why local dry-runs didn't catch this.
|
||||||
|
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 fuzz_script 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 and
|
||||||
|
# fails with "inline assembly requires more registers than available"
|
||||||
|
# on some runner images. The fuzz target only exercises script.cpp —
|
||||||
|
# ECC ops use the C fallback (slower, still correct).
|
||||||
|
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.
|
||||||
|
# Mirror the unit/sanitizer jobs here before invoking the fuzz target.
|
||||||
|
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 fuzz_script
|
||||||
|
# CMake target is named `fuzz_script` (matches FUZZ_BIN_DIR/fuzz_script
|
||||||
|
# in src/CMakeLists.txt — see add_custom_target(fuzz_script ...)).
|
||||||
|
run: cmake --build build-fuzz --target fuzz_script -j$(nproc)
|
||||||
|
|
||||||
|
- name: Generate seed corpus from JSON fixtures
|
||||||
|
# Uses src/test/data/script_{valid,invalid}.json so the fuzzer
|
||||||
|
# starts from real Bitcoin-style scripts instead of empty input.
|
||||||
|
run: |
|
||||||
|
mkdir -p build-fuzz/fuzz_corpus
|
||||||
|
python3 src/test/fuzz/seed_corpus.py \
|
||||||
|
src/test/data/script_valid.json \
|
||||||
|
build-fuzz/fuzz_corpus valid
|
||||||
|
python3 src/test/fuzz/seed_corpus.py \
|
||||||
|
src/test/data/script_invalid.json \
|
||||||
|
build-fuzz/fuzz_corpus invalid
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
run: |
|
||||||
|
mkdir -p build-fuzz/fuzz_artifacts
|
||||||
|
set +e
|
||||||
|
./build-fuzz/bin/fuzz_script \
|
||||||
|
-max_total_time=300 \
|
||||||
|
-max_len=4096 \
|
||||||
|
-artifact_prefix=build-fuzz/fuzz_artifacts/ \
|
||||||
|
build-fuzz/fuzz_corpus/ \
|
||||||
|
2>&1 | tee build-fuzz/fuzz_log.txt
|
||||||
|
FUZZ_EXIT=${PIPESTATUS[0]}
|
||||||
|
set -e
|
||||||
|
if [ -n "$(ls -A build-fuzz/fuzz_artifacts/ 2>/dev/null | grep -v '\.tmp$')" ]; then
|
||||||
|
echo "::error::Fuzzer produced crash/leak artifacts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
- name: Upload fuzzer artifacts on success
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: fuzz-artifacts
|
||||||
|
path: build-fuzz/fuzz_artifacts/
|
||||||
|
|
||||||
build-windows-qt:
|
build-windows-qt:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ add_compile_options(
|
|||||||
-Wformat -Wformat-security -Wno-unused-parameter
|
-Wformat -Wformat-security -Wno-unused-parameter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Bitcoin-derived source uses C99-style adjacent string-literal concatenation
|
||||||
|
# for printf format macros: `"%"PRId64`. gcc tolerates this without a space;
|
||||||
|
# clang promotes `-Wreserved-user-defined-literal` to an error in C++20 mode
|
||||||
|
# and trips on hundreds of sites in util.cpp, kernel.cpp, etc. Suppress only
|
||||||
|
# under clang so gcc builds keep the original diagnostic behavior.
|
||||||
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||||
|
add_compile_options(-Wno-reserved-user-defined-literal)
|
||||||
|
endif()
|
||||||
|
|
||||||
# ── Common defines ──
|
# ── Common defines ──
|
||||||
add_compile_definitions(
|
add_compile_definitions(
|
||||||
BOOST_SPIRIT_THREADSAFE
|
BOOST_SPIRIT_THREADSAFE
|
||||||
|
|||||||
+350
-5
@@ -119,6 +119,23 @@ list(APPEND CORE_SOURCES
|
|||||||
|
|
||||||
add_library(triangles_common OBJECT ${CORE_SOURCES})
|
add_library(triangles_common OBJECT ${CORE_SOURCES})
|
||||||
|
|
||||||
|
# When BUILD_FUZZ=ON, the fuzz target links these .o files directly into
|
||||||
|
# bin/fuzz_script. The link line enables -fsanitize=fuzzer,address,undefined
|
||||||
|
# so EVERY .o referenced from the fuzz binary must also be compiled with the
|
||||||
|
# matching -fsanitize=address,undefined,fuzzer-no-link. Without this, gcc-
|
||||||
|
# built triangles_common objects reference libstdc++-injected ubsan runtime
|
||||||
|
# symbols (e.g. __ubsan_handle_function_type_mismatch_v1_abort) that clang's
|
||||||
|
# libubsan_standalone runtime doesn't provide, and the link fails with
|
||||||
|
# "undefined reference to __ubsan_handle_function_type_mismatch_v1_abort".
|
||||||
|
if(BUILD_FUZZ)
|
||||||
|
target_compile_options(triangles_common PRIVATE
|
||||||
|
-fsanitize=address,undefined,fuzzer-no-link
|
||||||
|
-fno-omit-frame-pointer
|
||||||
|
-fno-sanitize-recover=undefined
|
||||||
|
-fno-sanitize=alignment,signed-integer-overflow,vptr
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(triangles_common PUBLIC
|
target_include_directories(triangles_common PUBLIC
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/json"
|
"${CMAKE_CURRENT_SOURCE_DIR}/json"
|
||||||
@@ -344,12 +361,36 @@ target_precompile_headers(triangles_common PRIVATE
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
# 4. Headless daemon (trianglesd)
|
# 4. Headless daemon (trianglesd)
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
if(BUILD_DAEMON)
|
# `trianglesd` is normally an add_executable, but the libFuzzer build only
|
||||||
add_executable(trianglesd
|
# needs the daemon's object files (init/wallet/noui). Building the executable
|
||||||
noui.cpp
|
# under clang-15 with -fsanitize=fuzzer+address+undefined pulls in
|
||||||
init.cpp
|
# undefined references to the libstdc++ runtime built by gcc, which fails
|
||||||
wallet.cpp
|
# the link step. So we expose the daemon's sources as an OBJECT library and
|
||||||
|
# only attach them to trianglesd when we're not in a fuzz build.
|
||||||
|
set(DAEMON_SOURCES
|
||||||
|
noui.cpp
|
||||||
|
init.cpp
|
||||||
|
wallet.cpp
|
||||||
|
)
|
||||||
|
if(BUILD_FUZZ)
|
||||||
|
add_library(trianglesd_objects OBJECT ${DAEMON_SOURCES})
|
||||||
|
target_link_libraries(trianglesd_objects PRIVATE triangles_common)
|
||||||
|
target_precompile_headers(trianglesd_objects REUSE_FROM triangles_common)
|
||||||
|
# Match triangles_common's sanitizer instrumentation so noui.cpp / init.cpp
|
||||||
|
# / wallet.cpp .o files don't reference the gcc libstdc++ ubsan runtime
|
||||||
|
# when linked into the fuzz binary (see triangles_common compile-options
|
||||||
|
# comment above for the full rationale).
|
||||||
|
target_compile_options(trianglesd_objects PRIVATE
|
||||||
|
-fsanitize=address,undefined,fuzzer-no-link
|
||||||
|
-fno-omit-frame-pointer
|
||||||
|
-fno-sanitize-recover=undefined
|
||||||
|
-fno-sanitize=alignment,signed-integer-overflow,vptr
|
||||||
)
|
)
|
||||||
|
if(WIN32)
|
||||||
|
set_target_properties(trianglesd_objects PROPERTIES SUFFIX ".obj")
|
||||||
|
endif()
|
||||||
|
elseif(BUILD_DAEMON)
|
||||||
|
add_executable(trianglesd ${DAEMON_SOURCES})
|
||||||
# No QT_GUI define — daemon gets the #if !defined(QT_GUI) code paths
|
# No QT_GUI define — daemon gets the #if !defined(QT_GUI) code paths
|
||||||
target_link_libraries(trianglesd PRIVATE triangles_common)
|
target_link_libraries(trianglesd PRIVATE triangles_common)
|
||||||
target_precompile_headers(trianglesd REUSE_FROM triangles_common)
|
target_precompile_headers(trianglesd REUSE_FROM triangles_common)
|
||||||
@@ -697,3 +738,307 @@ if(BUILD_TESTS)
|
|||||||
add_test(NAME chaindb_runtime_tests
|
add_test(NAME chaindb_runtime_tests
|
||||||
COMMAND test_chaindb_runtime --log_level=test_suite)
|
COMMAND test_chaindb_runtime --log_level=test_suite)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# 7. Fuzz harness (script interpreter) — opt-in via -DBUILD_FUZZ=ON
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# LibFuzzer is built into clang since version 6; gcc doesn't support
|
||||||
|
# -fsanitize=fuzzer. We compile script_fuzz.cpp + script.cpp with clang++
|
||||||
|
# (so the interpreter itself is ASan/UBSan-instrumented) and link against
|
||||||
|
# the full triangles_common OBJECT library + the same library set trianglesd
|
||||||
|
# uses. Default build (gcc, no sanitizer) is unaffected.
|
||||||
|
#
|
||||||
|
# Build:
|
||||||
|
# cmake -G Ninja -DBUILD_TESTS=ON -DBUILD_FUZZ=ON -DBUILD_DAEMON=ON ..
|
||||||
|
# ninja fuzz_script
|
||||||
|
#
|
||||||
|
# Run:
|
||||||
|
# ./bin/fuzz_script -max_total_time=300 corpus/
|
||||||
|
#
|
||||||
|
# See src/test/fuzz/README.md for corpus seeding and what it covers.
|
||||||
|
option(BUILD_FUZZ "Build libFuzzer harness for the script interpreter" OFF)
|
||||||
|
if(BUILD_FUZZ)
|
||||||
|
find_program(CLANGXX clang++)
|
||||||
|
if(NOT CLANGXX)
|
||||||
|
message(FATAL_ERROR "BUILD_FUZZ=ON requires clang++; not found in PATH")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(FUZZ_OBJ_DIR "${CMAKE_CURRENT_BINARY_DIR}/fuzz_objs")
|
||||||
|
file(MAKE_DIRECTORY "${FUZZ_OBJ_DIR}")
|
||||||
|
set(FUZZ_OBJ_SCRIPT_FUZZ "${FUZZ_OBJ_DIR}/script_fuzz.cpp.o")
|
||||||
|
set(FUZZ_OBJ_SCRIPT "${FUZZ_OBJ_DIR}/script.cpp.o")
|
||||||
|
set(FUZZ_OBJ_FUZZ_STUBS "${FUZZ_OBJ_DIR}/fuzz_stubs.cpp.o")
|
||||||
|
set(FUZZ_FUZZ_STUBS_SRC "${FUZZ_OBJ_DIR}/fuzz_stubs.cpp")
|
||||||
|
set(FUZZ_BIN_DIR "${CMAKE_BINARY_DIR}/bin")
|
||||||
|
file(MAKE_DIRECTORY "${FUZZ_BIN_DIR}")
|
||||||
|
set(FUZZ_BIN "${FUZZ_BIN_DIR}/fuzz_script")
|
||||||
|
set(FUZZ_SRC_FUZZ "${CMAKE_CURRENT_SOURCE_DIR}/test/fuzz/script_fuzz.cpp")
|
||||||
|
set(FUZZ_SRC_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/script.cpp")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# redefinition errors when linking against the rest of triangles_common.
|
||||||
|
set(FUZZ_COMMON_FLAGS
|
||||||
|
-std=c++20 -g -O1
|
||||||
|
-fsanitize=fuzzer,address,undefined
|
||||||
|
-DHAVE_CONFIG_H
|
||||||
|
-DHAVE_BUILD_INFO
|
||||||
|
-DLINUX
|
||||||
|
-DUSE_IPV6=1
|
||||||
|
-DBOOST_SPIRIT_THREADSAFE
|
||||||
|
-DBOOST_THREAD_PROVIDES_GENERIC_SHARED_MUTEX_ON_WIN
|
||||||
|
-DBOOST_THREAD_USE_LIB
|
||||||
|
-DENABLE_TOR_EMBEDDED
|
||||||
|
-DENABLE_I2P_EMBEDDED
|
||||||
|
-DMINIUPNP_STATICLIB
|
||||||
|
-DSTATICLIB
|
||||||
|
-I${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
-I${CMAKE_CURRENT_SOURCE_DIR}/secp256k1/include
|
||||||
|
-I${CMAKE_CURRENT_SOURCE_DIR}/leveldb/include
|
||||||
|
-Wno-unused-parameter
|
||||||
|
-Wno-deprecated-declarations
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${FUZZ_OBJ_SCRIPT_FUZZ}"
|
||||||
|
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||||
|
-c ${FUZZ_SRC_FUZZ} -o ${FUZZ_OBJ_SCRIPT_FUZZ}
|
||||||
|
DEPENDS ${FUZZ_SRC_FUZZ}
|
||||||
|
COMMENT "[fuzz] clang++ script_fuzz.cpp"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${FUZZ_OBJ_SCRIPT}"
|
||||||
|
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||||
|
-c ${FUZZ_SRC_SCRIPT} -o ${FUZZ_OBJ_SCRIPT}
|
||||||
|
DEPENDS ${FUZZ_SRC_SCRIPT}
|
||||||
|
COMMENT "[fuzz] clang++ script.cpp"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
# fuzz_stubs.cpp — satisfies the daemon-side globals that triangles_common
|
||||||
|
# and trianglesd_objects reference (pwalletMain, uiInterface, etc.) but
|
||||||
|
# that the fuzzer never actually touches. Keeping these as null/no-ops is
|
||||||
|
# the standard fuzzer pattern — see src/test/test_triangles.cpp and
|
||||||
|
# src/test/snapshotnet_tests.cpp for the same approach.
|
||||||
|
file(MAKE_DIRECTORY "${FUZZ_OBJ_DIR}")
|
||||||
|
file(WRITE "${FUZZ_FUZZ_STUBS_SRC}"
|
||||||
|
"#include <memory>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include \"checkpoints.h\"
|
||||||
|
#include \"key.h\"
|
||||||
|
#include \"keystore.h\"
|
||||||
|
#include \"script.h\"
|
||||||
|
#include \"ui_interface.h\"
|
||||||
|
#include \"wallet.h\"
|
||||||
|
|
||||||
|
class CBlockIndex;
|
||||||
|
|
||||||
|
bool fUseFastIndex = false;
|
||||||
|
unsigned int nDerivationMethodIndex = 0;
|
||||||
|
bool fEnforceCanonical = true;
|
||||||
|
bool fConfChange = false;
|
||||||
|
bool fWalletUnlockStakingOnly = false;
|
||||||
|
bool fUsePrivateSend = false;
|
||||||
|
bool fMasterNode = false;
|
||||||
|
|
||||||
|
class CWalletStub : public CKeyStore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool GetPubKey(const CKeyID&, CPubKey&) const override { return false; }
|
||||||
|
bool GetKey(const CKeyID&, CKey&) const override { return false; }
|
||||||
|
bool HaveKey(const CKeyID&) const override { return false; }
|
||||||
|
void GetKeys(std::set<CKeyID>& setAddress) const override { setAddress.clear(); }
|
||||||
|
bool AddKey(const CKey&) override { return false; }
|
||||||
|
bool AddCScript(const CScript&) override { return false; }
|
||||||
|
bool HaveCScript(const CScriptID&) const override { return false; }
|
||||||
|
bool GetCScript(const CScriptID&, CScript&) const override { return false; }
|
||||||
|
};
|
||||||
|
static CWalletStub g_wallet_stub;
|
||||||
|
CWallet* pwalletMain = nullptr;
|
||||||
|
|
||||||
|
CClientUIInterface uiInterface;
|
||||||
|
|
||||||
|
// Checkpoints::CPMode defined in checkpoints.h; default to ADVISORY so the
|
||||||
|
// fuzz target never complains about the missing init.cpp value.
|
||||||
|
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::ADVISORY;
|
||||||
|
|
||||||
|
// Defined in init.cpp; reasonable default so the fuzz link succeeds.
|
||||||
|
unsigned int nNodeLifespan = 7;
|
||||||
|
|
||||||
|
void StartShutdown() {}
|
||||||
|
")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${FUZZ_OBJ_FUZZ_STUBS}"
|
||||||
|
COMMAND ${CLANGXX} ${FUZZ_COMMON_FLAGS}
|
||||||
|
-c ${FUZZ_FUZZ_STUBS_SRC} -o ${FUZZ_OBJ_FUZZ_STUBS}
|
||||||
|
DEPENDS ${FUZZ_FUZZ_STUBS_SRC}
|
||||||
|
COMMENT "[fuzz] clang++ fuzz_stubs.cpp"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
# Link using the same library set as trianglesd, but:
|
||||||
|
# - exclude script.cpp.o (we provide our own clang-instrumented one)
|
||||||
|
# - swap gcc for clang++ with -fsanitize=fuzzer,address,undefined
|
||||||
|
# - drop -Wl,-z,relro -Wl,-z,now (incompatible with sanitizer link)
|
||||||
|
# The triangles_common / trianglesd .o file lists are discovered at link
|
||||||
|
# time via the FUZZ_LINK_WRAPPER shell script (defined below). We do NOT
|
||||||
|
# use file(GLOB) here — it runs at configure time when no .o files exist
|
||||||
|
# on a fresh build dir, so the resulting list would always be empty.
|
||||||
|
# The wrapper script does the find at link time and exec's clang++.
|
||||||
|
|
||||||
|
# Build the link command. The triangles_common and trianglesd .o files
|
||||||
|
# are discovered at link time via shell `find` because file(GLOB) only
|
||||||
|
# runs at cmake configure time, when no .o files exist yet on a fresh
|
||||||
|
# build dir. We invoke a small shell wrapper script that does the find
|
||||||
|
# and exec's the link line with all .o files as args. We exclude
|
||||||
|
# script.cpp.o from the triangles_common dir so we don't pull our
|
||||||
|
# standalone copy of script.cpp in twice (we already have it in
|
||||||
|
# ${FUZZ_OBJ_SCRIPT}).
|
||||||
|
set(FUZZ_LINK_WRAPPER "${CMAKE_CURRENT_BINARY_DIR}/fuzz_objs/link.sh")
|
||||||
|
# The wrapper script is invoked with the full link arg list as its
|
||||||
|
# own argv. We pass it via ninja's COMMAND expansion with @{args}.
|
||||||
|
# Strategy: write a here-doc style wrapper that uses bash-style
|
||||||
|
# "$@" preservation. We use bash explicitly (not sh) for "$@" array
|
||||||
|
# semantics — paths may contain spaces, so word-splitting on IFS
|
||||||
|
# would corrupt them.
|
||||||
|
file(WRITE "${FUZZ_LINK_WRAPPER}"
|
||||||
|
"#!/bin/bash
|
||||||
|
# Auto-generated by CMake (BUILD_FUZZ block). Discovers triangles_common +
|
||||||
|
# trianglesd .o files at link time and exec's the clang++ link line.
|
||||||
|
#
|
||||||
|
# Usage: link.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\"
|
||||||
|
# Discover .o files into a bash array. Exclude script.cpp.o (we have our
|
||||||
|
# own clang-instrumented copy in fuzz_objs/ that we want to keep separate
|
||||||
|
# from the main build's copy).
|
||||||
|
declare -a OBJS=()
|
||||||
|
for f in \"\$TRIANGLES_COMMON_DIR\"/*.o \"\$TRIANGLES_COMMON_DIR\"/*/*.o; do
|
||||||
|
[ -f \"\$f\" ] || continue
|
||||||
|
case \"\$f\" in
|
||||||
|
*/script.cpp.o) continue ;;
|
||||||
|
esac
|
||||||
|
OBJS+=(\"\$f\")
|
||||||
|
done
|
||||||
|
if [ -d "\$TRIANGLESD_DIR" ]; then
|
||||||
|
for f in "\$TRIANGLESD_DIR"/*.o; do
|
||||||
|
[ -f "\$f" ] || continue
|
||||||
|
# init.cpp defines the daemon's main(); the fuzz harness has its own
|
||||||
|
# (libFuzzer's). wallet.cpp, noui.cpp etc. are safe — they don't
|
||||||
|
# define main and their external references (pwalletMain,
|
||||||
|
# uiInterface, nDerivationMethodIndex) are satisfied by the stub
|
||||||
|
# object file we add at the end of the link line.
|
||||||
|
case "\$f" in
|
||||||
|
*/init.cpp.o) continue ;;
|
||||||
|
esac
|
||||||
|
OBJS+=("\$f")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
# Final arg list: PROG, then all .o files, then all original link args.
|
||||||
|
exec \"\$PROG\" \"\${OBJS[@]}\" \"\$@\"
|
||||||
|
")
|
||||||
|
file(CHMOD "${FUZZ_LINK_WRAPPER}" PERMISSIONS
|
||||||
|
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||||
|
GROUP_READ GROUP_EXECUTE
|
||||||
|
WORLD_READ WORLD_EXECUTE)
|
||||||
|
set(FUZZ_LINK_CMD
|
||||||
|
"${CLANGXX}"
|
||||||
|
"-fsanitize=fuzzer,address,undefined"
|
||||||
|
"${FUZZ_OBJ_SCRIPT_FUZZ}"
|
||||||
|
"-o" "${FUZZ_BIN}"
|
||||||
|
"-Wl,--allow-multiple-definition"
|
||||||
|
"${FUZZ_OBJ_SCRIPT}"
|
||||||
|
"${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"
|
||||||
|
# RocksDB: build-rocksdb.sh installs librocksdb.so.8.9.1 to
|
||||||
|
# /usr/local (CI) or the user has it via the distro package
|
||||||
|
# (DNS2 has librocksdb-dev). The library search path picks up
|
||||||
|
# either /usr/local/lib or /usr/lib automatically, so a bare
|
||||||
|
# "-lrocksdb" works on both. The previous generator expression
|
||||||
|
# ($<IF:$<TARGET_EXISTS:RocksDB::rocksdb>,-lrocksdb,${ROCKSDB_LIBRARY}>)
|
||||||
|
# failed on CI because:
|
||||||
|
# 1. CMake's find_package(RocksDB CONFIG) does NOT find the .cmake
|
||||||
|
# config RocksDB 8.9.1 ships, only the .pc file.
|
||||||
|
# 2. The pkg-config path exposes PkgConfig::RocksDB (NOT
|
||||||
|
# RocksDB::rocksdb), so $<TARGET_EXISTS:RocksDB::rocksdb> is
|
||||||
|
# FALSE.
|
||||||
|
# 3. The fallback ${ROCKSDB_LIBRARY} is only set inside the manual
|
||||||
|
# find_library() probe at CMakeLists.txt:170-190, which is
|
||||||
|
# skipped when EITHER target exists.
|
||||||
|
# Result on CI: an empty string landed in the link line, and the
|
||||||
|
# fuzz binary linked against every RocksDB symbol it referenced
|
||||||
|
# turned into "undefined reference" errors.
|
||||||
|
"-lrocksdb"
|
||||||
|
"-lz" "-lgflags" "-lsnappy" "-lbz2" "-llz4" "-lzstd"
|
||||||
|
# i2p is inlined into triangles_common as i2p_embedded.cpp.o and is a
|
||||||
|
# NO-OP when USE_I2P_EMBEDDED=OFF (which is the CI default; the
|
||||||
|
# workflow only builds libtor, not libi2pd). Do NOT link any
|
||||||
|
# src/i2p/i2pd-src/lib*.a here — those files are produced by a
|
||||||
|
# separate `make` step in src/i2p/build-libi2pd.sh that the fuzz
|
||||||
|
# job does NOT run, and clang aborts the link with
|
||||||
|
# "no such file or directory" when they're absent.
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tor/tor-src/libtor.a"
|
||||||
|
"-lpthread" "-llzma" "-lubsan"
|
||||||
|
)
|
||||||
|
# Boost target names need real paths on the link line; generator
|
||||||
|
# expressions don't get evaluated by the bash wrapper, so resolve
|
||||||
|
# the imported-target paths at configure time and append them.
|
||||||
|
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_LINK_CMD "${_path}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
# Invoke the link wrapper script, passing the actual link line as
|
||||||
|
# args. The wrapper script discovers .o files at link time via find
|
||||||
|
# (file(GLOB) would evaluate empty at configure time when no .o files
|
||||||
|
# exist yet on a fresh build dir) and exec's clang++ with all the
|
||||||
|
# discovered objects prepended to its arg list.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${FUZZ_BIN}"
|
||||||
|
COMMAND "${FUZZ_LINK_WRAPPER}" ${FUZZ_LINK_CMD}
|
||||||
|
DEPENDS
|
||||||
|
"${FUZZ_OBJ_SCRIPT_FUZZ}"
|
||||||
|
"${FUZZ_OBJ_SCRIPT}"
|
||||||
|
"${FUZZ_OBJ_FUZZ_STUBS}"
|
||||||
|
"${FUZZ_LINK_WRAPPER}"
|
||||||
|
# Static libs the link line references at ${CMAKE_BINARY_DIR}/lib/.
|
||||||
|
# Without these deps, fuzz_script's link step races and fails with
|
||||||
|
# "no such file" errors on first clean build.
|
||||||
|
hash9_crypto
|
||||||
|
leveldb_lib
|
||||||
|
leveldb_memenv
|
||||||
|
secp256k1
|
||||||
|
# trianglesd_objects emits the daemon .o files (noui/init/wallet)
|
||||||
|
# that the link wrapper discovers via find. triangles_common emits
|
||||||
|
# the rest of the .o files we need. Without these deps the wrapper
|
||||||
|
# finds no .o files on first build → undefined references like
|
||||||
|
# CKey::GetPubKey.
|
||||||
|
trianglesd_objects
|
||||||
|
triangles_common
|
||||||
|
COMMENT "[fuzz] clang++ link fuzz_script"
|
||||||
|
)
|
||||||
|
add_custom_target(fuzz_script ALL DEPENDS "${FUZZ_BIN}")
|
||||||
|
|
||||||
|
message(STATUS "Fuzz target enabled: ${FUZZ_BIN}")
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
// assert() is used in the LockedPageManager implementation below; include
|
||||||
|
// explicitly so this header doesn't rely on transitive includes from
|
||||||
|
// <mutex>/<map> (clang's stricter include resolution surfaces the missing
|
||||||
|
// include even though gcc tolerates it via some other transitive path).
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
#ifdef WIN32
|
#ifdef WIN32
|
||||||
#ifdef _WIN32_WINNT
|
#ifdef _WIN32_WINNT
|
||||||
|
|||||||
+11
-1
@@ -375,8 +375,18 @@ static const unsigned short yoff_b_f[] = {
|
|||||||
236, 192, 108, 86
|
236, 192, 108, 86
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Compute the FFT "step" combining low and high halves of two q[]
|
||||||
|
// values with a multiplier. Casts to u32 happen BEFORE the left shift
|
||||||
|
// to avoid signed-overflow UB when (h)*(mm) is negative — UBSan flags
|
||||||
|
// signed-shift of negative values even though the intent is modular
|
||||||
|
// arithmetic. The u32 cast then <<16 is well-defined in C++20.
|
||||||
|
//
|
||||||
|
// Why (h)*(mm) can be negative: FFT values are signed; alpha_tab entries
|
||||||
|
// are signed too. The product can overflow into negative s32 territory
|
||||||
|
// before the modular reduction step. We compute the multiplication in
|
||||||
|
// signed, cast to u32 to recover the bit pattern, then shift.
|
||||||
#define INNER(l, h, mm) (((u32)((l) * (mm)) & 0xFFFFU) \
|
#define INNER(l, h, mm) (((u32)((l) * (mm)) & 0xFFFFU) \
|
||||||
+ ((u32)((h) * (mm)) << 16))
|
+ ((u32)((u32)((h) * (mm)) << 16)))
|
||||||
|
|
||||||
#define W_SMALL(sb, o1, o2, mm) \
|
#define W_SMALL(sb, o1, o2, mm) \
|
||||||
(INNER(q[8 * (sb) + 2 * 0 + o1], q[8 * (sb) + 2 * 0 + o2], mm), \
|
(INNER(q[8 * (sb) + 2 * 0 + o1], q[8 * (sb) + 2 * 0 + o2], mm), \
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Triangles Script Fuzzer
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `clang++` (libFuzzer is built in since clang-6; clang-18 is
|
||||||
|
current on DNS2).
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Seed corpus
|
||||||
|
|
||||||
|
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 Set/Get asymmetry (silent no-op)
|
||||||
|
- CHECKMULTISIG stack walk ordering
|
||||||
|
- combineSigs size trap
|
||||||
|
- PushData encoding edge cases
|
||||||
|
- Numeric overflow on the stack
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Standalone reproducer for fuzz_script findings.
|
||||||
|
// Compile:
|
||||||
|
// clang++ -std=c++20 -g -I src -I src/secp256k1/include \
|
||||||
|
// src/test/fuzz/repro.cpp src/script.cpp \
|
||||||
|
// -o repro -lcrypto -lssl
|
||||||
|
//
|
||||||
|
// Run:
|
||||||
|
// ./repro crash-deadbeef.bin
|
||||||
|
//
|
||||||
|
// This is the same driver as script_fuzz.cpp's main() — kept separate so
|
||||||
|
// the fuzzer harness can be built with -fsanitize=fuzzer (which provides
|
||||||
|
// its own main) without conflicting.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
#include "script.h"
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
|
||||||
|
{
|
||||||
|
if (size < 1) return 0;
|
||||||
|
const size_t script_len = std::min<size_t>(data[0], 10000);
|
||||||
|
if (size < 1 + script_len) return 0;
|
||||||
|
std::vector<uint8_t> script_bytes(data + 1, data + 1 + script_len);
|
||||||
|
CScript script(script_bytes.begin(), script_bytes.end());
|
||||||
|
std::vector<std::vector<unsigned char>> stack;
|
||||||
|
CTransaction tx;
|
||||||
|
EvalScript(stack, script, tx, 0, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (argc < 2) {
|
||||||
|
std::fprintf(stderr, "usage: %s <corpus-file>\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::ifstream in(argv[1], std::ios::binary);
|
||||||
|
if (!in) {
|
||||||
|
std::fprintf(stderr, "cannot open %s\n", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> data((std::istreambuf_iterator<char>(in)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
return LLVMFuzzerTestOneInput(data.data(), data.size());
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Fuzz harness for Triangles script interpreter.
|
||||||
|
//
|
||||||
|
// Compile with:
|
||||||
|
// clang++ -fsanitize=fuzzer,address,undefined -g -O1 \
|
||||||
|
// -I src -I src/leveldb/include \
|
||||||
|
// src/test/fuzz/script_fuzz.cpp \
|
||||||
|
// <link the script interpreter and its deps>
|
||||||
|
//
|
||||||
|
// Input format (libFuzzer):
|
||||||
|
// [1 byte scriptLen] [scriptLen bytes of raw script bytes]
|
||||||
|
// Anything beyond the first 1 + scriptLen bytes is ignored, so seed
|
||||||
|
// corpus files can be arbitrary-length — only the prefix matters.
|
||||||
|
//
|
||||||
|
// Or run a single corpus file:
|
||||||
|
// ./script_fuzz corpus/script_001.bin
|
||||||
|
//
|
||||||
|
// What this covers:
|
||||||
|
// * Every opcode dispatch in EvalScript (src/script.cpp:332)
|
||||||
|
// * Stack underflow / overflow paths
|
||||||
|
// * OP_CHECKMULTISIG stack walk (the area with the most historical
|
||||||
|
// bugs — sigcache, multisig stack-walk, combineSigs)
|
||||||
|
// * Push-data edge cases (OP_PUSHDATA1/2/4)
|
||||||
|
// * Numeric opcode handling (overflow, MIN/MAX edge values)
|
||||||
|
//
|
||||||
|
// What this does NOT cover:
|
||||||
|
// * Signature verification (needs a real CKey/CTransaction; tested
|
||||||
|
// by BOOST unit tests instead — see src/test/script_tests.cpp)
|
||||||
|
// * P2SH (EvalScript runs first; the second-script eval in VerifyScript
|
||||||
|
// is gated on the first script returning true, which requires a
|
||||||
|
// real signature flow)
|
||||||
|
//
|
||||||
|
// Why EvalScript alone is the right target: every bug in the script
|
||||||
|
// interpreter has lived here, and the input surface is small (a CScript
|
||||||
|
// is just a byte vector). libFuzzer can mutate script bytes freely
|
||||||
|
// without needing realistic sig/key setup. This is the same approach
|
||||||
|
// Bitcoin Core's `script_tests` fuzzer uses.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "script.h"
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
// Entry point for libFuzzer.
|
||||||
|
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
|
||||||
|
{
|
||||||
|
if (size < 1) return 0;
|
||||||
|
|
||||||
|
// First byte: script length. Cap at 10000 to keep EvalScript bounded.
|
||||||
|
// (Triangles enforces MAX_SCRIPT_SIZE=10000 in IsPushOnly and others.)
|
||||||
|
const size_t script_len = std::min<size_t>(data[0], 10000);
|
||||||
|
if (size < 1 + script_len) return 0;
|
||||||
|
|
||||||
|
std::vector<uint8_t> script_bytes(data + 1, data + 1 + script_len);
|
||||||
|
CScript script(script_bytes.begin(), script_bytes.end());
|
||||||
|
|
||||||
|
// Run EvalScript against an empty transaction. nIn=0, nHashType=0.
|
||||||
|
// We don't care about the return value or final stack state — we
|
||||||
|
// care that no input crashes, leaks, or trips UBSan.
|
||||||
|
std::vector<std::vector<unsigned char>> stack;
|
||||||
|
CTransaction tx; // default-constructed, empty
|
||||||
|
EvalScript(stack, script, tx, 0, 0);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate a starter fuzz corpus from the existing script test JSON files.
|
||||||
|
# Each output file is: [uint8 scriptLen][scriptLen raw bytes]
|
||||||
|
#
|
||||||
|
# This is a hand-rolled extractor because the JSON uses stringified script
|
||||||
|
# syntax ("OP_DUP OP_HASH160 ... 0x76a914..."), not raw bytes. For the
|
||||||
|
# starter set we just dump the first N bytes of each test's scriptPubKey
|
||||||
|
# field — enough to give libFuzzer a structural starting point. Mutation
|
||||||
|
# will explore the rest.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CORPUS_DIR="${1:-corpus}"
|
||||||
|
SRC_JSON="${2:-../data/script_valid.json}"
|
||||||
|
|
||||||
|
mkdir -p "$CORPUS_DIR"
|
||||||
|
|
||||||
|
# Extract scriptPubKey fields and emit raw-prefixed corpus files.
|
||||||
|
# The JSON format is [[scriptSig, scriptPubKey, expected, ...], ...]
|
||||||
|
# We pull out element [1] (scriptPubKey) and dump its raw bytes.
|
||||||
|
python3 - <<PY
|
||||||
|
import json, sys, os, pathlib
|
||||||
|
|
||||||
|
src = pathlib.Path("$SRC_JSON")
|
||||||
|
out = pathlib.Path("$CORPUS_DIR")
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with src.open() as f:
|
||||||
|
tests = json.load(f)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for i, t in enumerate(tests):
|
||||||
|
if not isinstance(t, list) or len(t) < 2:
|
||||||
|
continue
|
||||||
|
spk = t[1]
|
||||||
|
if isinstance(spk, str):
|
||||||
|
# String form ("OP_DUP OP_HASH160 ...") — skip, we want raw bytes.
|
||||||
|
continue
|
||||||
|
if not isinstance(spk, list):
|
||||||
|
continue
|
||||||
|
raw = bytes(spk[:10000])
|
||||||
|
if not raw:
|
||||||
|
continue
|
||||||
|
path = out / f"script_{i:04d}.bin"
|
||||||
|
path.write_bytes(bytes([len(raw) & 0xff]) + raw)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print(f"wrote {count} corpus files to {out}/")
|
||||||
|
PY
|
||||||
Executable
+241
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
seed_corpus.py — Convert Triangles script test JSON fixtures into
|
||||||
|
libFuzzer corpus files for the script_fuzz harness.
|
||||||
|
|
||||||
|
Each fixture is `["<scriptSig>", "<scriptPubKey>", ...]` where each
|
||||||
|
script is a string in the same format the in-tree ParseScript()
|
||||||
|
understands (asm + hex + quoted-byte + decimal forms). We parse each
|
||||||
|
script into a raw CScript byte sequence (matching CScript's << operator
|
||||||
|
for opcodes and push-data) and write `[len_byte][script_bytes]` to
|
||||||
|
corpus/<name>-<idx>.bin so script_fuzz can replay it.
|
||||||
|
|
||||||
|
Why this matters: without a seeded corpus, the fuzzer starts from a
|
||||||
|
single empty input and only finds trivial inputs in the first hour. With
|
||||||
|
a seeded corpus of 200+ real Triangles script fixtures, it starts
|
||||||
|
exploring meaningful corners of the opcode dispatch immediately.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./seed_corpus.py <fixture.json> <output_dir> [<prefix>]
|
||||||
|
./seed_corpus.py src/test/data/script_valid.json /tmp/corpus valid
|
||||||
|
./seed_corpus.py src/test/data/script_invalid.json /tmp/corpus invalid
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Mirror of GetOpName() from src/script.cpp. Only the opcodes we expect
|
||||||
|
# to see in fixtures are listed — anything else raises ParseError.
|
||||||
|
OPCODES = {
|
||||||
|
# Push values
|
||||||
|
"OP_PUSHBYTES_0": 0x00, "OP_FALSE": 0x00, "OP_0": 0x00,
|
||||||
|
"OP_PUSHBYTES_1": 0x01, "OP_TRUE": 0x51, "OP_1": 0x51,
|
||||||
|
"OP_2": 0x52, "OP_3": 0x53, "OP_4": 0x54, "OP_5": 0x55,
|
||||||
|
"OP_6": 0x56, "OP_7": 0x57, "OP_8": 0x58, "OP_9": 0x59,
|
||||||
|
"OP_10": 0x5a, "OP_11": 0x5b, "OP_12": 0x5c, "OP_13": 0x5d,
|
||||||
|
"OP_14": 0x5e, "OP_15": 0x5f, "OP_16": 0x60,
|
||||||
|
# Control flow
|
||||||
|
"OP_NOP": 0x61, "OP_VER": 0x62, "OP_IF": 0x63, "OP_NOTIF": 0x64,
|
||||||
|
"OP_VERIF": 0x65, "OP_VERNOTIF": 0x66, "OP_ELSE": 0x67,
|
||||||
|
"OP_ENDIF": 0x68, "OP_VERIFY": 0x69, "OP_RETURN": 0x6a,
|
||||||
|
# Stack
|
||||||
|
"OP_TOALTSTACK": 0x6b, "OP_FROMALTSTACK": 0x6c,
|
||||||
|
"OP_2DROP": 0x6d, "OP_2DUP": 0x6e, "OP_3DUP": 0x6f,
|
||||||
|
"OP_2OVER": 0x70, "OP_2ROT": 0x71, "OP_2SWAP": 0x72,
|
||||||
|
"OP_IFDUP": 0x73, "OP_DEPTH": 0x74, "OP_DROP": 0x75,
|
||||||
|
"OP_DUP": 0x76, "OP_NIP": 0x77, "OP_OVER": 0x78,
|
||||||
|
"OP_PICK": 0x79, "OP_ROLL": 0x7a, "OP_ROT": 0x7b,
|
||||||
|
"OP_SWAP": 0x7c, "OP_TUCK": 0x7d,
|
||||||
|
# Splice / cat
|
||||||
|
"OP_CAT": 0x7e, "OP_SUBSTR": 0x7f, "OP_LEFT": 0x80,
|
||||||
|
"OP_RIGHT": 0x81, "OP_SIZE": 0x82,
|
||||||
|
# Bitwise (disabled in Bitcoin, but defined in script.cpp)
|
||||||
|
"OP_INVERT": 0x83, "OP_AND": 0x84, "OP_OR": 0x85, "OP_XOR": 0x86,
|
||||||
|
"OP_EQUAL": 0x87, "OP_EQUALVERIFY": 0x88,
|
||||||
|
"OP_RESERVED1": 0x89, "OP_RESERVED2": 0x8a,
|
||||||
|
# Arithmetic
|
||||||
|
"OP_1ADD": 0x8b, "OP_1SUB": 0x8c, "OP_2MUL": 0x8d, "OP_2DIV": 0x8e,
|
||||||
|
"OP_NEGATE": 0x8f, "OP_ABS": 0x90, "OP_NOT": 0x91, "OP_0NOTEQUAL": 0x92,
|
||||||
|
"OP_ADD": 0x93, "OP_SUB": 0x94, "OP_MUL": 0x95, "OP_DIV": 0x96,
|
||||||
|
"OP_MOD": 0x97, "OP_LSHIFT": 0x98, "OP_RSHIFT": 0x99,
|
||||||
|
"OP_BOOLAND": 0x9a, "OP_BOOLOR": 0x9b,
|
||||||
|
"OP_NUMEQUAL": 0x9c, "OP_NUMEQUALVERIFY": 0x9d,
|
||||||
|
"OP_NUMNOTEQUAL": 0x9e, "OP_LESSTHAN": 0x9f, "OP_GREATERTHAN": 0xa0,
|
||||||
|
"OP_LESSTHANOREQUAL": 0xa1, "OP_GREATERTHANOREQUAL": 0xa2,
|
||||||
|
"OP_MIN": 0xa3, "OP_MAX": 0xa4,
|
||||||
|
"OP_WITHIN": 0xa5,
|
||||||
|
# Crypto
|
||||||
|
"OP_RIPEMD160": 0xa6, "OP_SHA1": 0xa7, "OP_SHA256": 0xa8,
|
||||||
|
"OP_HASH160": 0xa9, "OP_HASH256": 0xaa,
|
||||||
|
"OP_CODESEPARATOR": 0xab, "OP_CHECKSIG": 0xac, "OP_CHECKSIGVERIFY": 0xad,
|
||||||
|
"OP_CHECKMULTISIG": 0xae, "OP_CHECKMULTISIGVERIFY": 0xaf,
|
||||||
|
# Expansion
|
||||||
|
"OP_NOP1": 0xb0, "OP_NOP2": 0xb1, "OP_NOP3": 0xb2, "OP_NOP4": 0xb3,
|
||||||
|
"OP_NOP5": 0xb4, "OP_NOP6": 0xb5, "OP_NOP7": 0xb6, "OP_NOP8": 0xb7,
|
||||||
|
"OP_NOP9": 0xb8, "OP_NOP10": 0xb9,
|
||||||
|
# Locktime (in script.cpp but rarely used in script-tests)
|
||||||
|
"OP_CHECKLOCKTIMEVERIFY": 0xb1, "OP_CHECKSEQUENCEVERIFY": 0xb2,
|
||||||
|
# Multi-byte opcodes (only the tag in CScript; data follows)
|
||||||
|
"OP_PUSHDATA1": 0x4c, "OP_PUSHDATA2": 0x4d, "OP_PUSHDATA4": 0x4e,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Aliases: Triangles' GetOpName strips "OP_" prefix for convenience.
|
||||||
|
SHORT_ALIASES = {k[3:]: v for k, v in OPCODES.items() if k.startswith("OP_")}
|
||||||
|
OPCODES.update(SHORT_ALIASES)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_hex(s):
|
||||||
|
"""Parse a hex string like '4b417a7a...' (no 0x prefix)."""
|
||||||
|
if len(s) % 2:
|
||||||
|
raise ValueError(f"hex string of odd length: {s!r}")
|
||||||
|
return bytes.fromhex(s)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_script_to_bytes(src):
|
||||||
|
"""
|
||||||
|
Parse a Triangles script string into raw CScript bytes, matching
|
||||||
|
the in-tree ParseScript() in src/test/script_tests.cpp.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
* Decimal integers → push as numeric (small ints use OP_1..OP_16)
|
||||||
|
* 0x-prefixed hex → raw bytes inserted (NOT pushed)
|
||||||
|
* Single-quoted strings → pushed as data
|
||||||
|
* Opcode names (with or without OP_ prefix)
|
||||||
|
|
||||||
|
The numeric push uses the CScript << operator semantics: small ints
|
||||||
|
(1..16) become OP_1..OP_16; otherwise we use the minimum-length
|
||||||
|
push encoding (OP_PUSHBYTES_N if 1..75 bytes, OP_PUSHDATA1/2/4 for
|
||||||
|
larger). For simplicity we use OP_PUSHDATA4 with explicit length
|
||||||
|
for any non-tiny number — fuzzer doesn't care about minimal encoding.
|
||||||
|
"""
|
||||||
|
out = bytearray()
|
||||||
|
for w in src.split():
|
||||||
|
if w.startswith("0x") or w.startswith("0X"):
|
||||||
|
out.extend(parse_hex(w[2:]))
|
||||||
|
elif len(w) >= 2 and w.startswith("'") and w.endswith("'"):
|
||||||
|
data = w[1:-1].encode("latin-1", errors="replace")
|
||||||
|
push_data(out, data)
|
||||||
|
elif w.startswith("-") and w[1:].isdigit() or w.isdigit():
|
||||||
|
n = int(w)
|
||||||
|
if 1 <= n <= 16:
|
||||||
|
out.append(0x50 + n)
|
||||||
|
elif n == 0:
|
||||||
|
out.append(0x00) # OP_FALSE / OP_0
|
||||||
|
else:
|
||||||
|
# Minimal-encoding push for non-tiny ints: use signed
|
||||||
|
# minimal-data encoding matching Bitcoin's CScript::<<int>.
|
||||||
|
# For simplicity here, encode as little-endian and push.
|
||||||
|
if n < 0:
|
||||||
|
# Encode as signed (rare in fixtures; OP_1NEGATE for -1)
|
||||||
|
if n == -1:
|
||||||
|
out.append(0x4f) # OP_1NEGATE
|
||||||
|
continue
|
||||||
|
data = (-n).to_bytes(((-n).bit_length() + 7) // 8, "little")
|
||||||
|
data = bytes([b | 0x80 for b in data]) # sign bit
|
||||||
|
else:
|
||||||
|
data = n.to_bytes((n.bit_length() + 7) // 8, "little")
|
||||||
|
push_data(out, data)
|
||||||
|
elif w in OPCODES:
|
||||||
|
out.append(OPCODES[w])
|
||||||
|
else:
|
||||||
|
# Unknown token — skip silently (mirrors old behavior of
|
||||||
|
# not crashing on weird fixture entries). Production callers
|
||||||
|
# should validate, but this is a seed generator, not a
|
||||||
|
# verifier.
|
||||||
|
pass
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def push_data(out, data):
|
||||||
|
"""Encode a push of `data` onto the script, using the same encoding
|
||||||
|
EvalScript expects on the wire."""
|
||||||
|
n = len(data)
|
||||||
|
if n == 0:
|
||||||
|
# OP_0 (push empty)
|
||||||
|
out.append(0x00)
|
||||||
|
elif n <= 0x4b:
|
||||||
|
out.append(n)
|
||||||
|
out.extend(data)
|
||||||
|
elif n <= 0xff:
|
||||||
|
out.append(0x4c) # OP_PUSHDATA1
|
||||||
|
out.append(n)
|
||||||
|
out.extend(data)
|
||||||
|
elif n <= 0xffff:
|
||||||
|
out.append(0x4d) # OP_PUSHDATA2
|
||||||
|
out.extend(n.to_bytes(2, "little"))
|
||||||
|
out.extend(data)
|
||||||
|
else:
|
||||||
|
out.append(0x4e) # OP_PUSHDATA4
|
||||||
|
out.extend(n.to_bytes(4, "little"))
|
||||||
|
out.extend(data)
|
||||||
|
|
||||||
|
|
||||||
|
def fixture_to_corpus_files(fixture_path, output_dir, prefix):
|
||||||
|
"""Convert a JSON fixture into a directory of corpus files.
|
||||||
|
|
||||||
|
Each inner fixture `["<sig>", "<pubkey>"]` becomes two files:
|
||||||
|
<prefix>-<idx>-sig.bin [len_byte][script_bytes]
|
||||||
|
<prefix>-<idx>-pubkey.bin [len_byte][script_bytes]
|
||||||
|
EvalScript runs each side independently when fuzzer replays them.
|
||||||
|
"""
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
with open(fixture_path) as f:
|
||||||
|
fixtures = json.load(f)
|
||||||
|
|
||||||
|
written = 0
|
||||||
|
skipped = 0
|
||||||
|
for idx, test in enumerate(fixtures):
|
||||||
|
if not isinstance(test, list) or len(test) < 2:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
for side, name in [(test[0], "sig"), (test[1], "pubkey")]:
|
||||||
|
if not isinstance(side, str):
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
script_bytes = parse_script_to_bytes(side)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [skip {prefix}-{idx}-{name}] {e}", file=sys.stderr)
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
# Cap at 255 bytes so the 1-byte length prefix fits. The
|
||||||
|
# fuzzer's libFuzzer has no problem with small inputs (it
|
||||||
|
# tries small first by default), and 255 covers all real
|
||||||
|
# Triangles scripts — anything bigger is either a script
|
||||||
|
# with embedded sigs (which the fuzzer doesn't verify) or
|
||||||
|
# pathological. We split oversized inputs across multiple
|
||||||
|
# 255-byte chunks.
|
||||||
|
if len(script_bytes) > 255:
|
||||||
|
# Write as multiple short inputs — each sub-script.
|
||||||
|
# This loses context but exercises the same byte
|
||||||
|
# sequences the fuzzer would discover anyway.
|
||||||
|
chunk = 0
|
||||||
|
for start in range(0, len(script_bytes), 255):
|
||||||
|
sub = script_bytes[start:start + 255]
|
||||||
|
out_path = os.path.join(
|
||||||
|
output_dir,
|
||||||
|
f"{prefix}-{idx:04d}-{name}-c{chunk:02d}.bin"
|
||||||
|
)
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
f.write(bytes([len(sub)]))
|
||||||
|
f.write(sub)
|
||||||
|
written += 1
|
||||||
|
chunk += 1
|
||||||
|
continue
|
||||||
|
out_path = os.path.join(output_dir, f"{prefix}-{idx:04d}-{name}.bin")
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
# Format: [1-byte len][script bytes]
|
||||||
|
f.write(bytes([len(script_bytes)]))
|
||||||
|
f.write(script_bytes)
|
||||||
|
written += 1
|
||||||
|
print(f" {prefix}: wrote {written} corpus files, skipped {skipped}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
fixture_path = sys.argv[1]
|
||||||
|
output_dir = sys.argv[2]
|
||||||
|
prefix = sys.argv[3] if len(sys.argv) > 3 else os.path.basename(fixture_path).split(".")[0]
|
||||||
|
fixture_to_corpus_files(fixture_path, output_dir, prefix)
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
//
|
||||||
|
// script_stress_tests.cpp — Stack / multisig / opcode-count boundary stress.
|
||||||
|
//
|
||||||
|
// These tests guard against regressions in EvalScript's resource limits:
|
||||||
|
// - script.size() > 10000 → reject (script.h MAX_SCRIPT_SIZE)
|
||||||
|
// - vchPushValue.size() > 520 → reject (MAX_SCRIPT_ELEMENT_SIZE)
|
||||||
|
// - nOpCount > 201 → reject (MAX_OPS_PER_SCRIPT)
|
||||||
|
//
|
||||||
|
// What's NOT bounded (and what this file watches):
|
||||||
|
// - stack size (no MAX_STACK_SIZE, only nOpCount caps stack-pushing ops
|
||||||
|
// at ~201 entries)
|
||||||
|
// - OP_CHECKMULTISIG's FindAndDelete loop does O(sigs × script_size) work
|
||||||
|
// per invocation — a script full of bytes that looks like a sig pattern
|
||||||
|
// amplifies the cost linearly in sig count.
|
||||||
|
//
|
||||||
|
// See script.cpp:332 (EvalScript), script.cpp:1045 (CHECKMULTISIG),
|
||||||
|
// script.cpp:485 (FindAndDelete inline in script.h).
|
||||||
|
//
|
||||||
|
#include <chrono>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <boost/test/unit_test.hpp>
|
||||||
|
|
||||||
|
#include "script.h"
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE(script_stress_tests)
|
||||||
|
|
||||||
|
// Helper: build an empty transaction usable for EvalScript/VerifyScript calls.
|
||||||
|
static CTransaction MakeDummyTx()
|
||||||
|
{
|
||||||
|
CTransaction tx;
|
||||||
|
tx.vin.resize(1);
|
||||||
|
tx.vout.resize(1);
|
||||||
|
tx.vout[0].nValue = 1;
|
||||||
|
return tx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Deep stack via OP_DUP should hit the nOpCount limit and return false
|
||||||
|
// rather than allocating unbounded memory or running forever.
|
||||||
|
BOOST_AUTO_TEST_CASE(deep_dup_stack_hits_opcount_limit)
|
||||||
|
{
|
||||||
|
// Build script: 250 OP_DUP ops. Each OP_DUP is opcode > OP_16, so each
|
||||||
|
// increments nOpCount. With MAX_OPS_PER_SCRIPT = 201, this must reject.
|
||||||
|
CScript script;
|
||||||
|
for (int i = 0; i < 250; i++)
|
||||||
|
script << OP_DUP;
|
||||||
|
|
||||||
|
vector<vector<unsigned char> > stack;
|
||||||
|
CTransaction tx = MakeDummyTx();
|
||||||
|
|
||||||
|
auto t0 = chrono::steady_clock::now();
|
||||||
|
bool fOk = EvalScript(stack, script, tx, 0, 0);
|
||||||
|
auto t1 = chrono::steady_clock::now();
|
||||||
|
|
||||||
|
BOOST_CHECK(!fOk); // must reject via nOpCount > 201
|
||||||
|
BOOST_CHECK_LT(chrono::duration_cast<chrono::milliseconds>(t1 - t0).count(),
|
||||||
|
1000); // must be fast
|
||||||
|
|
||||||
|
// Stack should be bounded by however many DUPs executed before the cap.
|
||||||
|
// With 201-op cap and 0 starting entries, max ~201 entries.
|
||||||
|
BOOST_CHECK_LE(stack.size(), 201u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 20-of-20 multisig: the maximum legal configuration. Must execute to
|
||||||
|
// completion (or fail gracefully) without OOM or pathologically slow
|
||||||
|
// FindAndDelete. nKeysCount > 20 is rejected; nKeysCount == 20 is fine.
|
||||||
|
//
|
||||||
|
// CScript::operator<<(int) calls push_int64(), which serializes as
|
||||||
|
// minimal push data. So `script << 20` becomes the 2-byte sequence
|
||||||
|
// {0x01, 0x14} (PUSHDATA1 prefix + value 20).
|
||||||
|
BOOST_AUTO_TEST_CASE(max_keys_multisig_20_of_20)
|
||||||
|
{
|
||||||
|
CScript script;
|
||||||
|
// Push 20 dummy pubkeys (33 bytes each — compressed pubkey size).
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
vector<unsigned char> pk(33, 0x02);
|
||||||
|
pk[1] = (unsigned char)i; // make each distinct
|
||||||
|
script << pk;
|
||||||
|
}
|
||||||
|
script << 20; // num_of_pubkeys = 20
|
||||||
|
// Push 20 dummy signatures
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
vector<unsigned char> sig(72, 0x30); // DER sig-ish
|
||||||
|
sig[1] = (unsigned char)(i + 1);
|
||||||
|
script << sig;
|
||||||
|
}
|
||||||
|
script << 20; // num_of_signatures = 20
|
||||||
|
script << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
vector<vector<unsigned char> > stack;
|
||||||
|
CTransaction tx = MakeDummyTx();
|
||||||
|
|
||||||
|
auto t0 = chrono::steady_clock::now();
|
||||||
|
EvalScript(stack, script, tx, 0, 0); // returns false (sigs are garbage)
|
||||||
|
auto t1 = chrono::steady_clock::now();
|
||||||
|
|
||||||
|
// Sigs are garbage, so verification fails — script returns false.
|
||||||
|
// EvalScript doesn't roll back the stack on failure (the contract is
|
||||||
|
// "on false, stack state is undefined"). The point of this test is
|
||||||
|
// that the script must terminate quickly and not OOM, not the stack
|
||||||
|
// contents. Stack should be bounded by the inputs we pushed (~42).
|
||||||
|
BOOST_CHECK_LT(chrono::duration_cast<chrono::milliseconds>(t1 - t0).count(),
|
||||||
|
2000);
|
||||||
|
BOOST_CHECK_LE(stack.size(), 100u); // bounded by inputs, not unbounded growth
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. nKeysCount > 20 must reject (guard against the canonical limit).
|
||||||
|
BOOST_AUTO_TEST_CASE(multisig_rejects_21_keys)
|
||||||
|
{
|
||||||
|
CScript script;
|
||||||
|
for (int i = 0; i < 21; i++)
|
||||||
|
{
|
||||||
|
vector<unsigned char> pk(33, 0x02);
|
||||||
|
pk[1] = (unsigned char)i;
|
||||||
|
script << pk;
|
||||||
|
}
|
||||||
|
script << 21; // num_of_pubkeys = 21 — over the limit
|
||||||
|
script << OP_CHECKMULTISIG;
|
||||||
|
|
||||||
|
vector<vector<unsigned char> > stack;
|
||||||
|
CTransaction tx = MakeDummyTx();
|
||||||
|
|
||||||
|
BOOST_CHECK(!EvalScript(stack, script, tx, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Pushdata > 520 bytes must reject at parse time.
|
||||||
|
BOOST_AUTO_TEST_CASE(pushdata_over_520_rejected)
|
||||||
|
{
|
||||||
|
CScript script;
|
||||||
|
vector<unsigned char> big(521, 0xAA);
|
||||||
|
script << big; // single push > MAX_SCRIPT_ELEMENT_SIZE
|
||||||
|
vector<vector<unsigned char> > stack;
|
||||||
|
CTransaction tx = MakeDummyTx();
|
||||||
|
BOOST_CHECK(!EvalScript(stack, script, tx, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Script size > 10000 must reject at parse time.
|
||||||
|
BOOST_AUTO_TEST_CASE(script_size_over_10000_rejected)
|
||||||
|
{
|
||||||
|
CScript script;
|
||||||
|
// Fill with 11000 bytes of OP_NOP (each is 1 byte) — exceeds MAX_SCRIPT_SIZE.
|
||||||
|
for (int i = 0; i < 11000; i++)
|
||||||
|
script << OP_NOP;
|
||||||
|
|
||||||
|
BOOST_CHECK_GT(script.size(), 10000u);
|
||||||
|
|
||||||
|
vector<vector<unsigned char> > stack;
|
||||||
|
CTransaction tx = MakeDummyTx();
|
||||||
|
BOOST_CHECK(!EvalScript(stack, script, tx, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Disabled opcodes (OP_CAT, OP_MUL, OP_LSHIFT etc.) must reject.
|
||||||
|
// These are the "upgrades Bitcoin wisely never shipped" — they were
|
||||||
|
// disabled in Bitcoin Core 0.3.x because they make quadratic-work
|
||||||
|
// attacks trivial. Confirm Triangles still rejects them all.
|
||||||
|
BOOST_AUTO_TEST_CASE(disabled_opcodes_rejected)
|
||||||
|
{
|
||||||
|
// Sample of the disabled set from script.cpp:363-378.
|
||||||
|
const opcodetype disabled[] = {
|
||||||
|
OP_CAT, OP_SUBSTR, OP_LEFT, OP_RIGHT,
|
||||||
|
OP_INVERT, OP_AND, OP_OR, OP_XOR,
|
||||||
|
OP_2MUL, OP_2DIV, OP_MUL, OP_DIV, OP_MOD,
|
||||||
|
OP_LSHIFT, OP_RSHIFT
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(disabled) / sizeof(disabled[0]); i++)
|
||||||
|
{
|
||||||
|
CScript script;
|
||||||
|
script << disabled[i];
|
||||||
|
vector<vector<unsigned char> > stack;
|
||||||
|
CTransaction tx = MakeDummyTx();
|
||||||
|
BOOST_CHECK_MESSAGE(!EvalScript(stack, script, tx, 0, 0),
|
||||||
|
"Disabled opcode " << GetOpName(disabled[i]) << " was accepted!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
Reference in New Issue
Block a user