Skip to content

Commit 3c5ea41

Browse files
committed
vm: Copy missing properties from context
This addresses a current shortcoming of the V8 SetNamedPropertyHandler function. It does not provide a way to intercept Object.defineProperty(..) calls. As a result, these properties are not copied onto the contextified sandbox when a new global property is added via either a function declaration or a Object.defineProperty(global, ...) call. Note that any function declarations or Object.defineProperty() globals that are created asynchronously (in a setTimeout, callback, etc.) will happen AFTER the call to copy properties, and thus not be caught. The way to properly fix this is to add some sort of a Object::SetNamedDefinePropertyHandler() function that takes a callback, which receives the property name and property descriptor as arguments. Luckily, such situations are rare, and asynchronously-added globals weren't supported by Node's VM module until 0.12 anyway. But, this should be fixed properly in V8, and this copy function should be removed once there is a better way. Fix #6416
1 parent 4c0195e commit 3c5ea41

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

src/node_contextify.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,79 @@ class ContextifyContext {
9696
}
9797

9898

99+
// XXX(isaacs): This function only exists because of a shortcoming of
100+
// the V8 SetNamedPropertyHandler function.
101+
//
102+
// It does not provide a way to intercept Object.defineProperty(..)
103+
// calls. As a result, these properties are not copied onto the
104+
// contextified sandbox when a new global property is added via either
105+
// a function declaration or a Object.defineProperty(global, ...) call.
106+
//
107+
// Note that any function declarations or Object.defineProperty()
108+
// globals that are created asynchronously (in a setTimeout, callback,
109+
// etc.) will happen AFTER the call to copy properties, and thus not be
110+
// caught.
111+
//
112+
// The way to properly fix this is to add some sort of a
113+
// Object::SetNamedDefinePropertyHandler() function that takes a callback,
114+
// which receives the property name and property descriptor as arguments.
115+
//
116+
// Luckily, such situations are rare, and asynchronously-added globals
117+
// weren't supported by Node's VM module until 0.12 anyway. But, this
118+
// should be fixed properly in V8, and this copy function should be
119+
// removed once there is a better way.
120+
void CopyProperties() {
121+
HandleScope scope(node_isolate);
122+
123+
Local<Context> context = PersistentToLocal(env()->isolate(), context_);
124+
Local<Object> global = context->Global()->GetPrototype()->ToObject();
125+
Local<Object> sandbox = PersistentToLocal(env()->isolate(), sandbox_);
126+
127+
Local<Function> clone_property_method;
128+
129+
Local<Array> names = global->GetOwnPropertyNames();
130+
int length = names->Length();
131+
for (int i = 0; i < length; i++) {
132+
Local<String> key = names->Get(i)->ToString();
133+
bool has = sandbox->HasOwnProperty(key);
134+
if (!has) {
135+
// Could also do this like so:
136+
//
137+
// PropertyAttribute att = global->GetPropertyAttributes(key_v);
138+
// Local<Value> val = global->Get(key_v);
139+
// sandbox->ForceSet(key_v, val, att);
140+
//
141+
// However, this doesn't handle ES6-style properties configured with
142+
// Object.defineProperty, and that's exactly what we're up against at
143+
// this point. ForceSet(key,val,att) only supports value properties
144+
// with the ES3-style attribute flags (DontDelete/DontEnum/ReadOnly),
145+
// which doesn't faithfully capture the full range of configurations
146+
// that can be done using Object.defineProperty.
147+
if (clone_property_method.IsEmpty()) {
148+
Local<String> code = FIXED_ONE_BYTE_STRING(node_isolate,
149+
"(function cloneProperty(source, key, target) {\n"
150+
" try {\n"
151+
" var desc = Object.getOwnPropertyDescriptor(source, key);\n"
152+
" if (desc.value === source) desc.value = target;\n"
153+
" Object.defineProperty(target, key, desc);\n"
154+
" } catch (e) {\n"
155+
" // Catch sealed properties errors\n"
156+
" }\n"
157+
"})");
158+
159+
Local<String> fname = FIXED_ONE_BYTE_STRING(node_isolate,
160+
"binding:script");
161+
Local<Script> script = Script::Compile(code, fname);
162+
clone_property_method = Local<Function>::Cast(script->Run());
163+
assert(clone_property_method->IsFunction());
164+
}
165+
Local<Value> args[] = { global, key, sandbox };
166+
clone_property_method->Call(global, ARRAY_SIZE(args), args);
167+
}
168+
}
169+
}
170+
171+
99172
// This is an object that just keeps an internal pointer to this
100173
// ContextifyContext. It's passed to the NamedPropertyHandler. If we
101174
// pass the main JavaScript context object we're embedded in, then the
@@ -421,6 +494,7 @@ class ContextifyScript : public WeakObject {
421494
display_errors,
422495
args,
423496
try_catch);
497+
contextify_context->CopyProperties();
424498
}
425499

426500
static int64_t GetTimeoutArg(const FunctionCallbackInfo<Value>& args,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var assert = require('assert');
24+
25+
var vm = require('vm');
26+
var o = vm.createContext({ console: console });
27+
28+
// This triggers the setter callback in node_contextify.cc
29+
var code = 'var a = function() {};\n';
30+
31+
// but this does not, since function decls are defineProperties,
32+
// not simple sets.
33+
code += 'function b(){}\n';
34+
35+
// Grab the global b function as the completion value, to ensure that
36+
// we are getting the global function, and not some other thing
37+
code += '(function(){return this})().b;\n'
38+
39+
var res = vm.runInContext(code, o, 'test');
40+
41+
assert.equal(typeof res, 'function', 'result should be function');
42+
assert.equal(res.name, 'b', 'res should be named b');
43+
assert.equal(typeof o.a, 'function', 'a should be function');
44+
assert.equal(typeof o.b, 'function', 'b should be function');
45+
assert.equal(res, o.b, 'result should be global b function');
46+
47+
console.log('ok');
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
var common = require('../common');
23+
var assert = require('assert');
24+
25+
var vm = require('vm');
26+
27+
var code =
28+
'Object.defineProperty(this, "f", {\n' +
29+
' get: function() { return x; },\n' +
30+
' set: function(k) { x = k; },\n' +
31+
' configurable: true,\n' +
32+
' enumerable: true\n' +
33+
'});\n' +
34+
'g = f;\n' +
35+
'f;\n';
36+
37+
var x = {};
38+
var o = vm.createContext({ console: console, x: x });
39+
40+
var res = vm.runInContext(code, o, 'test');
41+
42+
assert(res);
43+
assert.equal(typeof res, 'object');
44+
assert.equal(res, x);
45+
assert.equal(o.f, res);
46+
assert.deepEqual(Object.keys(o), ['console', 'x', 'g', 'f']);

0 commit comments

Comments
 (0)