fuzz: Block connection and chain reorganization fuzzing harnesses #34651

pull RobinDavid wants to merge 1 commits into bitcoin:master from RobinDavid:new-fuzz-harness-block-connection changing 2 files +506 −0
  1. RobinDavid commented at 3:25 pm on February 22, 2026: contributor

    Hi Bitcoin Core maintainers.

    This PR is a remastered version of fuzzing harnesses developed during a Bitcoin Core audit. It includes 1 fuzzing harnesses:

    • connect_block: Test the ConnectBlock() function responsible of validating the block and all its transaction with consensus rules. Thanks to justCheck parameter no side effect is performed on the internal state enabling relatively fast fuzzing.

    Besides pre-mining blocks, the harness initialization intends to bring a bit more of diversity by introducing additional transactions in blocks (otherwise only coinbases), and put some transactions in the mempool so that they can be ‘picked’ and put in a block by means of input mutation.

    Note: Some additional harnesses are added in another PR: #34895

    Authored by @RobinDavid and @nsurbay

  2. DrahtBot added the label Fuzzing on Feb 22, 2026
  3. DrahtBot commented at 3:26 pm on February 22, 2026: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • “It intend to leave more space to craft complex transactions and especially” -> “It intends to leave more space to craft complex transactions, especially …” [Subject-verb agreement makes the sentence grammatically incorrect]
    • “read transaction inside a block” -> “read transactions inside a block” [Plural mismatch reduces clarity]
    • “Read integer to read input coins…” -> “Read an integer to read input coins…” [Missing article makes the instruction awkward/unclear]
    • “The content of the CTxIn content is not read from the input per-se” -> “The content of the CTxIn is not read from the input per se” [Duplicate word + misspelling (“per-se”) impacts readability]
    • “Enable fuzzer to mutate every CTxIn fields after it being taken…” -> “Enable the fuzzer to mutate every CTxIn field after it has been taken…” [Grammar (“it being taken”) and plural form (“fields”) are incorrect]
    • “Adjust nonce if decided to generate a valid block or we already generated a block…” -> “Adjust the nonce if we decide to generate a valid block, or if we already generated a block…” [Missing structure/parallelism makes the condition hard to parse]
    • “Perform basic checks on the block to early reject it” -> “Perform basic checks on the block to early-reject it” [“to early” reads incorrectly as a broken phrase]

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • ConsumeTransaction(fuzzed_data_provider, additionalUTXO, true, targetHeight) in src/test/fuzz/connect_block.cpp
    • active_chainstate.ConnectBlock(block, state, &new_index, active_coins, /* justCheck*/ true) in src/test/fuzz/connect_block.cpp

    2026-03-22 15:34:18

  4. DrahtBot added the label CI failed on Feb 22, 2026
  5. DrahtBot commented at 4:12 pm on February 22, 2026: contributor

    🚧 At least one of the CI tasks failed. Task 32 bit ARM: https://github.com/bitcoin/bitcoin/actions/runs/22279929273/job/64448849605 LLM reason (✨ experimental): Compilation failed due to missing system header sys/sdt.h (not found).

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  6. in src/test/fuzz/connect_block.cpp:1 in 98ad4d8063
    0@@ -0,0 +1,941 @@
    1+// Copyright (c) 2019-2021 The Bitcoin Core developers
    


    brunoerg commented at 7:55 pm on February 23, 2026:
    2021?
  7. RobinDavid force-pushed on Feb 23, 2026
  8. in ci/test/00_setup_env_arm.sh:11 in 28afc49ca0
     7@@ -8,7 +8,7 @@ export LC_ALL=C.UTF-8
     8 
     9 export HOST=arm-linux-gnueabihf
    10 export DPKG_ADD_ARCH="armhf"
    11-export PACKAGES="python3-zmq g++-arm-linux-gnueabihf libc6:armhf libstdc++6:armhf libfontconfig1:armhf libxcb1:armhf"
    12+export PACKAGES="systemtap-sdt-dev python3-zmq g++-arm-linux-gnueabihf libc6:armhf libstdc++6:armhf libfontconfig1:armhf libxcb1:armhf"
    


    maflcko commented at 10:29 am on February 24, 2026:

    This won’t fix the ci failure. The CI fails because the build system does not add the usdt headers to the fuzz binary. So possible fixes are:

    • Add the headers in the build system to the fuzz binary
    • Copy-paste the connect-trace struct manually into the fuzz target code
    • Something else?

    RobinDavid commented at 8:40 am on March 17, 2026:
    Yes I fixed it, but duplicating the definition in connect_block.cpp. It was a ConnectTrace beforhand, now it is a ConnectedBlock struct.
  9. DrahtBot commented at 4:12 pm on March 2, 2026: contributor
    Could turn into draft while CI is red?
  10. RobinDavid marked this as a draft on Mar 2, 2026
  11. RobinDavid force-pushed on Mar 16, 2026
  12. RobinDavid force-pushed on Mar 16, 2026
  13. RobinDavid force-pushed on Mar 16, 2026
  14. RobinDavid force-pushed on Mar 16, 2026
  15. RobinDavid force-pushed on Mar 16, 2026
  16. DrahtBot removed the label CI failed on Mar 16, 2026
  17. RobinDavid marked this as ready for review on Mar 17, 2026
  18. RobinDavid commented at 8:43 am on March 17, 2026: contributor
    I fixed all the CI issues. Its ready for review now. cc @dergoegge
  19. sedited requested review from marcofleon on Mar 17, 2026
  20. in src/test/fuzz/connect_block.cpp:696 in 87c6d12d58 outdated
    691+                                                  /* justCheck*/ true);
    692+
    693+    if (success) {
    694+        DEBUGOUTPUT(std::cout << "Block connected successfully: " << curr_header.GetHash().ToString() << std::endl);
    695+        DEBUGOUTPUT(std::cout << "State: " << state.ToString() << std::endl);
    696+        //Assert(active_chainstate.DisconnectBlock(block, &new_index, active_coins) == DISCONNECT_OK);
    


    marcofleon commented at 2:22 pm on March 17, 2026:
    I like this thought a lot, a connect and disconnect roundtrip test. I think they are inverses for UTXO set operations, so it would be a great correctness check. It’s a shame we can’t test this statelessly without refactoring. DisconnectBlock needs undo data that ConnectBlock only writes to disk when justCheck is false, so a roundtrip here would require either making the harness stateful or refactoring to keep the undo data in memory.

    RobinDavid commented at 9:15 am on March 22, 2026:

    Hi. Ok I will create two separate PRs. As they share common functions, I will still put them in the same source file. Is it okay for you ?

    In a previous version the harness was doing the full round-trip, but lately, I encountered issues and though it would be better to keep it fully stateless. We keep it that way right ?

  21. marcofleon commented at 2:38 pm on March 17, 2026: contributor
    @dergoegge and I took a look at the connect_block harness and we think it’d be good to split that into its own PR. It’s relatively straightforward, valuable on its own, and would likely get through review quicker. The other two harnesses could remain open in this PR as a draft on top of the new connect_block PR.
  22. in src/test/fuzz/connect_block.cpp:137 in 87c6d12d58
    132+            /** Get the CTxOut output object  */
    133+            auto& vout = tx->vout[voutIndex];
    134+            /** Do not keep OP_RETURN outputs as they are not spendable */
    135+            if (vout.scriptPubKey.size() >= 1 && vout.scriptPubKey[0] == OP_RETURN) continue;
    136+            /** Create the CTxIn that can be used to spend this output */
    137+            allUTXOCTxIn[currentBlock->nHeight] = getSpendingScript(*tx, voutIndex);
    


    brunoerg commented at 1:36 pm on March 18, 2026:
    Not sure if it’s expected, but it looks only one CTxIn per block is retained because it’s being overwritten since you’re using the nHeight as index.

    RobinDavid commented at 2:24 pm on March 22, 2026:
    It works has pre-mined blocks only contains the coinbase transaction that only contains one output. We did it that way to keep the vector sorted by height (as block were iterated in from tip to genesis). But to make it more generic I changed it to use push_back and reverse it at the end.
  23. in src/test/fuzz/connect_block.cpp:301 in 87c6d12d58 outdated
    296+
    297+    /** Some harnesses want to explicitely read coinbase transactions in input */
    298+    if (coinbase) {
    299+        tx.vin.resize(1);                                   /** Size of vin hardcoded */
    300+        tx.vin[0].prevout.SetNull();
    301+        tx.vin[0].nSequence = CTxIn::MAX_SEQUENCE_NONFINAL; /** Make sure timelock is enforced. */
    


    brunoerg commented at 1:39 pm on March 18, 2026:
    nit: This comment looks wrong, timelock is not enforced, especially because it’s a coinbase.
  24. in src/test/fuzz/connect_block.cpp:579 in 87c6d12d58 outdated
    574+    /** If it does not exists write it into the ChainstateManager. */
    575+    if (blockIndex == nullptr) {
    576+        BlockValidationState state;
    577+
    578+        CBlockIndex* bestBlock = nullptr;
    579+        blockIndex = csm.m_blockman.AddToBlockIndex(block, bestBlock);
    


    marcofleon commented at 4:15 pm on March 18, 2026:
    I think pass in the chainstate manager’s m_best_header instead of the local bestBlock here. That way the headers get updated along with the blocks you add to the chain. This should fix the assertion failure.

    RobinDavid commented at 2:56 pm on March 22, 2026:

    Thas weird that it did not triggered earlier. Maybe the underlying behavior of AddBlockIndex had slightly changed. Writing it as an Assert was maybe a bit to Arbitrary. I changed it to:

    0        CBlockIndex* bestBlock = csm.m_best_header;
    1        blockIndex = csm.m_blockman.AddToBlockIndex(block, bestBlock);
    2        /* bestBlock equals blockIndex meaning it has properly been added
    3        *  and the block had accumulated more proof of work than the current best header.
    4        *  Thus early reject block otherwise.
    5        */
    6        if(bestBlock != blockIndex){
    7            DEBUGOUTPUT(std::cout << "Fail to add block to BlockManager or insufficient PoW" << std::endl);
    8            return nullptr;
    9        }
    
  25. marcofleon commented at 4:15 pm on March 18, 2026: contributor

    Base64: BAkAAAQJdwKGAAAAgBTpAACFBAk=

    crash_activate_best_chain_step.txt

     0===== stack trace ===== 
     1INFO: Running with entropic power schedule (0xFF, 100).
     2INFO: Seed: 3223231957
     3INFO: Loaded 1 modules   (580898 inline 8-bit counters): 580898 [0x556d05d1e360, 0x556d05dac082), 
     4INFO: Loaded 1 PC tables (580898 PCs): 580898 [0x556d05dac088,0x556d066892a8), 
     5/workdir/out/libfuzzer_msan/fuzz: Running 1 inputs 1 time(s) each.
     6Running: /workdir/workspace/solutions/crash-aa5b43f99124f0e080b2b68ac94024c65fc8381b
     7validation.cpp:5502 double ChainstateManager::GuessVerificationProgress(const CBlockIndex *) const: Assertion `m_best_header->nHeight >= pindex->nHeight' failed.
     8==10489== ERROR: libFuzzer: deadly signal
     9    [#0](/bitcoin-bitcoin/0/) 0x556d01ef1db9 in __sanitizer_print_stack_trace /llvm-project/compiler-rt/lib/msan/msan.cpp:790:3
    10    [#1](/bitcoin-bitcoin/1/) 0x556d01e618d8 in fuzzer::PrintStackTrace() /llvm-project/compiler-rt/lib/fuzzer/FuzzerUtil.cpp:210:5
    11    [#2](/bitcoin-bitcoin/2/) 0x556d01e442f3 in fuzzer::Fuzzer::CrashCallback() /llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:231:3
    12    [#3](/bitcoin-bitcoin/3/) 0x556d01f50c3d in SignalAction(int, void*, void*) /llvm-project/compiler-rt/lib/msan/msan_interceptors.cpp:1166:3
    13    [#4](/bitcoin-bitcoin/4/) 0x7f65ae046def  (/lib/x86_64-linux-gnu/libc.so.6+0x3fdef) (BuildId: fce446c9d4ad48e2b0c90cce1a11722897805281)
    14    [#5](/bitcoin-bitcoin/5/) 0x7f65ae09b95b  (/lib/x86_64-linux-gnu/libc.so.6+0x9495b) (BuildId: fce446c9d4ad48e2b0c90cce1a11722897805281)
    15    [#6](/bitcoin-bitcoin/6/) 0x7f65ae046cc1 in raise (/lib/x86_64-linux-gnu/libc.so.6+0x3fcc1) (BuildId: fce446c9d4ad48e2b0c90cce1a11722897805281)
    16    [#7](/bitcoin-bitcoin/7/) 0x7f65ae02f4ab in abort (/lib/x86_64-linux-gnu/libc.so.6+0x284ab) (BuildId: fce446c9d4ad48e2b0c90cce1a11722897805281)
    17    [#8](/bitcoin-bitcoin/8/) 0x556d03409b63 in assertion_fail(std::__1::source_location const&, std::__1::basic_string_view<char, std::__1::char_traits<char>>) /workdir/bitcoin/src/util/check.cpp:41:5
    18    [#9](/bitcoin-bitcoin/9/) 0x556d05435257 in bool&& inline_assertion_check<false, bool>(bool&&, std::__1::source_location const&, std::__1::basic_string_view<char, std::__1::char_traits<char>>) /workdir/bitcoin/src/util/check.h:90:13
    19    [#10](/bitcoin-bitcoin/10/) 0x556d05435257 in ChainstateManager::GuessVerificationProgress(CBlockIndex const*) const /workdir/bitcoin/src/validation.cpp:5502:10
    20    [#11](/bitcoin-bitcoin/11/) 0x556d05424776 in UpdateTipLog(ChainstateManager const&, CCoinsViewCache const&, CBlockIndex const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) /workdir/bitcoin/src/validation.cpp:2867:5
    21    [#12](/bitcoin-bitcoin/12/) 0x556d05422f9e in Chainstate::UpdateTip(CBlockIndex const*) /workdir/bitcoin/src/validation.cpp:2911:5
    22    [#13](/bitcoin-bitcoin/13/) 0x556d0542a016 in Chainstate::ConnectTip(BlockValidationState&, CBlockIndex*, std::__1::shared_ptr<CBlock const>, std::__1::vector<ConnectedBlock, std::__1::allocator<ConnectedBlock>>&, DisconnectedBlockTransactions&) /workdir/bitcoin/src/validation.cpp:3076:5
    23    [#14](/bitcoin-bitcoin/14/) 0x556d0543004c in Chainstate::ActivateBestChainStep(BlockValidationState&, CBlockIndex*, std::__1::shared_ptr<CBlock const> const&, bool&, std::__1::vector<ConnectedBlock, std::__1::allocator<ConnectedBlock>>&) /workdir/bitcoin/src/validation.cpp:3232:18
    24    [#15](/bitcoin-bitcoin/15/) 0x556d02312811 in (anonymous namespace)::TestChainstate::CallActivateBestChainStep(BlockValidationState&, CBlockIndex*, std::__1::shared_ptr<CBlock const> const&, bool&, std::__1::vector<ConnectedBlock, std::__1::allocator<ConnectedBlock>>&) /workdir/bitcoin/src/test/fuzz/connect_block.cpp:40:16
    25    [#16](/bitcoin-bitcoin/16/) 0x556d02312811 in activate_best_chain_step_fuzz_target(std::__1::span<unsigned char const, 18446744073709551615ul>) /workdir/bitcoin/src/test/fuzz/connect_block.cpp:755:32
    26    [#17](/bitcoin-bitcoin/17/) 0x556d01f6a365 in std::__1::__invoke_result_impl<void, void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>>::type std::__1::__invoke[abi:de210108]<void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>>(void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>&&) /libcxx_msan/include/c++/v1/__type_traits/invoke.h:87:27
    27    [#18](/bitcoin-bitcoin/18/) 0x556d01f6a365 in void std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:de210108]<void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>>(void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>&&) /libcxx_msan/include/c++/v1/__type_traits/invoke.h:342:5
    28    [#19](/bitcoin-bitcoin/19/) 0x556d01f6a365 in void std::__1::__invoke_r[abi:de210108]<void, void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>>(void (*&)(std::__1::span<unsigned char const, 18446744073709551615ul>), std::__1::span<unsigned char const, 18446744073709551615ul>&&) /libcxx_msan/include/c++/v1/__type_traits/invoke.h:348:10
    29    [#20](/bitcoin-bitcoin/20/) 0x556d01f6a365 in std::__1::__function::__func<void (*)(std::__1::span<unsigned char const, 18446744073709551615ul>), void (std::__1::span<unsigned char const, 18446744073709551615ul>)>::operator()(std::__1::span<unsigned char const, 18446744073709551615ul>&&) /libcxx_msan/include/c++/v1/__functional/function.h:174:12
    30    [#21](/bitcoin-bitcoin/21/) 0x556d02d8b8c1 in std::__1::__function::__value_func<void (std::__1::span<unsigned char const, 18446744073709551615ul>)>::operator()[abi:de210108](std::__1::span<unsigned char const, 18446744073709551615ul>&&) const /libcxx_msan/include/c++/v1/__functional/function.h:274:12
    31    [#22](/bitcoin-bitcoin/22/) 0x556d02d8b8c1 in std::__1::function<void (std::__1::span<unsigned char const, 18446744073709551615ul>)>::operator()(std::__1::span<unsigned char const, 18446744073709551615ul>) const /libcxx_msan/include/c++/v1/__functional/function.h:772:10
    32    [#23](/bitcoin-bitcoin/23/) 0x556d02d8b8c1 in test_one_input(std::__1::span<unsigned char const, 18446744073709551615ul>) /workdir/bitcoin/src/test/fuzz/fuzz.cpp:88:5
    33    [#24](/bitcoin-bitcoin/24/) 0x556d02d8b8c1 in LLVMFuzzerTestOneInput /workdir/bitcoin/src/test/fuzz/fuzz.cpp:216:5
    34    [#25](/bitcoin-bitcoin/25/) 0x556d01e45a4b in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:619:13
    35    [#26](/bitcoin-bitcoin/26/) 0x556d01e2f6f2 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
    36    [#27](/bitcoin-bitcoin/27/) 0x556d01e35590 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:864:9
    37    [#28](/bitcoin-bitcoin/28/) 0x556d01e62262 in main /llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    38    [#29](/bitcoin-bitcoin/29/) 0x7f65ae030ca7  (/lib/x86_64-linux-gnu/libc.so.6+0x29ca7) (BuildId: fce446c9d4ad48e2b0c90cce1a11722897805281)
    39    [#30](/bitcoin-bitcoin/30/) 0x7f65ae030d64 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29d64) (BuildId: fce446c9d4ad48e2b0c90cce1a11722897805281)
    40    [#31](/bitcoin-bitcoin/31/) 0x556d01e286e0 in _start (/workdir/out/libfuzzer_msan/fuzz+0x13696e0)
    41
    42NOTE: libFuzzer has rudimentary signal handlers.
    43      Combine libFuzzer with AddressSanitizer or similar for better crash reports.
    44SUMMARY: libFuzzer: deadly signal
    
  26. RobinDavid force-pushed on Mar 22, 2026
  27. RobinDavid force-pushed on Mar 22, 2026
  28. DrahtBot added the label CI failed on Mar 22, 2026
  29. RobinDavid force-pushed on Mar 22, 2026
  30. Implement connect_block fuzzing harness 673cfd3b44
  31. RobinDavid force-pushed on Mar 22, 2026
  32. DrahtBot removed the label CI failed on Mar 22, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-03-23 09:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me