vtcode_process_hardening/
lib.rs1#[cfg(unix)]
2use std::ffi::OsString;
3#[cfg(unix)]
4use std::os::unix::ffi::OsStrExt;
5
6use ctor::ctor;
7
8#[cfg(any(target_os = "linux", target_os = "android"))]
9#[allow(unsafe_code)]
10fn prctl_set_dumpable() -> i32 {
11 unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) }
14}
15
16#[cfg(target_os = "macos")]
17#[allow(unsafe_code)]
18fn ptrace_deny_attach() -> i32 {
19 unsafe { libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) }
22}
23
24#[cfg(unix)]
25#[allow(unsafe_code)]
26fn remove_env_var(key: OsString) {
27 unsafe { std::env::remove_var(key) }
31}
32
33pub fn pre_main_hardening() {
42 #[cfg(any(target_os = "linux", target_os = "android"))]
43 pre_main_hardening_linux();
44
45 #[cfg(target_os = "macos")]
46 pre_main_hardening_macos();
47
48 #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
49 pre_main_hardening_bsd();
50
51 #[cfg(windows)]
52 pre_main_hardening_windows();
53}
54
55#[cfg(any(target_os = "linux", target_os = "android"))]
56const PRCTL_FAILED_EXIT_CODE: i32 = 5;
57
58#[cfg(target_os = "macos")]
59const PTRACE_DENY_ATTACH_FAILED_EXIT_CODE: i32 = 6;
60
61#[cfg(any(
62 target_os = "linux",
63 target_os = "android",
64 target_os = "macos",
65 target_os = "freebsd",
66 target_os = "netbsd",
67 target_os = "openbsd"
68))]
69const SET_RLIMIT_CORE_FAILED_EXIT_CODE: i32 = 7;
70
71#[cfg(any(target_os = "linux", target_os = "android"))]
72pub(crate) fn pre_main_hardening_linux() {
73 cap_stack_rlimit();
74
75 let ret_code = prctl_set_dumpable();
77 if ret_code != 0 {
78 eprintln!(
79 "ERROR: prctl(PR_SET_DUMPABLE, 0) failed: {}",
80 std::io::Error::last_os_error()
81 );
82 std::process::exit(PRCTL_FAILED_EXIT_CODE);
83 }
84
85 set_core_file_size_limit_to_zero();
87
88 let ld_keys = env_keys_with_prefix(std::env::vars_os(), b"LD_");
91 for key in ld_keys {
92 remove_env_var(key);
93 }
94}
95
96#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
97pub(crate) fn pre_main_hardening_bsd() {
98 cap_stack_rlimit();
99
100 set_core_file_size_limit_to_zero();
102
103 let ld_keys = env_keys_with_prefix(std::env::vars_os(), b"LD_");
104 for key in ld_keys {
105 remove_env_var(key);
106 }
107}
108
109#[cfg(target_os = "macos")]
110pub(crate) fn pre_main_hardening_macos() {
111 cap_stack_rlimit();
112
113 let ret_code = ptrace_deny_attach();
115 if ret_code == -1 {
116 eprintln!(
117 "ERROR: ptrace(PT_DENY_ATTACH) failed: {}",
118 std::io::Error::last_os_error()
119 );
120 std::process::exit(PTRACE_DENY_ATTACH_FAILED_EXIT_CODE);
121 }
122
123 set_core_file_size_limit_to_zero();
125
126 let dyld_keys = env_keys_with_prefix(std::env::vars_os(), b"DYLD_");
129 for key in dyld_keys {
130 remove_env_var(key);
131 }
132}
133
134#[cfg(unix)]
135#[allow(unsafe_code)]
136fn set_core_file_size_limit_to_zero() {
137 let rlim = libc::rlimit {
138 rlim_cur: 0,
139 rlim_max: 0,
140 };
141 let ret_code = unsafe { libc::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)]
168#[allow(unsafe_code)]
169fn cap_stack_rlimit() {
170 #[cfg(any(
171 target_os = "linux",
172 target_os = "android",
173 target_os = "macos",
174 target_os = "freebsd",
175 target_os = "netbsd",
176 target_os = "openbsd"
177 ))]
178 {
179 const STACK_CAP_BYTES: u64 = 8 * 1024 * 1024;
180
181 let mut current: libc::rlimit = libc::rlimit {
182 rlim_cur: 0,
183 rlim_max: 0,
184 };
185 let ret = unsafe { libc::getrlimit(libc::RLIMIT_STACK, &mut current) };
187 if ret != 0 {
188 return;
189 }
190 if current.rlim_cur != libc::RLIM_INFINITY {
192 return;
193 }
194 let capped = libc::rlimit {
195 rlim_cur: STACK_CAP_BYTES,
196 rlim_max: STACK_CAP_BYTES,
197 };
198 let _ = unsafe { libc::setrlimit(libc::RLIMIT_STACK, &capped) };
201 }
202}
203
204#[cfg(windows)]
205pub(crate) fn pre_main_hardening_windows() {
206 }
210
211#[cfg(unix)]
224#[ctor(unsafe, priority = 101)]
225fn verify_hardening_effectiveness() {
226 use std::mem::MaybeUninit;
227
228 #[cfg(any(
230 target_os = "linux",
231 target_os = "android",
232 target_os = "macos",
233 target_os = "freebsd",
234 target_os = "netbsd",
235 target_os = "openbsd"
236 ))]
237 {
238 #[allow(unsafe_code)]
241 let mut rlim: libc::rlimit = unsafe { MaybeUninit::zeroed().assume_init() };
242 #[allow(unsafe_code)]
245 let ret = unsafe { libc::getrlimit(libc::RLIMIT_CORE, &mut rlim) };
246 if ret == 0 && rlim.rlim_cur != 0 {
247 #[allow(unsafe_code)]
252 unsafe {
253 let msg =
254 b"warning: vtcode-process-hardening: RLIMIT_CORE is not zero after hardening\n";
255 libc::write(
256 libc::STDERR_FILENO,
257 msg.as_ptr() as *const libc::c_void,
258 msg.len(),
259 );
260 }
261 }
262 }
263}
264
265#[cfg(unix)]
271#[ctor(unsafe, priority = 102)]
272fn check_environment_sanity() {
273 let home_set = std::env::var_os("HOME").is_some();
274 if !home_set {
275 #[allow(unsafe_code)]
278 unsafe {
279 let msg =
280 b"warning: HOME environment variable is not set; config resolution may fail\n";
281 libc::write(
282 libc::STDERR_FILENO,
283 msg.as_ptr() as *const libc::c_void,
284 msg.len(),
285 );
286 }
287 }
288
289 #[cfg(not(target_os = "windows"))]
291 {
292 let user_set = std::env::var_os("USER").is_some();
293 if !user_set {
294 #[allow(unsafe_code)]
297 unsafe {
298 let msg = b"warning: USER environment variable is not set; some features may not work correctly\n";
299 libc::write(
300 libc::STDERR_FILENO,
301 msg.as_ptr() as *const libc::c_void,
302 msg.len(),
303 );
304 }
305 }
306 }
307}
308
309#[cfg(unix)]
310fn env_keys_with_prefix<I>(vars: I, prefix: &[u8]) -> Vec<OsString>
311where
312 I: IntoIterator<Item = (OsString, OsString)>,
313{
314 vars.into_iter()
315 .filter_map(|(key, _)| {
316 key.as_os_str()
317 .as_bytes()
318 .starts_with(prefix)
319 .then_some(key)
320 })
321 .collect()
322}
323
324#[cfg(all(test, unix))]
325mod tests {
326 use super::*;
327 use pretty_assertions::assert_eq;
328 use std::ffi::OsStr;
329 use std::os::unix::ffi::OsStrExt;
330 use std::os::unix::ffi::OsStringExt;
331
332 #[test]
333 fn env_keys_with_prefix_handles_non_utf8_entries() {
334 let non_utf8_key1 = OsStr::from_bytes(b"R\xD6DBURK").to_os_string();
336 assert!(non_utf8_key1.clone().into_string().is_err());
337
338 let non_utf8_key2 = OsString::from_vec(vec![b'L', b'D', b'_', 0xF0]);
339 assert!(non_utf8_key2.clone().into_string().is_err());
340
341 let non_utf8_value = OsString::from_vec(vec![0xF0, 0x9F, 0x92, 0xA9]);
342
343 let keys = env_keys_with_prefix(
344 vec![
345 (non_utf8_key1, non_utf8_value.clone()),
346 (non_utf8_key2.clone(), non_utf8_value),
347 ],
348 b"LD_",
349 );
350
351 assert_eq!(
352 keys,
353 vec![non_utf8_key2],
354 "non-UTF-8 env entries with LD_ prefix should be retained"
355 );
356 }
357
358 #[test]
359 fn env_keys_with_prefix_filters_only_matching_keys() {
360 let ld_test_var = OsStr::from_bytes(b"LD_TEST");
361 let vars = vec![
362 (OsString::from("PATH"), OsString::from("/usr/bin")),
363 (ld_test_var.to_os_string(), OsString::from("1")),
364 (OsString::from("DYLD_FOO"), OsString::from("bar")),
365 ];
366
367 let keys = env_keys_with_prefix(vars, b"LD_");
368 assert_eq!(keys.len(), 1);
369 assert_eq!(keys[0].as_os_str(), ld_test_var);
370 }
371
372 #[test]
373 fn env_keys_with_prefix_returns_empty_when_no_matches_exist() {
374 let vars = vec![
375 (OsString::from("PATH"), OsString::from("/usr/bin")),
376 (OsString::from("HOME"), OsString::from("/tmp/home")),
377 ];
378
379 let keys = env_keys_with_prefix(vars, b"LD_");
380 assert!(keys.is_empty());
381 }
382
383 #[test]
384 fn env_keys_with_prefix_matches_exact_prefix_and_is_case_sensitive() {
385 let vars = vec![
386 (OsString::from("LD_"), OsString::from("exact-prefix")),
387 (OsString::from("Ld_TEST"), OsString::from("mixed-case")),
388 (OsString::from("LD_PRELOAD"), OsString::from("/tmp/lib.so")),
389 ];
390
391 let keys = env_keys_with_prefix(vars, b"LD_");
392 assert_eq!(
393 keys,
394 vec![OsString::from("LD_"), OsString::from("LD_PRELOAD")]
395 );
396 }
397
398 #[test]
399 fn env_keys_with_prefix_supports_dyld_prefix_filtering() {
400 let vars = vec![
401 (
402 OsString::from("DYLD_INSERT_LIBRARIES"),
403 OsString::from("/tmp/inject.dylib"),
404 ),
405 (OsString::from("DYLD_FOO"), OsString::from("bar")),
406 (
407 OsString::from("LD_PRELOAD"),
408 OsString::from("/tmp/other.so"),
409 ),
410 ];
411
412 let keys = env_keys_with_prefix(vars, b"DYLD_");
413 assert_eq!(
414 keys,
415 vec![
416 OsString::from("DYLD_INSERT_LIBRARIES"),
417 OsString::from("DYLD_FOO")
418 ]
419 );
420 }
421}