Fall back to the page's thread when new Worker throws in a sandboxed iframe (#1586)

This commit is contained in:
MarekSpodBiedry
2026-10-17 07:47:33 +02:00
parent 8f02f52a44
commit 351f2591fd
3 changed files with 57 additions and 6 deletions
+20 -2
View File
@@ -315,7 +315,15 @@ function spawn(base) {
/// path, bytes)` receives "reply" (json), "file" (path, bytes: a rendered file to download),
/// "store" (path, bytes: a file to keep in browser storage) and "error" (json = message).
export function workerRun(id, json, files, base, onMessage) {
const w = idle.pop() ?? spawn(base);
let w;
try {
w = idle.pop() ?? spawn(base);
} catch (e) {
// `new Worker` throws at once when the page's origin is opaque (a sandboxed iframe): the job
// fails like one whose worker didn't start.
setTimeout(() => onMessage("error", String((e && e.message) || e), null, null));
return;
}
running.set(id, w);
w.ready.then(() => {
w.worker.onmessage = (e) => {
@@ -380,7 +388,17 @@ const frameWorkers = [];
/// GPU effects on it.
export function frameWorkerStart(base, onMessage, gpu) {
const index = frameWorkers.length;
const worker = new Worker(new URL("worker.js", base), { type: "module", name: `effectcraft-frames-${index}` });
let worker;
try {
worker = new Worker(new URL("worker.js", base), { type: "module", name: `effectcraft-frames-${index}` });
} catch (e) {
// The constructor throws at once when the page's origin is opaque (a sandboxed iframe). The
// slot stays, and the failure is reported like a worker that died, after the caller has
// recorded the index: frames then render on the page's thread (as with `?noworkers`).
frameWorkers.push(null);
setTimeout(() => onMessage(index, undefined, undefined, String((e && e.message) || e || "frame worker failed")));
return index;
}
worker.onmessage = (e) => {
const m = e.data;
if (m && m.type === "frameReply") onMessage(index, m.json, m.bytes ? new Uint8Array(m.bytes) : null, undefined);
+31
View File
@@ -7,6 +7,8 @@
// (#218), and not when it is unchanged.
// - A worker whose start fails fails its job and is terminated, instead of the job waiting for
// "ready" forever (#216): host.js on the worker's error, worker.js re-raising the failed start.
// - A `new Worker` that throws (a sandboxed iframe's opaque origin, #1586) fails the job, and a
// frame worker is reported dead, instead of the exception escaping into startup.
// Exits non-zero on failure.
import assert from "node:assert/strict";
import { copyFileSync, mkdtempSync, writeFileSync } from "node:fs";
@@ -67,6 +69,35 @@ assert.equal(failed.kind, "error");
assert.ok(workers[1].terminated, "the failed worker is terminated");
assert.equal(host.workerStats().running, 0);
// A `new Worker` that throws (a sandboxed iframe's opaque origin): the job fails, and a frame
// worker is reported dead, instead of the exception escaping into startup.
failStart = false;
const RealWorker = globalThis.Worker;
globalThis.Worker = class {
constructor() {
throw new Error("SecurityError: cannot be accessed from origin 'null'");
}
};
// (Take the idle workers out of play first, so that a new one has to be built.)
for (let n = host.workerStats().idle, id = 100; n > 0; n--, id++) {
host.workerRun(id, "{}", [], "http://localhost/", () => {});
host.workerCancel(id);
}
assert.equal(host.workerStats().idle, 0);
const threw = await run(6, []);
assert.equal(threw.kind, "error");
assert.match(threw.json, /SecurityError/);
assert.equal(host.workerStats().running, 0);
let frameCall = null;
const frameDone = new Promise((resolve) => {
frameCall = host.frameWorkerStart("http://localhost/", (index, json, bytes, error) => resolve({ index, error }), false);
});
const dead = await frameDone;
assert.equal(dead.index, frameCall, "the dead worker keeps its index");
assert.match(dead.error, /SecurityError/);
host.frameWorkerPost(frameCall, { type: "frame" }); // no worker behind it: ignored
globalThis.Worker = RealWorker;
// #216, worker side: a rejected start is re-raised where the page's `onerror` sees it.
const dir = mkdtempSync(join(tmpdir(), "ec-worker-"));
copyFileSync(join(here, "../web/worker.js"), join(dir, "worker.js"));