From 3a9419e2b24be3e3a44ecc9dfa4aed957b294bbb Mon Sep 17 00:00:00 2001 From: raw391 Date: Thu, 4 Jun 2026 00:01:56 +0000 Subject: [PATCH] fix(tx_extra): enforce equal vector lengths in tx_extra_master_node_register tx_extra_master_node_register carries three parallel vectors (m_public_spend_keys, m_public_view_keys, m_portions) that are intended to be index-aligned. The serializer reads each as an independent vector with no cross-field invariant, so a malformed field with desynchronized lengths deserializes successfully and exposes consumers to out-of-bounds reads. Two consumers index by one vector size and read another: master_node_list.cpp:305-309 (block-include path) and core_rpc_server.cpp:711-720 (mempool-staged + RPC tx_extra=true). Enforces the equal-length invariant in the deserializer, matching the tx_extra_padding pattern at tx_extra.h:195-210. Adds defensive guards at both consumers so the call sites stay safe if the invariant drifts in a future change. --- src/cryptonote_basic/tx_extra.h | 7 +++++++ src/cryptonote_core/master_node_list.cpp | 3 +++ src/rpc/core_rpc_server.cpp | 10 +++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/cryptonote_basic/tx_extra.h b/src/cryptonote_basic/tx_extra.h index ac3089e2e5e..5bece68110e 100755 --- a/src/cryptonote_basic/tx_extra.h +++ b/src/cryptonote_basic/tx_extra.h @@ -330,6 +330,13 @@ namespace cryptonote FIELD(m_portions) FIELD(m_expiration_timestamp) FIELD(m_master_node_signature) + if (Archive::is_deserializer) + { + if (m_public_spend_keys.size() != m_public_view_keys.size()) + throw std::invalid_argument{"tx_extra_master_node_register: spend/view key count mismatch"}; + if (m_public_spend_keys.size() != m_portions.size()) + throw std::invalid_argument{"tx_extra_master_node_register: spend/portions count mismatch"}; + } END_SERIALIZE() }; diff --git a/src/cryptonote_core/master_node_list.cpp b/src/cryptonote_core/master_node_list.cpp index b6e90a1c8fd..9cb6ac559d7 100755 --- a/src/cryptonote_core/master_node_list.cpp +++ b/src/cryptonote_core/master_node_list.cpp @@ -301,6 +301,9 @@ namespace master_nodes if (!cryptonote::get_master_node_pubkey_from_tx_extra(tx.extra, master_node_key)) return false; + if (registration.m_public_spend_keys.size() != registration.m_public_view_keys.size() + || registration.m_public_spend_keys.size() != registration.m_portions.size()) + return false; contributor_args.addresses.clear(); contributor_args.addresses.reserve(registration.m_public_spend_keys.size()); for (size_t i = 0; i < registration.m_public_spend_keys.size(); i++) { diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 799aca942b2..5f275cbecd0 100755 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -710,9 +710,13 @@ namespace cryptonote::rpc { void operator()(const tx_extra_security_signature& x) { set("security_sig", tools::type_to_hex(x.m_security_signature)); } void operator()(const tx_extra_master_node_register& x) { json reservations{}; - for (size_t i = 0; i < x.m_portions.size(); i++) - reservations[get_account_address_as_str(nettype, false, {x.m_public_spend_keys[i], x.m_public_view_keys[i]})] - = microportion(x.m_portions[i]); + if (x.m_public_spend_keys.size() == x.m_public_view_keys.size() + && x.m_public_spend_keys.size() == x.m_portions.size()) + { + for (size_t i = 0; i < x.m_portions.size(); i++) + reservations[get_account_address_as_str(nettype, false, {x.m_public_spend_keys[i], x.m_public_view_keys[i]})] + = microportion(x.m_portions[i]); + } set("mn_registration", json{ {"fee", microportion(x.m_portions_for_operator)}, {"expiry", x.m_expiration_timestamp},