Skip to main content

vtcode_process_hardening/
lib.rs

1#[cfg(unix)]
2use std::ffi::OsString;
3#[cfg(unix)]
4use std::os::unix::ffi::OsStrExt;
5
6#[cfg(any(target_os = "linux", target_os = "android"))]
7#[allow(unsafe_code)]
8fn prctl_set_dumpable() -> i32 {
9    // SAFETY: `prctl` is called with the documented `PR_SET_DUMPABLE` command and
10    // integer arguments only. No pointers are dereferenced.
11    unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) }
12}
13
14#[cfg(target_os = "macos")]
15#[allow(unsafe_code)]
16fn ptrace_deny_attach() -> i32 {
17    // SAFETY: `ptrace(PT_DENY_ATTACH, ...)` is the documented macOS hardening call.
18    // The null pointer argument is not dereferenced by this request type.
19    unsafe { libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) }
20}
21
22#[cfg(unix)]
23#[allow(unsafe_code)]
24fn remove_env_var(key: OsString) {
25    // SAFETY: Caller must ensure this runs during single-threaded early process
26    // startup, before any threads are spawned, which satisfies the environment
27    // mutation safety requirement.
28    unsafe { std::env::remove_var(key) }
29}
30
31/// Perform early process hardening as the first operation in `main()`.
32///
33/// Call this before any other operations, thread spawning, or heap
34/// allocation to ensure the process is locked down before potential
35/// adversaries can influence its state. Steps include:
36/// - disabling core dumps
37/// - disabling ptrace attach on Linux and macOS
38/// - removing dangerous environment variables such as LD_PRELOAD and DYLD_*
39pub fn pre_main_hardening() {
40    #[cfg(any(target_os = "linux", target_os = "android"))]
41    pre_main_hardening_linux();
42
43    #[cfg(target_os = "macos")]
44    pre_main_hardening_macos();
45
46    #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
47    pre_main_hardening_bsd();
48
49    #[cfg(windows)]
50    pre_main_hardening_windows();
51}
52
53#[cfg(any(target_os = "linux", target_os = "android"))]
54const PRCTL_FAILED_EXIT_CODE: i32 = 5;
55
56#[cfg(target_os = "macos")]
57const PTRACE_DENY_ATTACH_FAILED_EXIT_CODE: i32 = 6;
58
59#[cfg(any(
60    target_os = "linux",
61    target_os = "android",
62    target_os = "macos",
63    target_os = "freebsd",
64    target_os = "netbsd",
65    target_os = "openbsd"
66))]
67const SET_RLIMIT_CORE_FAILED_EXIT_CODE: i32 = 7;
68
69#[cfg(any(target_os = "linux", target_os = "android"))]
70pub(crate) fn pre_main_hardening_linux() {
71    cap_stack_rlimit();
72
73    // Disable ptrace attach / mark process non-dumpable.
74    let ret_code = prctl_set_dumpable();
75    if ret_code != 0 {
76        eprintln!(
77            "ERROR: prctl(PR_SET_DUMPABLE, 0) failed: {}",
78            std::io::Error::last_os_error()
79        );
80        std::process::exit(PRCTL_FAILED_EXIT_CODE);
81    }
82
83    // For "defense in depth," set the core file size limit to 0.
84    set_core_file_size_limit_to_zero();
85
86    // VT Code is primarily MUSL-linked in release builds, which means that variables such
87    // as LD_PRELOAD are ignored anyway, but just to be sure, clear them here.
88    let ld_keys = env_keys_with_prefix(std::env::vars_os(), b"LD_");
89    for key in ld_keys {
90        remove_env_var(key);
91    }
92}
93
94#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
95pub(crate) fn pre_main_hardening_bsd() {
96    cap_stack_rlimit();
97
98    // FreeBSD/OpenBSD: set RLIMIT_CORE to 0 and clear LD_* env vars
99    set_core_file_size_limit_to_zero();
100
101    let ld_keys = env_keys_with_prefix(std::env::vars_os(), b"LD_");
102    for key in ld_keys {
103        remove_env_var(key);
104    }
105}
106
107#[cfg(target_os = "macos")]
108pub(crate) fn pre_main_hardening_macos() {
109    cap_stack_rlimit();
110
111    // Prevent debuggers from attaching to this process.
112    let ret_code = ptrace_deny_attach();
113    if ret_code == -1 {
114        eprintln!(
115            "ERROR: ptrace(PT_DENY_ATTACH) failed: {}",
116            std::io::Error::last_os_error()
117        );
118        std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE);
119    }
120
121    // Set the core file size limit to 0 to prevent core dumps.
122    set_core_file_size_limit_to_zero();
123
124    // Remove all DYLD_* environment variables, which can be used to subvert
125    // library loading.
126    let dyld_keys = env_keys_with_prefix(std::env::vars_os(), b"DYLD_");
127    for key in dyld_keys {
128        remove_env_var(key);
129    }
130}
131
132#[cfg(unix)]
133#[allow(unsafe_code)]
134fn set_core_file_size_limit_to_zero() {
135    let rlim = libc::rlimit {
136        rlim_cur: 0,
137        rlim_max: 0,
138    };
139    // SAFETY: `rlim` is fully initialized and passed by shared reference for the
140    // duration of the syscall only.
141    let ret_code = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rlim) };
142    if ret_code != 0 {
143        eprintln!(
144            "ERROR: setrlimit(RLIMIT_CORE) failed: {}",
145            std::io::Error::last_os_error()
146        );
147        std::process::exit(SET_RLIMIT_CORE_FAILED_EXIT_CODE);
148    }
149}
150
151/// Cap the main thread stack size if it is currently unlimited.
152///
153/// This is a complementary hardening measure to Rust's built-in stack clash
154/// protection (`-C probe-stack=inline`).  While probe-stack prevents attackers
155/// from jumping over the kernel guard page with a single large allocation,
156/// capping `RLIMIT_STACK` bounds stack resource exhaustion attacks.
157///
158/// Linux allows `RLIMIT_STACK` to be `RLIM_INFINITY` (unlimited).  We set it
159/// to a fixed cap (8 MiB) when that is the case.  If the current stack usage
160/// already exceeds the cap the call will fail harmlessly with `EINVAL`.
161// This function is a no-op on targets without RLIMIT_STACK (e.g., Windows).
162// The cfg gate prevents dead_code on those targets; the allow attribute
163// suppresses the remaining unused-variables warning on the function-level
164// `current` borrow when no cfg branch matches.
165#[allow(unused_variables)]
166#[allow(unsafe_code)]
167fn cap_stack_rlimit() {
168    #[cfg(any(
169        target_os = "linux",
170        target_os = "android",
171        target_os = "macos",
172        target_os = "freebsd",
173        target_os = "netbsd",
174        target_os = "openbsd"
175    ))]
176    {
177        const STACK_CAP_BYTES: u64 = 8 * 1024 * 1024;
178
179        let mut current: libc::rlimit = libc::rlimit {
180            rlim_cur: 0,
181            rlim_max: 0,
182        };
183        // SAFETY: `current` points to valid writable memory for the syscall to fill.
184        let ret = unsafe { libc::getrlimit(libc::RLIMIT_STACK, &mut current) };
185        if ret != 0 {
186            return;
187        }
188        // Only cap if the soft limit is currently unlimited.
189        if current.rlim_cur != libc::RLIM_INFINITY {
190            return;
191        }
192        let capped = libc::rlimit {
193            rlim_cur: STACK_CAP_BYTES,
194            rlim_max: STACK_CAP_BYTES,
195        };
196        // SAFETY: `capped` is fully initialized and passed by shared reference for
197        // the duration of the syscall only.
198        let _ = unsafe { libc::setrlimit(libc::RLIMIT_STACK, &capped) };
199    }
200}
201
202#[cfg(windows)]
203pub(crate) fn pre_main_hardening_windows() {
204    // Windows process hardening would involve using Job Objects to limit
205    // resource usage and restrict UI access, or using restricted tokens.
206    // This is currently a future enhancement.
207}
208
209#[cfg(unix)]
210fn env_keys_with_prefix<I>(vars: I, prefix: &[u8]) -> Vec<OsString>
211where
212    I: IntoIterator<Item = (OsString, OsString)>,
213{
214    vars.into_iter()
215        .filter_map(|(key, _)| {
216            key.as_os_str()
217                .as_bytes()
218                .starts_with(prefix)
219                .then_some(key)
220        })
221        .collect()
222}
223
224#[cfg(all(test, unix))]
225mod tests {
226    use super::*;
227    use pretty_assertions::assert_eq;
228    use std::ffi::OsStr;
229    use std::os::unix::ffi::OsStrExt;
230    use std::os::unix::ffi::OsStringExt;
231
232    #[test]
233    fn env_keys_with_prefix_handles_non_utf8_entries() {
234        // RĂ–DBURK - non-UTF8 environment variable name
235        let non_utf8_key1 = OsStr::from_bytes(b"R\xD6DBURK").to_os_string();
236        assert!(non_utf8_key1.clone().into_string().is_err());
237
238        let non_utf8_key2 = OsString::from_vec(vec![b'L', b'D', b'_', 0xF0]);
239        assert!(non_utf8_key2.clone().into_string().is_err());
240
241        let non_utf8_value = OsString::from_vec(vec![0xF0, 0x9F, 0x92, 0xA9]);
242
243        let keys = env_keys_with_prefix(
244            vec![
245                (non_utf8_key1, non_utf8_value.clone()),
246                (non_utf8_key2.clone(), non_utf8_value),
247            ],
248            b"LD_",
249        );
250
251        assert_eq!(
252            keys,
253            vec![non_utf8_key2],
254            "non-UTF-8 env entries with LD_ prefix should be retained"
255        );
256    }
257
258    #[test]
259    fn env_keys_with_prefix_filters_only_matching_keys() {
260        let ld_test_var = OsStr::from_bytes(b"LD_TEST");
261        let vars = vec![
262            (OsString::from("PATH"), OsString::from("/usr/bin")),
263            (ld_test_var.to_os_string(), OsString::from("1")),
264            (OsString::from("DYLD_FOO"), OsString::from("bar")),
265        ];
266
267        let keys = env_keys_with_prefix(vars, b"LD_");
268        assert_eq!(keys.len(), 1);
269        assert_eq!(keys[0].as_os_str(), ld_test_var);
270    }
271
272    #[test]
273    fn env_keys_with_prefix_returns_empty_when_no_matches_exist() {
274        let vars = vec![
275            (OsString::from("PATH"), OsString::from("/usr/bin")),
276            (OsString::from("HOME"), OsString::from("/tmp/home")),
277        ];
278
279        let keys = env_keys_with_prefix(vars, b"LD_");
280        assert!(keys.is_empty());
281    }
282
283    #[test]
284    fn env_keys_with_prefix_matches_exact_prefix_and_is_case_sensitive() {
285        let vars = vec![
286            (OsString::from("LD_"), OsString::from("exact-prefix")),
287            (OsString::from("Ld_TEST"), OsString::from("mixed-case")),
288            (OsString::from("LD_PRELOAD"), OsString::from("/tmp/lib.so")),
289        ];
290
291        let keys = env_keys_with_prefix(vars, b"LD_");
292        assert_eq!(
293            keys,
294            vec![OsString::from("LD_"), OsString::from("LD_PRELOAD")]
295        );
296    }
297
298    #[test]
299    fn env_keys_with_prefix_supports_dyld_prefix_filtering() {
300        let vars = vec![
301            (
302                OsString::from("DYLD_INSERT_LIBRARIES"),
303                OsString::from("/tmp/inject.dylib"),
304            ),
305            (OsString::from("DYLD_FOO"), OsString::from("bar")),
306            (
307                OsString::from("LD_PRELOAD"),
308                OsString::from("/tmp/other.so"),
309            ),
310        ];
311
312        let keys = env_keys_with_prefix(vars, b"DYLD_");
313        assert_eq!(
314            keys,
315            vec![
316                OsString::from("DYLD_INSERT_LIBRARIES"),
317                OsString::from("DYLD_FOO")
318            ]
319        );
320    }
321}