Merge pull request #13075 from Budibase/fix-context-in-js

Fix re-used context in JS runner.
This commit is contained in:
Michael Drury 2024-02-20 11:29:50 +00:00 committed by GitHub
commit 40aff20af9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 104 additions and 36 deletions

View File

@ -3,9 +3,10 @@ import { IsolatedVM } from "../../jsRunner/vm"
export async function execute(ctx: Ctx) { export async function execute(ctx: Ctx) {
const { script, context } = ctx.request.body const { script, context } = ctx.request.body
const runner = new IsolatedVM().withContext(context) const vm = new IsolatedVM()
const result = vm.withContext(context, () =>
const result = runner.execute(`(function(){\n${script}\n})();`) vm.execute(`(function(){\n${script}\n})();`)
)
ctx.body = result ctx.body = result
} }

View File

@ -2135,5 +2135,48 @@ describe.each([
} }
) )
}) })
it("should not carry over context between formulas", async () => {
const js = Buffer.from(`return $("[text]");`).toString("base64")
const table = await config.createTable({
name: "table",
type: "table",
schema: {
text: {
name: "text",
type: FieldType.STRING,
},
formula: {
name: "formula",
type: FieldType.FORMULA,
formula: `{{ js "${js}"}}`,
formulaType: FormulaType.DYNAMIC,
},
},
})
for (let i = 0; i < 10; i++) {
await config.api.row.save(table._id!, { text: `foo${i}` })
}
const { rows } = await config.api.row.search(table._id!)
expect(rows).toHaveLength(10)
const formulaValues = rows.map(r => r.formula)
expect(formulaValues).toEqual(
expect.arrayContaining([
"foo0",
"foo1",
"foo2",
"foo3",
"foo4",
"foo5",
"foo6",
"foo7",
"foo8",
"foo9",
])
)
})
}) })
}) })

View File

@ -23,25 +23,20 @@ export function init() {
try { try {
const bbCtx = context.getCurrentContext() const bbCtx = context.getCurrentContext()
let vm = bbCtx?.vm const vm = bbCtx?.vm
if (!vm) { ? bbCtx.vm
// Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource : new IsolatedVM({
const { helpers, ...ctxToPass } = ctx
vm = new IsolatedVM({
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS, invocationTimeout: env.JS_PER_INVOCATION_TIMEOUT_MS,
isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS, isolateAccumulatedTimeout: env.JS_PER_REQUEST_TIMEOUT_MS,
}) }).withHelpers()
.withContext(ctxToPass)
.withHelpers()
if (bbCtx) { if (bbCtx) {
// If we have a context, we want to persist it to reuse the isolate // If we have a context, we want to persist it to reuse the isolate
bbCtx.vm = vm bbCtx.vm = vm
} }
} const { helpers, ...rest } = ctx
return vm.execute(js) return vm.withContext(rest, () => vm.execute(js))
} catch (error: any) { } catch (error: any) {
if (error.message === "Script execution timed out.") { if (error.message === "Script execution timed out.") {
throw new JsErrorTimeout() throw new JsErrorTimeout()

View File

@ -15,6 +15,17 @@ export class BuiltInVM implements VM {
this.span = span this.span = span
} }
withContext<T>(context: Record<string, any>, executeWithContext: () => T): T {
this.ctx = vm.createContext(context)
try {
return executeWithContext()
} finally {
for (const key in context) {
delete this.ctx[key]
}
}
}
execute(code: string) { execute(code: string) {
const perRequestLimit = env.JS_PER_REQUEST_TIMEOUT_MS const perRequestLimit = env.JS_PER_REQUEST_TIMEOUT_MS
let track: TrackerFn = f => f() let track: TrackerFn = f => f()

View File

@ -97,10 +97,14 @@ export class IsolatedVM implements VM {
return this return this
} }
withContext(context: Record<string, any>) { withContext<T>(context: Record<string, any>, executeWithContext: () => T) {
this.addToContext(context) this.addToContext(context)
return this try {
return executeWithContext()
} finally {
this.removeFromContext(Object.keys(context))
}
} }
withParsingBson(data: any) { withParsingBson(data: any) {
@ -224,6 +228,12 @@ export class IsolatedVM implements VM {
} }
} }
private removeFromContext(keys: string[]) {
for (let key of keys) {
this.jail.deleteSync(key)
}
}
private getFromContext(key: string) { private getFromContext(key: string) {
const ref = this.vm.global.getSync(key, { reference: true }) const ref = this.vm.global.getSync(key, { reference: true })
const result = ref.copySync() const result = ref.copySync()

View File

@ -7,16 +7,26 @@ export class VM2 implements VM {
vm: vm2.VM vm: vm2.VM
results: { out: string } results: { out: string }
constructor(context: any) { constructor() {
this.vm = new vm2.VM({ this.vm = new vm2.VM({
timeout: JS_TIMEOUT_MS, timeout: JS_TIMEOUT_MS,
}) })
this.results = { out: "" } this.results = { out: "" }
this.vm.setGlobals(context)
this.vm.setGlobal("fetch", fetch) this.vm.setGlobal("fetch", fetch)
this.vm.setGlobal("results", this.results) this.vm.setGlobal("results", this.results)
} }
withContext<T>(context: Record<string, any>, executeWithContext: () => T): T {
this.vm.setGlobals(context)
try {
return executeWithContext()
} finally {
for (const key in context) {
this.vm.setGlobal(key, undefined)
}
}
}
execute(script: string) { execute(script: string) {
const code = `let fn = () => {\n${script}\n}; results.out = fn();` const code = `let fn = () => {\n${script}\n}; results.out = fn();`
const vmScript = new vm2.VMScript(code) const vmScript = new vm2.VMScript(code)

View File

@ -131,24 +131,21 @@ class QueryRunner {
if (transformer) { if (transformer) {
let runner: VM let runner: VM
if (!USE_ISOLATED_VM) { if (!USE_ISOLATED_VM) {
runner = new VM2({ runner = new VM2()
data: rows,
params: enrichedParameters,
})
} else { } else {
transformer = `(function(){\n${transformer}\n})();` transformer = `(function(){\n${transformer}\n})();`
let isolatedVm = new IsolatedVM().withContext({ let vm = new IsolatedVM()
if (datasource.source === SourceName.MONGODB) {
vm = vm.withParsingBson(rows)
}
runner = vm
}
const ctx = {
data: rows, data: rows,
params: enrichedParameters, params: enrichedParameters,
})
if (datasource.source === SourceName.MONGODB) {
isolatedVm = isolatedVm.withParsingBson(rows)
} }
rows = runner.withContext(ctx, () => runner.execute(transformer))
runner = isolatedVm
}
rows = runner.execute(transformer)
} }
// if the request fails we retry once, invalidating the cached value // if the request fails we retry once, invalidating the cached value

View File

@ -1,3 +1,4 @@
export interface VM { export interface VM {
execute(code: string): any execute(code: string): any
withContext<T>(context: Record<string, any>, executeWithContext: () => T): T
} }