From 49537aea56312729248b63af8a121987642f8485 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sat, 8 Aug 2026 17:34:06 +0900 Subject: [PATCH 1/2] fix(codex): reject profile FIFOs without blocking Exercise the production config read path in a bounded subprocess so POSIX CI verifies O_NONBLOCK without the vault safety preflight masking it. --- src/codex/native-profile-store.ts | 2 +- tests/native-profile-store.test.ts | 36 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/codex/native-profile-store.ts b/src/codex/native-profile-store.ts index 7afba81730..794e8ea006 100644 --- a/src/codex/native-profile-store.ts +++ b/src/codex/native-profile-store.ts @@ -383,7 +383,7 @@ function readBounded(path: string, limit: number, testSeam?: BoundedReadTestSeam testSeam?.beforeOpen?.(path); const flags = process.platform === "win32" ? fsConstants.O_RDONLY - : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW; + : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK; fd = openSync(path, flags); const opened = fstatSync(fd, { bigint: true }); if (!opened.isFile() || opened.size > BigInt(limit)) throw new Error("invalid bounded file"); diff --git a/tests/native-profile-store.test.ts b/tests/native-profile-store.test.ts index e8cc9d8d80..27239a8f33 100644 --- a/tests/native-profile-store.test.ts +++ b/tests/native-profile-store.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { execFileSync, spawnSync } from "node:child_process"; import { appendFileSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -382,6 +383,41 @@ describe("native-profile recovery journal storage", () => { expect((caught as NativeProfileError).code).toBe("VAULT_INVALID"); }); + test.skipIf(process.platform === "win32")("rejects a config FIFO without waiting for a writer", () => { + const store = context(); + const configPath = join(store.codexHome, "config.toml"); + execFileSync("mkfifo", [configPath]); + const moduleUrl = new URL("../src/codex/native-profile-store.ts", import.meta.url).href; + const childSource = ` + import { resolveNativeCredentialStoreMode } from ${JSON.stringify(moduleUrl)}; + const raw = process.env.OCX_NATIVE_PROFILE_CONTEXT; + if (!raw) process.exit(90); + try { + resolveNativeCredentialStoreMode(JSON.parse(raw)); + process.exit(91); + } catch (error) { + if (!error || typeof error !== "object" || !("code" in error) || error.code !== "UNSUPPORTED_AUTH_STORE") { + console.error(error); + process.exit(92); + } + } + `; + const child = spawnSync(process.execPath, ["--eval", childSource], { + encoding: "utf8", + env: { ...process.env, OCX_NATIVE_PROFILE_CONTEXT: JSON.stringify(store) }, + timeout: 2_000, + }); + + if (child.error || child.signal !== null || child.status !== 0) { + throw new Error([ + "config FIFO probe did not terminate cleanly", + `status=${String(child.status)} signal=${String(child.signal)} error=${String(child.error)}`, + `stderr=${child.stderr}`, + ].join("\n")); + } + expect(child.status).toBe(0); + }); + test("reads at most journal cap plus one when the opened journal grows after fstat", () => { const store = context(); const initial = serializeNativeProfileJournal(journal()); From bce53d854fac5a9ecf6f5f44dddfde2b39410d1e Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sat, 8 Aug 2026 23:41:22 +0900 Subject: [PATCH 2/2] test(codex): cover vault FIFO replacement race Replace the pre-opened config FIFO fixture with the actual vault TOCTOU window: a regular vault passes layout validation, then the bounded-read seam swaps it for a FIFO before open. Keep the probe in a bounded child process so an O_NONBLOCK regression cannot hang the suite. --- tests/native-profile-store.test.ts | 54 +++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/tests/native-profile-store.test.ts b/tests/native-profile-store.test.ts index 27239a8f33..69d9a075a4 100644 --- a/tests/native-profile-store.test.ts +++ b/tests/native-profile-store.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { execFileSync, spawnSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import { appendFileSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -383,20 +383,44 @@ describe("native-profile recovery journal storage", () => { expect((caught as NativeProfileError).code).toBe("VAULT_INVALID"); }); - test.skipIf(process.platform === "win32")("rejects a config FIFO without waiting for a writer", () => { - const store = context(); - const configPath = join(store.codexHome, "config.toml"); - execFileSync("mkfifo", [configPath]); + test.skipIf(process.platform === "win32")("rejects a vault replaced by a FIFO after layout validation", () => { + const base = mkdtempSync(join(tmpdir(), "ocx-native-profile-fifo-race-")); + roots.push(base); + const codexHome = join(base, "codex"); + const configDir = join(base, "opencodex"); + mkdirSync(codexHome, { mode: 0o700 }); + mkdirSync(configDir, { mode: 0o700 }); const moduleUrl = new URL("../src/codex/native-profile-store.ts", import.meta.url).href; const childSource = ` - import { resolveNativeCredentialStoreMode } from ${JSON.stringify(moduleUrl)}; - const raw = process.env.OCX_NATIVE_PROFILE_CONTEXT; - if (!raw) process.exit(90); + import { execFileSync } from "node:child_process"; + import { lstatSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs"; + import { readNativeProfileVault, resolveNativeProfileContext } from ${JSON.stringify(moduleUrl)}; + const codexHome = process.env.OCX_NATIVE_PROFILE_CODEX_HOME; + const configDir = process.env.OCX_NATIVE_PROFILE_CONFIG_DIR; + if (!codexHome || !configDir) process.exit(90); + const store = resolveNativeProfileContext({ codexHome, configDir }); + mkdirSync(store.rootDir, { recursive: true, mode: 0o700 }); + writeFileSync(store.vaultPath, "{}\\n", { mode: 0o600 }); + let replacedWithFifo = false; + store[Symbol.for("opencodex.native-profile-store.bounded-read-test-seam")] = { + beforeOpen(path) { + if (path !== store.vaultPath) process.exit(93); + unlinkSync(path); + execFileSync("mkfifo", [path]); + replacedWithFifo = lstatSync(path).isFIFO(); + }, + }; try { - resolveNativeCredentialStoreMode(JSON.parse(raw)); + readNativeProfileVault(store); process.exit(91); } catch (error) { - if (!error || typeof error !== "object" || !("code" in error) || error.code !== "UNSUPPORTED_AUTH_STORE") { + if ( + !replacedWithFifo + || !error + || typeof error !== "object" + || !("code" in error) + || error.code !== "VAULT_INVALID" + ) { console.error(error); process.exit(92); } @@ -404,17 +428,23 @@ describe("native-profile recovery journal storage", () => { `; const child = spawnSync(process.execPath, ["--eval", childSource], { encoding: "utf8", - env: { ...process.env, OCX_NATIVE_PROFILE_CONTEXT: JSON.stringify(store) }, + env: { + ...process.env, + OCX_NATIVE_PROFILE_CODEX_HOME: codexHome, + OCX_NATIVE_PROFILE_CONFIG_DIR: configDir, + }, timeout: 2_000, }); if (child.error || child.signal !== null || child.status !== 0) { throw new Error([ - "config FIFO probe did not terminate cleanly", + "vault FIFO replacement probe did not terminate cleanly", `status=${String(child.status)} signal=${String(child.signal)} error=${String(child.error)}`, `stderr=${child.stderr}`, ].join("\n")); } + expect(child.error).toBeUndefined(); + expect(child.signal).toBeNull(); expect(child.status).toBe(0); });