vtcode_process_hardening/
lib.rs1#[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: i32, rlim: &libc::rlimit) -> i32 {
27 unsafe { libc::setrlimit(resource, rlim) }
28}
29
30#[cfg(unix)]
31#[allow(unsafe_code)]
32fn getrlimit(resource: i32, rlim: &mut libc::rlimit) -> i32 {
33 unsafe { libc::getrlimit(resource, rlim) }
34}
35
36pub 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 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 set_core_file_size_limit_to_zero();
90
91 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 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 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_core_file_size_limit_to_zero();
128
129 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#[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 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 }
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 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}