首页
/ openinterpreter 中 codex-process-hardening 源码解析:Rust 进程启动前加固(禁用 Core Dump、拒绝 ptrace、清理危险环境变量)

openinterpreter 中 codex-process-hardening 源码解析:Rust 进程启动前加固(禁用 Core Dump、拒绝 ptrace、清理危险环境变量)

2026-09-06 13:07:52作者:冯梦姬Eddie

本文围绕 codex-rs/process-hardening 这个独立 crate 展开,讲清楚它提供的 pre_main_hardening() 函数在 Rust 二进制启动前(pre-main())究竟做了哪三层加固:禁用 core dump、拒绝调试器 attach、移除 LD_* / DYLD_* 等可被用来劫持动态链接过程的环境变量。读完后你将掌握:各平台(Linux/Android/macOS/BSD)对应的系统调用及其失败时的专用退出码、该 crate 在仓库中两个真实调用方的接入方式(#[ctor::ctor] 预初始化与 Linux 沙箱桥接进程加固),以及可以直接复用这套模式加固自己 Rust 二进制的完整做法。

1. 定位:一个在 main() 之前执行的加固工具 crate

codex-process-hardening 的官方说明(README.md)非常精炼:

This crate provides pre_main_hardening(), which is designed to be called pre-main() (using #[ctor::ctor]) to perform various process hardening steps, such as

  • disabling core dumps
  • disabling ptrace attach on Linux and macOS
  • removing dangerous environment variables such as LD_PRELOAD and DYLD_*

这里的关键设计决策是调用时机。函数名里的 pre_main 不是装饰,而是硬性要求:

  • 环境变量清理必须发生在程序任何逻辑执行之前,否则恶意 LD_PRELOAD 注入的代码早已随动态链接器加载完毕,之后再删环境变量为时已晚;
  • 拒绝调试器 attach 同样越早越好,理想情况是进程一进入用户态就关闭攻击窗口。

Rust 生态中实现 pre-main 钩子的惯用手段就是 ctor crate 提供的 #[ctor::ctor] 属性宏:被标注的函数会注册为 C 风格的构造器,在 main() 之前、由运行时完成基础初始化之后执行。这正是仓库中真实调用方的写法,见第 4 节。

Cargo.toml 可以看到该 crate 极其轻量:

[package]
name = "codex-process-hardening"
version.workspace = true
edition.workspace = true
license.workspace = true

[lib]
name = "codex_process_hardening"
path = "src/lib.rs"
doctest = false

[dependencies]
libc = { workspace = true }

唯一的运行时依赖是 libc(用于直接调用系统调用),版本、edition、license 全部继承 workspace 定义(见 codex-rs/Cargo.toml 中的 workspace members 与 codex-process-hardening = { path = "process-hardening" } 依赖声明);doctest = false 表明它不面向文档示例使用;测试期依赖 pretty_assertions 仅用于断言输出美化。构建系统方面,BUILD.bazel 仅一行核心逻辑,将其注册为 codex_rust_crate 目标 process-hardening、crate 名为 codex_process_hardening

整个实现集中在单文件 src/lib.rs(约 190 行),入口分发函数结构如下:

pub fn pre_main_hardening() {
    #[cfg(any(target_os = "linux", target_os = "android"))]
    pre_main_hardening_linux();

    #[cfg(target_os = "macos")]
    pre_main_hardening_macos();

    // On FreeBSD and OpenBSD, apply similar hardening to Linux/macOS:
    #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
    pre_main_hardening_bsd();

    #[cfg(windows)]
    pre_main_hardening_windows();
}

lib.rs#L12-L25)通过 cfg 条件编译,每个平台只编译并执行自己对应的那一段,未覆盖的平台(如 NetBSD、Windows 之外的 BSD 变体)编译为空操作,不会破坏跨平台构建。

2. 三层加固的源码级拆解

2.1 Linux / Android:prctl(PR_SET_DUMPABLE, 0) + RLIMIT_CORE + 清理 LD_*

Linux 分支 pre_main_hardening_linux 做三件事:

(1)把进程标记为 non-dumpable。

// Disable ptrace attach / mark process non-dumpable.
let ret_code = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
if ret_code != 0 {
    eprintln!("ERROR: prctl(PR_SET_DUMPABLE, 0) failed: {}",
        std::io::Error::last_os_error());
    std::process::exit(PRCTL_FAILED_EXIT_CODE);
}

这里利用了一个 Linux 内核语义:调用 prctl(PR_SET_DUMPABLE, 0) 将进程的 dumpable 标志清零。dumpable 标志同时决定两件事——进程崩溃时是否生成 core dump,以及同 UID 的进程是否允许通过 ptrace attach 到它(内核 task_dumpable 检查是 Yama/ptrace 权限模型的一部分)。一次系统调用同时关掉了"core dump 泄露内存"和"同用户调试器/注入器 attach"两条攻击面,这也是源码注释把这两件事写在一行的原因。

(2)纵深防御:把 core 文件大小限制归零。

// For "defense in depth," set the core file size limit to 0.
set_core_file_size_limit_to_zero();

(3)清理 LD_ 前缀环境变量。

// Official Codex releases are MUSL-linked, which means that variables such
// as LD_PRELOAD are ignored anyway, but just to be sure, clear them here.
remove_env_vars_with_prefix(b"LD_");

这段注释点出了一个重要前提:官方发布二进制是 MUSL 静态链接的,glibc 才负责解释 LD_PRELOADLD_LIBRARY_PATH 等变量,MUSL 静态链接的程序根本不读它们。既然如此为什么还要清理?答案是:其一,静态链接保证的只是"官方构建",从源码自行编译(可能链接 glibc)或未来链接方式变化时,这行清理依然是兜底;其二,进程 fork 出的子进程会继承环境,清空 LD_* 可防止被继承给未加固的第三方子进程。

RLIMIT_CORE 的实现lib.rs#L102-L117,Linux 与 macOS 共用):

#[cfg(unix)]
fn set_core_file_size_limit_to_zero() {
    let rlim = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
    let ret_code = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rlim) };
    if ret_code != 0 {
        eprintln!("ERROR: setrlimit(RLIMIT_CORE) failed: {}",
            std::io::Error::last_os_error());
        std::process::exit(SET_RLIMIT_CORE_FAILED_EXIT_CODE);
    }
}

