codex-security Native 原语层解析:Rust 实现 Node 缺失的 OS 操作与跨平台分发验证
2026/9/24 14:54:46 网站建设 项目流程
  • 应用安全
  • 漏洞扫描
  • AI 应用

【免费下载链接】codex-security

OpenAI's Codex Security CLI and TypeScript SDK for finding, validating, and fixing security vulnerabilities. npm: https://www.npmjs.com/package/@openai/codex-security

项目地址:https://gitcode.com/gh_mirrors/co/codex-security
点击查看免费下载

导读

本篇文章围绕 OpenAI Codex Security 项目中 plugins/codex-security/native/README.md 所描述的 Native OS primitives 模块展开,它是整个 codex-security 插件在 Unix 与 Windows 上安全处理文件系统路径、账户解析与文件锁的底层基石。文章将深入讲解这组 Node-API 8 原生绑定的设计动机、九个 Unix 核心函数与 Windows 句柄模型、字节级路径语义、EINTR 重试约定,以及从本地构建、行为证明(proof)到分发门槛检查与 CI 打包的完整工程化流程。读完本文,你将掌握这套跨平台原生层的调用契约、验证方式和发布链路,并能直接复现其构建与测试命令。

背景:为什么 Node 需要一套原生 OS 原语

Node.js 的标准库并没有暴露全部操作系统能力。在 codex-security 中,resolve-security-md这个辅助工具负责解析仓库内所有SECURITY.md安全策略文件并拼接为扫描上下文(见 resolve-security-md.ts),它需要完成 Unix 上的原生账户查找(~user形式的 home 目录展开),以及 Windows 上不受 Node 高层 API 限制的路径、文件和目录操作。这些需求正是 native/README.md 中定义的绑定层的来源:

These bindings supply OS operations that Node does not expose. Theresolve-security-mdhelper uses native account lookup on Unix and native path, file, and directory operations on Windows.

从实现结构看,整个原生层是一个 Rust crate、两个平台后端

  • Cargo.toml 声明了 cratecodex-security-native,以cdylib形式编译,依赖napi = 3.12.2(启用napi8feature)与napi-derive = 3.6.3;Unix 侧额外使用libc = 0.2.189,Windows 侧使用windows-sys = 0.61.2
  • src/lib.rs 仅通过#[cfg(unix)]/#[cfg(windows)]分别引入 src/unix.rs 与 src/windows.rs;
  • 发布配置(profile.release)开启strip = truelto = truecodegen-units = 1,保证产物精简且优化充分。

Unix 绑定:九个 Node-API 8 函数的设计契约

README 明确说明“The nine Node-API 8 functions are typed inbinding.mts”,这九个函数的完整签名定义在 binding.mts 的UnixBinding接口中:

函数签名底层实现
openAt(directory, name, flags, mode) => { value, errno }libc::openat+O_CLOEXEC,自动重试 EINTR
duplicate(descriptor) => { value, errno }fcntl(F_DUPFD_CLOEXEC),复制描述符并置 close-on-exec
makeDirectoryAt(directory, name, mode) => { value, errno }libc::mkdirat
renameAt(oldDir, oldName, newDir, newName) => { value, errno }libc::renameat
unlinkAt(directory, name) => { value, errno }libc::unlinkat
statAt(directory, name) => { errno, mode, device, inode }fstatat+AT_SYMLINK_NOFOLLOW
readLinkAt(directory, name) => { errno, value }readlinkat,缓冲区不足时倍增扩容
fileLock(descriptor, unlock, nonblocking) => { value, errno }flock(LOCK_EX / LOCK_UN / LOCK_NB),自动重试 EINTR
userHome(username) => { errno, value }getpwnam_rERANGE时倍增缓冲区重试

路径保持为不解释的 POSIX 字节

README 强调“Paths remain byte buffers”(路径始终保持为字节缓冲区)。在 unix.rs 中,所有名称参数都以napi::bindgen_prelude::Buffer接收,再通过CString::new校验并转换——唯一拒绝的情形是路径中包含 NUL 字节(Path contains a NUL byte)。这意味着:

  • 文件名不需要是合法 UTF-8,可以携带任意不可解码字节(例如 Linux 上的0xff等字节序列);
  • 往返过程零解码零编码,规避了 JavaScript 字符串在路径边界上的语义损耗;
  • proof.mts 的fixtureName刻意区分平台:macOS(APFS 要求合法 UTF-8)使用prefix-é,而 Linux 直接构造Buffer.from([prefix.charCodeAt(0), byte])来演练不可解码文件名。

