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