Skip to content

Commit

Permalink
Only enable JS runtime limits during execution (microsoft#5730)
Browse files Browse the repository at this point in the history
  • Loading branch information
achamayou authored Oct 16, 2023
1 parent 5a0e0e5 commit c2f42d4
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .daily_canary
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-^- ___ ___
(- -) (= =) | Y & +--?
( V ) / . \ | +---=---'
/--x-m- /--n-n---xXx--/--yY------>>>----<<<
/--x-m- /--n-n---xXx--/--yY------>>>----<<<>>
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [5.0.0-dev5]

[5.0.0-dev5]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.0-dev5

- In governance contexts, JS runtimes now only use runtime limits from the [public:ccf.gov.js_runtime_options map](https://microsoft.github.io/CCF/main/audit/builtin_maps.html#js-runtime-options) if they are strictly higher than the defaults (#5730).
- Fixed an issue where a JS runtime limit could be hit out of user code execution, leading to an incorrectly constructed JS runtime or a crash (#5730).

## [5.0.0-dev4]

[5.0.0-dev4]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.0-dev4
Expand Down
10 changes: 8 additions & 2 deletions src/apps/js_generic/js_generic_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,9 @@ namespace ccfapp
// Update the top of the stack for the current thread, used by the stack
// guard Note this is only active outside SGX
JS_UpdateStackTop(ctx.runtime());
// Make the heap and stack limits safe while we init the runtime
ctx.runtime().reset_runtime_options();

ctx.runtime().set_runtime_options(&endpoint_ctx.tx);
JS_SetModuleLoaderFunc(
ctx.runtime(), nullptr, js::js_app_module_loader, &endpoint_ctx.tx);

Expand Down Expand Up @@ -353,7 +354,12 @@ namespace ccfapp

// Call exported function
auto request = create_request_obj(endpoint, endpoint_ctx, ctx);
auto val = ctx.call(export_func, {request});

auto val = ctx.call_with_rt_options(
export_func,
{request},
&endpoint_ctx.tx,
ccf::js::RuntimeLimitsPolicy::NONE);

if (JS_IsException(val))
{
Expand Down
55 changes: 44 additions & 11 deletions src/js/wrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "node/rpc/jwt_management.h"
#include "node/rpc/node_interface.h"

#include <algorithm>
#include <memory>
#include <quickjs/quickjs-exports.h>
#include <quickjs/quickjs.h>
Expand Down Expand Up @@ -137,7 +138,7 @@ namespace ccf::js
}
}

JSWrappedValue Context::call(
JSWrappedValue Context::inner_call(
const JSWrappedValue& f, const std::vector<js::JSWrappedValue>& argv)
{
std::vector<JSValue> argvn;
Expand All @@ -146,14 +147,28 @@ namespace ccf::js
{
argvn.push_back(a.val);
}

return W(JS_Call(
ctx, f, ccf::js::constants::Undefined, argv.size(), argvn.data()));
}

JSWrappedValue Context::call_with_rt_options(
const JSWrappedValue& f,
const std::vector<js::JSWrappedValue>& argv,
kv::Tx* tx,
RuntimeLimitsPolicy policy)
{
rt.set_runtime_options(tx, 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();
interrupt_data.access = access;
JS_SetInterruptHandler(rt, js_custom_interrupt_handler, &interrupt_data);

return W(JS_Call(
ctx, f, ccf::js::constants::Undefined, argv.size(), argvn.data()));
auto rv = inner_call(f, argv);

rt.reset_runtime_options();

return rv;
}

Runtime::Runtime()
Expand Down Expand Up @@ -388,7 +403,7 @@ namespace ccf::js
jsctx.new_array_buffer_copy(k.data(), k.size()),
obj};

auto val = jsctx.call(func, args);
auto val = jsctx.inner_call(func, args);

if (JS_IsException(val))
{
Expand Down Expand Up @@ -1483,7 +1498,8 @@ namespace ccf::js
auto& tx = *tx_ctx_ptr->tx;

js::Context ctx2(js::TxAccess::APP);
ctx2.runtime().set_runtime_options(tx_ctx_ptr->tx);
ctx2.runtime().set_runtime_options(
tx_ctx_ptr->tx, js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);
JS_SetModuleLoaderFunc(
ctx2.runtime(), nullptr, js::js_app_module_loader, &tx);

Expand Down Expand Up @@ -2355,7 +2371,14 @@ namespace ccf::js
}
}

void Runtime::set_runtime_options(kv::Tx* tx)
void Runtime::reset_runtime_options()
{
JS_SetMaxStackSize(rt, 0);
JS_SetMemoryLimit(rt, -1);
JS_SetInterruptHandler(rt, NULL, NULL);
}

void Runtime::set_runtime_options(kv::Tx* tx, RuntimeLimitsPolicy policy)
{
size_t stack_size = default_stack_size;
size_t heap_size = default_heap_size;
Expand All @@ -2365,10 +2388,20 @@ namespace ccf::js

if (js_runtime_options.has_value())
{
heap_size = js_runtime_options.value().max_heap_bytes;
stack_size = js_runtime_options.value().max_stack_bytes;
max_exec_time = std::chrono::milliseconds{
js_runtime_options.value().max_execution_time_ms};
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;
Expand Down
23 changes: 21 additions & 2 deletions src/js/wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ namespace ccf::js
const size_t default_stack_size = 1024 * 1024;
const size_t default_heap_size = 100 * 1024 * 1024;

enum class RuntimeLimitsPolicy
{
NONE,
NO_LOWER_THAN_DEFAULTS
};

/// Describes the context in which JS script is currently executing. Used to
/// determine which KV tables should be accessible.
enum class TxAccess
Expand Down Expand Up @@ -224,6 +230,8 @@ namespace ccf::js
void js_dump_error(JSContext* ctx);
std::pair<std::string, std::optional<std::string>> js_error_message(
Context& ctx);
std::pair<std::string, std::optional<std::string>> js_error_message_from_val(
Context& ctx, JSWrappedValue& exc);

JSValue js_body_text(
JSContext* ctx,
Expand Down Expand Up @@ -276,7 +284,8 @@ namespace ccf::js
return rt;
}

void set_runtime_options(kv::Tx* tx);
void reset_runtime_options();
void set_runtime_options(kv::Tx* tx, RuntimeLimitsPolicy policy);

std::chrono::milliseconds get_max_exec_time() const
{
Expand Down Expand Up @@ -517,7 +526,17 @@ namespace ccf::js
return W(JS_GetException(ctx));
}

JSWrappedValue call(
JSWrappedValue call_with_rt_options(
const JSWrappedValue& f,
const std::vector<js::JSWrappedValue>& argv,
kv::Tx* tx,
RuntimeLimitsPolicy policy);

// Call a JS function _without_ any stack, heap or execution time limits.
// Only to be used, as the name indicates, for calls inside an already
// invoked JS function, where the caller has already set up the necessary
// limits.
JSWrappedValue inner_call(
const JSWrappedValue& f, const std::vector<js::JSWrappedValue>& argv);

JSWrappedValue parse_json(const nlohmann::json& j) const
Expand Down
29 changes: 21 additions & 8 deletions src/node/gov/handlers/proposals.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ namespace ccf::gov::endpoints
for (const auto& [mid, mb] : proposal_info.ballots)
{
js::Context js_context(js::TxAccess::GOV_RO);
js_context.runtime().set_runtime_options(&tx);
js::TxContext txctx{&tx};
js::populate_global_ccf_kv(&txctx, js_context);
auto ballot_func = js_context.function(
Expand All @@ -156,7 +155,12 @@ namespace ccf::gov::endpoints
proposal_info.proposer_id.data(),
proposal_info.proposer_id.size())};

auto val = js_context.call(ballot_func, argv);
auto val = js_context.call_with_rt_options(
ballot_func,
argv,
&tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

if (!JS_IsException(val))
{
votes.emplace_back(mid, JS_ToBool(js_context, val));
Expand All @@ -182,7 +186,6 @@ namespace ccf::gov::endpoints
{
{
js::Context js_context(js::TxAccess::GOV_RO);
js_context.runtime().set_runtime_options(&tx);
js::TxContext txctx{&tx};
js::populate_global_ccf_kv(&txctx, js_context);
auto resolve_func = js_context.function(
Expand Down Expand Up @@ -213,7 +216,11 @@ namespace ccf::gov::endpoints
}
argv.push_back(vs);

auto val = js_context.call(resolve_func, argv);
auto val = js_context.call_with_rt_options(
resolve_func,
argv,
&tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

if (JS_IsException(val))
{
Expand Down Expand Up @@ -267,7 +274,6 @@ namespace ccf::gov::endpoints
{
// Evaluate apply function
js::Context js_context(js::TxAccess::GOV_RW);
js_context.runtime().set_runtime_options(&tx);
js::TxContext txctx{&tx};

auto gov_effects =
Expand All @@ -292,7 +298,11 @@ namespace ccf::gov::endpoints
js_context.new_string_len(
proposal_id.c_str(), proposal_id.size())};

auto val = js_context.call(apply_func, argv);
auto val = js_context.call_with_rt_options(
apply_func,
argv,
&tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

if (JS_IsException(val))
{
Expand Down Expand Up @@ -438,7 +448,6 @@ namespace ccf::gov::endpoints
}

js::Context context(js::TxAccess::GOV_RO);
context.runtime().set_runtime_options(&ctx.tx);
js::TxContext txctx{&ctx.tx};
js::populate_global_ccf_kv(&txctx, context);

Expand All @@ -450,7 +459,11 @@ namespace ccf::gov::endpoints
proposal_body = cose_ident.content;
auto proposal_arg = context.new_string_len(
(const char*)proposal_body.data(), proposal_body.size());
auto validate_result = context.call(validate_func, {proposal_arg});
auto validate_result = context.call_with_rt_options(
validate_func,
{proposal_arg},
&ctx.tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

// Handle error cases of validation
{
Expand Down
32 changes: 23 additions & 9 deletions src/node/rpc/member_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ namespace ccf
for (const auto& [mid, mb] : pi_->ballots)
{
js::Context context(js::TxAccess::GOV_RO);
context.runtime().set_runtime_options(&tx);
js::TxContext txctx{&tx};
js::populate_global_ccf_kv(&txctx, context);
auto ballot_func = context.function(
Expand All @@ -163,7 +162,12 @@ namespace ccf
context.new_string_len(
pi_->proposer_id.data(), pi_->proposer_id.size())};

auto val = context.call(ballot_func, argv);
auto val = context.call_with_rt_options(
ballot_func,
argv,
&tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

if (!JS_IsException(val))
{
votes.emplace_back(mid, JS_ToBool(context, val));
Expand All @@ -187,7 +191,6 @@ namespace ccf

{
js::Context js_context(js::TxAccess::GOV_RO);
js_context.runtime().set_runtime_options(&tx);
js::TxContext txctx{&tx};
js::populate_global_ccf_kv(&txctx, js_context);
auto resolve_func = js_context.function(
Expand Down Expand Up @@ -216,7 +219,11 @@ namespace ccf
}
argv.push_back(vs);

auto val = js_context.call(resolve_func, argv);
auto val = js_context.call_with_rt_options(
resolve_func,
argv,
&tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

std::optional<jsgov::Failure> failure = std::nullopt;
if (JS_IsException(val))
Expand Down Expand Up @@ -284,7 +291,6 @@ namespace ccf
if (pi_.value().state == ProposalState::ACCEPTED)
{
js::Context apply_js_context(js::TxAccess::GOV_RW);
apply_js_context.runtime().set_runtime_options(&tx);

js::TxContext apply_txctx{&tx};

Expand All @@ -310,7 +316,11 @@ namespace ccf
apply_js_context.new_string_len(
proposal_id.c_str(), proposal_id.size())};

auto apply_val = apply_js_context.call(apply_func, apply_argv);
auto apply_val = apply_js_context.call_with_rt_options(
apply_func,
apply_argv,
&tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

if (JS_IsException(apply_val))
{
Expand Down Expand Up @@ -1151,7 +1161,6 @@ namespace ccf
auto validate_script = constitution.value();

js::Context context(js::TxAccess::GOV_RO);
context.runtime().set_runtime_options(&ctx.tx);
js::TxContext txctx{&ctx.tx};
js::populate_global_ccf_kv(&txctx, context);

Expand All @@ -1166,7 +1175,11 @@ namespace ccf
auto body_len = proposal_body.size();

auto proposal = context.new_string_len(body, body_len);
auto val = context.call(validate_func, {proposal});
auto val = context.call_with_rt_options(
validate_func,
{proposal},
&ctx.tx,
js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);

if (JS_IsException(val))
{
Expand Down Expand Up @@ -1677,7 +1690,8 @@ namespace ccf

{
js::Context context(js::TxAccess::GOV_RO);
context.runtime().set_runtime_options(&ctx.tx);
context.runtime().set_runtime_options(
&ctx.tx, js::RuntimeLimitsPolicy::NO_LOWER_THAN_DEFAULTS);
auto ballot_func =
context.function(params["ballot"], "vote", "body[\"ballot\"]");
}
Expand Down
Loading

0 comments on commit c2f42d4

Please sign in to comment.