From 44a536d58178600f0cf0e91f9a51bd1ae184ead0 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sat, 27 Jun 2026 11:34:28 -0500 Subject: [PATCH 1/2] fix: reject semantically invalid deserialized ExtNetInfo states `ExtNetInfo::Validate()` can return `BadInput`, `Duplicate`, and `MaxLimit` for states reachable via deserialization that bypass the in-memory builder's per-entry checks. `CheckService` previously asserted on those statuses; map them to graceful `TX_BAD_SPECIAL` rejections with stable reason strings instead. Also enforce the per-purpose `MAX_ENTRIES_EXTNETINFO` cap inside `ExtNetInfo::Validate()` so an oversized deserialized list is rejected rather than silently accepted. Adds unit coverage for the three deserialization paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/evo/netinfo.cpp | 5 +++ src/evo/specialtxman.cpp | 6 ++-- src/test/evo_netinfo_tests.cpp | 63 ++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index a3f53693ccaf..48eae4e11232 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -601,6 +601,11 @@ NetInfoStatus ExtNetInfo::Validate() const // Purpose if present in map must have at least one entry return NetInfoStatus::Malformed; } + if (entries.size() > MAX_ENTRIES_EXTNETINFO) { + // Per-purpose entry count is bounded at insertion; a deserialized object + // may exceed it and must be rejected here + return NetInfoStatus::MaxLimit; + } if (HasAddrDuplicates(entries)) { return NetInfoStatus::Duplicate; } diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index bc68f76664f1..72ba23ca3206 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -883,11 +883,13 @@ static bool CheckService(const ProTx& proTx, TxValidationState& state) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); case NetInfoStatus::Success: return true; - // Shouldn't be possible during self-checks + // Reachable through a serialized netInfo that bypasses the in-memory builder's checks case NetInfoStatus::BadInput: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); case NetInfoStatus::Duplicate: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); case NetInfoStatus::MaxLimit: - assert(false); + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-maxlimit"); } // no default case, so the compiler can warn about missing cases assert(false); } diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 9fc3e3d8be91..4455456ac9a2 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -497,4 +497,67 @@ BOOST_AUTO_TEST_CASE(domainport_rules) } } +BOOST_FIXTURE_TEST_CASE(extnetinfo_validate_deser, RegTestingSetup) +{ + // The in-memory builder (AddEntry/ProcessCandidate) refuses duplicates, domains + // on non-HTTPS purposes, and lists exceeding MAX_ENTRIES_EXTNETINFO. None of those + // checks run on deserialization, so Validate() must catch them itself. + const uint16_t port{9998}; + + auto write_header = [](CDataStream& ds, size_t n_purposes) { + ds << uint8_t{1}; // current version + WriteCompactSize(ds, n_purposes); + }; + + { + // Two identical entries under the same purpose + CDataStream ds(SER_DISK, CLIENT_VERSION); + write_header(ds, 1); + ds << NetInfoPurpose::CORE_P2P; + WriteCompactSize(ds, 2); + const NetInfoEntry entry{LookupNumeric("1.1.1.1", port)}; + BOOST_CHECK(entry.IsTriviallyValid()); + ds << entry << entry; + + ExtNetInfo netInfo; + ds >> netInfo; + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Duplicate); + } + + { + // List exceeds MAX_ENTRIES_EXTNETINFO per purpose + CDataStream ds(SER_DISK, CLIENT_VERSION); + write_header(ds, 1); + ds << NetInfoPurpose::CORE_P2P; + WriteCompactSize(ds, MAX_ENTRIES_EXTNETINFO + 1); + for (size_t i = 1; i <= MAX_ENTRIES_EXTNETINFO + 1; ++i) { + const NetInfoEntry entry{LookupNumeric(strprintf("1.1.1.%d", i), port)}; + BOOST_CHECK(entry.IsTriviallyValid()); + ds << entry; + } + + ExtNetInfo netInfo; + ds >> netInfo; + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::MaxLimit); + } + + { + // Domain entry under a non-PLATFORM_HTTPS purpose + DomainPort domain; + BOOST_CHECK_EQUAL(domain.Set("example.com", 443), DomainPort::Status::Success); + const NetInfoEntry entry{domain}; + BOOST_CHECK(entry.IsTriviallyValid()); + + CDataStream ds(SER_DISK, CLIENT_VERSION); + write_header(ds, 1); + ds << NetInfoPurpose::CORE_P2P; + WriteCompactSize(ds, 1); + ds << entry; + + ExtNetInfo netInfo; + ds >> netInfo; + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::BadInput); + } +} + BOOST_AUTO_TEST_SUITE_END() From 334198886a0f32399d48bdb576f831869aa5e32d Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sat, 27 Jun 2026 11:59:01 -0500 Subject: [PATCH 2/2] test: cover invalid deserialized ExtNetInfo tx validation --- src/test/evo_deterministicmns_tests.cpp | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 3af64daa6347..116686892aa4 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -14,12 +15,14 @@ #include #include #include +#include #include #include #include