From 47d68cd981fb2985acca0b4d2b8d79dbdccb136d Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 24 Apr 2026 10:17:29 +0200 Subject: [PATCH 1/8] ci: backport iwyu PR 2013 std::hash mapping Cherry-pick include-what-you-use commit 52f85e1f4d99 onto the clang_22 branch tracked by ci/test/00_setup_env_native_iwyu.sh, fixing the false positive where std::hash was mapped to / instead of its real provider (, , etc). --- ci/test/01_base_install.sh | 5 ++++- ci/test/02_iwyu_hash.patch | 44 ++++++++++++++++++++++++++++++++++++++ ci/test_imagefile | 3 ++- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 ci/test/02_iwyu_hash.patch diff --git a/ci/test/01_base_install.sh b/ci/test/01_base_install.sh index 5a6e06811fa0..9a93890b41a5 100755 --- a/ci/test/01_base_install.sh +++ b/ci/test/01_base_install.sh @@ -86,7 +86,10 @@ fi if [[ "${RUN_IWYU}" == true ]]; then ${CI_RETRY_EXE} git clone --depth=1 https://github.com/include-what-you-use/include-what-you-use -b clang_"${IWYU_LLVM_V}" /include-what-you-use - (cd /include-what-you-use && patch -p1 < /ci_container_base/ci/test/01_iwyu.patch) + pushd /include-what-you-use + patch -p1 < /ci_container_base/ci/test/01_iwyu.patch + patch -p1 < /ci_container_base/ci/test/02_iwyu_hash.patch + popd cmake -B /iwyu-build/ -G 'Unix Makefiles' -DCMAKE_PREFIX_PATH=/usr/lib/llvm-"${IWYU_LLVM_V}" -S /include-what-you-use make -C /iwyu-build/ install "$MAKEJOBS" fi diff --git a/ci/test/02_iwyu_hash.patch b/ci/test/02_iwyu_hash.patch new file mode 100644 index 000000000000..12df7a04ed5c --- /dev/null +++ b/ci/test/02_iwyu_hash.patch @@ -0,0 +1,44 @@ +Map std::hash to its providing standard headers. +Backport of https://github.com/include-what-you-use/include-what-you-use/pull/2013 +(commit 52f85e1f4d990f55fc6556d543eb051d79364a16) to the clang_22 release +branch. Drop once the upstream fix lands in the IWYU branch tracked by +ci/test/00_setup_env_native_iwyu.sh. + +--- a/std_symbol_map.inc ++++ b/std_symbol_map.inc +@@ -1054,12 +1054,27 @@ + { "std::has_unique_object_representations_v", kPrivate, "", kPublic }, + { "std::has_virtual_destructor", kPrivate, "", kPublic }, + { "std::has_virtual_destructor_v", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, + { "std::hash>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, + { "std::hash, :0>>", kPrivate, "", kPublic }, ++{ "std::hash>", kPrivate, "", kPublic }, + { "std::hash>", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, +@@ -1069,6 +1084,7 @@ + { "std::hash>", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, ++{ "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, + { "std::hash", kPrivate, "", kPublic }, diff --git a/ci/test_imagefile b/ci/test_imagefile index 908e9a0f5ab4..dac6b5531564 100644 --- a/ci/test_imagefile +++ b/ci/test_imagefile @@ -16,7 +16,8 @@ ENV BASE_ROOT_DIR=${BASE_ROOT_DIR} # Make retry available in PATH, needed for CI_RETRY_EXE COPY ./ci/retry/retry /usr/bin/retry -COPY ./ci/test/00_setup_env.sh ./${FILE_ENV} ./ci/test/01_base_install.sh ./ci/test/01_iwyu.patch /ci_container_base/ci/test/ +COPY ./ci/test/00_setup_env.sh ./${FILE_ENV} ./ci/test/01_base_install.sh /ci_container_base/ci/test/ +COPY ./ci/test/*.patch /ci_container_base/ci/test/ # Bash is required, so install it when missing RUN sh -c "bash -c 'true' || ( apk update && apk add --no-cache bash )" From a9301cfa0730d1d08c9e982f990dfc8aa0b3a27a Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 27 Apr 2026 14:14:07 +0200 Subject: [PATCH 2/8] refactor: disable default std::hash for CTransactionRef The default std::hash for shared_ptr compares by pointer. CTransactionRefHash or a custom hasher should be used instead. Co-authored-by: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> --- src/primitives/transaction.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 34bb9571c153..3a7735e149b6 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -403,4 +403,16 @@ struct CMutableTransaction typedef std::shared_ptr CTransactionRef; template static inline CTransactionRef MakeTransactionRef(Tx&& txIn) { return std::make_shared(std::forward(txIn)); } +namespace std { +/** Disable default std::hash for CTransactionRef to prevent accidentally + * comparing by pointer. Use CTransactionRefHash or provide a custom + * hasher. */ +template <> +struct hash { + hash() = delete; + // Belt-and-suspenders, already implied by the above. + size_t operator()(const CTransactionRef&) const = delete; +}; +} // namespace std + #endif // BITCOIN_PRIMITIVES_TRANSACTION_H From 0e4b0bacecf94063342c7f9eb9b03dac8a7a7936 Mon Sep 17 00:00:00 2001 From: marcofleon Date: Mon, 27 Apr 2026 18:16:44 +0100 Subject: [PATCH 3/8] validation: Don't add pruned blocks to m_blocks_unlinked on startup LoadBlockIndex() adds to m_blocks_unlinked based only on nTx > 0, without checking BLOCK_HAVE_DATA. Pruning preserves nTx but clears BLOCK_HAVE_DATA, so a pruned block whose parent was header-only gets re-added on every restart, causing the CheckBlockIndex() assertion that entries must have data on disk to fail. Check that BLOCK_HAVE_DATA is set before inserting into m_blocks_unlinked. Fixes #35050. --- src/node/blockstorage.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index b0842a005059..411f0ca849b0 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -487,7 +487,9 @@ bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockha pindex->m_chain_tx_count = pindex->pprev->m_chain_tx_count + pindex->nTx; } else { pindex->m_chain_tx_count = 0; - m_blocks_unlinked.insert(std::make_pair(pindex->pprev, pindex)); + if (pindex->nStatus & BLOCK_HAVE_DATA) { + m_blocks_unlinked.insert(std::make_pair(pindex->pprev, pindex)); + } } } else { pindex->m_chain_tx_count = pindex->nTx; From b3a3f88346dfd218a049acec6a77166f319c70e8 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 10 May 2026 12:49:50 +0200 Subject: [PATCH 4/8] crypto: cleanse HMAC stack buffers after use CHMAC_SHA256 and CHMAC_SHA512 leave two stack buffers populated on return: rkey[] holds K' XOR ipad after the constructor, and temp[] holds the inner-hash output after Finalize(). When the HMAC is keyed with sensitive material (chain code in BIP32Hash() in hash.cpp for BIP32 child key derivation; PRK in HKDF-Expand in hkdf_sha256_32.cpp, used for BIP324 transport keying), rkey is one constant XOR from that key, and temp is a one-way digest covering it. Cleanse both buffers with memory_cleanse(), matching the convention in chacha20.cpp and chacha20poly1305.cpp. No observable change for callers. --- src/crypto/hmac_sha256.cpp | 4 ++++ src/crypto/hmac_sha512.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/crypto/hmac_sha256.cpp b/src/crypto/hmac_sha256.cpp index a95ef70849b5..0796bbeb3271 100644 --- a/src/crypto/hmac_sha256.cpp +++ b/src/crypto/hmac_sha256.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -26,6 +27,8 @@ CHMAC_SHA256::CHMAC_SHA256(const unsigned char* key, size_t keylen) for (int n = 0; n < 64; n++) rkey[n] ^= 0x5c ^ 0x36; inner.Write(rkey, 64); + + memory_cleanse(rkey, sizeof(rkey)); } void CHMAC_SHA256::Finalize(unsigned char hash[OUTPUT_SIZE]) @@ -33,4 +36,5 @@ void CHMAC_SHA256::Finalize(unsigned char hash[OUTPUT_SIZE]) unsigned char temp[32]; inner.Finalize(temp); outer.Write(temp, 32).Finalize(hash); + memory_cleanse(temp, sizeof(temp)); } diff --git a/src/crypto/hmac_sha512.cpp b/src/crypto/hmac_sha512.cpp index f37e709d13cf..0a9d1041a67d 100644 --- a/src/crypto/hmac_sha512.cpp +++ b/src/crypto/hmac_sha512.cpp @@ -5,6 +5,7 @@ #include #include +#include #include @@ -26,6 +27,8 @@ CHMAC_SHA512::CHMAC_SHA512(const unsigned char* key, size_t keylen) for (int n = 0; n < 128; n++) rkey[n] ^= 0x5c ^ 0x36; inner.Write(rkey, 128); + + memory_cleanse(rkey, sizeof(rkey)); } void CHMAC_SHA512::Finalize(unsigned char hash[OUTPUT_SIZE]) @@ -33,4 +36,5 @@ void CHMAC_SHA512::Finalize(unsigned char hash[OUTPUT_SIZE]) unsigned char temp[64]; inner.Finalize(temp); outer.Write(temp, 64).Finalize(hash); + memory_cleanse(temp, sizeof(temp)); } From 3f44f9aef7ccd0417fcd0c2f33f20615ea5c11e6 Mon Sep 17 00:00:00 2001 From: marcofleon Date: Mon, 27 Apr 2026 19:15:46 +0100 Subject: [PATCH 5/8] test: Add coverage for m_blocks_unlinked invariant in LoadBlockIndex A pruned stale-fork block whose parent doesn't have any transactions shouldn't be added to m_blocks_unlinked when starting up a node. --- test/functional/feature_prune_stale_fork.py | 39 +++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 40 insertions(+) create mode 100755 test/functional/feature_prune_stale_fork.py diff --git a/test/functional/feature_prune_stale_fork.py b/test/functional/feature_prune_stale_fork.py new file mode 100755 index 000000000000..5badca0a7cfc --- /dev/null +++ b/test/functional/feature_prune_stale_fork.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# 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. +"""Test node restart with a pruned stale-fork block whose parent has no transactions.""" + +from test_framework.blocktools import create_empty_fork +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class FeaturePruneStaleForkTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [["-prune=1", "-fastprune"]] + + def run_test(self): + node = self.nodes[0] + + self.log.info("Create a 2-block stale fork: parent has no transactions, child has transactions") + [side_parent, side_child] = create_empty_fork(node, 2) + + node.submitheader(side_parent.serialize().hex()) + node.submitblock(side_child.serialize().hex()) + assert_equal(node.getblockheader(side_parent.hash_hex)["nTx"], 0) + assert_equal(node.getblockheader(side_child.hash_hex)["nTx"], 1) + + self.log.info("Advance and prune so the stale-fork child's block data is removed from disk") + self.generate(node, 500) + node.pruneblockchain(node.getblockcount() - 100) + assert_raises_rpc_error(-1, "Block not available (pruned data)", node.getblock, side_child.hash_hex) + + self.log.info("Restart and mine; node must reload cleanly after the stale-fork child was pruned") + self.restart_node(0) + self.generate(node, 1) + + +if __name__ == '__main__': + FeaturePruneStaleForkTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 698742bc79e7..9236f3a90d39 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -369,6 +369,7 @@ 'feature_blocksdir.py', 'wallet_startup.py', 'feature_remove_pruned_files_on_startup.py', + 'feature_prune_stale_fork.py', 'p2p_i2p_ports.py', 'p2p_i2p_sessions.py', 'feature_presegwit_node_upgrade.py', From 21a1380c13470d28f53cc0b85d902693365d39d3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 12 May 2026 21:24:19 +0200 Subject: [PATCH 6/8] key: cleanse ChainCode on destruction HMAC primitives cleanse their internal stack buffers, but a caller's ChainCode remains populated in memory after use. Promote ChainCode from `typedef uint256` to a `base_blob<256>` subclass with a memory_cleanse() destructor, so chain codes in CExtKey, CExtPubKey, and local variables are cleansed on scope exit. Retype MUSIG_CHAINCODE from `constexpr uint256` to `const ChainCode` to match its BIP328 semantic role. Dropping `constexpr` (ChainCode is no longer a literal type) also removes the GCC-14 consteval lambda workaround. Remove the duplicate typedef in pubkey.h (which includes hash.h transitively). Two fuzz-test call sites in test/fuzz/key.cpp now construct the chain-code argument explicitly rather than relying on the typedef. --- src/hash.h | 10 +++++++++- src/musig.cpp | 5 +---- src/pubkey.h | 2 -- src/test/fuzz/key.cpp | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/hash.h b/src/hash.h index 34486af64a1d..b671761fbb6e 100644 --- a/src/hash.h +++ b/src/hash.h @@ -13,12 +13,20 @@ #include #include #include +#include #include #include #include -typedef uint256 ChainCode; +/** A BIP32 chain code. Cleansed on destruction. */ +class ChainCode : public base_blob<256> { +public: + constexpr ChainCode() = default; + constexpr explicit ChainCode(std::span vch) : base_blob<256>(vch) {} + constexpr explicit ChainCode(const base_blob<256>& b) : base_blob<256>(b) {} + ~ChainCode() { memory_cleanse(data(), size()); } +}; /** A hasher class for Bitcoin's 256-bit hash (double SHA-256). */ class CHash256 { diff --git a/src/musig.cpp b/src/musig.cpp index 706874be2cf5..b04a26db123c 100644 --- a/src/musig.cpp +++ b/src/musig.cpp @@ -9,10 +9,7 @@ //! MuSig2 chaincode as defined by BIP 328 using namespace util::hex_literals; -constexpr uint256 MUSIG_CHAINCODE{ - // Use immediate lambda to work around GCC-14 bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=117966 - []() consteval { return uint256{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8}; }(), -}; +const ChainCode MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8}; static bool GetMuSig2KeyAggCache(const std::vector& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache) { diff --git a/src/pubkey.h b/src/pubkey.h index 02ad7371a796..0391609ebcc3 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -27,8 +27,6 @@ class CKeyID : public uint160 explicit CKeyID(const uint160& in) : uint160(in) {} }; -typedef uint256 ChainCode; - /** An encapsulated public key. */ class CPubKey { diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp index e4bedff8b512..19101ae81c2d 100644 --- a/src/test/fuzz/key.cpp +++ b/src/test/fuzz/key.cpp @@ -85,7 +85,7 @@ FUZZ_TARGET(key, .init = initialize_key) { CKey child_key; ChainCode child_chaincode; - const bool ok = key.Derive(child_key, child_chaincode, 0, random_uint256); + const bool ok = key.Derive(child_key, child_chaincode, 0, ChainCode{random_uint256}); assert(ok); assert(child_key.IsValid()); assert(!(child_key == key)); @@ -275,7 +275,7 @@ FUZZ_TARGET(key, .init = initialize_key) { CPubKey child_pubkey; ChainCode child_chaincode; - const bool ok = pubkey.Derive(child_pubkey, child_chaincode, 0, random_uint256); + const bool ok = pubkey.Derive(child_pubkey, child_chaincode, 0, ChainCode{random_uint256}); assert(ok); assert(child_pubkey != pubkey); assert(child_pubkey.IsCompressed()); From 2189a6f5f226d5a2905f1939eb7eea9571502b90 Mon Sep 17 00:00:00 2001 From: codeabysss <248203105+codeabysss@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:47:45 +0300 Subject: [PATCH 7/8] p2p: Saturate LocalServiceInfo::nScore updates at INT_MAX Signed overflow on nScore updates is undefined behavior. Use SaturatingAdd in AddLocal() and SeenLocal() so increments saturate at INT_MAX instead of overflowing. Add unit test coverage for saturation in both code paths. --- src/net.cpp | 5 +++-- src/test/net_tests.cpp | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index f8149ef309d1..ca0289c8316e 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -296,7 +297,7 @@ bool AddLocal(const CService& addr_, int nScore) const auto [it, is_newly_added] = mapLocalHost.emplace(addr, LocalServiceInfo()); LocalServiceInfo &info = it->second; if (is_newly_added || nScore >= info.nScore) { - info.nScore = nScore + (is_newly_added ? 0 : 1); + info.nScore = SaturatingAdd(nScore, is_newly_added ? 0 : 1); info.nPort = addr.GetPort(); } } @@ -325,7 +326,7 @@ bool SeenLocal(const CService& addr) LOCK(g_maplocalhost_mutex); const auto it = mapLocalHost.find(addr); if (it == mapLocalHost.end()) return false; - ++it->second.nScore; + it->second.nScore = SaturatingAdd(it->second.nScore, 1); return true; } diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 32801d97b8bd..7168d3dcaec2 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -802,6 +802,41 @@ BOOST_AUTO_TEST_CASE(LocalAddress_BasicLifecycle) BOOST_CHECK(!IsLocal(addr)); } +BOOST_AUTO_TEST_CASE(LocalAddress_nScore_Overflow) +{ + g_reachable_nets.Add(NET_IPV4); + const CService addr{UtilBuildAddress(0x002, 0x001, 0x001, 0x001), 1000}; // 2.1.1.1:1000 + + const auto get_score = [](const CService& service) -> int { + LOCK(g_maplocalhost_mutex); + const auto it = mapLocalHost.find(service); + return it != mapLocalHost.end() ? it->second.nScore : 0; + }; + + const int initial_score = 1000; + BOOST_REQUIRE(AddLocal(addr, initial_score)); + BOOST_REQUIRE(IsLocal(addr)); + BOOST_CHECK_EQUAL(get_score(addr), initial_score); + + // SeenLocal should increment nScore by 1. + BOOST_CHECK(SeenLocal(addr)); + BOOST_CHECK_EQUAL(get_score(addr), initial_score + 1); + + // AddLocal() saturates nScore when updating an existing entry at INT_MAX. + BOOST_REQUIRE(AddLocal(addr, std::numeric_limits::max())); + BOOST_CHECK_EQUAL(get_score(addr), std::numeric_limits::max()); + + BOOST_CHECK(AddLocal(addr, std::numeric_limits::max())); + BOOST_CHECK_EQUAL(get_score(addr), std::numeric_limits::max()); + + // SeenLocal() also saturates at INT_MAX. + BOOST_CHECK(SeenLocal(addr)); + BOOST_CHECK_EQUAL(get_score(addr), std::numeric_limits::max()); + + RemoveLocal(addr); + BOOST_CHECK(!IsLocal(addr)); +} + BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message) { LOCK(NetEventsInterface::g_msgproc_mutex); From 19b32a2e18017c3bf8ea12d25d48b17347f490b6 Mon Sep 17 00:00:00 2001 From: Hao Xu Date: Sun, 7 Jun 2026 15:42:49 +0800 Subject: [PATCH 8/8] fuzz: reset the mockable steady clock between iterations CheckGlobalsImpl's constructor runs at the start of every fuzz iteration and already resets the global RNG flags and the mockable NodeClock via SetMockTime(0s), but it never reset the mockable steady clock. A value written to g_mock_steady_time by one input therefore leaked into the next one. For example, FuzzedSock's constructor calls SetMockTime(INITIAL_MOCK_TIME) and never clears it, so the mocked steady time stays set for all subsequent iterations. Reset MockableSteadyClock symmetrically with NodeClock so each input starts from an unmocked steady clock. This also brings the steady clock under the same discipline as the system clock: a target that reads MockableSteadyClock::now() without first mocking it is now caught by the existing g_used_system_time check instead of silently reusing a leaked value. --- src/test/fuzz/util/check_globals.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/fuzz/util/check_globals.cpp b/src/test/fuzz/util/check_globals.cpp index ca3111534d3b..87d96ebd9726 100644 --- a/src/test/fuzz/util/check_globals.cpp +++ b/src/test/fuzz/util/check_globals.cpp @@ -19,6 +19,7 @@ struct CheckGlobalsImpl { g_seeded_g_prng_zero = false; g_used_system_time = false; SetMockTime(0s); + MockableSteadyClock::ClearMockTime(); } ~CheckGlobalsImpl() {