Files
codex/codex-rs/process-hardening/src/lib.rs
T
Felipe CouryandGitHub 8a94430bb2 fix(process-hardening): preserve macos malloc diagnostics (#24479)
## Summary

Follow-up to #24459 and partial behavioral revert of `a71fc47` / #16699.

- Stop removing `MallocStackLogging*` and `MallocLogFile*` from macOS
pre-main hardening.
- Remove documentation that claims Codex suppresses those allocator
diagnostic controls.
- Retain the shared `remove_env_vars_with_prefix` refactor and existing
`LD_` / `DYLD_` hardening.

## Why

#24459 fixes the composer-corruption problem at the terminal stderr
boundary while preserving redirected stderr. With that guard in place,
stripping macOS malloc diagnostic settings is unnecessary and can hide
diagnostics intentionally enabled by callers.

## Validation

- `just fmt`
- `just test -p codex-process-hardening`
- `just argument-comment-lint-from-source -p codex-process-hardening`
- `git diff --check`
2026-05-25 17:26:10 -03:00

194 lines
5.8 KiB
Rust

#[cfg(unix)]
use std::ffi::OsString;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
/// 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();
}
#[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;
#[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_");
}
/// 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())
}
}
#[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_");
}
#[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_");
}
#[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);
}
}
#[cfg(windows)]
pub(crate) fn pre_main_hardening_windows() {
// TODO(mbolin): Perform the appropriate configuration for Windows.
}
#[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()
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::ffi::OsStringExt;
#[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"
);
}
#[test]
fn env_keys_with_prefix_filters_only_matching_keys() {
let ld_test_var = OsStr::from_bytes(b"LD_TEST");
let vars = vec![
(OsString::from("PATH"), OsString::from("/usr/bin")),
(ld_test_var.to_os_string(), OsString::from("1")),
(OsString::from("DYLD_FOO"), OsString::from("bar")),
];
let keys = env_keys_with_prefix(vars, b"LD_");
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].as_os_str(), ld_test_var);
}
}