Skip to content

ProcessHardening

从 ctor 调用点到 ptrace、core dump、动态加载器环境和 Linux bridge hardening,解释进程加固的真实覆盖范围。

基于rust-v0.150.0
CodexRustSecurityProcess

ProcessHardening ​

codex-process-hardening 是进程自身的纵深防御层,不是文件系统或网络 sandbox。它处理三类风险:降低进程内存被同用户调试器读取的可能性、禁止生成 core dump、清理可能影响后续子进程动态加载行为的环境变量。不同平台支持的原语不同,Windows 当前仍为空实现;而且 crate 不会自动作用于所有 Codex 二进制,调用方必须显式在 main 前或运行期调用对应入口。

本文承接Linux Seccomp与Namespace和WindowsSandbox架构。前文解释平台 sandbox 如何限制命令,本篇只回答当前进程如何缩小调试、dump 和环境继承攻击面,并说明这些措施不能替代 sandbox、ACL、Seccomp 或 secret redaction。

1. 入口由调用方决定 ​

pre_main_hardening 只是平台分派函数。它的文档说明应通过 #[ctor::ctor] 在 main 前调用,但 crate 自身没有注册全局 ctor。当前源码中,responses-api-proxy 显式定义 pre_main ctor;因此不能从 workspace 依赖关系推断所有 Codex 进程都会自动执行 hardening。

源码位置:codex-rs/process-hardening/src/lib.rs :: pre_main_hardening

rust
/// This 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_*
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();
}

源码位置:codex-rs/responses-api-proxy/src/main.rs :: pre_main、main

rust
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)
}

执行顺序是 loader 初始化 → ctor → Rust main。这比在 main 开头调用更早,但仍不早于操作系统动态加载器本身。

2. Linux与Android ​

Linux/Android 分支先调用 PR_SET_DUMPABLE=0,失败立即退出;随后把 soft/hard core limit 都设为 0,最后移除所有 LD_ 前缀环境变量。三步的目标不同:dumpable 影响 ptrace 与 /proc 暴露规则,RLIMIT_CORE 控制 core 文件,环境清理影响后续继承和读取。

源码位置:codex-rs/process-hardening/src/lib.rs :: pre_main_hardening_linux

rust
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) fn pre_main_hardening_linux() {
    // 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);
    }

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

    // 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_");
}

PR_SET_DUMPABLE=0 不是完整的反调试边界:具有足够 privilege 的进程仍可能绕过普通同用户限制。它也不是 Seccomp,不限制被加固进程可以调用哪些 syscall。

3. 运行期Linux入口 ​

Linux 另有一个返回 io::Result 的 disable_process_dumping。它没有设置 RLIMIT_CORE,也不清理 LD_ 环境;调用方可以在已经运行的生命周期中单独把当前进程标为 non-dumpable。

源码位置:codex-rs/process-hardening/src/lib.rs :: disable_process_dumping

rust
/// 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())
    }
}

Linux sandbox 的 host proxy bridge 就使用这个窄入口。它先断开标准输入输出、设置 parent-death signal,再关闭 dumpable;任一步失败都会返回错误,bridge 不会假装已经完成 hardening。

源码位置:codex-rs/linux-sandbox/src/proxy_lifecycle.rs :: harden_bridge_process、set_parent_death_signal

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

fn set_parent_death_signal(expected_parent_pid: libc::pid_t) -> io::Result<()> {
    let res = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) };
    if res != 0 {
        Err(io::Error::last_os_error())
    } else if unsafe { libc::getppid() } != expected_parent_pid {
        Err(io::Error::other("parent process already exited"))
    } else {
        Ok(())
    }
}

这里的 parent PID 二次检查关闭了一个竞态:父进程可能在 prctl 前已经退出,此时仅设置 PDEATHSIG 不一定收到过去的死亡事件。

4. Core dump限制 ​

Unix 共用 set_core_file_size_limit_to_zero,同时把 rlim_cur 与 rlim_max 设为 0。hard limit 也归零意味着普通非特权进程不能在后续自行恢复 core dump 配额;该限制通常还会被子进程继承。

源码位置:codex-rs/process-hardening/src/lib.rs :: set_core_file_size_limit_to_zero

rust
#[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);
    }
}

RLIMIT_CORE 只阻止常规 core 文件生成,不等于擦除进程内存,也不能替代日志与错误信息中的 secret redaction。

5. macOS与BSD ​

macOS 使用 ptrace(PT_DENY_ATTACH),再设置 core limit 并移除 DYLD_ 环境变量。FreeBSD/OpenBSD 没有对应的 ptrace 调用,只执行 core limit 与 LD_ 清理。NetBSD 出现在 exit-code cfg 中,但没有 pre_main_hardening 分支,因此当前 dispatcher 不会在 NetBSD 调用 core-limit helper。

源码位置:codex-rs/process-hardening/src/lib.rs :: pre_main_hardening_macos

rust
#[cfg(target_os = "macos")]
pub(crate) fn pre_main_hardening_macos() {
    // 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::io::Error::last_os_error()
        );
        std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE);
    }

    // Set the core file size limit to 0 to prevent core dumps.
    set_core_file_size_limit_to_zero();

    // Remove all DYLD_ environment variables, which can be used to subvert
    // library loading.
    remove_env_vars_with_prefix(b"DYLD_");
}

源码位置:codex-rs/process-hardening/src/lib.rs :: pre_main_hardening_bsd

