fix: spawn CDN pthread workers via a same-origin blob trampoline

Closes the doc-23 §7 KNOWN GAP that killed the editor wherever the wasm is
CDN-served (staging/prod platform): emscripten 6 spawns pthread workers from
_scriptName — the glue's absolute CDN URL — and new Worker(<cross-origin>) is
a SecurityError, observed on staging as 'Failed to construct Worker' right
after instantiation and an editor that never renders. No runtime hook exists
post-mainScriptUrlOrBlob, so wrap window.Worker and redirect EXACTLY the
glue-URL construction to the (formerly dormant) pthreadWorkerScript blob that
importScripts() the glue — blob workers inherit the page origin, and the CDN
already sends the CORP/ACAO the page's COEP requires. ?trace= now reaches
pthread realms through the same blob, same-origin included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLSht9cadprtT2mhynawWu
This commit is contained in:
Gergő Törcsvári 2026-08-18 20:13:25 +02:00
commit 5191a63fd8
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
2 changed files with 147 additions and 9 deletions

View file

@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { hasWritableLib } from "./boot";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
hasWritableLib,
installPthreadWorkerRedirect,
pthreadWorkerScript,
resetWorkerRedirectForTest,
} from "./boot";
import type { LibInfo } from "./libs/source";
const lib = (type?: string): LibInfo => ({ id: `id-${type}`, name: `n-${type}`, type });
@ -22,3 +27,85 @@ describe("hasWritableLib", () => {
expect(hasWritableLib([])).toBe(false);
});
});
/**
* Cross-origin (CDN) pthread spawn fix doc-23 §7 KNOWN GAP. Emscripten
* spawns pthread workers from the glue's absolute URL; when that URL is the
* CDN, `new Worker(...)` is a SecurityError and the editor dies right after
* instantiation (observed on staging). The redirect substitutes a same-origin
* blob that importScripts() the glue for EXACTLY that URL, nothing else.
*/
describe("installPthreadWorkerRedirect", () => {
class FakeWorker {
constructor(
public scriptURL: string | URL,
public opts?: WorkerOptions,
) {}
}
const PAGE = "https://editor.example.test/some/route";
const CDN = "https://cdn.example.test/wasm/kicad_editor/rev";
function stubWindow(): { win: { location: { href: string; origin: string }; Worker: unknown } } {
const win = {
location: { href: PAGE, origin: "https://editor.example.test" },
Worker: FakeWorker as unknown,
};
vi.stubGlobal("window", win);
return { win };
}
afterEach(() => {
vi.unstubAllGlobals();
resetWorkerRedirectForTest();
});
it("same-origin base without trace installs nothing", () => {
const { win } = stubWindow();
installPthreadWorkerRedirect("/wasm/kicad_editor/rev", "kicad_editor");
expect(win.Worker).toBe(FakeWorker);
});
it("cross-origin base: the glue URL is redirected to a same-origin blob; everything else passes through", () => {
const { win } = stubWindow();
installPthreadWorkerRedirect(CDN, "kicad_editor");
expect(win.Worker).not.toBe(FakeWorker);
const W = win.Worker as new (u: string | URL, o?: WorkerOptions) => FakeWorker;
// Exactly what emscripten does: new Worker(_scriptName) with the glue URL.
const pthread = new W(`${CDN}/kicad_editor.js`, { name: "em-pthread" });
expect(String(pthread.scriptURL).startsWith("blob:")).toBe(true);
expect(pthread.opts).toEqual({ name: "em-pthread" });
// Unrelated workers (ngspice/occ services…) are untouched.
const other = new W("/assets/other-worker.js");
expect(other.scriptURL).toBe("/assets/other-worker.js");
});
it("?trace= rides the blob even same-origin (worker realms need the env)", () => {
const { win } = stubWindow();
installPthreadWorkerRedirect(
"/wasm/kicad_editor/rev",
"kicad_editor",
"KI_TRACE_SYM_CHOOSER",
);
const W = win.Worker as new (u: string | URL) => FakeWorker;
const pthread = new W(
"https://editor.example.test/wasm/kicad_editor/rev/kicad_editor.js",
);
expect(String(pthread.scriptURL).startsWith("blob:")).toBe(true);
});
it("the blob trampoline importScripts the absolute glue URL", async () => {
stubWindow();
const script = pthreadWorkerScript(CDN, "kicad_editor");
expect(script).toBeInstanceOf(Blob);
const text = await (script as Blob).text();
expect(text).toBe(
`importScripts(${JSON.stringify(`${CDN}/kicad_editor.js`)});`,
);
// Same-origin without trace stays a plain relative URL (no blob detour).
expect(pthreadWorkerScript("/wasm/rev", "kicad_editor")).toBe(
"/wasm/rev/kicad_editor.js",
);
});
});

