Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions ext/mini_racer_extension/mini_racer_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,56 @@
#include <cstring>
#include <vector>

// note: the filter function gets called inside the safe context,
// i.e., the context that has not been tampered with by user JS
// convention: $-prefixed identifiers signify objects from the
// user JS context and should be handled with special care
static const char safe_context_script_source[] = R"js(
;(function($globalThis) {
const {Map: $Map, Set: $Set} = $globalThis
const sentinel = {}
return function filter(v) {
if (typeof v === "function")
return sentinel
if (typeof v !== "object" || v === null)
return v
if (v instanceof $Map) {
const m = new Map()
for (let [k, t] of Map.prototype.entries.call(v)) {
t = filter(t)
if (t !== sentinel)
m.set(k, t)
}
return m
} else if (v instanceof $Set) {
const s = new Set()
for (let t of Set.prototype.values.call(v)) {
t = filter(t)
if (t !== sentinel)
s.add(t)
}
return s
} else {
const o = Array.isArray(v) ? [] : {}
const pds = Object.getOwnPropertyDescriptors(v)
for (const [k, d] of Object.entries(pds)) {
if (!d.enumerable)
continue
let t = d.value
if (d.get) {
// *not* d.get.call(...), may have been tampered with
t = Function.prototype.call.call(d.get, v, k)
}
t = filter(t)
if (t !== sentinel)
Object.defineProperty(o, k, {value: t, enumerable: true})
}
return o
}
}
})
)js";

