Skip to content

Commit

Permalink
Ensure DynamicJSEndpointRegistry can control its own runtime options (
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyashton authored Jun 4, 2024
1 parent 78796c8 commit cf1c6e2
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 87 deletions.
3 changes: 2 additions & 1 deletion include/ccf/js/core/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <chrono>
#include <quickjs/quickjs-exports.h>
#include <quickjs/quickjs.h>
#include <span>

// Forward declarations
namespace ccf
Expand Down Expand Up @@ -161,7 +162,7 @@ namespace ccf::js::core
JSWrappedValue call_with_rt_options(
const JSWrappedValue& f,
const std::vector<JSWrappedValue>& argv,
kv::Tx* tx,
const std::optional<ccf::JSRuntimeOptions>& options,
RuntimeLimitsPolicy policy);

// Call a JS function _without_ any stack, heap or execution time limits.
Expand Down
19 changes: 10 additions & 9 deletions include/ccf/js/core/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/tx.h"
#include "ccf/service/tables/jsengine.h"

#include <chrono>
#include <quickjs/quickjs.h>
Expand All @@ -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();
Expand All @@ -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<ccf::JSRuntimeOptions>& options_opt,
RuntimeLimitsPolicy policy);

std::chrono::milliseconds get_max_exec_time() const
{
Expand Down
1 change: 1 addition & 0 deletions include/ccf/js/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(ccf::js::core::Context&)>;

Expand Down
23 changes: 17 additions & 6 deletions include/ccf/service/tables/jsengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 11 additions & 7 deletions src/apps/js_generic/js_generic_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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::JSEngine>(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)
Expand All @@ -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("<no trace>"));
}

if (rt.return_exception_details)
if (options.return_exception_details)
{
std::vector<nlohmann::json> details = {
ODataJSExceptionDetails{ccf::errors::JSException, reason, trace}};
Expand Down Expand Up @@ -326,15 +330,15 @@ 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:{} {}",
reason,
trace.value_or("<no trace>"));
}

if (rt.return_exception_details)
if (options.return_exception_details)
{
std::vector<nlohmann::json> details = {
ODataJSExceptionDetails{
Expand Down Expand Up @@ -363,15 +367,15 @@ 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:{} {}",
reason,
trace.value_or("<no trace>"));
}

if (rt.return_exception_details)
if (options.return_exception_details)
{
std::vector<nlohmann::json> details = {ODataJSExceptionDetails{
ccf::errors::JSException, reason, trace}};
Expand Down
5 changes: 3 additions & 2 deletions src/js/core/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -445,17 +445,18 @@ namespace ccf::js::core
JSWrappedValue Context::call_with_rt_options(
const JSWrappedValue& f,
const std::vector<JSWrappedValue>& argv,
kv::Tx* tx,
const std::optional<ccf::JSRuntimeOptions>& 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();
JS_SetInterruptHandler(rt, js_custom_interrupt_handler, &interrupt_data);

auto rv = inner_call(f, argv);

JS_SetInterruptHandler(rt, NULL, NULL);
rt.reset_runtime_options();

return rv;
Expand Down
60 changes: 30 additions & 30 deletions src/js/core/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <vector>
Expand Down Expand Up @@ -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<ccf::JSRuntimeOptions>& 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::JSEngine>(ccf::Tables::JSENGINE);
const std::optional<JSRuntimeOptions> 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;
}
}
4 changes: 3 additions & 1 deletion src/js/extensions/ccf/gov_effects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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::JSEngine>(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::ModulesQuickJsVersion>(ccf::Tables::MODULES_QUICKJS_VERSION);
Expand Down
24 changes: 15 additions & 9 deletions src/js/registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ccf::JSEngine>(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)
Expand All @@ -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("<no trace>"));
}

if (rt.return_exception_details)
if (options.return_exception_details)
{
std::vector<nlohmann::json> details = {ccf::ODataJSExceptionDetails{
ccf::errors::JSException, reason, trace}};
Expand Down Expand Up @@ -257,15 +261,15 @@ 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:{} {}",
reason,
trace.value_or("<no trace>"));
}

if (rt.return_exception_details)
if (options.return_exception_details)
{
std::vector<nlohmann::json> details = {
ccf::ODataJSExceptionDetails{
Expand Down Expand Up @@ -294,15 +298,15 @@ 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:{} {}",
reason,
trace.value_or("<no trace>"));
}

if (rt.return_exception_details)
if (options.return_exception_details)
{
std::vector<nlohmann::json> details = {
ccf::ODataJSExceptionDetails{
Expand Down Expand Up @@ -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<ccf::js::AbstractInterpreterCache>();
Expand Down Expand Up @@ -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<ccf::JSEngine>(runtime_options_map)->get(),
ccf::js::core::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

auto quickjs_version =
ctx.tx.wo<ccf::ModulesQuickJsVersion>(modules_quickjs_version_map);
Expand Down
Loading

0 comments on commit cf1c6e2

Please sign in to comment.