Skip to content

Commit 2406ebc

Browse files
Merge dashpay#7418: fix(net): bound signing message vector intake
8ff75e0 llmq: cover QBSIGSHARES aggregate sig-share bound with a testable decoder (PastaClaw) e51e304 llmq: bound LLMQ signing vector intake (PastaClaw) c0af156 llmq: bound batched sig-share intake (PastaClaw) Pull request description: Depends on dashpay#7439. Please review only the two command-specific commits here. ## Issue being fixed or feature implemented LLMQ signing P2P messages previously deserialized peer-controlled vectors before enforcing the existing per-message count limits. This hardens the signing message intake path so oversized counts are rejected before vector materialization. This intentionally does not include the QFCOMMITMENT dynamic-bitset fix. ## What was done? - Use the shared runtime-bounded vector reader for signing message batches in `NetSigning::ProcessMessage`. - Bound `CBatchedSigShares::sigShares` declaratively with `LIMITED_VECTOR`. - Retain explicit running-total accounting for QBSIGSHARES, where many individually valid batches could exceed the aggregate cap. - Ban peers on malformed or oversized signing vectors. - Add unit coverage for the exact boundary and oversized batched sig-share vectors. The reusable bounded-vector serialization primitives are introduced separately in dashpay#7439. ## How Has This Been Tested? - `src/test/test_dash --run_test=llmq_utils_tests` - `src/test/test_dash --run_test=serialize_tests` - `test/lint/lint-whitespace.py` - `test/lint/lint-circular-dependencies.py` ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only) ACKs for top commit: UdjinM6: utACK 8ff75e0 Tree-SHA512: 873fb78ef55749449c0548828dc8709bf0709637732a000bd009d8bba9ed0270e1582eb0020f852c3a8e8bb6d448d1f7b8c0402e271ac42155081a9d13d25bc8
2 parents f153015 + 8ff75e0 commit 2406ebc

4 files changed

Lines changed: 143 additions & 30 deletions

File tree

src/llmq/net_signing.cpp

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#include <llmq/signing_shares.h>
1111
#include <llmq/signing.h>
1212
#include <spork.h>
13-
#include <util/std23.h>
1413

1514
#include <logging.h>
1615
#include <streams.h>
@@ -24,6 +23,26 @@
2423
#include <unordered_map>
2524

