work in progress: replace vm2 and vm with isolated-vm

This commit is contained in:
Sam Rose 2024-01-05 13:48:20 +00:00
parent 3e6848fda5
commit 58abca62de
No known key found for this signature in database
10 changed files with 187 additions and 778 deletions

View File

@ -124,6 +124,8 @@ HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh
# must set this just before running # must set this just before running
ENV NODE_ENV=production ENV NODE_ENV=production
# this is required for isolated-vm to work on Node 20+
ENV NODE_OPTIONS="--no-node-snapshot"
WORKDIR / WORKDIR /
CMD ["./runner.sh"] CMD ["./runner.sh"]

View File

@ -82,6 +82,8 @@ EXPOSE 4001
# due to this causing yarn to stop installing dev dependencies # due to this causing yarn to stop installing dev dependencies
# which are actually needed to get this environment up and running # which are actually needed to get this environment up and running
ENV NODE_ENV=production ENV NODE_ENV=production
# this is required for isolated-vm to work on Node 20+
ENV NODE_OPTIONS="--no-node-snapshot"
ENV CLUSTER_MODE=${CLUSTER_MODE} ENV CLUSTER_MODE=${CLUSTER_MODE}
ENV TOP_LEVEL_PATH=/app ENV TOP_LEVEL_PATH=/app

View File

@ -9,5 +9,5 @@
], ],
"ext": "js,ts,json", "ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js", "../*/dist/**/*"], "ignore": ["src/**/*.spec.ts", "src/**/*.spec.js", "../*/dist/**/*"],
"exec": "yarn build && node ./dist/index.js" "exec": "yarn build && node --no-node-snapshot ./dist/index.js"
} }

View File

@ -72,6 +72,7 @@
"google-auth-library": "7.12.0", "google-auth-library": "7.12.0",
"google-spreadsheet": "3.2.0", "google-spreadsheet": "3.2.0",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"isolated-vm": "^4.6.0",
"jimp": "0.16.1", "jimp": "0.16.1",
"joi": "17.6.0", "joi": "17.6.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
@ -106,7 +107,6 @@
"to-json-schema": "0.2.5", "to-json-schema": "0.2.5",
"uuid": "3.3.2", "uuid": "3.3.2",
"validate.js": "0.13.1", "validate.js": "0.13.1",
"vm2": "^3.9.19",
"worker-farm": "1.7.0", "worker-farm": "1.7.0",
"xml2js": "0.5.0" "xml2js": "0.5.0"
}, },

View File

@ -4,11 +4,12 @@ set -e
if [[ -n $CI ]] if [[ -n $CI ]]
then then
# Running in ci, where resources are limited # Running in ci, where resources are limited
export NODE_OPTIONS="--max-old-space-size=4096" export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot"
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail" echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail"
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail
else else
# --maxWorkers performs better in development # --maxWorkers performs better in development
export NODE_OPTIONS="--no-node-snapshot"
echo "jest --coverage --maxWorkers=2 --forceExit" echo "jest --coverage --maxWorkers=2 --forceExit"
jest --coverage --maxWorkers=2 --forceExit jest --coverage --maxWorkers=2 --forceExit
fi fi

View File

@ -1,4 +1,5 @@
import vm from "vm" import vm from "vm"
import ivm from "isolated-vm"
import env from "./environment" import env from "./environment"
import { setJSRunner } from "@budibase/string-templates" import { setJSRunner } from "@budibase/string-templates"
import { context, timers } from "@budibase/backend-core" import { context, timers } from "@budibase/backend-core"
@ -31,18 +32,31 @@ export function init() {
} }
} }
ctx = { const isolate = new ivm.Isolate({ memoryLimit: 64 })
...ctx, const vm = isolate.createContextSync()
alert: undefined, const jail = vm.global
setInterval: undefined, jail.setSync("global", jail.derefInto())
setTimeout: undefined,
for (let key in ctx) {
let value
if (["alert", "setInterval", "setTimeout"].includes(key)) {
value = undefined
} else {
value = ctx[key]
} }
vm.createContext(ctx) jail.setSync(
return track(() => key,
vm.runInNewContext(js, ctx, { new ivm.ExternalCopy(value).copyInto({ release: true })
)
}
const script = isolate.compileScriptSync(js)
return track(() => {
return script.runSync(vm, {
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
}) })
) })
}) })
}) })
} }

View File

@ -1,29 +1,64 @@
import fetch from "node-fetch" import ivm, { Context, Script } from "isolated-vm"
import { VM, VMScript } from "vm2"
const JS_TIMEOUT_MS = 1000 const JS_TIMEOUT_MS = 1000
class ScriptRunner { class ScriptRunner {
vm: VM vm: IsolatedVM
results: { out: string }
script: VMScript
constructor(script: string, context: any) { constructor(script: string, context: any) {
const code = `let fn = () => {\n${script}\n}; results.out = fn();` const code = `let fn = () => {\n${script}\n}; results.out = fn();`
this.vm = new VM({ this.vm = new IsolatedVM({ memoryLimit: 8 })
timeout: JS_TIMEOUT_MS, this.vm.context = {
}) data: context.data,
this.results = { out: "" } params: context.params,
this.vm.setGlobals(context) results: { out: "" },
this.vm.setGlobal("fetch", fetch) }
this.vm.setGlobal("results", this.results) this.vm.code = code
this.script = new VMScript(code)
} }
execute() { execute() {
this.vm.run(this.script) this.vm.runScript()
return this.results.out const results = this.vm.getValue("results")
return results.out
} }
} }
class IsolatedVM {
isolate: ivm.Isolate
vm: ivm.Context
jail: ivm.Reference
script: any
constructor({ memoryLimit }: { memoryLimit: number }) {
this.isolate = new ivm.Isolate({ memoryLimit })
this.vm = this.isolate.createContextSync()
this.jail = this.vm.global
this.jail.setSync("global", this.jail.derefInto())
}
getValue(key: string) {
const ref = this.vm.global.getSync(key, { reference: true })
const result = ref.copySync()
ref.release()
return result
}
set context(context: Record<string, any>) {
for (let key in context) {
this.jail.setSync(key, this.copyRefToVm(context[key]))
}
}
set code(code: string) {
this.script = this.isolate.compileScriptSync(code)
}
runScript() {
this.script.runSync(this.vm, { timeout: JS_TIMEOUT_MS })
}
copyRefToVm(value: Object): ivm.Copy<Object> {
return new ivm.ExternalCopy(value).copyInto({ release: true })
}
}
export default ScriptRunner export default ScriptRunner

View File

@ -44,6 +44,8 @@ EXPOSE 4001
# due to this causing yarn to stop installing dev dependencies # due to this causing yarn to stop installing dev dependencies
# which are actually needed to get this environment up and running # which are actually needed to get this environment up and running
ENV NODE_ENV=production ENV NODE_ENV=production
# this is required for isolated-vm to work on Node 20+
ENV NODE_OPTIONS="--no-node-snapshot"
ENV CLUSTER_MODE=${CLUSTER_MODE} ENV CLUSTER_MODE=${CLUSTER_MODE}
ENV SERVICE=worker-service ENV SERVICE=worker-service
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU

View File

@ -9,5 +9,5 @@
], ],
"ext": "js,ts,json", "ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js", "../*/dist/**/*"], "ignore": ["src/**/*.spec.ts", "src/**/*.spec.js", "../*/dist/**/*"],
"exec": "yarn build && node dist/index.js" "exec": "yarn build && node --no-node-snapshot dist/index.js"
} }

853
yarn.lock

File diff suppressed because it is too large Load Diff