struct Callback
{
struct State *st;
Expand All @@ -32,8 +82,10 @@ struct State
// extra context for when we need access to built-ins like Array
// and want to be sure they haven't been tampered with by JS code
v8::Local<v8::Context> safe_context;
v8::Persistent<v8::Context> persistent_context; // single-thread mode only
v8::Persistent<v8::Context> persistent_safe_context; // single-thread mode only
v8::Local<v8::Function> safe_context_function;
v8::Persistent<v8::Context> persistent_context; // single-thread mode only
v8::Persistent<v8::Context> persistent_safe_context; // single-thread mode only
v8::Persistent<v8::Function> persistent_safe_context_function; // single-thread mode only
Context *ruby_context;
int64_t max_memory;
int err_reason;
Expand Down Expand Up @@ -73,6 +125,23 @@ struct Serialized
// throws JS exception on serialization error
bool reply(State& st, v8::Local<v8::Value> v)
{
v8::TryCatch try_catch(st.isolate);
{
Serialized serialized(st, v);
if (serialized.data) {
v8_reply(st.ruby_context, serialized.data, serialized.size);
return true;
}
}
if (!try_catch.CanContinue()) {
try_catch.ReThrow();
return false;
}
auto recv = v8::Undefined(st.isolate);
if (!st.safe_context_function->Call(st.safe_context, recv, 1, &v).ToLocal(&v)) {
try_catch.ReThrow();
return false;
}
Serialized serialized(st, v);
if (serialized.data)
v8_reply(st.ruby_context, serialized.data, serialized.size);
Expand Down Expand Up @@ -240,7 +309,29 @@ extern "C" State *v8_thread_init(Context *c, const uint8_t *snapshot_buf,
st.safe_context = v8::Context::New(st.isolate);
st.context = v8::Context::New(st.isolate);
v8::Context::Scope context_scope(st.context);
{
v8::Context::Scope context_scope(st.safe_context);
auto source = v8::String::NewFromUtf8Literal(st.isolate, safe_context_script_source);
auto filename = v8::String::NewFromUtf8Literal(st.isolate, "safe_context_script.js");
v8::ScriptOrigin origin(filename);
auto script =
v8::Script::Compile(st.safe_context, source, &origin)
.ToLocalChecked();
auto function_v = script->Run(st.safe_context).ToLocalChecked();
auto function = v8::Function::Cast(*function_v);
auto recv = v8::Undefined(st.isolate);
v8::Local<v8::Value> arg = st.context->Global();
// grant the safe context access to the user context's globalThis
st.safe_context->SetSecurityToken(st.context->GetSecurityToken());
function_v =
function->Call(st.safe_context, recv, 1, &arg)
.ToLocalChecked();
// revoke access again now that the script did its one-time setup
st.safe_context->UseDefaultSecurityToken();
st.safe_context_function = v8::Local<v8::Function>::Cast(function_v);
}
if (single_threaded) {
st.persistent_safe_context_function.Reset(st.isolate, st.safe_context_function);
st.persistent_safe_context.Reset(st.isolate, st.safe_context);
st.persistent_context.Reset(st.isolate, st.context);
return pst; // intentionally returning early and keeping alive
Expand Down Expand Up @@ -792,12 +883,14 @@ extern "C" void v8_single_threaded_enter(State *pst, Context *c, void (*f)(Conte
v8::Isolate::Scope isolate_scope(st.isolate);
v8::HandleScope handle_scope(st.isolate);
{
st.safe_context_function = v8::Local<v8::Function>::New(st.isolate, st.persistent_safe_context_function);
st.safe_context = v8::Local<v8::Context>::New(st.isolate, st.persistent_safe_context);
st.context = v8::Local<v8::Context>::New(st.isolate, st.persistent_context);
v8::Context::Scope context_scope(st.context);
f(c);
st.context = v8::Local<v8::Context>();
st.safe_context = v8::Local<v8::Context>();
st.safe_context_function = v8::Local<v8::Function>();
}
}

Expand Down
27 changes: 21 additions & 6 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ def test_wasm_ref
skip "TruffleRuby does not support WebAssembly"
end
context = MiniRacer::Context.new
expected = {"error" => "Error: [object Object] could not be cloned."}
expected = {}
actual = context.eval("
var b = [0,97,115,109,1,0,0,0,1,26,5,80,0,95,0,80,0,95,1,127,0,96,0,1,110,96,1,100,2,1,111,96,0,1,100,3,3,4,3,3,2,4,7,26,2,12,99,114,101,97,116,101,83,116,114,117,99,116,0,1,7,114,101,102,70,117,110,99,0,2,9,5,1,3,0,1,0,10,23,3,8,0,32,0,20,2,251,27,11,7,0,65,12,251,0,1,11,4,0,210,0,11,0,44,4,110,97,109,101,1,37,3,0,11,101,120,112,111,114,116,101,100,65,110,121,1,12,99,114,101,97,116,101,83,116,114,117,99,116,2,7,114,101,102,70,117,110,99]
var o = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array(b))).exports
Expand Down Expand Up @@ -1083,19 +1083,34 @@ def test_regexp_string_iterator
# TODO(bnoordhuis) maybe detect the iterator object and serialize
# it as a string or array of strings; problem is there is no V8 API
# to detect regexp string iterator objects
expected = {"error" => "Error: [object RegExp String Iterator] could not be cloned."}
expected = {}
assert_equal expected, context.eval("'abc'.matchAll(/./g)")
end

def test_function_property
context = MiniRacer::Context.new
if RUBY_ENGINE == "truffleruby"
expected = {"x" => 42}
expected = {
"m" => {1 => 2, 3 => 4},
"s" => {},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eregon fyi - seems like a bug

"x" => 42,
}
else
# regrettably loses the non-function properties
expected = {"error" => "Error: f() {} could not be cloned."}
expected = {
"m" => {"1" => 2, "3" => 4}, # TODO(bnoordhuis) retain numeric keys
"s" => [5, 7, 11, 13],
"x" => 42,
}
end
assert_equal expected, context.eval("({ x: 42, f() {} })")
script = <<~JS
({
f: () => {},
m: new Map([[1,2],[3,4]]),
s: new Set([5,7,11,13]),
x: 42,
})
JS
assert_equal expected, context.eval(script)
end

def test_string_encoding
Expand Down
Loading