From 37033dd4689b89825bee911fef2b87c7b4fcf49e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 24 Jan 2024 18:03:09 +0100 Subject: [PATCH] Handle js timeouts --- packages/server/src/jsRunner.ts | 168 +++++++++++++----------- packages/string-templates/src/errors.js | 11 ++ packages/string-templates/src/index.cjs | 5 + packages/string-templates/src/index.js | 5 + packages/string-templates/src/index.mjs | 2 + 5 files changed, 111 insertions(+), 80 deletions(-) create mode 100644 packages/string-templates/src/errors.js diff --git a/packages/server/src/jsRunner.ts b/packages/server/src/jsRunner.ts index e2033eab33..14ad5bedaa 100644 --- a/packages/server/src/jsRunner.ts +++ b/packages/server/src/jsRunner.ts @@ -1,6 +1,6 @@ import ivm from "isolated-vm" import env from "./environment" -import { setJSRunner } from "@budibase/string-templates" +import { setJSRunner, JsErrorTimeout } from "@budibase/string-templates" import { context } from "@budibase/backend-core" import tracer from "dd-trace" import fs from "fs" @@ -22,14 +22,15 @@ class ExecutionTimeoutError extends Error { export function init() { setJSRunner((js: string, ctx: Record) => { return tracer.trace("runJS", {}, span => { - const bbCtx = context.getCurrentContext()! + try { + const bbCtx = context.getCurrentContext()! - const isolateRefs = bbCtx.isolateRefs - if (!isolateRefs) { - const jsIsolate = new ivm.Isolate({ memoryLimit: 64 }) - const jsContext = jsIsolate.createContextSync() + const isolateRefs = bbCtx.isolateRefs + if (!isolateRefs) { + const jsIsolate = new ivm.Isolate({ memoryLimit: 64 }) + const jsContext = jsIsolate.createContextSync() - const injectedRequire = `const require = function(val){ + const injectedRequire = `const require = function(val){ switch (val) { case "url": return { @@ -39,97 +40,104 @@ export function init() { } };` - const global = jsContext.global - global.setSync( - "urlResolveCb", - new ivm.Callback((...params: Parameters) => - url.resolve(...params) + const global = jsContext.global + global.setSync( + "urlResolveCb", + new ivm.Callback((...params: Parameters) => + url.resolve(...params) + ) ) - ) - global.setSync( - "urlParseCb", - new ivm.Callback((...params: Parameters) => - url.parse(...params) + global.setSync( + "urlParseCb", + new ivm.Callback((...params: Parameters) => + url.parse(...params) + ) ) - ) - const helpersModule = jsIsolate.compileModuleSync( - `${injectedRequire};${helpersSource}` - ) + const helpersModule = jsIsolate.compileModuleSync( + `${injectedRequire};${helpersSource}` + ) - const cryptoModule = jsIsolate.compileModuleSync(`export default { + const cryptoModule = jsIsolate.compileModuleSync(`export default { randomUUID: cryptoRandomUUIDCb, }`) - cryptoModule.instantiateSync(jsContext, specifier => { - throw new Error(`No imports allowed. Required: ${specifier}`) - }) + cryptoModule.instantiateSync(jsContext, specifier => { + throw new Error(`No imports allowed. Required: ${specifier}`) + }) - global.setSync( - "cryptoRandomUUIDCb", - new ivm.Callback( - (...params: Parameters) => { - return crypto.randomUUID(...params) - } + global.setSync( + "cryptoRandomUUIDCb", + new ivm.Callback( + (...params: Parameters) => { + return crypto.randomUUID(...params) + } + ) ) + + helpersModule.instantiateSync(jsContext, specifier => { + if (specifier === "crypto") { + return cryptoModule + } + throw new Error(`No imports allowed. Required: ${specifier}`) + }) + + for (const [key, value] of Object.entries(ctx)) { + if (key === "helpers") { + // Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource + continue + } + global.setSync(key, value) + } + + bbCtx.isolateRefs = { jsContext, jsIsolate, helpersModule } + } + + let { jsIsolate, jsContext, helpersModule } = bbCtx.isolateRefs! + + const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS + if (perRequestLimit) { + const cpuMs = Number(jsIsolate.cpuTime) / 1e6 + if (cpuMs > perRequestLimit) { + throw new ExecutionTimeoutError( + `CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)` + ) + } + } + + const script = jsIsolate.compileModuleSync( + `import helpers from "compiled_module";${js};cb(run());`, + {} ) - helpersModule.instantiateSync(jsContext, specifier => { - if (specifier === "crypto") { - return cryptoModule + script.instantiateSync(jsContext, specifier => { + if (specifier === "compiled_module") { + return helpersModule } - throw new Error(`No imports allowed. Required: ${specifier}`) + + throw new Error(`"${specifier}" import not allowed`) }) - for (const [key, value] of Object.entries(ctx)) { - if (key === "helpers") { - // Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource - continue - } - global.setSync(key, value) - } + let result + jsContext.global.setSync( + "cb", + new ivm.Callback((value: any) => { + result = value + }) + ) - bbCtx.isolateRefs = { jsContext, jsIsolate, helpersModule } - } - - let { jsIsolate, jsContext, helpersModule } = bbCtx.isolateRefs! - - const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS - if (perRequestLimit) { - const cpuMs = Number(jsIsolate.cpuTime) / 1e6 - if (cpuMs > perRequestLimit) { - throw new ExecutionTimeoutError( - `CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)` - ) - } - } - - const script = jsIsolate.compileModuleSync( - `import helpers from "compiled_module";${js};cb(run());`, - {} - ) - - script.instantiateSync(jsContext, specifier => { - if (specifier === "compiled_module") { - return helpersModule - } - - throw new Error(`"${specifier}" import not allowed`) - }) - - let result - jsContext.global.setSync( - "cb", - new ivm.Callback((value: any) => { - result = value + script.evaluateSync({ + timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, }) - ) - script.evaluateSync({ - timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, - }) + return result + } catch (error: any) { + if (error.message === "Script execution timed out.") { + throw new JsErrorTimeout() + } - return result + throw error + } }) }) } diff --git a/packages/string-templates/src/errors.js b/packages/string-templates/src/errors.js new file mode 100644 index 0000000000..e0dcc3de1c --- /dev/null +++ b/packages/string-templates/src/errors.js @@ -0,0 +1,11 @@ +class JsErrorTimeout extends Error { + code = "ERR_SCRIPT_EXECUTION_TIMEOUT" + + constructor() { + super() + } +} + +module.exports = { + JsErrorTimeout, +} diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs index aedb7fc052..b6475e42cd 100644 --- a/packages/string-templates/src/index.cjs +++ b/packages/string-templates/src/index.cjs @@ -35,3 +35,8 @@ if (!process.env.NO_JS) { return vm.run(js) }) } + +const errors = require("./errors") +for (const error in errors) { + module.exports[error] = errors[error] +} diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index 63da7fde4d..8e28956f75 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -394,3 +394,8 @@ module.exports.convertToJS = hbs => { } module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX + +const errors = require("./errors") +for (const error in errors) { + module.exports[error] = errors[error] +} diff --git a/packages/string-templates/src/index.mjs b/packages/string-templates/src/index.mjs index 43cda8183f..925eda3695 100644 --- a/packages/string-templates/src/index.mjs +++ b/packages/string-templates/src/index.mjs @@ -37,3 +37,5 @@ if (process && !process.env.NO_JS) { return vm.runInNewContext(js, context, { timeout: 1000 }) }) } + +export * from "./errors.js"