fix: Handle malformed ledger replay responses

This commit is contained in:
Bart
2026-07-16 16:29:31 -04:00
committed by Ayaz Salikhov
parent 981c256933
commit 4a9ee54c88
6 changed files with 219 additions and 68 deletions

View File

@@ -13,6 +13,7 @@ extern Charge const kFeeRequestNoReply; // A request that we cannot satisfy.
extern Charge const kFeeInvalidSignature; // An object whose signature we had to check that failed.
extern Charge const kFeeUselessData; // Data we have no use for.
extern Charge const kFeeInvalidData; // Data we have to verify before rejecting.
extern Charge const kFeeMalformedData; // Data that no honest peer would send.
// RPC loads
extern Charge const kFeeMalformedRpc; // An RPC request that we can immediately tell is invalid.

View File

@@ -9,6 +9,7 @@ Charge const kFeeRequestNoReply(10, "unsatisfiable request");
Charge const kFeeInvalidSignature(2000, "invalid signature");
Charge const kFeeUselessData(150, "useless data");
Charge const kFeeInvalidData(400, "invalid data");
Charge const kFeeMalformedData(2000, "malformed data");
Charge const kFeeMalformedRpc(100, "malformed RPC");
Charge const kFeeReferenceRpc(20, "reference RPC");

View File

