diff --git a/skills/chrome-control/native-host/host.js b/skills/chrome-control/native-host/host.js index 6ad69ab..1b47803 100644 --- a/skills/chrome-control/native-host/host.js +++ b/skills/chrome-control/native-host/host.js @@ -301,20 +301,26 @@ function lstatIfExists(file) { } } function listenUnix() { - server.listen(socketPath, () => { - try { - const bound = node_fs.default.lstatSync(socketPath); - boundSocket = { - dev: bound.dev, - ino: bound.ino - }; - ownsSocket = true; - node_fs.default.chmodSync(socketPath, 384); - releaseStartupLock(); - } catch { - refuseEndpoint(); - } - }); + const previousUmask = node_process.default.umask(127); + try { + server.listen(socketPath, onUnixListening); + } finally { + node_process.default.umask(previousUmask); + } +} +function onUnixListening() { + try { + const bound = node_fs.default.lstatSync(socketPath); + boundSocket = { + dev: bound.dev, + ino: bound.ino + }; + ownsSocket = true; + node_fs.default.chmodSync(socketPath, 384); + releaseStartupLock(); + } catch { + refuseEndpoint(); + } } function removeOwnSocket() { try { diff --git a/src/native-host/host.ts b/src/native-host/host.ts index af464c2..95c6549 100755 --- a/src/native-host/host.ts +++ b/src/native-host/host.ts @@ -243,17 +243,23 @@ function lstatIfExists(file: string) { // A bind never replaces an existing file, so a racing host fails with // EADDRINUSE instead of taking over this endpoint. function listenUnix() { - server.listen(socketPath, () => { - try { - const bound = fs.lstatSync(socketPath); - boundSocket = { dev: bound.dev, ino: bound.ino }; - ownsSocket = true; - fs.chmodSync(socketPath, 0o600); - releaseStartupLock(); - } catch { - refuseEndpoint(); - } - }); + // The bind inside listen is synchronous, so this umask makes the socket + // 0600 from the moment it exists instead of after the chmod below. + const previousUmask = process.umask(0o177); + try { server.listen(socketPath, onUnixListening); } + finally { process.umask(previousUmask); } +} + +function onUnixListening() { + try { + const bound = fs.lstatSync(socketPath); + boundSocket = { dev: bound.dev, ino: bound.ino }; + ownsSocket = true; + fs.chmodSync(socketPath, 0o600); + releaseStartupLock(); + } catch { + refuseEndpoint(); + } } // Another host may have replaced this endpoint after a stale-socket recovery, diff --git a/tests/security/host.test.ts b/tests/security/host.test.ts index 1840da1..9b111e5 100644 --- a/tests/security/host.test.ts +++ b/tests/security/host.test.ts @@ -14,6 +14,7 @@ async function host(env: NodeJS.ProcessEnv = {}, protocolVersion = 2) { const child = spawn(process.execPath, ["dist/native-host/host.js"], { env: { ...process.env, OPZERO_CHROME_HOST_SOCKET: endpoint, ...env }, stdio: ["pipe", "pipe", "pipe"] }); + child.stdin.on("error", () => undefined); cleanup.push(() => { child.kill(); fs.rmSync(directory, { recursive: true, force: true }); }); const native: any[] = []; let buffer = Buffer.alloc(0); @@ -185,6 +186,7 @@ function spawnHost(endpoint: string) { const child = spawn(process.execPath, ["dist/native-host/host.js"], { env: { ...process.env, OPZERO_CHROME_HOST_SOCKET: endpoint }, stdio: ["pipe", "ignore", "pipe"] }); + child.stdin.on("error", () => undefined); cleanup.push(() => child.kill()); const exited = new Promise(resolve => child.once("exit", resolve)); return { child, exited };