diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 2e12c7fa55..e431f4459a 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -254,6 +254,9 @@ constexpr std::uint8_t kMaxAssetCheckDepth = 5; /** Maximum length of a Data field in Escrow object that can be updated by WASM code. */ constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB +/** Maximum amount of data transfer across hostfunction<->wasm border. */ +constexpr std::size_t kWasmTransferLimit = 1 << 20; // 1MB + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/include/xrpl/tx/wasm/WasmCommon.h b/include/xrpl/tx/wasm/WasmCommon.h index ed03a4aaf4..9dc130f667 100644 --- a/include/xrpl/tx/wasm/WasmCommon.h +++ b/include/xrpl/tx/wasm/WasmCommon.h @@ -124,6 +124,12 @@ public: virtual std::int64_t setGas(std::int64_t gas) = 0; + + virtual std::int64_t + getTransferLimit() = 0; + + virtual std::int64_t + setTransferLimit(std::int64_t transferLimit) = 0; }; using RTOptRef = std::optional>; diff --git a/include/xrpl/tx/wasm/WasmiVM.h b/include/xrpl/tx/wasm/WasmiVM.h index d31c096b15..9616011af1 100644 --- a/include/xrpl/tx/wasm/WasmiVM.h +++ b/include/xrpl/tx/wasm/WasmiVM.h @@ -147,6 +147,7 @@ class InstanceWrapper mutable int memIdx_ = -1; InstancePtr instance_; beast::Journal j_ = beast::Journal(beast::Journal::getNullSink()); + std::int64_t transferLimit_ = kWasmTransferLimit; private: static InstancePtr @@ -194,6 +195,12 @@ public: std::int64_t setGas(std::int64_t) const; + + std::int64_t + getTransferLimit() const; + + std::int64_t + setTransferLimit(std::int64_t); }; class ModuleWrapper diff --git a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp index 9939407034..17e84391ab 100644 --- a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp +++ b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp @@ -41,6 +41,8 @@ namespace xrpl { using SFieldCRef = std::reference_wrapper; +constexpr int64_t unalignedGas = 50; + static inline Expected checkGas(WasmRuntimeWrapper& rt, int64_t delta) { @@ -81,6 +83,21 @@ checkGas(WasmRuntimeWrapper& rt, WasmImportFunc const& impFunc) return *g; } +static inline Expected +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, 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 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(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 diff --git a/src/libxrpl/tx/wasm/WasmiVM.cpp b/src/libxrpl/tx/wasm/WasmiVM.cpp index 018b97e22b..91f4770459 100644 --- a/src/libxrpl/tx/wasm/WasmiVM.cpp +++ b/src/libxrpl/tx/wasm/WasmiVM.cpp @@ -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::max(); + } + else + { + transferLimit_ = x; + } + + return transferLimit_; +} + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// ModulePtr diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp index 700c706198..377a7c652a 100644 --- a/src/test/app/HostFuncImpl_test.cpp +++ b/src/test/app/HostFuncImpl_test.cpp @@ -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::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(); } };