statAt不跟随最终符号链接,大整数用十进制字符串

statAt底层使用fstatat(..., AT_SYMLINK_NOFOLLOW),因此对符号链接本身返回链接的modeS_IFLNK),而不是目标文件的元数据——这是安全扫描场景中识别链接、拒绝越界遍历的关键。由于st_devst_ino在 64 位平台上可能超出 JavaScript 的精确整数范围,Rust 侧将它们序列化为十进制字符串返回((stat.st_dev as u64).to_string()),避免 JS 端Number舍入造成身份比较失真。Windows 侧的卷序列号与文件位置同样遵循“十进制字符串”约定(见下文)。

openAtduplicate强制 close-on-exec

两个创建描述符的入口都保证描述符不会泄漏到后续exec的子进程中:open_at在 flags 中强制并入O_CLOEXECduplicate使用F_DUPFD_CLOEXEC。这确保了扫描过程中派生的外部进程无法继承这些已锚定的文件描述符。

EINTR 重试策略:只有两个例外

README 特别约定:“openAtandfileLockretry EINTR, matching the current Python helpers. Other operations return their native errno.” 在 unix.rs 中通过retry_eintr闭包实现:循环调用直到 errno 不是EINTR。其余操作(mkdiratrenameatunlinkatstatAtreadLinkAt)直接返回原生 errno,由调用方决定处理策略。

与此同时,Node 侧的 binding.mts 提供了readDescriptor:对fs.readSync捕获EINTR异常后原地重试,并且保留调用者之前已经读入的字节Retry the interrupted read, preserving the caller's previously read bytes),避免一次被信号中断的读取丢失已落盘的数据。

userHome:不经 Git、不依赖HOME环境变量的账户解析

user_home通过getpwnam_r读取系统账户数据库,返回pw_dir的原始字节;当用户名不存在时返回errnovalue: null。这一点对resolve-security-md至关重要:它要展开~someone/...形式的扫描路径(见 resolve-security-md.ts 中的expandHome),必须查真实账户而不是 Git 配置。proof 中accountProof还会断言:当前用户 home 与os.userInfo()一致(容器内无账户条目时允许ERR_SYSTEM_ERROR/ENOENT)、root存在、随机不存在的用户名返回null、包含 NUL 的用户名直接抛错。

阻塞锁与主事件循环:进程生命周期释放

README 给出两条重要使用约束:

  1. 阻塞锁必须在主 JavaScript 事件循环之外运行fileLock的阻塞调用会挂起线程,若在事件循环线程内执行会阻塞整个 Node 进程,因此 proof.mts 中所有阻塞加锁场景都通过子进程(nativeLockWorker)完成;
  2. 持有锁的进程在关闭或退出时自动释放flock的语义保证进程死亡后内核释放锁,proof 中专门验证了“持有者被 SIGKILL 后,等待方能够拿到锁”(peerDeathReleasesLock/nativeDeathReleasesLock)。

README 还提到“A Python signal handler can raise during a blocked call, so later routing must preserve cancellation through the worker lifecycle”,即 Python 侧信号处理器可能在阻塞调用期间抛异常,后续的路由逻辑必须把这种取消状态贯穿 worker 生命周期——这解释了为什么锁的争用、解锁、进程死亡交接都通过标准输入输出协议在子进程之间编排。

Windows 绑定:WindowsHandle与 UTF-16LE 边界

Windows 侧是另一套完全不同的模型,定义在 windows-binding.mts 与 src/windows.rs:

句柄所有权:RustFile独占

WindowsHandle内部持有Option<File>(Rust 标准库文件对象),句柄不可继承,且从不进入 Node 的 CRT 描述符表。释放途径有两个:

  • 显式调用close()(幂等,self.file.take()后 drop);
  • 垃圾回收兜底(napi对象析构时同样 drop)。

因此 proof 使用--expose-gc参数显式触发 GC 来验证句柄生命周期。

路径与名称:无 NUL 终止符的 UTF-16LE 缓冲区

Windows 侧所有路径参数与返回名称都是 UTF-16LE 编码的Buffer不携带 NUL 终止符,并且完整保留孤立代理项(lone surrogates)——这是为了能原样表示 Windows 文件系统中任何合法文件名,即使它不是合法 Unicode。wide_path只做两项校验:字节长度必须是偶数(整 code unit),以及不含 NUL code unit。

文件操作面与错误码

