diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7ba445a2b..e18f6b174 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Hardened Linux Chromium executable detection to reject non-executable files, non-browser wrappers, and candidates that hang during the version probe. + ## [17.2.9] - 2026-08-05 ### Breaking Changes diff --git a/packages/coding-agent/src/tools/browser/launch.ts b/packages/coding-agent/src/tools/browser/launch.ts index c8c892347..3019a69b3 100644 --- a/packages/coding-agent/src/tools/browser/launch.ts +++ b/packages/coding-agent/src/tools/browser/launch.ts @@ -205,11 +205,19 @@ function isExecutableFile(p: string): boolean { async function isChromiumExecutable(p: string): Promise { if (!isExecutableFile(p)) return false; try { + const probeTimeoutMs = 3000; const proc = Bun.spawn([p, "--version"], { stdout: "pipe", stderr: "ignore", + signal: AbortSignal.timeout(probeTimeoutMs), + killSignal: "SIGKILL", }); - const stdout = await new Response(proc.stdout).text(); + const stdout = await Promise.race([ + new Response(proc.stdout).text(), + proc.exited.then(() => null), + Bun.sleep(probeTimeoutMs).then(() => null), + ]); + if (stdout === null) return false; await proc.exited; return proc.exitCode === 0 && /Chrom|Edg/i.test(stdout); } catch { diff --git a/packages/coding-agent/test/tools/browser-launch.test.ts b/packages/coding-agent/test/tools/browser-launch.test.ts index 47539dc65..c23a834c3 100644 --- a/packages/coding-agent/test/tools/browser-launch.test.ts +++ b/packages/coding-agent/test/tools/browser-launch.test.ts @@ -106,19 +106,38 @@ describe("browser executable selection", () => { try { const wrapper = path.join(tempDir.path(), "google-chrome"); const chromium = path.join(tempDir.path(), "chromium"); + const nonExecutable = path.join(tempDir.path(), "not-executable"); await Bun.write(wrapper, "#!/bin/sh\necho browser bridge\n"); await Bun.write(chromium, "#!/bin/sh\necho Chromium 123.0\n"); + await Bun.write(nonExecutable, "#!/bin/sh\necho Chromium 123.0\n"); fs.chmodSync(wrapper, 0o755); fs.chmodSync(chromium, 0o755); + fs.chmodSync(nonExecutable, 0o644); await expect(chromiumExecutableProbeForTest(wrapper)).resolves.toBe(false); await expect(chromiumExecutableProbeForTest(chromium)).resolves.toBe(true); + await expect(chromiumExecutableProbeForTest(nonExecutable)).resolves.toBe(false); } finally { await tempDir.remove(); } }, ); + it.skipIf(process.platform === "win32")("rejects wrappers that hang during the version probe", async () => { + const tempDir = TempDir.createSync("@browser-probe-hanging-"); + try { + const hangingWrapper = path.join(tempDir.path(), "google-chrome"); + await Bun.write(hangingWrapper, "#!/bin/sh\nsleep 60\n"); + fs.chmodSync(hangingWrapper, 0o755); + + const startedAt = performance.now(); + await expect(chromiumExecutableProbeForTest(hangingWrapper)).resolves.toBe(false); + expect(performance.now() - startedAt).toBeLessThan(5000); + } finally { + await tempDir.remove(); + } + }); + it("honors PUPPETEER_EXECUTABLE_PATH before a detected Windows system Chrome", async () => { const tempDir = TempDir.createSync("@browser-executable-"); try {