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
/// 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
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
#[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
/// 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
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
#[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
#[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
#[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
#[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
#[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
#[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
#[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 行为。
在源码仓库中可运行:
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 检测与脱敏。