README 列出的能力与 windows.rs 一一对应:

  • 创建createWindowsDirectoryCreateDirectoryW)与createWindowsDirectoriesfs::create_dir_all);
  • 属性与重解析标签attributes()通过GetFileInformationByHandleEx(FileAttributeTagInfo)拿到FileAttributesReparseTag
  • 身份与名称identity()返回十进制字符串的卷序列号 + 128 位文件 ID 的原始缓冲区;finalPath(flags)调用GetFinalPathNameByHandleW并自动扩容缓冲区;
  • 读写与游标read/write通过 RustFileRead/Writetrait 完成,seek接受 64 位 BigInt 偏移与FILE_BEGIN/FILE_CURRENT/FILE_END三种 origin,sizeGetFileSizeExsetEndOfFile先取stream_positionset_len(游标保留式截断),flush对应sync_all
  • 精确句柄重命名与删除rename(destination, replace)构造FILE_RENAME_INFO(含ReplaceIfExists),setDisposition(true)标记删除;
  • 独占整文件锁lock(nonblocking)/unlock()映射到 Rust 的File::lock/try_lock/unlock

错误统一返回数字 Windows 错误码,README 明确给出两个例子:6(ERROR_INVALID_HANDLE,已关闭句柄)33(ERROR_LOCK_VIOLATION,非阻塞锁争用)open_windows_file还会拒绝FILE_FLAG_OVERLAPPED——因为挂起的重叠 I/O 可能在同步调用返回后仍持有原生缓冲区。

四个在 Node 边界保留字符串的操作

除文件句柄操作外,windows.rs 还提供了四个字符串级操作,README 对它们逐一描述:

