1#![allow(unsafe_code)]
4
5#[cfg(unix)]
6pub(crate) mod unix {
7 use std::io;
8
9 pub(crate) fn effective_uid() -> u32 {
10 unsafe { libc::geteuid() }
12 }
13
14 pub(crate) fn try_lock_exclusive(file: &std::fs::File) -> io::Result<bool> {
18 use std::os::unix::io::AsRawFd as _;
19 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0 {
21 return Ok(true);
22 }
23 let error = io::Error::last_os_error();
24 if error.kind() == io::ErrorKind::WouldBlock {
25 return Ok(false);
26 }
27 Err(error)
28 }
29
30 pub(crate) fn process_exists(pid: u32) -> bool {
33 let Ok(pid) = i32::try_from(pid) else { return false };
34 if pid <= 1 {
35 return false;
36 }
37 if unsafe { libc::kill(pid, 0) } == 0 {
40 return true;
41 }
42 io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
43 }
44
45 pub(crate) fn signal_process(pid: u32, signal: i32) -> io::Result<()> {
48 let pid = i32::try_from(pid)
49 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "pid does not fit pid_t"))?;
50 signal_raw(pid, signal)
51 }
52
53 pub(crate) fn signal_raw(pid: i32, signal: i32) -> io::Result<()> {
54 if unsafe { libc::kill(pid, signal) } == 0 {
56 return Ok(());
57 }
58 let error = io::Error::last_os_error();
59 if error.raw_os_error() == Some(libc::ESRCH) {
60 Ok(())
61 } else {
62 Err(error)
63 }
64 }
65
66 pub(crate) fn owned_process_group(pid: u32) -> Option<i32> {
68 let pid = i32::try_from(pid).ok()?;
69 if pid <= 1 {
70 return None;
71 }
72 let group = unsafe { libc::getpgid(pid) };
75 (group == pid).then_some(group)
76 }
77
78 pub(crate) fn signal_group(group: i32, signal: i32) -> io::Result<()> {
79 if group <= 1 {
80 return Err(io::Error::new(
81 io::ErrorKind::InvalidInput,
82 "process group must be greater than 1",
83 ));
84 }
85 signal_raw(-group, signal)
86 }
87
88 pub(crate) fn configure_detached(command: &mut std::process::Command) {
91 use std::os::unix::process::CommandExt as _;
92 unsafe {
95 command.pre_exec(create_session);
96 }
97 }
98
99 fn create_session() -> io::Result<()> {
102 if unsafe { libc::setsid() } == -1 {
104 Err(io::Error::last_os_error())
105 } else {
106 Ok(())
107 }
108 }
109
110 #[cfg(test)]
111 mod tests {
112 #[test]
113 fn current_process_exists_and_reserved_ids_are_rejected() {
114 assert!(super::process_exists(std::process::id()));
115 assert!(!super::process_exists(0));
116 assert!(!super::process_exists(1));
117 }
118 }
119}
120
121pub fn harden_process() {
126 #[cfg(windows)]
127 windows::restrict_dll_search_path();
128}
129
130pub(crate) fn process_exists(pid: u32) -> bool {
131 #[cfg(unix)]
132 {
133 unix::process_exists(pid)
134 }
135 #[cfg(windows)]
136 {
137 windows::process_exists(pid)
138 }
139 #[cfg(not(any(unix, windows)))]
140 {
141 let _ = pid;
142 false
143 }
144}
145
146#[cfg(windows)]
147pub(crate) mod windows {
148 use std::ffi::c_void;
149 use std::io;
150
151 use windows_sys::Win32::Foundation::{
152 CloseHandle, LocalFree, SetHandleInformation, ERROR_ACCESS_DENIED, HANDLE,
153 HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, STILL_ACTIVE,
154 };
155 use windows_sys::Win32::Security::Authorization::{
156 ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
157 SDDL_REVISION_1,
158 };
159 use windows_sys::Win32::Security::{
160 GetTokenInformation, TokenElevation, TokenUser, SECURITY_ATTRIBUTES, TOKEN_ELEVATION,
161 TOKEN_QUERY, TOKEN_USER,
162 };
163 use windows_sys::Win32::System::Console::{
164 GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
165 };
166 use windows_sys::Win32::System::JobObjects::{
167 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
168 SetInformationJobObject, TerminateJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
169 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
170 };
171 use windows_sys::Win32::System::LibraryLoader::{
172 SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS,
173 };
174 use windows_sys::Win32::System::Threading::{
175 GetCurrentProcess, GetExitCodeProcess, OpenProcess, OpenProcessToken, TerminateProcess,
176 CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS,
177 PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_QUOTA, PROCESS_TERMINATE,
178 };
179
180 struct OwnedHandle(HANDLE);
182
183 unsafe impl Send for OwnedHandle {}
186 unsafe impl Sync for OwnedHandle {}
188
189 impl Drop for OwnedHandle {
190 fn drop(&mut self) {
191 unsafe {
194 CloseHandle(self.0);
195 }
196 }
197 }
198
199 fn open_process(pid: u32, access: u32) -> io::Result<OwnedHandle> {
200 let handle = unsafe { OpenProcess(access, 0, pid) };
203 if handle.is_null() {
204 Err(io::Error::last_os_error())
205 } else {
206 Ok(OwnedHandle(handle))
207 }
208 }
209
210 pub(crate) fn process_exists(pid: u32) -> bool {
214 if pid == 0 {
215 return false;
216 }
217 match open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) {
218 Ok(handle) => {
219 let mut code = 0u32;
220 let queried =
222 unsafe { GetExitCodeProcess(handle.0, std::ptr::from_mut(&mut code)) } != 0;
223 queried && code == STILL_ACTIVE as u32
224 }
225 Err(error) => error.raw_os_error() == Some(ERROR_ACCESS_DENIED.cast_signed()),
226 }
227 }
228
229 pub(crate) fn terminate_process(pid: u32) -> io::Result<()> {
232 let handle = match open_process(pid, PROCESS_TERMINATE) {
233 Ok(handle) => handle,
234 Err(error) if !process_exists(pid) => {
235 let _ = error;
236 return Ok(());
237 }
238 Err(error) => return Err(error),
239 };
240 if unsafe { TerminateProcess(handle.0, 1) } == 0 {
242 let error = io::Error::last_os_error();
243 if process_exists(pid) {
244 return Err(error);
245 }
246 }
247 Ok(())
248 }
249
250 pub(crate) fn restrict_dll_search_path() {
261 if unsafe { SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) } == 0 {
264 tracing::warn!(
265 error = %io::Error::last_os_error(),
266 "could not restrict the DLL search path"
267 );
268 }
269 }
270
271 pub(crate) struct PipeSecurity {
281 descriptor: *mut c_void,
282 attributes: SECURITY_ATTRIBUTES,
283 }
284
285 unsafe impl Send for PipeSecurity {}
288 unsafe impl Sync for PipeSecurity {}
290
291 impl Drop for PipeSecurity {
292 fn drop(&mut self) {
293 unsafe {
297 LocalFree(self.descriptor);
298 }
299 }
300 }
301
302 impl PipeSecurity {
303 pub(crate) fn owner_only() -> io::Result<Self> {
304 let sddl = owner_only_sddl()?;
305 let descriptor = parse_sddl(&sddl)?;
306 let attributes = SECURITY_ATTRIBUTES {
307 nLength: u32::try_from(std::mem::size_of::<SECURITY_ATTRIBUTES>())
308 .map_err(|_| io::Error::other("security attributes too large"))?,
309 lpSecurityDescriptor: descriptor,
310 bInheritHandle: 0,
311 };
312 Ok(Self { descriptor, attributes })
313 }
314
315 pub(crate) fn create_pipe_instance(
317 &self,
318 options: &tokio::net::windows::named_pipe::ServerOptions,
319 name: &str,
320 ) -> io::Result<tokio::net::windows::named_pipe::NamedPipeServer> {
321 let attributes: *const SECURITY_ATTRIBUTES = &raw const self.attributes;
322 unsafe {
325 options.create_with_security_attributes_raw(name, attributes.cast_mut().cast())
326 }
327 }
328 }
329
330 fn owner_only_sddl() -> io::Result<String> {
332 let token = current_process_token()?;
333 let sid = token_user_sid(&token)?;
334 let integrity = if token_is_elevated(&token)? { "HI" } else { "ME" };
335 Ok(format!("D:P(A;;GA;;;SY)(A;;GRGW;;;{sid})S:(ML;;NRNW;;;{integrity})"))
336 }
337
338 fn current_process_token() -> io::Result<OwnedHandle> {
339 let mut token: HANDLE = std::ptr::null_mut();
340 if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) } == 0 {
343 return Err(io::Error::last_os_error());
344 }
345 Ok(OwnedHandle(token))
346 }
347
348 fn token_user_sid(token: &OwnedHandle) -> io::Result<String> {
349 let mut needed = 0u32;
350 unsafe {
353 GetTokenInformation(token.0, TokenUser, std::ptr::null_mut(), 0, &raw mut needed);
354 }
355 if needed == 0 {
356 return Err(io::Error::last_os_error());
357 }
358 let mut buffer = vec![0usize; (needed as usize).div_ceil(std::mem::size_of::<usize>())];
360 if unsafe {
362 GetTokenInformation(
363 token.0,
364 TokenUser,
365 buffer.as_mut_ptr().cast(),
366 needed,
367 &raw mut needed,
368 )
369 } == 0
370 {
371 return Err(io::Error::last_os_error());
372 }
373 let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
376 let mut sid_text: *mut u16 = std::ptr::null_mut();
377 if unsafe { ConvertSidToStringSidW(user.User.Sid, &raw mut sid_text) } == 0 {
380 return Err(io::Error::last_os_error());
381 }
382 let sid = wide_to_string(sid_text);
383 unsafe {
385 LocalFree(sid_text.cast());
386 }
387 Ok(sid)
388 }
389
390 fn token_is_elevated(token: &OwnedHandle) -> io::Result<bool> {
391 let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
392 let mut needed = 0u32;
393 let size = u32::try_from(std::mem::size_of::<TOKEN_ELEVATION>())
394 .map_err(|_| io::Error::other("token elevation block too large"))?;
395 if unsafe {
397 GetTokenInformation(
398 token.0,
399 TokenElevation,
400 (&raw mut elevation).cast(),
401 size,
402 &raw mut needed,
403 )
404 } == 0
405 {
406 return Err(io::Error::last_os_error());
407 }
408 Ok(elevation.TokenIsElevated != 0)
409 }
410
411 fn parse_sddl(sddl: &str) -> io::Result<*mut c_void> {
414 let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
415 let mut descriptor: *mut c_void = std::ptr::null_mut();
416 if unsafe {
419 ConvertStringSecurityDescriptorToSecurityDescriptorW(
420 wide.as_ptr(),
421 SDDL_REVISION_1,
422 &raw mut descriptor,
423 std::ptr::null_mut(),
424 )
425 } == 0
426 {
427 return Err(io::Error::last_os_error());
428 }
429 Ok(descriptor)
430 }
431
432 fn wide_to_string(text: *const u16) -> String {
433 let mut len = 0usize;
434 while unsafe { *text.add(len) } != 0 {
437 len += 1;
438 }
439 String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(text, len) })
441 }
442
443 pub(crate) fn disable_std_handle_inheritance() {
453 for id in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
454 let handle = unsafe { GetStdHandle(id) };
457 if handle.is_null() || handle == INVALID_HANDLE_VALUE {
458 continue;
459 }
460 unsafe {
464 SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
465 }
466 }
467 }
468
469 pub(crate) fn configure_detached(command: &mut std::process::Command, breakaway: bool) {
474 use std::os::windows::process::CommandExt as _;
475 disable_std_handle_inheritance();
476 let mut flags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW;
477 if breakaway {
478 flags |= CREATE_BREAKAWAY_FROM_JOB;
479 }
480 command.creation_flags(flags);
481 }
482
483 pub(crate) struct ProcessTreeJob(OwnedHandle);
486
487 impl ProcessTreeJob {
488 pub(crate) fn new() -> io::Result<Self> {
489 let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
492 if job.is_null() {
493 return Err(io::Error::last_os_error());
494 }
495 let job = OwnedHandle(job);
496 let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
499 limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
500 let size = u32::try_from(std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
501 .map_err(|_| io::Error::other("job limit block too large"))?;
502 let configured = unsafe {
504 SetInformationJobObject(
505 job.0,
506 JobObjectExtendedLimitInformation,
507 std::ptr::from_ref(&limits).cast::<c_void>(),
508 size,
509 )
510 };
511 if configured == 0 {
512 return Err(io::Error::last_os_error());
513 }
514 Ok(Self(job))
515 }
516
517 pub(crate) fn assign(&self, pid: u32) -> io::Result<()> {
518 let process = open_process(pid, PROCESS_SET_QUOTA | PROCESS_TERMINATE)?;
519 if unsafe { AssignProcessToJobObject(self.0 .0, process.0) } == 0 {
521 return Err(io::Error::last_os_error());
522 }
523 Ok(())
524 }
525
526 pub(crate) fn terminate(&self) {
527 unsafe {
529 TerminateJobObject(self.0 .0, 1);
530 }
531 }
532 }
533
534 #[cfg(test)]
535 mod tests {
536 #![allow(clippy::expect_used)]
537
538 #[test]
539 fn current_process_exists_and_pid_zero_does_not() {
540 assert!(super::process_exists(std::process::id()));
541 assert!(!super::process_exists(0));
542 }
543
544 #[test]
545 fn job_object_can_be_created_and_owns_a_child() {
546 let job = super::ProcessTreeJob::new().expect("job object");
547 let mut child = std::process::Command::new("cmd.exe")
548 .args(["/c", "ping -n 30 127.0.0.1 >nul"])
549 .spawn()
550 .expect("spawn child");
551 job.assign(child.id()).expect("assign child to job");
552 job.terminate();
553 let status = child.wait().expect("child reaped after job termination");
554 assert!(!status.success());
555 assert!(!super::process_exists(child.id()));
556 }
557
558 #[test]
559 fn owner_only_sddl_names_the_current_user_and_an_integrity_label() {
560 let sddl = super::owner_only_sddl().expect("sddl");
561 assert!(sddl.starts_with("D:P(A;;GA;;;SY)(A;;GRGW;;;S-1-"), "{sddl}");
562 assert!(
563 sddl.ends_with("S:(ML;;NRNW;;;ME)") || sddl.ends_with("S:(ML;;NRNW;;;HI)"),
564 "{sddl}"
565 );
566 let security = super::PipeSecurity::owner_only().expect("descriptor");
567 assert!(!security.descriptor.is_null());
568 }
569
570 #[test]
571 fn dll_search_path_restriction_is_accepted() {
572 super::restrict_dll_search_path();
573 }
574
575 #[test]
576 fn std_handles_stop_being_inheritable() {
577 use windows_sys::Win32::Foundation::{
578 GetHandleInformation, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE,
579 };
580 use windows_sys::Win32::System::Console::{
581 GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
582 };
583
584 super::disable_std_handle_inheritance();
585 for id in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
586 let handle = unsafe { GetStdHandle(id) };
588 if handle.is_null() || handle == INVALID_HANDLE_VALUE {
589 continue;
590 }
591 let mut flags = 0u32;
592 let queried =
594 unsafe { GetHandleInformation(handle, std::ptr::from_mut(&mut flags)) };
595 if queried == 0 {
596 continue;
598 }
599 assert_eq!(flags & HANDLE_FLAG_INHERIT, 0, "std handle {id} still inheritable");
600 }
601 }
602 }
603}