Merge bitcoin/bitcoin#35872: rpc: avoid descriptor range counter overflow
What changed, and why it matters
This update fixes a counting bug in several Bitcoin Core RPC commands that scan descriptors. When a user requested a descriptor range ending at the maximum allowed value (2,147,483,647), the internal counter used a smaller integer type and could wrap past its maximum, causing undefined behavior. In practice this could crash builds that catch such errors, or silently misbehave in others. The fix widens the loop counter to a 64-bit integer so it can safely reach and pass the endpoint. A test was added to confirm the edge case now works.
Apply the patch to widen the loop counter to int64_t and include the regression test. Because the affected RPCs require authentication, prioritize updating nodes where untrusted or compromised authenticated users may call these endpoints. No emergency network-wide action is indicated.
Security signals we found
Signed integer overflow in descriptor expansion loop
Crash/undefined behavior on maximum-range descriptor scans
Authenticated RPC surface affected (scantxoutset, scanblocks, getdescriptoractivity, utxoupdatepsbt, descriptorprocesspsbt)
Prior similar fix in deriveaddresses (PR #26275) indicates recurring endpoint overflow pattern
Evidence from the diff
In src/rpc/util.cpp, EvalDescriptorStringOrObject iterates descriptor expansion over [range.first, range.second] using an int counter. Because range.second can be INT_MAX (2^31-1), the loop body executes for i == INT_MAX, then ++i overflows int before the i <= range.second comparison. This is signed integer overflow (undefined behavior); on trap-enabled builds it aborts, otherwise it wraps to INT_MIN and continues. The patch changes the loop variable to int64_t, making the one-past-the-end value representable and avoiding overflow. The supplied test in rpc_scantxoutset.py exercises scantxoutset with range [2^31-1, 2^31-1] and expects success. The same helper is used by scantxoutset, scanblocks, getdescriptoractivity, utxoupdatepsbt, and descriptorprocesspsbt, so all are affected.
Changed components
src/rpc/util.cpp::EvalDescriptorStringOrObjectRPC scantxoutsetRPC scanblocksRPC getdescriptoractivityRPC utxoupdatepsbtRPC descriptorprocesspsbtInspect captured patch +3 / −1
### src/rpc/util.cpp
@@ -1364,7 +1364,7 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
range.second = 0;
}
std::vector<CScript> ret;
- for (int i = range.first; i <= range.second; ++i) {
+ for (int64_t i = range.first; i <= range.second; ++i) {
for (const auto& desc : descs) {
std::vector<CScript> scripts;
if (!desc->Expand(i, provider, scripts, provider)) {
### test/functional/rpc_scantxoutset.py
@@ -80,6 +80,8 @@ def run_test(self):
assert_raises_rpc_error(-8, "End of range is too high", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [(2 << 31 + 1) - 1000000, (2 << 31 + 1)]}])
assert_raises_rpc_error(-8, "Range specified as [begin,end] must not have begin after end", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [2, 1]}])
assert_raises_rpc_error(-8, "Range is too large", self.nodes[0].scantxoutset, "start", [{"desc": "desc", "range": [0, 1000001]}])
+ range_end = 2**31 - 1
+ assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": [range_end, range_end]}])['success'], True)
self.log.info("Test extended key derivation.")
# Run various scans, and verify that the sum of the amounts of the matches corresponds to the expected subset.Why this scored 62/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.