I want to use the vm
module as a safe way to run external code. It works pretty well, but there is one issue left:
var UNKNOWN_CODE = "while(true){}";
var vm = require("vm");
var obj = {};
var ctx = vm.createContext(obj);
var script = vm.createScript(UNKNOWN_CODE);
script.runInNewContext(ctx);
console.log("finished"); //never executed
Is there any way to cancel the execution (e.g. if it lasts for more than 5s)?
Thanks in advance!
You need to run it in a separate process, for example:
master.js:
var cluster = require('cluster');
cluster.setupMaster({
exec : "runner.js",
args : process.argv.slice(2),
silent : false
});
//This will be fired when the forked process becomes online
cluster.on( "online", function(worker) {
var timer = 0;
worker.on( "message", function(msg) {
clearTimeout(timer); //The worker responded in under 5 seconds, clear the timeout
console.log(msg);
worker.destroy(); //Don't leave him hanging
});
timer = setTimeout( function() {
worker.destroy(); //Give it 5 seconds to run, then abort it
console.log("worker timed out");
}, 5000);
worker.send( 'while(true){}' ); //Send the code to run for the worker
});
cluster.fork();
runner.js:
//The runner.js is ran in a separate process and just listens for the message which contains code to be executed
process.on('message', function( UNKNOWN_CODE ) {
var vm = require("vm");
var obj = {};
var ctx = vm.createContext(obj);
var script = vm.createScript(UNKNOWN_CODE);
script.runInNewContext(ctx);
process.send( "finished" ); //Send the finished message to the parent process
});
To run this example, place those files in the same folder and dir to it and run
node master.js
You should see "worker timed out" message after 5 seconds. If you change it to 'while(false){}'
the worker will execute the code immediately and you should see "finished"
instead.
You might wanna check Threads a Gogo. Unfortunately, it hasn't been updated to 0.8.x yet.
However, as mentioned by @Esailija, there is no way to run external code safely unless it's in another process.
var Threads = require('threads_a_gogo');
var t = Threads.create();
t.eval("while(true) { console.log('.'); }");
setTimeout(function() {
t.destroy();
console.log('finished');
}, 1000);
You can embed "script breaker" into UNKNOWN_CODE. Something like:
;setTimeout(function() { throw new Error("Execution time limit reached!") }, 2000);
So, the whole thing would look like this:
var UNKNOWN_CODE = "while(true){}";
var scriptBreaker = ';setTimeout(function() { throw new Error("Execution time limit reached!") }, 2000);';
var vm = require("vm");
var obj = {};
var ctx = vm.createContext(obj);
var script = vm.createScript(scriptBreaker + UNKNOWN_CODE);
try {
script.runInNewContext(ctx);
console.log("Finished");
}
catch (err) {
console.log("Timeout!");
// Handle Timeout Error...
}
UPDATE:
After more tests I've come to conclusion that reliable approach would be to use process as pointed Esailija. However, I'm doing it a bit differently.
In main app I have a code like this:
var cp = require('child_process');
function runUnsafeScript(script, callback) {
var worker = cp.fork('./script-runner', [script]);
worker.on('message', function(data) {
worker.kill();
callback(false, data);
});
worker.on('exit', function (code, signal) {
callback(new Error(code), false);
});
worker.on('error', function (err) {
callback(err, false);
});
setTimeout(function killOnTimeOut() {
worker.kill();
callback(new Error("Timeout"), false);
}, 5000);
}
In script-runner.js it looks like following:
var vm = require("vm");
var script = vm.createScript( process.argv[2] );
var obj = { sendResult:function (result) { process.send(result); process.exit(0); } };
var context = vm.createContext(obj);
script.runInNewContext(context);
process.on('uncaughtException', function(err) {
process.exit(1);
});
This approach allowed to achieve following goals:
Yes, this is now possible because I added timeout
parameter support to the Node vm
module. You can simply pass in a millisecond timeout value to runInNewContext()
and it will throw an exception if the code does not finish executing in the specified amount of time.
Note, this does not imply any kind of security model for running untrusted code. This simply allows you to timeout code which you do trust or otherwise secure.
var vm = require("vm");
try {
vm.runInNewContext("while(true) {}", {}, "loop", 1000);
} catch (e) {
// Exception thrown after 1000ms
}
console.log("finished"); // Will now be executed
Exactly what you would expect:
$ time ./node test.js
finished
real 0m1.069s
user 0m1.047s
sys 0m0.017s
In newer versions of Node.js (v0.12
and onward), you will be able to pass a timeout option to vm.runInNewContext
.
This isn't in any stable node release yet, but if you wish to use the latest unstable release (v0.11.13
), you can pass a timeout parameter in like this:
vm.runInNewContext('while(1) {}', {}, {timeout: '1000'});
Now, after 1000 milliseconds the script will throw a timeout error. You can catch it like this:
try {
vm.runInNewContext('while(1) {}', {}, {timeout: '1000'});
}
catch(e) {
console.log(e); // Script execution timed out.
}