Memory transfer limit (#7000)

Count bytes copied across the boundaries (Wasm VM <-> Hostfunctions) and return error if limit reached (1 mb default)
This commit is contained in:
Olek
2026-06-17 12:45:01 -04:00
committed by GitHub
parent ca2d999618
commit 9767d86de4
6 changed files with 255 additions and 2 deletions

View File

@@ -166,8 +166,11 @@ class VirtualRuntime : public WasmRuntimeWrapper
{
Bytes buffer_;
std::int64_t gas_ = 1'000'000;
std::int64_t transferLimit_ = kWasmTransferLimit;
public:
static constexpr std::int64_t transferDiff = 1024;
VirtualRuntime() : buffer_(1024 * 1024)
{
}
@@ -203,6 +206,37 @@ public:
return gas_;
}
std::int64_t
getTransferLimit() override
{
transferLimit_ -= transferDiff;
return transferLimit_;
}
[[nodiscard]] std::int64_t
getTestTransferLimit() const
{
return transferLimit_;
}
std::int64_t
setTransferLimit(std::int64_t x) override
{
if (x == -2)
return -1;
if (x < 0)
{
transferLimit_ = std::numeric_limits<decltype(transferLimit_)>::max();
}
else
{
transferLimit_ = x;
}
return transferLimit_;
}
void
checkIdx(WasmValVec const& params, size_t i) const
{
@@ -6061,6 +6095,110 @@ struct HostFuncImpl_test : public beast::unit_test::Suite
BEAST_EXPECT(ex);
}
void
testTransferLimit()
{
testcase("transferLimit");
using namespace test::jtx;
Env env{*this};
OpenView ov{*env.current()};
ApplyContext ac = createApplyContext(env, ov);
auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master));
VirtualRuntime vrt;
WasmHostFunctionsImpl hfs(ac, dummyEscrow);
auto import = xrpl::createWasmImport(hfs);
hfs.setRT(vrt);
// Test 1: Test setData() - copying FROM host TO wasm
// Multiple calls to getLedgerSqn() which uses setData() to write result to WASM memory
vrt.setTransferLimit(kWasmTransferLimit + 1024);
// hfs.getLedgerSqn();
for (int i = 0; i < (kWasmTransferLimit / vrt.transferDiff) - 3; ++i)
{
WasmValVec params(2), result(1);
auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
BEAST_EXPECT(result[0].of.i32 == sizeof(std::uint32_t)) &&
BEAST_EXPECT(vrt.getUint32(params, 0) == env.current()->header().seq);
}
BEAST_EXPECT((vrt.getTestTransferLimit() >= 0) && (vrt.getTestTransferLimit() < 1024));
// Next call should hit OutOfTransferLimit
{
WasmValVec params(2), result(1);
auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
BEAST_EXPECT(
result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
}
// After limit exhausted, all next call return OutOfTransferLimit
{
WasmValVec params(2), result(1);
auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
BEAST_EXPECT(
result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
}
// Reset transfer limit to a small value that can accommodate overhead but not AccountID
// copy
vrt.setTransferLimit(vrt.transferDiff + 10);
Account const alice("alice");
auto const aliceID = env.master.id();
vrt.setBytes(0, aliceID.data(), AccountID::size());
// This should fail because getDataAccountID() needs to copy AccountID (20 bytes)
// After getTransferLimit() overhead (1024), we only have 10 bytes left, not enough for 20
{
WasmValVec params(4), result(1);
auto* trap =
ww(&import.at("accountroot_id"), params, result, 0, AccountID::size(), 100, 32);
BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
BEAST_EXPECT(
result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
}
// Verify that reading slices (without copying) does NOT consume transfer limit
vrt.setTransferLimit(vrt.transferDiff + 10);
// trace() uses getDataString() -> getDataSlice() which does NOT check transfer limit
std::string testMsg = "This message is longer than 10 bytes to prove slices don't count";
vrt.setBytes(0, testMsg.data(), testMsg.size());
vrt.setBytes(100, (uint8_t const*)"dummy", 5); // Empty data slice for trace
{
WasmValVec params(5), result(1);
// trace(msg_ptr, msg_len, data_ptr, data_len, asHex)
auto* trap = ww(&import.at("trace"), params, result, 0, testMsg.size(), 100, 5, 0);
// Should succeed even though message is >10 bytes, because trace only uses slices
// (no transfer limit check in getDataSlice)
BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
BEAST_EXPECT(result[0].of.i32 == 0);
}
// setData should return when transfer limit is exhausted
// After trace consumed overhead (1024 bytes), we have 10 - 1024 = negative limit left
{
WasmValVec params(2), result(1);
auto* trap = ww(&import.at("parent_ldgr_hash"), params, result, 500, 32);
// the transfer limit went negative
BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
BEAST_EXPECT(
result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
}
}
void
run() override
{
@@ -6099,6 +6237,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite
testFloats();
testVectorIndexes();
testTransferLimit();
}
};