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

@@ -41,6 +41,8 @@ namespace xrpl {
using SFieldCRef = std::reference_wrapper<SField const>;
constexpr int64_t unalignedGas = 50;
static inline Expected<std::int64_t, HostFunctionError>
checkGas(WasmRuntimeWrapper& rt, int64_t delta)
{
@@ -81,6 +83,21 @@ checkGas(WasmRuntimeWrapper& rt, WasmImportFunc const& impFunc)
return *g;
}
static inline Expected<std::int64_t, HostFunctionError>
checkTransfer(WasmRuntimeWrapper& rt, int64_t delta)
{
auto const transLimit = rt.getTransferLimit();
int64_t const x = transLimit >= delta ? transLimit - delta : 0;
if (rt.setTransferLimit(x) < 0)
return Unexpected(HostFunctionError::Internal); // LCOV_EXCL_LINE
if (transLimit < delta)
return Unexpected(HostFunctionError::OutOfTransferLimit);
return x;
}
static Expected<std::tuple<HostFunctions&, WasmImportFunc const&>, wasm_trap_t*>
mainCheck(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results)
{
@@ -146,6 +163,9 @@ setData(
if (srcSize > dstSize)
return hfErrorToInt(HostFunctionError::BufferTooSmall);
if (auto t = checkTransfer(runtime, srcSize); !t)
return hfErrorToInt(t.error());
memcpy(memory.p + dst, src, srcSize);
return srcSize;
@@ -255,6 +275,9 @@ getDataUInt256(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_
if (slice->size() != uint256::size())
return Unexpected(HostFunctionError::InvalidParams);
if (auto t = checkTransfer(runtime, uint256::size()); !t)
return Unexpected(t.error());
return uint256::fromVoid(slice->data());
}
@@ -268,6 +291,9 @@ getDataAccountID(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int3
if (slice->size() != AccountID::size())
return Unexpected(HostFunctionError::InvalidParams);
if (auto t = checkTransfer(runtime, AccountID::size()); !t)
return Unexpected(t.error());
return AccountID::fromVoid(slice->data());
}
@@ -281,6 +307,9 @@ getDataCurrency(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32
if (slice->size() != Currency::size())
return Unexpected(HostFunctionError::InvalidParams);
if (auto t = checkTransfer(runtime, Currency::size()); !t)
return Unexpected(t.error());
return Currency::fromVoid(slice->data());
}
@@ -293,12 +322,18 @@ getDataAsset(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t&
if (slice->size() == MPTID::size())
{
if (auto t = checkTransfer(runtime, slice->size()); !t)
return Unexpected(t.error());
auto const mptid = MPTID::fromVoid(slice->data());
return Asset{mptid};
}
if (slice->size() == Currency::size())
{
if (auto t = checkTransfer(runtime, slice->size()); !t)
return Unexpected(t.error());
auto const currency = Currency::fromVoid(slice->data());
auto const issue = Issue{currency, xrpAccount()};
if (!issue.native())
@@ -309,6 +344,9 @@ getDataAsset(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t&
if (slice->size() == (Currency::size() + AccountID::size()))
{
if (auto t = checkTransfer(runtime, slice->size()); !t)
return Unexpected(t.error());
auto const issue = Issue(
Currency::fromVoid(slice->data()),
AccountID::fromVoid(slice->data() + Currency::size()));
@@ -349,6 +387,12 @@ getDataLocator(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_
if ((p & (alignof(int32_t) - 1)) != 0u)
{ // unaligned
// Use gas and transfer limit for copying
if (auto g = checkGas(runtime, unalignedGas); !g)
return Unexpected(g.error());
if (auto t = checkTransfer(runtime, slice->size()); !t)
return Unexpected(t.error());
std::vector<int32_t> locBuf(locSize);
memcpy(&locBuf[0], slice->data(), slice->size());
FieldLocator locator(std::move(locBuf));
@@ -481,6 +525,7 @@ returnResult(
}
//----------------------------------------------------------------------------------------------------------------------
wasm_trap_t*
HostFuncMain_wrap(WASM_CB_PARAMS_LIST)
{
@@ -490,9 +535,9 @@ HostFuncMain_wrap(WASM_CB_PARAMS_LIST)
{
auto const mc = mainCheck(env, params, results);
if (!mc)
return mc.error(); // LCOV_EXCL_LINE
auto& [hf, impFunc] = *mc;
return mc.error();
auto& [hf, impFunc] = *mc;
hfName = impFunc.name;
auto* fWrap = reinterpret_cast<wasmSecondaryCbFuncType*>(impFunc.wrap);
return fWrap(hf, params, results);
@@ -1677,6 +1722,7 @@ class MockWasmRuntimeWrapper : public WasmRuntimeWrapper
Wmem mem_;
std::int64_t gas_ = 1'000'000;
std::int64_t transferLimit_ = kWasmTransferLimit;
public:
MockWasmRuntimeWrapper(Wmem memory) : mem_(memory)
@@ -1702,6 +1748,19 @@ public:
gas_ = gas;
return gas_;
}
std::int64_t
getTransferLimit() override
{
return transferLimit_;
}
std::int64_t
setTransferLimit(std::int64_t x) override
{
transferLimit_ = x;
return transferLimit_;
}
};
bool

View File

@@ -106,6 +106,18 @@ public:
{
return iw_.setGas(gas);
}
std::int64_t
getTransferLimit() override
{
return iw_.getTransferLimit();
}
std::int64_t
setTransferLimit(std::int64_t x) override
{
return iw_.setTransferLimit(x);
}
};
InstancePtr
@@ -247,6 +259,32 @@ InstanceWrapper::setGas(std::int64_t gas) const
return gas;
}
std::int64_t
InstanceWrapper::getTransferLimit() const
{
if (store_ == nullptr)
return -1; // LCOV_EXCL_LINE
return transferLimit_;
}
std::int64_t
InstanceWrapper::setTransferLimit(std::int64_t x)
{
if (store_ == nullptr)
return -1; // LCOV_EXCL_LINE
if (x < 0)
{
transferLimit_ = std::numeric_limits<decltype(transferLimit_)>::max();
}
else
{
transferLimit_ = x;
}
return transferLimit_;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
ModulePtr

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();
}
};