注意 rlim_currlim_max 都置 0,同时封死"程序自己再调大限制"的路径。失败即 exit(7),fail-fast 策略与下文退出码表一致。

可复用的独立导出函数。 除了 pre-main 入口,crate 还单独导出了 disable_process_dumping

/// Mark the current Linux process non-dumpable so same-user processes cannot attach with ptrace.
#[cfg(target_os = "linux")]
pub fn disable_process_dumping() -> std::io::Result<()> {
    let ret_code = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
    if ret_code == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) }
}

它与 pre-main 路径的区别在于不直接 exit,而是返回 io::Result,供调用方在运行时(而非启动前)以可处理错误的方式执行同一操作——这正是 Linux 沙箱模块选择它的理由,见第 4 节。

2.2 macOS:ptrace(PT_DENY_ATTACH) + RLIMIT_CORE + 清理 DYLD_*

macOS 分支 pre_main_hardening_macos 与 Linux 语义对应但系统调用不同:

// Prevent debuggers from attaching to this process.
let ret_code = unsafe { libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) };
if ret_code == -1 {
    eprintln!("ERROR: ptrace(PT_DENY_ATTACH) failed: {}", ...);
    std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE);
}

macOS 没有 PR_SET_DUMPABLE 这个 prctl 选项,对应的机制是 BSD 风格的 ptrace(PT_DENY_ATTACH):由进程自己声明拒绝被 attach,之后 gdblldb 或其他同用户调试器无法再附加到该进程(ptrace(PT_DENY_ATTACH) 返回 -1 时按失败处理并 exit(6))。随后同样调用 set_core_file_size_limit_to_zero() 关闭 core dump,并以 DYLD_ 前缀(而非 LD_)清理 macOS 上可劫持动态链接的环境变量(如 DYLD_INSERT_LIBRARIESDYLD_LIBRARY_PATH)。

2.3 FreeBSD / OpenBSD:RLIMIT_CORE + 清理 LD_*

