Add checks in cryptonote serialization routine
What changed, and why it matters
This commit adds size and bounds checks to Monero's serialization code, which converts blockchain data between raw bytes and usable structures. The changes prevent the code from trying to read more data than is actually available, and stop attackers from tricking the node into reserving huge amounts of memory based on a maliciously crafted message. In short, it hardens the network parsing layer against malformed data that could crash nodes or exhaust resources.
Treat as a security hardening fix and include in the next release. Nodes and wallets should upgrade to a build containing this commit to reduce exposure to malformed transaction/block parsing. If running a public RPC or P2P node, prioritize deployment because the parsing path is reachable from the network.
Security signals we found
Deserialization bounds check added to vector preparation macro
Integer-overflow-prone remaining_bytes check rewritten as division
Per-call-site minimum element sizes supplied for signatures, range proofs, bulletproofs, MLSAG/CLSAG structures, and ECDH info
Memory pre-allocation now gated on available input bytes
No explicit CVE or advisory referenced in commit materials
Evidence from the diff
The patch modifies the PREPARE_CUSTOM_VECTOR_SERIALIZATION macro and its helper prepare_custom_vector_serialization() so that, when loading (deserializing), it checks whether ar.remaining_bytes() / size < min_size before resizing the target vector. It also fixes a related integer-overflow-style sanity check in src/serialization/crypto.h by changing a multiplication-based comparison to a division-based one. Call sites in cryptonote_basic.h and rctTypes.h are updated to supply per-element minimum wire sizes (e.g., sizeof(key), sizeof(crypto::signature), sizeof(key)*193 for rangeSigs). This prevents undersized inputs from causing out-of-bounds reads and makes oversized-count fields fail early instead of allocating arbitrary amounts of memory.
Changed components
src/serialization/serialization.hsrc/serialization/crypto.hsrc/cryptonote_basic/cryptonote_basic.hsrc/ringct/rctTypes.hInspect captured patch +45 / −23
diff --git a/src/cryptonote_basic/cryptonote_basic.h b/src/cryptonote_basic/cryptonote_basic.h
index 4639da3..6e3c880 100644
--- a/src/cryptonote_basic/cryptonote_basic.h
+++ b/src/cryptonote_basic/cryptonote_basic.h
@@ -258,7 +258,7 @@ namespace cryptonote
ar.tag("signatures");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(vin.size(), signatures);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(vin.size(), signatures, 0);
bool signatures_not_expected = signatures.empty();
if (!signatures_not_expected && vin.size() != signatures.size())
return false;
@@ -274,7 +274,7 @@ namespace cryptonote
return false;
}
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(signature_size, signatures[i]);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(signature_size, signatures[i], sizeof(crypto::signature));
if (signature_size != signatures[i].size())
return false;
diff --git a/src/ringct/rctTypes.h b/src/ringct/rctTypes.h
index 829fcce..49e02a1 100644
--- a/src/ringct/rctTypes.h
+++ b/src/ringct/rctTypes.h
@@ -346,7 +346,7 @@ namespace rct {
{
ar.tag("pseudoOuts");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(inputs, pseudoOuts);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(inputs, pseudoOuts, sizeof(key));
if (pseudoOuts.size() != inputs)
return false;
for (size_t i = 0; i < inputs; ++i)
@@ -360,12 +360,18 @@ namespace rct {
ar.tag("ecdhInfo");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(outputs, ecdhInfo);
+
+ const bool compressed_ecdh =
+ (type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus);
+ const std::size_t min_size = compressed_ecdh ? sizeof(crypto::hash8) : sizeof(key) * 2;
+
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(outputs, ecdhInfo, min_size);
if (ecdhInfo.size() != outputs)
return false;
+
for (size_t i = 0; i < outputs; ++i)
{
- if (type == RCTTypeBulletproof2 || type == RCTTypeCLSAG || type == RCTTypeBulletproofPlus)
+ if (compressed_ecdh)
{
// Since RCTTypeBulletproof2 enote types, we don't serialize the blinding factor, and only serialize the
// first 8 bytes of ecdhInfo[i].amount
@@ -391,7 +397,7 @@ namespace rct {
ar.tag("outPk");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(outputs, outPk);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(outputs, outPk, sizeof(key));
if (outPk.size() != outputs)
return false;
for (size_t i = 0; i < outputs; ++i)
@@ -444,7 +450,7 @@ namespace rct {
ar.begin_array();
if (nbp > outputs)
return false;
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(nbp, bulletproofs_plus);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(nbp, bulletproofs_plus, sizeof(key) * 6);
for (size_t i = 0; i < nbp; ++i)
{
FIELDS(bulletproofs_plus[i])
@@ -466,7 +472,7 @@ namespace rct {
ar.begin_array();
if (nbp > outputs)
return false;
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(nbp, bulletproofs);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(nbp, bulletproofs, sizeof(key) * 9);
for (size_t i = 0; i < nbp; ++i)
{
FIELDS(bulletproofs[i])
@@ -481,7 +487,7 @@ namespace rct {
{
ar.tag("rangeSigs");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(outputs, rangeSigs);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(outputs, rangeSigs, sizeof(key) * 193);
if (rangeSigs.size() != outputs)
return false;
for (size_t i = 0; i < outputs; ++i)
@@ -497,7 +503,7 @@ namespace rct {
{
ar.tag("CLSAGs");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(inputs, CLSAGs);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(inputs, CLSAGs, sizeof(key) * 3);
if (CLSAGs.size() != inputs)
return false;
for (size_t i = 0; i < inputs; ++i)
@@ -508,7 +514,7 @@ namespace rct {
ar.begin_object();
ar.tag("s");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(mixin + 1, CLSAGs[i].s);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(mixin + 1, CLSAGs[i].s, sizeof(key));
if (CLSAGs[i].s.size() != mixin + 1)
return false;
for (size_t j = 0; j <= mixin; ++j)
@@ -540,9 +546,12 @@ namespace rct {
// we keep a byte for size of MGs, because we don't know whether this is
// a simple or full rct signature, and it's starting to annoy the hell out of me
size_t mg_elements = (type == RCTTypeSimple || type == RCTTypeBulletproof || type == RCTTypeBulletproof2) ? inputs : 1;
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(mg_elements, MGs);
+
+ // Each MGs has `cc` (`key`) AND at least 1 MGs[i].ss which has at least 1 `key`
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(mg_elements, MGs, sizeof(key) * 2);
if (MGs.size() != mg_elements)
return false;
+
for (size_t i = 0; i < mg_elements; ++i)
{
// we save the MGs contents directly, because we want it to save its
@@ -551,14 +560,17 @@ namespace rct {
ar.begin_object();
ar.tag("ss");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(mixin + 1, MGs[i].ss);
+
+ // each MGs[i].ss has at least one `key`
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(mixin + 1,MGs[i].ss, sizeof(key));
if (MGs[i].ss.size() != mixin + 1)
return false;
+
for (size_t j = 0; j < mixin + 1; ++j)
{
ar.begin_array();
size_t mg_ss2_elements = ((type == RCTTypeSimple || type == RCTTypeBulletproof || type == RCTTypeBulletproof2) ? 1 : inputs) + 1;
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(mg_ss2_elements, MGs[i].ss[j]);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(mg_ss2_elements, MGs[i].ss[j], sizeof(key));
if (MGs[i].ss[j].size() != mg_ss2_elements)
return false;
for (size_t k = 0; k < mg_ss2_elements; ++k)
@@ -588,7 +600,7 @@ namespace rct {
{
ar.tag("pseudoOuts");
ar.begin_array();
- PREPARE_CUSTOM_VECTOR_SERIALIZATION(inputs, pseudoOuts);
+ PREPARE_CUSTOM_VECTOR_SERIALIZATION(inputs, pseudoOuts, sizeof(key));
if (pseudoOuts.size() != inputs)
return false;
for (size_t i = 0; i < inputs; ++i)
diff --git a/src/serialization/crypto.h b/src/serialization/crypto.h
index 57f0549..4a26920 100644
--- a/src/serialization/crypto.h
+++ b/src/serialization/crypto.h
@@ -46,7 +46,7 @@ bool do_serialize(Archive<false> &ar, std::vector<crypto::signature> &v)
v.clear();
// very basic sanity check
- if (ar.remaining_bytes() < cnt*sizeof(crypto::signature)) {
+ if (ar.remaining_bytes() / sizeof(crypto::signature) < cnt) {
ar.set_fail();
return false;
}
diff --git a/src/serialization/serialization.h b/src/serialization/serialization.h
index 4287bb2..70cc16b 100644
--- a/src/serialization/serialization.h
+++ b/src/serialization/serialization.h
@@ -195,10 +195,15 @@ inline auto do_serialize(Archive &ar, T &v, Args&&... args)
template <bool W, template <bool> class Archive> \
bool do_serialize_object(Archive<W> &ar, stype &v) {
-/*! \macro PREPARE_CUSTOM_VECTOR_SERIALIZATION
+/*! \macro RESERVE_VECTOR_SERIALIZATION. `min_wire` must be the minimum bytes
+ * each element takes on the wire.
*/
-#define PREPARE_CUSTOM_VECTOR_SERIALIZATION(size, vec) \
- ::serialization::detail::prepare_custom_vector_serialization(size, vec, typename Archive<W>::is_saving())
+#define PREPARE_CUSTOM_VECTOR_SERIALIZATION(size, vec, min_wire) \
+ do \
+ { \
+ if (!::serialization::detail::prepare_custom_vector_serialization(size, vec, min_wire, ar)) \
+ return false; \
+ } while (0);
/*! \macro END_SERIALIZE
* \brief self-explanatory
@@ -295,6 +300,7 @@ inline auto do_serialize(Archive &ar, T &v, Args&&... args)
namespace serialization {
+
/*! \namespace detail
*
* \brief declaration and default definition for the functions used the API
@@ -306,15 +312,19 @@ namespace serialization {
*
* prepares the vector /vec for serialization
*/
- template <typename T>
- void prepare_custom_vector_serialization(size_t size, std::vector<T>& vec, const boost::mpl::bool_<true>& /*is_saving*/)
+ template <typename T, template<bool> class A>
+ constexpr bool prepare_custom_vector_serialization(size_t, const std::vector<T>& vec, size_t, const A<true>&) noexcept
{
+ return true;
}
- template <typename T>
- void prepare_custom_vector_serialization(size_t size, std::vector<T>& vec, const boost::mpl::bool_<false>& /*is_saving*/)
+ template <typename T, template<bool> class A>
+ bool prepare_custom_vector_serialization(const size_t size, std::vector<T>& vec, const size_t min_size, const A<false>& ar)
{
+ if (size && ar.remaining_bytes() / size < min_size)
+ return false;
vec.resize(size);
+ return true;
}
/*! \fn do_check_stream_state
Why this scored 65/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.