{
 "number": 35936,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35936",
 "title": "net: reject oversized locators before allocating",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-08-07T23:26:46Z",
 "updated_at": "2026-09-16T16:35:20Z",
 "age_days": 40,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "e327916668258557ca52c0e343c766aec8f9b440",
 "head_ref": "l0rinc/reject-oversized-locators",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-08-08T00:19:59Z",
   "sha": "725bf357d9ef652eacd809a1bf2f6f79ec73a0b4"
  },
  {
   "t": "2026-09-04T20:51:37Z",
   "sha": "1389e2cf26124674fac8afe0065b3ae59fa150f3"
  },
  {
   "t": "2026-09-04T21:03:28Z",
   "sha": "e327916668258557ca52c0e343c766aec8f9b440"
  }
 ],
 "additions": 62,
 "deletions": 20,
 "changed_files": 4,
 "commit_count": 4,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "stale_ack": [
     {
      "login": "achow101",
      "url": "https://github.com/bitcoin/bitcoin/pull/35936#issuecomment-5320659000"
     },
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/35936#issuecomment-5348373195"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36244,
     "title": "validation, net: Process blocks asynchronously and reduce cs_main contention",
     "author": "w0xlt"
    },
    {
     "number": 35027,
     "title": "net: add -outboundbind option for outgoing source address",
     "author": "8144225309"
    }
   ]
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "approach_ack",
   "hash": null,
   "t": "2026-08-09T14:12:51Z",
   "stale": false
  },
  "achow101": {
   "kind": "ack",
   "hash": "725bf357d9ef652eacd809a1bf2f6f79ec73a0b4",
   "t": "2026-08-17T21:48:40Z",
   "stale": true
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 0,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "achow101",
   "ajtowns",
   "jeanpablojp",
   "mzumsande",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-04T21:05:28Z",
  "last_reviewer_activity": "2026-09-01T20:54:24Z",
  "last_reviewer": "ajtowns",
  "author_silent_days": 12,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   36244,
   35027
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/primitives/block.h"
 ],
 "body": "**Problem:** `getblocks` and `getheaders` enforce `MAX_LOCATOR_SZ` only after deserializing locator hashes.\nA truncated locator advertising an oversized count makes generic vector deserialization fail before the disconnect check, leaving the peer connected.\n\n**Fix:** Read the advertised count before allocating hashes and disconnect when it exceeds the existing limit.\nCatch only the size-limit error so other deserialization failures remain non-disconnecting.\nComplete oversized locators continue to disconnect without discouragement, and `p2p_invalid_locator.py` verifies that both messages accept 101 hashes and disconnect at 102.",
 "commits": [
  {
   "sha": "7c4a1c4104b473d0be8728778334fdbb6a566a14",
   "date": "2026-09-04T20:58:02Z",
   "message": "test: characterize oversized locator handling\n\nRecord that truncated `getblocks` and `getheaders` locators leave the peer connected when their advertised count exceeds `MAX_LOCATOR_SZ` and generic vector deserialization fails."
  },
  {
   "sha": "962d4ea2a86531c85941b4503c68d8067cbf12df",
   "date": "2026-09-04T20:58:02Z",
   "message": "serialize: distinguish limited vector errors\n\n`LIMITED_VECTOR` throws a generic stream failure when the advertised count exceeds its limit.\nAdd a dedicated subtype so callers can catch that condition without handling unrelated deserialization errors.\n\nApply the formatter's limit even when the count exceeds `MAX_SIZE`, retaining the full `uint64_t` count until after the check.\nThis also preserves the reported count on 32-bit builds.\n\nCo-authored-by: Anthony Towns <aj@erisian.com.au>\nCo-authored-by: JP <jeanpablo.jp@hotmail.com>"
  },
  {
   "sha": "0b282936fdc4cbee22ae10d15a4a72f7e7635d69",
   "date": "2026-09-04T20:58:03Z",
   "message": "net: extract block locator reader\n\nShare locator deserialization and the existing `MAX_LOCATOR_SZ` check between `getblocks` and `getheaders`."
  },
  {
   "sha": "e327916668258557ca52c0e343c766aec8f9b440",
   "date": "2026-09-04T20:58:03Z",
   "message": "net: reject oversized locators before allocating\n\n`getblocks` and `getheaders` apply `MAX_LOCATOR_SZ` only after deserializing locator hashes.\nA truncated locator with an oversized count fails generic vector deserialization before that check, leaving the peer connected.\n\nUse `CBlockLocator::LimitedRead` to detect the oversized count before allocating hashes.\nThe message handler catches only `LimitedVectorExceededError` and disconnects the peer.\nOther deserialization failures keep following the generic non-disconnecting exception path.\n\nComplete oversized locators continue to disconnect without discouragement.\n`p2p_invalid_locator.py` verifies that both messages accept 101 hashes and disconnect at 102.\n\nCo-authored-by: Anthony Towns <aj@erisian.com.au>"
  }
 ],
 "timeline": [
  {
   "t": "2026-08-08T00:19:59Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "725bf357d9ef652eacd809a1bf2f6f79ec73a0b4"
  },
  {
   "t": "2026-08-09T14:12:51Z",
   "kind": "comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "text": "Approach ACK. The dedicated exception subtype is the right shape, and the truncated case it targets is fixed.\n\nThe 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.\n\nReading the count without the range check makes `Limit` the only bound, and that case disconnects too:\n\n```diff\n-        size_t size = ReadCompactSize(s);\n+        const uint64_t size{ReadCompactSize(s, /*range_check=*/false)};\n         if (size > Limit) {\n             throw LimitedVectorExceededError{size};\n         }\n-        v.reserve(size);\n+        v.reserve(static_cast<size_t>(size));\n```\n\n`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.\n\nReverting 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.\n\nI have tested the code on your head merged onto master 128456b.\n\nIs the case above `MAX_SIZE` out of scope here on purpose?"
  },
  {
   "t": "2026-08-09T23:49:03Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/primitives/block.h",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "in_reply_to": null,
   "text": "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."
  },
  {
   "t": "2026-08-09T23:50:52Z",
   "kind": "review",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "725bf357d9ef652eacd809a1bf2f6f79ec73a0b4",
   "text": "Untested, but looks fine to me."
  },
  {
   "t": "2026-08-10T01:56:27Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/block.h",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "in_reply_to": 3745707937,
   "text": "[quoted text omitted]\n\nI wanted to ignore adding a code comment here explaining the meaning of the magic constant, which `DUMMY_VERSION` already does.\nWe're basically skipping over the version field which \"ignore size of version\" documents with code nicely.\nLet me know if you feel strongly about it and I'll change it, but I did this deliberately."
  },
  {
   "t": "2026-08-17T21:48:40Z",
   "kind": "comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "text": "ACK 725bf357d9ef652eacd809a1bf2f6f79ec73a0b4"
  },
  {
   "t": "2026-08-19T21:45:31Z",
   "kind": "comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "text": "tACK 725bf357d9ef652eacd809a1bf2f6f79ec73a0b4"
  },
  {
   "t": "2026-09-01T13:36:00Z",
   "kind": "review_comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "path": "src/primitives/block.h",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "in_reply_to": null,
   "text": "I'm not very familiar with the serialization code, so I don't have a strong opinion - just a question:\nWhy 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?"
  },
  {
   "t": "2026-09-01T16:22:23Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/primitives/block.h",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "in_reply_to": 3904646262,
   "text": "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."
  },
  {
   "t": "2026-09-01T20:54:24Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/primitives/block.h",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "in_reply_to": 3904646262,
   "text": "`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.\n\nThe difference would be something like:\n\n```c++\n    try {\n        locator.LimitedRead<MAX_LOCATOR_SZ>(stream);\n        stream >> hash_stop;\n        return true;\n    } catch (LimitedVectorExceededError& e) {\n        LogDebug(BCLog::NET, \"%s locator size %u > %u, %s\", msg_type, e.m_size, MAX_LOCATOR_SZ, node.DisconnectMsg());\n        node.fDisconnect = true;\n        return false;\n    }\n```\n\nvs\n\n```c++\n    stream.ignore(4); // dummy version\n    size_t loc_sz = ReadCompactSize(stream);\n    if (loc_sz > MAX_LOCATOR_SZ) {\n        LogDebug(BCLog::NET, \"%s locator size %u > %u, %s\", msg_type, e.m_size, MAX_LOCATOR_SZ, node.DisconnectMsg());\n        node.fDisconnect = true;\n        return false;\n    }\n    locator.vHave.reserve(loc_sz);\n    while (loc_sz-- > 0) {\n        locator.vHave.emplace_back();\n        stream >> locator.vHave.back();\n    }\n    return true;\n```\n\nwhich 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.\n\nJust 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."
  },
  {
   "t": "2026-09-04T20:51:08Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/primitives/block.h",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "in_reply_to": 3904646262,
   "text": "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.\n\nI'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."
  },
  {
   "t": "2026-09-04T20:51:37Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "1389e2cf26124674fac8afe0065b3ae59fa150f3"
  },
  {
   "t": "2026-09-04T21:03:28Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440"
  },
  {
   "t": "2026-09-04T21:05:28Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "e327916668258557ca52c0e343c766aec8f9b440",
   "text": "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!"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-07T23:26:49Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-08T00:20:19Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-08T01:47:39Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-04T21:04:44Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-04T22:14:02Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 7544,
 "text_tokens_estimate": 1886,
 "changed_paths": [
  "src/net_processing.cpp",
  "src/primitives/block.h",
  "src/serialize.h",
  "src/test/net_tests.cpp"
 ],
 "files": [
  {
   "path": "src/net_processing.cpp",
   "add": 16,
   "del": 14
  },
  {
   "path": "src/primitives/block.h",
   "add": 8,
   "del": 0
  },
  {
   "path": "src/serialize.h",
   "add": 12,
   "del": 6
  },
  {
   "path": "src/test/net_tests.cpp",
   "add": 26,
   "del": 0
  }
 ],
 "test_lines": 26,
 "git": {
  "head": "e327916668258557ca52c0e343c766aec8f9b440",
  "head_matches_backup": true,
  "base": "4519933391dd23dbf1a4eceec6dd53d2e9e71cc3",
  "commits": [
   {
    "sha": "7c4a1c4104",
    "subject": "test: characterize oversized locator handling",
    "files": 1,
    "add": 26,
    "del": 0
   },
   {
    "sha": "962d4ea2a8",
    "subject": "serialize: distinguish limited vector errors",
    "files": 1,
    "add": 12,
    "del": 6
   },
   {
    "sha": "0b282936fd",
    "subject": "net: extract block locator reader",
    "files": 1,
    "add": 14,
    "del": 14
   },
   {
    "sha": "e327916668",
    "subject": "net: reject oversized locators before allocating",
    "files": 3,
    "add": 15,
    "del": 5
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "24ba67e0db3441be",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}