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
6 changes: 5 additions & 1 deletion .github/workflows/native-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ jobs:
run: |
cargo fmt --check
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cargo clippy --locked --target ${{ matrix.target }} -- -D warnings
cargo clippy --locked --target ${{ matrix.target }} --lib --example windows-wide-launcher -- -D warnings
- name: Build and verify with Node.js 22
run: |
node build.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node check.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node --test windows-files.test.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$nativeNode = (Get-Command node).Source
$env:PATH = ""
& $nativeNode --expose-gc proof-windows.mjs
Expand All @@ -74,6 +76,8 @@ jobs:
run: |
node check.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node --test windows-files.test.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$nativeNode = (Get-Command node).Source
$env:PATH = ""
& $nativeNode --expose-gc proof-windows.mjs
Expand Down
6 changes: 6 additions & 0 deletions plugins/codex-security/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ 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 one enumerator through `entriesWithTypes`; product commands do not use it yet.

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

Build on Windows after compiling the TypeScript tools, then run:

```sh
Expand All @@ -53,6 +57,8 @@ 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.

## Package inputs

The `native-artifacts` workflow calls all three platform workflows and combines their eight verified payloads into `native-universal-<commit>`. PR validation jobs share one artifact assembled by `node-ci`; release and standalone validation runs assemble their own. The standalone MCP builder and npm package include the same complete `mcp/native` tree; neither compiles nor downloads code at runtime.
Expand Down
20 changes: 19 additions & 1 deletion plugins/codex-security/native/build.mts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@ const flags = [
];
const target = resolve(root, process.env["CARGO_TARGET_DIR"] ?? "target");
const args = ["build", "--release", "--locked"];
if (windowsTarget !== undefined) args.push("--target", windowsTarget);
if (windowsTarget !== undefined)
args.push(
"--target",
windowsTarget,
"--lib",
"--example",
"windows-wide-launcher",
);
execFileSync("cargo", args, {
cwd: root,
stdio: "inherit",
Expand All @@ -63,4 +70,15 @@ const library = join(
checkPrivatePaths(readFileSync(library), [root, cargoHome, sysroot, target]);
mkdirSync(output, { recursive: true });
copyFileSync(library, binaryPath);
if (windowsTarget !== undefined)
copyFileSync(
join(
target,
windowsTarget,
"release",
"examples",
"windows-wide-launcher.exe",
),
join(output, "windows-wide-launcher.exe"),
);
console.log(`Built ${nativeTarget} Node-API 8 primitives.`);
105 changes: 105 additions & 0 deletions plugins/codex-security/native/examples/windows-wide-launcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Test-only launcher: Node's Windows startup has already replaced lone surrogates.
#[cfg(not(windows))]
fn main() {}

#[cfg(windows)]
fn main() -> std::io::Result<()> {
use std::{
env,
ffi::OsString,
fs, io,
os::windows::{ffi::OsStringExt, fs::OpenOptionsExt},
path::{Path, PathBuf},
process::Command,
};

fn raw(prefix: &str, unit: u16) -> OsString {
OsString::from_wide(&prefix.encode_utf16().chain([unit]).collect::<Vec<_>>())
}

fn run(node: OsString, script: OsString, root: &Path) -> io::Result<()> {
let cwd = root.join(raw("cwd-", 0xd800));
fs::create_dir(&cwd)?;
let replacement = root.join("cwd-\u{fffd}");
fs::create_dir(&replacement)?;
fs::write(replacement.join("sentinel"), "replacement cwd untouched")?;
let names = [
raw("high-", 0xd800),
raw("high-", 0xfffd),
raw("low-", 0xdc80),
raw("low-", 0xfffd),
raw("tail-", 0xdfff),
raw("tail-", 0xfffd),
OsString::from("unicode-🔐-東京"),
];
for (index, name) in names.iter().enumerate() {
fs::write(cwd.join(name), format!("sentinel-{index}"))?;
}
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"))?;
std::os::windows::fs::symlink_dir("empty", cwd.join("directory-link"))?;
std::os::windows::fs::symlink_dir(
raw("missing-", 0xdfff),
cwd.join("dangling-directory-link"),
)?;
let locked = cwd.join(raw("locked-", 0xdfff));
fs::write(&locked, "directory enumeration does not open this file")?;
fs::write(root.join(raw("parent-", 0xd800)), "parent sentinel")?;
let verbatim = fs::canonicalize(&cwd)?;
for (name, contents) in [
("trailing", "ordinary dot sibling"),
("trailing.", "literal dot file"),
("space", "ordinary space sibling"),
("space ", "literal space file"),
] {
fs::write(verbatim.join(name), contents)?;
}
let arguments = [
raw("arg-high-", 0xd800),
raw("arg-low-", 0xdc80),
raw("arg-tail-", 0xdfff),
OsString::from("replacement-\u{fffd}"),
OsString::from("Unicode 🔐 東京"),
OsString::from(""),
OsString::from("space and\ttab"),
OsString::from("quoted \"value\" and trailing\\"),
OsString::from("backslash\\\"quote"),
];
let guard = fs::OpenOptions::new()
.read(true)
.share_mode(0)
.open(&locked)?;
let status = Command::new(node)
.arg(script)
.arg("wide-worker")
.arg(root)
.args(arguments)
.current_dir(&cwd)
.env("CODEX_SECURITY_WIDE_VALUE", raw("value-", 0xd800))
.env("CODEX_SECURITY_WIDE_EMPTY", "")
.env_remove("CODEX_SECURITY_WIDE_ABSENT")
.env(raw("CODEX_SECURITY_WIDE_NAME_", 0xdfff), "wide name value")
.env("CODEX_SECURITY_WIDE_LONG", "x".repeat(1024))
.env("USERPROFILE", &cwd)
.status()?;
if !status.success() {
return Err(io::Error::other("Wide Windows child proof failed"));
}
drop(guard);
if fs::read(replacement.join("sentinel"))? != b"replacement cwd untouched" {
return Err(io::Error::other("Replacement cwd was changed"));
}
Ok(())
}

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");
fs::create_dir(&root)?;
let result = run(node, script, &root);
let cleanup = fs::remove_dir_all(&root);
result?;
cleanup
}
Loading