diff --git a/lib/bridge.js b/lib/bridge.js index f21913a..bae700d 100644 --- a/lib/bridge.js +++ b/lib/bridge.js @@ -311,7 +311,7 @@ function createBridge(otherInit, registerProxy) { try { return otherReflectApply(otherObjectHasOwnProperty, object, [key]) === true; } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } } @@ -324,7 +324,7 @@ function createBridge(otherInit, registerProxy) { try { ret = otherReflectApply(getter, object, [key]); } catch (e) { - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } } else { ret = desc.value; @@ -338,7 +338,7 @@ function createBridge(otherInit, registerProxy) { try { to[key] = otherFromThis(from[key]); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } return true; } @@ -366,7 +366,7 @@ function createBridge(otherInit, registerProxy) { try { keys = otherReflectOwnKeys(object); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } for (let i = 0; i < keys.length; i++) { const key = keys[i]; // @prim @@ -374,7 +374,7 @@ function createBridge(otherInit, registerProxy) { try { desc = otherSafeGetOwnPropertyDescriptor(object, key); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } if (!desc) continue; if (!desc.configurable) { @@ -440,7 +440,7 @@ function createBridge(otherInit, registerProxy) { try { ret = otherReflectGet(object, key); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } return this.fromOtherWithContext(ret); } @@ -455,7 +455,7 @@ function createBridge(otherInit, registerProxy) { value = otherFromThis(value); return otherReflectSet(object, key, value) === true; } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } } @@ -478,7 +478,7 @@ function createBridge(otherInit, registerProxy) { args = otherFromThisArguments(args); ret = otherReflectApply(object, context, args); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } return thisFromOther(ret); } @@ -491,7 +491,7 @@ function createBridge(otherInit, registerProxy) { args = otherFromThisArguments(args); ret = otherReflectConstruct(object, args); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } return thisFromOtherWithFactory(this.getFactory(), ret, thisFromOther(object)); } @@ -510,7 +510,7 @@ function createBridge(otherInit, registerProxy) { try { desc = otherSafeGetOwnPropertyDescriptor(object, prop); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } desc = this.getOwnPropertyDescriptorDesc(target, prop, desc); @@ -575,7 +575,7 @@ function createBridge(otherInit, registerProxy) { otherDesc = otherSafeGetOwnPropertyDescriptor(object, prop); } } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } if (!otherDesc.configurable) { @@ -608,7 +608,7 @@ function createBridge(otherInit, registerProxy) { try { return otherReflectDeleteProperty(object, prop) === true; } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } } @@ -618,7 +618,7 @@ function createBridge(otherInit, registerProxy) { try { return otherReflectHas(object, key) === true; } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } } @@ -628,7 +628,7 @@ function createBridge(otherInit, registerProxy) { try { if (otherReflectIsExtensible(object)) return true; } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } if (thisReflectIsExtensible(target)) { this.doPreventExtensions(target, object, this); @@ -643,7 +643,7 @@ function createBridge(otherInit, registerProxy) { try { res = otherReflectOwnKeys(object); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } return thisFromOther(res); } @@ -654,7 +654,7 @@ function createBridge(otherInit, registerProxy) { try { if (!otherReflectPreventExtensions(object)) return false; } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } if (thisReflectIsExtensible(target)) { this.doPreventExtensions(target, object, this); @@ -669,7 +669,7 @@ function createBridge(otherInit, registerProxy) { try { res = otherReflectEnumerate(object); } catch (e) { // @other(unsafe) - throw thisFromOther(e); + throw thisFromOtherForThrow(e); } return this.fromOtherWithContext(res); } @@ -817,26 +817,25 @@ function createBridge(otherInit, registerProxy) { const type = typeof other; switch (type) { case 'object': - case 'function': if (other === null) { return null; - } else { - let proto = thisReflectGetPrototypeOf(other); - if (!proto) { - return other; - } - while (proto) { - const mapping = thisReflectApply(thisMapGet, protoMappings, [proto]); - if (mapping) { - const mapped = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [other]); - if (mapped) return mapped; - return mapping(defaultFactory, other); - } - proto = thisReflectGetPrototypeOf(proto); - } + } + // fallthrough + case 'function': + let proto = thisReflectGetPrototypeOf(other); + if (!proto) { return other; } - + while (proto) { + const mapping = thisReflectApply(thisMapGet, protoMappings, [proto]); + if (mapping) { + const mapped = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [other]); + if (mapped) return mapped; + return mapping(defaultFactory, other); + } + proto = thisReflectGetPrototypeOf(proto); + } + return other; case 'undefined': case 'string': case 'number': @@ -850,42 +849,40 @@ function createBridge(otherInit, registerProxy) { } } - function thisFromOtherWithFactory(factory, other, proto) { + function thisFromOtherForThrow(other) { for (let loop = 0; loop < 10; loop++) { const type = typeof other; switch (type) { case 'object': - case 'function': if (other === null) { return null; - } else { - const mapped = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [other]); - if (mapped) return mapped; - if (proto) { - return thisProxyOther(factory, other, proto); - } + } + // fallthrough + case 'function': + const mapped = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [other]); + if (mapped) return mapped; + let proto; + try { + proto = otherReflectGetPrototypeOf(other); + } catch (e) { // @other(unsafe) + other = e; + break; + } + if (!proto) { + return thisProxyOther(defaultFactory, other, null); + } + for (;;) { + const mapping = thisReflectApply(thisMapGet, protoMappings, [proto]); + if (mapping) return mapping(defaultFactory, other); try { - proto = otherReflectGetPrototypeOf(other); + proto = otherReflectGetPrototypeOf(proto); } catch (e) { // @other(unsafe) other = e; break; } - if (!proto) { - return thisProxyOther(factory, other, null); - } - while (proto) { - const mapping = thisReflectApply(thisMapGet, protoMappings, [proto]); - if (mapping) return mapping(factory, other); - try { - proto = otherReflectGetPrototypeOf(proto); - } catch (e) { // @other(unsafe) - other = e; - break; - } - } - return thisProxyOther(factory, other, thisObjectPrototype); + if (!proto) return thisProxyOther(defaultFactory, other, thisObjectPrototype); } - + break; case 'undefined': case 'string': case 'number': @@ -897,12 +894,55 @@ function createBridge(otherInit, registerProxy) { default: // new, unknown types can be dangerous throw new VMError(`Unknown type '${type}'`); } - factory = defaultFactory; - proto = undefined; } throw new VMError('Exception recursion depth'); } + function thisFromOtherWithFactory(factory, other, proto) { + const type = typeof other; + switch (type) { + case 'object': + if (other === null) { + return null; + } + // fallthrough + case 'function': + const mapped = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [other]); + if (mapped) return mapped; + if (proto) { + return thisProxyOther(factory, other, proto); + } + try { + proto = otherReflectGetPrototypeOf(other); + } catch (e) { // @other(unsafe) + throw thisFromOtherForThrow(e); + } + if (!proto) { + return thisProxyOther(factory, other, null); + } + do { + const mapping = thisReflectApply(thisMapGet, protoMappings, [proto]); + if (mapping) return mapping(factory, other); + try { + proto = otherReflectGetPrototypeOf(proto); + } catch (e) { // @other(unsafe) + throw thisFromOtherForThrow(e); + } + } while (proto); + return thisProxyOther(factory, other, thisObjectPrototype); + case 'undefined': + case 'string': + case 'number': + case 'boolean': + case 'symbol': + case 'bigint': + return other; + + default: // new, unknown types can be dangerous + throw new VMError(`Unknown type '${type}'`); + } + } + function thisFromOtherArguments(args) { // Note: args@other(safe-array) returns@this(safe-array) throws@this(unsafe) const arr = []; diff --git a/lib/nodevm.js b/lib/nodevm.js index e75f9e9..b1236ee 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -410,7 +410,7 @@ class NodeVM extends VM { } const prefix = strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX; let scriptCode = this._compiler(code, unresolvedFilename); - scriptCode = transformer(null, scriptCode, false, false).code; + scriptCode = transformer(null, scriptCode, false, false, unresolvedFilename).code; script = new Script(prefix + scriptCode + MODULE_SUFFIX, { __proto__: null, filename: unresolvedFilename, diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 86453f0..06cd7e2 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -296,7 +296,7 @@ function resolverFromOptions(vm, options, override, compiler) { return checkedRootPaths.some(path => { if (!filename.startsWith(path)) return false; const len = path.length; - if (filename.length === len) return true; + if (filename.length === len || (len > 0 && path[len-1] === pa.sep)) return true; const sep = filename[len]; return sep === '/' || sep === pa.sep; }); diff --git a/lib/script.js b/lib/script.js index 088fa52..e4c9d9d 100644 --- a/lib/script.js +++ b/lib/script.js @@ -309,7 +309,7 @@ class VMScript { getCompiledCode() { if (!this._compiledCode) { const comp = this._compiler(this._prefix + removeShebang(this._code) + this._suffix, this.filename); - const res = transformer(null, comp, false, false); + const res = transformer(null, comp, false, false, this.filename); this._compiledCode = res.code; this._hasAsync = res.hasAsync; } diff --git a/lib/setup-sandbox.js b/lib/setup-sandbox.js index de49fdd..2149cc3 100644 --- a/lib/setup-sandbox.js +++ b/lib/setup-sandbox.js @@ -308,7 +308,8 @@ const withProxy = localObjectFreeze({ const interanState = localObjectFreeze({ __proto__: null, wrapWith(x) { - return new LocalProxy(x, withProxy); + if (x === null || x === undefined) return x; + return new LocalProxy(localObject(x), withProxy); }, handleException: ensureThis, import(what) { diff --git a/lib/transformer.js b/lib/transformer.js index 8bb4de1..f33f6ab 100644 --- a/lib/transformer.js +++ b/lib/transformer.js @@ -1,7 +1,6 @@ -const {parse: acornParse} = require('acorn'); +const {Parser: AcornParser, isNewLine: acornIsNewLine, getLineInfo: acornGetLineInfo} = require('acorn'); const {full: acornWalkFull} = require('acorn-walk'); -const {compileFunction} = require('vm'); const INTERNAL_STATE_NAME = 'VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL'; @@ -11,7 +10,43 @@ function assertType(node, type) { return node; } -function transformer(args, body, isAsync, isGenerator) { +function makeNiceSyntaxError(message, code, filename, location, tokenizer) { + const loc = acornGetLineInfo(code, location); + let end = location; + while (end < code.length && !acornIsNewLine(code.charCodeAt(end))) { + end++; + } + let markerEnd = tokenizer.start === location ? tokenizer.end : location + 1; + if (!markerEnd || markerEnd > end) markerEnd = end; + let markerLen = markerEnd - location; + if (markerLen <= 0) markerLen = 1; + if (message === 'Unexpected token') { + const type = tokenizer.type; + if (type.label === 'name' || type.label === 'privateId') { + message = 'Unexpected identifier'; + } else if (type.label === 'eof') { + message = 'Unexpected end of input'; + } else if (type.label === 'num') { + message = 'Unexpected number'; + } else if (type.label === 'string') { + message = 'Unexpected string'; + } else if (type.label === 'regexp') { + message = 'Unexpected token \'/\''; + markerLen = 1; + } else { + const token = tokenizer.value || type.label; + message = `Unexpected token '${token}'`; + } + } + const error = new SyntaxError(message); + if (!filename) return error; + const line = code.slice(location - loc.column, end); + const marker = line.slice(0, loc.column).replace(/\S/g, ' ') + '^'.repeat(markerLen); + error.stack = `${filename}:${loc.line}\n${line}\n${marker}\n\n${error.stack}`; + return error; +} + +function transformer(args, body, isAsync, isGenerator, filename) { let code; let argsOffset; if (args === null) { @@ -27,17 +62,23 @@ function transformer(args, body, isAsync, isGenerator) { code += '\n})'; } + const parser = new AcornParser({ + __proto__: null, + ecmaVersion: 2022, + allowAwaitOutsideFunction: args === null && isAsync, + allowReturnOutsideFunction: args === null + }, code); let ast; try { - ast = acornParse(code, { - __proto__: null, - ecmaVersion: 2020, - allowAwaitOutsideFunction: args === null && isAsync, - allowReturnOutsideFunction: args === null - }); + ast = parser.parse(); } catch (e) { // Try to generate a nicer error message. - compileFunction(code); + if (e instanceof SyntaxError && e.pos !== undefined) { + let message = e.message; + const match = message.match(/^(.*) \(\d+:\d+\)$/); + if (match) message = match[1]; + e = makeNiceSyntaxError(message, code, filename, e.pos, parser); + } throw e; } @@ -54,11 +95,17 @@ function transformer(args, body, isAsync, isGenerator) { const insertions = []; let hasAsync = false; - const RIGHT = -100; - const LEFT = 100; + const TO_LEFT = -100; + const TO_RIGHT = 100; + + let internStateValiable = undefined; acornWalkFull(ast, (node, state, type) => { - if (type === 'CatchClause') { + if (type === 'Function') { + if (node.async) hasAsync = true; + } + const nodeType = node.type; + if (nodeType === 'CatchClause') { const param = node.param; if (param) { const name = assertType(param, 'Identifier').name; @@ -67,40 +114,48 @@ function transformer(args, body, isAsync, isGenerator) { insertions.push({ __proto__: null, pos: cBody.body[0].start, - order: RIGHT, + order: TO_LEFT, code: `${name}=${INTERNAL_STATE_NAME}.handleException(${name});` }); } } - } else if (type === 'WithStatement') { + } else if (nodeType === 'WithStatement') { insertions.push({ __proto__: null, pos: node.object.start, - order: RIGHT, + order: TO_LEFT, code: INTERNAL_STATE_NAME + '.wrapWith(' }); insertions.push({ __proto__: null, pos: node.object.end, - order: LEFT, + order: TO_RIGHT, code: ')' }); - } else if (type === 'Identifier') { + } else if (nodeType === 'Identifier') { if (node.name === INTERNAL_STATE_NAME) { - throw new SyntaxError('Use of internal vm2 state variable'); + if (internStateValiable === undefined || internStateValiable.start > node.start) { + internStateValiable = node; + } } - } else if (type === 'ImportExpression') { + } else if (nodeType === 'ImportExpression') { insertions.push({ __proto__: null, pos: node.start, - order: LEFT, + order: TO_RIGHT, code: INTERNAL_STATE_NAME + '.' }); - } else if (type === 'Function') { - if (node.async) hasAsync = true; } }); + if (internStateValiable) { + throw makeNiceSyntaxError('Use of internal vm2 state variable', code, filename, internStateValiable.start, { + __proto__: null, + start: internStateValiable.start, + end: internStateValiable.end + }); + } + if (insertions.length === 0) return {__proto__: null, code, hasAsync}; insertions.sort((a, b) => (a.pos == b.pos ? a.order - b.order : a.pos - b.pos)); diff --git a/lib/vm.js b/lib/vm.js index fea6f92..1463e17 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -89,7 +89,7 @@ function checkAsync(allow) { } function transformAndCheck(args, code, isAsync, isGenerator, allowAsync) { - const ret = transformer(args, code, isAsync, isGenerator); + const ret = transformer(args, code, isAsync, isGenerator, undefined); checkAsync(allowAsync || !ret.hasAsync); return ret.code; } @@ -488,7 +488,7 @@ class VM extends EventEmitter { } else { const useFileName = filename || 'vm.js'; let scriptCode = this._compiler(code, useFileName); - const ret = transformer(null, scriptCode, false, false); + const ret = transformer(null, scriptCode, false, false, useFileName); scriptCode = ret.code; checkAsync(this._allowAsync || !ret.hasAsync); // Compile the script here so that we don't need to create a instance of VMScript.