View file

@ -154,12 +154,11 @@ function loadScript(src: string): Promise<void> {
}
/**
* DORMANT nothing consumes this today. It built the value once passed as
* `Module.mainScriptUrlOrBlob`, which emscripten 6 ignores: pthread child
* workers spawn from `_scriptName` instead, which is same-origin-only. Kept
* because the cross-origin (CDN) pthread fix a KNOWN GAP, see
* docs/features/async/23-jspi-runtime.md will need exactly this plumbing:
* a SAME-ORIGIN `blob:` worker that `importScripts()` the cross-origin glue.
* The pthread worker-script substitute consumed by
* {@link installPthreadWorkerRedirect} (formerly the dormant
* `mainScriptUrlOrBlob` plumbing emscripten 6 dropped that option, closing
* the doc-23 §7 KNOWN GAP required this instead): a SAME-ORIGIN `blob:`
* worker that `importScripts()` the cross-origin glue.
* `new Worker(<cross-origin URL>)` is a SecurityError, but a `blob:` URL
* inherits the page origin (legal), and a classic worker's `importScripts` MAY
* load a cross-origin script when the CDN sends
@ -185,7 +184,7 @@ function isWasmDiagnostic(line: string): boolean {
return line.startsWith("[wx-dispatch]") || line.startsWith("[wx-timer]");
}
function pthreadWorkerScript(
export function pthreadWorkerScript(
base: string,
bundle: Bundle,
traceMask?: string | null,
@ -211,6 +210,55 @@ function pthreadWorkerScript(
});
}
let workerRedirectInstalled = false;
/** Test-only: clear the install latch (one boot per page in production). */
export function resetWorkerRedirectForTest(): void {
workerRedirectInstalled = false;
}
/**
* Cross-origin (CDN) pthread spawn fix closes the doc-23 §7 KNOWN GAP that
* broke the editor wherever the wasm is CDN-served (staging/prod platform):
* emscripten 6 spawns every pthread worker from `_scriptName` (the glue's
* absolute URL, captured at script execution) and offers no override, and
* `new Worker(<cross-origin URL>)` is a SecurityError observed as the tool
* dying right after instantiation with zero pthreads spawned.
*
* The runtime gives us no hook, so take the one seam that exists: wrap
* `window.Worker` and redirect EXACTLY the glue-URL construction to the
* same-origin substitute from {@link pthreadWorkerScript}. Everything else
* (ngspice/occ service workers, third-party code) passes through untouched.
* Also the only way `?trace=` reaches pthread realms (the blob seeds
* `__KICAD_TRACE__` before importScripts), so it installs for the trace case
* even same-origin. One boot per page (see `booted`), so never uninstalled.
*/
export function installPthreadWorkerRedirect(
base: string,
bundle: Bundle,
traceMask?: string | null,
): void {
const glueHref = new URL(`${base}/${bundle}.js`, window.location.href).href;
const crossOrigin = new URL(glueHref).origin !== window.location.origin;
if ((!crossOrigin && !traceMask) || workerRedirectInstalled) return;
workerRedirectInstalled = true;
const script = pthreadWorkerScript(base, bundle, traceMask);
const substitute =
typeof script === "string" ? script : URL.createObjectURL(script);
const NativeWorker = window.Worker;
window.Worker = new Proxy(NativeWorker, {
construct(target, args: [string | URL, WorkerOptions?]) {
let href: string;
try {
href = new URL(String(args[0]), window.location.href).href;
} catch {
href = String(args[0]);
}
return new target(href === glueHref ? substitute : args[0], args[1]);
},
});
}
/**
* Fetch the tool's `.wasm` ourselves so we can report download progress (the big,
* slow asset 175338 MB). Emscripten otherwise fetches it internally with no
@ -676,6 +724,9 @@ async function doBoot(opts: BootOptions): Promise<void> {
// tool aborts at startup with "wxDomCreateControl is not defined".
// <tool>.js — the tool glue, whose execution captures currentScript.src as
// Emscripten's _scriptName.
// BEFORE the glue executes: pthread workers must spawn from a same-origin
// script when `base` is the cross-origin CDN (and carry the trace env).
installPthreadWorkerRedirect(base, bundle, traceMask);
await loadScript(`${base}/wx.js`);
await loadScript(`${base}/wx-dom.js`);
await loadScript(`${base}/${bundle}.js`);