Skip to content

Commit

Permalink
Use 100ms timeout on infinite loop protection, and don't mess with HT…
Browse files Browse the repository at this point in the history
…ML or CSS.
  • Loading branch information
Tom Ashworth committed Jul 15, 2013
1 parent cac74a4 commit b35d186
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 138 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -18,7 +18,7 @@
"url": "git://github.com/remy/jsbin.git"
},
"scripts": {
"test": "./node_modules/mocha/bin/mocha"
"test": "mocha"
},
"dependencies": {
"express": "3.0.x",
Expand Down
12 changes: 9 additions & 3 deletions public/js/render/render.js
Expand Up @@ -66,6 +66,11 @@ var getPreparedCode = (function () {

hasCSS = !!$.trim(css);

// Rewrite loops to detect infiniteness.
// This is done by rewriting the for/while/do loops to perform a check at
// the start of each iteration.
js = loopProtect.rewriteLoops(js);

// escape any script tags in the JS code, because that'll break the mushing together
js = js.replace(re.script, '<\\/script');

Expand Down Expand Up @@ -102,7 +107,7 @@ var getPreparedCode = (function () {

// redirect console logged to our custom log while debugging
if (re.console.test(source)) {
var replaceWith = 'window.runnerWindow.proxyconsole.';
var replaceWith = 'window.runnerWindow.proxyConsole.';
// yes, this code looks stupid, but in fact what it does is look for
// 'console.' and then checks the position of the code. If it's inside
// an openning script tag, it'll change it to window.top._console,
Expand Down Expand Up @@ -143,8 +148,9 @@ var getPreparedCode = (function () {
// }

// Add defer to all inline script tags in IE.
// This is because IE runs scripts as it loads them, so variables that scripts like jQuery add to the
// global scope are undefined. See http://jsbin.com/ijapom/5
// This is because IE runs scripts as it loads them, so variables that
// scripts like jQuery add to the global scope are undefined.
// See http://jsbin.com/ijapom/5
if (jsbin.ie && re.scriptopen.test(source)) {
source = source.replace(/<script(.*?)>/gi, function (all, match) {
if (match.indexOf('src') !== -1) {
Expand Down
92 changes: 92 additions & 0 deletions public/js/runner/loop-protect.js
@@ -0,0 +1,92 @@
/**
* Protect against infinite loops.
* Look for for, while and do loops, and insert a check function at the start of
* the loop. If the check function is called many many times then it returns
* true, preventing the loop from running again.
*/
var loopProtect = (function () {

var loopProtect = {};

// used in the loop detection
loopProtect.counters = {};

/**
* Look for for, while and do loops, and inserts *just* at the start of the
* loop, a check function.
*/
loopProtect.rewriteLoops = function (code, offset) {
var recompiled = [],
lines = code.split('\n'),
re = /for\b|while\b|do\b/;

if (!offset) offset = 0;

var method = 'window.runnerWindow.protect';

lines.forEach(function (line, i) {
var index = 0,
lineNum = i - offset;

if (re.test(line) && line.indexOf('jsbin') === -1) {
// try to insert the tracker after the openning brace (like while (true) { ^here^ )
index = line.indexOf('{');
if (index !== -1) {
line = line.substring(0, index + 1) + ';\nif (' + method + '({ line: ' + lineNum + ' })) break;';
} else {
index = line.indexOf(')');
if (index !== -1) {
// look for a one liner
var colonIndex = line.substring(index).indexOf(';');
if (colonIndex !== -1) {
// in which case, rewrite the loop to add braces
colonIndex += index;
line = line.substring(0, index + 1) + '{\nif (' + method + '({ line: ' + lineNum + ' })) break;\n' + line.substring(index + 1) + '\n}\n'; // extra new lines ensure we clear comment lines
}
}
}

line = ';' + method + '({ line: ' + lineNum + ', reset: true });\n' + line;
loopProtect.counters[lineNum] = {};
}
recompiled.push(line);
});

return recompiled.join('\n');
};

/**
* Injected code in to user's code to **try** to protect against infinite
* loops cropping up in the code, and killing the browser. Returns true
* when the loops has been running for more than 100ms.
*/
loopProtect.protect = function (state) {
loopProtect.counters[state.line] = loopProtect.counters[state.line] || {};
var line = loopProtect.counters[state.line];
if (state.reset) {
line.time = +new Date;
}
if ((+new Date - line.time) > 100) {
// We've spent over 100ms on this loop... smells infinite.
var msg = "Suspicious loop detected at line " + state.line;
if (window.proxyConsole) {
window.proxyConsole.error(msg);
} else console.error(msg);
// Returning true prevents the loop running again
return true;
}
return false;
};

loopProtect.reset = function () {
// reset the counters
loopProtect.counters = {};
};

return loopProtect;

}());

if (typeof exports !== 'undefined') {
module.exports = loopProtect;
}
80 changes: 0 additions & 80 deletions public/js/runner/processor.js
Expand Up @@ -42,78 +42,6 @@ var processor = (function () {
}) + '</pre>';
};

// used in the loop detection
processor.counters = {};

/**
* Look for for, while and do loops, and inserts *just* at the start
* of the loop, a check function. If the check function is called
* many many times, then it throws an exception suspecting this might
* be an infinite loop.
*/
processor.rewriteLoops = function (code, offset) {
var recompiled = [],
lines = code.split('\n'),
re = /for\b|while\b|do\b/;

if (!offset) offset = 0;

// reset the counters
processor.counters = {};

var counter = 'window.runnerWindow.protect';

lines.forEach(function (line, i) {
var index = 0,
lineNum = i - offset;

if (re.test(line) && line.indexOf('jsbin') === -1) {
// try to insert the tracker after the openning brace (like while (true) { ^here^ )
index = line.indexOf('{');
if (index !== -1) {
line = line.substring(0, index + 1) + ';\nif (' + counter + '({ line: ' + lineNum + ' })) break;';
} else {
index = line.indexOf(')');
if (index !== -1) {
// look for a one liner
var colonIndex = line.substring(index).indexOf(';');
if (colonIndex !== -1) {
// in which case, rewrite the loop to add braces
colonIndex += index;
line = line.substring(0, index + 1) + '{\nif (' + counter + '({ line: ' + lineNum + ' })) break;\n' + line.substring(index + 1) + '\n}\n'; // extra new lines ensure we clear comment lines
}
}
}

line = ';' + counter + '({ line: ' + lineNum + ', reset: true });\n' + line;
processor.counters[lineNum] = {};
}
recompiled.push(line);
});

return recompiled.join('\n');
};

/**
* Injected code in to user's code to **try** to protect against infinite loops
* cropping up in the code, and killing the browser. This will throw an exception
* when a loop has hit over X number of times.
*/
processor.protect = function (state) {
var line = processor.counters[state.line];
if (state.reset) {
line.count = 0;
} else {
line.count++;
if (line.count > 100000) {
// we've done a ton of loops, then let's say it smells like an infinite loop
console.error("Suspicious loop detected at line " + state.line);
return true;
}
}
return false;
};

/**
* Render – build the final source code to be written to the iframe. Takes
* the original source and an options object.
Expand All @@ -123,7 +51,6 @@ var processor = (function () {
options = options || [];
source = source || '';


var combinedSource = [],
realtime = (options.requested !== true),
noRealtimeJs = (options.includeJsInRealtime === false);
Expand All @@ -138,13 +65,6 @@ var processor = (function () {
// the editable area.
source = source.replace(/(<.*?\s)(autofocus)/g, '$1');


// since we're running in real time, let's try hook in some loop protection
// basically if a loop runs for many, many times, it's probably an infinite loop
// so we'll throw an exception. This is done by rewriting the for/while/do
// loops to call our check at the start of each.
source = processor.rewriteLoops(source, options.scriptOffset);

// Make sure the doctype is the first thing in the source
var doctypeObj = processor.getDoctype(source),
doctype = doctypeObj.doctype;
Expand Down
Expand Up @@ -3,17 +3,17 @@
* Proxy console.logs out to the parent window
* ========================================================================== */

var proxyconsole = (function () {
var proxyConsole = (function () {

var supportsConsole = true;
try { window.console.log('runner'); } catch (e) { supportsConsole = false; }

var proxyconsole = {};
var proxyConsole = {};

/**
* Stringify all of the console objects from an array for proxying
*/
proxyconsole.stringifyArgs = function (args) {
proxyConsole.stringifyArgs = function (args) {
var newArgs = [];
// TODO this was forEach but when the array is [undefined] it wouldn't
// iterate over them
Expand All @@ -34,10 +34,10 @@ var proxyconsole = (function () {
var methods = ['debug', 'error', 'info', 'log', 'warn', 'dir', 'props'];
methods.forEach(function (method) {
// Create console method
proxyconsole[method] = function () {
proxyConsole[method] = function () {
// Replace args that can't be sent through postMessage
var originalArgs = [].slice.call(arguments),
args = proxyconsole.stringifyArgs(originalArgs);
args = proxyConsole.stringifyArgs(originalArgs);
// Post up with method and the arguments
runner.postMessage('console', {
method: method,
Expand All @@ -51,6 +51,6 @@ var proxyconsole = (function () {
};
});

return proxyconsole;
return proxyConsole;

}());
4 changes: 2 additions & 2 deletions public/js/runner/runner.js
Expand Up @@ -84,8 +84,8 @@ var runner = (function () {
// that the user's code (that runs as a result of the following
// childDoc.write) can access the objects.
childWindow.runnerWindow = {
proxyconsole: proxyconsole,
protect: processor.protect
proxyConsole: proxyConsole,
protect: loopProtect.protect
};

// Write the source out. IE crashes if you have lots of these, so that's
Expand Down
2 changes: 1 addition & 1 deletion public/js/runner/sandbox.js
Expand Up @@ -130,7 +130,7 @@ var sandbox = (function () {
output = e.message;
type = 'error';
}
return proxyconsole[type](output);
return proxyConsole[type](output);
};

/**
Expand Down
4 changes: 3 additions & 1 deletion scripts.json
Expand Up @@ -21,6 +21,7 @@
"/js/vendor/codemirror3/addon/search/match-highlighter.js",
"/js/vendor/json2.js",
"/js/vendor/prettyprint.js",
"/js/runner/loop-protect.js",
"/js/chrome/storage.js",
"/js/jsbin.js",
"/js/editors/mobileCodeMirror.js",
Expand Down Expand Up @@ -51,7 +52,8 @@
"/js/vendor/polyfills.js",
"/js/vendor/stringify.js",
"/js/runner/utils.js",
"/js/runner/proxyconsole.js",
"/js/runner/loop-protect.js",
"/js/runner/proxy-console.js",
"/js/runner/processor.js",
"/js/runner/sandbox.js",
"/js/runner/runner.js",
Expand Down
20 changes: 10 additions & 10 deletions test/loop_detection_test.js
@@ -1,10 +1,10 @@
var assert = require('assert');
var sinon = require('sinon');
var processor = require('../public/js/runner/processor');
var loopProtect = require('../public/js/runner/loop-protect');

// expose a window object for processor compatibility
// expose a window object for loopProtect compatibility
global.window = {
runnerWindow: processor
runnerWindow: loopProtect
};

var code = {
Expand All @@ -14,7 +14,7 @@ var code = {
simplewhile: 'var i = 0; while (i < 100) {\ni += 10;\n}\nreturn i;',
onelinewhile: 'var i = 0; while (i < 100) i += 10;\nreturn i;',
whiletrue: 'var i = 0;\nwhile(true) {\ni++;\n}\nreturn i;',
irl1: 'var nums = [0,1];\n var total = 8;\n for(i = 0; i <= total; i++){\n var newest = nums[i--]\n nums.push(newest);\n }\n return (nums);',
irl1: 'var nums = [0,1];\n var total = 8;\n for(var i = 0; i <= total; i++){\n var newest = nums[i--]\n nums.push(newest);\n }\n return (nums);',
irl2: 'var a = 0;\n for(var j=1;j<=2;j++){\n for(var i=1;i<=60000;i++) {\n a += 1;\n }\n }\n return a;',
};

Expand All @@ -32,39 +32,39 @@ describe('loop', function () {


it('should leave none loop code alone', function () {
assert(processor.rewriteLoops(code.simple) === code.simple);
assert(loopProtect.rewriteLoops(code.simple) === code.simple);
});

it('should rewrite for loops', function () {
var compiled = processor.rewriteLoops(code.simplefor);
var compiled = loopProtect.rewriteLoops(code.simplefor);
assert(compiled !== code);
var result = run(compiled);
assert(result === 9);
});

it('should rewrite one line for loops', function () {
var compiled = processor.rewriteLoops(code.onelinefor);
var compiled = loopProtect.rewriteLoops(code.onelinefor);
assert(compiled !== code);
var result = run(compiled);
assert(result === 10);
});

it('should throw on infinite while', function () {
var compiled = processor.rewriteLoops(code.whiletrue);
var compiled = loopProtect.rewriteLoops(code.whiletrue);

try { spy(compiled); } catch (e) {}

assert(spy.threw);
});

it('should throw on infinite for', function () {
var compiled = processor.rewriteLoops(code.irl1);
var compiled = loopProtect.rewriteLoops(code.irl1);
try { spy(compiled); } catch (e) {}
assert(spy.threw);
});

it('should should allow nested loops to run', function () {
var compiled = processor.rewriteLoops(code.irl2);
var compiled = loopProtect.rewriteLoops(code.irl2);
assert(run(compiled) === 120000);
});

Expand Down
2 changes: 1 addition & 1 deletion test/mocha.opts
@@ -1,3 +1,3 @@
--require should sinon
--require should
--reporter spec
--ui bdd

0 comments on commit b35d186

Please sign in to comment.