net: reject oversized locators before allocating #35936

pull l0rinc wants to merge 4 commits into bitcoin:master from l0rinc:l0rinc/reject-oversized-locators changing 4 files +58 −18
  1. l0rinc commented at 11:26 PM on August 7, 2026: contributor

    Problem: getblocks and getheaders enforce MAX_LOCATOR_SZ only after deserializing locator hashes. A truncated locator advertising an oversized count makes generic vector deserialization fail before the disconnect check, leaving the peer connected.

    Fix: Read the advertised count before allocating hashes and disconnect when it exceeds the existing limit. Catch only the size-limit error so other deserialization failures remain non-disconnecting. Complete oversized locators continue to disconnect without discouragement, and p2p_invalid_locator.py preserves the established boundary: both messages accept 101 hashes and disconnect at 102.

  2. test: characterize oversized locator handling
    Record that truncated oversized `getblocks` and `getheaders` locators currently leave the peer connected after generic vector deserialization fails.
    1f53cab13d
  3. serialize: distinguish limited vector errors
    `LIMITED_VECTOR` throws a generic stream failure when the advertised count exceeds its limit.
    Add a dedicated subtype so callers can catch that condition without handling unrelated deserialization errors.
    
    Co-authored-by: Anthony Towns <aj@erisian.com.au>
    36321350a5
  4. net: extract block locator reader
    Share locator deserialization and the existing `MAX_LOCATOR_SZ` check between `getblocks` and `getheaders`.
    dd5e1119ed
  5. DrahtBot added the label P2P on Aug 7, 2026
  6. DrahtBot commented at 11:26 PM on August 7, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35936.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Approach ACK jeanpablojp

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  7. net: reject oversized locators before allocating
    `getblocks` and `getheaders` apply `MAX_LOCATOR_SZ` only after deserializing locator hashes, so a truncated locator advertising an oversized count makes generic vector deserialization fail before the disconnect check and leaves the peer connected.
    
    Use `CBlockLocator::LimitedRead` to detect the oversized count before allocating hashes, then catch only `LimitedVectorExceededError` in the message handler and disconnect the peer.
    Other deserialization failures keep following the generic non-disconnecting exception path.
    
    Complete oversized locators continue to disconnect without discouragement, and `p2p_invalid_locator.py` preserves the established boundary: both messages accept 101 hashes and disconnect at 102.
    
    Co-authored-by: Anthony Towns <aj@erisian.com.au>
    725bf357d9
  8. l0rinc force-pushed on Aug 8, 2026
  9. DrahtBot added the label CI failed on Aug 8, 2026
  10. DrahtBot commented at 12:20 AM on August 8, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/31227149534/job/93023654808</sub> <sub>LLM reason (✨ experimental): CI failed because IWYU reported a headers/include issue (generated “Failure generated from IWYU” and exited non-zero on src/primitives/block.h).</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  11. DrahtBot removed the label CI failed on Aug 8, 2026
  12. jeanpablojp commented at 2:12 PM on August 9, 2026: none

    Approach ACK. The dedicated exception subtype is the right shape, and the truncated case it targets is fixed.

    The same message still leaves the peer connected once the count goes above MAX_SIZE. LIMITED_VECTOR only sees counts that ReadCompactSize accepts, and ReadCompactSize range-checks against MAX_SIZE first, throwing a plain std::ios_base::failure, so a locator advertising more than 33554432 hashes reaches the generic handler in ProcessMessages. I ran your test with COMPACTSIZE(uint64_t{MAX_SIZE} + 1) in place of COMPACTSIZE(MAX_SIZE / sizeof(uint256)): getblocks and getheaders both end with fDisconnect false on 725bf357d9ef652eacd809a1bf2f6f79ec73a0b4. Both counts are the same 5 bytes on the wire.

    Reading the count without the range check makes Limit the only bound, and that case disconnects too:

    -        size_t size = ReadCompactSize(s);
    +        const uint64_t size{ReadCompactSize(s, /*range_check=*/false)};
             if (size > Limit) {
                 throw LimitedVectorExceededError{size};
             }
    -        v.reserve(size);
    +        v.reserve(static_cast<size_t>(size));
    

    m_size has to widen to uint64_t with it, since size_t truncates on the 32-bit builds. With that applied the unit suite, p2p_invalid_locator.py and p2p_invalid_messages.py pass.

    Reverting only src/net_processing.cpp to 1f53cab and keeping the new test makes net_tests/oversized_locator_handling fail on node.fDisconnect for both message types.

    I have tested the code on your head merged onto master 128456b.

    Is the case above MAX_SIZE out of scope here on purpose?

  13. in src/primitives/block.h:144 in 725bf357d9
     137 | @@ -137,6 +138,13 @@ struct CBlockLocator
     138 |          READWRITE(obj.vHave);
     139 |      }
     140 |  
     141 | +    template <size_t Limit, typename Stream>
     142 | +    void LimitedRead(Stream& s)
     143 | +    {
     144 | +        s.ignore(sizeof(DUMMY_VERSION));
    


    ajtowns commented at 11:49 PM on August 9, 2026:

    I think s.ignore(4); would be better; we don't do ignore(sizeof(..)) anywhere else, as far as I can see, and it just seems to be added indirection.


    l0rinc commented at 1:56 AM on August 10, 2026:

    it just seems to be added indirection

    I wanted to ignore adding a code comment here explaining the meaning of the magic constant, which DUMMY_VERSION already does. We're basically skipping over the version field which "ignore size of version" documents with code nicely. Let me know if you feel strongly about it and I'll change it, but I did this deliberately.

  14. ajtowns commented at 11:50 PM on August 9, 2026: contributor

    Untested, but looks fine to me.


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-08-11 10:50 UTC

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