From 2face298f6ab0c77aa303a50e567d17c729fda17 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Wed, 9 Sep 2026 12:05:30 +0700 Subject: [PATCH] fix(manifest): discard oversized legacy dumps without disconnecting --- src/test/overlay/compression_test.cpp | 55 ++++++++++++++++++++-- src/xrpld/overlay/Message.h | 9 ++-- src/xrpld/overlay/detail/ProtocolMessage.h | 18 +++++-- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp index 47a58b375e..c7a09a1ca8 100644 --- a/src/test/overlay/compression_test.cpp +++ b/src/test/overlay/compression_test.cpp @@ -386,7 +386,7 @@ public: void testManifestFrameLimit() { - testcase("manifest frame limits before payload allocation"); + testcase("manifest frame limits and legacy discard alignment"); struct Handler { bool @@ -442,12 +442,18 @@ public: (reject ? make_error_code(boost::system::errc::message_size) : boost::system::error_code{})); BEAST_EXPECT(handler.received == 0); + if (!reject) + BEAST_EXPECT(hint == wire); }; check(maxManifestMessageSize, maxManifestMessageSize, false, false); - check(maxManifestMessageSize + 1, 0, false, true); - check(1, maxManifestMessageSize + 1, true, true); - check(maxManifestMessageSize + 1, 1, true, true); + check(maxManifestMessageSize + 1, 0, false, enforceManifestFrameLimit); + check(1, maxManifestMessageSize + 1, true, enforceManifestFrameLimit); + check(maxManifestMessageSize + 1, 1, true, enforceManifestFrameLimit); check(maxManifestMessageSize, maxManifestMessageSize, true, false); + // Legacy tolerance never bypasses the generic hard ceiling, even + // when only the compressed header (no payload) has arrived. + check(1, maximiumMessageSize + 1, true, true); + check(1, maximiumMessageSize + 1, true, true, protocol::mtLEDGER_DATA); check( maxManifestMessageSize + 1, 0, @@ -476,6 +482,47 @@ public: hint); BEAST_EXPECT(!second.second && second.first == bytes.size()); BEAST_EXPECT(handler.received == 3); + + for (bool const compressed : {false, true}) + { + Serializer frame; + std::uint32_t const wire = + compressed ? 1u : maxManifestMessageSize + 1; + frame.add32(wire | (compressed ? 0x90000000u : 0u)); + frame.add16(protocol::mtMANIFESTS); + if (compressed) + frame.add32(std::uint32_t(maxManifestMessageSize + 1)); + // Deliberately invalid protobuf/LZ4. Discard must not decode it, + // nor consume the good frame coalesced behind it. + frame.addRaw(Blob(wire, 0xff)); + auto const discardedSize = frame.size(); + frame.addRaw(bytes.data(), bytes.size()); + int const received = handler.received; + auto discarded = invokeProtocolMessage( + boost::asio::buffer(frame.data(), frame.size()), handler, hint); + BEAST_EXPECT(handler.received == received); + if (enforceManifestFrameLimit) + { + BEAST_EXPECT(discarded.first == 0); + BEAST_EXPECT( + discarded.second == + make_error_code(boost::system::errc::message_size)); + } + else + { + BEAST_EXPECT(!discarded.second); + BEAST_EXPECT(discarded.first == discardedSize); + auto next = invokeProtocolMessage( + boost::asio::buffer( + reinterpret_cast(frame.data()) + + discarded.first, + frame.size() - discarded.first), + handler, + hint); + BEAST_EXPECT(!next.second && next.first == bytes.size()); + BEAST_EXPECT(handler.received == received + 1); + } + } } void diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h index 512f38d45c..dc6493b70c 100644 --- a/src/xrpld/overlay/Message.h +++ b/src/xrpld/overlay/Message.h @@ -37,10 +37,13 @@ namespace ripple { constexpr std::size_t maximiumMessageSize = megabytes(64); -// Manifests are small identity records, not bulk ledger data. Apply the frame -// limit before allocating/decompressing its payload, and the entry limits -// before queuing signature work. Outbound gossip uses the same limits. +// Manifests are small identity records, not bulk ledger data. Bound parsing +// and signature work; outbound gossip uses the same packet/entry limits. constexpr std::size_t maxManifestMessageSize = kilobytes(256); +// TODO: negotiate xahau-onchain-manifests and requireProtocolFeature at the +// agreed activation boundary before enabling disconnects. Until then, discard +// oversized legacy cache dumps without parsing; the generic 64 MiB cap stays. +constexpr bool enforceManifestFrameLimit = false; constexpr int maxManifestEntries = 256; constexpr std::size_t maxManifestSize = 1024; // Node-wide overload cutoff for bounded signature batches, not a per-peer diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h index 9732a08729..874b496490 100644 --- a/src/xrpld/overlay/detail/ProtocolMessage.h +++ b/src/xrpld/overlay/detail/ProtocolMessage.h @@ -361,11 +361,13 @@ invokeProtocolMessage( // whose size exceeds this may result in the connection being dropped. A // larger message size may be supported in the future or negotiated as // part of a protocol upgrade. - auto const sizeLimit = header->message_type == protocol::mtMANIFESTS - ? maxManifestMessageSize - : maximiumMessageSize; - if (header->payload_wire_size > sizeLimit || - header->uncompressed_size > sizeLimit) + bool const oversizedManifest = + header->message_type == protocol::mtMANIFESTS && + (header->payload_wire_size > maxManifestMessageSize || + header->uncompressed_size > maxManifestMessageSize); + if (header->payload_wire_size > maximiumMessageSize || + header->uncompressed_size > maximiumMessageSize || + (enforceManifestFrameLimit && oversizedManifest)) { result.second = make_error_code(boost::system::errc::message_size); return result; @@ -387,6 +389,12 @@ invokeProtocolMessage( return result; } + // Old peers send their cache in one packet. Consume exactly that frame, + // without decompression, protobuf parsing or dispatch, and keep the link. + // Buffering is still bounded by the generic ceiling checked above. + if (oversizedManifest) + return {header->total_wire_size, {}}; + bool success; switch (header->message_type)