Skip to content

Commit 728da79

Browse files
authored
Add property filter fallback mode to serializer (#336)
* Add property filter fallback mode to serializer For a JS value that cannot be serialized as-is by V8's serializer, clone and filter its properties, then try again. With this change, serialization should be possible for practically all JS values. * fixup! truffleruby
1 parent 9bfacdd commit 728da79

File tree

2 files changed

+116
-8
lines changed

2 files changed

+116
-8
lines changed

ext/mini_racer_extension/mini_racer_v8.cc

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,56 @@
1111
#include <cstring>
1212
#include <vector>
1313

14+
// note: the filter function gets called inside the safe context,
15+
// i.e., the context that has not been tampered with by user JS
16+
// convention: $-prefixed identifiers signify objects from the
17+
// user JS context and should be handled with special care
18+
static const char safe_context_script_source[] = R"js(
19+
;(function($globalThis) {
20+
const {Map: $Map, Set: $Set} = $globalThis
21+
const sentinel = {}
22+
return function filter(v) {
23+
if (typeof v === "function")
24+
return sentinel
25+
if (typeof v !== "object" || v === null)
26+
return v
27+
if (v instanceof $Map) {
28+
const m = new Map()
29+
for (let [k, t] of Map.prototype.entries.call(v)) {
30+
t = filter(t)
31+
if (t !== sentinel)
32+
m.set(k, t)
33+
}
34+
return m
35+
} else if (v instanceof $Set) {
36+
const s = new Set()
37+
for (let t of Set.prototype.values.call(v)) {
38+
t = filter(t)
39+
if (t !== sentinel)
40+
s.add(t)
41+
}
42+
return s
43+
} else {
44+
const o = Array.isArray(v) ? [] : {}
45+
const pds = Object.getOwnPropertyDescriptors(v)
46+
for (const [k, d] of Object.entries(pds)) {
47+
if (!d.enumerable)
48+
continue
49+
let t = d.value
50+
if (d.get) {
51+
// *not* d.get.call(...), may have been tampered with
52+
t = Function.prototype.call.call(d.get, v, k)
53+
}
54+
t = filter(t)
55+
if (t !== sentinel)
56+
Object.defineProperty(o, k, {value: t, enumerable: true})
57+
}
58+
return o
59+
}
60+
}
61+
})
62+
)js";
63+
1464
struct Callback
1565
{
1666
struct State *st;
@@ -32,8 +82,10 @@ struct State
3282
// extra context for when we need access to built-ins like Array
3383
// and want to be sure they haven't been tampered with by JS code
3484
v8::Local<v8::Context> safe_context;
35-
v8::Persistent<v8::Context> persistent_context; // single-thread mode only
36-
v8::Persistent<v8::Context> persistent_safe_context; // single-thread mode only
85+
v8::Local<v8::Function> safe_context_function;
86+
v8::Persistent<v8::Context> persistent_context; // single-thread mode only
87+
v8::Persistent<v8::Context> persistent_safe_context; // single-thread mode only
88+
v8::Persistent<v8::Function> persistent_safe_context_function; // single-thread mode only
3789
Context *ruby_context;
3890
int64_t max_memory;
3991
int err_reason;
@@ -73,6 +125,23 @@ struct Serialized
73125
// throws JS exception on serialization error
74126
bool reply(State& st, v8::Local<v8::Value> v)
75127
{
128+
v8::TryCatch try_catch(st.isolate);
129+
{
130+
Serialized serialized(st, v);
131+
if (serialized.data) {
132+
v8_reply(st.ruby_context, serialized.data, serialized.size);
133+
return true;
134+
}
135+
}
136+
if (!try_catch.CanContinue()) {
137+
try_catch.ReThrow();
138+
return false;
139+
}
140+
auto recv = v8::Undefined(st.isolate);
141+
if (!st.safe_context_function->Call(st.safe_context, recv, 1, &v).ToLocal(&v)) {
142+
try_catch.ReThrow();
143+
return false;
144+
}
76145
Serialized serialized(st, v);
77146
if (serialized.data)
78147
v8_reply(st.ruby_context, serialized.data, serialized.size);
@@ -240,7 +309,29 @@ extern "C" State *v8_thread_init(Context *c, const uint8_t *snapshot_buf,
240309
st.safe_context = v8::Context::New(st.isolate);
241310
st.context = v8::Context::New(st.isolate);
242311
v8::Context::Scope context_scope(st.context);
312+
{
313+
v8::Context::Scope context_scope(st.safe_context);
314+
auto source = v8::String::NewFromUtf8Literal(st.isolate, safe_context_script_source);
315+
auto filename = v8::String::NewFromUtf8Literal(st.isolate, "safe_context_script.js");
316+
v8::ScriptOrigin origin(filename);
317+
auto script =
318+
v8::Script::Compile(st.safe_context, source, &origin)
319+
.ToLocalChecked();
320+
auto function_v = script->Run(st.safe_context).ToLocalChecked();
321+
auto function = v8::Function::Cast(*function_v);
322+
auto recv = v8::Undefined(st.isolate);
323+
v8::Local<v8::Value> arg = st.context->Global();
324+
// grant the safe context access to the user context's globalThis
325+
st.safe_context->SetSecurityToken(st.context->GetSecurityToken());
326+
function_v =
327+
function->Call(st.safe_context, recv, 1, &arg)
328+
.ToLocalChecked();
329+
// revoke access again now that the script did its one-time setup
330+
st.safe_context->UseDefaultSecurityToken();
331+
st.safe_context_function = v8::Local<v8::Function>::Cast(function_v);
332+
}
243333
if (single_threaded) {
334+
st.persistent_safe_context_function.Reset(st.isolate, st.safe_context_function);
244335
st.persistent_safe_context.Reset(st.isolate, st.safe_context);
245336
st.persistent_context.Reset(st.isolate, st.context);
246337
return pst; // intentionally returning early and keeping alive
@@ -792,12 +883,14 @@ extern "C" void v8_single_threaded_enter(State *pst, Context *c, void (*f)(Conte
792883
v8::Isolate::Scope isolate_scope(st.isolate);
793884
v8::HandleScope handle_scope(st.isolate);
794885
{
886+
st.safe_context_function = v8::Local<v8::Function>::New(st.isolate, st.persistent_safe_context_function);
795887
st.safe_context = v8::Local<v8::Context>::New(st.isolate, st.persistent_safe_context);
796888
st.context = v8::Local<v8::Context>::New(st.isolate, st.persistent_context);
797889
v8::Context::Scope context_scope(st.context);
798890
f(c);
799891
st.context = v8::Local<v8::Context>();
800892
st.safe_context = v8::Local<v8::Context>();
893+
st.safe_context_function = v8::Local<v8::Function>();
801894
}
802895
}
803896

test/mini_racer_test.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ def test_wasm_ref
899899
skip "TruffleRuby does not support WebAssembly"
900900
end
901901
context = MiniRacer::Context.new
902-
expected = {"error" => "Error: [object Object] could not be cloned."}
902+
expected = {}
903903
actual = context.eval("
904904
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]
905905
var o = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array(b))).exports
@@ -1083,19 +1083,34 @@ def test_regexp_string_iterator
10831083
# TODO(bnoordhuis) maybe detect the iterator object and serialize
10841084
# it as a string or array of strings; problem is there is no V8 API
10851085
# to detect regexp string iterator objects
1086-
expected = {"error" => "Error: [object RegExp String Iterator] could not be cloned."}
1086+
expected = {}
10871087
assert_equal expected, context.eval("'abc'.matchAll(/./g)")
10881088
end
10891089

10901090
def test_function_property
10911091
context = MiniRacer::Context.new
10921092
if RUBY_ENGINE == "truffleruby"
1093-
expected = {"x" => 42}
1093+
expected = {
1094+
"m" => {1 => 2, 3 => 4},
1095+
"s" => {},
1096+
"x" => 42,
1097+
}
10941098
else
1095-
# regrettably loses the non-function properties
1096-
expected = {"error" => "Error: f() {} could not be cloned."}
1099+
expected = {
1100+
"m" => {"1" => 2, "3" => 4}, # TODO(bnoordhuis) retain numeric keys
1101+
"s" => [5, 7, 11, 13],
1102+
"x" => 42,
1103+
}
10971104
end
1098-
assert_equal expected, context.eval("({ x: 42, f() {} })")
1105+
script = <<~JS
1106+
({
1107+
f: () => {},
1108+
m: new Map([[1,2],[3,4]]),
1109+
s: new Set([5,7,11,13]),
1110+
x: 42,
1111+
})
1112+
JS
1113+
assert_equal expected, context.eval(script)
10991114
end
11001115

11011116
def test_string_encoding

0 commit comments

Comments
 (0)