From cf1c6e23d0a7b3daa1481af3b4f071c0753271d6 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Tue, 4 Jun 2024 17:39:42 +0100 Subject: [PATCH] Ensure `DynamicJSEndpointRegistry` can control its own runtime options (#6224) --- include/ccf/js/core/context.h | 3 +- include/ccf/js/core/runtime.h | 19 ++++---- include/ccf/js/registry.h | 1 + include/ccf/service/tables/jsengine.h | 23 +++++++--- src/apps/js_generic/js_generic_base.cpp | 18 +++++--- src/js/core/context.cpp | 5 ++- src/js/core/runtime.cpp | 60 ++++++++++++------------- src/js/extensions/ccf/gov_effects.cpp | 4 +- src/js/registry.cpp | 24 ++++++---- src/node/gov/handlers/proposals.h | 8 ++-- src/node/rpc/member_frontend.h | 13 +++--- src/node/rpc/node_frontend.h | 18 +++----- 12 files changed, 109 insertions(+), 87 deletions(-) diff --git a/include/ccf/js/core/context.h b/include/ccf/js/core/context.h index a08c7d6b05d2..2ac142a567fe 100644 --- a/include/ccf/js/core/context.h +++ b/include/ccf/js/core/context.h @@ -12,6 +12,7 @@ #include #include #include +#include // Forward declarations namespace ccf @@ -161,7 +162,7 @@ namespace ccf::js::core JSWrappedValue call_with_rt_options( const JSWrappedValue& f, const std::vector& argv, - kv::Tx* tx, + const std::optional& options, RuntimeLimitsPolicy policy); // Call a JS function _without_ any stack, heap or execution time limits. diff --git a/include/ccf/js/core/runtime.h b/include/ccf/js/core/runtime.h index 809b6f79b2ee..ad56c8aa250b 100644 --- a/include/ccf/js/core/runtime.h +++ b/include/ccf/js/core/runtime.h @@ -2,7 +2,7 @@ // Licensed under the Apache 2.0 License. #pragma once -#include "ccf/tx.h" +#include "ccf/service/tables/jsengine.h" #include #include @@ -19,17 +19,16 @@ namespace ccf::js::core { JSRuntime* rt = nullptr; - std::chrono::milliseconds max_exec_time = default_max_execution_time; + std::chrono::milliseconds max_exec_time{ + ccf::JSRuntimeOptions::Defaults::max_execution_time_ms}; void add_ccf_classdefs(); public: - static constexpr std::chrono::milliseconds default_max_execution_time{1000}; - static constexpr size_t default_stack_size = 1024 * 1024; - static constexpr size_t default_heap_size = 100 * 1024 * 1024; - - bool log_exception_details = false; - bool return_exception_details = false; + bool log_exception_details = + ccf::JSRuntimeOptions::Defaults::log_exception_details; + bool return_exception_details = + ccf::JSRuntimeOptions::Defaults::return_exception_details; Runtime(); ~Runtime(); @@ -40,7 +39,9 @@ namespace ccf::js::core } void reset_runtime_options(); - void set_runtime_options(kv::Tx* tx, RuntimeLimitsPolicy policy); + void set_runtime_options( + const std::optional& options_opt, + RuntimeLimitsPolicy policy); std::chrono::milliseconds get_max_exec_time() const { diff --git a/include/ccf/js/registry.h b/include/ccf/js/registry.h index a6869f55d829..d7ef63224e54 100644 --- a/include/ccf/js/registry.h +++ b/include/ccf/js/registry.h @@ -49,6 +49,7 @@ namespace ccf::js std::string interpreter_flush_map; std::string modules_quickjs_version_map; std::string modules_quickjs_bytecode_map; + std::string runtime_options_map; using PreExecutionHook = std::function; diff --git a/include/ccf/service/tables/jsengine.h b/include/ccf/service/tables/jsengine.h index 06e74e344790..d5779cbf9ae9 100644 --- a/include/ccf/service/tables/jsengine.h +++ b/include/ccf/service/tables/jsengine.h @@ -2,29 +2,40 @@ // Licensed under the Apache 2.0 License. #pragma once +#include "ccf/ds/json.h" #include "ccf/service/map.h" namespace ccf { struct JSRuntimeOptions { + struct Defaults + { + static constexpr size_t max_heap_bytes = 100 * 1024 * 1024; + static constexpr size_t max_stack_bytes = 1024 * 1024; + static constexpr uint64_t max_execution_time_ms = 1000; + static constexpr bool log_exception_details = false; + static constexpr bool return_exception_details = false; + static constexpr size_t max_cached_interpreters = 10; + }; + /// @brief heap size for QuickJS runtime - size_t max_heap_bytes; + size_t max_heap_bytes = Defaults::max_heap_bytes; /// @brief stack size for QuickJS runtime - size_t max_stack_bytes; + size_t max_stack_bytes = Defaults::max_stack_bytes; /// @brief max execution time for QuickJS - uint64_t max_execution_time_ms; + uint64_t max_execution_time_ms = Defaults::max_execution_time_ms; /// @brief emit exception details to the log /// NOTE: this is a security risk as it may leak sensitive information /// to anyone with access to the application log, which is /// unprotected. - bool log_exception_details = false; + bool log_exception_details = Defaults::log_exception_details; /// @brief return exception details in the response /// NOTE: this is a security risk as it may leak sensitive information, /// albeit to the caller only. - bool return_exception_details = false; + bool return_exception_details = Defaults::return_exception_details; /// @brief how many interpreters may be cached in-memory for future reuse - size_t max_cached_interpreters = 10; + size_t max_cached_interpreters = Defaults::max_cached_interpreters; }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JSRuntimeOptions) diff --git a/src/apps/js_generic/js_generic_base.cpp b/src/apps/js_generic/js_generic_base.cpp index bfc3292d470c..8d8ab27f26c1 100644 --- a/src/apps/js_generic/js_generic_base.cpp +++ b/src/apps/js_generic/js_generic_base.cpp @@ -213,10 +213,14 @@ namespace ccfapp auto request = request_extension->create_request_obj( ctx, endpoint->full_uri_path, endpoint_ctx, this); + auto options = endpoint_ctx.tx.ro(ccf::Tables::JSENGINE) + ->get() + .value_or(ccf::JSRuntimeOptions()); + auto val = ctx.call_with_rt_options( export_func, {request}, - &endpoint_ctx.tx, + options, ccf::js::core::RuntimeLimitsPolicy::NONE); for (auto extension : local_extensions) @@ -239,12 +243,12 @@ namespace ccfapp auto [reason, trace] = ctx.error_message(); - if (rt.log_exception_details) + if (options.log_exception_details) { CCF_APP_FAIL("{}: {}", reason, trace.value_or("")); } - if (rt.return_exception_details) + if (options.return_exception_details) { std::vector details = { ODataJSExceptionDetails{ccf::errors::JSException, reason, trace}}; @@ -326,7 +330,7 @@ namespace ccfapp { auto [reason, trace] = ctx.error_message(); - if (rt.log_exception_details) + if (options.log_exception_details) { CCF_APP_FAIL( "Failed to convert return value to JSON:{} {}", @@ -334,7 +338,7 @@ namespace ccfapp trace.value_or("")); } - if (rt.return_exception_details) + if (options.return_exception_details) { std::vector details = { ODataJSExceptionDetails{ @@ -363,7 +367,7 @@ namespace ccfapp { auto [reason, trace] = ctx.error_message(); - if (rt.log_exception_details) + if (options.log_exception_details) { CCF_APP_FAIL( "Failed to convert return value to JSON:{} {}", @@ -371,7 +375,7 @@ namespace ccfapp trace.value_or("")); } - if (rt.return_exception_details) + if (options.return_exception_details) { std::vector details = {ODataJSExceptionDetails{ ccf::errors::JSException, reason, trace}}; diff --git a/src/js/core/context.cpp b/src/js/core/context.cpp index 280291930002..86f93ec7de42 100644 --- a/src/js/core/context.cpp +++ b/src/js/core/context.cpp @@ -445,10 +445,10 @@ namespace ccf::js::core JSWrappedValue Context::call_with_rt_options( const JSWrappedValue& f, const std::vector& argv, - kv::Tx* tx, + const std::optional& options, RuntimeLimitsPolicy policy) { - rt.set_runtime_options(tx, policy); + rt.set_runtime_options(options, policy); const auto curr_time = ccf::get_enclave_time(); interrupt_data.start_time = curr_time; interrupt_data.max_execution_time = rt.get_max_exec_time(); @@ -456,6 +456,7 @@ namespace ccf::js::core auto rv = inner_call(f, argv); + JS_SetInterruptHandler(rt, NULL, NULL); rt.reset_runtime_options(); return rv; diff --git a/src/js/core/runtime.cpp b/src/js/core/runtime.cpp index 159de74428dc..df74a4e638f1 100644 --- a/src/js/core/runtime.cpp +++ b/src/js/core/runtime.cpp @@ -3,8 +3,6 @@ #include "ccf/js/core/runtime.h" -#include "ccf/service/tables/jsengine.h" -#include "ccf/tx.h" #include "js/global_class_ids.h" #include @@ -46,41 +44,43 @@ namespace ccf::js::core void Runtime::reset_runtime_options() { - JS_SetMaxStackSize(rt, 0); + using Defaults = ccf::JSRuntimeOptions::Defaults; + JS_SetMemoryLimit(rt, -1); - JS_SetInterruptHandler(rt, NULL, NULL); + JS_SetMaxStackSize(rt, 0); + + this->max_exec_time = + std::chrono::milliseconds{Defaults::max_execution_time_ms}; } - void Runtime::set_runtime_options(kv::Tx* tx, RuntimeLimitsPolicy policy) + void Runtime::set_runtime_options( + const std::optional& options_opt, + RuntimeLimitsPolicy policy) { - size_t stack_size = default_stack_size; - size_t heap_size = default_heap_size; + using Defaults = ccf::JSRuntimeOptions::Defaults; - const auto jsengine = tx->ro(ccf::Tables::JSENGINE); - const std::optional js_runtime_options = jsengine->get(); + ccf::JSRuntimeOptions js_runtime_options = + options_opt.value_or(ccf::JSRuntimeOptions{}); - if (js_runtime_options.has_value()) - { - bool no_lower_than_defaults = - policy == RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS; - - heap_size = std::max( - js_runtime_options.value().max_heap_bytes, - no_lower_than_defaults ? default_heap_size : 0); - stack_size = std::max( - js_runtime_options.value().max_stack_bytes, - no_lower_than_defaults ? default_stack_size : 0); - max_exec_time = std::max( - std::chrono::milliseconds{ - js_runtime_options.value().max_execution_time_ms}, - no_lower_than_defaults ? default_max_execution_time : - std::chrono::milliseconds{0}); - log_exception_details = js_runtime_options.value().log_exception_details; - return_exception_details = - js_runtime_options.value().return_exception_details; - } + bool no_lower_than_defaults = + policy == RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS; - JS_SetMaxStackSize(rt, stack_size); + auto heap_size = std::max( + js_runtime_options.max_heap_bytes, + no_lower_than_defaults ? Defaults::max_heap_bytes : 0); JS_SetMemoryLimit(rt, heap_size); + + auto stack_size = std::max( + js_runtime_options.max_stack_bytes, + no_lower_than_defaults ? Defaults::max_stack_bytes : 0); + JS_SetMaxStackSize(rt, stack_size); + + this->max_exec_time = std::chrono::milliseconds{std::max( + js_runtime_options.max_execution_time_ms, + no_lower_than_defaults ? Defaults::max_execution_time_ms : 0)}; + + this->log_exception_details = js_runtime_options.log_exception_details; + this->return_exception_details = + js_runtime_options.return_exception_details; } } diff --git a/src/js/extensions/ccf/gov_effects.cpp b/src/js/extensions/ccf/gov_effects.cpp index 6706dece4b09..978fb72ba934 100644 --- a/src/js/extensions/ccf/gov_effects.cpp +++ b/src/js/extensions/ccf/gov_effects.cpp @@ -43,8 +43,10 @@ namespace ccf::js::extensions auto& tx = *tx_ptr; js::core::Context ctx2(js::TxAccess::APP_RW); + const auto options_handle = tx.ro(ccf::Tables::JSENGINE); ctx2.runtime().set_runtime_options( - tx_ptr, js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); + options_handle->get(), + js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); auto quickjs_version = tx.wo(ccf::Tables::MODULES_QUICKJS_VERSION); diff --git a/src/js/registry.cpp b/src/js/registry.cpp index 5c99dc12f69f..bd714034ef7e 100644 --- a/src/js/registry.cpp +++ b/src/js/registry.cpp @@ -146,10 +146,14 @@ namespace ccf::js auto request = request_extension->create_request_obj( ctx, endpoint->full_uri_path, endpoint_ctx, this); + auto options = endpoint_ctx.tx.ro(runtime_options_map) + ->get() + .value_or(ccf::JSRuntimeOptions()); + auto val = ctx.call_with_rt_options( export_func, {request}, - &endpoint_ctx.tx, + options, ccf::js::core::RuntimeLimitsPolicy::NONE); for (auto extension : local_extensions) @@ -170,12 +174,12 @@ namespace ccf::js auto [reason, trace] = ctx.error_message(); - if (rt.log_exception_details) + if (options.log_exception_details) { CCF_APP_FAIL("{}: {}", reason, trace.value_or("")); } - if (rt.return_exception_details) + if (options.return_exception_details) { std::vector details = {ccf::ODataJSExceptionDetails{ ccf::errors::JSException, reason, trace}}; @@ -257,7 +261,7 @@ namespace ccf::js { auto [reason, trace] = ctx.error_message(); - if (rt.log_exception_details) + if (options.log_exception_details) { CCF_APP_FAIL( "Failed to convert return value to JSON:{} {}", @@ -265,7 +269,7 @@ namespace ccf::js trace.value_or("")); } - if (rt.return_exception_details) + if (options.return_exception_details) { std::vector details = { ccf::ODataJSExceptionDetails{ @@ -294,7 +298,7 @@ namespace ccf::js { auto [reason, trace] = ctx.error_message(); - if (rt.log_exception_details) + if (options.log_exception_details) { CCF_APP_FAIL( "Failed to convert return value to JSON:{} {}", @@ -302,7 +306,7 @@ namespace ccf::js trace.value_or("")); } - if (rt.return_exception_details) + if (options.return_exception_details) { std::vector details = { ccf::ODataJSExceptionDetails{ @@ -408,7 +412,8 @@ namespace ccf::js modules_quickjs_version_map( fmt::format("{}.modules_quickjs_version", kv_prefix)), modules_quickjs_bytecode_map( - fmt::format("{}.modules_quickjs_bytecode", kv_prefix)) + fmt::format("{}.modules_quickjs_bytecode", kv_prefix)), + runtime_options_map(fmt::format("{}.runtime_options", kv_prefix)) { interpreter_cache = context.get_subsystem(); @@ -491,7 +496,8 @@ namespace ccf::js // Refresh app bytecode ccf::js::core::Context jsctx(ccf::js::TxAccess::APP_RW); jsctx.runtime().set_runtime_options( - &ctx.tx, ccf::js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); + ctx.tx.ro(runtime_options_map)->get(), + ccf::js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); auto quickjs_version = ctx.tx.wo(modules_quickjs_version_map); diff --git a/src/node/gov/handlers/proposals.h b/src/node/gov/handlers/proposals.h index 25b1c221c52a..5c773f96f82a 100644 --- a/src/node/gov/handlers/proposals.h +++ b/src/node/gov/handlers/proposals.h @@ -169,7 +169,7 @@ namespace ccf::gov::endpoints auto val = js_context.call_with_rt_options( ballot_func, argv, - &tx, + tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); if (!val.is_exception()) @@ -231,7 +231,7 @@ namespace ccf::gov::endpoints auto val = js_context.call_with_rt_options( resolve_func, argv, - &tx, + tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); if (val.is_exception()) @@ -320,7 +320,7 @@ namespace ccf::gov::endpoints auto val = js_context.call_with_rt_options( apply_func, argv, - &tx, + tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); if (val.is_exception()) @@ -456,7 +456,7 @@ namespace ccf::gov::endpoints auto validate_result = context.call_with_rt_options( validate_func, {proposal_arg}, - &ctx.tx, + ctx.tx.template ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); // Handle error cases of validation diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 287c0bb74d6d..7b3b6f7c8aef 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -175,7 +175,7 @@ namespace ccf auto val = context.call_with_rt_options( ballot_func, argv, - &tx, + tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); if (!val.is_exception()) @@ -231,7 +231,7 @@ namespace ccf auto val = js_context.call_with_rt_options( resolve_func, argv, - &tx, + tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); std::optional failure = std::nullopt; @@ -330,7 +330,7 @@ namespace ccf auto apply_val = apply_js_context.call_with_rt_options( apply_func, apply_argv, - &tx, + tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); if (apply_val.is_exception()) @@ -1189,7 +1189,7 @@ namespace ccf auto val = context.call_with_rt_options( validate_func, {proposal}, - &ctx.tx, + ctx.tx.ro(ccf::Tables::JSENGINE)->get(), js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); if (val.is_exception()) @@ -1698,8 +1698,11 @@ namespace ccf { js::core::Context context(js::TxAccess::GOV_RO); + const auto options_handle = + ctx.tx.ro(ccf::Tables::JSENGINE); context.runtime().set_runtime_options( - &ctx.tx, js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); + options_handle->get(), + js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS); auto ballot_func = context.get_exported_function( params["ballot"], "vote", "body[\"ballot\"]"); } diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index d35a011590fe..522ecabb403b 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -1431,19 +1431,11 @@ namespace ccf m.bytecode_used = version_val->get() == std::string(ccf::quickjs_version); - auto js_engine_options = js_engine_map->get(); - m.max_stack_size = js::core::Runtime::default_stack_size; - m.max_heap_size = js::core::Runtime::default_heap_size; - m.max_execution_time = - js::core::Runtime::default_max_execution_time.count(); - if (js_engine_options.has_value()) - { - auto& options = js_engine_options.value(); - m.max_stack_size = options.max_stack_bytes; - m.max_heap_size = options.max_heap_bytes; - m.max_execution_time = options.max_execution_time_ms; - m.max_cached_interpreters = options.max_cached_interpreters; - } + auto options = js_engine_map->get().value_or(ccf::JSRuntimeOptions{}); + m.max_stack_size = options.max_stack_bytes; + m.max_heap_size = options.max_heap_bytes; + m.max_execution_time = options.max_execution_time_ms; + m.max_cached_interpreters = options.max_cached_interpreters; return m; };