From 7575210577164a2235c7678581013ffd3092d903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=BE=D0=BB=D0=BB=D0=B8?= Date: Tue, 31 Mar 2026 20:19:16 +0300 Subject: [PATCH 1/2] fuzz: align CheckGlobals with upstream Bitcoin Core pattern Add CheckGlobals to fuzz framework test_one_input() wrapper, matching upstream. Extract minimal Sv2FuzzInitialize() into sv2_fuzz_util.h, removing duplicated init from sv2_noise.cpp. --- src/test/fuzz/fuzz.cpp | 2 ++ src/test/fuzz/sv2_fuzz_util.h | 31 ++++++++++++++++++++ src/test/fuzz/sv2_noise.cpp | 54 +++-------------------------------- 3 files changed, 37 insertions(+), 50 deletions(-) create mode 100644 src/test/fuzz/sv2_fuzz_util.h diff --git a/src/test/fuzz/fuzz.cpp b/src/test/fuzz/fuzz.cpp index 671cd246..487cd496 100644 --- a/src/test/fuzz/fuzz.cpp +++ b/src/test/fuzz/fuzz.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -93,6 +94,7 @@ static const TypeTestOneInput* g_test_one_input{nullptr}; static void test_one_input(FuzzBufferType buffer) { + CheckGlobals check{}; (*Assert(g_test_one_input))(buffer); } diff --git a/src/test/fuzz/sv2_fuzz_util.h b/src/test/fuzz/sv2_fuzz_util.h new file mode 100644 index 00000000..a9a70869 --- /dev/null +++ b/src/test/fuzz/sv2_fuzz_util.h @@ -0,0 +1,31 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_TEST_FUZZ_SV2_FUZZ_UTIL_H +#define BITCOIN_TEST_FUZZ_SV2_FUZZ_UTIL_H + +#include +#include +#include + +#include +#include +#include + +/** Shared one-time initialization for SV2 fuzz targets. */ +inline void Sv2FuzzInitialize() +{ + static const auto testing_setup = std::make_unique(); +} + +/** Consume 32 bytes from the fuzzer to produce a uint256. + * Returns a zero uint256 if fewer than 32 bytes remain. */ +[[nodiscard]] inline uint256 ConsumeUint256(FuzzedDataProvider& provider) noexcept +{ + auto v = provider.ConsumeBytes(32); + if (v.size() != 32) return {}; + return uint256{std::span(v)}; +} + +#endif // BITCOIN_TEST_FUZZ_SV2_FUZZ_UTIL_H diff --git a/src/test/fuzz/sv2_noise.cpp b/src/test/fuzz/sv2_noise.cpp index b1117204..135bd0d3 100644 --- a/src/test/fuzz/sv2_noise.cpp +++ b/src/test/fuzz/sv2_noise.cpp @@ -7,59 +7,14 @@ #include #include #include -#include #include -#include -#include -#include -#include +#include -#include +#include #include +#include #include -// Exposed by the fuzz harness to pass through double-dash arguments. -extern const std::function()> G_TEST_COMMAND_LINE_ARGUMENTS; - -namespace { - -void Initialize() -{ - // Add test context for debugging. Usage: - // --debug=sv2 --loglevel=sv2:trace - static const auto testing_setup = std::make_unique(); - - // Optional: enable console logging when requested via double-dash args. - // Recognized flags: --printtoconsole=1, --debug=sv2, --loglevel=sv2:trace - // These flags are passed through the fuzz harness and exposed via G_TEST_COMMAND_LINE_ARGUMENTS. - bool want_console{false}; - bool want_sv2_debug{false}; - bool want_sv2_trace{false}; - if (G_TEST_COMMAND_LINE_ARGUMENTS) { - for (const char* arg : G_TEST_COMMAND_LINE_ARGUMENTS()) { - if (!arg) continue; - std::string_view s{arg}; - // Accept both forms in case a caller wants to force console logging explicitly. - if (s == "--printtoconsole" || s == "--printtoconsole=1") want_console = true; - if (s == "--debug=sv2" || s == "--debug=1" || s == "--debug=all") want_sv2_debug = true; - if (s == "--loglevel=sv2:trace" || s == "--loglevel=trace") want_sv2_trace = true; - } - } - if (want_console || std::getenv("SV2_FUZZ_LOG")) { - // Turn on console logging and ensure SV2 category is enabled at the desired level. - LogInstance().m_print_to_console = true; - LogInstance().EnableCategory(BCLog::SV2); - if (want_sv2_trace) { - LogInstance().SetCategoryLogLevel({{BCLog::SV2, BCLog::Level::Trace}}); - } else if (want_sv2_debug || std::getenv("SV2_FUZZ_LOG_DEBUG")) { - LogInstance().SetCategoryLogLevel({{BCLog::SV2, BCLog::Level::Debug}}); - } - // Start logging to flush any buffered messages. - LogInstance().StartLogging(); - } -} -} // namespace - bool MaybeDamage(FuzzedDataProvider& provider, std::vector& transport) { if (transport.size() == 0) return false; @@ -77,9 +32,8 @@ bool MaybeDamage(FuzzedDataProvider& provider, std::vector& transport return damage; } -FUZZ_TARGET(sv2_noise_cipher_roundtrip, .init = Initialize) +FUZZ_TARGET(sv2_noise_cipher_roundtrip, .init = Sv2FuzzInitialize) { - const CheckGlobals check_globals{}; SeedRandomStateForTest(SeedRand::ZEROS); // Test that Sv2Noise's encryption and decryption agree. From 9e9104f7142cdf6f87509aa058608d17a3237915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=BE=D0=BB=D0=BB=D0=B8?= Date: Tue, 31 Mar 2026 20:51:35 +0300 Subject: [PATCH 2/2] fuzz: add raw deserialization targets for SV2 message types Cover all client-to-TP message types that accept untrusted input: SetupConnection, RequestTransactionData, SubmitSolution, CoinbaseOutputConstraints, NetHeader, and NetMsg. Types with both Serialize and Unserialize include roundtrip invariant checks. --- src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/sv2_messages.cpp | 117 +++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/test/fuzz/sv2_messages.cpp diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index 6b22b473..2939c49c 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources(fuzz PRIVATE fuzz.cpp check_globals.cpp + sv2_messages.cpp sv2_noise.cpp ../sv2_test_setup.cpp ) diff --git a/src/test/fuzz/sv2_messages.cpp b/src/test/fuzz/sv2_messages.cpp new file mode 100644 index 00000000..3297f380 --- /dev/null +++ b/src/test/fuzz/sv2_messages.cpp @@ -0,0 +1,117 @@ +// Copyright (c) 2026-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include + +#include +#include + +using node::Sv2MsgType; +using node::Sv2NetHeader; +using node::Sv2NetMsg; +using node::Sv2SetupConnectionMsg; +using node::Sv2CoinbaseOutputConstraintsMsg; +using node::Sv2RequestTransactionDataMsg; +using node::Sv2SubmitSolutionMsg; + +// Feed arbitrary bytes into a deserializer. +// Sanitizers (ASan/UBSan/MSan) catch memory errors along the way. +template +static void FuzzDeserialize(FuzzBufferType buffer) +{ + DataStream ds{buffer}; + try { + T msg; + ds >> msg; + } catch (const std::ios_base::failure&) { + } +} + +// Deserialize, then verify the roundtrip invariant: serialize the +// result and deserialize again. Only for types with both Serialize +// and Unserialize. +template +static void FuzzDeserializeRoundtrip(FuzzBufferType buffer) +{ + DataStream ds{buffer}; + try { + T msg; + ds >> msg; + + DataStream rt{}; + rt << msg; + T msg2; + rt >> msg2; + } catch (const std::ios_base::failure&) { + } +} + +// Client -> TP messages: these arrive over the network from untrusted +// peers and are the primary deserialization attack surface. +// These types have Unserialize only (no Serialize). + +FUZZ_TARGET(sv2_setup_connection_raw, .init = Sv2FuzzInitialize) +{ + FuzzDeserialize(buffer); +} + +FUZZ_TARGET(sv2_request_transaction_data_raw, .init = Sv2FuzzInitialize) +{ + FuzzDeserialize(buffer); +} + +FUZZ_TARGET(sv2_submit_solution_raw, .init = Sv2FuzzInitialize) +{ + FuzzDeserialize(buffer); +} + +// CoinbaseOutputConstraints has both Serialize and Unserialize, +// and uses catch(...) for the optional sigops field -- roundtrip it. +FUZZ_TARGET(sv2_coinbase_output_constraints_raw, .init = Sv2FuzzInitialize) +{ + FuzzDeserializeRoundtrip(buffer); +} + +// Sv2NetHeader uses a 24-bit little-endian length encoding and ignores +// a 2-byte extension type prefix -- unusual parsing worth fuzzing. +FUZZ_TARGET(sv2_net_header_raw, .init = Sv2FuzzInitialize) +{ + DataStream ds{buffer}; + try { + Sv2NetHeader hdr; + ds >> hdr; + + // Roundtrip + DataStream rt{}; + rt << hdr; + Sv2NetHeader hdr2; + rt >> hdr2; + assert(hdr.m_msg_type == hdr2.m_msg_type); + assert(hdr.m_msg_len == hdr2.m_msg_len); + } catch (const std::ios_base::failure&) { + } +} + +// Sv2NetMsg::Unserialize calls m_msg.resize(s.size()) which allocates +// based on remaining stream size -- test with arbitrary input lengths. +FUZZ_TARGET(sv2_net_msg_raw, .init = Sv2FuzzInitialize) +{ + DataStream ds{buffer}; + try { + Sv2NetMsg msg(Sv2MsgType::SETUP_CONNECTION, {}); + ds >> msg; + + // Roundtrip + DataStream rt{}; + rt << msg; + Sv2NetMsg msg2(Sv2MsgType::SETUP_CONNECTION, {}); + rt >> msg2; + assert(msg.m_msg_type == msg2.m_msg_type); + assert(msg.m_msg == msg2.m_msg); + } catch (const std::ios_base::failure&) { + } +}