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
2 changes: 1 addition & 1 deletion .github/workflows/native-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ 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
Expand Down
4 changes: 4 additions & 0 deletions plugins/codex-security/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ 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 byte-range locking. Calls return numeric Windows errors. Buffer ranges, path encoding, and 64-bit arguments are checked before FFI calls. 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 avoid Node's lossy Windows string conversions. `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. `windowsDirectoryNames` returns every name as UTF-16LE; iteration failures return their Windows error and an empty array. `windowsDirectoryEntries` also returns the cached directory attribute and exact symbolic-link status, distinguishing directory symlinks from junctions without opening each entry. `windows-files.mts` exposes these through `entriesWithTypes` with `isDirectory()` and `isSymbolicLink()` alongside the existing names-only `entries`; product commands do not use this adapter yet.

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

```sh
Expand All @@ -49,6 +51,8 @@ node --expose-gc plugins/codex-security/native/proof-windows.mjs

The `native-windows` workflow builds x64 and arm64 with MSVC and a static CRT. It checks PE architecture and private paths, then runs the same artifact on Node 22.13.0 and 20.0.0 with an empty `PATH`. The proof covers handle lifetime and garbage collection, ancestor replacement, junctions, exact-handle operations, raw UTF-16 and long paths, numeric errors, and cross-process byte-zero locking and release. Blocking locks run in child processes. Comparison with the existing Python `msvcrt` lock remains a separate migration gate before production routing.

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. 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>`. Package, release, container, and test workflows prepare this artifact before building the plugin. 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.`);
178 changes: 178 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,178 @@
// 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,
mem::size_of,
os::windows::ffi::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
process::Command,
ptr::null_mut,
};
use windows_sys::Win32::Security::*;

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

fn deny_file_access(path: &Path, operation: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
let path = path
.as_os_str()
.encode_wide()
.chain([0])
.collect::<Vec<_>>();
let mut length = 0;
unsafe {
GetFileSecurityW(
path.as_ptr(),
DACL_SECURITY_INFORMATION,
null_mut(),
0,
&mut length,
)
};
if length == 0 {
return Err(io::Error::last_os_error());
}
let mut saved = vec![0_usize; (length as usize).div_ceil(size_of::<usize>())];
let mut control = 0;
let mut revision = 0;
if unsafe {
GetFileSecurityW(
path.as_ptr(),
DACL_SECURITY_INFORMATION,
saved.as_mut_ptr().cast(),
length,
&mut length,
) == 0
|| GetSecurityDescriptorControl(
saved.as_mut_ptr().cast(),
&mut control,
&mut revision,
) == 0
} {
return Err(io::Error::last_os_error());
}
let mut acl = ACL::default();
let mut descriptor = SECURITY_DESCRIPTOR::default();
let descriptor = (&mut descriptor as *mut SECURITY_DESCRIPTOR).cast();
if unsafe {
InitializeAcl(&mut acl, size_of::<ACL>() as u32, ACL_REVISION) == 0
|| InitializeSecurityDescriptor(descriptor, 1) == 0
|| SetSecurityDescriptorDacl(descriptor, 1, &acl, 0) == 0
|| SetFileSecurityW(
path.as_ptr(),
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
descriptor,
) == 0
} {
return Err(io::Error::last_os_error());
}
let result = operation();
let protection = if control & SE_DACL_PROTECTED != 0 {
PROTECTED_DACL_SECURITY_INFORMATION
} else {
UNPROTECTED_DACL_SECURITY_INFORMATION
};
// Restore the owned fixture's original DACL even if the child proof fails.
if unsafe {
SetFileSecurityW(
path.as_ptr(),
DACL_SECURITY_INFORMATION | protection,
saved.as_mut_ptr().cast(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
result
}

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 denied = cwd.join(raw("denied-", 0xdfff));
fs::write(&denied, "directory enumeration does not open this file")?;
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"),
];
deny_file_access(&denied, || {
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"));
}
Ok(())
})?;
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
Loading