Skip to main content

codex_process_hardening/
lib.rs

1#[cfg(unix)]
2use std::ffi::OsString;
3
4#[cfg(unix)]
5use std::os::unix::ffi::OsStrExt;
6
7/// This is designed to be called pre-main() (using `#[ctor::ctor]`) to perform
8/// various process hardening steps, such as
9/// - disabling core dumps
10/// - disabling ptrace attach on Linux and macOS.
11/// - removing dangerous environment variables such as LD_PRELOAD and DYLD_*
12pub fn pre_main_hardening() {
13    #[cfg(any(target_os = "linux", target_os = "android"))]
14    pre_main_hardening_linux();
15
16    #[cfg(target_os = "macos")]
17    pre_main_hardening_macos();
18
19    // On FreeBSD and OpenBSD, apply similar hardening to Linux/macOS:
20    #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
21    pre_main_hardening_bsd();
22
23    #[cfg(windows)]
24    pre_main_hardening_windows();
25}
26
27#[cfg(any(target_os = "linux", target_os = "android"))]
28const PRCTL_FAILED_EXIT_CODE: i32 = 5;
29
30#[cfg(target_os = "macos")]
31const PTRACE_DENY_ATTACH_FAILED_EXIT_CODE: i32 = 6;
32
33#[cfg(any(
34    target_os = "linux",
35    target_os = "android",
36    target_os = "macos",
37    target_os = "freebsd",
38    target_os = "netbsd",
39    target_os = "openbsd"
40))]
41const SET_RLIMIT_CORE_FAILED_EXIT_CODE: i32 = 7;
42
43#[cfg(any(target_os = "linux", target_os = "android"))]
44pub(crate) fn pre_main_hardening_linux() {
45    // Disable ptrace attach / mark process non-dumpable.
46    let ret_code = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
47    if ret_code != 0 {
48        eprintln!(
49            "ERROR: prctl(PR_SET_DUMPABLE, 0) failed: {}",
50            std::io::Error::last_os_error()
51        );
52        std::process::exit(PRCTL_FAILED_EXIT_CODE);
53    }
54
55    // For "defense in depth," set the core file size limit to 0.
56    set_core_file_size_limit_to_zero();
57
58    // Official Codex releases are MUSL-linked, which means that variables such
59    // as LD_PRELOAD are ignored anyway, but just to be sure, clear them here.
60    remove_env_vars_with_prefix(b"LD_");
61}
62
63/// Mark the current Linux process non-dumpable so same-user processes cannot attach with ptrace.
64#[cfg(target_os = "linux")]
65pub fn disable_process_dumping() -> std::io::Result<()> {
66    let ret_code = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
67    if ret_code == 0 {
68        Ok(())
69    } else {
70        Err(std::io::Error::last_os_error())
71    }
72}
73
74#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
75pub(crate) fn pre_main_hardening_bsd() {
76    // FreeBSD/OpenBSD: set RLIMIT_CORE to 0 and clear LD_* env vars
77    set_core_file_size_limit_to_zero();
78
79    remove_env_vars_with_prefix(b"LD_");
80}
81
82#[cfg(target_os = "macos")]
83pub(crate) fn pre_main_hardening_macos() {
84    // Prevent debuggers from attaching to this process.
85    let ret_code = unsafe { libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) };
86    if ret_code == -1 {
87        eprintln!(
88            "ERROR: ptrace(PT_DENY_ATTACH) failed: {}",
89            std::io::Error::last_os_error()
90        );
91        std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE);
92    }
93
94    // Set the core file size limit to 0 to prevent core dumps.
95    set_core_file_size_limit_to_zero();
96
97    // Remove all DYLD_ environment variables, which can be used to subvert
98    // library loading.
99    remove_env_vars_with_prefix(b"DYLD_");
100}
101
102#[cfg(unix)]
103fn set_core_file_size_limit_to_zero() {
104    let rlim = libc::rlimit {
105        rlim_cur: 0,
106        rlim_max: 0,
107    };
108
109    let ret_code = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rlim) };
110    if ret_code != 0 {
111        eprintln!(
112            "ERROR: setrlimit(RLIMIT_CORE) failed: {}",
113            std::io::Error::last_os_error()
114        );
115        std::process::exit(SET_RLIMIT_CORE_FAILED_EXIT_CODE);
116    }
117}
118
119#[cfg(windows)]
120pub(crate) fn pre_main_hardening_windows() {
121    // TODO(mbolin): Perform the appropriate configuration for Windows.
122}
123
124#[cfg(unix)]
125fn remove_env_vars_with_prefix(prefix: &[u8]) {
126    for key in env_keys_with_prefix(std::env::vars_os(), prefix) {
127        unsafe {
128            std::env::remove_var(key);
129        }
130    }
131}
132
133#[cfg(unix)]
134fn env_keys_with_prefix<I>(vars: I, prefix: &[u8]) -> Vec<OsString>
135where
136    I: IntoIterator<Item = (OsString, OsString)>,
137{
138    vars.into_iter()
139        .filter_map(|(key, _)| {
140            key.as_os_str()
141                .as_bytes()
142                .starts_with(prefix)
143                .then_some(key)
144        })
145        .collect()
146}
147
148#[cfg(all(test, unix))]
149mod tests {
150    use super::*;
151    use pretty_assertions::assert_eq;
152    use std::ffi::OsStr;
153    use std::os::unix::ffi::OsStrExt;
154    use std::os::unix::ffi::OsStringExt;
155
156    #[test]
157    fn env_keys_with_prefix_handles_non_utf8_entries() {
158        // RĂ–DBURK
159        let non_utf8_key1 = OsStr::from_bytes(b"R\xD6DBURK").to_os_string();
160        assert!(non_utf8_key1.clone().into_string().is_err());
161        let non_utf8_key2 = OsString::from_vec(vec![b'L', b'D', b'_', 0xF0]);
162        assert!(non_utf8_key2.clone().into_string().is_err());
163
164        let non_utf8_value = OsString::from_vec(vec![0xF0, 0x9F, 0x92, 0xA9]);
165
166        let keys = env_keys_with_prefix(
167            vec![
168                (non_utf8_key1, non_utf8_value.clone()),
169                (non_utf8_key2.clone(), non_utf8_value),
170            ],
171            b"LD_",
172        );
173        assert_eq!(
174            keys,
175            vec![non_utf8_key2],
176            "non-UTF-8 env entries with LD_ prefix should be retained"
177        );
178    }
179
180    #[test]
181    fn env_keys_with_prefix_filters_only_matching_keys() {
182        let ld_test_var = OsStr::from_bytes(b"LD_TEST");
183        let vars = vec![
184            (OsString::from("PATH"), OsString::from("/usr/bin")),
185            (ld_test_var.to_os_string(), OsString::from("1")),
186            (OsString::from("DYLD_FOO"), OsString::from("bar")),
187        ];
188
189        let keys = env_keys_with_prefix(vars, b"LD_");
190        assert_eq!(keys.len(), 1);
191        assert_eq!(keys[0].as_os_str(), ld_test_var);
192    }
193}