diff --git a/src/quickjs/quickjs-libc.c b/src/quickjs/quickjs-libc.c index 475ffd74f..0da5af9e4 100644 --- a/src/quickjs/quickjs-libc.c +++ b/src/quickjs/quickjs-libc.c @@ -3340,7 +3340,7 @@ static void *worker_func(void *opaque) JSContext *ctx; JSValue val; - rt = JS_NewRuntime(); + rt = JS_NewRuntime(-1); if (rt == NULL) { fprintf(stderr, "JS_NewRuntime failure"); exit(1); diff --git a/src/quickjs/quickjs.c b/src/quickjs/quickjs.c index 4157a3cc9..df8dc1d09 100644 --- a/src/quickjs/quickjs.c +++ b/src/quickjs/quickjs.c @@ -303,6 +303,7 @@ struct JSRuntime { uint32_t operator_count; #endif void *user_opaque; + int64_t instruction_limit; }; struct JSClass { @@ -1615,7 +1616,7 @@ static inline BOOL js_check_stack_overflow(JSRuntime *rt, size_t alloca_size) } #endif -JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque) +JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque, uint32_t instructionLimit) { JSRuntime *rt; JSMallocState ms; @@ -1635,6 +1636,7 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque) } rt->malloc_state = ms; rt->malloc_gc_threshold = 256 * 1024; + rt->instruction_limit = instructionLimit; bf_context_init(&rt->bf_ctx, js_bf_realloc, rt); set_dummy_numeric_ops(&rt->bigint_ops); @@ -1772,9 +1774,9 @@ static const JSMallocFunctions def_malloc_funcs = { js_def_malloc_usable_size, }; -JSRuntime *JS_NewRuntime(void) +JSRuntime *JS_NewRuntime(uint32_t instructionLimit) { - return JS_NewRuntime2(&def_malloc_funcs, NULL); + return JS_NewRuntime2(&def_malloc_funcs, NULL, instructionLimit); } void JS_SetMemoryLimit(JSRuntime *rt, size_t limit) @@ -6857,6 +6859,15 @@ static no_inline __exception int __js_poll_interrupts(JSContext *ctx) static inline __exception int js_poll_interrupts(JSContext *ctx) { + if (unlikely(--ctx->rt->instruction_limit <= 0)) + { + /* XXX: should set a specific flag to avoid catching */ + /* RHTODO: investigate if this is user-catchable. */ + JS_ThrowInternalError(ctx, "interrupted"); + JS_SetUncatchableError(ctx, ctx->rt->current_exception, TRUE); + return -1; + } + if (unlikely(--ctx->interrupt_counter <= 0)) { return __js_poll_interrupts(ctx); } else { diff --git a/src/quickjs/quickjs.h b/src/quickjs/quickjs.h index fa55254ca..779fbda10 100644 --- a/src/quickjs/quickjs.h +++ b/src/quickjs/quickjs.h @@ -331,7 +331,7 @@ typedef struct JSMallocFunctions { typedef struct JSGCObjectHeader JSGCObjectHeader; -JSRuntime *JS_NewRuntime(void); +JSRuntime *JS_NewRuntime(uint32_t instructionLimit); /* info lifetime must exceed that of rt */ void JS_SetRuntimeInfo(JSRuntime *rt, const char *info); void JS_SetMemoryLimit(JSRuntime *rt, size_t limit); @@ -341,7 +341,7 @@ void JS_SetMaxStackSize(JSRuntime *rt, size_t stack_size); /* should be called when changing thread to update the stack top value used to check stack overflow. */ void JS_UpdateStackTop(JSRuntime *rt); -JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque); +JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque, uint32_t instructionLimit); void JS_FreeRuntime(JSRuntime *rt); void *JS_GetRuntimeOpaque(JSRuntime *rt); void JS_SetRuntimeOpaque(JSRuntime *rt, void *opaque); diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 272bda1e7..602b3ff59 100644 --- a/src/ripple/app/hook/Enum.h +++ b/src/ripple/app/hook/Enum.h @@ -225,6 +225,8 @@ enum hook_log_code : uint16_t { 86, // the wasm contained a custom section (id=0) INTERNAL_ERROR = 87, // an internal error described by the log text JS_TEST_FAILURE = 88, // smoke test of js bytecode failed + JS_FEE_MISSING = 89, + JS_FEE_TOO_HIGH = 90, // RH NOTE: only HookSet msgs got log codes, possibly all Hook log lines // should get a code? }; diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 34d282218..d4d6f8606 100644 --- a/src/ripple/app/hook/applyHook.h +++ b/src/ripple/app/hook/applyHook.h @@ -819,7 +819,8 @@ apply( uint32_t hookArgument, uint8_t hookChainPosition, // result of apply() if this is weak exec - std::shared_ptr const& provisionalMeta); + std::shared_ptr const& provisionalMeta, + uint32_t instructionLimit); struct HookContext; @@ -998,6 +999,7 @@ public: size_t len, bool callback, uint32_t param, + uint32_t instructionLimit, beast::Journal const& j) = 0; static std::optional validate( @@ -1122,6 +1124,7 @@ public: size_t len, bool callback, uint32_t hookArgument, + uint32_t instructionLimit, /* this is unused in wasm, set to 0 */ beast::Journal const& j) override { // HookExecutorWasm can only execute once @@ -1298,9 +1301,9 @@ public: JSRuntime* rt = NULL; JSContext* ctx = NULL; - QuickJSVM(void* hookCtx) + QuickJSVM(void* hookCtx, uint32_t instructionLimit) { - rt = JS_NewRuntime(); + rt = JS_NewRuntime(instructionLimit); ctx = JS_NewContextRaw(rt); JS_AddIntrinsicBaseObjects(ctx); JS_AddIntrinsicEval(ctx); @@ -1430,7 +1433,7 @@ public: }; /** - * Validate that a web assembly blob can be loaded by wasmedge + * Validate that a js blob can be loaded by quickjs */ static std::optional validate(const void* buf, size_t buf_len) @@ -1441,7 +1444,8 @@ public: std::optional retval; - QuickJSVM vm{NULL}; + // RHNOTE: hard instrction (internal program counter loop) limit of 1MM, like wasm. + QuickJSVM vm{NULL, 1000000}; JSContext* ctx = vm.ctx; if (!vm.sane()) @@ -1511,6 +1515,7 @@ public: size_t buf_len, bool callback, uint32_t hookArgument, + uint32_t instructionLimit, beast::Journal const& j) override { // HookExecutorWasm can only execute once @@ -1520,7 +1525,7 @@ public: JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: creating quickjs instance"; - QuickJSVM vm{reinterpret_cast(&hookCtx)}; + QuickJSVM vm{reinterpret_cast(&hookCtx), instructionLimit}; JSContext* ctx = vm.ctx; if (!vm.sane()) diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index 17885afa7..50c919155 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -1535,7 +1535,8 @@ hook::apply( bool isStrong, uint32_t hookArgument, uint8_t hookChainPosition, - std::shared_ptr const& provisionalMeta) + std::shared_ptr const& provisionalMeta, + uint32_t instructionLimit) { HookContext hookCtx = { .applyCtx = applyCtx, @@ -1582,7 +1583,8 @@ hook::apply( HookExecutorWasm executor{hookCtx}; executor.execute( - bytecode.data(), (size_t)bytecode.size(), isCallback, hookArgument, j); + bytecode.data(), + (size_t)bytecode.size(), isCallback, hookArgument, 0 /* instructioin limit not used in wasm */, j); break; } @@ -1594,7 +1596,7 @@ hook::apply( HookExecutorJS executor{hookCtx}; executor.execute( - bytecode.data(), (size_t)bytecode.size(), isCallback, hookArgument, j); + bytecode.data(), (size_t)bytecode.size(), isCallback, hookArgument, instructionLimit, j); break; } diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp index e9876f104..efc8199fe 100644 --- a/src/ripple/app/tx/impl/SetHook.cpp +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -433,7 +433,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) auto version = hookSetObj.getFieldU16(sfHookApiVersion); if (version > 1) { - // we currently only accept api version 0 + // we currently only accept api version 0 and 1 JLOG(ctx.j.trace()) << "HookSet(" << hook::log::API_INVALID << ")[" << HS_ACC() << "]: Malformed transaction: SetHook " @@ -462,7 +462,33 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) if (version == 1) { // RHTODO: guard or other check for js, depending on design choices - + + if (hookSetObj.isFieldPresent(sfFee) && + isXRP(hookSetObj.getFieldAmount(sfFee))) + { + STAmount amt = hookSetObj.getFieldAmount(sfFee); + uint64_t fee = amt.xrp().drops(); + if (amt < beast::zero || fee < 1 || fee > 1000000) + { + JLOG(ctx.j.trace()) + << "HookSet(" << hook::log::JS_FEE_TOO_HIGH << ")[" + << HS_ACC() + << "]: Malformed transaction: When creating a JS Hook " + << "you must include a Fee <= 1000000."; + return false; + + } + } + else + { + JLOG(ctx.j.trace()) + << "HookSet(" << hook::log::JS_FEE_MISSING << ")[" + << HS_ACC() + << "]: Malformed transaction: When creating a JS Hook " + << "you must include a Fee field indicating the instruction limit."; + return false; + } + std::optional result = hook::HookExecutorJS::validate( hook.data(), (size_t)hook.size()); @@ -749,7 +775,8 @@ SetHook::preflight(PreflightContext const& ctx) if (name != sfCreateCode && name != sfHookHash && name != sfHookNamespace && name != sfHookParameters && name != sfHookOn && name != sfHookGrants && - name != sfHookApiVersion && name != sfFlags) + name != sfHookApiVersion && name != sfFlags && + name != sfFee) { JLOG(ctx.j.trace()) << "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")[" @@ -1525,6 +1552,17 @@ SetHook::setHook() return tecREQUIRES_FLAG; } + uint16_t hookApiVersion = hookSetObj->get().getFieldU16(sfHookApiVersion); + + if (hookApiVersion == 1 && !hookSetObj->get().isFieldPresent(sfFee)) + { + JLOG(ctx.j.warn()) + << "HookSet(" << hook::log::JS_FEE_MISSING << ")[" + << HS_ACC() + << "]: Malformed transaction: SetHook operation for JS Hook missing fee."; + return tecINTERNAL; + } + ripple::Blob wasmBytes = hookSetObj->get().getFieldVL(sfCreateCode); @@ -1611,6 +1649,14 @@ SetHook::setHook() slesToUpdate.emplace(*oldDefKeylet, oldDefSLE); } + // override instruction count with fee for js + if (hookApiVersion == 1) + { + uint64_t fee = hookSetObj->get().getFieldAmount(sfFee).xrp().drops(); + maxInstrCountHook = fee; + maxInstrCountCbak = fee; // RH TODO: add a second fee for cbak? + } + auto newHookDef = std::make_shared(keylet); newHookDef->setFieldH256(sfHookHash, *createHookHash); newHookDef->setFieldH256(sfHookOn, *newHookOn); @@ -1620,9 +1666,7 @@ SetHook::setHook() hookSetObj->get().isFieldPresent(sfHookParameters) ? hookSetObj->get().getFieldArray(sfHookParameters) : STArray{}); - newHookDef->setFieldU16( - sfHookApiVersion, - hookSetObj->get().getFieldU16(sfHookApiVersion)); + newHookDef->setFieldU16(sfHookApiVersion, hookApiVersion); newHookDef->setFieldVL(sfCreateCode, wasmBytes); newHookDef->setFieldH256( sfHookSetTxnID, ctx.tx.getTransactionID()); diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 8ea8dee1d..9ea8503ab 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -1244,6 +1244,8 @@ Transactor::executeHookChain( uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion); + uint32_t fee = (uint32_t)(hookDef->getFieldAmount(sfFee).xrp().drops()); + try { results.push_back(hook::apply( @@ -1262,7 +1264,8 @@ Transactor::executeHookChain( strong, (strong ? 0 : 1UL), // 0 = strong, 1 = weak hook_no - 1, - provisionalMeta)); + provisionalMeta, + fee)); executedHookCount_++; @@ -1379,6 +1382,8 @@ Transactor::doHookCallback( ? hookObj.getFieldH256(sfHookNamespace) : hookDef->getFieldH256(sfHookNamespace)); + uint64_t instructionLimit = hookDef->getFieldAmount(sfFee).xrp().drops(); + std::map, std::vector> parameters; if (hook::gatherHookParameters(hookDef, hookObj, parameters, j_)) { @@ -1413,7 +1418,8 @@ Transactor::doHookCallback( ? 1UL : 0UL, hook_no - 1, - provisionalMeta); + provisionalMeta, + instructionLimit); executedHookCount_++; @@ -1658,6 +1664,8 @@ Transactor::doAgainAsWeak( return; } + uint32_t instructionLimit = (uint32_t)(hookDef->getFieldAmount(sfFee).xrp().drops()); + try { hook::HookResult aawResult = hook::apply( @@ -1676,7 +1684,8 @@ Transactor::doAgainAsWeak( false, 2UL, // param 2 = aaw hook_no - 1, - provisionalMeta); + provisionalMeta, + instructionLimit); executedHookCount_++; diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index e52d6ff8f..a14e1e895 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -101,6 +101,7 @@ InnerObjectFormats::InnerObjectFormats() {sfHookParameters, soeOPTIONAL}, {sfHookOn, soeOPTIONAL}, {sfHookApiVersion, soeOPTIONAL}, + {sfFee, soeOPTIONAL}, {sfFlags, soeOPTIONAL}}); add(sfHookGrant.jsonName.c_str(),