From b554542c7548e228ef1b0c040bd887f978ca1999 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 21 Apr 2026 12:31:32 -0500 Subject: [PATCH] fix(ui): address sandboxed parser review feedback --- ui/src/adapters/dynamic-loader.ts | 35 ++++++++++++++----- .../adapters/sandboxed-parser-worker.test.ts | 19 ++++++++++ ui/src/adapters/sandboxed-parser-worker.ts | 23 ++++-------- 3 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 ui/src/adapters/sandboxed-parser-worker.test.ts diff --git a/ui/src/adapters/dynamic-loader.ts b/ui/src/adapters/dynamic-loader.ts index 68806c1d6da..fcf63e63a97 100644 --- a/ui/src/adapters/dynamic-loader.ts +++ b/ui/src/adapters/dynamic-loader.ts @@ -47,6 +47,7 @@ interface SandboxedParser { hasFactory: boolean; nextId: number; pendingResolves: Map void>; + pendingParserCreates: Map void>; } // ── State ─────────────────────────────────────────────────────────────────── @@ -85,6 +86,18 @@ function parseLineAsync(sandbox: SandboxedParser, line: string, ts: string): Pro }); } +function drainPendingRequests(sandbox: SandboxedParser): void { + for (const resolver of sandbox.pendingResolves.values()) { + resolver([]); + } + sandbox.pendingResolves.clear(); + + for (const resolver of sandbox.pendingParserCreates.values()) { + resolver(null); + } + sandbox.pendingParserCreates.clear(); +} + /** * Create a sandboxed worker, send the parser source, and wait for init. */ @@ -97,10 +110,12 @@ function initSandboxedWorker(source: string): Promise { hasFactory: false, nextId: 1, pendingResolves: new Map(), + pendingParserCreates: new Map(), }; // Timeout if the worker doesn't respond within 5s const timeout = setTimeout(() => { + drainPendingRequests(sandbox); worker.terminate(); reject(new Error("Parser worker init timed out")); }, 5000); @@ -123,12 +138,14 @@ function initSandboxedWorker(source: string): Promise { resolver(resp.entries as TranscriptEntry[]); } } else if (resp.type === "parser_created") { - const resolver = sandbox.pendingResolves.get(resp.id); + const resolver = sandbox.pendingParserCreates.get(resp.id); if (resolver) { - sandbox.pendingResolves.delete(resp.id); - // Encode parserId in a single-element array for the promise chain - resolver([{ kind: "system", ts: "", text: String(resp.parserId) }] as unknown as TranscriptEntry[]); + sandbox.pendingParserCreates.delete(resp.id); + resolver(resp.parserId); } + } else if (resp.type === "error") { + console.error("[adapter-ui-loader] Worker reported error:", resp.message); + drainPendingRequests(sandbox); } }; @@ -138,6 +155,7 @@ function initSandboxedWorker(source: string): Promise { if (msg.type === "error") { clearTimeout(timeout); + drainPendingRequests(sandbox); worker.terminate(); reject(new Error(msg.message)); return; @@ -146,6 +164,7 @@ function initSandboxedWorker(source: string): Promise { worker.onerror = (ev) => { clearTimeout(timeout); + drainPendingRequests(sandbox); worker.terminate(); reject(new Error(`Worker error: ${ev.message}`)); }; @@ -200,10 +219,9 @@ function buildParserModule(sandbox: SandboxedParser): DynamicParserModule { // Request a parser instance from the worker. const id = nextRequestId(sandbox); - sandbox.pendingResolves.set(id, (entries) => { - // The parserId is encoded in the first entry's text field. - if (entries.length > 0 && (entries[0] as TranscriptEntry & { kind: string }).kind === "system") { - parserId = parseInt((entries[0] as TranscriptEntry & { text: string }).text, 10); + sandbox.pendingParserCreates.set(id, (createdParserId) => { + if (createdParserId !== null) { + parserId = createdParserId; parserReady = true; } }); @@ -303,6 +321,7 @@ export function invalidateDynamicParser(adapterType: string): boolean { // Terminate the worker to free resources. const sandbox = sandboxedParsers.get(adapterType); if (sandbox) { + drainPendingRequests(sandbox); sandbox.worker.terminate(); sandboxedParsers.delete(adapterType); } diff --git a/ui/src/adapters/sandboxed-parser-worker.test.ts b/ui/src/adapters/sandboxed-parser-worker.test.ts new file mode 100644 index 00000000000..4ba64835d01 --- /dev/null +++ b/ui/src/adapters/sandboxed-parser-worker.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { getWorkerBootstrapSource } from "./sandboxed-parser-worker"; + +describe("sandboxed parser worker bootstrap", () => { + it("disables child worker and object URL escape hatches", () => { + const source = getWorkerBootstrapSource(); + + expect(source).toContain("self.Worker = _undefined"); + expect(source).toContain("self.SharedWorker = _undefined"); + expect(source).toContain("self.Blob = _undefined"); + expect(source).toContain('"createObjectURL"'); + expect(source).toContain('"revokeObjectURL"'); + }); + + it("does not include the unused parse_batch protocol branch", () => { + expect(getWorkerBootstrapSource()).not.toContain("parse_batch"); + }); +}); diff --git a/ui/src/adapters/sandboxed-parser-worker.ts b/ui/src/adapters/sandboxed-parser-worker.ts index e9ed767f46a..7351c370c40 100644 --- a/ui/src/adapters/sandboxed-parser-worker.ts +++ b/ui/src/adapters/sandboxed-parser-worker.ts @@ -17,7 +17,6 @@ export type SandboxRequest = | { type: "init"; source: string } | { type: "parse"; id: number; line: string; ts: string } - | { type: "parse_batch"; id: number; lines: { line: string; ts: string }[] } | { type: "create_parser"; id: number } | { type: "parser_parse"; id: number; parserId: number; line: string; ts: string } | { type: "parser_reset"; parserId: number }; @@ -63,6 +62,13 @@ self.caches = _undefined; // Import / eval escape hatches self.importScripts = _undefined; +self.Worker = _undefined; +self.SharedWorker = _undefined; +self.Blob = _undefined; +if (self.URL) { + try { Object.defineProperty(self.URL, "createObjectURL", { value: _undefined, writable: false, configurable: false }); } catch {} + try { Object.defineProperty(self.URL, "revokeObjectURL", { value: _undefined, writable: false, configurable: false }); } catch {} +} // Beacon / reporting if (self.navigator) { @@ -71,7 +77,6 @@ if (self.navigator) { // Service worker / broadcast channel self.BroadcastChannel = _undefined; -self.SharedWorker = _undefined; // IndexedDB (prevents persistent state exfiltration) self.indexedDB = _undefined; @@ -145,20 +150,6 @@ self.onmessage = function (e) { return; } - if (msg.type === "parse_batch") { - try { - const allEntries = []; - for (const item of msg.lines) { - const entries = parseStdoutLine ? parseStdoutLine(item.line, item.ts) : []; - allEntries.push(...(entries || [])); - } - self.postMessage({ type: "result", id: msg.id, entries: allEntries }); - } catch (err) { - self.postMessage({ type: "result", id: msg.id, entries: [] }); - } - return; - } - if (msg.type === "create_parser") { try { if (!createStdoutParser) {