Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 26 additions & 48 deletions plugins/codex-security/mcp-app/src/helpers/resolve-security-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -36,56 +36,38 @@ type FileInfo = Pick<Stats, "isDirectory" | "isFile" | "isSymbolicLink"> & {
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())
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) || ".";
}

Expand Down Expand Up @@ -114,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();
Expand Down
4 changes: 2 additions & 2 deletions plugins/codex-security/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The binding exposes synchronous file and directory creation, attributes and repa

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. `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:

Expand All @@ -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.

Expand Down
11 changes: 11 additions & 0 deletions plugins/codex-security/native/examples/windows-wide-launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ 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(raw("missing-", 0xdfff), cwd.join("missing-link"))?;
std::os::windows::fs::symlink_file("loop-link", cwd.join("loop-link"))?;
std::os::windows::fs::symlink_dir("empty", cwd.join("directory-link"))?;
std::os::windows::fs::symlink_dir(
raw("missing-", 0xdfff),
Expand Down Expand Up @@ -203,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";
Expand Down
86 changes: 73 additions & 13 deletions plugins/codex-security/native/proof-windows-wide.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ function worker(root: string): Record<string, boolean> {
const directoryLinks = symlinks
? ["dangling-directory-link", "directory-link"]
: [];
const links = symlinks ? [...directoryLinks, "file-link"] : [];
const links = symlinks
? [...directoryLinks, "file-link", "loop-link", "missing-link"]
: [];
const cwd = win32.join(root, "cwd-\ud800");
const expectedArguments = [
"arg-high-\ud800",
Expand Down Expand Up @@ -67,9 +69,11 @@ function worker(root: string): Record<string, boolean> {
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);
Expand All @@ -92,6 +96,7 @@ function worker(root: string): Record<string, boolean> {
`${drive}\\rooted-\ud800`,
);
samePath(files.realpath(widePath(".")), cwd);
assert.throws(() => files.readFile(widePath(".")), { winerror: 5 });

const names = [
"high-\ud800",
Expand Down Expand Up @@ -175,21 +180,54 @@ function worker(root: string): Record<string, boolean> {
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(
files.realpath(widePath(`${drive}.\\..\\parent-\ud800`)),
win32.join(root, "parent-\ud800"),
);
assert(files.stat(widePath(".")).isDirectory());
if (symlinks) {
assert.deepEqual(
files.readFile(widePath("file-link")),
Buffer.from("sentinel-0"),
);
samePath(
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",
});
}

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,
);
const bounded = Buffer.alloc(4);
assert.equal(files.readInto(widePath(names[0]!), bounded), 4);
assert.equal(bounded.toString(), "sent");
Expand All @@ -200,13 +238,27 @@ function worker(root: string): Record<string, boolean> {
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),
]);
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(),
Expand Down Expand Up @@ -244,6 +296,15 @@ function worker(root: string): Record<string, boolean> {
win32.join(cwd, `directory-${name.slice(0, -1)}`),
);
files.mkdir(ordinaryDirectory);
const missing = files.realpath(
widePath(`${pathText(directory)}\\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), []);
}

Expand Down Expand Up @@ -283,7 +344,6 @@ function worker(root: string): Record<string, boolean> {
completeWideDirectoryIteration: true,
cachedDirectoryAttributesWithoutFileAccess: true,
cachedSymlinkTagsIncludingDanglingDirectories: symlinks,
existingFilesWithTrailingSeparators: true,
distinctRawAndReplacementFiles: true,
canonicalPathsBoundedReadsAndTruncation: true,
verbatimTrailingDotsAndSpaces: true,
Expand Down
Loading
Loading