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 +62 −20
  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 verifies that both messages accept 101 hashes and disconnect at 102.

  2. DrahtBot added the label P2P on Aug 7, 2026
  3. 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
    Stale ACK achow101, 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.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35027 (net: add -outboundbind option for outgoing source address by 8144225309)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. l0rinc force-pushed on Aug 8, 2026
  5. DrahtBot added the label CI failed on Aug 8, 2026
  6. 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>

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

    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?

  9. in src/primitives/block.h:144 in 725bf357d9 outdated
     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.


    mzumsande commented at 1:36 PM on September 1, 2026:

    I'm not very familiar with the serialization code, so I don't have a strong opinion - just a question: Why did you choose this approach (having to touch CBlockLocator the and serialization code), instead of the existing HEADERS approach that does it locally in net_processing? Should we use one of the two approaches in multiple places for consistency?


    sedited commented at 4:22 PM on September 1, 2026:

    I think I'd have a slight preference towards using the same approach. Adding another exception, to what should just be normal control flow reads a bit clunky.


    ajtowns commented at 8:54 PM on September 1, 2026:

    HEADERS processing does it manually because there's a discrepancy between the serialize and deserialize formats -- it's serialized as a vector of cblocks with no transactions since sending the message predated the CBlockHeader structure, but is deserialized as essentially a vector of pairs of CBlockHeader and an ignored compactsize value.

    The difference would be something like:

        try {
            locator.LimitedRead<MAX_LOCATOR_SZ>(stream);
            stream >> hash_stop;
            return true;
        } catch (LimitedVectorExceededError& e) {
            LogDebug(BCLog::NET, "%s locator size %u > %u, %s", msg_type, e.m_size, MAX_LOCATOR_SZ, node.DisconnectMsg());
            node.fDisconnect = true;
            return false;
        }
    

    vs

        stream.ignore(4); // dummy version
        size_t loc_sz = ReadCompactSize(stream);
        if (loc_sz > MAX_LOCATOR_SZ) {
            LogDebug(BCLog::NET, "%s locator size %u > %u, %s", msg_type, e.m_size, MAX_LOCATOR_SZ, node.DisconnectMsg());
            node.fDisconnect = true;
            return false;
        }
        locator.vHave.reserve(loc_sz);
        while (loc_sz-- > 0) {
            locator.vHave.emplace_back();
            stream >> locator.vHave.back();
        }
        return true;
    

    which seems worse to me. I don't think the "exception" overhead versus "normal control flow" is a loss here, but losing the encapsulation of dummy-version and vector deserialization is something of a loss.

    Just using LIMITED_VECTOR directly in the CBlockLocator serialization function would be slightly simpler than introducing the LimitedRead function, making the non-exceptional path just try { stream >> locator >> hash_stop; return true; }. Would probably change fuzz test behaviour though.


    l0rinc commented at 8:51 PM on September 4, 2026:

    Thanks for the comments, as @ajtowns also mentioned checking the count locally means we can no longer use ordinary vector deserialization since we've consumed the size already and have to reproduce the element-reading loop in net_processing.

    I'm also not a fan of exceptions for control flow, but this is arguably an exceptional case - but I don't think it really matters. Let me know if you have strong opinions and concrete alternative suggestions.

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

    Untested, but looks fine to me.

  11. achow101 commented at 9:48 PM on August 17, 2026: member

    ACK 725bf357d9ef652eacd809a1bf2f6f79ec73a0b4

  12. jeanpablojp commented at 9:45 PM on August 19, 2026: contributor

    tACK 725bf357d9ef652eacd809a1bf2f6f79ec73a0b4

  13. sedited requested review from mzumsande on Aug 29, 2026
  14. l0rinc force-pushed on Sep 4, 2026
  15. test: characterize oversized locator handling
    Record that truncated `getblocks` and `getheaders` locators leave the peer connected when their advertised count exceeds `MAX_LOCATOR_SZ` and generic vector deserialization fails.
    7c4a1c4104
  16. 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.
    
    Apply the formatter's limit even when the count exceeds `MAX_SIZE`, retaining the full `uint64_t` count until after the check.
    This also preserves the reported count on 32-bit builds.
    
    Co-authored-by: Anthony Towns <aj@erisian.com.au>
    Co-authored-by: JP <jeanpablo.jp@hotmail.com>
    962d4ea2a8
  17. net: extract block locator reader
    Share locator deserialization and the existing `MAX_LOCATOR_SZ` check between `getblocks` and `getheaders`.
    0b282936fd
  18. net: reject oversized locators before allocating
    `getblocks` and `getheaders` apply `MAX_LOCATOR_SZ` only after deserializing locator hashes.
    A truncated locator with an oversized count fails generic vector deserialization before that check, leaving the peer connected.
    
    Use `CBlockLocator::LimitedRead` to detect the oversized count before allocating hashes.
    The message handler catches only `LimitedVectorExceededError` and disconnects the peer.
    Other deserialization failures keep following the generic non-disconnecting exception path.
    
    Complete oversized locators continue to disconnect without discouragement.
    `p2p_invalid_locator.py` verifies that both messages accept 101 hashes and disconnect at 102.
    
    Co-authored-by: Anthony Towns <aj@erisian.com.au>
    e327916668
  19. l0rinc force-pushed on Sep 4, 2026
  20. DrahtBot added the label CI failed on Sep 4, 2026
  21. l0rinc commented at 9:05 PM on September 4, 2026: contributor

    Rebased and addressed @jeanpablojp's finding: counts above MAX_SIZE now also disconnect, with regression coverage alongside the original truncated-locator case. The count remains uint64_t through the limit check to avoid truncation on 32-bit builds. Thanks!

  22. DrahtBot removed the label CI failed on Sep 4, 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-09-09 07:56 UTC