BSD 分支(lib.rs#L74-L80)执行与 macOS 类似的子集:set_core_file_size_limit_to_zero()remove_env_vars_with_prefix(b"LD_")。源码注释说明意图是"apply similar hardening to Linux/macOS",但没有等价的 ptrace-拒绝调用,因此这两项是它当前的全部加固。

2.4 Windows:预留的空实现

#[cfg(windows)]
pub(crate) fn pre_main_hardening_windows() {
    // TODO(mbolin): Perform the appropriate configuration for Windows.
}

lib.rs#L119-L122)当前 Windows 分支是显式的 TODO 占位,调用 pre_main_hardening() 不会报错但不做任何事。如果依赖本 crate 做安全加固,在 Windows 目标上需要自行补充相应机制。

2.5 环境变量的字节级前缀匹配

清理环境变量用的是 remove_env_vars_with_prefixenv_keys_with_prefix,实现上刻意绕开了 str

#[cfg(unix)]
fn env_keys_with_prefix<I>(vars: I, prefix: &[u8]) -> Vec<OsString>
where
    I: IntoIterator<Item = (OsString, OsString)>,
{
    vars.into_iter()
        .filter_map(|(key, _)| {
            key.as_os_str().as_bytes().starts_with(prefix).then_some(key)
        })
        .collect()
}

关键点:它通过 OsStrExt::as_bytes()原始字节层面做前缀匹配,而不是先转 UTF-8 字符串。环境变量键并非保证是合法 UTF-8,如果先 into_string() 就会把非 UTF-8 的危险键(比如攻击者构造的 LD_\xF0...)漏掉。crate 内自带两个测试固化了这一行为(lib.rs#L148-L193):

  • env_keys_with_prefix_handles_non_utf8_entries:构造非 UTF-8 键(含 LD_\xF0)与合法键混排的输入,断言非 UTF-8 的 LD_ 键仍被保留、非 LD_ 前缀键被剔除;
  • env_keys_with_prefix_filters_only_matching_keys:混合 PATHLD_TESTDYLD_FOO 输入,断言仅匹配指定前缀。

测试只在 unix 目标下编译(#[cfg(all(test, unix))]),因为 OsStrExt 是 unix 专属 trait。

3. Fail-fast:专用退出码设计

这是本 crate 一个值得注意的运维友好设计:每类加固失败都会以互不相同的退出码终止进程,并先向 stderr 打印带系统错误信息的诊断行:

退出码 常量 触发条件 适用平台
5 PRCTL_FAILED_EXIT_CODE prctl(PR_SET_DUMPABLE, 0) 失败 Linux / Android
6 PTRACE_DENY_ATTACH_FAILED_EXIT_CODE ptrace(PT_DENY_ATTACH) 返回 -1 macOS
7 SET_RLIMIT_CORE_FAILED_EXIT_CODE setrlimit(RLIMIT_CORE, 0) 失败 Linux、Android、macOS 及各 BSD(见 lib.rs#L33-L41 的 cfg 范围)

(常量定义见 lib.rs#L27-L41

设计意图是:对"安全敏感型"二进制而言,加固失败意味着二进制可能带着未闭合的攻击面运行,此时静默降级比直接失败更危险,因此选择 fail-fast;而不同的退出码让脚本或监控系统无需解析 stderr 文本即可区分失败在哪一层,便于定位是内核 ptrace 权限问题(如 Yama 策略异常)还是 rlimit 配置问题。

4. 仓库中的两处真实调用

4.1 responses-api-proxy:标准 #[ctor::ctor] 接入方式

responses-api-proxy/src/main.rs 是最完整的接入示例:

use clap::Parser;
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;

#[ctor::ctor]
fn pre_main() {
    codex_process_hardening::pre_main_hardening();
}

pub fn main() -> anyhow::Result<()> {
    let args = ResponsesApiProxyArgs::parse();
    codex_responses_api_proxy::run_main(args)
}

该代理进程在内存中长期持有 API 密钥类敏感材料(其 README.md 中专门说明了限制 OPENAI_API_KEY 读取/拷贝的取值约束),因此选择把整个进程标记为 non-dumpable、拒绝调试器 attach 并从环境里清掉 LD_*/DYLD_*,是与其威胁模型直接匹配的加固组合。注意它同时声明了 ctor 依赖(responses-api-proxy/Cargo.tomlctor = { workspace = true }),这是 #[ctor::ctor] 能工作的必要前提。

4.2 linux-sandbox:对桥接进程(bridge process)的运行时加固

Linux 沙箱模块在启动网络代理桥接进程时,没有走 pre-main 路径,而是调用第 2.1 节介绍的 disable_process_dumping(),与其他两项措施组合成 harden_bridge_process

pub(crate) fn harden_bridge_process() -> io::Result<()> {
    detach_bridge_stdio()?;
    set_parent_death_signal()?;
    codex_process_hardening::disable_process_dumping()
}

proxy_lifecycle.rs#L121-L136)三步分别是:把桥接进程的 stdio 重定向到 /dev/null 切断外部 I/O 通道;用 prctl(PR_SET_PDEATHSIG, SIGTERM) 让父进程退出时桥接进程自动被杀、防止孤儿进程;最后调用 disable_process_dumping() 使其对同用户 ptrace 不可 attach、且不再产生 core dump。这里选用返回 io::Result 的函数而非 pre-main 版本,是因为加固发生在进程生命周期中途(子进程侧初始化阶段),调用方需要以 ? 传播错误并决定失败处理策略,而不是直接 exit。这一处用法同时展示了本 crate 的两种消费模式:启动前整体加固(pre_main_hardening)与运行时单项加固(disable_process_dumping)。

依赖关系上,linux-sandbox/Cargo.toml 同样以 workspace 依赖声明了 codex-process-hardening = { workspace = true }

5. 如何在自己项目中复用这套模式

从源码结构看,该 crate 是 workspace 内部 crate(版本走 workspace = true),直接以路径依赖方式引入即可参考其写法。若要独立实现等价加固,可按如下清单照搬:

  1. Linux/Android:pre-main 中先 prctl(PR_SET_DUMPABLE, 0)(同时关闭 core dump 与同用户 ptrace attach),再 setrlimit(RLIMIT_CORE, {0,0}) 兜底,最后删除所有 LD_ 前缀环境变量;
  2. macOS:pre-main 中 ptrace(PT_DENY_ATTACH, 0, NULL, 0)setrlimit(RLIMIT_CORE, {0,0}),删除所有 DYLD_ 前缀环境变量;
  3. 环境变量清理必须在字节层(OsStr::as_bytes)做前缀匹配,不要假设键名是 UTF-8;
  4. 失败策略:要么像本 crate 一样 fail-fast 并用独立退出码区分失败层,要么返回 Result 由调用方决策,二者取其一,避免"加固失败但继续裸奔运行";
  5. 静态链接可显著削弱 LD_PRELOAD 风险(MUSL 静态链接程序不解释 glibc 的动态链接变量),但源码注释明确把它当作"just to be sure"的第二道防线而非替代。

适用前提与限制:上述调用全部依赖 libc 且为 unsafe,仅覆盖 Linux、Android、macOS、FreeBSD、OpenBSD,Windows 为空实现;PT_DENY_ATTACH 只对当前进程生效,不会传播给后续 fork 的子进程;PR_SET_DUMPABLE=0 会让该进程自身的 core dump 永远不可用,需要崩溃转储排障的场景应权衡后在构建层关掉加固而非运行时绕过。

6. 小结

codex-process-hardening 用一个不到 200 行、单依赖 libc 的 crate,把"安全敏感型 Rust 二进制启动前该做的三件事"(禁 core dump、拒绝调试器 attach、清 LD_*/DYLD_*)做成了平台分发的标准库形态,并以专用退出码(5/6/7)保证加固失败时 fail-fast 且可观测;仓库内 responses-api-proxylinux-sandbox 两处调用分别示范了 pre-main 整体加固与运行时单项加固两种消费方式。对需要保护内存中密钥材料、或会 fork 出守护/桥接子进程的 Rust 网络型进程而言,这是一份可以直接抄作业的加固基线。

登录后查看全文
热门项目推荐
相关项目推荐