From 418dd3bdb6357d251084ba18466d6c5edbb462fd Mon Sep 17 00:00:00 2001 From: Bob Thomas Date: Mon, 4 Apr 2022 15:11:44 -0400 Subject: [PATCH] Handle errors thrown in async functions If a callback passed to setTimeout (and other timer functions) throws an error, the error propogates up through the host node process, potentially crashing it unless process.on('uncaughtException') is used. Wrap all callback functions passed to the various host timer functions in a try/catch, so errors are handled and emitted as events on the vm. Handle several cases where a promise could end up in an unhandledRejection state (which will cause the process to exit in a future version of Node). --- lib/nodevm.js | 3 ++- lib/setup-node-sandbox.js | 45 ++++++++++++++++++++++++++++++++++++--- test/nodevm.js | 29 +++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/lib/nodevm.js b/lib/nodevm.js index b1236ee..81891b1 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -59,7 +59,8 @@ const HOST = Object.freeze({ setImmediate, clearTimeout, clearInterval, - clearImmediate + clearImmediate, + Promise }); /** diff --git a/lib/setup-node-sandbox.js b/lib/setup-node-sandbox.js index aecf2f6..e0cdf2c 100644 --- a/lib/setup-node-sandbox.js +++ b/lib/setup-node-sandbox.js @@ -208,7 +208,11 @@ global.setTimeout = function setTimeout(callback, delay, ...args) { if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function'); const obj = new Timeout(callback, args); const cb = () => { - localReflectApply(callback, null, args); + try { + localReflectApply(callback, null, args); + } catch (err) { + vm.emit('uncaughtException', err); + } }; const tmr = host.setTimeout(cb, delay); @@ -227,7 +231,11 @@ global.setInterval = function setInterval(callback, interval, ...args) { if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function'); const obj = new Interval(); const cb = () => { - localReflectApply(callback, null, args); + try { + localReflectApply(callback, null, args); + } catch (err) { + vm.emit('uncaughtException', err); + } }; const tmr = host.setInterval(cb, interval); @@ -246,7 +254,11 @@ global.setImmediate = function setImmediate(callback, ...args) { if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function'); const obj = new Immediate(); const cb = () => { - localReflectApply(callback, null, args); + try { + localReflectApply(callback, null, args); + } catch (err) { + vm.emit('uncaughtException', err); + } }; const tmr = host.setImmediate(cb); @@ -275,6 +287,33 @@ global.clearImmediate = function clearImmediate(immediate) { clearTimer(immediate); }; +global.Promise = function _Promise(fn) { + const wrapFn = (resolve, reject) => { + const _reject = reason => { + try { + reject(reason); + } catch (err) { + vm.emit('unhandledRejection', err); + } + }; + try { + fn(resolve, _reject); + } catch (err) { + vm.emit('unhandledRejection', err); + } + }; + const p = new host.Promise(wrapFn); + // prevents unhandledRejection errors from propagating, + // but we don't know if it is ever properly handled or not + p.catch(err => vm.emit('promiseRejected', err, p)); + return p; +}; +global.Promise.prototype = host.Promise.prototype; +global.Promise.all = host.Promise.all; +global.Promise.race = host.Promise.race; +global.Promise.reject = host.Promise.reject; +global.Promise.resolve = host.Promise.resolve; + const localProcess = host.process; function vmEmitArgs(event, args) { diff --git a/test/nodevm.js b/test/nodevm.js index 9d450d2..b038601 100644 --- a/test/nodevm.js +++ b/test/nodevm.js @@ -73,6 +73,35 @@ describe('NodeVM', () => { }); }); +describe('error events', () => { + it('async errors', done => { + const vm = new NodeVM; + vm.on('uncaughtException', err => { + assert.equal(err.message, 'fail'); + done(); + }); + vm.run('setTimeout(function() { throw new Error("fail"); })'); + }); + + it('promise errors', done => { + const vm = new NodeVM; + vm.on('unhandledRejection', err => { + assert.equal(err.message, 'fail'); + done(); + }); + vm.run('new Promise(function() { throw new Error("fail"); })'); + }); + + it('rejected promises', done => { + const vm = new NodeVM; + vm.on('promiseRejected', err => { + assert.equal(err.message, 'fail'); + done(); + }); + vm.run('new Promise(function(resolve, reject) { reject(new Error("fail")); })'); + }); +}); + describe('modules', () => { it('require json', () => { const vm = new NodeVM({