Skip to content

fix(evo): bound DomainPort string deserialization and stop swallowing deser failures - #7487

Draft
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:sec/v048
Draft

fix(evo): bound DomainPort string deserialization and stop swallowing deser failures#7487
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:sec/v048

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

DomainPort deserializes an unbounded std::string, and NetInfoEntry::Unserialize()
swallows the resulting ios_base::failure. Together these give a large allocate-and-zero
amplification factor from a small ProTx payload.

Mechanism:

  • DomainPort's serializer reads m_addr as a plain std::string, so an attacker-chosen
    CompactSize drives resize() before any length check. ValidateDomain() enforces the
    253-byte RFC 1035 ceiling, but only after the allocation has already happened.
  • When the body is short or oversized and deserialization throws, NetInfoEntry::Unserialize()
    catches ios_base::failure and calls Clear(). The stream position is left unmoved (or
    advanced only by the CompactSize), so the enclosing vector loop keeps going and repeats
    the allocation.

The result is that a few attacker-controlled bytes turn into repeated multi-megabyte
allocate-and-zero cycles while the payload is parsed.

What was done?

  • DomainPort's serializer now uses LIMITED_STRING(obj.m_addr, DOMAIN_MAX_LEN), so an
    oversized CompactSize claim is rejected before resize() rather than after.
    LimitedStringFormatter checks the length and throws without allocating.
  • DOMAIN_MAX_LEN moved from netinfo.cpp to netinfo.h (and uint8_tsize_t for the
    template parameter) so the serialization layer and ValidateDomain() share one constant
    and cannot drift apart.
  • The Domain branch of NetInfoEntry::Unserialize() no longer swallows
    ios_base::failure. It propagates, so a malformed domain entry rejects the whole ProTx
    payload instead of leaving the stream desynchronised for the rest of the loop.

The Service branch deliberately still catches. A CService is fixed-size, so a malformed
one cannot desynchronise the stream or amplify allocation — there is nothing to defend
against there, and changing it would alter behaviour for no benefit.

Note on deployment status

This tightens a serialization rule, so the natural question is whether it needs gating.
It does not: DEPLOYMENT_V24 is NEVER_ACTIVE on both mainnet (chainparams.cpp:213) and
testnet (chainparams.cpp:415), so ExtNetInfo — and therefore DomainPort — cannot
appear in any mainnet or testnet chain data. There is nothing already mined for the stricter
rule to retroactively invalidate.

How Has This Been Tested?

Unit tests in src/test/evo_netinfo_tests.cpp, added in the commit preceding the fix:

  • domainport_deser_rejects_oversized_string
  • netinfoentry_domain_deser_rejects_oversized_string
  • extnetinfo_domain_amplification_rejected

Without the fix all three fail with exception std::ios_base::failure expected but not raised. With the fix the full evo_netinfo_tests suite passes (14 cases), confirming
well-formed domains up to the 253-byte ceiling still round-trip.

Built and run on macOS/arm64 against current develop.

Breaking Changes

None reachable on mainnet or testnet, per the deployment note above. On devnet/regtest,
a ProTx carrying a domain entry longer than 253 bytes is now rejected at deserialization
instead of being cleared and then rejected by netinfo validation — same outcome, earlier
and cheaper.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • 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

Add regression coverage: DomainPort accepts attacker-chosen string lengths up to MAX_SIZE, and NetInfoEntry swallows ios_base::failure so ExtNetInfo vectors can force repeated large allocate-and-zero work from a small payload. These cases expect rejection (throw) and fail on the unfixed tree.
DomainPort serialized m_addr as a bare std::string, so CompactSize claims up to MAX_SIZE (32 MiB) allocated before any body was read. NetInfoEntry::Unserialize caught ios_base::failure and Clear()ed, letting ExtNetInfo vector loops retry from an unadvanced stream position and amplify a ~10 KB ProTx payload into tens of GB of allocate-and-zero under cs_main.

Cap the domain with LIMITED_STRING(..., DOMAIN_MAX_LEN=253) at the serialization layer (matching ValidateDomain and RFC 1035 FQDN limits; hard-fork-safe because valid domains were already rejected above 253) and rethrow DomainPort deser failures so the whole ProTx payload fails instead of looping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant