fix(manifest): discard oversized legacy dumps without disconnecting

This commit is contained in:
Nicholas Dudfield
2026-09-09 12:05:30 +07:00
parent c9d9be84cc
commit 2face298f6
3 changed files with 70 additions and 12 deletions

View File

@@ -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<std::uint8_t const*>(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

View File

@@ -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

View File

@@ -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)