diff --git a/src/llmq/net_quorum.cpp b/src/llmq/net_quorum.cpp index a5eafb8be4fd..97015bad6252 100644 --- a/src/llmq/net_quorum.cpp +++ b/src/llmq/net_quorum.cpp @@ -203,8 +203,16 @@ void NetQuorum::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataS // Check if request has QUORUM_VERIFICATION_VECTOR data if (request.GetDataMask() & CQuorumDataRequest::QUORUM_VERIFICATION_VECTOR) { + // Reject the wire count before decoding any BLS G1 element so a bogus + // count cannot spend arbitrary CPU on doomed decodes. A mismatch — over + // or under — is a protocol violation worth a full ban. + const size_t expected_vvec_size{static_cast(pQuorum->params.threshold)}; std::vector verificationVector; - vRecv >> verificationVector; + if (!UnserializeVectorWithMaxSize(vRecv, verificationVector, expected_vvec_size) || + verificationVector.size() != expected_vvec_size) { + m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100, "invalid quorum verification vector size"); + return; + } if (pQuorum->SetVerificationVector(verificationVector)) { m_qman.QueueQuorumForWarming(pQuorum); @@ -279,7 +287,12 @@ bool NetQuorum::ProcessContribQDATA(CNode& pfrom, CDataStream& vRecv, } std::vector> vecEncrypted; - vRecv >> vecEncrypted; + const size_t expected_contributions{static_cast(std::ranges::count(quorum.qc->validMembers, true))}; + if (!UnserializeVectorWithMaxSize(vRecv, vecEncrypted, expected_contributions) || + vecEncrypted.size() != expected_contributions) { + m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100, "invalid encrypted contribution vector size"); + return false; + } std::vector vecSecretKeys; vecSecretKeys.resize(vecEncrypted.size()); diff --git a/test/functional/p2p_quorum_data.py b/test/functional/p2p_quorum_data.py index 386f741498b2..ec649154ac2a 100755 --- a/test/functional/p2p_quorum_data.py +++ b/test/functional/p2p_quorum_data.py @@ -3,9 +3,20 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +import copy +import struct import time -from test_framework.messages import CSigSharesInv, msg_qgetdata, msg_qsigsinv, msg_qwatch +from test_framework.messages import ( + CSigSharesInv, + msg_qdata, + msg_qgetdata, + msg_qsigsinv, + msg_qwatch, + ser_compact_size, + ser_uint256, + ser_vector, +) from test_framework.p2p import ( p2p_lock, P2PInterface, @@ -54,6 +65,46 @@ oversized_inv_count = 65536 # ~512 KiB wire -> ~256 GiB declared allocation +class _QDataWithRawPayload(msg_qdata): + """A msg_qdata that ships arbitrary bytes as its wire body. + + The p2p framework calls serialize() to build the message payload; overriding + it lets us craft a QDATA whose declared vvec/contribs CompactSize does not + match the wire body — the exact shape a malicious peer would send. + """ + __slots__ = ("_raw_payload",) + + def __init__(self, raw_payload): + super().__init__() + self._raw_payload = raw_payload + + def serialize(self): + return self._raw_payload + + +def craft_qdata_with_bad_section(qdata_valid, *, bad_vvec=None, bad_contribs=None): + """Build a QDATA payload from qdata_valid with a single malformed vector section. + + Both `bad_vvec` and `bad_contribs`, if provided, are raw wire bytes that + fully replace that vector's section (compact-size prefix + any element + bytes the attacker wants to include, typically none). Sections not + overridden are copied verbatim from qdata_valid, so ProcessMessage reaches + the intended check point (a valid vvec is required to reach the + contributions check). + """ + payload = b"" + payload += struct.pack("