rust
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
pub(crate) fn pre_main_hardening_bsd() {
    // FreeBSD/OpenBSD: set RLIMIT_CORE to 0 and clear LD_* env vars
    set_core_file_size_limit_to_zero();

    remove_env_vars_with_prefix(b"LD_");
}

PT_DENY_ATTACH 会影响调试体验,但不等价于系统级 sandbox 或 code-signing policy。

6. 环境变量清理 ​

环境变量 key 在 Unix 上不保证 UTF-8。实现通过 OsStrExt::as_bytes 做 prefix 匹配,先收集 key,再逐个 remove_var,避免在遍历环境时原地修改集合。

源码位置:codex-rs/process-hardening/src/lib.rs :: remove_env_vars_with_prefix、env_keys_with_prefix

rust
#[cfg(unix)]
fn remove_env_vars_with_prefix(prefix: &[u8]) {
    for key in env_keys_with_prefix(std::env::vars_os(), prefix) {
        unsafe {
            std::env::remove_var(key);
        }
    }
}

#[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()
}

这一步有明确时间边界:ctor 执行时,动态加载器已经完成当前进程的初始装载。因此清理不能撤销已经生效的 LD_PRELOAD 或 DYLD_INSERT_LIBRARIES;它主要减少程序后续读取和子进程继承这些变量的风险。源码注释也明确指出官方 Linux 构建使用 musl,这使 LD_PRELOAD 在该构建形态下本就不生效,清理属于纵深防御。

7. Windows空实现 ​

Windows 分支只有 TODO,没有 process mitigation policy、dump restriction、token 或 DLL search hardening。Windows restricted token、ACL、private desktop 和 Job Object 属于 windows-sandbox-rs,不能算作本 crate 的实现。

源码位置:codex-rs/process-hardening/src/lib.rs :: pre_main_hardening_windows

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

跨平台能力表必须把 Windows 标为 no-op,而不是因为函数存在就标记为已支持。

8. 失败即退出 ​

pre-main 路径不返回 Result。Linux prctl 失败以 5 退出,macOS ptrace 失败以 6 退出,Unix setrlimit 失败以 7 退出;环境变量删除没有单独错误返回。设计意图是:声明启用的核心 hardening 原语失败时,不进入业务 main。

源码位置:codex-rs/process-hardening/src/lib.rs :: hardening exit codes

rust
#[cfg(any(target_os = "linux", target_os = "android"))]
const PRCTL_FAILED_EXIT_CODE: i32 = 5;

#[cfg(target_os = "macos")]
const PTRACE_DENY_ATTACH_FAILED_EXIT_CODE: i32 = 6;

#[cfg(any(
    target_os = "linux",
    target_os = "android",
    target_os = "macos",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd"
))]
const SET_RLIMIT_CORE_FAILED_EXIT_CODE: i32 = 7;

运行期 disable_process_dumping 则返回 io::Result,由 bridge 调用方决定如何停止启动;两个入口的失败接口不能混为一谈。

9. 测试与边界 ​

crate 的可移植单元测试只覆盖环境 key 选择,尤其验证非 UTF-8 LD_ key。它们不会主动调用 pre_main_hardening,因为 ptrace、dumpable 和 hard-limit 修改会影响测试进程本身且难以恢复。

源码位置:codex-rs/process-hardening/src/lib.rs :: env_keys_with_prefix_handles_non_utf8_entries

rust
#[test]
fn env_keys_with_prefix_handles_non_utf8_entries() {
    // RÖDBURK
    let non_utf8_key1 = OsStr::from_bytes(b"R\xD6DBURK").to_os_string();
    assert!(non_utf8_key1.clone().into_string().is_err());
    let non_utf8_key2 = OsString::from_vec(vec![b'L', b'D', b'_', 0xF0]);
    assert!(non_utf8_key2.clone().into_string().is_err());

    let non_utf8_value = OsString::from_vec(vec![0xF0, 0x9F, 0x92, 0xA9]);

    let keys = env_keys_with_prefix(
        vec![
            (non_utf8_key1, non_utf8_value.clone()),
            (non_utf8_key2.clone(), non_utf8_value),
        ],
        b"LD_",
    );
    assert_eq!(
        keys,
        vec![non_utf8_key2],
        "non-UTF-8 env entries with LD_ prefix should be retained"
    );
}

测试输入是两条非 UTF-8 key,其中只有一条以 LD_ 开头;断言返回集合只包含匹配 key。它证明 byte-prefix 过滤,不证明 prctl、ptrace、core dump 或 loader 行为。

在源码仓库中可运行:

text
cd codex-rs
cargo test -p codex-process-hardening --lib -- --test-threads=1

当前源码文件在两个版本标签间内容一致,复核结论是技术主线保持稳定;但消费者和证明边界仍需重新确认,不能只因 diff 为零就跳过阅读。

补充平台分支图,突出 pre_main_hardening 的统一入口与各平台实现的差异。

10. 阅读闭环 ​

建议按 consumer ctor → pre_main_hardening dispatcher → Linux/macOS/BSD 分支 → core limit → raw env key 清理 → Windows no-op → Linux bridge runtime helper 阅读。读完后应能解释:为什么 crate 不会自动保护所有二进制;为什么 ctor 清理无法撤销 loader 已执行的注入;为什么 bridge 只调用 non-dumpable helper;以及 process hardening 与 sandbox、secret redaction、Job Object 的责任边界。

下一篇进入 Secrets 检测与脱敏。