函数行为
windowsArguments返回完整 OS 参数向量(含可执行文件与 Node 选项),使用 Rust CRT 兼容解析器(std::env::args_os
windowsEnvironment(name)读取单个宽环境变量,区分“不存在”(返回null)与“空缓冲区”
windowsAbsolutePath(path)基于原生当前目录与驱动器目录解析绝对路径(std::path::absolute/GetFullPathNameW),不要求目标已存在
windowsDirectoryEntries(path)std::fs::read_dir+ 缓存的DirEntry::file_type()枚举目录,不逐个打开子项;名称保持 UTF-16LE;构造或迭代失败返回数字错误与空数组

其中目录条目同时报告is_directoryis_symbolic_link两个标志(目录符号链接与 junction 两者兼有),通过entriesWithTypes暴露给resolve-security-md --list(Windows 上的目录遍历入口,见 resolve-security-md.ts)。

路径归一化与重解析点策略

windows-files.mts将普通绝对路径解析与规范化委托给GetFullPathNameWGetFinalPathNameByHandleW,仅在根目录以下裁剪尾部分隔符;它的小型 verbatim 路径归一化器在处理点段(./..)时保留盘符与 UNC 共享根,包括字面尾随点与空格。stat(path, false)保留精确的符号链接/重解析点元数据,使调用方可以独立于枚举器的链接标签来拒绝 junction 遍历。README 强调,路径授权、祖先遍历与重解析点策略仍然是调用方(而非原生层)的责任。

本地构建与行为证明:一条命令链

README 给出了从仓库根目录运行的完整开发流程(需先安装固定的 Rust 工具链与现有 TypeScript 依赖):

pnpm --dir sdk/typescript install --frozen-lockfile pnpm --dir sdk/typescript run build:ci node plugins/codex-security/native/build.mjs node plugins/codex-security/native/proof.mjs cargo +1.97.1 fmt --check --manifest-path plugins/codex-security/native/Cargo.toml cargo +1.97.1 clippy --locked --manifest-path plugins/codex-security/native/Cargo.toml -- -D warnings

注意 Rust 工具链被固定为1.97.1(rust-toolchain.toml 与 Cargo.toml 的rust-version = "1.97"双重约束),clippy 强制-D warnings

proof.mjs(即 proof.mts)不依赖 Python,它通过loadBinding()加载当前平台产物,覆盖 README 列出的全部场景:目录替换(rename 后锚定 FD 仍指向原 inode)、字节路径(含不可解码文件名)、不可读文件的元数据读取(mode 000 也能 stat)、超长原始符号链接(80 层component/拼接)、描述符复制(duplicate 后原 FD 可关闭)、Node 描述符 I/O(写、fsync、fstat、读回)、账户查找、锁争用、解锁交接与进程死亡释放。proof 还会把结果以 JSON 形式输出(node,platform,architecture,nodeApi: 8等),便于 CI 断言。

构建产物位于被忽略的targetdist目录。Linux 输出目录按 C 运行时细分:linux-x64-gnulinux-arm64-gnulinux-x64-musllinux-arm64-musl。目标目录的推导逻辑在 platform.mts:通过process.report.getReport()header.glibcVersionRuntime是否存在来区分 glibc 与 musl——纯 Node 实现、零子进程。

分发门槛检查:check.mjs的硬性指标

任何产物上传前都必须运行:

node plugins/codex-security/native/check.mjs

check.mts 对三类平台各设硬性门槛:

平台门槛
GNU Linux编译前重映射源码、Cargo registry 与编译器路径;产物字节中不得包含私有构建路径标记(/Users//home/dev-user等,同时检查 UTF-8 与 UTF-16LE 两种编码);readelf检查不得引入比GLIBC 2.28更新的符号版本
musl Linux必须是当前架构的 ELF 镜像(\x7fELF、EI_CLASS=2、machine 字段校验),依赖对应架构的libc.musl-*.so.1,且不能有任何来自 glibc 的符号版本要求(libgcc_s.so.1自身的GLIBC_2.0兼容导出被单独豁免)
macOSotool -l解析LC_BUILD_VERSION/LC_VERSION_MIN_MACOSX,部署目标必须≤ 11.0
Windows校验MZ+PE\0\0头与机器类型(x640x8664/ arm640xaa64

README 特别提醒:从较新的 GNU Linux 工作站构建的产物可能通过行为 proof 却在分发检查中失败,因为本机链接器引入的 glibc 版本可能高于 2.28。musl 没有 glibc 式符号版本下限,因此其运行时兼容性还要依赖后续的 Node 加载 proof。

CI 工作流:三个平台流水线 + 产物合并

README 描述了三条原生构建流水线:

  • native-unix:在 digest-pinned 的 manylinux 2.28 镜像中构建 Linux 产物;挂载固定 Rust 工具链与已拉取的 Cargo registry、离线编译,并在编译期间封锁 Python 命令;macOS 构建设置MACOSX_DEPLOYMENT_TARGET=11.0;CI 在 Node 20.0.0 与 22.13.0 两个版本上分别验证 x64 与 arm64 产物。
  • native-musl:使用原生 x64/arm64 Ubuntu worker + digest-pinned 的 Rust 1.97.1 Alpine 编译器镜像;禁用静态 CRT 链接(让 Node 能够加载共享库);ELF 与私有路径检查通过后,每个未变更产物在 pinned 的 Node 20.0.0 + Alpine 3.17 与 Node 22.13.0 + Alpine 3.21 镜像中运行完整 proof(对应 musl 1.2.3 与 1.2.5);运行时容器只读挂载源码与产物,无 Python,proof 进程 PATH 为空。
  • native-windows:以 MSVC + 静态 CRT 构建 x64 与 arm64;检查 PE 架构与私有路径后,在同一产物上以 Node 22.13.0 与 20.0.0、空 PATH 运行 proof;proof 覆盖句柄生命周期与 GC、祖先替换、junction、精确句柄操作、原始 UTF-16 与长路径、数字错误码、整文件锁与释放;阻塞锁在子进程中执行;另有一次 Node 22 调用会使用 runner 的 Python 与既有msvcrt字节零锁做双向对比(争用、解锁、关闭、进程死亡释放),Python 仅是可选迁移 oracle:
node --expose-gc plugins/codex-security/native/proof-windows.mjs python plugins/codex-security/scripts

Windows 构建还会额外编译测试专用的windows-wide-launcherRust 示例:它以孤立代理项启动一个 Node proof 子进程(参数、环境变量、工作目录均含),子进程验证完整目录迭代、孤立代理项与替换字符文件的区分、规范化路径、有界读取、输出截断以及通过 typed adapter 的递归长路径;Rust 侧一个禁止共享的文件 guard 保持打开,子进程枚举其名称时显式数据读取必须报共享冲突,而仅属性访问不受 Windows 文件共享阻止。该 launcher 会清理宽字符夹具,且永不进入上传或打包的原生负载

创建文件/目录符号链接需要 Windows 开发者模式或符号链接特权;缺失时 proof 仍运行其余断言(含目录 junction),并在 JSON 输出中将跳过的符号链接断言标为false。CI 两个架构都要求真实符号链接,同时强制受限场景以覆盖两条路径。

打包输入:host 与 universal 两档分发

README 的 “Package inputs” 章节给出插件独立构建流程:

pnpm --dir plugins/codex-security/mcp-app install --frozen-lockfile node plugins/codex-security/mcp-app/scripts/build_native.mjs node plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs --output plugins/codex-security/mcp --native host
  • build_native.mjs复用 MCP app 的依赖编译 TypeScript 工具、拉取锁定版本的 Cargo 依赖,并把宿主二进制与许可证声明写入native/dist
  • --native host只打包当前平台与架构的文件到mcp/目录(插件 launcher 的预期位置),CI 在 Linux、macOS、Windows 上测试此构建,无需 SDK;
  • 插件与 npm 发布则使用默认的--native universal,它要求native/prebuilt中齐备全部八个已验证二进制(Linux gnu/musl × x64/arm64、macOS x64/arm64、Windows x64/arm64)。

native-artifacts工作流汇总三个平台流水线的八个已验证负载,合并为native-universal-<commit>单一产物;PR 校验任务共享node-ci组装的一份产物,发布与独立校验运行各自组装。默认情况下,独立 MCP builder 与 npm 包包含同一份完整mcp/native目录树,运行时既不编译也不下载代码

GNU x64 任务还会对锁定的 Cargo 元数据运行notices.mjs,收集各 crate 许可证与固定 Rust 标准库声明(覆盖两个包面)。由于 NAPI crates 的 registry 归档不附带许可证文件,licenses/napi.txt保留了其固定的上游许可证文本,升级这些依赖时需复查该覆盖文件。

universal 构建、SDK 测试或 Docker 构建需要下载某个成功运行所产出的工件(README 提示可在已推送分支上手动运行native-artifacts):

gh run download <run-id> --name native-universal-<commit> --dir plugins/codex-security/native/prebuilt

被忽略的prebuilt目录必须包含全部八个平台目录与共享声明;更改原生源码或构建工具链后需刷新它;缺失负载会让构建失败(即使宿主只加载其中一个);已安装包的检查会以空PATH加载匹配的工件。

与既有 Python 实现的迁移对齐

整套原生层并非从零发明,而是与仓库内已有的 Python 辅助脚本对齐并逐步迁移。README 提供了一条可选的双向对比命令:

node plugins/codex-security/native/proof.mjs python3 plugins/codex-security/scripts

proof.mts 中的pythonOracle直接 import 既有workbench_db.pyacquire_completion_file_lock/release_completion_file_lock/posix_file_lock,在同样的锁文件上验证四个方向:Node 持有、Python 探测争用(期望EAGAIN/EWOULDBLOCK);Python 持有、Node 等待并在解锁后获得;以及进程死亡后的锁交接。README 明确标注这段协议只是“temporary interoperability oracle”(临时互操作对照),永不复用为迁移后的实现或发布工件。这也印证了原生层的目标:在flock语义上与既有 Python 行为严格一致,同时把扫描路径处理从 Python 迁移到 Rust + Node 的字节安全模型上。

总结:从原语到发布的一体化工程质量

回顾整条链路,codex-security 的 Native OS primitives 模块体现了几个值得借鉴的工程决策:

  1. 边界最小化:Node 不暴露的才进原生层,路径以字节/UTF-16LE 原样传递,杜绝编码转换引入的安全缝隙;
  2. 语义可证明:九个 Unix 函数、Windows 句柄模型、锁的进程生命周期,全部由不依赖 Python 的 proof 程序在真实文件系统上逐项断言;
  3. 分发可审计:私有路径字节检查、glibc 2.28 / musl / macOS 11.0 / PE 架构四类硬门槛,配合 digest-pinned 的 CI 镜像与空PATH运行,保证“行为正确”与“分布兼容”双重达标;
  4. 迁移有对照:以既有 Python 实现为 oracle 做双向验证,为渐进替换提供可回退的安全网。

对于希望在扫描类工具中引入安全文件系统原语的开发者,native/README.md 及其配套的 binding.mts、src/unix.rs、src/windows.rs、check.mts 与 proof.mts 构成了一套完整、可直接复现的参考实现。

  • 应用安全
  • 漏洞扫描
  • AI 应用

【免费下载链接】codex-security

OpenAI's Codex Security CLI and TypeScript SDK for finding, validating, and fixing security vulnerabilities. npm: https://www.npmjs.com/package/@openai/codex-security

项目地址:https://gitcode.com/gh_mirrors/co/codex-security
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询