2625
namespace llmq {
26+
std::vector<CBatchedSigShares> UnserializeBatchedSigShares(CDataStream& vRecv)
27+
{
28+
std::vector<CBatchedSigShares> msgs;
29+
const uint64_t msgs_size{ReadCompactSize(vRecv, /*range_check=*/false)};
30+
if (msgs_size > MAX_MSGS_TOTAL_BATCHED_SIGS) {
31+
throw std::ios_base::failure("QBSIGSHARES batch count too large");
32+
}
33+
msgs.reserve(msgs_size);
34+
size_t total_sigs_count{0};
35+
while (msgs.size() < msgs_size) {
36+
msgs.emplace_back();
37+
vRecv >> msgs.back();
38+
total_sigs_count += msgs.back().sigShares.size();
39+
if (total_sigs_count > MAX_MSGS_TOTAL_BATCHED_SIGS) {
40+
throw std::ios_base::failure("QBSIGSHARES sig share count too large");
41+
}
42+
}
43+
return msgs;
44+
}
45+
2746
void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv)
2847
{
2948
if (msg_type == NetMsgType::QSIGREC) {
@@ -45,13 +64,15 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData
4564

4665
if (m_sporkman.IsSporkActive(SPORK_21_QUORUM_ALL_CONNECTED) && msg_type == NetMsgType::QSIGSHARE) {
4766
std::vector<CSigShare> receivedSigShares;
48-
vRecv >> receivedSigShares;
49-
50-
if (receivedSigShares.size() > CSigSharesManager::MAX_MSGS_SIG_SHARES) {
51-
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- too many sigs in QSIGSHARE message. cnt=%d, max=%d, node=%d\n",
52-
__func__, receivedSigShares.size(), CSigSharesManager::MAX_MSGS_SIG_SHARES, pfrom.GetId());
67+
try {
68+
if (!UnserializeVectorWithMaxSize(vRecv, receivedSigShares, CSigSharesManager::MAX_MSGS_SIG_SHARES)) {
69+
throw std::ios_base::failure("QSIGSHARE vector size too large");
70+
}
71+
} catch (const std::ios_base::failure& e) {
72+
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- rejected %s from peer=%d: %s\n",
73+
__func__, msg_type, pfrom.GetId(), e.what());
5374
BanNode(pfrom.GetId());
54-
return;
75+
throw;
5576
}
5677

5778
for (const auto& sigShare : receivedSigShares) {
@@ -63,13 +84,15 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData
6384

6485
if (msg_type == NetMsgType::QSIGSESANN) {
6586
std::vector<CSigSesAnn> msgs;
66-
vRecv >> msgs;
67-
if (msgs.size() > CSigSharesManager::MAX_MSGS_CNT_QSIGSESANN) {
68-
LogPrint(BCLog::LLMQ_SIGS, /* Continued */
69-
"NetSigning::%s -- too many announcements in QSIGSESANN message. cnt=%d, max=%d, node=%d\n",
70-
__func__, msgs.size(), CSigSharesManager::MAX_MSGS_CNT_QSIGSESANN, pfrom.GetId());
87+
try {
88+
if (!UnserializeVectorWithMaxSize(vRecv, msgs, CSigSharesManager::MAX_MSGS_CNT_QSIGSESANN)) {
89+
throw std::ios_base::failure("QSIGSESANN vector size too large");
90+
}
91+
} catch (const std::ios_base::failure& e) {
92+
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- rejected %s from peer=%d: %s\n",
93+
__func__, msg_type, pfrom.GetId(), e.what());
7194
BanNode(pfrom.GetId());
72-
return;
95+
throw;
7396
}
7497
if (!std::ranges::all_of(msgs, [this, &pfrom](const auto& ann) {
7598
return m_shares_manager->ProcessMessageSigSesAnn(pfrom, ann);
@@ -80,17 +103,15 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData
80103
} else if (msg_type == NetMsgType::QSIGSHARESINV || msg_type == NetMsgType::QGETSIGSHARES) {
81104
std::vector<CSigSharesInv> msgs;
82105
try {
83-
vRecv >> msgs;
84-
} catch (const std::ios_base::failure&) {
106+
if (!UnserializeVectorWithMaxSize(vRecv, msgs, CSigSharesManager::MAX_MSGS_CNT_QSIGSHARES)) {
107+
throw std::ios_base::failure("QSIGSHARESINV vector size too large");
108+
}
109+
} catch (const std::ios_base::failure& e) {
110+
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- rejected %s from peer=%d: %s\n",
111+
__func__, msg_type, pfrom.GetId(), e.what());
85112
BanNode(pfrom.GetId());
86113
throw;
87114
}
88-
if (msgs.size() > CSigSharesManager::MAX_MSGS_CNT_QSIGSHARES) {
89-
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- too many invs in %s message. cnt=%d, max=%d, node=%d\n",
90-
__func__, msg_type, msgs.size(), CSigSharesManager::MAX_MSGS_CNT_QSIGSHARES, pfrom.GetId());
91-
BanNode(pfrom.GetId());
92-
return;
93-
}
94115
if (!std::ranges::all_of(msgs, [this, &pfrom, &msg_type](const auto& inv) {
95116
return m_shares_manager->ProcessMessageSigShares(pfrom, inv, msg_type);
96117
})) {
@@ -99,15 +120,13 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData
99120
}
100121
} else if (msg_type == NetMsgType::QBSIGSHARES) {
101122
std::vector<CBatchedSigShares> msgs;
102-
vRecv >> msgs;
103-
const size_t totalSigsCount = std23::ranges::fold_left(msgs, size_t{0}, [](size_t s, const auto& bs) {
104-
return s + bs.sigShares.size();
105-
});
106-
if (totalSigsCount > MAX_MSGS_TOTAL_BATCHED_SIGS) {
107-
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- too many sigs in QBSIGSHARES message. cnt=%d, max=%d, node=%d\n",
108-
__func__, msgs.size(), MAX_MSGS_TOTAL_BATCHED_SIGS, pfrom.GetId());
123+
try {
124+
msgs = UnserializeBatchedSigShares(vRecv);
125+
} catch (const std::ios_base::failure& e) {
126+
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- rejected %s from peer=%d: %s\n",
127+
__func__, msg_type, pfrom.GetId(), e.what());
109128
BanNode(pfrom.GetId());
110-
return;
129+
throw;
111130
}
112131
if (!std::ranges::all_of(msgs, [this, &pfrom](const auto& bs) {
113132
return m_shares_manager->ProcessMessageBatchedSigShares(pfrom, bs);

src/llmq/net_signing.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ namespace llmq {
2424
class CSigSharesManager;
2525
class CSigningManager;
2626

27+
//! Decode a QBSIGSHARES payload into its vector of CBatchedSigShares.
28+
//!
29+
//! The inner sigShares vector of each batch is bounded by CBatchedSigShares's
30+
//! SERIALIZE_METHODS via LIMITED_VECTOR, but many individually-valid batches
31+
//! could still exceed the total sig-share cap, so this bounds the outer batch
32+
//! count and checks the running total of inner sig shares as it decodes,
33+
//! stopping before an attacker forces us through the full cross product of the
34+
//! per-vector limits. A wire count above the cap (outer batch count or running
35+
//! inner total) throws std::ios_base::failure once detected, leaving the caller
36+
//! to log, ban, and rethrow uniformly.
37+
std::vector<CBatchedSigShares> UnserializeBatchedSigShares(CDataStream& vRecv);
38+
2739
class NetSigning final : public NetHandler, public CValidationInterface
2840
{
2941
public:

src/llmq/signing_shares.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ class CBatchedSigShares
154154
public:
155155
SERIALIZE_METHODS(CBatchedSigShares, obj)
156156
{
157-
READWRITE(VARINT(obj.sessionId), obj.sigShares);
157+
READWRITE(VARINT(obj.sessionId), LIMITED_VECTOR(obj.sigShares, MAX_MSGS_TOTAL_BATCHED_SIGS));
158158
}
159159

160160
[[nodiscard]] std::string ToInvString() const;

src/test/llmq_utils_tests.cpp

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
#include <test/util/setup_common.h>
77

88
#include <consensus/params.h>
9+
#include <llmq/net_signing.h>
910
#include <llmq/params.h>
1011
#include <llmq/signing_shares.h>
1112
#include <llmq/utils.h>
1213
#include <netaddress.h>
1314
#include <random.h>
15+
#include <streams.h>
1416

1517
#include <boost/test/unit_test.hpp>
1618

@@ -171,6 +173,86 @@ BOOST_AUTO_TEST_CASE(pending_sig_shares_session_removal_updates_count)
171173
BOOST_CHECK_EQUAL(node_state.pendingIncomingSigShares.Size(), 1U);
172174
}
173175

176+
BOOST_AUTO_TEST_CASE(batched_sig_shares_rejects_oversized_inner_vector)
177+
{
178+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
179+
stream << VARINT(uint32_t{1});
180+
WriteCompactSize(stream, MAX_MSGS_TOTAL_BATCHED_SIGS + 1);
181+
182+
CBatchedSigShares batched_sig_shares;
183+
BOOST_CHECK_THROW(stream >> batched_sig_shares, std::ios_base::failure);
184+
BOOST_CHECK(batched_sig_shares.sigShares.empty());
185+
}
186+
187+
BOOST_AUTO_TEST_CASE(batched_sig_shares_accepts_max_inner_vector)
188+
{
189+
CBatchedSigShares batched;
190+
batched.sessionId = 1;
191+
batched.sigShares.resize(MAX_MSGS_TOTAL_BATCHED_SIGS); // exactly the cap must be accepted
192+
193+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
194+
stream << batched;
195+
196+
CBatchedSigShares roundtripped;
197+
BOOST_CHECK_NO_THROW(stream >> roundtripped);
198+
BOOST_CHECK_EQUAL(roundtripped.sigShares.size(), MAX_MSGS_TOTAL_BATCHED_SIGS);
199+
}
200+
201+
static CBatchedSigShares MakeBatch(uint32_t session_id, size_t share_count)
202+
{
203+
CBatchedSigShares batch;
204+
batch.sessionId = session_id;
205+
batch.sigShares.resize(share_count); // default pairs are enough to exercise the count invariant
206+
return batch;
207+
}
208+
209+
// Exercise the production QBSIGSHARES decoder directly: the outer batch count is
210+
// bounded regardless of the inner contents. Every batch here is empty, so the
211+
// aggregate running-total guard never fires; only the outer-count guard can
212+
// reject the stream, which keeps this case a genuine test of that guard rather
213+
// than an end-of-stream artifact.
214+
BOOST_AUTO_TEST_CASE(qbsigshares_rejects_oversized_batch_count)
215+
{
216+
std::vector<CBatchedSigShares> msgs(MAX_MSGS_TOTAL_BATCHED_SIGS + 1); // count over cap, 0 inner shares each
217+
218+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
219+
stream << msgs;
220+
221+
BOOST_CHECK_THROW(UnserializeBatchedSigShares(stream), std::ios_base::failure);
222+
}
223+
224+
// The regression this targets: each batch is individually within the per-batch
225+
// LIMITED_VECTOR cap, but their running aggregate exceeds MAX_MSGS_TOTAL_BATCHED_SIGS,
226+
// so the decoder must abort mid-stream rather than accept the cross product.
227+
BOOST_AUTO_TEST_CASE(qbsigshares_rejects_oversized_aggregate_total)
228+
{
229+
std::vector<CBatchedSigShares> msgs;
230+
msgs.push_back(MakeBatch(1, 300));
231+
msgs.push_back(MakeBatch(2, 200)); // 300 + 200 = 500 > 400, though each batch is <= the cap
232+
233+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
234+
stream << msgs;
235+
236+
BOOST_CHECK_THROW(UnserializeBatchedSigShares(stream), std::ios_base::failure);
237+
}
238+
239+
// Batches whose aggregate is exactly at the cap must be accepted and decoded intact.
240+
BOOST_AUTO_TEST_CASE(qbsigshares_accepts_aggregate_at_cap)
241+
{
242+
std::vector<CBatchedSigShares> msgs;
243+
msgs.push_back(MakeBatch(1, 200));
244+
msgs.push_back(MakeBatch(2, MAX_MSGS_TOTAL_BATCHED_SIGS - 200)); // aggregate == cap
245+
246+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
247+
stream << msgs;
248+
249+
std::vector<CBatchedSigShares> decoded;
250+
BOOST_CHECK_NO_THROW(decoded = UnserializeBatchedSigShares(stream));
251+
BOOST_CHECK_EQUAL(decoded.size(), 2U);
252+
BOOST_CHECK_EQUAL(decoded[0].sigShares.size(), 200U);
253+
BOOST_CHECK_EQUAL(decoded[1].sigShares.size(), MAX_MSGS_TOTAL_BATCHED_SIGS - 200);
254+
}
255+
174256
BOOST_AUTO_TEST_CASE(deterministic_outbound_connection_test)
175257
{
176258
// Test deterministic behavior

0 commit comments

Comments
 (0)