fix: stop user code after 500ms of execution

Code like `var xs = []; while(true){ xs.push(1) }` can quickly run the
browser out of memory causing it to crash.  These changes stop user loops
from running indefinitely so that common mistakes will no longer cause
the browser to crash.

Also, the user is informed if a long running loop is detected (js and
jsx challenges) during preview or testing.  Before this there was no
protection for js challenges and no information was given to the
user if they had created such a loop.

Co-Authored-By: Tom <20648924+moT01@users.noreply.github.com>
Co-Authored-By: mrugesh <1884376+raisedadead@users.noreply.github.com>
Co-Authored-By: Randell Dawson <5313213+RandellDawson@users.noreply.github.com>
This commit is contained in:
Oliver Eyton-Williams
2019-11-27 02:51:43 +01:00
committed by mrugesh
parent 1a56f4d5f4
commit f5feff386a
5 changed files with 83 additions and 39 deletions

View File

@ -1236,6 +1236,11 @@
"prop-types": "^15.5.10" "prop-types": "^15.5.10"
} }
}, },
"@freecodecamp/loop-protect": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@freecodecamp/loop-protect/-/loop-protect-2.2.1.tgz",
"integrity": "sha512-px2gy/jHfMyTFOAY+c5IiNuBJCP+B0vC20SGdaS0YgnCJov82bewHDqE9a2fci4XYTxjxyJpuTKZMelLxDcyJg=="
},
"@freecodecamp/react-bootstrap": { "@freecodecamp/react-bootstrap": {
"version": "0.32.3", "version": "0.32.3",
"resolved": "https://registry.npmjs.org/@freecodecamp/react-bootstrap/-/react-bootstrap-0.32.3.tgz", "resolved": "https://registry.npmjs.org/@freecodecamp/react-bootstrap/-/react-bootstrap-0.32.3.tgz",
@ -15513,11 +15518,6 @@
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.3.tgz", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.3.tgz",
"integrity": "sha512-9lz5IVdpwsKLMzQi0MQ+oD9EA0mIGcWYP7jXMTZVXP8D42PwuAk+M/HBFYQoxt1G5OR8m7aSIgb1UymfWGBWEw==" "integrity": "sha512-9lz5IVdpwsKLMzQi0MQ+oD9EA0mIGcWYP7jXMTZVXP8D42PwuAk+M/HBFYQoxt1G5OR8m7aSIgb1UymfWGBWEw=="
}, },
"loop-protect": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/loop-protect/-/loop-protect-2.1.6.tgz",
"integrity": "sha512-eGNk917T5jQ9A/ER/zJlEXCGD/NQepYyLnLBgVPSuspHauG2HUiDx5oKDSpyVQOzGb+yUKMA1k41+Old2ZmcRQ=="
},
"loose-envify": { "loose-envify": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",

View File

@ -14,6 +14,7 @@
"@fortawesome/free-regular-svg-icons": "^5.11.2", "@fortawesome/free-regular-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.4", "@fortawesome/react-fontawesome": "^0.1.4",
"@freecodecamp/loop-protect": "^2.2.1",
"@freecodecamp/react-bootstrap": "^0.32.3", "@freecodecamp/react-bootstrap": "^0.32.3",
"@reach/router": "^1.2.1", "@reach/router": "^1.2.1",
"algoliasearch": "^3.35.1", "algoliasearch": "^3.35.1",
@ -40,7 +41,6 @@
"gatsby-transformer-remark": "^2.6.30", "gatsby-transformer-remark": "^2.6.30",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"loop-protect": "^2.1.6",
"monaco-editor": "^0.18.1", "monaco-editor": "^0.18.1",
"monaco-editor-webpack-plugin": "^1.7.0", "monaco-editor-webpack-plugin": "^1.7.0",
"nanoid": "^1.2.2", "nanoid": "^1.2.2",

View File

@ -13,7 +13,7 @@ import {
import * as Babel from '@babel/standalone'; import * as Babel from '@babel/standalone';
import presetEnv from '@babel/preset-env'; import presetEnv from '@babel/preset-env';
import presetReact from '@babel/preset-react'; import presetReact from '@babel/preset-react';
import protect from 'loop-protect'; import protect from '@freecodecamp/loop-protect';
import * as vinyl from '../utils/polyvinyl.js'; import * as vinyl from '../utils/polyvinyl.js';
import createWorker from '../utils/worker-executor'; import createWorker from '../utils/worker-executor';
@ -23,7 +23,26 @@ import createWorker from '../utils/worker-executor';
import { filename as sassCompile } from '../../../../config/sass-compile'; import { filename as sassCompile } from '../../../../config/sass-compile';
const protectTimeout = 100; const protectTimeout = 100;
Babel.registerPlugin('loopProtection', protect(protectTimeout)); const testProtectTimeout = 1500;
const loopsPerTimeoutCheck = 2000;
function loopProtectCB(line) {
console.log(
`Potential infinite loop detected on line ${line}. Tests may fail if this is not changed.`
);
}
function testLoopProtectCB(line) {
console.log(
`Potential infinite loop detected on line ${line}. Tests may be failing because of this.`
);
}
Babel.registerPlugin('loopProtection', protect(protectTimeout, loopProtectCB));
Babel.registerPlugin(
'testLoopProtection',
protect(testProtectTimeout, testLoopProtectCB, loopsPerTimeoutCheck)
);
const babelOptionsJSX = { const babelOptionsJSX = {
plugins: ['loopProtection'], plugins: ['loopProtection'],
@ -31,9 +50,15 @@ const babelOptionsJSX = {
}; };
const babelOptionsJS = { const babelOptionsJS = {
plugins: ['testLoopProtection'],
presets: [presetEnv] presets: [presetEnv]
}; };
const babelOptionsJSPreview = {
...babelOptionsJS,
plugins: ['loopProtection']
};
const babelTransformCode = options => code => const babelTransformCode = options => code =>
Babel.transform(code, options).code; Babel.transform(code, options).code;
@ -69,28 +94,31 @@ function tryTransform(wrap = identity) {
}; };
} }
export const babelTransformer = cond([ const babelTransformer = (preview = false) =>
[ cond([
testJS, [
flow( testJS,
partial( flow(
vinyl.transformHeadTailAndContents, partial(
tryTransform(babelTransformCode(babelOptionsJS)) vinyl.transformHeadTailAndContents,
tryTransform(
babelTransformCode(preview ? babelOptionsJSPreview : babelOptionsJS)
)
)
) )
) ],
], [
[ testJSX,
testJSX, flow(
flow( partial(
partial( vinyl.transformHeadTailAndContents,
vinyl.transformHeadTailAndContents, tryTransform(babelTransformCode(babelOptionsJSX))
tryTransform(babelTransformCode(babelOptionsJSX)) ),
), partial(vinyl.setExt, 'js')
partial(vinyl.setExt, 'js') )
) ],
], [stubTrue, identity]
[stubTrue, identity] ]);
]);
const sassWorker = createWorker(sassCompile); const sassWorker = createWorker(sassCompile);
async function transformSASS(element) { async function transformSASS(element) {
@ -141,7 +169,14 @@ export const htmlTransformer = cond([
export const transformers = [ export const transformers = [
replaceNBSP, replaceNBSP,
babelTransformer, babelTransformer(),
composeHTML,
htmlTransformer
];
export const transformersPreview = [
replaceNBSP,
babelTransformer(true),
composeHTML, composeHTML,
htmlTransformer htmlTransformer
]; ];

View File

@ -33,6 +33,9 @@ import {
isJavaScriptChallenge isJavaScriptChallenge
} from '../utils/build'; } from '../utils/build';
// How long before bailing out of a preview.
const previewTimeout = 2500;
export function* executeChallengeSaga() { export function* executeChallengeSaga() {
const isBuildEnabled = yield select(isBuildEnabledSelector); const isBuildEnabled = yield select(isBuildEnabledSelector);
if (!isBuildEnabled) { if (!isBuildEnabled) {
@ -90,9 +93,9 @@ function* takeEveryConsole(channel) {
}); });
} }
function* buildChallengeData(challengeData) { function* buildChallengeData(challengeData, preview) {
try { try {
return yield call(buildChallenge, challengeData); return yield call(buildChallenge, challengeData, preview);
} catch (e) { } catch (e) {
yield put(disableBuildOnError()); yield put(disableBuildOnError());
throw e; throw e;
@ -155,7 +158,7 @@ function* previewChallengeSaga() {
const challengeData = yield select(challengeDataSelector); const challengeData = yield select(challengeDataSelector);
if (canBuildChallenge(challengeData)) { if (canBuildChallenge(challengeData)) {
const buildData = yield buildChallengeData(challengeData); const buildData = yield buildChallengeData(challengeData, true);
// evaluate the user code in the preview frame or in the worker // evaluate the user code in the preview frame or in the worker
if (challengeHasPreview(challengeData)) { if (challengeHasPreview(challengeData)) {
const document = yield getContext('document'); const document = yield getContext('document');
@ -163,10 +166,14 @@ function* previewChallengeSaga() {
} else if (isJavaScriptChallenge(challengeData)) { } else if (isJavaScriptChallenge(challengeData)) {
const runUserCode = getTestRunner(buildData, { proxyLogger }); const runUserCode = getTestRunner(buildData, { proxyLogger });
// without a testString the testRunner just evaluates the user's code // without a testString the testRunner just evaluates the user's code
yield call(runUserCode, null, 5000); yield call(runUserCode, null, previewTimeout);
} }
} }
} catch (err) { } catch (err) {
if (err === 'timeout') {
// eslint-disable-next-line no-ex-assign
err = `The code you have written is taking longer than the ${previewTimeout}ms our challenges allow. You may have created an infinite loop or need to write a more efficient algorithm`;
}
console.log(err); console.log(err);
yield put(updateConsole(escape(err))); yield put(updateConsole(escape(err)));
} }

View File

@ -1,4 +1,4 @@
import { transformers } from '../rechallenge/transformers'; import { transformers, transformersPreview } from '../rechallenge/transformers';
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js'; import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challengeTypes';
import createWorker from './worker-executor'; import createWorker from './worker-executor';
@ -76,11 +76,11 @@ export function canBuildChallenge(challengeData) {
return buildFunctions.hasOwnProperty(challengeType); return buildFunctions.hasOwnProperty(challengeType);
} }
export async function buildChallenge(challengeData) { export async function buildChallenge(challengeData, preview = false) {
const { challengeType } = challengeData; const { challengeType } = challengeData;
let build = buildFunctions[challengeType]; let build = buildFunctions[challengeType];
if (build) { if (build) {
return build(challengeData); return build(challengeData, preview);
} }
throw new Error(`Cannot build challenge of type ${challengeType}`); throw new Error(`Cannot build challenge of type ${challengeType}`);
} }
@ -137,8 +137,10 @@ export function buildDOMChallenge({ files, required = [], template = '' }) {
})); }));
} }
export function buildJSChallenge({ files }) { export function buildJSChallenge({ files }, preview = false) {
const pipeLine = composeFunctions(...transformers); const pipeLine = preview
? composeFunctions(...transformersPreview)
: composeFunctions(...transformers);
const finalFiles = Object.keys(files) const finalFiles = Object.keys(files)
.map(key => files[key]) .map(key => files[key])
.map(pipeLine); .map(pipeLine);