From 61999db83af2df76017580214fea330b2ce8612f Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Tue, 8 Sep 2026 23:33:59 +0000 Subject: [PATCH 01/15] refactor(plugin): add wide Windows candidate file operations --- .../src/helpers/resolve-security-md.ts | 39 +--- plugins/codex-security/native/README.md | 4 +- .../native/examples/windows-wide-launcher.rs | 12 +- .../native/proof-windows-wide.mts | 103 ++++++++- plugins/codex-security/native/src/windows.rs | 17 ++ .../codex-security/native/windows-binding.mts | 1 + .../codex-security/native/windows-files.mts | 217 +++++++++++++++--- .../native/windows-files.test.mts | 96 ++++++++ 8 files changed, 420 insertions(+), 69 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index 6e87b6d09..e5b38993a 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -13,7 +13,11 @@ import { homedir } from "node:os"; import { basename, dirname, parse, sep } from "node:path"; import { parseArgs } from "node:util"; import { unixBinding, windowsBinding } from "../native"; -import { windowsFileSystem } from "../../../native/windows-files.mjs"; +import { + windowsFileSystem, + windowsJoin, + windowsParts, +} from "../../../native/windows-files.mjs"; import { decodePosixBytes, encodePosixPath, @@ -36,39 +40,6 @@ type FileInfo = Pick & { const statPath = (path: Buffer): FileInfo => windows ? windowsFiles().stat(path) : statSync(path); -function windowsParts(value: string): [string, string, string] { - const path = value.replaceAll("/", "\\"); - if (path.startsWith("\\\\")) { - const start = path.slice(0, 8).toUpperCase() === "\\\\?\\UNC\\" ? 8 : 2; - const server = path.indexOf("\\", start); - const share = server === -1 ? -1 : path.indexOf("\\", server + 1); - return share === -1 - ? [value, "", ""] - : [value.slice(0, share), value[share]!, value.slice(share + 1)]; - } - const drive = path[1] === ":" ? 2 : 0; - const root = path[drive] === "\\" ? 1 : 0; - return [ - value.slice(0, drive), - value.slice(drive, drive + root), - value.slice(drive + root), - ]; -} - -function windowsJoin(left: string, right: string): string { - const [leftDrive, leftRoot, leftPath] = windowsParts(left); - const [rightDrive, rightRoot, rightPath] = windowsParts(right); - if (rightRoot) return (rightDrive || leftDrive) + rightRoot + rightPath; - if (rightDrive && rightDrive.toLowerCase() !== leftDrive.toLowerCase()) - return right; - const drive = rightDrive || leftDrive; - const path = - leftPath + (leftPath && !/[/\\]$/u.test(leftPath) ? "\\" : "") + rightPath; - const root = - leftRoot || (path && drive && !/[:/\\]$/u.test(drive) ? "\\" : ""); - return drive + root + path; -} - function parsedPath(value: string): string { // pathlib removes empty and '.' components while preserving symlink/.. pairs. let root = windows diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index 12e50e601..97251c1a7 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -39,9 +39,9 @@ Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns The binding exposes synchronous file and directory creation, attributes and reparse tags, identity and final/opened names, read/write/seek/size/EOF/flush, exact-handle rename and deletion, and exclusive whole-file locking. Rust's `File` supplies ordinary I/O, cursor-preserving truncation, `sync_all` for flush, and locks. Calls return numeric Windows errors, including 6 for closed handles and 33 for nonblocking lock contention. Buffer ranges, path encoding, and 64-bit seek arguments are checked before use. Overlapped handles are unsupported because pending operations could retain native buffers beyond the call. Path authorization, ancestor traversal, and reparse-point policy remain the caller's responsibility. -Four additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. +Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; the typed adapter uses it to resolve missing paths without losing raw filenames. -`windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. +`windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. Non-strict `realpath` can retain unresolved components; callers must check containment independently. It also supports missing output paths. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. Build on Windows after compiling the TypeScript tools, then run: diff --git a/plugins/codex-security/native/examples/windows-wide-launcher.rs b/plugins/codex-security/native/examples/windows-wide-launcher.rs index 635875fb9..9cf15ea57 100644 --- a/plugins/codex-security/native/examples/windows-wide-launcher.rs +++ b/plugins/codex-security/native/examples/windows-wide-launcher.rs @@ -35,6 +35,16 @@ fn main() -> std::io::Result<()> { for (index, name) in names.iter().enumerate() { fs::write(cwd.join(name), format!("sentinel-{index}"))?; } + std::os::windows::fs::symlink_file(&names[0], cwd.join("relative-link"))?; + std::os::windows::fs::symlink_file(raw("missing-", 0xdfff), cwd.join("missing-link"))?; + std::os::windows::fs::symlink_file( + Path::new("..").join(raw("missing-", 0xdfff)), + cwd.join("missing-parent-link"), + )?; + std::os::windows::fs::symlink_file("loop-link", cwd.join("loop-link"))?; + fs::write(cwd.join("missing-tail"), "ordinary sibling")?; + std::os::windows::fs::symlink_file("missing-tail.", cwd.join("dot-target-link"))?; + std::os::windows::fs::symlink_file("missing-tail ", cwd.join("space-target-link"))?; fs::create_dir(cwd.join("empty"))?; fs::create_dir(cwd.join(raw("directory-", 0xdc80)))?; std::os::windows::fs::symlink_file(&names[0], cwd.join("file-link"))?; @@ -230,7 +240,7 @@ fn main() -> std::io::Result<()> { let mut args = env::args_os().skip(1); let node = args.next().expect("Node executable path"); let script = args.next().expect("Windows wide proof script"); - let root = PathBuf::from(args.next().expect("Proof fixture directory")).join("wide-process"); + let root = PathBuf::from(args.next().expect("Proof fixture directory")).join("wide-İprocess"); fs::create_dir(&root)?; let result = if args.next().is_some_and(|argument| argument == "policy") { policy_proof(node, script, &root) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 5ec3c1ccb..47909d8fa 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -85,6 +85,8 @@ function worker(root: string): Record { `${drive}\\rooted-\ud800`, ); samePath(files.realpath(widePath(".")), cwd); + for (const name of ["NUL", "nUl", ".\\NUL", "x\\..\\NUL"]) + samePath(files.realpath(widePath(name)), "\\\\.\\NUL"); const names = [ "high-\ud800", @@ -110,6 +112,13 @@ function worker(root: string): Record { "directory-link", "dangling-directory-link", "locked-\udfff", + "relative-link", + "missing-link", + "missing-parent-link", + "loop-link", + "missing-tail", + "dot-target-link", + "space-target-link", ].sort(), ); for (const spelling of [".", `${drive}.`, cwd, win32.toNamespacedPath(cwd)]) { @@ -135,7 +144,17 @@ function worker(root: string): Record { .filter((entry) => entry.isSymbolicLink()) .map((entry) => pathText(entry.name)) .sort(), - ["dangling-directory-link", "directory-link", "file-link"], + [ + "dangling-directory-link", + "directory-link", + "dot-target-link", + "file-link", + "loop-link", + "missing-link", + "missing-parent-link", + "relative-link", + "space-target-link", + ], ); assert.throws( () => files.readInto(widePath("locked-\udfff"), Buffer.alloc(1)), @@ -186,6 +205,75 @@ function worker(root: string): Record { win32.join(root, "parent-\ud800"), ); assert(files.stat(widePath(".")).isDirectory()); + assert.deepEqual( + files.readlink(widePath("relative-link")), + widePath(names[0]!), + ); + assert.deepEqual( + files.readFile(widePath("relative-link")), + Buffer.from("sentinel-0"), + ); + samePath( + files.realpath(widePath("missing-link"), false), + win32.join(cwd, "missing-\udfff"), + ); + assert.equal( + pathText( + files.realpath( + widePath(`${win32.toNamespacedPath(cwd)}\\missing-parent-link`), + false, + ), + ).toLowerCase(), + win32.toNamespacedPath(win32.join(root, "missing-\udfff")).toLowerCase(), + ); + assert.throws(() => files.realpath(widePath("loop-link"), false), { + code: "ELOOP", + }); + for (const siblingExists of [true, false]) { + if (!siblingExists) files.unlink(widePath("missing-tail")); + for (const [link, target] of [ + ["dot-target-link", "missing-tail."], + ["space-target-link", "missing-tail "], + ] as const) { + assert.deepEqual(files.readlink(widePath(link)), widePath(target)); + const resolved = files.realpath(widePath(link), false); + samePath(resolved, win32.join(cwd, target)); + assert.throws(() => files.stat(resolved), { code: "ENOENT" }); + files.writeFile(resolved, Buffer.from("literal link target")); + assert.equal( + files.readFile(widePath(link)).toString(), + "literal link target", + ); + files.unlink(resolved); + if (siblingExists) + assert.equal( + files.readFile(widePath("missing-tail")).toString(), + "ordinary sibling", + ); + else + assert.throws(() => files.stat(widePath("missing-tail")), { + code: "ENOENT", + }); + } + } + const streamFile = win32.join(cwd, "a"); + files.writeFile(widePath(streamFile), Buffer.from("base file")); + for (const parent of [cwd, win32.toNamespacedPath(cwd)]) { + const stream = `${parent}\\a:stream`; + const resolved = files.realpath(widePath(stream), false); + samePath(resolved, stream); + files.writeFile(resolved, Buffer.from("stream payload"), true); + assert.equal(files.readFile(widePath(stream)).toString(), "stream payload"); + files.unlink(resolved); + assert.equal(files.readFile(widePath(streamFile)).toString(), "base file"); + } + files.unlink(widePath(streamFile)); + const missingDeepPath = `${win32.toNamespacedPath(cwd)}\\${"a\\".repeat(8_000)}missing`; + assert.equal( + pathText(files.realpath(widePath(missingDeepPath), false)), + missingDeepPath, + ); + assert.notEqual(native.windowsReadLink(widePath("empty")).error, 0); const bounded = Buffer.alloc(4); assert.equal(files.readInto(widePath(names[0]!), bounded), 4); assert.equal(bounded.toString(), "sent"); @@ -197,12 +285,24 @@ function worker(root: string): Record { Buffer.from("replacement output untouched"), ); files.writeFile(rawOutput, Buffer.from("a longer initial output")); + files.writeFile(rawOutput, Buffer.alloc(128 * 1024 + 1, 7)); + assert.deepEqual(files.readFile(rawOutput), Buffer.alloc(128 * 1024 + 1, 7)); files.writeFile(rawOutput, Buffer.from("short")); const contents = Buffer.alloc(64); assert.equal(files.readInto(rawOutput, contents), 5); assert.equal(contents.subarray(0, 5).toString(), "short"); files.writeFile(rawOutput, Buffer.alloc(0)); assert.equal(files.readInto(rawOutput, contents), 0); + assert.throws(() => + files.writeFile(rawOutput, Buffer.from("no replacement"), true), + ); + assert.equal(files.readFile(rawOutput).length, 0); + const renamed = widePath("renamed-\udc80"); + files.writeFile(renamed, Buffer.from("previous destination")); + files.rename(rawOutput, renamed); + assert.equal(files.readFile(renamed).length, 0); + files.unlink(renamed); + assert.throws(() => files.stat(renamed), { code: "ENOENT" }); const replacementLength = files.readInto(replacementOutput, contents); assert.equal( contents.subarray(0, replacementLength).toString(), @@ -271,6 +371,7 @@ function worker(root: string): Record { assert.throws(() => native.windowsEnvironment(malformed)); assert.throws(() => native.windowsAbsolutePath(malformed)); assert.throws(() => native.windowsDirectoryEntries(malformed)); + assert.throws(() => native.windowsReadLink(malformed)); } return { rawArgumentsAndCrtQuoting: true, diff --git a/plugins/codex-security/native/src/windows.rs b/plugins/codex-security/native/src/windows.rs index 1ab3485e3..820e3a92d 100644 --- a/plugins/codex-security/native/src/windows.rs +++ b/plugins/codex-security/native/src/windows.rs @@ -169,6 +169,23 @@ pub fn windows_directory_entries(path: Buffer) -> napi::Result napi::Result { + Ok(match std::fs::read_link(os_string(path)?) { + Ok(target) => BufferResult { + error: 0, + value: wide_bytes(target.as_os_str().encode_wide()), + }, + Err(error) => BufferResult { + error: error + .raw_os_error() + .ok_or_else(|| napi::Error::from_reason(error.to_string()))? + as u32, + value: Vec::new().into(), + }, + }) +} + fn io_range(buffer: &Buffer, offset: f64, length: f64) -> napi::Result<(usize, u32)> { if !offset.is_finite() || !length.is_finite() diff --git a/plugins/codex-security/native/windows-binding.mts b/plugins/codex-security/native/windows-binding.mts index 7b90d458b..61c346fff 100644 --- a/plugins/codex-security/native/windows-binding.mts +++ b/plugins/codex-security/native/windows-binding.mts @@ -36,6 +36,7 @@ export interface WindowsBinding { ): WindowsResult< { name: Buffer; isDirectory: boolean; isSymbolicLink: boolean }[] >; + windowsReadLink(path: Buffer): WindowsResult; openWindowsFile( path: Buffer, access: number, diff --git a/plugins/codex-security/native/windows-files.mts b/plugins/codex-security/native/windows-files.mts index 37f42ca78..5a6f12a18 100644 --- a/plugins/codex-security/native/windows-files.mts +++ b/plugins/codex-security/native/windows-files.mts @@ -5,6 +5,39 @@ import { windowsFlags as flags } from "./windows-flags.mjs"; export const widePath = (path: string): Buffer => Buffer.from(path, "utf16le"); export const pathText = (path: Buffer): string => path.toString("utf16le"); +export function windowsParts(value: string): [string, string, string] { + const path = value.replaceAll("/", "\\"); + if (path.startsWith("\\\\")) { + const start = path.slice(0, 8).toUpperCase() === "\\\\?\\UNC\\" ? 8 : 2; + const server = path.indexOf("\\", start); + const share = server === -1 ? -1 : path.indexOf("\\", server + 1); + return share === -1 + ? [value, "", ""] + : [value.slice(0, share), value[share]!, value.slice(share + 1)]; + } + const drive = path[1] === ":" ? 2 : 0; + const root = path[drive] === "\\" ? 1 : 0; + return [ + value.slice(0, drive), + value.slice(drive, drive + root), + value.slice(drive + root), + ]; +} + +export function windowsJoin(left: string, right: string): string { + const [leftDrive, leftRoot, leftPath] = windowsParts(left); + const [rightDrive, rightRoot, rightPath] = windowsParts(right); + if (rightRoot) return (rightDrive || leftDrive) + rightRoot + rightPath; + if (rightDrive && rightDrive.toLowerCase() !== leftDrive.toLowerCase()) + return right; + const drive = rightDrive || leftDrive; + const path = + leftPath + (leftPath && !/[/\\]$/u.test(leftPath) ? "\\" : "") + rightPath; + const root = + leftRoot || (path && drive && !/[:/\\]$/u.test(drive) ? "\\" : ""); + return drive + root + path; +} + export function windowsFileSystem(native: WindowsBinding) { function check(error: number, path: Buffer): void { if (error === 0) return; @@ -67,24 +100,92 @@ export function windowsFileSystem(native: WindowsBinding) { } } - function realpath(path: Buffer): Buffer { - let normalizedText: string; - if (pathText(path).startsWith("\\\\?\\")) { - // Verbatim paths bypass Win32 dot parsing; normalize only below their root. - const text = pathText(path).replaceAll("/", "\\"); - const root = - /^\\\\\?\\(?:UNC\\[^\\]+\\[^\\]+(?:\\|$)|[^\\]+\\)/iu.exec(text)?.[0] ?? - win32.parse(text).root; - normalizedText = - root + - win32.join("\\", text.slice(root.length)).slice(1).replace(/\\+$/u, ""); - } else { - const text = pathText(absolute(path)); - const root = win32.parse(text).root; - normalizedText = root + text.slice(root.length).replace(/\\+$/u, ""); + function readlink(path: Buffer): Buffer { + const result = native.windowsReadLink(operationPath(path)); + check(result.error, path); + return result.value; + } + + function realpath(path: Buffer, strict = true): Buffer { + function normalize(value: Buffer): Buffer { + let normalizedText: string; + if (pathText(value).startsWith("\\\\?\\")) { + // Verbatim paths bypass Win32 dot parsing; normalize only below their root. + const text = pathText(value).replaceAll("/", "\\"); + const root = + /^\\\\\?\\(?:UNC\\[^\\]+\\[^\\]+(?:\\|$)|[^\\]+\\)/iu.exec( + text, + )?.[0] ?? win32.parse(text).root; + normalizedText = + root + + win32 + .join("\\", text.slice(root.length)) + .slice(1) + .replace(/\\+$/u, ""); + } else { + const text = pathText(absolute(value)); + const root = win32.parse(text).root; + normalizedText = root + text.slice(root.length).replace(/\\+$/u, ""); + } + return widePath(normalizedText); } - const normalized = widePath(normalizedText); - const resolved = finalPath(normalized); + if (win32.normalize(pathText(path)).toLowerCase() === "nul") + return widePath("\\\\.\\NUL"); + const normalized = normalize(path); + const seen = new Set(); + let initialError: number | undefined; + function resolveMissing(value: Buffer): Buffer { + const tail: string[] = []; + while (true) { + try { + value = finalPath(value); + break; + } catch (error) { + if (strict) throw error; + const winerror = (error as { winerror?: number }).winerror; + // Match pathlib's non-strict Windows resolution errors. + if ( + ![ + 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1920, 1921, + ].includes(winerror ?? 0) + ) + throw error; + initialError ??= winerror; + // Unicode lowercasing can merge distinct Windows filenames. + const key = pathText(value); + if ((error as { code?: string }).code === "ELOOP" || seen.has(key)) { + check(1921, value); + } + seen.add(key); + const parent = widePath(win32.dirname(pathText(value))); + if (parent.equals(value)) break; + let target: Buffer | undefined; + try { + target = readlink(value); + } catch { + // Missing and ordinary entries have no link target to follow. + } + if (target !== undefined) { + // Native link targets retain literal trailing dots and spaces. + value = normalize( + widePath( + win32.toNamespacedPath( + windowsJoin(pathText(parent), pathText(target)), + ), + ), + ); + continue; + } + tail.push(win32.basename(pathText(value))); + value = parent; + } + } + if (tail.length === 0) return value; + const base = pathText(value).replace(/\\$/u, ""); + // These are filename components; a:stream must not become drive A. + return widePath(`${base}\\${tail.reverse().join("\\")}`); + } + const resolved = resolveMissing(absolute(normalized)); if (pathText(normalized).startsWith("\\\\?\\")) return resolved; const text = pathText(resolved); const shortened = text.startsWith("\\\\?\\UNC\\") @@ -96,8 +197,14 @@ export function windowsFileSystem(native: WindowsBinding) { const candidate = widePath(shortened); try { if (finalPath(candidate).equals(resolved)) return candidate; - } catch { + } catch (error) { // Extended paths can be valid when their ordinary spelling is not. + if ( + !strict && + (error as { winerror?: number }).winerror === initialError && + operationPath(candidate).equals(resolved) + ) + return candidate; } return resolved; } @@ -174,28 +281,72 @@ export function windowsFileSystem(native: WindowsBinding) { return length; } - function writeFile(path: Buffer, buffer: Buffer): void { - const handle = open(path, flags.GENERIC_WRITE, flags.CREATE_ALWAYS); - let offset = 0; + function readFile(path: Buffer): Buffer { + const handle = open(path, flags.GENERIC_READ); + const chunks: Buffer[] = []; try { - while (offset < buffer.length) { - const result = handle.write( - buffer, - offset, - Math.min(buffer.length - offset, 0xffffffff), - ); + while (true) { + const chunk = Buffer.alloc(64 * 1024); + const result = handle.read(chunk, 0, chunk.length); check(result.error, path); - if (result.value === 0) - throw new Error( - `Windows file write made no progress: ${pathText(path)}`, + if (result.value === 0) return Buffer.concat(chunks); + chunks.push(chunk.subarray(0, result.value)); + } + } finally { + check(handle.close(), path); + } + } + + function writeFile( + path: Buffer, + data: Buffer | Iterable, + exclusive = false, + ): void { + const handle = open( + path, + flags.GENERIC_WRITE, + exclusive ? flags.CREATE_NEW : flags.CREATE_ALWAYS, + ); + try { + for (const buffer of Buffer.isBuffer(data) ? [data] : data) { + let offset = 0; + while (offset < buffer.length) { + const result = handle.write( + buffer, + offset, + Math.min(buffer.length - offset, 0xffffffff), ); - offset += result.value; + check(result.error, path); + if (result.value === 0) + throw new Error( + `Windows file write made no progress: ${pathText(path)}`, + ); + offset += result.value; + } } } finally { check(handle.close(), path); } } + function rename(source: Buffer, destination: Buffer): void { + const handle = open(source, flags.DELETE, flags.OPEN_EXISTING, false); + try { + check(handle.rename(operationPath(destination), true), destination); + } finally { + check(handle.close(), source); + } + } + + function unlink(path: Buffer): void { + const handle = open(path, flags.DELETE, flags.OPEN_EXISTING, false); + try { + check(handle.setDisposition(true), path); + } finally { + check(handle.close(), path); + } + } + return { absolute, realpath, @@ -203,7 +354,11 @@ export function windowsFileSystem(native: WindowsBinding) { identity, entriesWithTypes, mkdir, + readlink, readInto, + readFile, writeFile, + rename, + unlink, }; } diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 29291731a..8e8cda1fc 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -65,3 +65,99 @@ for (const [input, absolute] of [ ); }); } + +for (const target of ["missing.", "missing "]) { + for (const siblingExists of [true, false]) { + test(`dangling link target ${JSON.stringify(target)} with ordinary sibling present=${siblingExists}`, () => { + const native = { + windowsAbsolutePath(path: Buffer) { + const text = pathText(path); + return { + error: 0, + value: widePath( + text.startsWith("\\\\?\\") ? text : text.replace(/[. ]+$/u, ""), + ), + }; + }, + windowsReadLink(path: Buffer) { + return pathText(path) === "\\\\?\\C:\\links\\link" + ? { error: 0, value: widePath(target) } + : { error: 4390, value: Buffer.alloc(0) }; + }, + openWindowsFile(path: Buffer) { + if ( + ![ + "\\\\?\\C:\\links", + ...(siblingExists ? ["\\\\?\\C:\\links\\missing"] : []), + ].includes(pathText(path)) + ) + return { error: 2, handle: null }; + return { + error: 0, + handle: { + finalPath: () => ({ error: 0, path }), + close: () => 0, + }, + }; + }, + } as unknown as WindowsBinding; + assert.equal( + pathText( + windowsFileSystem(native).realpath( + widePath("C:\\links\\link"), + false, + ), + ), + `\\\\?\\C:\\links\\${target}`, + ); + }); + } +} + +for (const parent of ["C:\\dir", "\\\\server\\share\\dir"]) { + for (const namespaced of [false, true]) { + const input = `${namespaced ? win32.toNamespacedPath(parent) : parent}\\a:stream`; + test(`non-strict resolution preserves the stream parent: ${JSON.stringify(input)}`, () => { + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + windowsReadLink: () => ({ error: 2, value: Buffer.alloc(0) }), + openWindowsFile(path: Buffer) { + return pathText(path) === win32.toNamespacedPath(parent) + ? { + error: 0, + handle: { + finalPath: () => ({ error: 0, path }), + close: () => 0, + }, + } + : { error: 2, handle: null }; + }, + } as unknown as WindowsBinding; + assert.equal( + pathText(windowsFileSystem(native).realpath(widePath(input), false)), + input, + ); + }); + } +} + +test("non-strict resolution handles deeply nested missing paths", () => { + const root = "\\\\?\\C:\\"; + const input = root + "a\\".repeat(8_000) + "missing"; + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + windowsReadLink: () => ({ error: 3, value: Buffer.alloc(0) }), + openWindowsFile(path: Buffer) { + return pathText(path) === root + ? { + error: 0, + handle: { finalPath: () => ({ error: 0, path }), close: () => 0 }, + } + : { error: 3, handle: null }; + }, + } as unknown as WindowsBinding; + assert.equal( + pathText(windowsFileSystem(native).realpath(widePath(input), false)), + input, + ); +}); From e8e3a38255380448448e24a5ce3b02f7116c27b1 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Wed, 9 Sep 2026 18:34:51 +0000 Subject: [PATCH 02/15] fix(plugin): retain Windows directory errors and relative paths --- .../native/proof-windows-wide.mts | 2 + .../codex-security/native/windows-files.mts | 6 +- .../native/windows-files.test.mts | 59 +++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 47909d8fa..71869e226 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -85,6 +85,8 @@ function worker(root: string): Record { `${drive}\\rooted-\ud800`, ); samePath(files.realpath(widePath(".")), cwd); + samePath(files.realpath(widePath(" "), false), win32.join(cwd, " ")); + assert.throws(() => files.readFile(widePath(".")), { winerror: 5 }); for (const name of ["NUL", "nUl", ".\\NUL", "x\\..\\NUL"]) samePath(files.realpath(widePath(name)), "\\\\.\\NUL"); diff --git a/plugins/codex-security/native/windows-files.mts b/plugins/codex-security/native/windows-files.mts index 5a6f12a18..69559fc88 100644 --- a/plugins/codex-security/native/windows-files.mts +++ b/plugins/codex-security/native/windows-files.mts @@ -82,7 +82,7 @@ export function windowsFileSystem(native: WindowsBinding) { access, flags.FILE_SHARE_READ | flags.FILE_SHARE_WRITE | flags.FILE_SHARE_DELETE, disposition, - flags.FILE_FLAG_BACKUP_SEMANTICS | + (access === flags.GENERIC_READ ? 0 : flags.FILE_FLAG_BACKUP_SEMANTICS) | (follow ? 0 : flags.FILE_FLAG_OPEN_REPARSE_POINT), ); check(result.error, path); @@ -131,6 +131,10 @@ export function windowsFileSystem(native: WindowsBinding) { } if (win32.normalize(pathText(path)).toLowerCase() === "nul") return widePath("\\\\.\\NUL"); + if (!win32.isAbsolute(pathText(path))) + path = widePath( + windowsJoin(pathText(absolute(widePath("."))), pathText(path)), + ); const normalized = normalize(path); const seen = new Set(); let initialError: number | undefined; diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 8e8cda1fc..1e0e683a2 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -43,14 +43,12 @@ for (const [input, absolute] of [ ["C:\\file\\", "C:\\file\\"], ] as const) { test(`ordinary realpath uses native absolute resolution: ${JSON.stringify(input)}`, () => { - let absoluteCalls = 0; const native = { windowsAbsolutePath(path: Buffer) { - if (absoluteCalls++ === 0) { - assert.equal(pathText(path), input); - return { error: 0, value: widePath(absolute) }; - } - return { error: 0, value: path }; + return { + error: 0, + value: widePath(win32.resolve("C:\\parent\\child", pathText(path))), + }; }, openWindowsFile(path: Buffer) { const root = win32.parse(absolute).root; @@ -66,6 +64,55 @@ for (const [input, absolute] of [ }); } +for (const share of ["\\\\server\\share", "//server/share"]) { + test(`realpath resolves the UNC share root from a directory on that share: ${share}`, () => { + const native = { + windowsAbsolutePath(path: Buffer) { + return { + error: 0, + value: widePath( + win32.resolve("\\\\server\\share\\nested", pathText(path)), + ), + }; + }, + openWindowsFile(path: Buffer) { + assert.equal(pathText(path), "\\\\?\\UNC\\server\\share\\"); + throw opened; + }, + } as unknown as WindowsBinding; + assert.throws( + () => windowsFileSystem(native).realpath(widePath(share)), + (error) => error === opened, + ); + }); +} + +test("non-strict realpath retains a whitespace-only relative component", () => { + const native = { + windowsAbsolutePath(path: Buffer) { + return pathText(path).trim() === "" + ? { error: 123, value: Buffer.alloc(0) } + : { + error: 0, + value: widePath(win32.resolve("C:\\work", pathText(path))), + }; + }, + windowsReadLink: () => ({ error: 2, value: Buffer.alloc(0) }), + openWindowsFile(path: Buffer) { + return pathText(path) === "\\\\?\\C:\\work" + ? { + error: 0, + handle: { finalPath: () => ({ error: 0, path }), close: () => 0 }, + } + : { error: 2, handle: null }; + }, + } as unknown as WindowsBinding; + assert.equal( + pathText(windowsFileSystem(native).realpath(widePath(" "), false)), + "C:\\work\\ ", + ); +}); + for (const target of ["missing.", "missing "]) { for (const siblingExists of [true, false]) { test(`dangling link target ${JSON.stringify(target)} with ordinary sibling present=${siblingExists}`, () => { From 06112a516431a9e12de644e8621b0176946da0a5 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Wed, 9 Sep 2026 18:51:42 +0000 Subject: [PATCH 03/15] test(plugin): match native Windows whitespace path normalization --- plugins/codex-security/native/proof-windows-wide.mts | 2 +- plugins/codex-security/native/windows-files.test.mts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 71869e226..a36f7f056 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -85,7 +85,7 @@ function worker(root: string): Record { `${drive}\\rooted-\ud800`, ); samePath(files.realpath(widePath(".")), cwd); - samePath(files.realpath(widePath(" "), false), win32.join(cwd, " ")); + samePath(files.realpath(widePath(" "), false), cwd); assert.throws(() => files.readFile(widePath(".")), { winerror: 5 }); for (const name of ["NUL", "nUl", ".\\NUL", "x\\..\\NUL"]) samePath(files.realpath(widePath(name)), "\\\\.\\NUL"); diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 1e0e683a2..2ad1492db 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -87,14 +87,16 @@ for (const share of ["\\\\server\\share", "//server/share"]) { }); } -test("non-strict realpath retains a whitespace-only relative component", () => { +test("non-strict realpath resolves a whitespace-only relative path from cwd", () => { const native = { windowsAbsolutePath(path: Buffer) { return pathText(path).trim() === "" ? { error: 123, value: Buffer.alloc(0) } : { error: 0, - value: widePath(win32.resolve("C:\\work", pathText(path))), + value: widePath( + win32.resolve("C:\\work", pathText(path).replace(/ +$/u, "")), + ), }; }, windowsReadLink: () => ({ error: 2, value: Buffer.alloc(0) }), @@ -109,7 +111,7 @@ test("non-strict realpath retains a whitespace-only relative component", () => { } as unknown as WindowsBinding; assert.equal( pathText(windowsFileSystem(native).realpath(widePath(" "), false)), - "C:\\work\\ ", + "C:\\work", ); }); From 8a518ed4559637a6266036edc57e7441c71292ea Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 10:15:45 +0000 Subject: [PATCH 04/15] Simplify Windows file handle ownership and path resolution --- .../native/proof-windows-wide.mts | 5 +- .../codex-security/native/windows-files.mts | 307 ++++++++---------- .../native/windows-files.test.mts | 138 ++++++++ 3 files changed, 286 insertions(+), 164 deletions(-) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index b3bd695bc..25e31cfdd 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -288,7 +288,10 @@ function worker(root: string): Record { Buffer.from("replacement output untouched"), ); files.writeFile(rawOutput, Buffer.from("a longer initial output")); - files.writeFile(rawOutput, Buffer.alloc(128 * 1024 + 1, 7)); + files.writeFile(rawOutput, [ + Buffer.alloc(64 * 1024, 7), + Buffer.alloc(64 * 1024 + 1, 7), + ]); assert.deepEqual(files.readFile(rawOutput), Buffer.alloc(128 * 1024 + 1, 7)); files.writeFile(rawOutput, Buffer.from("short")); const contents = Buffer.alloc(64); diff --git a/plugins/codex-security/native/windows-files.mts b/plugins/codex-security/native/windows-files.mts index 69559fc88..7e0b73baf 100644 --- a/plugins/codex-security/native/windows-files.mts +++ b/plugins/codex-security/native/windows-files.mts @@ -71,12 +71,13 @@ export function windowsFileSystem(native: WindowsBinding) { ); } - function open( + function withFile( path: Buffer, - access = 0, + access: number, + action: (handle: WindowsHandle) => T, disposition: number = flags.OPEN_EXISTING, follow = true, - ): WindowsHandle { + ): T { const result = native.openWindowsFile( operationPath(path), access, @@ -86,20 +87,23 @@ export function windowsFileSystem(native: WindowsBinding) { (follow ? 0 : flags.FILE_FLAG_OPEN_REPARSE_POINT), ); check(result.error, path); - return result.handle!; - } - - function finalPath(path: Buffer): Buffer { - const handle = open(path); + const handle = result.handle!; try { - const result = handle.finalPath(0); - check(result.error, path); - return result.path; + return action(handle); } finally { check(handle.close(), path); } } + function finalPath(text: string): string { + const path = widePath(text); + return withFile(path, 0, (handle) => { + const result = handle.finalPath(0); + check(result.error, path); + return pathText(result.path); + }); + } + function readlink(path: Buffer): Buffer { const result = native.windowsReadLink(operationPath(path)); check(result.error, path); @@ -107,148 +111,127 @@ export function windowsFileSystem(native: WindowsBinding) { } function realpath(path: Buffer, strict = true): Buffer { - function normalize(value: Buffer): Buffer { - let normalizedText: string; - if (pathText(value).startsWith("\\\\?\\")) { - // Verbatim paths bypass Win32 dot parsing; normalize only below their root. - const text = pathText(value).replaceAll("/", "\\"); - const root = - /^\\\\\?\\(?:UNC\\[^\\]+\\[^\\]+(?:\\|$)|[^\\]+\\)/iu.exec( - text, - )?.[0] ?? win32.parse(text).root; - normalizedText = - root + - win32 - .join("\\", text.slice(root.length)) - .slice(1) - .replace(/\\+$/u, ""); - } else { - const text = pathText(absolute(value)); - const root = win32.parse(text).root; - normalizedText = root + text.slice(root.length).replace(/\\+$/u, ""); - } - return widePath(normalizedText); + function normalize(value: string): string { + const verbatim = value.startsWith("\\\\?\\"); + const text = verbatim + ? value.replaceAll("/", "\\") + : pathText(absolute(widePath(value))); + // Verbatim paths bypass Win32 dot parsing; normalize only below their root. + const root = + (verbatim + ? /^\\\\\?\\(?:UNC\\[^\\]+\\[^\\]+(?:\\|$)|[^\\]+\\)/iu.exec( + text, + )?.[0] + : undefined) ?? win32.parse(text).root; + const tail = text.slice(root.length); + return ( + root + + (verbatim ? win32.join("\\", tail).slice(1) : tail).replace(/\\+$/u, "") + ); } - if (win32.normalize(pathText(path)).toLowerCase() === "nul") + let text = pathText(path); + if (win32.normalize(text).toLowerCase() === "nul") return widePath("\\\\.\\NUL"); - if (!win32.isAbsolute(pathText(path))) - path = widePath( - windowsJoin(pathText(absolute(widePath("."))), pathText(path)), - ); - const normalized = normalize(path); + if (!win32.isAbsolute(text)) + text = windowsJoin(pathText(absolute(widePath("."))), text); + const normalized = normalize(text); + text = pathText(absolute(widePath(normalized))); const seen = new Set(); let initialError: number | undefined; - function resolveMissing(value: Buffer): Buffer { - const tail: string[] = []; - while (true) { + const tail: string[] = []; + while (true) { + try { + text = finalPath(text); + break; + } catch (error) { + if (strict) throw error; + const winerror = (error as { winerror?: number }).winerror; + // Match pathlib's non-strict Windows resolution errors. + if ( + ![ + 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1920, 1921, + ].includes(winerror ?? 0) + ) + throw error; + initialError ??= winerror; + const value = widePath(text); + // Unicode lowercasing can merge distinct Windows filenames. + if (winerror === 1921 || seen.has(text)) check(1921, value); + seen.add(text); + const parent = win32.dirname(text); + if (parent === text) break; + let target: Buffer | undefined; try { - value = finalPath(value); - break; - } catch (error) { - if (strict) throw error; - const winerror = (error as { winerror?: number }).winerror; - // Match pathlib's non-strict Windows resolution errors. - if ( - ![ - 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1920, 1921, - ].includes(winerror ?? 0) - ) - throw error; - initialError ??= winerror; - // Unicode lowercasing can merge distinct Windows filenames. - const key = pathText(value); - if ((error as { code?: string }).code === "ELOOP" || seen.has(key)) { - check(1921, value); - } - seen.add(key); - const parent = widePath(win32.dirname(pathText(value))); - if (parent.equals(value)) break; - let target: Buffer | undefined; - try { - target = readlink(value); - } catch { - // Missing and ordinary entries have no link target to follow. - } - if (target !== undefined) { - // Native link targets retain literal trailing dots and spaces. - value = normalize( - widePath( - win32.toNamespacedPath( - windowsJoin(pathText(parent), pathText(target)), - ), - ), - ); - continue; - } - tail.push(win32.basename(pathText(value))); - value = parent; + target = readlink(value); + } catch { + // Missing and ordinary entries have no link target to follow. + } + if (target !== undefined) { + // Native link targets retain literal trailing dots and spaces. + text = normalize( + win32.toNamespacedPath(windowsJoin(parent, pathText(target))), + ); + continue; } + tail.push(win32.basename(text)); + text = parent; } - if (tail.length === 0) return value; - const base = pathText(value).replace(/\\$/u, ""); - // These are filename components; a:stream must not become drive A. - return widePath(`${base}\\${tail.reverse().join("\\")}`); } - const resolved = resolveMissing(absolute(normalized)); - if (pathText(normalized).startsWith("\\\\?\\")) return resolved; - const text = pathText(resolved); + // These are filename components; a:stream must not become drive A. + if (tail.length) + text = `${text.replace(/\\$/u, "")}\\${tail.reverse().join("\\")}`; + if (normalized.startsWith("\\\\?\\")) return widePath(text); const shortened = text.startsWith("\\\\?\\UNC\\") ? `\\\\${text.slice(8)}` : text.startsWith("\\\\?\\") ? text.slice(4) : text; // Like pathlib, remove the device prefix only if that spelling resolves too. - const candidate = widePath(shortened); try { - if (finalPath(candidate).equals(resolved)) return candidate; + if (finalPath(shortened) === text) text = shortened; } catch (error) { // Extended paths can be valid when their ordinary spelling is not. if ( !strict && (error as { winerror?: number }).winerror === initialError && - operationPath(candidate).equals(resolved) + pathText(operationPath(widePath(shortened))) === text ) - return candidate; + text = shortened; } - return resolved; + return widePath(text); } function stat(path: Buffer, follow = true) { - const handle = open( + return withFile( path, flags.FILE_READ_ATTRIBUTES, + (handle) => { + const info = handle.attributes(); + check(info.error, path); + const type = handle.fileType(); + check(type.error, path); + const link = !follow && info.reparseTag === 0xa000000c; + const directory = + (info.attributes & flags.FILE_ATTRIBUTE_DIRECTORY) !== 0; + return { + isDirectory: () => !link && directory, + isFile: () => !link && !directory && type.value === 1, + isSymbolicLink: () => link, + isReparsePoint: () => + (info.attributes & flags.FILE_ATTRIBUTE_REPARSE_POINT) !== 0, + }; + }, flags.OPEN_EXISTING, follow, ); - try { - const info = handle.attributes(); - check(info.error, path); - const type = handle.fileType(); - check(type.error, path); - const link = !follow && info.reparseTag === 0xa000000c; - const directory = - (info.attributes & flags.FILE_ATTRIBUTE_DIRECTORY) !== 0; - return { - isDirectory: () => !link && directory, - isFile: () => !link && !directory && type.value === 1, - isSymbolicLink: () => link, - isReparsePoint: () => - (info.attributes & flags.FILE_ATTRIBUTE_REPARSE_POINT) !== 0, - }; - } finally { - check(handle.close(), path); - } } function identity(path: Buffer) { - const handle = open(path, flags.FILE_READ_ATTRIBUTES); - try { + return withFile(path, flags.FILE_READ_ATTRIBUTES, (handle) => { const result = handle.identity(); check(result.error, path); return { volume: result.volume, fileId: result.fileId }; - } finally { - check(handle.close(), path); - } + }); } function entriesWithTypes(path: Buffer) { @@ -266,9 +249,8 @@ export function windowsFileSystem(native: WindowsBinding) { } function readInto(path: Buffer, buffer: Buffer): number { - const handle = open(path, flags.GENERIC_READ); - let length = 0; - try { + return withFile(path, flags.GENERIC_READ, (handle) => { + let length = 0; while (length < buffer.length) { const result = handle.read( buffer, @@ -279,16 +261,13 @@ export function windowsFileSystem(native: WindowsBinding) { if (result.value === 0) break; length += result.value; } - } finally { - check(handle.close(), path); - } - return length; + return length; + }); } function readFile(path: Buffer): Buffer { - const handle = open(path, flags.GENERIC_READ); - const chunks: Buffer[] = []; - try { + return withFile(path, flags.GENERIC_READ, (handle) => { + const chunks: Buffer[] = []; while (true) { const chunk = Buffer.alloc(64 * 1024); const result = handle.read(chunk, 0, chunk.length); @@ -296,9 +275,7 @@ export function windowsFileSystem(native: WindowsBinding) { if (result.value === 0) return Buffer.concat(chunks); chunks.push(chunk.subarray(0, result.value)); } - } finally { - check(handle.close(), path); - } + }); } function writeFile( @@ -306,49 +283,53 @@ export function windowsFileSystem(native: WindowsBinding) { data: Buffer | Iterable, exclusive = false, ): void { - const handle = open( + withFile( path, flags.GENERIC_WRITE, - exclusive ? flags.CREATE_NEW : flags.CREATE_ALWAYS, - ); - try { - for (const buffer of Buffer.isBuffer(data) ? [data] : data) { - let offset = 0; - while (offset < buffer.length) { - const result = handle.write( - buffer, - offset, - Math.min(buffer.length - offset, 0xffffffff), - ); - check(result.error, path); - if (result.value === 0) - throw new Error( - `Windows file write made no progress: ${pathText(path)}`, + (handle) => { + for (const buffer of Buffer.isBuffer(data) ? [data] : data) { + let offset = 0; + while (offset < buffer.length) { + const result = handle.write( + buffer, + offset, + Math.min(buffer.length - offset, 0xffffffff), ); - offset += result.value; + check(result.error, path); + if (result.value === 0) + throw new Error( + `Windows file write made no progress: ${pathText(path)}`, + ); + offset += result.value; + } } - } - } finally { - check(handle.close(), path); - } + }, + exclusive ? flags.CREATE_NEW : flags.CREATE_ALWAYS, + ); } function rename(source: Buffer, destination: Buffer): void { - const handle = open(source, flags.DELETE, flags.OPEN_EXISTING, false); - try { - check(handle.rename(operationPath(destination), true), destination); - } finally { - check(handle.close(), source); - } + withFile( + source, + flags.DELETE, + (handle) => { + check(handle.rename(operationPath(destination), true), destination); + }, + flags.OPEN_EXISTING, + false, + ); } function unlink(path: Buffer): void { - const handle = open(path, flags.DELETE, flags.OPEN_EXISTING, false); - try { - check(handle.setDisposition(true), path); - } finally { - check(handle.close(), path); - } + withFile( + path, + flags.DELETE, + (handle) => { + check(handle.setDisposition(true), path); + }, + flags.OPEN_EXISTING, + false, + ); } return { diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 2ad1492db..63c93788d 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -3,6 +3,7 @@ import { test } from "node:test"; import { win32 } from "node:path"; import { type WindowsBinding } from "./windows-binding.mjs"; import { pathText, widePath, windowsFileSystem } from "./windows-files.mjs"; +import { windowsFlags as flags } from "./windows-flags.mjs"; const opened = new Error("Captured native open"); @@ -210,3 +211,140 @@ test("non-strict resolution handles deeply nested missing paths", () => { input, ); }); + +for (const bounded of [true, false]) { + test(`${bounded ? "bounded" : "complete"} reads continue after short reads and close at EOF`, () => { + const chunks = [Buffer.from("ab"), Buffer.from("c"), Buffer.alloc(0)]; + let closes = 0; + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + openWindowsFile( + path: Buffer, + access: number, + share: number, + disposition: number, + options: number, + ) { + assert.equal(pathText(path), "\\\\?\\C:\\file-\ud800"); + assert.equal(access, flags.GENERIC_READ); + assert.equal( + share, + flags.FILE_SHARE_READ | + flags.FILE_SHARE_WRITE | + flags.FILE_SHARE_DELETE, + ); + assert.equal(disposition, flags.OPEN_EXISTING); + assert.equal(options & flags.FILE_FLAG_BACKUP_SEMANTICS, 0); + return { + error: 0, + handle: { + read(buffer: Buffer, offset: number, length: number) { + const chunk = chunks.shift()!; + assert(chunk.length <= length); + chunk.copy(buffer, offset); + return { error: 0, value: chunk.length }; + }, + close: () => { + closes++; + return 0; + }, + }, + }; + }, + } as unknown as WindowsBinding; + const files = windowsFileSystem(native); + const path = widePath("C:\\file-\ud800"); + if (bounded) { + const buffer = Buffer.alloc(8); + assert.equal(files.readInto(path, buffer), 3); + assert.equal(buffer.subarray(0, 3).toString(), "abc"); + } else assert.equal(files.readFile(path).toString(), "abc"); + assert.equal(chunks.length, 0); + assert.equal(closes, 1); + }); +} + +test("exclusive writes handle short writes without consuming input on open failure", () => { + const written: Buffer[] = []; + let opens = 0; + let closes = 0; + let iterations = 0; + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + openWindowsFile( + path: Buffer, + access: number, + share: number, + disposition: number, + ) { + assert.equal(pathText(path), "\\\\?\\C:\\output-\ud800"); + assert.equal(access, flags.GENERIC_WRITE); + assert.equal( + share, + flags.FILE_SHARE_READ | + flags.FILE_SHARE_WRITE | + flags.FILE_SHARE_DELETE, + ); + assert.equal(disposition, flags.CREATE_NEW); + if (opens++) return { error: 80, handle: null }; + return { + error: 0, + handle: { + write(buffer: Buffer, offset: number, length: number) { + const count = Math.min(length, 2); + written.push(buffer.subarray(offset, offset + count)); + return { error: 0, value: count }; + }, + close: () => { + closes++; + return 0; + }, + }, + }; + }, + } as unknown as WindowsBinding; + const data = { + *[Symbol.iterator]() { + iterations++; + yield Buffer.from("abc"); + yield Buffer.alloc(0); + yield Buffer.from("def"); + }, + }; + const files = windowsFileSystem(native); + const path = widePath("C:\\output-\ud800"); + files.writeFile(path, data, true); + assert.equal(Buffer.concat(written).toString(), "abcdef"); + assert.throws(() => files.writeFile(path, data, true), { winerror: 80 }); + assert.equal(iterations, 1); + assert.equal(closes, 1); +}); + +for (const closeError of [0, 5]) { + test(`write input failures close the handle, preserving close error ${closeError}`, () => { + const inputError = new Error("input failed"); + let closes = 0; + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + openWindowsFile: () => ({ + error: 0, + handle: { + close: () => { + closes++; + return closeError; + }, + }, + }), + } as unknown as WindowsBinding; + const data = { + [Symbol.iterator](): Iterator { + throw inputError; + }, + }; + assert.throws( + () => windowsFileSystem(native).writeFile(widePath("C:\\output"), data), + closeError ? { winerror: closeError } : (error) => error === inputError, + ); + assert.equal(closes, 1); + }); +} From 225aa8a2206e589f7ca0cf19e4df407adcc24bee Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 14:43:48 +0000 Subject: [PATCH 05/15] Simplify Windows adapter regression fixtures --- plugins/codex-security/native/README.md | 2 +- .../native/examples/windows-wide-launcher.rs | 1 - .../native/proof-windows-wide.mts | 6 +- .../native/windows-files.test.mts | 73 ++++++------------- 4 files changed, 24 insertions(+), 58 deletions(-) diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index 62ba82f9e..c793c4141 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -41,7 +41,7 @@ The binding exposes synchronous file and directory creation, attributes and repa Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; the typed adapter uses it to resolve missing paths without losing raw filenames. -`windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. Non-strict `realpath` can retain unresolved components; callers must check containment independently. It also supports missing output paths. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. +`windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. Non-strict `realpath` can retain unresolved components; callers must check containment independently. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. Build on Windows after compiling the TypeScript tools, then run: diff --git a/plugins/codex-security/native/examples/windows-wide-launcher.rs b/plugins/codex-security/native/examples/windows-wide-launcher.rs index 9d2b9a996..15d1e203b 100644 --- a/plugins/codex-security/native/examples/windows-wide-launcher.rs +++ b/plugins/codex-security/native/examples/windows-wide-launcher.rs @@ -59,7 +59,6 @@ fn main() -> std::io::Result<()> { std::os::windows::fs::symlink_file(&names[0], cwd.join("file-link")) })?; if symlinks { - std::os::windows::fs::symlink_file(&names[0], cwd.join("relative-link"))?; std::os::windows::fs::symlink_file(raw("missing-", 0xdfff), cwd.join("missing-link"))?; std::os::windows::fs::symlink_file( Path::new("..").join(raw("missing-", 0xdfff)), diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 25e31cfdd..04186679c 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -37,7 +37,6 @@ function worker(root: string): Record { "loop-link", "missing-link", "missing-parent-link", - "relative-link", "space-target-link", ] : []; @@ -208,11 +207,11 @@ function worker(root: string): Record { assert(files.stat(widePath(".")).isDirectory()); if (symlinks) { assert.deepEqual( - files.readlink(widePath("relative-link")), + files.readlink(widePath("file-link")), widePath(names[0]!), ); assert.deepEqual( - files.readFile(widePath("relative-link")), + files.readFile(widePath("file-link")), Buffer.from("sentinel-0"), ); samePath( @@ -287,7 +286,6 @@ function worker(root: string): Record { replacementOutput, Buffer.from("replacement output untouched"), ); - files.writeFile(rawOutput, Buffer.from("a longer initial output")); files.writeFile(rawOutput, [ Buffer.alloc(64 * 1024, 7), Buffer.alloc(64 * 1024 + 1, 7), diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 63c93788d..8d01a53d7 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -7,7 +7,7 @@ import { windowsFlags as flags } from "./windows-flags.mjs"; const opened = new Error("Captured native open"); -for (const [input, expected] of [ +for (const [input, expected, cwd = "C:\\parent\\child"] of [ ["\\\\?\\C:\\\\..\\file", "\\\\?\\C:\\file"], ["\\\\?\\UNC\\server\\share\\\\..\\file", "\\\\?\\UNC\\server\\share\\file"], [ @@ -19,42 +19,34 @@ for (const [input, expected] of [ ["\\\\?\\C:\\child\\.\\..\\", "\\\\?\\C:\\"], ["\\\\?\\C:\\trailing.\\", "\\\\?\\C:\\trailing."], ["\\\\?\\UNC\\server\\share\\space \\", "\\\\?\\UNC\\server\\share\\space "], + ["C:.\\..\\sentinel", "\\\\?\\C:\\parent\\sentinel"], + ["\\\\server\\share\\..\\file\\", "\\\\?\\UNC\\server\\share\\file"], + ["C:/", "\\\\?\\C:\\"], + ["C:\\file\\", "\\\\?\\C:\\file"], + [ + "\\\\server\\share", + "\\\\?\\UNC\\server\\share\\", + "\\\\server\\share\\nested", + ], + [ + "//server/share", + "\\\\?\\UNC\\server\\share\\", + "\\\\server\\share\\nested", + ], ] as const) { - test(`verbatim realpath preserves its root: ${JSON.stringify(input)}`, () => { - const native = { - windowsAbsolutePath(path: Buffer) { - return { error: 0, value: path }; - }, - openWindowsFile(path: Buffer) { - assert.equal(pathText(path), expected); - throw opened; - }, - } as unknown as WindowsBinding; - assert.throws( - () => windowsFileSystem(native).realpath(widePath(input)), - (error) => error === opened, - ); - }); -} - -for (const [input, absolute] of [ - ["C:.\\..\\sentinel", "C:\\parent\\sentinel"], - ["\\\\server\\share\\..\\file\\", "\\\\server\\share\\file\\"], - ["C:/", "C:\\"], - ["C:\\file\\", "C:\\file\\"], -] as const) { - test(`ordinary realpath uses native absolute resolution: ${JSON.stringify(input)}`, () => { + test(`realpath opens the normalized path: ${JSON.stringify(input)}`, () => { const native = { windowsAbsolutePath(path: Buffer) { + const text = pathText(path); return { error: 0, - value: widePath(win32.resolve("C:\\parent\\child", pathText(path))), + value: text.startsWith("\\\\?\\") + ? path + : widePath(win32.resolve(cwd, text)), }; }, openWindowsFile(path: Buffer) { - const root = win32.parse(absolute).root; - const trimmed = root + absolute.slice(root.length).replace(/\\+$/u, ""); - assert.equal(pathText(path), win32.toNamespacedPath(trimmed)); + assert.equal(pathText(path), expected); throw opened; }, } as unknown as WindowsBinding; @@ -65,29 +57,6 @@ for (const [input, absolute] of [ }); } -for (const share of ["\\\\server\\share", "//server/share"]) { - test(`realpath resolves the UNC share root from a directory on that share: ${share}`, () => { - const native = { - windowsAbsolutePath(path: Buffer) { - return { - error: 0, - value: widePath( - win32.resolve("\\\\server\\share\\nested", pathText(path)), - ), - }; - }, - openWindowsFile(path: Buffer) { - assert.equal(pathText(path), "\\\\?\\UNC\\server\\share\\"); - throw opened; - }, - } as unknown as WindowsBinding; - assert.throws( - () => windowsFileSystem(native).realpath(widePath(share)), - (error) => error === opened, - ); - }); -} - test("non-strict realpath resolves a whitespace-only relative path from cwd", () => { const native = { windowsAbsolutePath(path: Buffer) { From e6ac90d0c533e94093a8f89180ea94ea1ed42781 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 15:36:21 +0000 Subject: [PATCH 06/15] refactor(plugin): use native Windows path semantics --- .../src/helpers/resolve-security-md.ts | 39 ++- plugins/codex-security/native/README.md | 6 +- .../native/examples/windows-wide-launcher.rs | 9 +- .../native/proof-windows-wide.mts | 80 +----- plugins/codex-security/native/src/windows.rs | 17 -- .../codex-security/native/windows-binding.mts | 1 - .../codex-security/native/windows-files.mts | 151 ++-------- .../native/windows-files.test.mts | 262 +++++++----------- 8 files changed, 181 insertions(+), 384 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index e5b38993a..6e87b6d09 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -13,11 +13,7 @@ import { homedir } from "node:os"; import { basename, dirname, parse, sep } from "node:path"; import { parseArgs } from "node:util"; import { unixBinding, windowsBinding } from "../native"; -import { - windowsFileSystem, - windowsJoin, - windowsParts, -} from "../../../native/windows-files.mjs"; +import { windowsFileSystem } from "../../../native/windows-files.mjs"; import { decodePosixBytes, encodePosixPath, @@ -40,6 +36,39 @@ type FileInfo = Pick & { const statPath = (path: Buffer): FileInfo => windows ? windowsFiles().stat(path) : statSync(path); +function windowsParts(value: string): [string, string, string] { + const path = value.replaceAll("/", "\\"); + if (path.startsWith("\\\\")) { + const start = path.slice(0, 8).toUpperCase() === "\\\\?\\UNC\\" ? 8 : 2; + const server = path.indexOf("\\", start); + const share = server === -1 ? -1 : path.indexOf("\\", server + 1); + return share === -1 + ? [value, "", ""] + : [value.slice(0, share), value[share]!, value.slice(share + 1)]; + } + const drive = path[1] === ":" ? 2 : 0; + const root = path[drive] === "\\" ? 1 : 0; + return [ + value.slice(0, drive), + value.slice(drive, drive + root), + value.slice(drive + root), + ]; +} + +function windowsJoin(left: string, right: string): string { + const [leftDrive, leftRoot, leftPath] = windowsParts(left); + const [rightDrive, rightRoot, rightPath] = windowsParts(right); + if (rightRoot) return (rightDrive || leftDrive) + rightRoot + rightPath; + if (rightDrive && rightDrive.toLowerCase() !== leftDrive.toLowerCase()) + return right; + const drive = rightDrive || leftDrive; + const path = + leftPath + (leftPath && !/[/\\]$/u.test(leftPath) ? "\\" : "") + rightPath; + const root = + leftRoot || (path && drive && !/[:/\\]$/u.test(drive) ? "\\" : ""); + return drive + root + path; +} + function parsedPath(value: string): string { // pathlib removes empty and '.' components while preserving symlink/.. pairs. let root = windows diff --git a/plugins/codex-security/native/README.md b/plugins/codex-security/native/README.md index c793c4141..37f090393 100644 --- a/plugins/codex-security/native/README.md +++ b/plugins/codex-security/native/README.md @@ -39,9 +39,9 @@ Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns The binding exposes synchronous file and directory creation, attributes and reparse tags, identity and final/opened names, read/write/seek/size/EOF/flush, exact-handle rename and deletion, and exclusive whole-file locking. Rust's `File` supplies ordinary I/O, cursor-preserving truncation, `sync_all` for flush, and locks. Calls return numeric Windows errors, including 6 for closed handles and 33 for nonblocking lock contention. Buffer ranges, path encoding, and 64-bit seek arguments are checked before use. Overlapped handles are unsupported because pending operations could retain native buffers beyond the call. Path authorization, ancestor traversal, and reparse-point policy remain the caller's responsibility. -Five additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. `windowsReadLink` returns a UTF-16LE link target or its numeric Windows error; the typed adapter uses it to resolve missing paths without losing raw filenames. +Four additional operations preserve Windows strings at the Node boundary. `windowsArguments` returns the complete OS argument vector, including the executable and Node options, using Rust's CRT-compatible parser. `windowsEnvironment` reads one wide environment name and distinguishes an absent value (`null`) from an empty buffer. `windowsAbsolutePath` resolves against the native current directory and drive directories without requiring the destination to exist. `windowsDirectoryEntries` uses `std::fs::read_dir` and cached `DirEntry::file_type()` values without opening each child; names remain UTF-16LE, and construction or iteration failures return their numeric Windows error and an empty array. Directory symlinks and junctions have both directory and symbolic-link flags. The typed adapter exposes this enumerator through `entriesWithTypes`, which `resolve-security-md --list` uses on Windows. -`windows-files.mts` leaves ordinary absolute-path resolution and canonicalization to `GetFullPathNameW` and `GetFinalPathNameByHandleW`, trimming trailing separators below the root. Its small verbatim-path normalizer preserves drive and UNC share roots when resolving dot segments, including literal trailing dots and spaces. Non-strict `realpath` can retain unresolved components; callers must check containment independently. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. +`windows-files.mts` uses native absolute and final paths, preserving raw UTF-16 and the returned device prefix. Non-strict `realpath` resolves the nearest existing ancestor of a missing path; dangling links and other access errors remain errors. Callers must check containment independently. It follows native Windows path behavior rather than emulating Python's special cases for device names, whitespace, verbatim dot segments, and prefix removal. `stat(path, false)` retains exact symbolic-link and reparse-point metadata so callers can reject junction traversal independently of the enumerator's link label. The SDK's public runtime floor remains Node 22.13.0. Node 20.0.0 is an additional native-foundation compatibility proof; it does not change the SDK engine requirement. Build on Windows after compiling the TypeScript tools, then run: @@ -57,7 +57,7 @@ The `native-windows` workflow builds x64 and arm64 with MSVC and a static CRT. I node --expose-gc plugins/codex-security/native/proof-windows.mjs python plugins/codex-security/scripts ``` -The build also compiles the test-only `windows-wide-launcher` Rust example. It starts a Node proof child with lone surrogates in arguments, environment values, and its working directory. That child checks complete directory iteration, distinct surrogate and replacement-character files, canonical paths, bounded reads, output truncation, and recursive long paths through the typed adapter. A Rust file guard with sharing disabled remains open while the child enumerates its name; an explicit data read fails with a sharing violation. Attribute-only access is not blocked by Windows file sharing. Root-normalization tables run on the same matrix. The launcher cleans up the wide fixtures and is never included in the uploaded or bundled native payloads. +The build also compiles the test-only `windows-wide-launcher` Rust example. It starts a Node proof child with lone surrogates in arguments, environment values, and its working directory. That child checks complete directory iteration, distinct surrogate and replacement-character files, canonical paths, bounded reads, output truncation, and recursive long paths through the typed adapter. A Rust file guard with sharing disabled remains open while the child enumerates its name; an explicit data read fails with a sharing violation. Attribute-only access is not blocked by Windows file sharing. Adapter path and I/O tests run on the same matrix. The launcher cleans up the wide fixtures and is never included in the uploaded or bundled native payloads. Creating file and directory symbolic links requires Windows Developer Mode or the symbolic-link privilege. Without it, the proofs still run their other assertions, including directory junctions, and report the skipped symbolic-link assertions as `false` in their JSON output. CI requires real symbolic links on both architectures and also forces the restricted case to exercise both paths. diff --git a/plugins/codex-security/native/examples/windows-wide-launcher.rs b/plugins/codex-security/native/examples/windows-wide-launcher.rs index 15d1e203b..61327415b 100644 --- a/plugins/codex-security/native/examples/windows-wide-launcher.rs +++ b/plugins/codex-security/native/examples/windows-wide-launcher.rs @@ -52,7 +52,6 @@ fn main() -> std::io::Result<()> { for (index, name) in names.iter().enumerate() { fs::write(cwd.join(name), format!("sentinel-{index}"))?; } - fs::write(cwd.join("missing-tail"), "ordinary sibling")?; fs::create_dir(cwd.join("empty"))?; fs::create_dir(cwd.join(raw("directory-", 0xdc80)))?; let symlinks = symlink_fixture(|| { @@ -60,13 +59,7 @@ fn main() -> std::io::Result<()> { })?; if symlinks { std::os::windows::fs::symlink_file(raw("missing-", 0xdfff), cwd.join("missing-link"))?; - std::os::windows::fs::symlink_file( - Path::new("..").join(raw("missing-", 0xdfff)), - cwd.join("missing-parent-link"), - )?; std::os::windows::fs::symlink_file("loop-link", cwd.join("loop-link"))?; - std::os::windows::fs::symlink_file("missing-tail.", cwd.join("dot-target-link"))?; - std::os::windows::fs::symlink_file("missing-tail ", cwd.join("space-target-link"))?; std::os::windows::fs::symlink_dir("empty", cwd.join("directory-link"))?; std::os::windows::fs::symlink_dir( raw("missing-", 0xdfff), @@ -268,7 +261,7 @@ fn main() -> std::io::Result<()> { let mut args = env::args_os().skip(1); let node = args.next().expect("Node executable path"); let script = args.next().expect("Windows wide proof script"); - let root = PathBuf::from(args.next().expect("Proof fixture directory")).join("wide-İprocess"); + let root = PathBuf::from(args.next().expect("Proof fixture directory")).join("wide-process"); fs::create_dir(&root)?; let result = if args.next().is_some_and(|argument| argument == "policy") { policy_proof(node, script, &root) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 04186679c..3b6f80b5e 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -30,15 +30,7 @@ function worker(root: string): Record { ? ["dangling-directory-link", "directory-link"] : []; const links = symlinks - ? [ - ...directoryLinks, - "dot-target-link", - "file-link", - "loop-link", - "missing-link", - "missing-parent-link", - "space-target-link", - ] + ? [...directoryLinks, "file-link", "loop-link", "missing-link"] : []; const cwd = win32.join(root, "cwd-\ud800"); const expectedArguments = [ @@ -102,10 +94,7 @@ function worker(root: string): Record { `${drive}\\rooted-\ud800`, ); samePath(files.realpath(widePath(".")), cwd); - samePath(files.realpath(widePath(" "), false), cwd); assert.throws(() => files.readFile(widePath(".")), { winerror: 5 }); - for (const name of ["NUL", "nUl", ".\\NUL", "x\\..\\NUL"]) - samePath(files.realpath(widePath(name)), "\\\\.\\NUL"); const names = [ "high-\ud800", @@ -128,7 +117,6 @@ function worker(root: string): Record { "space ", "directory-\udc80", "locked-\udfff", - "missing-tail", ...links, ].sort(), ); @@ -190,14 +178,6 @@ function worker(root: string): Record { assert(files.stat(widePath(name)).isFile()); assert(!files.stat(widePath(name), false).isSymbolicLink()); samePath(files.realpath(widePath(name)), win32.join(cwd, name)); - for (const input of [ - `${name}/`, - `${name}\\`, - `${drive}${name}/`, - `${win32.join(cwd, name)}\\`, - ]) { - samePath(files.realpath(widePath(input)), win32.join(cwd, name)); - } } samePath(files.realpath(widePath(`${drive}///`)), `${drive}\\`); samePath( @@ -206,58 +186,29 @@ function worker(root: string): Record { ); assert(files.stat(widePath(".")).isDirectory()); if (symlinks) { - assert.deepEqual( - files.readlink(widePath("file-link")), - widePath(names[0]!), - ); assert.deepEqual( files.readFile(widePath("file-link")), Buffer.from("sentinel-0"), ); samePath( - files.realpath(widePath("missing-link"), false), - win32.join(cwd, "missing-\udfff"), - ); - assert.equal( - pathText( - files.realpath( - widePath(`${win32.toNamespacedPath(cwd)}\\missing-parent-link`), - false, - ), - ).toLowerCase(), - win32.toNamespacedPath(win32.join(root, "missing-\udfff")).toLowerCase(), + files.realpath(widePath("directory-link\\missing-\udfff"), false), + win32.join(cwd, "empty", "missing-\udfff"), ); + for (const path of [ + "missing-link", + "missing-link\\child", + "dangling-directory-link", + "dangling-directory-link\\child", + ]) { + assert.throws(() => files.realpath(widePath(path), false), { + code: "ENOENT", + }); + } assert.throws(() => files.realpath(widePath("loop-link"), false), { code: "ELOOP", }); - for (const siblingExists of [true, false]) { - if (!siblingExists) files.unlink(widePath("missing-tail")); - for (const [link, target] of [ - ["dot-target-link", "missing-tail."], - ["space-target-link", "missing-tail "], - ] as const) { - assert.deepEqual(files.readlink(widePath(link)), widePath(target)); - const resolved = files.realpath(widePath(link), false); - samePath(resolved, win32.join(cwd, target)); - assert.throws(() => files.stat(resolved), { code: "ENOENT" }); - files.writeFile(resolved, Buffer.from("literal link target")); - assert.equal( - files.readFile(widePath(link)).toString(), - "literal link target", - ); - files.unlink(resolved); - if (siblingExists) - assert.equal( - files.readFile(widePath("missing-tail")).toString(), - "ordinary sibling", - ); - else - assert.throws(() => files.stat(widePath("missing-tail")), { - code: "ENOENT", - }); - } - } } + const streamFile = win32.join(cwd, "a"); files.writeFile(widePath(streamFile), Buffer.from("base file")); for (const parent of [cwd, win32.toNamespacedPath(cwd)]) { @@ -275,7 +226,6 @@ function worker(root: string): Record { pathText(files.realpath(widePath(missingDeepPath), false)), missingDeepPath, ); - assert.notEqual(native.windowsReadLink(widePath("empty")).error, 0); const bounded = Buffer.alloc(4); assert.equal(files.readInto(widePath(names[0]!), bounded), 4); assert.equal(bounded.toString(), "sent"); @@ -375,7 +325,6 @@ function worker(root: string): Record { assert.throws(() => native.windowsEnvironment(malformed)); assert.throws(() => native.windowsAbsolutePath(malformed)); assert.throws(() => native.windowsDirectoryEntries(malformed)); - assert.throws(() => native.windowsReadLink(malformed)); } return { rawArgumentsAndCrtQuoting: true, @@ -384,7 +333,6 @@ function worker(root: string): Record { completeWideDirectoryIteration: true, cachedDirectoryAttributesWithoutFileAccess: true, cachedSymlinkTagsIncludingDanglingDirectories: symlinks, - existingFilesWithTrailingSeparators: true, distinctRawAndReplacementFiles: true, canonicalPathsBoundedReadsAndTruncation: true, verbatimTrailingDotsAndSpaces: true, diff --git a/plugins/codex-security/native/src/windows.rs b/plugins/codex-security/native/src/windows.rs index 820e3a92d..1ab3485e3 100644 --- a/plugins/codex-security/native/src/windows.rs +++ b/plugins/codex-security/native/src/windows.rs @@ -169,23 +169,6 @@ pub fn windows_directory_entries(path: Buffer) -> napi::Result napi::Result { - Ok(match std::fs::read_link(os_string(path)?) { - Ok(target) => BufferResult { - error: 0, - value: wide_bytes(target.as_os_str().encode_wide()), - }, - Err(error) => BufferResult { - error: error - .raw_os_error() - .ok_or_else(|| napi::Error::from_reason(error.to_string()))? - as u32, - value: Vec::new().into(), - }, - }) -} - fn io_range(buffer: &Buffer, offset: f64, length: f64) -> napi::Result<(usize, u32)> { if !offset.is_finite() || !length.is_finite() diff --git a/plugins/codex-security/native/windows-binding.mts b/plugins/codex-security/native/windows-binding.mts index 61c346fff..7b90d458b 100644 --- a/plugins/codex-security/native/windows-binding.mts +++ b/plugins/codex-security/native/windows-binding.mts @@ -36,7 +36,6 @@ export interface WindowsBinding { ): WindowsResult< { name: Buffer; isDirectory: boolean; isSymbolicLink: boolean }[] >; - windowsReadLink(path: Buffer): WindowsResult; openWindowsFile( path: Buffer, access: number, diff --git a/plugins/codex-security/native/windows-files.mts b/plugins/codex-security/native/windows-files.mts index 7e0b73baf..e92a59797 100644 --- a/plugins/codex-security/native/windows-files.mts +++ b/plugins/codex-security/native/windows-files.mts @@ -5,39 +5,6 @@ import { windowsFlags as flags } from "./windows-flags.mjs"; export const widePath = (path: string): Buffer => Buffer.from(path, "utf16le"); export const pathText = (path: Buffer): string => path.toString("utf16le"); -export function windowsParts(value: string): [string, string, string] { - const path = value.replaceAll("/", "\\"); - if (path.startsWith("\\\\")) { - const start = path.slice(0, 8).toUpperCase() === "\\\\?\\UNC\\" ? 8 : 2; - const server = path.indexOf("\\", start); - const share = server === -1 ? -1 : path.indexOf("\\", server + 1); - return share === -1 - ? [value, "", ""] - : [value.slice(0, share), value[share]!, value.slice(share + 1)]; - } - const drive = path[1] === ":" ? 2 : 0; - const root = path[drive] === "\\" ? 1 : 0; - return [ - value.slice(0, drive), - value.slice(drive, drive + root), - value.slice(drive + root), - ]; -} - -export function windowsJoin(left: string, right: string): string { - const [leftDrive, leftRoot, leftPath] = windowsParts(left); - const [rightDrive, rightRoot, rightPath] = windowsParts(right); - if (rightRoot) return (rightDrive || leftDrive) + rightRoot + rightPath; - if (rightDrive && rightDrive.toLowerCase() !== leftDrive.toLowerCase()) - return right; - const drive = rightDrive || leftDrive; - const path = - leftPath + (leftPath && !/[/\\]$/u.test(leftPath) ? "\\" : "") + rightPath; - const root = - leftRoot || (path && drive && !/[:/\\]$/u.test(drive) ? "\\" : ""); - return drive + root + path; -} - export function windowsFileSystem(native: WindowsBinding) { function check(error: number, path: Buffer): void { if (error === 0) return; @@ -95,110 +62,39 @@ export function windowsFileSystem(native: WindowsBinding) { } } - function finalPath(text: string): string { - const path = widePath(text); - return withFile(path, 0, (handle) => { - const result = handle.finalPath(0); - check(result.error, path); - return pathText(result.path); - }); - } - - function readlink(path: Buffer): Buffer { - const result = native.windowsReadLink(operationPath(path)); - check(result.error, path); - return result.value; - } - function realpath(path: Buffer, strict = true): Buffer { - function normalize(value: string): string { - const verbatim = value.startsWith("\\\\?\\"); - const text = verbatim - ? value.replaceAll("/", "\\") - : pathText(absolute(widePath(value))); - // Verbatim paths bypass Win32 dot parsing; normalize only below their root. - const root = - (verbatim - ? /^\\\\\?\\(?:UNC\\[^\\]+\\[^\\]+(?:\\|$)|[^\\]+\\)/iu.exec( - text, - )?.[0] - : undefined) ?? win32.parse(text).root; - const tail = text.slice(root.length); - return ( - root + - (verbatim ? win32.join("\\", tail).slice(1) : tail).replace(/\\+$/u, "") - ); - } - let text = pathText(path); - if (win32.normalize(text).toLowerCase() === "nul") - return widePath("\\\\.\\NUL"); - if (!win32.isAbsolute(text)) - text = windowsJoin(pathText(absolute(widePath("."))), text); - const normalized = normalize(text); - text = pathText(absolute(widePath(normalized))); - const seen = new Set(); - let initialError: number | undefined; - const tail: string[] = []; + let current = absolute(path); + const missing: string[] = []; while (true) { try { - text = finalPath(text); - break; + return withFile(current, 0, (handle) => { + const result = handle.finalPath(0); + check(result.error, current); + if (!missing.length) return result.path; + // Join filename components directly so a:stream remains a filename. + return widePath( + `${pathText(result.path).replace(/\\$/u, "")}\\${missing.reverse().join("\\")}`, + ); + }); } catch (error) { - if (strict) throw error; - const winerror = (error as { winerror?: number }).winerror; - // Match pathlib's non-strict Windows resolution errors. - if ( - ![ - 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1920, 1921, - ].includes(winerror ?? 0) - ) + if (strict || (error as NodeJS.ErrnoException).code !== "ENOENT") throw error; - initialError ??= winerror; - const value = widePath(text); - // Unicode lowercasing can merge distinct Windows filenames. - if (winerror === 1921 || seen.has(text)) check(1921, value); - seen.add(text); - const parent = win32.dirname(text); - if (parent === text) break; - let target: Buffer | undefined; try { - target = readlink(value); - } catch { - // Missing and ordinary entries have no link target to follow. - } - if (target !== undefined) { - // Native link targets retain literal trailing dots and spaces. - text = normalize( - win32.toNamespacedPath(windowsJoin(parent, pathText(target))), - ); + withFile(current, 0, () => {}, flags.OPEN_EXISTING, false); + } catch (sourceError) { + if ((sourceError as NodeJS.ErrnoException).code !== "ENOENT") + throw sourceError; + const text = pathText(current); + const parent = win32.dirname(text); + if (parent === text) throw error; + missing.push(win32.basename(text)); + current = widePath(parent); continue; } - tail.push(win32.basename(text)); - text = parent; + // An existing entry that cannot be followed is not a missing output. + throw error; } } - // These are filename components; a:stream must not become drive A. - if (tail.length) - text = `${text.replace(/\\$/u, "")}\\${tail.reverse().join("\\")}`; - if (normalized.startsWith("\\\\?\\")) return widePath(text); - const shortened = text.startsWith("\\\\?\\UNC\\") - ? `\\\\${text.slice(8)}` - : text.startsWith("\\\\?\\") - ? text.slice(4) - : text; - // Like pathlib, remove the device prefix only if that spelling resolves too. - try { - if (finalPath(shortened) === text) text = shortened; - } catch (error) { - // Extended paths can be valid when their ordinary spelling is not. - if ( - !strict && - (error as { winerror?: number }).winerror === initialError && - pathText(operationPath(widePath(shortened))) === text - ) - text = shortened; - } - return widePath(text); } function stat(path: Buffer, follow = true) { @@ -339,7 +235,6 @@ export function windowsFileSystem(native: WindowsBinding) { identity, entriesWithTypes, mkdir, - readlink, readInto, readFile, writeFile, diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 8d01a53d7..1011bb271 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -5,182 +5,132 @@ import { type WindowsBinding } from "./windows-binding.mjs"; import { pathText, widePath, windowsFileSystem } from "./windows-files.mjs"; import { windowsFlags as flags } from "./windows-flags.mjs"; -const opened = new Error("Captured native open"); - -for (const [input, expected, cwd = "C:\\parent\\child"] of [ - ["\\\\?\\C:\\\\..\\file", "\\\\?\\C:\\file"], - ["\\\\?\\UNC\\server\\share\\\\..\\file", "\\\\?\\UNC\\server\\share\\file"], - [ - "\\\\?\\UNC\\server\\share\\..\\other\\file", - "\\\\?\\UNC\\server\\share\\other\\file", - ], - ["\\\\?\\UNC\\server\\share\\child\\..\\..\\", "\\\\?\\UNC\\server\\share\\"], - ["\\\\?\\C:\\..\\file-\ud800", "\\\\?\\C:\\file-\ud800"], - ["\\\\?\\C:\\child\\.\\..\\", "\\\\?\\C:\\"], - ["\\\\?\\C:\\trailing.\\", "\\\\?\\C:\\trailing."], - ["\\\\?\\UNC\\server\\share\\space \\", "\\\\?\\UNC\\server\\share\\space "], - ["C:.\\..\\sentinel", "\\\\?\\C:\\parent\\sentinel"], - ["\\\\server\\share\\..\\file\\", "\\\\?\\UNC\\server\\share\\file"], - ["C:/", "\\\\?\\C:\\"], - ["C:\\file\\", "\\\\?\\C:\\file"], - [ - "\\\\server\\share", - "\\\\?\\UNC\\server\\share\\", - "\\\\server\\share\\nested", - ], - [ - "//server/share", - "\\\\?\\UNC\\server\\share\\", - "\\\\server\\share\\nested", - ], +for (const [input, absolute] of [ + ["C:.\\..\\sentinel", "C:\\parent\\sentinel"], + ["\\\\server\\share", "\\\\server\\share\\"], + ["\\\\?\\C:\\file-\ud800", "\\\\?\\C:\\file-\ud800"], + ["\\\\?\\C:\\trailing.", "\\\\?\\C:\\trailing."], ] as const) { - test(`realpath opens the normalized path: ${JSON.stringify(input)}`, () => { + test(`realpath uses native absolute and final paths: ${JSON.stringify(input)}`, () => { + const final = widePath("\\\\?\\C:\\canonical-\ud800"); + let closes = 0; + let resolutions = 0; const native = { windowsAbsolutePath(path: Buffer) { - const text = pathText(path); + if (resolutions++ === 0) assert.deepEqual(path, widePath(input)); + return { error: 0, value: widePath(absolute) }; + }, + openWindowsFile(path: Buffer) { + assert.equal(pathText(path), win32.toNamespacedPath(absolute)); return { error: 0, - value: text.startsWith("\\\\?\\") - ? path - : widePath(win32.resolve(cwd, text)), + handle: { + finalPath: () => ({ error: 0, path: final }), + close: () => { + closes++; + return 0; + }, + }, }; }, + } as unknown as WindowsBinding; + assert.deepEqual( + windowsFileSystem(native).realpath(widePath(input)), + final, + ); + assert.equal(closes, 1); + }); +} + +for (const [parent, tail] of [ + ["C:\\alias", "missing-\udfff\\child"], + ["C:\\alias", "a:stream"], + ["\\\\server\\share\\alias", "missing"], + ["\\\\?\\C:\\alias", "a\\".repeat(8_000) + "missing"], +] as const) { + test(`non-strict realpath resolves the existing ancestor: ${JSON.stringify(parent)} (${tail.length} chars)`, () => { + const canonical = "\\\\?\\C:\\destination"; + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), openWindowsFile(path: Buffer) { - assert.equal(pathText(path), expected); - throw opened; + return pathText(path) === win32.toNamespacedPath(parent) + ? { + error: 0, + handle: { + finalPath: () => ({ error: 0, path: widePath(canonical) }), + close: () => 0, + }, + } + : { error: 3, handle: null }; }, } as unknown as WindowsBinding; + assert.equal( + pathText( + windowsFileSystem(native).realpath( + widePath(`${parent}\\${tail}`), + false, + ), + ), + `${canonical}\\${tail}`, + ); assert.throws( - () => windowsFileSystem(native).realpath(widePath(input)), - (error) => error === opened, + () => windowsFileSystem(native).realpath(widePath(`${parent}\\${tail}`)), + { code: "ENOENT" }, ); }); } -test("non-strict realpath resolves a whitespace-only relative path from cwd", () => { - const native = { - windowsAbsolutePath(path: Buffer) { - return pathText(path).trim() === "" - ? { error: 123, value: Buffer.alloc(0) } - : { - error: 0, - value: widePath( - win32.resolve("C:\\work", pathText(path).replace(/ +$/u, "")), - ), - }; - }, - windowsReadLink: () => ({ error: 2, value: Buffer.alloc(0) }), - openWindowsFile(path: Buffer) { - return pathText(path) === "\\\\?\\C:\\work" - ? { - error: 0, - handle: { finalPath: () => ({ error: 0, path }), close: () => 0 }, - } - : { error: 2, handle: null }; - }, - } as unknown as WindowsBinding; - assert.equal( - pathText(windowsFileSystem(native).realpath(widePath(" "), false)), - "C:\\work", - ); -}); - -for (const target of ["missing.", "missing "]) { - for (const siblingExists of [true, false]) { - test(`dangling link target ${JSON.stringify(target)} with ordinary sibling present=${siblingExists}`, () => { - const native = { - windowsAbsolutePath(path: Buffer) { - const text = pathText(path); - return { - error: 0, - value: widePath( - text.startsWith("\\\\?\\") ? text : text.replace(/[. ]+$/u, ""), - ), - }; - }, - windowsReadLink(path: Buffer) { - return pathText(path) === "\\\\?\\C:\\links\\link" - ? { error: 0, value: widePath(target) } - : { error: 4390, value: Buffer.alloc(0) }; - }, - openWindowsFile(path: Buffer) { - if ( - ![ - "\\\\?\\C:\\links", - ...(siblingExists ? ["\\\\?\\C:\\links\\missing"] : []), - ].includes(pathText(path)) - ) - return { error: 2, handle: null }; - return { - error: 0, - handle: { - finalPath: () => ({ error: 0, path }), - close: () => 0, - }, - }; - }, - } as unknown as WindowsBinding; - assert.equal( - pathText( - windowsFileSystem(native).realpath( - widePath("C:\\links\\link"), - false, - ), +for (const suffix of ["", "\\child"]) { + test(`non-strict realpath rejects dangling reparse points${suffix}`, () => { + let closes = 0; + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + openWindowsFile( + path: Buffer, + _access: number, + _share: number, + _disposition: number, + options: number, + ) { + return pathText(path) === "\\\\?\\C:\\link" && + options & flags.FILE_FLAG_OPEN_REPARSE_POINT + ? { + error: 0, + handle: { + close: () => { + closes++; + return 0; + }, + }, + } + : { error: 3, handle: null }; + }, + } as unknown as WindowsBinding; + assert.throws( + () => + windowsFileSystem(native).realpath( + widePath(`C:\\link${suffix}`), + false, ), - `\\\\?\\C:\\links\\${target}`, - ); - }); - } + { code: "ENOENT" }, + ); + assert.equal(closes, 1); + }); } -for (const parent of ["C:\\dir", "\\\\server\\share\\dir"]) { - for (const namespaced of [false, true]) { - const input = `${namespaced ? win32.toNamespacedPath(parent) : parent}\\a:stream`; - test(`non-strict resolution preserves the stream parent: ${JSON.stringify(input)}`, () => { - const native = { - windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), - windowsReadLink: () => ({ error: 2, value: Buffer.alloc(0) }), - openWindowsFile(path: Buffer) { - return pathText(path) === win32.toNamespacedPath(parent) - ? { - error: 0, - handle: { - finalPath: () => ({ error: 0, path }), - close: () => 0, - }, - } - : { error: 2, handle: null }; - }, - } as unknown as WindowsBinding; - assert.equal( - pathText(windowsFileSystem(native).realpath(widePath(input), false)), - input, - ); - }); - } +for (const error of [5, 32, 1921]) { + test(`non-strict realpath preserves native error ${error}`, () => { + const native = { + windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), + openWindowsFile: () => ({ error, handle: null }), + } as unknown as WindowsBinding; + assert.throws( + () => windowsFileSystem(native).realpath(widePath("C:\\file"), false), + { winerror: error }, + ); + }); } -test("non-strict resolution handles deeply nested missing paths", () => { - const root = "\\\\?\\C:\\"; - const input = root + "a\\".repeat(8_000) + "missing"; - const native = { - windowsAbsolutePath: (path: Buffer) => ({ error: 0, value: path }), - windowsReadLink: () => ({ error: 3, value: Buffer.alloc(0) }), - openWindowsFile(path: Buffer) { - return pathText(path) === root - ? { - error: 0, - handle: { finalPath: () => ({ error: 0, path }), close: () => 0 }, - } - : { error: 3, handle: null }; - }, - } as unknown as WindowsBinding; - assert.equal( - pathText(windowsFileSystem(native).realpath(widePath(input), false)), - input, - ); -}); - for (const bounded of [true, false]) { test(`${bounded ? "bounded" : "complete"} reads continue after short reads and close at EOF`, () => { const chunks = [Buffer.from("ab"), Buffer.from("c"), Buffer.alloc(0)]; From 8ade4608a005d4798439b6f37cbf07f48b315f37 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 15:49:39 +0000 Subject: [PATCH 07/15] fix(plugin): join Windows scopes before restoring native prefixes --- .../mcp-app/src/helpers/resolve-security-md.ts | 14 +++++++++++++- .../native/examples/windows-wide-launcher.rs | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index 6e87b6d09..59b2dd541 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -10,7 +10,7 @@ import { type Stats, } from "node:fs"; import { homedir } from "node:os"; -import { basename, dirname, parse, sep } from "node:path"; +import { basename, dirname, parse, sep, win32 } from "node:path"; import { parseArgs } from "node:util"; import { unixBinding, windowsBinding } from "../native"; import { windowsFileSystem } from "../../../native/windows-files.mjs"; @@ -56,6 +56,18 @@ function windowsParts(value: string): [string, string, string] { } function windowsJoin(left: string, right: string): string { + if (left.startsWith("\\\\?\\")) { + const base = left.startsWith("\\\\?\\UNC\\") + ? `\\\\${left.slice(8)}` + : left.slice(4); + if (win32.isAbsolute(base)) { + // Join scopes before restoring the prefix that preserves raw filenames. + const joined = windowsJoin(base, right); + return win32.isAbsolute(joined) && !joined.startsWith("\\\\?\\") + ? win32.toNamespacedPath(joined) + : joined; + } + } const [leftDrive, leftRoot, leftPath] = windowsParts(left); const [rightDrive, rightRoot, rightPath] = windowsParts(right); if (rightRoot) return (rightDrive || leftDrive) + rightRoot + rightPath; diff --git a/plugins/codex-security/native/examples/windows-wide-launcher.rs b/plugins/codex-security/native/examples/windows-wide-launcher.rs index 61327415b..395f8ef83 100644 --- a/plugins/codex-security/native/examples/windows-wide-launcher.rs +++ b/plugins/codex-security/native/examples/windows-wide-launcher.rs @@ -205,6 +205,15 @@ fn main() -> std::io::Result<()> { } fs::remove_file(&output)?; } + for scope in [PathBuf::from("."), PathBuf::from(&scopes[0]).join("..")] { + let child = invoke(&["--repo".into(), repo.clone(), "--scope".into(), scope])?; + let expected = b"## SECURITY.md source: \"SECURITY.md\"\n\nroot raw\n"; + if !child.status.success() || !child.stderr.is_empty() || child.stdout != expected { + return Err(io::Error::other( + "Windows policy helper did not resolve the root scope", + )); + } + } let listing = invoke(&["--repo".into(), "~".into(), "--list".into()])?; let expected = b"[\"SECURITY.md\", \"scope-\\udfff/SECURITY.md\", \"scope-\\ufffd/SECURITY.md\"]\n"; From 0950a761084930732a98ec448fe7ead184e0fc52 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 16:02:53 +0000 Subject: [PATCH 08/15] fix(plugin): preserve native Windows path identity --- .../src/helpers/resolve-security-md.ts | 4 +- .../native/proof-windows-wide.mts | 9 +++++ .../codex-security/native/windows-files.mts | 2 +- .../native/windows-files.test.mts | 40 +++++++++++++++++++ .../tests-ts/security-policy-helper.test.ts | 4 +- 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index 59b2dd541..a7ad57acd 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -63,7 +63,9 @@ function windowsJoin(left: string, right: string): string { if (win32.isAbsolute(base)) { // Join scopes before restoring the prefix that preserves raw filenames. const joined = windowsJoin(base, right); - return win32.isAbsolute(joined) && !joined.startsWith("\\\\?\\") + return !win32.isAbsolute(right) && + win32.isAbsolute(joined) && + !joined.startsWith("\\\\?\\") ? win32.toNamespacedPath(joined) : joined; } diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 3b6f80b5e..3a56571a7 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -294,6 +294,15 @@ function worker(root: string): Record { win32.join(cwd, `directory-${name.slice(0, -1)}`), ); files.mkdir(ordinaryDirectory); + const missing = files.realpath( + widePath(win32.join(cwd, `directory-${name}`, "new.json")), + false, + ); + files.writeFile(missing, Buffer.from("new literal child")); + assert.equal( + files.readFile(widePath(`${pathText(directory)}\\new.json`)).toString(), + "new literal child", + ); assert.deepEqual(files.entriesWithTypes(ordinaryDirectory), []); } diff --git a/plugins/codex-security/native/windows-files.mts b/plugins/codex-security/native/windows-files.mts index e92a59797..de57b7f89 100644 --- a/plugins/codex-security/native/windows-files.mts +++ b/plugins/codex-security/native/windows-files.mts @@ -63,7 +63,7 @@ export function windowsFileSystem(native: WindowsBinding) { } function realpath(path: Buffer, strict = true): Buffer { - let current = absolute(path); + let current = operationPath(path); const missing: string[] = []; while (true) { try { diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 1011bb271..745043c18 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -80,6 +80,46 @@ for (const [parent, tail] of [ }); } +for (const suffix of [".", " "]) { + test(`non-strict realpath preserves a parent's literal ${JSON.stringify(suffix)}`, () => { + const parent = `C:\\root\\dir${suffix}`; + const native = { + windowsAbsolutePath(path: Buffer) { + const text = pathText(path); + return { + error: 0, + value: widePath( + text.startsWith("\\\\?\\") ? text : text.replace(/[. ]+$/u, ""), + ), + }; + }, + openWindowsFile(path: Buffer) { + return [ + win32.toNamespacedPath(parent), + "\\\\?\\C:\\root\\dir", + ].includes(pathText(path)) + ? { + error: 0, + handle: { + finalPath: () => ({ error: 0, path }), + close: () => 0, + }, + } + : { error: 2, handle: null }; + }, + } as unknown as WindowsBinding; + assert.equal( + pathText( + windowsFileSystem(native).realpath( + widePath(`${parent}\\new.json`), + false, + ), + ), + `\\\\?\\${parent}\\new.json`, + ); + }); +} + for (const suffix of ["", "\\child"]) { test(`non-strict realpath rejects dangling reparse points${suffix}`, () => { let closes = 0; diff --git a/sdk/typescript/tests-ts/security-policy-helper.test.ts b/sdk/typescript/tests-ts/security-policy-helper.test.ts index 74d2b78e0..0efc2f290 100644 --- a/sdk/typescript/tests-ts/security-policy-helper.test.ts +++ b/sdk/typescript/tests-ts/security-policy-helper.test.ts @@ -787,7 +787,7 @@ describe("built SECURITY.md helper", () => { }); test.skipIf(process.platform !== "win32")( - "resolves drive-relative and rooted scopes using the repository drive", + "resolves Windows scopes using the repository drive and native normalization", () => { const { root } = fixture("İrepository"); write(root, "src/SECURITY.md", "component policy\n"); @@ -796,6 +796,8 @@ describe("built SECURITY.md helper", () => { for (const scope of [ `${drive}src\\app.ts`, join(root, "src", "app.ts").slice(2), + `${join(root, "src")}.`, + `${join(root, "src").slice(2)}.`, ]) { const result = run( ["--repo", root, "--scope", scope], From 702b6637b5a8214c57b43a452ef999c30118bc78 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 16:19:46 +0000 Subject: [PATCH 09/15] test(plugin): preserve native prefixes in Windows proof comparisons --- plugins/codex-security/native/proof-windows-wide.mts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 3a56571a7..133ec8678 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -69,9 +69,11 @@ function worker(root: string): Record { assert.deepEqual(environment("USERPROFILE"), widePath(cwd)); function samePath(actual: Buffer, expected: string): void { + const namespaced = (path: string) => + path.startsWith("\\\\?\\") ? path : win32.toNamespacedPath(path); assert.equal( - win32.toNamespacedPath(pathText(actual)).toLowerCase(), - win32.toNamespacedPath(expected).toLowerCase(), + namespaced(pathText(actual)).toLowerCase(), + namespaced(expected).toLowerCase(), ); } samePath(files.absolute(widePath(".")), cwd); From fde0f66d57bc630d6a3533358edeba9d145f2ef8 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 16:35:55 +0000 Subject: [PATCH 10/15] test(plugin): use native paths for literal Windows fixtures --- .../native/proof-windows-wide.mts | 2 +- .../native/windows-files.test.mts | 42 +------------------ 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/plugins/codex-security/native/proof-windows-wide.mts b/plugins/codex-security/native/proof-windows-wide.mts index 133ec8678..cadd8ccd1 100644 --- a/plugins/codex-security/native/proof-windows-wide.mts +++ b/plugins/codex-security/native/proof-windows-wide.mts @@ -297,7 +297,7 @@ function worker(root: string): Record { ); files.mkdir(ordinaryDirectory); const missing = files.realpath( - widePath(win32.join(cwd, `directory-${name}`, "new.json")), + widePath(`${pathText(directory)}\\new.json`), false, ); files.writeFile(missing, Buffer.from("new literal child")); diff --git a/plugins/codex-security/native/windows-files.test.mts b/plugins/codex-security/native/windows-files.test.mts index 745043c18..f869e69bd 100644 --- a/plugins/codex-security/native/windows-files.test.mts +++ b/plugins/codex-security/native/windows-files.test.mts @@ -46,6 +46,8 @@ for (const [parent, tail] of [ ["C:\\alias", "missing-\udfff\\child"], ["C:\\alias", "a:stream"], ["\\\\server\\share\\alias", "missing"], + ["\\\\?\\C:\\trailing.", "new.json"], + ["\\\\?\\C:\\space ", "new.json"], ["\\\\?\\C:\\alias", "a\\".repeat(8_000) + "missing"], ] as const) { test(`non-strict realpath resolves the existing ancestor: ${JSON.stringify(parent)} (${tail.length} chars)`, () => { @@ -80,46 +82,6 @@ for (const [parent, tail] of [ }); } -for (const suffix of [".", " "]) { - test(`non-strict realpath preserves a parent's literal ${JSON.stringify(suffix)}`, () => { - const parent = `C:\\root\\dir${suffix}`; - const native = { - windowsAbsolutePath(path: Buffer) { - const text = pathText(path); - return { - error: 0, - value: widePath( - text.startsWith("\\\\?\\") ? text : text.replace(/[. ]+$/u, ""), - ), - }; - }, - openWindowsFile(path: Buffer) { - return [ - win32.toNamespacedPath(parent), - "\\\\?\\C:\\root\\dir", - ].includes(pathText(path)) - ? { - error: 0, - handle: { - finalPath: () => ({ error: 0, path }), - close: () => 0, - }, - } - : { error: 2, handle: null }; - }, - } as unknown as WindowsBinding; - assert.equal( - pathText( - windowsFileSystem(native).realpath( - widePath(`${parent}\\new.json`), - false, - ), - ), - `\\\\?\\${parent}\\new.json`, - ); - }); -} - for (const suffix of ["", "\\child"]) { test(`non-strict realpath rejects dangling reparse points${suffix}`, () => { let closes = 0; From e9da224824d39303e904f9b231db350838479056 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 29 Sep 2026 17:29:43 +0000 Subject: [PATCH 11/15] refactor(plugin): delegate Windows policy paths to node --- .../src/helpers/resolve-security-md.ts | 86 ++++++------------- .../tests-ts/security-policy-helper.test.ts | 24 ++++-- 2 files changed, 41 insertions(+), 69 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index a7ad57acd..f94ad872f 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -36,70 +36,38 @@ type FileInfo = Pick & { const statPath = (path: Buffer): FileInfo => windows ? windowsFiles().stat(path) : statSync(path); -function windowsParts(value: string): [string, string, string] { - const path = value.replaceAll("/", "\\"); - if (path.startsWith("\\\\")) { - const start = path.slice(0, 8).toUpperCase() === "\\\\?\\UNC\\" ? 8 : 2; - const server = path.indexOf("\\", start); - const share = server === -1 ? -1 : path.indexOf("\\", server + 1); - return share === -1 - ? [value, "", ""] - : [value.slice(0, share), value[share]!, value.slice(share + 1)]; - } - const drive = path[1] === ":" ? 2 : 0; - const root = path[drive] === "\\" ? 1 : 0; - return [ - value.slice(0, drive), - value.slice(drive, drive + root), - value.slice(drive + root), - ]; -} - function windowsJoin(left: string, right: string): string { - if (left.startsWith("\\\\?\\")) { - const base = left.startsWith("\\\\?\\UNC\\") - ? `\\\\${left.slice(8)}` - : left.slice(4); - if (win32.isAbsolute(base)) { - // Join scopes before restoring the prefix that preserves raw filenames. - const joined = windowsJoin(base, right); - return !win32.isAbsolute(right) && - win32.isAbsolute(joined) && - !joined.startsWith("\\\\?\\") - ? win32.toNamespacedPath(joined) - : joined; - } - } - const [leftDrive, leftRoot, leftPath] = windowsParts(left); - const [rightDrive, rightRoot, rightPath] = windowsParts(right); - if (rightRoot) return (rightDrive || leftDrive) + rightRoot + rightPath; - if (rightDrive && rightDrive.toLowerCase() !== leftDrive.toLowerCase()) + if (right.startsWith("\\\\?\\") || right.startsWith("\\\\.\\")) return right; + const namespaced = left.startsWith("\\\\?\\"); + const base = left.startsWith("\\\\?\\UNC\\") + ? `\\\\${left.slice(8)}` + : namespaced + ? left.slice(4) + : left; + const drive = win32.parse(right).root; + if ( + drive.endsWith(":") && + drive.toLowerCase() !== base.slice(0, 2).toLowerCase() + ) return right; - const drive = rightDrive || leftDrive; - const path = - leftPath + (leftPath && !/[/\\]$/u.test(leftPath) ? "\\" : "") + rightPath; - const root = - leftRoot || (path && drive && !/[:/\\]$/u.test(drive) ? "\\" : ""); - return drive + root + path; + const joined = win32.resolve(base, right); + return namespaced && !win32.isAbsolute(right) + ? win32.toNamespacedPath(joined) + : joined; } function parsedPath(value: string): string { - // pathlib removes empty and '.' components while preserving symlink/.. pairs. - let root = windows - ? windowsParts(value).slice(0, 2).join("").replaceAll("/", "\\") + // Preserve symlink/.. pairs while removing empty and '.' components. + const root = windows + ? win32.parse(value).root.replaceAll("/", "\\") : value.startsWith("//") && !value.startsWith("///") ? "//" : parse(value).root; - if (windows && root.startsWith("\\\\") && !root.endsWith("\\")) { - const parts = root.split("\\"); - if ((parts.length === 4 && !"?.".includes(parts[2]!)) || parts.length === 6) - root += "\\"; - } const parts = value .slice(root.length) - .split(process.platform === "win32" ? /[/\\]/u : /\//u) + .split(windows ? /[/\\]/u : /\//u) .filter((part) => part !== "" && part !== "."); - if (windows && !root && windowsParts(parts[0] ?? "")[0]) parts.unshift("."); + if (windows && !root && win32.parse(parts[0] ?? "").root) parts.unshift("."); return root + parts.join(sep) || "."; } @@ -128,23 +96,19 @@ function expandHome(path: string, posixHome: string | undefined): string { let home = environment("USERPROFILE"); const homePath = environment("HOMEPATH"); if (home === undefined && homePath !== undefined) { - home = windowsJoin(environment("HOMEDRIVE") ?? "", homePath); + home = `${environment("HOMEDRIVE") ?? ""}${homePath}`; } if (home === undefined) throw new HomeExpansionError("Could not determine home directory."); if (username !== "" && username !== currentUsername) { - const [drive, root, tail] = windowsParts(home); - const separator = Math.max(tail.lastIndexOf("/"), tail.lastIndexOf("\\")); - if (currentUsername !== tail.slice(separator + 1)) { + if (currentUsername !== win32.basename(home)) { throw new HomeExpansionError("Could not determine home directory."); } - const parent = - drive + root + tail.slice(0, separator + 1).replace(/[/\\]+$/u, ""); - home = windowsJoin(parent, username); + home = win32.join(win32.dirname(home), username); } if (home.startsWith("~")) throw new HomeExpansionError("Could not determine home directory."); - return windowsJoin(home, separator === -1 ? "" : path.slice(end + 1)); + return win32.join(home, separator === -1 ? "" : path.slice(end + 1)); } if (path === "~" || path.startsWith("~/")) { const home = posixHome ?? homedir(); diff --git a/sdk/typescript/tests-ts/security-policy-helper.test.ts b/sdk/typescript/tests-ts/security-policy-helper.test.ts index 0efc2f290..4b6a64b6d 100644 --- a/sdk/typescript/tests-ts/security-policy-helper.test.ts +++ b/sdk/typescript/tests-ts/security-policy-helper.test.ts @@ -175,7 +175,7 @@ describe("built SECURITY.md helper", () => { expect(result.stdout).toContain("home-variable policy"); } expect(run(["--repo", root, "--scope", "~"], homeEnv({})).status).toBe(1); - const other = homeEnv({ USERPROFILE: `${home}\\`, USERNAME: "current" }); + const other = homeEnv({ USERPROFILE: home, USERNAME: "different" }); expect(run(["--repo", "~other", "--scope", "."], other).status).toBe(1); }, ); @@ -736,13 +736,18 @@ describe("built SECURITY.md helper", () => { const profiles = join(root, "profiles"); write(profiles, "current/SECURITY.md", "current policy\n"); write(profiles, "sibling/SECURITY.md", "sibling policy\n"); - const result = run(["--repo", "~sibling", "--scope", "~sibling"], { - ...process.env, - USERPROFILE: join(profiles, "current"), - USERNAME: "current", - }); - expect(result.status, result.stderr).toBe(0); - expect(result.stdout).toContain("sibling policy\n"); + for (const home of [ + join(profiles, "current"), + `${join(profiles, "current")}\\`, + ]) { + const result = run(["--repo", "~sibling", "--scope", "~sibling"], { + ...process.env, + USERPROFILE: home, + USERNAME: "current", + }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("sibling policy\n"); + } }, ); @@ -795,6 +800,9 @@ describe("built SECURITY.md helper", () => { const drive = root.slice(0, 2); for (const scope of [ `${drive}src\\app.ts`, + "src/app.ts", + "src/../src/app.ts", + win32.toNamespacedPath(join(root, "src", "app.ts")), join(root, "src", "app.ts").slice(2), `${join(root, "src")}.`, `${join(root, "src").slice(2)}.`, From 8359c3c210bd269bf9c8096df183a06ccf780b68 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 2 Oct 2026 02:21:51 +0000 Subject: [PATCH 12/15] fix: preserve Windows policy path semantics --- .../src/helpers/resolve-security-md.ts | 35 ++++++++++++------ .../tests-ts/security-policy-helper.test.ts | 37 +++++++++++++++---- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index f94ad872f..033e930ef 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -85,6 +85,12 @@ function resolvedPath(path: Buffer): Buffer { function expandHome(path: string, posixHome: string | undefined): string { if (!path.startsWith("~")) return path; if (process.platform === "win32") { + // path.join('C:', 'name') is rooted; 'C:.' keeps it drive-relative. + const joinHome = (home: string, child: string) => + win32.join( + home.length === 2 && home[1] === ":" ? `${home}.` : home, + child, + ); const environment = (name: string) => windowsBinding() .windowsEnvironment(Buffer.from(name, "utf16le")) @@ -104,11 +110,11 @@ function expandHome(path: string, posixHome: string | undefined): string { if (currentUsername !== win32.basename(home)) { throw new HomeExpansionError("Could not determine home directory."); } - home = win32.join(win32.dirname(home), username); + home = joinHome(win32.dirname(home), username); } if (home.startsWith("~")) throw new HomeExpansionError("Could not determine home directory."); - return win32.join(home, separator === -1 ? "" : path.slice(end + 1)); + return joinHome(home, separator === -1 ? "" : path.slice(end + 1)); } if (path === "~" || path.startsWith("~/")) { const home = posixHome ?? homedir(); @@ -329,17 +335,24 @@ function resolveSecurityMd( ): string { const root = resolveRoot(repo, posixHome); const expandedScope = parsedPath(expandHome(scope, posixHome)); - const requestedScope = - process.platform === "win32" - ? windowsFiles().absolute( - encodePath(windowsJoin(decodePath(root), expandedScope)), - ) - : expandedScope.startsWith("/") - ? encodePosixPath(expandedScope) - : appendPath(root, encodePosixPath(expandedScope)); + let requestedScope: Buffer; + if (windows) { + const files = windowsFiles(); + // Preserve the requested namespace semantics when joining relative scopes. + const requestedRoot = files.absolute( + encodePath(parsedPath(expandHome(repo, posixHome))), + ); + requestedScope = files.absolute( + encodePath(windowsJoin(decodePath(requestedRoot), expandedScope)), + ); + } else { + requestedScope = expandedScope.startsWith("/") + ? encodePosixPath(expandedScope) + : appendPath(root, encodePosixPath(expandedScope)); + } let resolvedScope: Buffer; try { - // Resolve links before '..', including Python's accepted file/.. paths. + // On POSIX, resolve links before '..', including accepted file/.. paths. resolvedScope = resolvedPath(requestedScope); } catch (error) { if (error instanceof SymlinkLoopError) throw error; diff --git a/sdk/typescript/tests-ts/security-policy-helper.test.ts b/sdk/typescript/tests-ts/security-policy-helper.test.ts index 4b6a64b6d..e7dfedcfe 100644 --- a/sdk/typescript/tests-ts/security-policy-helper.test.ts +++ b/sdk/typescript/tests-ts/security-policy-helper.test.ts @@ -162,6 +162,8 @@ describe("built SECURITY.md helper", () => { ], [{ HOMEDRIVE: drive, HOMEPATH: "current" }, "~/project", root], [{ USERPROFILE: `${drive}current` }, "~/project", root], + [{ USERPROFILE: drive }, "~/project", home], + [{ HOMEDRIVE: drive, HOMEPATH: "" }, "~/project", home], [{ USERPROFILE: "" }, "~", join(home, "project")], [{ HOMEDRIVE: drive, HOMEPATH: "" }, "~", join(home, "project")], ]; @@ -736,15 +738,20 @@ describe("built SECURITY.md helper", () => { const profiles = join(root, "profiles"); write(profiles, "current/SECURITY.md", "current policy\n"); write(profiles, "sibling/SECURITY.md", "sibling policy\n"); - for (const home of [ - join(profiles, "current"), - `${join(profiles, "current")}\\`, + for (const [home, cwd] of [ + [join(profiles, "current"), undefined], + [`${join(profiles, "current")}\\`, undefined], + [`${root.slice(0, 2)}current`, profiles], ]) { - const result = run(["--repo", "~sibling", "--scope", "~sibling"], { - ...process.env, - USERPROFILE: home, - USERNAME: "current", - }); + const result = run( + ["--repo", "~sibling", "--scope", "~sibling"], + { + ...process.env, + USERPROFILE: home, + USERNAME: "current", + }, + cwd, + ); expect(result.status, result.stderr).toBe(0); expect(result.stdout).toContain("sibling policy\n"); } @@ -797,11 +804,17 @@ describe("built SECURITY.md helper", () => { const { root } = fixture("İrepository"); write(root, "src/SECURITY.md", "component policy\n"); write(root, "src/app.ts", "export {};\n"); + const literalRoot = win32.toNamespacedPath(join(root, "src.")); + write(literalRoot, "SECURITY.md", "literal directory policy\n"); const drive = root.slice(0, 2); for (const scope of [ `${drive}src\\app.ts`, "src/app.ts", "src/../src/app.ts", + "src.", + "src ", + `${drive}src.`, + `${drive}src `, win32.toNamespacedPath(join(root, "src", "app.ts")), join(root, "src", "app.ts").slice(2), `${join(root, "src")}.`, @@ -817,6 +830,14 @@ describe("built SECURITY.md helper", () => { ["src/SECURITY.md", "component policy"], ]); } + for (const [repo, scope, source] of [ + [literalRoot, ".", "SECURITY.md"], + [win32.toNamespacedPath(root), "src.", "src./SECURITY.md"], + ] as const) { + const result = run(["--repo", repo, "--scope", scope]); + expect(result.status, result.stderr).toBe(0); + expectGuidance(result.stdout, [[source, "literal directory policy"]]); + } }, ); From aa5e39d44caf716788de74cf75c29a876b25c7fc Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 2 Oct 2026 02:28:05 +0000 Subject: [PATCH 13/15] fix: retain device namespaces when joining policy scopes --- .../src/helpers/resolve-security-md.ts | 2 +- .../native/proof-policy-windows.mts | 56 ++++++++++++++++++- .../codex-security/native/windows-flags.mts | 1 + 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index 033e930ef..23083fb94 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -41,7 +41,7 @@ function windowsJoin(left: string, right: string): string { const namespaced = left.startsWith("\\\\?\\"); const base = left.startsWith("\\\\?\\UNC\\") ? `\\\\${left.slice(8)}` - : namespaced + : namespaced && win32.isAbsolute(left.slice(4)) ? left.slice(4) : left; const drive = win32.parse(right).root; diff --git a/plugins/codex-security/native/proof-policy-windows.mts b/plugins/codex-security/native/proof-policy-windows.mts index 5ca186a39..5bcd15308 100644 --- a/plugins/codex-security/native/proof-policy-windows.mts +++ b/plugins/codex-security/native/proof-policy-windows.mts @@ -1,9 +1,20 @@ +import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { + copyFileSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { binaryPath, output, root } from "./binding.mjs"; import { nativeTarget } from "./platform.mjs"; +import { + loadWindowsBinding, + windowsFlags as flags, +} from "./windows-binding.mjs"; const testDirectory = join(output, "policy-proof"); const helper = join(testDirectory, "helpers.cjs"); @@ -28,6 +39,42 @@ if (process.argv[2] === "build") { } else { const fixture = mkdtempSync(join(tmpdir(), "codex-security-policy-proof-")); try { + const repo = join(fixture, "volume-repository"); + mkdirSync(repo); + writeFileSync(join(repo, "SECURITY.md"), "volume policy\n"); + const opened = loadWindowsBinding().openWindowsFile( + Buffer.from(repo, "utf16le"), + 0, + flags.FILE_SHARE_READ | flags.FILE_SHARE_WRITE | flags.FILE_SHARE_DELETE, + flags.OPEN_EXISTING, + flags.FILE_FLAG_BACKUP_SEMANTICS, + ); + assert.equal(opened.error, 0); + assert(opened.handle); + let volumePath: string; + try { + const final = opened.handle.finalPath(flags.VOLUME_NAME_GUID); + assert.equal(final.error, 0); + volumePath = final.path.toString("utf16le"); + } finally { + assert.equal(opened.handle.close(), 0); + } + assert.equal( + execFileSync( + process.execPath, + [ + helper, + "--helper", + "resolve-security-md", + "--repo", + volumePath, + "--scope", + ".", + ], + { encoding: "utf8" }, + ), + '## SECURITY.md source: "SECURITY.md"\n\nvolume policy\n', + ); const proof: unknown = JSON.parse( execFileSync( join(output, "windows-wide-launcher.exe"), @@ -36,7 +83,12 @@ if (process.argv[2] === "build") { ), ); console.log( - JSON.stringify({ node: process.version, arch: process.arch, proof }), + JSON.stringify({ + node: process.version, + arch: process.arch, + proof, + volumeGuidScope: true, + }), ); } finally { rmSync(fixture, { recursive: true, force: true }); diff --git a/plugins/codex-security/native/windows-flags.mts b/plugins/codex-security/native/windows-flags.mts index fa88aab07..5d351facf 100644 --- a/plugins/codex-security/native/windows-flags.mts +++ b/plugins/codex-security/native/windows-flags.mts @@ -17,6 +17,7 @@ export const windowsFlags = { FILE_FLAG_OPEN_REPARSE_POINT: 0x00200000, FILE_FLAG_OVERLAPPED: 0x40000000, FILE_NAME_OPENED: 8, + VOLUME_NAME_GUID: 1, FILE_BEGIN: 0, FILE_CURRENT: 1, FILE_END: 2, From 52f8135d637065b07fe2f635df2bed1b5c8bca1c Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 2 Oct 2026 02:36:45 +0000 Subject: [PATCH 14/15] fix: canonicalize explicit device roots before scope joins --- .../src/helpers/resolve-security-md.ts | 14 +++++--- .../native/proof-policy-windows.mts | 36 ++++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index 23083fb94..cc972ee69 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -41,7 +41,7 @@ function windowsJoin(left: string, right: string): string { const namespaced = left.startsWith("\\\\?\\"); const base = left.startsWith("\\\\?\\UNC\\") ? `\\\\${left.slice(8)}` - : namespaced && win32.isAbsolute(left.slice(4)) + : namespaced ? left.slice(4) : left; const drive = win32.parse(right).root; @@ -338,12 +338,16 @@ function resolveSecurityMd( let requestedScope: Buffer; if (windows) { const files = windowsFiles(); - // Preserve the requested namespace semantics when joining relative scopes. - const requestedRoot = files.absolute( - encodePath(parsedPath(expandHome(repo, posixHome))), + const requestedRoot = decodePath( + files.absolute(encodePath(parsedPath(expandHome(repo, posixHome)))), ); + // Keep ordinary paths for OS normalization; canonicalize explicit device roots. + const scopeRoot = + requestedRoot.startsWith("\\\\?\\") || requestedRoot.startsWith("\\\\.\\") + ? decodePath(root) + : requestedRoot; requestedScope = files.absolute( - encodePath(windowsJoin(decodePath(requestedRoot), expandedScope)), + encodePath(windowsJoin(scopeRoot, expandedScope)), ); } else { requestedScope = expandedScope.startsWith("/") diff --git a/plugins/codex-security/native/proof-policy-windows.mts b/plugins/codex-security/native/proof-policy-windows.mts index 5bcd15308..d649cde92 100644 --- a/plugins/codex-security/native/proof-policy-windows.mts +++ b/plugins/codex-security/native/proof-policy-windows.mts @@ -8,7 +8,7 @@ import { writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { join, win32 } from "node:path"; import { binaryPath, output, root } from "./binding.mjs"; import { nativeTarget } from "./platform.mjs"; import { @@ -59,22 +59,24 @@ if (process.argv[2] === "build") { } finally { assert.equal(opened.handle.close(), 0); } - assert.equal( - execFileSync( - process.execPath, - [ - helper, - "--helper", - "resolve-security-md", - "--repo", - volumePath, - "--scope", - ".", - ], - { encoding: "utf8" }, - ), - '## SECURITY.md source: "SECURITY.md"\n\nvolume policy\n', - ); + for (const scope of [".", repo.slice(win32.parse(repo).root.length - 1)]) { + assert.equal( + execFileSync( + process.execPath, + [ + helper, + "--helper", + "resolve-security-md", + "--repo", + volumePath, + "--scope", + scope, + ], + { encoding: "utf8" }, + ), + '## SECURITY.md source: "SECURITY.md"\n\nvolume policy\n', + ); + } const proof: unknown = JSON.parse( execFileSync( join(output, "windows-wide-launcher.exe"), From 518f849171890f76d795b25ea013635244a708e6 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 2 Oct 2026 02:47:25 +0000 Subject: [PATCH 15/15] fix: preserve UNC share roots during home expansion --- .../src/helpers/resolve-security-md.ts | 11 ++++++++-- .../tests-ts/security-policy-helper.test.ts | 22 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts index cc972ee69..51494a03b 100644 --- a/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts +++ b/plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts @@ -106,15 +106,22 @@ function expandHome(path: string, posixHome: string | undefined): string { } if (home === undefined) throw new HomeExpansionError("Could not determine home directory."); + // node:path recognizes share roots in ordinary UNC paths, not extended UNC. + const namespacedUnc = home.slice(0, 8).toUpperCase() === "\\\\?\\UNC\\"; + if (namespacedUnc) home = `\\\\${home.slice(8)}`; if (username !== "" && username !== currentUsername) { - if (currentUsername !== win32.basename(home)) { + if (currentUsername !== win32.parse(home).base) { throw new HomeExpansionError("Could not determine home directory."); } home = joinHome(win32.dirname(home), username); } if (home.startsWith("~")) throw new HomeExpansionError("Could not determine home directory."); - return joinHome(home, separator === -1 ? "" : path.slice(end + 1)); + const expanded = joinHome( + home, + separator === -1 ? "" : path.slice(end + 1), + ); + return namespacedUnc ? win32.toNamespacedPath(expanded) : expanded; } if (path === "~" || path.startsWith("~/")) { const home = posixHome ?? homedir(); diff --git a/sdk/typescript/tests-ts/security-policy-helper.test.ts b/sdk/typescript/tests-ts/security-policy-helper.test.ts index e7dfedcfe..303d4101d 100644 --- a/sdk/typescript/tests-ts/security-policy-helper.test.ts +++ b/sdk/typescript/tests-ts/security-policy-helper.test.ts @@ -179,6 +179,16 @@ describe("built SECURITY.md helper", () => { expect(run(["--repo", root, "--scope", "~"], homeEnv({})).status).toBe(1); const other = homeEnv({ USERPROFILE: home, USERNAME: "different" }); expect(run(["--repo", "~other", "--scope", "."], other).status).toBe(1); + for (const profile of [ + "\\\\host\\share\\", + "\\\\?\\UNC\\host\\share\\", + "\\\\?\\unc\\host\\share", + ]) { + const shareRoot = homeEnv({ USERPROFILE: profile, USERNAME: "share" }); + const result = run(["--repo", "~other", "--scope", "."], shareRoot); + expect(result.status, result.stderr).toBe(1); + expect(result.stderr).toContain("Could not determine home directory."); + } }, ); @@ -738,13 +748,13 @@ describe("built SECURITY.md helper", () => { const profiles = join(root, "profiles"); write(profiles, "current/SECURITY.md", "current policy\n"); write(profiles, "sibling/SECURITY.md", "sibling policy\n"); - for (const [home, cwd] of [ - [join(profiles, "current"), undefined], - [`${join(profiles, "current")}\\`, undefined], - [`${root.slice(0, 2)}current`, profiles], - ]) { + for (const [home, cwd, scope] of [ + [join(profiles, "current"), undefined, "~sibling"], + [`${join(profiles, "current")}\\`, undefined, "~sibling"], + [`${root.slice(0, 2)}current`, profiles, "."], + ] as const) { const result = run( - ["--repo", "~sibling", "--scope", "~sibling"], + ["--repo", "~sibling", "--scope", scope], { ...process.env, USERPROFILE: home,