@@ -58,6 +58,8 @@
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <format>
#include <functional>
#include <map>
#include <memory>
@@ -958,7 +960,8 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
auto reply = std::make_shared<protocol::TMProofPathResponse>(
server.msgHandler.processProofPathRequest(request));
BEAST_EXPECT(reply->has_error());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::BadData);
}
{
// request, wrong hash
@@ -982,7 +985,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
auto reply = std::make_shared<protocol::TMProofPathResponse>(
server.msgHandler.processProofPathRequest(request));
BEAST_EXPECT(!reply->has_error());
BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply));
BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::Ok);
{
// bad reply: invalid hash/key sizes
@@ -990,37 +993,49 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
// reply with undersized ledgerhash (31 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with oversized ledgerhash (33 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with empty ledgerhash
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_ledgerhash(std::string());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with undersized key (31 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with oversized key (33 bytes)
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with empty key
auto bad = std::make_shared<protocol::TMProofPathResponse>(*reply);
bad->set_key(std::string());
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(bad) ==
ReplayMsgStatus::Malformed);
}
}
@@ -1030,13 +1045,18 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
std::string r(reply->ledgerheader());
r.back()--;
reply->set_ledgerheader(r);
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(reply) ==
ReplayMsgStatus::Malformed);
r.back()++;
reply->set_ledgerheader(r);
BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::Ok);
// bad proof path
reply->mutable_path()->RemoveLast();
BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply));
BEAST_EXPECT(
server.msgHandler.processProofPathResponse(reply) ==
ReplayMsgStatus::Malformed);
}
}
}
@@ -1054,14 +1074,16 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
auto reply = std::make_shared<protocol::TMReplayDeltaResponse>(
server.msgHandler.processReplayDeltaRequest(request));
BEAST_EXPECT(reply->has_error());
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::BadData);
// request, wrong hash
uint256 hash(1234567);
request->set_ledgerhash(hash.data(), hash.size());
reply = std::make_shared<protocol::TMReplayDeltaResponse>(
server.msgHandler.processReplayDeltaRequest(request));
BEAST_EXPECT(reply->has_error());
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::BadData);
}
{
@@ -1071,7 +1093,8 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
auto reply = std::make_shared<protocol::TMReplayDeltaResponse>(
server.msgHandler.processReplayDeltaRequest(request));
BEAST_EXPECT(!reply->has_error());
BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::Ok);
{
// bad reply: invalid hash sizes
@@ -1079,19 +1102,25 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
// reply with undersized ledgerhash (31 bytes)
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string(31, '\x01'));
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with oversized ledgerhash (33 bytes)
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string(33, '\x01'));
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(bad) ==
ReplayMsgStatus::Malformed);
}
{
// reply with empty ledgerhash
auto bad = std::make_shared<protocol::TMReplayDeltaResponse>(*reply);
bad->set_ledgerhash(std::string());
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(bad) ==
ReplayMsgStatus::Malformed);
}
}
@@ -1101,17 +1130,77 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
std::string r(reply->ledgerheader());
r.back()--;
reply->set_ledgerheader(r);
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(reply) ==
ReplayMsgStatus::Malformed);
r.back()++;
reply->set_ledgerheader(r);
BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::Ok);
// bad txns
reply->mutable_transaction()->RemoveLast();
BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply));
BEAST_EXPECT(
server.msgHandler.processReplayDeltaResponse(reply) ==
ReplayMsgStatus::Malformed);
}
}
}
void
testTruncatedHeader()
{
testcase("TruncatedLedgerHeader");
LedgerServer server(*this, {.initLedgers = 1});
auto const l = server.ledgerMaster.getClosedLedger();
auto runNoThrow = [this](auto fn, char const* what) {
try
{
BEAST_EXPECT(fn() == ReplayMsgStatus::Malformed);
}
catch (std::exception const& e)
{
fail(
std::format("processor threw on truncated header ({}): {}", what, e.what()),
__FILE__,
__LINE__);
}
catch (...)
{
fail(
std::format("processor threw unknown exception ({}) on truncated header", what),
__FILE__,
__LINE__);
}
};
{
auto request = std::make_shared<protocol::TMReplayDeltaRequest>();
request->set_ledgerhash(l->header().hash.data(), l->header().hash.size());
auto reply = std::make_shared<protocol::TMReplayDeltaResponse>(
server.msgHandler.processReplayDeltaRequest(request));
BEAST_EXPECT(!reply->has_error());
reply->set_ledgerheader(std::string(1, '\x00'));
runNoThrow(
[&] { return server.msgHandler.processReplayDeltaResponse(reply); }, "ReplayDelta");
}
{
auto request = std::make_shared<protocol::TMProofPathRequest>();
request->set_ledgerhash(l->header().hash.data(), l->header().hash.size());
request->set_type(protocol::TMLedgerMapType::lmACCOUNT_STATE);
request->set_key(keylet::skip().key.data(), keylet::skip().key.size());
auto reply = std::make_shared<protocol::TMProofPathResponse>(
server.msgHandler.processProofPathRequest(request));
BEAST_EXPECT(!reply->has_error());
reply->set_ledgerheader(std::string(1, '\x00'));
runNoThrow(
[&] { return server.msgHandler.processProofPathResponse(reply); }, "ProofPath");
}
}
void
testTaskParameter()
{
@@ -1514,6 +1603,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
{
testProofPath();
testReplayDelta();
testTruncatedHeader();
testTaskParameter();
testConfig();
testHandshake();

View File

@@ -101,42 +101,54 @@ LedgerReplayMsgHandler::processProofPathRequest(
return reply;
}
bool
ReplayMsgStatus
LedgerReplayMsgHandler::processProofPathResponse(
std::shared_ptr<protocol::TMProofPathResponse> const& msg)
{
protocol::TMProofPathResponse const& reply = *msg;
if (reply.has_error() || !reply.has_key() || !reply.has_ledgerhash() || !reply.has_type() ||
if (reply.has_error())
{
JLOG(journal_.debug()) << "ProofPathResponse: peer reported error";
return ReplayMsgStatus::BadData;
}
if (!reply.has_key() || !reply.has_ledgerhash() || !reply.has_type() ||
!reply.has_ledgerheader() || reply.path_size() == 0 ||
reply.ledgerhash().size() != uint256::size() || reply.key().size() != uint256::size())
{
JLOG(journal_.debug()) << "Bad message: Error reply";
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (missing or wrong-size fields)";
return ReplayMsgStatus::Malformed;
}
if (reply.type() != protocol::lmACCOUNT_STATE)
{
JLOG(journal_.debug()) << "Bad message: we only support the state ShaMap for now";
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (unsupported map type)";
return ReplayMsgStatus::Malformed;
}
// deserialize the header
auto info = deserializeHeader({reply.ledgerheader().data(), reply.ledgerheader().size()});
LedgerHeader info;
try
{
info = deserializeHeader(makeSlice(reply.ledgerheader()));
}
catch (std::exception const& e)
{
JLOG(journal_.debug()) << "ProofPathResponse: malformed header (" << e.what() << ")";
return ReplayMsgStatus::Malformed;
}
uint256 const replyHash = uint256::fromRaw(reply.ledgerhash());
if (calculateLedgerHash(info) != replyHash)
{
JLOG(journal_.debug()) << "Bad message: Hash mismatch";
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (hash mismatch)";
return ReplayMsgStatus::Malformed;
}
info.hash = replyHash;
uint256 const key = uint256::fromRaw(reply.key());
if (key != keylet::skip().key)
{
JLOG(journal_.debug()) << "Bad message: we only support the short skip list for now. "
"Key in reply "
<< key;
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (unexpected key " << key << ")";
return ReplayMsgStatus::Malformed;
}
// verify the skip list
@@ -149,26 +161,35 @@ LedgerReplayMsgHandler::processProofPathResponse(
if (!SHAMap::verifyProofPath(info.accountHash, key, path))
{
JLOG(journal_.debug()) << "Bad message: Proof path verify failed";
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (proof path verify failed)";
return ReplayMsgStatus::Malformed;
}
// deserialize the SHAMapItem
auto node = SHAMapTreeNode::makeFromWire(makeSlice(path.front()));
SHAMapTreeNodePtr node;
try
{
node = SHAMapTreeNode::makeFromWire(makeSlice(path.front()));
}
catch (std::exception const& e)
{
JLOG(journal_.debug()) << "ProofPathResponse: malformed SHAMap node (" << e.what() << ")";
return ReplayMsgStatus::Malformed;
}
if (!node || !node->isLeaf())
{
JLOG(journal_.debug()) << "Bad message: Cannot deserialize";
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (not a leaf node)";
return ReplayMsgStatus::Malformed;
}
if (auto item = safeDowncast<SHAMapLeafNode*>(node.get())->peekItem())
{
replayer_.gotSkipList(info, item);
return true;
return ReplayMsgStatus::Ok;
}
JLOG(journal_.debug()) << "Bad message: Cannot get ShaMapItem";
return false;
JLOG(journal_.debug()) << "ProofPathResponse: malformed (no SHAMapItem)";
return ReplayMsgStatus::Malformed;
}
protocol::TMReplayDeltaResponse
@@ -210,24 +231,38 @@ LedgerReplayMsgHandler::processReplayDeltaRequest(
return reply;
}
bool
ReplayMsgStatus
LedgerReplayMsgHandler::processReplayDeltaResponse(
std::shared_ptr<protocol::TMReplayDeltaResponse> const& msg)
{
protocol::TMReplayDeltaResponse const& reply = *msg;
if (reply.has_error() || !reply.has_ledgerheader() || !reply.has_ledgerhash() ||
if (reply.has_error())
{
JLOG(journal_.debug()) << "ReplayDeltaResponse: peer reported error";
return ReplayMsgStatus::BadData;
}
if (!reply.has_ledgerheader() || !reply.has_ledgerhash() ||
reply.ledgerhash().size() != uint256::size())
{
JLOG(journal_.debug()) << "Bad message: Error reply";
return false;
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (missing or wrong-size fields)";
return ReplayMsgStatus::Malformed;
}
auto info = deserializeHeader({reply.ledgerheader().data(), reply.ledgerheader().size()});
LedgerHeader info;
try
{
info = deserializeHeader(makeSlice(reply.ledgerheader()));
}
catch (std::exception const& e)
{
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed header (" << e.what() << ")";
return ReplayMsgStatus::Malformed;
}
uint256 const replyHash = uint256::fromRaw(reply.ledgerhash());
if (calculateLedgerHash(info) != replyHash)
{
JLOG(journal_.debug()) << "Bad message: Hash mismatch";
return false;
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (hash mismatch)";
return ReplayMsgStatus::Malformed;
}
info.hash = replyHash;
@@ -252,8 +287,8 @@ LedgerReplayMsgHandler::processReplayDeltaResponse(
auto tx = std::make_shared<STTx const>(txSit);
if (!tx)
{
JLOG(journal_.debug()) << "Bad message: Cannot deserialize";
return false;
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (tx deserialize)";
return ReplayMsgStatus::Malformed;
}
auto tid = tx->getTransactionID();
STObject meta(metaSit, sfMetadata);
@@ -262,25 +297,26 @@ LedgerReplayMsgHandler::processReplayDeltaResponse(
if (!txMap.addGiveItem(
SHAMapNodeType::TnTransactionMd, makeShamapitem(tid, shaMapItemData.slice())))
{
JLOG(journal_.debug()) << "Bad message: Cannot deserialize";
return false;
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (tx map add)";
return ReplayMsgStatus::Malformed;
}
}
}
catch (std::exception const&)
catch (std::exception const& e)
{
JLOG(journal_.debug()) << "Bad message: Cannot deserialize";
return false;
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed transactions (" << e.what()
<< ")";
return ReplayMsgStatus::Malformed;
}
if (txMap.getHash().asUInt256() != info.txHash)
{
JLOG(journal_.debug()) << "Bad message: Transactions verify failed";
return false;
JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (transactions verify failed)";
return ReplayMsgStatus::Malformed;
}
replayer_.gotReplayDelta(info, std::move(orderedTxns));
return true;
return ReplayMsgStatus::Ok;
}
} // namespace xrpl

View File

@@ -10,6 +10,15 @@ namespace xrpl {
class Application;
class LedgerReplayer;
/**
* Outcome of processing an incoming ledger-replay response.
*/
enum class ReplayMsgStatus {
Ok, ///< Accepted.
BadData, ///< Peer reported has_error() (legitimate "cannot fulfill" signal).
Malformed, ///< Protocol-level violation; no honest peer would produce this.
};
class LedgerReplayMsgHandler final
{
public:
@@ -19,31 +28,31 @@ public:
/**
* Process TMProofPathRequest and return TMProofPathResponse
* @note check has_error() and error() of the response for error
* @return TMProofPathResponse with the proof path, or with error() set if
* the request cannot be fulfilled
*/
protocol::TMProofPathResponse
processProofPathRequest(std::shared_ptr<protocol::TMProofPathRequest> const& msg);
/**
* Process TMProofPathResponse
* @return false if the response message has bad format or bad data;
* true otherwise
*/
bool
ReplayMsgStatus
processProofPathResponse(std::shared_ptr<protocol::TMProofPathResponse> const& msg);
/**
* Process TMReplayDeltaRequest and return TMReplayDeltaResponse
* @note check has_error() and error() of the response for error
* @return TMReplayDeltaResponse with the ledger delta, or with error() set
* if the request cannot be fulfilled
*/
protocol::TMReplayDeltaResponse
processReplayDeltaRequest(std::shared_ptr<protocol::TMReplayDeltaRequest> const& msg);
/**
* Process TMReplayDeltaResponse
* @return false if the response message has bad format or bad data;
* true otherwise
*/
bool
ReplayMsgStatus
processReplayDeltaResponse(std::shared_ptr<protocol::TMReplayDeltaResponse> const& msg);
private:

View File

@@ -1565,9 +1565,16 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProofPathResponse> const& m)
return;
}
if (!ledgerReplayMsgHandler_.processProofPathResponse(m))
switch (ledgerReplayMsgHandler_.processProofPathResponse(m))
{
fee_.update(Resource::kFeeInvalidData, "proof_path_response");
case ReplayMsgStatus::Ok:
break;
case ReplayMsgStatus::BadData:
fee_.update(Resource::kFeeInvalidData, "proof_path_response");
break;
case ReplayMsgStatus::Malformed:
fee_.update(Resource::kFeeMalformedData, "proof_path_response malformed");
break;
}
}
@@ -1615,9 +1622,16 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMReplayDeltaResponse> const& m)
return;
}
if (!ledgerReplayMsgHandler_.processReplayDeltaResponse(m))
switch (ledgerReplayMsgHandler_.processReplayDeltaResponse(m))
{
fee_.update(Resource::kFeeInvalidData, "replay_delta_response");
case ReplayMsgStatus::Ok:
break;
case ReplayMsgStatus::BadData:
fee_.update(Resource::kFeeInvalidData, "replay_delta_response");
break;
case ReplayMsgStatus::Malformed:
fee_.update(Resource::kFeeMalformedData, "replay_delta_response malformed");
break;
}
}