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