From 8022fc33cf12800fcccac0a5b88b9b0dd2e43f24 Mon Sep 17 00:00:00 2001 From: pwang200 <354723+pwang200@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:35:46 -0400 Subject: [PATCH] host function error path refactor (#7639) --- include/xrpl/protocol/TER.h | 1 + include/xrpl/tx/wasm/HostFunc.h | 132 ++++++++++----------- include/xrpl/tx/wasm/WasmCommon.h | 30 ++++- include/xrpl/tx/wasm/WasmVM.h | 4 +- include/xrpl/tx/wasm/WasmiVM.h | 11 +- src/libxrpl/protocol/TER.cpp | 1 + src/libxrpl/tx/wasm/HostFuncImplGetter.cpp | 7 +- src/libxrpl/tx/wasm/HostFuncWrapper.cpp | 98 ++++++--------- src/libxrpl/tx/wasm/WasmVM.cpp | 11 +- src/libxrpl/tx/wasm/WasmiVM.cpp | 72 +++++++++-- src/test/app/TestHostFunctions.h | 2 +- src/test/app/Wasm_test.cpp | 14 ++- 12 files changed, 225 insertions(+), 158 deletions(-) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index c9e2b6eabd..60c6b3111d 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -360,6 +360,7 @@ enum TECcodes : TERUnderlyingType { tecLIMIT_EXCEEDED = 195, tecPSEUDO_ACCOUNT = 196, tecPRECISION_LOSS = 197, + tecOUT_OF_GAS = 200, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/tx/wasm/HostFunc.h b/include/xrpl/tx/wasm/HostFunc.h index 914e9b6ed1..d8424239e8 100644 --- a/include/xrpl/tx/wasm/HostFunc.h +++ b/include/xrpl/tx/wasm/HostFunc.h @@ -113,398 +113,398 @@ public: virtual Expected getLedgerSqn() const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getParentLedgerTime() const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getParentLedgerHash() const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getBaseFee() const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected isAmendmentEnabled(uint256 const& amendmentId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected isAmendmentEnabled(std::string_view const& amendmentName) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getTxField(SField const& fname) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getCurrentLedgerObjField(SField const& fname) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getLedgerObjField(int32_t cacheIdx, SField const& fname) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getTxNestedField(FieldLocator const& locator) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getCurrentLedgerObjNestedField(FieldLocator const& locator) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getTxArrayLen(SField const& fname) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getCurrentLedgerObjArrayLen(SField const& fname) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getTxNestedArrayLen(FieldLocator const& locator) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected updateData(Slice const& data) { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected computeSha512HalfHash(Slice const& data) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected accountKeylet(AccountID const& account) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected ammKeylet(Asset const& issue1, Asset const& issue2) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected checkKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected didKeylet(AccountID const& account) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected delegateKeylet(AccountID const& account, AccountID const& authorize) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected depositPreauthKeylet(AccountID const& account, AccountID const& authorize) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected escrowKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected lineKeylet(AccountID const& account1, AccountID const& account2, Currency const& currency) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected mptIssuanceKeylet(AccountID const& issuer, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected mptokenKeylet(MPTID const& mptid, AccountID const& holder) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected nftOfferKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected offerKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected oracleKeylet(AccountID const& account, std::uint32_t docId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected paychanKeylet(AccountID const& account, AccountID const& destination, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected permissionedDomainKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected signersKeylet(AccountID const& account) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected ticketKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected vaultKeylet(AccountID const& account, std::uint32_t seq) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getNFT(AccountID const& account, uint256 const& nftId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getNFTIssuer(uint256 const& nftId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getNFTTaxon(uint256 const& nftId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getNFTFlags(uint256 const& nftId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getNFTTransferFee(uint256 const& nftId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected getNFTSerial(uint256 const& nftId) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected trace(std::string_view const& msg, Slice const& data, bool asHex) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected traceNum(std::string_view const& msg, int64_t data) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected traceAccount(std::string_view const& msg, AccountID const& account) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected traceFloat(std::string_view const& msg, Slice const& data) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected traceAmount(std::string_view const& msg, STAmount const& amount) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatFromInt(int64_t x, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatFromUint(uint64_t x, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatFromSTAmount(STAmount const& x, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatFromSTNumber(STNumber const& x, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatToInt(Slice const& x, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatToMantExp(Slice const& x) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatCompare(Slice const& x, Slice const& y) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatAdd(Slice const& x, Slice const& y, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatSubtract(Slice const& x, Slice const& y, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatMultiply(Slice const& x, Slice const& y, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatDivide(Slice const& x, Slice const& y, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatRoot(Slice const& x, int32_t n, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual Expected floatPower(Slice const& x, int32_t n, int32_t mode) const { - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } virtual ~HostFunctions() = default; diff --git a/include/xrpl/tx/wasm/WasmCommon.h b/include/xrpl/tx/wasm/WasmCommon.h index 9dc130f667..8d3cff09c7 100644 --- a/include/xrpl/tx/wasm/WasmCommon.h +++ b/include/xrpl/tx/wasm/WasmCommon.h @@ -2,10 +2,12 @@ #include #include +#include #include #include #include +#include #include namespace xrpl { @@ -14,8 +16,20 @@ using Bytes = std::vector; using Hash = xrpl::uint256; using FloatPair = std::pair; +// Error signals that cross the wasm boundary as trap messages (the C API has no +// trap code). WasmiEngine::call maps them to TER: hfErrInternal -> tecINTERNAL, +// hfErrOutOfGas / wasmi's OutOfFuel -> tecOUT_OF_GAS, anything else -> +// tecFAILED_PROCESSING. +// +// Matched as substrings, not by equality: the C API returns the Rust Debug form +// of the error, e.g. `Error { kind: Message("HfInternal") }` or +// `Error { kind: TrapCode(OutOfFuel) }`. +std::string_view inline constexpr hfErrInternal = "HfInternal"; +std::string_view inline constexpr hfErrOutOfGas = "HfOutOfGas"; +std::string_view inline constexpr wasmiTrapOutOfFuel = "OutOfFuel"; + enum class HostFunctionError : int32_t { - Internal = -1, + Unimplemented = -1, FieldNotFound = -2, BufferTooSmall = -3, NoArray = -4, @@ -25,7 +39,7 @@ enum class HostFunctionError : int32_t { SlotsFull = -8, EmptySlot = -9, LedgerObjNotFound = -10, - Decoding = -11, + OutOfTransferLimit = -11, DataFieldTooLarge = -12, PointerOutOfBounds = -13, NoMemExported = -14, @@ -35,9 +49,6 @@ enum class HostFunctionError : int32_t { IndexOutOfBounds = -18, FloatInputMalformed = -19, FloatComputationError = -20, - NoRuntime = -21, - OutOfGas = -22, - OutOfTransferLimit = -23, }; enum class WasmTypes { WtI32, WtI64 }; @@ -61,6 +72,15 @@ struct WasmResult }; using EscrowResult = WasmResult; +// Engine error when wasm does not run to completion. `cost` is the gas consumed +// when meaningful (tecOUT_OF_GAS / tecFAILED_PROCESSING; caller writes it to tx +// metadata); std::nullopt for tecINTERNAL and malformed input (no gas reported). +struct WasmTER +{ + TER ter; + std::optional cost; +}; + class FieldLocator { int32_t const* ptr_ = nullptr; diff --git a/include/xrpl/tx/wasm/WasmVM.h b/include/xrpl/tx/wasm/WasmVM.h index ac3d987d1d..48b0438654 100644 --- a/include/xrpl/tx/wasm/WasmVM.h +++ b/include/xrpl/tx/wasm/WasmVM.h @@ -40,7 +40,7 @@ public: static WasmEngine& instance(); - Expected, TER> + Expected, WasmTER> run(Bytes const& wasmCode, HostFunctions& hfs, int64_t gasLimit, @@ -71,7 +71,7 @@ public: ImportVec createWasmImport(HostFunctions& hfs); -Expected +Expected runEscrowWasm( Bytes const& wasmCode, HostFunctions& hfs, diff --git a/include/xrpl/tx/wasm/WasmiVM.h b/include/xrpl/tx/wasm/WasmiVM.h index 9616011af1..45ef703e91 100644 --- a/include/xrpl/tx/wasm/WasmiVM.h +++ b/include/xrpl/tx/wasm/WasmiVM.h @@ -6,6 +6,8 @@ #include #include +#include + namespace xrpl { template @@ -120,7 +122,10 @@ using WasmImporttypeVec = WasmVec< struct WasmiResult { WasmValVec r; - bool f{false}; // failure flag + // Set iff the call trapped. Holds the TER the trap was classified into + // (tecINTERNAL / tecOUT_OF_GAS / tecFAILED_PROCESSING); see + // WasmiEngine::call. std::nullopt means the call returned normally. + std::optional ter; WasmiResult(unsigned n = 0) : r(n) { @@ -304,7 +309,7 @@ public: static EnginePtr init(); - Expected, TER> + Expected, WasmTER> run(Bytes const& wasmCode, HostFunctions& hfs, int64_t gas, @@ -355,7 +360,7 @@ private: return moduleWrap_ ? moduleWrap_->getMem() : Wmem(); } - Expected, TER> + Expected, WasmTER> runHlp( Bytes const& wasmCode, HostFunctions& hfs, diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 443befe246..38e1ecdfe6 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -106,6 +106,7 @@ transResults() MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), + MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp b/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp index f86bc5741e..344c1f038a 100644 --- a/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp +++ b/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -13,6 +14,8 @@ #include #include +#include +#include #include #include @@ -121,7 +124,9 @@ getAnyFieldData(FieldValue const& variantObj) if (uint256 const* const* u = std::get_if(&variantObj)) return Bytes((*u)->begin(), (*u)->end()); - return Unexpected(HostFunctionError::Internal); // LCOV_EXCL_LINE + // Unreachable: the variant only holds the two alternatives above. If not, + // it's an xrpld bug -> tecINTERNAL (thrown, caught by HostFuncMain_wrap). + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE } static inline bool diff --git a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp index 17e84391ab..af8bb637d4 100644 --- a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp +++ b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,8 @@ #include #include #include +#include +#include #include #include #include @@ -43,7 +46,10 @@ using SFieldCRef = std::reference_wrapper; constexpr int64_t unalignedGas = 50; -static inline Expected +// Charge `delta` gas; returns the remaining gas. Out-of-gas throws hfErrOutOfGas +// (-> tecOUT_OF_GAS); a failed setGas is an xrpld bug, throws hfErrInternal +// (-> tecINTERNAL). HostFuncMain_wrap turns both into traps. +static inline std::int64_t checkGas(WasmRuntimeWrapper& rt, int64_t delta) { int64_t const gas = rt.getGas(); @@ -53,36 +59,16 @@ checkGas(WasmRuntimeWrapper& rt, int64_t delta) int64_t const x = gas >= delta ? gas - delta : 0; if (rt.setGas(x) < 0) - return Unexpected(HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE if (gas < delta) - return Unexpected(HostFunctionError::OutOfGas); + Throw(std::string(hfErrOutOfGas)); return x; } -static inline Expected -checkGas(WasmRuntimeWrapper& rt, WasmImportFunc const& impFunc) -{ - auto g = checkGas(rt, impFunc.gas); - - if (!g) - { - if (g.error() == HostFunctionError::OutOfGas) - { - wasm_trap_t* const trap = // NOLINT - reinterpret_cast(WasmEngine::instance().newTrap("hf out of gas")); - return Unexpected(trap); - } - - wasm_trap_t* trap = reinterpret_cast( // NOLINT - WasmEngine::instance().newTrap("can't set gas")); // LCOV_EXCL_LINE - return Unexpected(trap); // LCOV_EXCL_LINE - } - - return *g; -} - +// Transfer limit is a separate soft budget: exceeding it is a normal guest-facing +// return code, not a trap. Only a failed setTransferLimit (an xrpld bug) throws. static inline Expected checkTransfer(WasmRuntimeWrapper& rt, int64_t delta) { @@ -90,7 +76,7 @@ checkTransfer(WasmRuntimeWrapper& rt, int64_t delta) int64_t const x = transLimit >= delta ? transLimit - delta : 0; if (rt.setTransferLimit(x) < 0) - return Unexpected(HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE if (transLimit < delta) return Unexpected(HostFunctionError::OutOfTransferLimit); @@ -98,37 +84,27 @@ checkTransfer(WasmRuntimeWrapper& rt, int64_t delta) return x; } -static Expected, wasm_trap_t*> +// On any failure here a C++ exception is thrown; HostFuncMain_wrap's catch-all +// turns it into tecINTERNAL. These conditions are all xrpld-side invariants. +static std::tuple mainCheck(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) { if (env == nullptr) - { - wasm_trap_t* trap = reinterpret_cast( // NOLINT - WasmEngine::instance().newTrap("no environment")); // LCOV_EXCL_LINE - return Unexpected(trap); // LCOV_EXCL_LINE - } + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE if (params == nullptr) - { - wasm_trap_t* trap = reinterpret_cast( // NOLINT - WasmEngine::instance().newTrap("no params")); // LCOV_EXCL_LINE - return Unexpected(trap); // LCOV_EXCL_LINE - } + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE if (results == nullptr) - { - wasm_trap_t* trap = reinterpret_cast( // NOLINT - WasmEngine::instance().newTrap("no results")); // LCOV_EXCL_LINE - return Unexpected(trap); // LCOV_EXCL_LINE - } + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE WasmUserData const* udata = reinterpret_cast(env); HostFunctions& hf = udata->first; WasmRuntimeWrapper& rt = hf.getRT(); WasmImportFunc const& impFunc = udata->second; - if (auto g = checkGas(rt, impFunc); !g) - return Unexpected(g.error()); // LCOV_EXCL_LINE + // Charge the per-call gas. Throws (and terminates) if out of gas. + checkGas(rt, impFunc.gas); return std::tie(hf, impFunc); } @@ -387,9 +363,10 @@ 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()); + // Use gas and transfer limit for copying. checkGas throws (and + // terminates execution) if out of gas; checkTransfer keeps returning a + // guest-facing code when the transfer limit is exceeded. + checkGas(runtime, unalignedGas); if (auto t = checkTransfer(runtime, slice->size()); !t) return Unexpected(t.error()); @@ -435,7 +412,7 @@ returnResult( if constexpr (std::is_same_v) { if (index < 0 || index + 1 >= params->size) - return hfResult(results, HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE auto const dataResult = setData( runtime, @@ -448,7 +425,7 @@ returnResult( else if constexpr (std::is_same_v) { if (index < 0 || index + 1 >= params->size) - return hfResult(results, HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE auto const dataResult = setData( runtime, @@ -465,7 +442,7 @@ returnResult( else if constexpr (std::is_same_v) { if (index < 0 || index + 1 >= params->size) - return hfResult(results, HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE auto const resultValue = adjustWasmEndianess(res.value()); auto const dataResult = setData( @@ -479,7 +456,7 @@ returnResult( else if constexpr (std::is_same_v) { if (index < 0 || index + 1 >= params->size) - return hfResult(results, HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE auto const resultValue = adjustWasmEndianess(res.value()); auto const dataResult = setData( @@ -493,7 +470,7 @@ returnResult( else if constexpr (std::is_same_v) { if (index < 0 || index + 3 >= params->size) - return hfResult(results, HostFunctionError::Internal); // LCOV_EXCL_LINE + Throw(std::string(hfErrInternal)); // LCOV_EXCL_LINE auto const mantissa = adjustWasmEndianess(res->first); auto const r1 = setData( @@ -533,11 +510,7 @@ HostFuncMain_wrap(WASM_CB_PARAMS_LIST) try { - auto const mc = mainCheck(env, params, results); - if (!mc) - return mc.error(); - - auto& [hf, impFunc] = *mc; + auto [hf, impFunc] = mainCheck(env, params, results); hfName = impFunc.name; auto* fWrap = reinterpret_cast(impFunc.wrap); return fWrap(hf, params, results); @@ -547,8 +520,11 @@ HostFuncMain_wrap(WASM_CB_PARAMS_LIST) #ifdef DEBUG_OUTPUT std::cerr << "Hostfunction " << hfName << " exception: " << e.what() << std::endl; #endif + // Normalize to the two boundary signals: explicit out-of-gas, else any + // exception (including stray ones from helpers) is an internal fault. + bool const oog = std::string_view(e.what()) == hfErrOutOfGas; wasm_trap_t* trap = reinterpret_cast( // NOLINT - WasmEngine::instance().newTrap(e.what())); // LCOV_EXCL_LINE + WasmEngine::instance().newTrap(std::string(oog ? hfErrOutOfGas : hfErrInternal))); return trap; } catch (...) @@ -556,12 +532,12 @@ HostFuncMain_wrap(WASM_CB_PARAMS_LIST) #ifdef DEBUG_OUTPUT std::cerr << "Hostfunction " << hfName << " unknown exception." << std::endl; #endif - wasm_trap_t* trap = reinterpret_cast( // NOLINT - WasmEngine::instance().newTrap("Unknown exception")); // LCOV_EXCL_LINE + wasm_trap_t* trap = reinterpret_cast( // NOLINT + WasmEngine::instance().newTrap(std::string(hfErrInternal))); // LCOV_EXCL_LINE return trap; } - return nullptr; + return nullptr; // LCOV_EXCL_LINE } //---------------------------------------------------------------------------------------------------------------------- diff --git a/src/libxrpl/tx/wasm/WasmVM.cpp b/src/libxrpl/tx/wasm/WasmVM.cpp index 97b44dba43..0a2dcbde65 100644 --- a/src/libxrpl/tx/wasm/WasmVM.cpp +++ b/src/libxrpl/tx/wasm/WasmVM.cpp @@ -115,7 +115,7 @@ createWasmImport(HostFunctions& hfs) return i; } -Expected +Expected runEscrowWasm( Bytes const& wasmCode, HostFunctions& hfs, @@ -133,9 +133,12 @@ runEscrowWasm( if (!ret) { #ifdef DEBUG_OUTPUT - std::cout << ", error: " << ret.error() << std::endl; + std::cout << ", error: " << ret.error().ter << std::endl; #endif - return Unexpected(ret.error()); + // Carries the TER (tecOUT_OF_GAS / tecFAILED_PROCESSING / tecINTERNAL / + // temBAD_AMOUNT) and, when meaningful, the gas consumed. The caller is + // responsible for writing that gas to tx metadata. + return Unexpected(ret.error()); } #ifdef DEBUG_OUTPUT @@ -174,7 +177,7 @@ WasmEngine::instance() return e; } -Expected, TER> +Expected, WasmTER> WasmEngine::run( Bytes const& wasmCode, HostFunctions& hfs, diff --git a/src/libxrpl/tx/wasm/WasmiVM.cpp b/src/libxrpl/tx/wasm/WasmiVM.cpp index 91f4770459..57968d311e 100644 --- a/src/libxrpl/tx/wasm/WasmiVM.cpp +++ b/src/libxrpl/tx/wasm/WasmiVM.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -78,6 +79,26 @@ printWasmError(std::string_view msg, wasm_trap_t* trap, beast::Journal jlog) } // LCOV_EXCL_STOP +// Extract a trap's message into a std::string (the only signal the C API gives +// for classification; see the trap-signal constants in WasmCommon.h). Does not +// take ownership of `trap`. +std::string +trapMessage(wasm_trap_t* trap) +{ + if (trap == nullptr) + return {}; // LCOV_EXCL_LINE + wasm_byte_vec_t msg WASM_EMPTY_VEC; + wasm_trap_message(trap, &msg); + std::string out; + if (msg.size != 0u) + { + // wasm_trap_message NUL-terminates, so drop the trailing NUL. + out.assign(msg.data, msg.size - 1); + wasm_byte_vec_delete(&msg); + } + return out; +} + } // namespace class WasmiRuntimeWrapper : public WasmRuntimeWrapper @@ -679,7 +700,24 @@ WasmiEngine::call(FuncInfo const& f, std::vector& in) if (trap) { - ret.f = true; + // Classify the trap into a TER by matching tokens as substrings of the + // message (see the trap-signal constants in WasmCommon.h for why). + std::string const msg = trapMessage(trap); + auto const has = [&msg](std::string_view token) { + return msg.find(token) != std::string::npos; + }; + if (has(hfErrInternal)) + { + ret.ter = tecINTERNAL; + } + else if (has(hfErrOutOfGas) || has(wasmiTrapOutOfFuel)) + { + ret.ter = tecOUT_OF_GAS; + } + else + { + ret.ter = tecFAILED_PROCESSING; + } printWasmError("failure to call func", trap, j_); } @@ -719,7 +757,7 @@ checkImports(ImportVec const& imports, HostFunctions* hfs) } } -Expected, TER> +Expected, WasmTER> WasmiEngine::run( Bytes const& wasmCode, HostFunctions& hfs, @@ -730,7 +768,7 @@ WasmiEngine::run( beast::Journal j) { if (gas <= 0) - return Unexpected(temBAD_AMOUNT); + return Unexpected(WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt}); try { @@ -747,10 +785,12 @@ WasmiEngine::run( printWasmError(std::string("exception: unknown"), nullptr, j); } // LCOV_EXCL_STOP - return Unexpected(tecFAILED_PROCESSING); + // An exception escaping the engine is an xrpld-side fault -> tecINTERNAL, + // no gas. Genuine wasm faults don't throw; they surface as traps in runHlp. + return Unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}); } -Expected, TER> +Expected, WasmTER> WasmiEngine::runHlp( Bytes const& wasmCode, HostFunctions& hfs, @@ -793,8 +833,24 @@ WasmiEngine::runHlp( auto const res = call<1>(f, p); - if (res.f) - Throw("<" + std::string(funcName) + "> failure"); + if (gas == -1) + gas = std::numeric_limits::max(); + + if (res.ter.has_value()) + { + // call() already classified the trap (see WasmiEngine::call). + // tecINTERNAL is an xrpld-side bug: report no gas. + if (*res.ter == tecINTERNAL) + return Unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}); + + // Out-of-gas / wasm faults report gas (caller writes it to metadata). + // Force fuel to 0 on out-of-gas so cost is the full limit (wasmi leaves + // nonzero leftover fuel on its own out-of-fuel trap). + if (*res.ter == tecOUT_OF_GAS) + iw.setGas(0); + + return Unexpected(WasmTER{.ter = *res.ter, .cost = gas - moduleWrap_->getGas()}); + } if (res.r.empty()) { @@ -809,8 +865,6 @@ WasmiEngine::runHlp( "> return type mismatch, ret: " + std::to_string(static_cast(res.r[0].kind))); } - if (gas == -1) - gas = std::numeric_limits::max(); WasmResult const ret{.result = res.r[0].of.i32, .cost = gas - moduleWrap_->getGas()}; // #ifdef DEBUG_OUTPUT diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index 13bc18e09c..9d8bc94231 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -135,7 +135,7 @@ public: return Bytes{s.begin(), s.end()}; } - return Unexpected(HostFunctionError::Internal); + return Unexpected(HostFunctionError::Unimplemented); } Expected diff --git a/src/test/app/Wasm_test.cpp b/src/test/app/Wasm_test.cpp index bae464f7be..230494f498 100644 --- a/src/test/app/Wasm_test.cpp +++ b/src/test/app/Wasm_test.cpp @@ -53,13 +53,13 @@ struct Wasm_test : public beast::unit_test::Suite { void checkResult( - Expected, TER> re, + Expected, WasmTER> re, int32_t expectedResult, int64_t expectedCost, std::source_location const location = std::source_location::current()) { auto const lineStr = " (" + std::to_string(location.line()) + ")"; - if (BEAST_EXPECTS(re.has_value(), transToken(re.error()) + lineStr)) + if (BEAST_EXPECTS(re.has_value(), transToken(re.error().ter) + lineStr)) { BEAST_EXPECTS(re->result == expectedResult, std::to_string(re->result) + lineStr); BEAST_EXPECTS(re->cost == expectedCost, std::to_string(re->cost) + lineStr); @@ -258,8 +258,10 @@ struct Wasm_test : public beast::unit_test::Suite if (BEAST_EXPECT(!re)) { + // Running out of gas now terminates with tecOUT_OF_GAS (was + // previously collapsed into tecFAILED_PROCESSING). BEAST_EXPECTS( - re.error() == tecFAILED_PROCESSING, std::to_string(TERtoInt(re.error()))); + re.error().ter == tecOUT_OF_GAS, std::to_string(TERtoInt(re.error().ter))); } env.close(); @@ -286,7 +288,7 @@ struct Wasm_test : public beast::unit_test::Suite TestHostFunctions hfs(env); auto re = runEscrowWasm(allHFWasm, hfs, -1, escrowFunctionName, {}); BEAST_EXPECT(!re.has_value()); - BEAST_EXPECT(re.error() == temBAD_AMOUNT); + BEAST_EXPECT(re.error().ter == temBAD_AMOUNT); } { @@ -294,7 +296,7 @@ struct Wasm_test : public beast::unit_test::Suite TestHostFunctions hfs(env); auto re = runEscrowWasm(allHFWasm, hfs, 0, escrowFunctionName, {}); BEAST_EXPECT(!re.has_value()); - BEAST_EXPECT(re.error() == temBAD_AMOUNT); + BEAST_EXPECT(re.error().ter == temBAD_AMOUNT); } { @@ -379,7 +381,7 @@ struct Wasm_test : public beast::unit_test::Suite auto& engine = WasmEngine::instance(); auto re = engine.run(badAlignWasm, hfs, 1'000'000, "test", {}, imports, env.journal); - if (BEAST_EXPECTS(re, transToken(re.error()))) + if (BEAST_EXPECTS(re, transToken(re.error().ter))) { BEAST_EXPECTS(re->result == 0x47308594, std::to_string(re->result)); }