diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index f73dc9f5c7..65e9a3778a 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -1,5 +1,5 @@ import { IdentityContext } from "@budibase/types" -import { ExecutionTimeTracker } from "../timers" +import { Isolate, Context } from "isolated-vm" // keep this out of Budibase types, don't want to expose context info export type ContextMap = { @@ -10,5 +10,6 @@ export type ContextMap = { isScim?: boolean automationId?: string isMigrating?: boolean - jsExecutionTracker?: ExecutionTimeTracker + jsIsolate: Isolate + jsContext: Context } diff --git a/packages/backend-core/src/timers/timers.ts b/packages/backend-core/src/timers/timers.ts index 9121c576cd..000be74821 100644 --- a/packages/backend-core/src/timers/timers.ts +++ b/packages/backend-core/src/timers/timers.ts @@ -20,41 +20,3 @@ export function cleanup() { } intervals = [] } - -export class ExecutionTimeoutError extends Error { - public readonly name = "ExecutionTimeoutError" -} - -export class ExecutionTimeTracker { - static withLimit(limitMs: number) { - return new ExecutionTimeTracker(limitMs) - } - - constructor(readonly limitMs: number) {} - - private totalTimeMs = 0 - - track(f: () => T): T { - this.checkLimit() - const start = process.hrtime.bigint() - try { - return f() - } finally { - const end = process.hrtime.bigint() - this.totalTimeMs += Number(end - start) / 1e6 - this.checkLimit() - } - } - - get elapsedMS() { - return this.totalTimeMs - } - - checkLimit() { - if (this.totalTimeMs > this.limitMs) { - throw new ExecutionTimeoutError( - `Execution time limit of ${this.limitMs}ms exceeded: ${this.totalTimeMs}ms` - ) - } - } -} diff --git a/packages/server/src/jsRunner.ts b/packages/server/src/jsRunner.ts index 24ff7f1ade..08816aad42 100644 --- a/packages/server/src/jsRunner.ts +++ b/packages/server/src/jsRunner.ts @@ -1,76 +1,41 @@ import ivm from "isolated-vm" import env from "./environment" import { setJSRunner } from "@budibase/string-templates" -import { context, timers } from "@budibase/backend-core" +import { context } from "@budibase/backend-core" import tracer from "dd-trace" +import { readFileSync } from "fs" -type TrackerFn = (f: () => T) => T +const helpersSource = readFileSync( + "node_modules/@budibase/string-templates/dist/bundle.mjs", + "utf8" +) export function init() { setJSRunner((js: string, ctx: Record) => { return tracer.trace("runJS", {}, span => { - const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS - let track: TrackerFn = f => f() - if (perRequestLimit) { - const bbCtx = tracer.trace("runJS.getCurrentContext", {}, span => - context.getCurrentContext() - ) - if (bbCtx) { - if (!bbCtx.jsExecutionTracker) { - span?.addTags({ - createdExecutionTracker: true, - }) - bbCtx.jsExecutionTracker = tracer.trace( - "runJS.createExecutionTimeTracker", - {}, - span => timers.ExecutionTimeTracker.withLimit(perRequestLimit) - ) - } - span?.addTags({ - js: { - limitMS: bbCtx.jsExecutionTracker.limitMs, - elapsedMS: bbCtx.jsExecutionTracker.elapsedMS, - }, - }) - // We call checkLimit() here to prevent paying the cost of creating - // a new VM context below when we don't need to. - tracer.trace("runJS.checkLimitAndBind", {}, span => { - bbCtx.jsExecutionTracker!.checkLimit() - track = bbCtx.jsExecutionTracker!.track.bind( - bbCtx.jsExecutionTracker - ) - }) - } - } - - const isolate = new ivm.Isolate({ memoryLimit: 64 }) - const vm = isolate.createContextSync() - const jail = vm.global - jail.setSync("global", jail.derefInto()) - - for (let key in ctx) { - let value - if (["alert", "setInterval", "setTimeout"].includes(key)) { - value = undefined - } else { - value = ctx[key] - } - - if (typeof value === "function") { - js = value.toString() + "\n" + js - continue - } else { - value = new ivm.ExternalCopy(value).copyInto({ release: true }) - } - jail.setSync(key, value) - } - - const script = isolate.compileScriptSync(js) - - return track(() => { - return script.runSync(vm, { - timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, + const bbCtx = context.getCurrentContext()! + if (!bbCtx.jsIsolate) { + bbCtx.jsIsolate = new ivm.Isolate({ memoryLimit: 64 }) + bbCtx.jsContext = bbCtx.jsIsolate.createContextSync() + const helpersModule = bbCtx.jsIsolate.compileModuleSync(helpersSource) + helpersModule.instantiateSync(bbCtx.jsContext, () => { + throw new Error("No imports allowed") }) + } + + const perRequestLimit = env.JS_PER_REQUEST_TIME_LIMIT_MS + if (perRequestLimit) { + const cpuMs = Number(bbCtx.jsIsolate.cpuTime) / 1e6 + if (cpuMs > perRequestLimit) { + throw new Error( + `CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)` + ) + } + } + + const script = bbCtx.jsIsolate.compileScriptSync(js) + return script.runSync(bbCtx.jsContext, { + timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, }) }) })