From 2fc599068f95d90eed41cbd2302d90c6e4c8fa9c Mon Sep 17 00:00:00 2001 From: faizan-oai <269039902+faizan-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:47:18 -0700 Subject: [PATCH 1/2] fix(windows): compare exact file identities --- sdk/typescript/src/multiscan.ts | 21 +++--- sdk/typescript/src/runtime.ts | 45 +++++++---- sdk/typescript/tests-ts/multiscan.test.ts | 72 ++++++++++++++++++ sdk/typescript/tests-ts/runtime.test.ts | 91 ++++++++++++++++++++++- 4 files changed, 203 insertions(+), 26 deletions(-) diff --git a/sdk/typescript/src/multiscan.ts b/sdk/typescript/src/multiscan.ts index 63f3a7909..6cd7ae37d 100644 --- a/sdk/typescript/src/multiscan.ts +++ b/sdk/typescript/src/multiscan.ts @@ -360,16 +360,18 @@ function notifyProgress( } async function ensureOutputDirectory(path: string): Promise { - const metadata = await lstat(path).catch((error: NodeJS.ErrnoException) => { - if (error.code !== "ENOENT") throw error; - return undefined; - }); + const metadata = await lstat(path, { bigint: true }).catch( + (error: NodeJS.ErrnoException) => { + if (error.code !== "ENOENT") throw error; + return undefined; + }, + ); if (metadata?.isSymbolicLink()) { throw new Error("Multiscan output directories must not be symbolic links."); } await mkdir(path, { recursive: true, mode: 0o700 }); const canonical = await realpath(path); - const directory = await lstat(canonical); + const directory = await lstat(canonical, { bigint: true }); if ( metadata !== undefined && (directory.dev !== metadata.dev || directory.ino !== metadata.ino) @@ -377,13 +379,13 @@ async function ensureOutputDirectory(path: string): Promise { throw new Error("Multiscan output directories changed during preparation."); } if (process.platform === "win32") return canonical; - if ((directory.mode & 0o022) !== 0) { + if ((directory.mode & 0o022n) !== 0n) { throw new Error( "Multiscan output directories must not be group- or world-writable.", ); } const owner = process.geteuid?.(); - if (owner !== undefined && directory.uid !== owner) { + if (owner !== undefined && directory.uid !== BigInt(owner)) { throw new Error( "Multiscan output directories must be owned by the current user.", ); @@ -419,7 +421,8 @@ async function acquireLock(output: string): Promise<() => Promise> { await rm(stale, { recursive: true, force: true }); } } - const createdLock = await lstat(path); + // Windows file IDs can exceed JavaScript's safe integer range. + const createdLock = await lstat(path, { bigint: true }); const owner = `${JSON.stringify({ pid: process.pid, ownerId: randomUUID(), @@ -429,7 +432,7 @@ async function acquireLock(output: string): Promise<() => Promise> { try { await writeFile(ownerPath, owner, { flag: "wx", mode: 0o600 }); } catch (error) { - const currentLock = await lstat(path).catch( + const currentLock = await lstat(path, { bigint: true }).catch( (cleanup: NodeJS.ErrnoException) => { if (cleanup.code !== "ENOENT") throw cleanup; return undefined; diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 13c41f2c2..b756552b3 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -1,6 +1,12 @@ import { execFile as execFileCallback, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; -import { constants, existsSync, readdirSync, type Stats } from "node:fs"; +import { + constants, + existsSync, + readdirSync, + type BigIntStats, + type Stats, +} from "node:fs"; import { chmod, cp, @@ -148,7 +154,7 @@ export async function prepareCodexSecurityCredentialHome( throw error; } if ((process.umask() & 0o700) !== 0) await chmod(path, 0o700); - const metadata = await lstat(path); + const metadata = await lstat(path, { bigint: true }); if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new OutputDirectoryError( `Codex Security credential home is not a directory: ${path}`, @@ -183,17 +189,17 @@ export async function requireSecureCredentialHome( options: { platform?: NodeJS.Platform; secureWindowsHome?: (path: string) => Promise; - metadata?: Stats; - expectedDevice?: number; - expectedInode?: number; + metadata?: BigIntStats; + expectedDevice?: bigint; + expectedInode?: bigint; validateWindowsAcl?: boolean; } = {}, -): Promise { +): Promise { const platform = options.platform ?? process.platform; let metadata = options.metadata; if (metadata === undefined) { try { - metadata = await lstat(path); + metadata = await lstat(path, { bigint: true }); } catch (error) { throw new OutputDirectoryError( `Unable to inspect the Codex Security credential home: ${path}`, @@ -208,7 +214,7 @@ export async function requireSecureCredentialHome( } const canonical = await realpath(path); requireModelSafeOutputDir(canonical); - const canonicalMetadata = await lstat(canonical); + const canonicalMetadata = await lstat(canonical, { bigint: true }); if ( canonicalMetadata.dev !== metadata.dev || canonicalMetadata.ino !== metadata.ino @@ -235,14 +241,22 @@ export async function requireSecureCredentialHome( } if (platform === "win32") { if (options.validateWindowsAcl !== false) { - await requirePrivateCredentialHome(metadata, canonical, { - platform, - secureWindowsHome: options.secureWindowsHome, - }); + await requirePrivateCredentialHome( + { mode: Number(metadata.mode), uid: Number(metadata.uid) }, + canonical, + { + platform, + secureWindowsHome: options.secureWindowsHome, + }, + ); } return metadata; } - await requirePrivateCredentialHome(metadata, canonical, { platform }); + await requirePrivateCredentialHome( + { mode: Number(metadata.mode), uid: Number(metadata.uid) }, + canonical, + { platform }, + ); await requireSecureOutputAncestry(canonical); return metadata; } @@ -2490,9 +2504,10 @@ async function isRegularFile(path: string): Promise { async function sameFile(left: string, right: string): Promise { try { + // NTFS file IDs can exceed JavaScript's safe integer range. const [leftMetadata, rightMetadata] = await Promise.all([ - stat(left), - stat(right), + stat(left, { bigint: true }), + stat(right, { bigint: true }), ]); return ( leftMetadata.dev === rightMetadata.dev && diff --git a/sdk/typescript/tests-ts/multiscan.test.ts b/sdk/typescript/tests-ts/multiscan.test.ts index 33676192a..01f352500 100644 --- a/sdk/typescript/tests-ts/multiscan.test.ts +++ b/sdk/typescript/tests-ts/multiscan.test.ts @@ -1403,6 +1403,24 @@ describe("multiscan", () => { processStartedAt: performance.timeOrigin, }); const originalWriteFile = filesystem.writeFile; + const originalLstat = filesystem.lstat; + const firstExactIdentity = BigInt(Number.MAX_SAFE_INTEGER) + 1n; + let replacementCreated = false; + const lstatLock = spyOn(filesystem, "lstat").mockImplementation( + async (path, options) => { + const stats = await originalLstat(path, options as never); + if (String(path) !== lock) return stats as never; + const exactIdentity = + firstExactIdentity + (replacementCreated ? 1n : 0n); + return { + ...stats, + ino: + typeof stats.ino === "bigint" + ? exactIdentity + : Number(exactIdentity), + } as never; + }, + ); const writeOwner = spyOn(filesystem, "writeFile").mockImplementation( async (path, data, options) => { if (String(path) !== ownerPath) { @@ -1411,6 +1429,7 @@ describe("multiscan", () => { writeOwner.mockRestore(); await rename(lock, join(paths.output, ".lock.stale-owner-creation")); await mkdir(lock, { mode: 0o700 }); + replacementCreated = true; if (ownerPublished) { await originalWriteFile(ownerPath, replacement, { mode: 0o600 }); } @@ -1437,6 +1456,7 @@ describe("multiscan", () => { } } finally { writeOwner.mockRestore(); + lstatLock.mockRestore(); } }, ); @@ -1793,6 +1813,58 @@ describe("multiscan", () => { expect(await readdir(external)).toEqual(["attempt-1"]); }); + test("rejects an output directory replaced during preparation when numeric identities collide", async () => { + const paths = await fixture(); + const source = await repository(paths.root, "output-identity-race"); + await writeFile( + paths.input, + `id,repository,revision\nrace,${source.path},${source.revision}\n`, + ); + await mkdir(paths.output, { mode: 0o700 }); + const originalLstat = filesystem.lstat; + const canonicalOutput = await realpath(paths.output); + const firstExactIdentity = BigInt(Number.MAX_SAFE_INTEGER) + 1n; + let outputInspections = 0; + const inspectOutput = spyOn(filesystem, "lstat").mockImplementation( + async (path, options) => { + const stats = await originalLstat(path, options as never); + if (String(path) !== paths.output && String(path) !== canonicalOutput) { + return stats as never; + } + const exactIdentity = + firstExactIdentity + (outputInspections++ === 0 ? 0n : 1n); + return Object.assign( + Object.create(Object.getPrototypeOf(stats)), + stats, + { + ino: + typeof stats.ino === "bigint" + ? exactIdentity + : Number(exactIdentity), + }, + ) as never; + }, + ); + let scans = 0; + + try { + await expect( + runMultiscan( + options( + paths, + client(async (_repository, scanOptions = {}) => { + scans += 1; + return await completedScan(scanOptions.outputDir!); + }), + ), + ), + ).rejects.toThrow("changed during preparation"); + expect(scans).toBe(0); + } finally { + inspectOutput.mockRestore(); + } + }); + testPosix( "rejects other-user-writable campaigns while preserving readable existing campaigns", async () => { diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 16280e70e..c10ec4958 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -30,7 +30,7 @@ import { import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { brotliDecompressSync } from "node:zlib"; -import { afterEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import { strToU8, zipSync } from "fflate"; import { BUNDLED_PLUGIN_VERSION, @@ -62,6 +62,7 @@ import { planOutputArchive, prepareCodexSecurityCredentialHome, preparePersistentScanRoot, + preserveCodexSecurityPluginRegistration, requirePrivateCredentialHome, requirePrivateCredentialFile, requirePrivateOutputDirectory, @@ -1499,6 +1500,51 @@ describe("plugin runtime preparation", () => { ]); }); + test("does not preserve a different marketplace when numeric identities collide", async () => { + const root = await temporaryDirectory(); + const home = join(root, "home"); + const marketplace = join(home, "sdk-marketplace"); + const differentSource = join(home, "different-marketplace"); + await mkdir(marketplace, { recursive: true }); + await mkdir(differentSource); + await writeFile( + join(home, "config.toml"), + `[marketplaces.codex-security-sdk]\nsource_type = "local"\nsource = ${JSON.stringify(differentSource)}\n[plugins."codex-security@codex-security-sdk"]\nenabled = true\n`, + ); + const originalStat = fsPromises.stat; + const firstExactIdentity = BigInt(Number.MAX_SAFE_INTEGER) + 1n; + const inspectMarketplaces = spyOn(fsPromises, "stat").mockImplementation( + async (path, options) => { + const stats = await originalStat(path, options as never); + const value = String(path); + if (value !== marketplace && value !== differentSource) { + return stats as never; + } + const exactIdentity = + firstExactIdentity + (value === marketplace ? 1n : 0n); + return Object.assign( + Object.create(Object.getPrototypeOf(stats)), + stats, + { + ino: + typeof stats.ino === "bigint" + ? exactIdentity + : Number(exactIdentity), + }, + ) as never; + }, + ); + const config = { model: "comparison-model" }; + + try { + expect(await preserveCodexSecurityPluginRegistration(home, config)).toBe( + config, + ); + } finally { + inspectMarketplaces.mockRestore(); + } + }); + test("refreshes cached plugins before forwarding delegated scan attribution", async () => { const root = await temporaryDirectory(); const previous = await plugin(join(root, "previous"), "0.1.19"); @@ -2034,7 +2080,7 @@ describe("runtime directories and plugin Python boundary", () => { const home = await prepareCodexSecurityCredentialHome({ CODEX_SECURITY_STATE_DIR: join(root, "state"), }); - const stale = await lstat(home); + const stale = await lstat(home, { bigint: true }); await rename(home, join(root, "original-home")); await mkdir(home, { mode: 0o700 }); @@ -2979,6 +3025,47 @@ describe("runtime directories and plugin Python boundary", () => { } }); + test("rejects replacement credential homes when numeric identities collide", async () => { + const root = await temporaryDirectory(); + const home = join(root, "home"); + await mkdir(home); + const canonicalHome = await realpath(home); + const originalLstat = fsPromises.lstat; + const firstExactIdentity = BigInt(Number.MAX_SAFE_INTEGER) + 1n; + let homeInspections = 0; + const inspectHome = spyOn(fsPromises, "lstat").mockImplementation( + async (path, options) => { + const stats = await originalLstat(path, options as never); + if (String(path) !== home && String(path) !== canonicalHome) { + return stats as never; + } + const exactIdentity = + firstExactIdentity + (homeInspections++ === 0 ? 0n : 1n); + return Object.assign( + Object.create(Object.getPrototypeOf(stats)), + stats, + { + ino: + typeof stats.ino === "bigint" + ? exactIdentity + : Number(exactIdentity), + }, + ) as never; + }, + ); + + try { + await expect( + requireSecureCredentialHome(home, { + platform: "win32", + secureWindowsHome: async () => {}, + }), + ).rejects.toThrow("credential home was replaced"); + } finally { + inspectHome.mockRestore(); + } + }); + test("revalidates the Windows credential ACL every time the home is used", async () => { const root = await temporaryDirectory(); const home = join(root, "home"); From 5a7cefed7723466d349f929ee88e403cfda28c81 Mon Sep 17 00:00:00 2001 From: faizan-oai Date: Mon, 17 Aug 2026 16:02:49 -0700 Subject: [PATCH 2/2] refactor: share credential privacy validation --- sdk/typescript/src/runtime.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index b756552b3..d36bdfe09 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -239,25 +239,19 @@ export async function requireSecureCredentialHome( `Codex Security credential home was replaced: ${canonical}`, ); } - if (platform === "win32") { - if (options.validateWindowsAcl !== false) { - await requirePrivateCredentialHome( - { mode: Number(metadata.mode), uid: Number(metadata.uid) }, - canonical, - { - platform, - secureWindowsHome: options.secureWindowsHome, - }, - ); - } - return metadata; + if (platform !== "win32" || options.validateWindowsAcl !== false) { + await requirePrivateCredentialHome( + { mode: Number(metadata.mode), uid: Number(metadata.uid) }, + canonical, + { + platform, + secureWindowsHome: options.secureWindowsHome, + }, + ); + } + if (platform !== "win32") { + await requireSecureOutputAncestry(canonical); } - await requirePrivateCredentialHome( - { mode: Number(metadata.mode), uid: Number(metadata.uid) }, - canonical, - { platform }, - ); - await requireSecureOutputAncestry(canonical); return metadata; }