What changed, and why it matters
This commit adds a new fuzz test for Bitcoin Core's inter-process communication (IPC) system. Fuzz tests are automated quality-assurance tools that feed random or semi-random data into code to find crashes or bugs. The change only introduces test code and build configuration; it does not alter any production networking, consensus, or wallet logic. There is no indication this commit fixes or introduces a security vulnerability.
No security action required. Treat as a normal test/QA commit. If reviewing further, verify that the fuzz target builds correctly with ENABLE_IPC and that the libmultiprocess empty-vector UBSan note is tracked upstream.
Security signals we found
No production code changes
Test-only fuzz target addition
No input from untrusted network or wallet paths in production
No privilege changes or authentication modifications
No memory-unsafe patterns observed in the diff beyond ordinary C++ test code
Evidence from the diff
The commit introduces an IPC round-trip fuzz target gated by ENABLE_IPC. It creates an in-process two-way Cap’n Proto pipe, sets up a reflected IpcFuzzImplementation server, and exercises client/server serialization/deserialization of Int32, COutPoint, std::vector
Changed components
src/ipc/test/fuzz/ipc.cppsrc/ipc/test/fuzz/ipc_fuzz.capnpsrc/ipc/test/fuzz/ipc_fuzz.hsrc/ipc/test/fuzz/ipc_fuzz_types.hsrc/ipc/CMakeLists.txtsrc/ipc/test/fuzz/CMakeLists.txtsrc/test/fuzz/CMakeLists.txtsrc/test/fuzz/util.cppsrc/test/fuzz/util.hInspect captured patch +233 / −0
diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt
index 8326423d..e9bdf0b3 100644
--- a/src/ipc/CMakeLists.txt
+++ b/src/ipc/CMakeLists.txt
@@ -47,4 +47,19 @@ if(BUILD_TESTS)
)
endif()
+if (BUILD_FUZZ_BINARY)
+ add_library(bitcoin_ipc_fuzz STATIC EXCLUDE_FROM_ALL)
+ target_capnp_sources(bitcoin_ipc_fuzz ${CMAKE_CURRENT_SOURCE_DIR}
+ test/fuzz/ipc_fuzz.capnp
+ )
+ add_dependencies(bitcoin_ipc_fuzz bitcoin_ipc_headers)
+
+ target_link_libraries(bitcoin_ipc_fuzz
+ PRIVATE
+ core_interface
+ univalue
+ Boost::headers
+ )
+endif()
+
configure_file(.clang-tidy.in .clang-tidy USE_SOURCE_PERMISSIONS COPYONLY)
diff --git a/src/ipc/test/fuzz/CMakeLists.txt b/src/ipc/test/fuzz/CMakeLists.txt
new file mode 100644
index 00000000..7af1477a
--- /dev/null
+++ b/src/ipc/test/fuzz/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright (c) The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit/.
+
+target_sources(fuzz PRIVATE ${PROJECT_SOURCE_DIR}/src/ipc/test/fuzz/ipc.cpp)
+target_link_libraries(fuzz bitcoin_ipc_fuzz multiprocess)
diff --git a/src/ipc/test/fuzz/ipc.cpp b/src/ipc/test/fuzz/ipc.cpp
new file mode 100644
index 00000000..76374c43
--- /dev/null
+++ b/src/ipc/test/fuzz/ipc.cpp
@@ -0,0 +1,137 @@
+// 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 <primitives/transaction.h>
+#include <capnp/capability.h>
+#include <capnp/rpc.h>
+#include <kj/memory.h>
+#include <mp/proxy-io.h>
+#include <mp/proxy.h>
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+#include <ipc/test/fuzz/ipc_fuzz.capnp.h>
+#include <ipc/test/fuzz/ipc_fuzz.capnp.proxy.h>
+#include <ipc/test/fuzz/ipc_fuzz.h>
+#include <test/fuzz/util.h>
+#include <test/util/setup_common.h>
+
+#include <future>
+#include <memory>
+#include <stdexcept>
+#include <thread>
+
+namespace {
+class IpcFuzzSetup
+{
+public:
+ IpcFuzzSetup()
+ {
+ std::promise<std::unique_ptr<mp::ProxyClient<test::fuzz::messages::IpcFuzzInterface>>> client_promise;
+ auto client_future{client_promise.get_future()};
+ m_loop_thread = std::thread([&client_promise] {
+ mp::EventLoop loop("ipc-fuzz", [](mp::LogMessage message) {
+ if (message.level == mp::Log::Raise) throw std::runtime_error(message.message);
+ });
+ auto pipe = loop.m_io_context.provider->newTwoWayPipe();
+
+ auto server_connection = std::make_unique<mp::Connection>(
+ loop,
+ kj::mv(pipe.ends[0]),
+ [&](mp::Connection& connection) {
+ auto server_proxy = kj::heap<mp::ProxyServer<test::fuzz::messages::IpcFuzzInterface>>(
+ std::make_shared<IpcFuzzImplementation>(), connection);
+ return capnp::Capability::Client(kj::mv(server_proxy));
+ });
+ server_connection->onDisconnect([&] { server_connection.reset(); });
+
+ auto client_connection = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[1]));
+ auto client_proxy = std::make_unique<mp::ProxyClient<test::fuzz::messages::IpcFuzzInterface>>(
+ client_connection->m_rpc_system->bootstrap(mp::ServerVatId().vat_id)
+ .castAs<test::fuzz::messages::IpcFuzzInterface>(),
+ client_connection.get(),
+ /* destroy_connection= */ true);
+ (void)client_connection.release();
+
+ client_promise.set_value(std::move(client_proxy));
+ loop.loop();
+ });
+ m_client = client_future.get();
+ }
+
+ ~IpcFuzzSetup()
+ {
+ m_client.reset();
+ if (m_loop_thread.joinable()) m_loop_thread.join();
+ }
+
+ std::unique_ptr<mp::ProxyClient<test::fuzz::messages::IpcFuzzInterface>> m_client;
+
+private:
+ std::thread m_loop_thread;
+};
+
+static IpcFuzzSetup* g_ipc;
+
+static void initialize_ipc()
+{
+ static const auto testing_setup = MakeNoLogFileContext<>();
+ (void)testing_setup;
+
+ // Ensure g_thread_context is destroyed after the IPC setup, since C++
+ // destroys thread_local objects in reverse construction order.
+ mp::ThreadContext& thread_context{mp::g_thread_context};
+ (void)thread_context;
+
+ thread_local static IpcFuzzSetup ipc; // NOLINT(bitcoin-nontrivial-threadlocal)
+ g_ipc = &ipc;
+}
+
+FUZZ_TARGET(ipc, .init = initialize_ipc)
+{
+ auto& ipc = *g_ipc;
+ FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
+ const size_t iterations = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 64);
+
+ for (size_t i = 0; i < iterations; ++i) {
+ CallOneOf(
+ fuzzed_data_provider,
+ [&] {
+ static constexpr int MIN_ADD{-1'000'000};
+ static constexpr int MAX_ADD{1'000'000};
+ const int a = fuzzed_data_provider.ConsumeIntegralInRange<int>(MIN_ADD, MAX_ADD);
+ const int b = fuzzed_data_provider.ConsumeIntegralInRange<int>(MIN_ADD, MAX_ADD);
+ assert(ipc.m_client->add(a, b) == a + b);
+ },
+ [&] {
+ COutPoint outpoint{Txid::FromUint256(ConsumeUInt256(fuzzed_data_provider)),
+ fuzzed_data_provider.ConsumeIntegral<uint32_t>()};
+ COutPoint expected{outpoint.hash, outpoint.n ^ 0xFFFFFFFFu};
+ assert(ipc.m_client->passOutPoint(outpoint) == expected);
+ },
+ [&] {
+ std::vector<uint8_t> value = ConsumeRandomLengthByteVector<uint8_t>(fuzzed_data_provider, 512);
+ // Empty Data currently trips UBSan in the libmultiprocess byte-span serializer.
+ if (value.empty()) value.push_back(0);
+ std::vector<uint8_t> expected{value.rbegin(), value.rend()};
+ assert(ipc.m_client->passVectorUint8(value) == expected);
+ },
+ [&] {
+ CScript script{ConsumeScript(fuzzed_data_provider)};
+ CScript expected{script};
+ expected << OP_NOP;
+ assert(ipc.m_client->passScript(script) == expected);
+ },
+ [&] {
+ UniValue value = ConsumeUniValue(fuzzed_data_provider);
+ assert(ipc.m_client->passUniValue(value).write() == value.write());
+ },
+ [&] {
+ const CMutableTransaction mutable_tx = ConsumeTransaction(fuzzed_data_provider, std::nullopt);
+ if (mutable_tx.vin.empty()) return;
+ const CTransactionRef tx = MakeTransactionRef(mutable_tx);
+ assert(*ipc.m_client->passTransaction(tx) == *tx);
+ });
+ }
+}
+} // namespace
diff --git a/src/ipc/test/fuzz/ipc_fuzz.capnp b/src/ipc/test/fuzz/ipc_fuzz.capnp
new file mode 100644
index 00000000..3e7ec8b3
--- /dev/null
+++ b/src/ipc/test/fuzz/ipc_fuzz.capnp
@@ -0,0 +1,21 @@
+# 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.
+
+@0xf918ff05f5bf04d1;
+
+using Cxx = import "/capnp/c++.capnp";
+$Cxx.namespace("test::fuzz::messages");
+
+using Proxy = import "/mp/proxy.capnp";
+$Proxy.include("ipc/test/fuzz/ipc_fuzz.h");
+$Proxy.includeTypes("ipc/test/fuzz/ipc_fuzz_types.h");
+
+interface IpcFuzzInterface $Proxy.wrap("IpcFuzzImplementation") {
+ add @0 (a :Int32, b :Int32) -> (result :Int32);
+ passOutPoint @1 (arg :Data) -> (result :Data);
+ passVectorUint8 @2 (arg :Data) -> (result :Data);
+ passScript @3 (arg :Data) -> (result :Data);
+ passUniValue @4 (arg :Text) -> (result :Text);
+ passTransaction @5 (arg :Data) -> (result :Data);
+}
diff --git a/src/ipc/test/fuzz/ipc_fuzz.h b/src/ipc/test/fuzz/ipc_fuzz.h
new file mode 100644
index 00000000..b3c89908
--- /dev/null
+++ b/src/ipc/test/fuzz/ipc_fuzz.h
@@ -0,0 +1,26 @@
+// 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_IPC_TEST_FUZZ_IPC_FUZZ_H
+#define BITCOIN_IPC_TEST_FUZZ_IPC_FUZZ_H
+
+#include <primitives/transaction.h>
+#include <script/script.h>
+#include <univalue.h>
+
+#include <algorithm>
+#include <vector>
+
+class IpcFuzzImplementation
+{
+public:
+ int add(int a, int b) { return a + b; }
+ COutPoint passOutPoint(COutPoint o) { return COutPoint{o.hash, o.n ^ 0xFFFFFFFFu}; }
+ std::vector<uint8_t> passVectorUint8(std::vector<uint8_t> v) { std::reverse(v.begin(), v.end()); return v; }
+ CScript passScript(CScript s) { s << OP_NOP; return s; }
+ UniValue passUniValue(UniValue v) { return v; }
+ CTransactionRef passTransaction(CTransactionRef t) { return t; }
+};
+
+#endif // BITCOIN_IPC_TEST_FUZZ_IPC_FUZZ_H
diff --git a/src/ipc/test/fuzz/ipc_fuzz_types.h b/src/ipc/test/fuzz/ipc_fuzz_types.h
new file mode 100644
index 00000000..52cb7328
--- /dev/null
+++ b/src/ipc/test/fuzz/ipc_fuzz_types.h
@@ -0,0 +1,11 @@
+// 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_IPC_TEST_FUZZ_IPC_FUZZ_TYPES_H
+#define BITCOIN_IPC_TEST_FUZZ_IPC_FUZZ_TYPES_H
+
+#include <ipc/capnp/common-types.h>
+#include <ipc/test/fuzz/ipc_fuzz.capnp.h>
+
+#endif // BITCOIN_IPC_TEST_FUZZ_IPC_FUZZ_TYPES_H
diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt
index fc82fdc0..2b395e6b 100644
--- a/src/test/fuzz/CMakeLists.txt
+++ b/src/test/fuzz/CMakeLists.txt
@@ -138,6 +138,10 @@ add_executable(fuzz
versionbits.cpp
)
+if(ENABLE_IPC)
+ add_subdirectory(${PROJECT_SOURCE_DIR}/src/ipc/test/fuzz ipc)
+endif()
+
add_windows_application_manifest(fuzz)
target_link_libraries(fuzz
diff --git a/src/test/fuzz/util.cpp b/src/test/fuzz/util.cpp
index da0e2dea..3c4c0a20 100644
--- a/src/test/fuzz/util.cpp
+++ b/src/test/fuzz/util.cpp
@@ -237,6 +237,16 @@ CKey ConsumePrivateKey(FuzzedDataProvider& fuzzed_data_provider, std::optional<b
return key;
}
+UniValue ConsumeUniValue(FuzzedDataProvider& fuzzed_data_provider) noexcept
+{
+ UniValue value{UniValue::VOBJ};
+ value.pushKV("bool", fuzzed_data_provider.ConsumeBool());
+ value.pushKV("number", fuzzed_data_provider.ConsumeIntegralInRange<int>(-1'000'000, 1'000'000));
+ value.pushKV("string", "ipc fuzz");
+
+ return value;
+}
+
bool ContainsSpentInput(const CTransaction& tx, const CCoinsViewCache& inputs) noexcept
{
for (const CTxIn& tx_in : tx.vin) {
diff --git a/src/test/fuzz/util.h b/src/test/fuzz/util.h
index fd53e39f..7a9098c5 100644
--- a/src/test/fuzz/util.h
+++ b/src/test/fuzz/util.h
@@ -5,6 +5,7 @@
#ifndef BITCOIN_TEST_FUZZ_UTIL_H
#define BITCOIN_TEST_FUZZ_UTIL_H
+#include <univalue.h>
#include <addresstype.h>
#include <arith_uint256.h>
#include <coins.h>
@@ -209,6 +210,8 @@ template <class Dur>
[[nodiscard]] CKey ConsumePrivateKey(FuzzedDataProvider& fuzzed_data_provider, std::optional<bool> compressed = std::nullopt) noexcept;
+[[nodiscard]] UniValue ConsumeUniValue(FuzzedDataProvider& fuzzed_data_provider) noexcept;
+
template <typename T>
[[nodiscard]] bool MultiplicationOverflow(const T i, const T j) noexcept
{
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.