From 9fa456deddb436c2fa9d385249f6de3846789c12 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 30 Oct 2026 11:23:25 -0700 Subject: [PATCH] Stop reading the runtime worker's own channel, so guest postMessage cannot reach the kernel The runtime already spoke only over the port handed over in init, but the kernel also routed the worker's channel into the same handler, so a guest's postMessage({ t: "stdio", ... }) wrote to the process's stdout. The runtime also drops its init handler, leaving self.onmessage to the guest. --- packages/kernel/src/kernel/main.ts | 3 ++- packages/kernel/src/runtime/main.ts | 2 ++ packages/kernel/tests/process-runtime.spec.ts | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/kernel/src/kernel/main.ts b/packages/kernel/src/kernel/main.ts index 950e679..052fb12 100644 --- a/packages/kernel/src/kernel/main.ts +++ b/packages/kernel/src/kernel/main.ts @@ -1300,7 +1300,8 @@ function startRuntime(proc: Proc, thread: Thread, init: KernelToRuntimeInit, tra if (child?.parent === thread) finishThread(proc, child, 1, true); } }; - thread.worker.onmessage = (rev: MessageEvent) => handleRuntimeMessage(rev.data); + // The runtime speaks only over the port handed over in init. The worker's own channel + // belongs to guest code, so nothing posted on it is read. // A worker that fails to load or dies outside guest code ends like a crash. thread.worker.onerror = () => { if (thread === proc.main) { diff --git a/packages/kernel/src/runtime/main.ts b/packages/kernel/src/runtime/main.ts index 74a51a2..086d8c5 100644 --- a/packages/kernel/src/runtime/main.ts +++ b/packages/kernel/src/runtime/main.ts @@ -143,6 +143,8 @@ const ctx: RequireContext = { self.onmessage = (ev: MessageEvent): void => { const msg = ev.data; if (msg.t !== "init") return; + // From here on the kernel speaks only over msg.port; the worker scope is the guest's. + self.onmessage = null; kernelPort = msg.port; sab = new SharedArrayBuffer(SAB_BYTES); kernelPort.postMessage({ t: "attach", mailbox: sab } satisfies RuntimeToKernel); diff --git a/packages/kernel/tests/process-runtime.spec.ts b/packages/kernel/tests/process-runtime.spec.ts index 17d93d0..e464f4f 100644 --- a/packages/kernel/tests/process-runtime.spec.ts +++ b/packages/kernel/tests/process-runtime.spec.ts @@ -109,3 +109,18 @@ test("node stays the default runtime", async ({ page }) => { }, "/app/main.mjs"); expect(out).toEqual({ code: 0, signal: null, stdout: "object function function object\n", stderr: "" }); }); + +test("the worker scope belongs to the guest: its postMessage cannot reach the kernel", async ({ page }) => { + const forged = "postMessage({ t: 'stdio', stream: 'stdout', bytes: new TextEncoder().encode('forged\\n') });"; + for (const [runtime, entry] of [["web", "/app/main.mjs"], ["node", "/app/main.mjs"]] as const) { + const out = await runGuest(page, { + "/app/main.mjs": [ + "let heard = 0;", + "self.onmessage = () => { heard++; };", + forged, + "setTimeout(() => console.log('still talking to the kernel', heard), 20);", + ].join("\n"), + }, entry, "", { runtime }); + expect(out, runtime).toEqual({ code: 0, signal: null, stdout: "still talking to the kernel 0\n", stderr: "" }); + } +}); -- knotmirror 0.0.1