fix(js-eval): 使用私有最终表达式槽

This commit is contained in:
jiwangyihao
2026-05-12 21:08:44 +08:00
committed by can1357
parent 7855028814
commit 370fda14ff
3 changed files with 65 additions and 20 deletions
@@ -187,7 +187,7 @@ function returnFinalExpression(code: string): { source: string; returned: boolea
const suffix = code.slice(expression.end);
const semicolonMatch = statement.match(/;\s*$/);
const trimmedStatement = semicolonMatch ? statement.slice(0, semicolonMatch.index) : statement;
return { source: `${prefix}globalThis.__omp_final_expr__ = (${trimmedStatement});${suffix}`, returned: true };
return { source: `${prefix}__omp_set_final_expr__((${trimmedStatement}));${suffix}`, returned: true };
}
/**
@@ -48,6 +48,8 @@ export class JsRuntime {
readonly sessionId: string;
#env: Map<string, string>;
#getHooks: () => RuntimeHooks | null;
#finalExpressionSet = false;
#finalExpressionValue: unknown;
constructor(opts: RuntimeOptions) {
this.#cwd = opts.initialCwd;
@@ -82,16 +84,21 @@ export class JsRuntime {
}
async run(code: string, filename?: string): Promise<unknown> {
Reflect.deleteProperty(globalThis, "__omp_final_expr__");
this.#finalExpressionSet = false;
this.#finalExpressionValue = undefined;
const wrapped = wrapCode(code);
const value = indirectEval(wrapped.source, filename);
const awaited = await awaitMaybePromise(value);
if (wrapped.finalExpressionReturned && Object.hasOwn(globalThis, "__omp_final_expr__")) {
const finalValue = (globalThis as { __omp_final_expr__?: unknown }).__omp_final_expr__;
Reflect.deleteProperty(globalThis, "__omp_final_expr__");
return await awaitMaybePromise(finalValue);
if (wrapped.finalExpressionReturned) {
const awaited = await awaitMaybePromise(value);
if (this.#finalExpressionSet) {
const finalValue = this.#finalExpressionValue;
this.#finalExpressionSet = false;
this.#finalExpressionValue = undefined;
return await awaitMaybePromise(finalValue);
}
return awaited;
}
return awaited;
return await awaitMaybePromise(value);
}
displayValue(value: unknown): void {
@@ -133,6 +140,10 @@ export class JsRuntime {
this.#getHooks()?.onText(text.endsWith("\n") ? text : `${text}\n`);
},
__omp_display__: (value: unknown) => this.displayValue(value),
__omp_set_final_expr__: (value: unknown) => {
this.#finalExpressionSet = true;
this.#finalExpressionValue = value;
},
webcrypto: crypto,
// `process` is intentionally not overridden — user code gets the host worker's real
// `process` object. Subsetting it caused segfaults in workers that share state with
@@ -94,20 +94,22 @@ describe("executeJs", () => {
expect(persisted.output.trim()).toBe("41");
});
it("ignores inherited final expression markers", async () => {
try {
await executeJs("Object.prototype.__omp_final_expr__ = 'poisoned';", { sessionId, session, sessionFile });
it("does not expose the final expression marker as a global property", async () => {
const result = await executeJs("const localOnly = 7; localOnly;", { sessionId, session, sessionFile });
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("7");
const result = await executeJs("const localOnly = 7;", { sessionId, session, sessionFile });
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("");
const marker = await executeJs("return Object.hasOwn(globalThis, '__omp_final_expr__');", {
sessionId,
session,
sessionFile,
});
expect(marker.exitCode).toBe(0);
expect(marker.output.trim()).toBe("false");
const persisted = await executeJs("return localOnly;", { sessionId, session, sessionFile });
expect(persisted.exitCode).toBe(0);
expect(persisted.output.trim()).toBe("7");
} finally {
await executeJs("delete Object.prototype.__omp_final_expr__;", { sessionId, session, sessionFile });
}
const persisted = await executeJs("return localOnly;", { sessionId, session, sessionFile });
expect(persisted.exitCode).toBe(0);
expect(persisted.output.trim()).toBe("7");
});
it("ignores user-assigned final expression markers without a rewritten final expression", async () => {
@@ -121,6 +123,38 @@ describe("executeJs", () => {
expect(result.output.trim()).toBe("actual");
});
it("captures promise-valued final expression before promise callbacks can mutate the marker", async () => {
const result = await executeJs(
"const pending = Promise.resolve(1).then(value => { globalThis.__omp_final_expr__ = 999; return value; }); pending;",
{ sessionId, session, sessionFile },
);
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("1");
});
it("awaits rewritten thenable final expressions once", async () => {
const result = await executeJs(
[
"globalThis.thenCalls = 0;",
"const thenable = {",
" then(resolve) {",
" globalThis.thenCalls++;",
" resolve('done');",
" },",
"};",
"thenable;",
].join("\n"),
{ sessionId, session, sessionFile },
);
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe("done");
const calls = await executeJs("return globalThis.thenCalls;", { sessionId, session, sessionFile });
expect(calls.exitCode).toBe(0);
expect(calls.output.trim()).toBe("1");
});
it("exposes the worker's real process object", async () => {
const result = await executeJs(
[