1#[cfg(feature = "ipc")]
4#[path = "platform_linux/ipc.rs"]
5mod ipc;
6#[cfg(feature = "ipc")]
7pub use ipc::{
8 current_user_id as ipc_current_user_id, Endpoint as IpcEndpoint, Listener as IpcListener,
9 ListenerNonblockingMode as IpcListenerNonblockingMode, PeerIdentity as IpcPeerIdentity,
10 Stream as IpcStream,
11};
12#[cfg(feature = "ipc-async")]
13pub use ipc::{AsyncListener as IpcAsyncListener, AsyncStream as IpcAsyncStream};
14
15#[cfg(feature = "session-relay")]
16#[path = "platform_linux_session_relay.rs"]
17mod session_relay;
18#[cfg(feature = "session-relay")]
19pub use session_relay::relay_local_socket_session;
20
21#[path = "platform_linux/terminal.rs"]
22pub mod terminal;
23pub use terminal::active_graphics_probe;
24pub use crate::platform::terminal_input;
25
26#[path = "platform_linux/window_icon.rs"]
27mod window_icon;
28pub use window_icon::{icon_support as window_icon_support_impl, set_icon as set_window_icon_impl};
29
30pub fn shell_command(command: &str) -> std::process::Command {
31 let mut shell = std::process::Command::new("/bin/sh");
32 shell.arg("-lc").arg(command);
33 shell
34}
35
36pub fn compat_shell_command(command: &str) -> std::process::Command {
37 let mut shell = std::process::Command::new("/bin/sh");
38 shell.arg("-lc").arg(command);
39 shell
40}
41
42pub fn canonical_environment_pairs(pairs: Vec<(String, String)>) -> Vec<(String, String)> {
43 pairs
44}
45
46pub fn monitor_console_windows(
47 _duration: std::time::Duration,
48) -> Vec<crate::platform::process::ConsoleWindowInfo> {
49 Vec::new()
50}
51
52use std::ffi::OsStr;
53use std::io;
54use std::io::Read;
55use std::os::fd::{AsRawFd, RawFd};
56use std::os::unix::net::UnixStream;
57use std::sync::Mutex;
58
59use tokio::process::{Child, Command};
60
61use crate::SpawnSpec;
62
63#[path = "platform_linux_descendants.rs"]
64mod descendants;
65pub use descendants::start_descendant_monitor;
66
67#[path = "platform_linux_trace.rs"]
68mod exact_trace;
69pub use exact_trace::{configure_exact_trace, start_exact_trace, TracedChild};
70
71pub fn exact_trace_capability() -> crate::platform::process::ExactTraceCapability {
72 crate::platform::process::ExactTraceCapability {
73 available: true,
74 backend: "linux-ptrace",
75 reason: "launch-time PTRACE_TRACEME with follow-fork/clone/exec/exit supervision",
76 non_invasive_backend: "proc-descendant-snapshot",
77 non_invasive_grade:
78 crate::platform::process::NonInvasiveObservationGrade::SnapshotInferred,
79 }
80}
81
82pub struct WindowsJobHandle;
83
84pub fn assign_child_to_windows_job(
85 _child: &std::process::Child,
86 _direct_pid: u32,
87 _address_space_limit_bytes: Option<u64>,
88 _emit: Option<Box<dyn Fn(crate::platform::process::DescendantEvent) + Send>>,
89) -> io::Result<WindowsJobHandle> {
90 Err(io::Error::new(
91 io::ErrorKind::Unsupported,
92 "Windows Job Objects are unavailable on Linux",
93 ))
94}
95
96#[derive(Default)]
97pub struct CaptureCancellation {
98 wakers: Mutex<CaptureWakers>,
99}
100
101#[derive(Default)]
102struct CaptureWakers {
103 stdout: Option<UnixStream>,
104 stderr: Option<UnixStream>,
105}
106
107struct CancelableCaptureReader<R> {
108 reader: R,
109 wake_reader: UnixStream,
110}
111
112impl<R: Read + AsRawFd> Read for CancelableCaptureReader<R> {
113 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
114 if buf.is_empty() { return Ok(0); }
115 loop {
116 let mut poll_fds = [
117 libc::pollfd { fd: self.reader.as_raw_fd(), events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, revents: 0 },
118 libc::pollfd { fd: self.wake_reader.as_raw_fd(), events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, revents: 0 },
119 ];
120 let polled = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as _, -1) };
122 if polled < 0 {
123 let error = io::Error::last_os_error();
124 if error.kind() == io::ErrorKind::Interrupted { continue; }
125 return Err(error);
126 }
127 if poll_fds[1].revents != 0 {
128 return Err(io::Error::new(io::ErrorKind::Interrupted, "capture reader cancelled"));
129 }
130 if poll_fds[0].revents != 0 {
131 match self.reader.read(buf) {
132 Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
133 result => return result,
134 }
135 }
136 }
137 }
138}
139
140pub fn prepare_capture_reader<R>(
141 reader: R,
142 cancellation: &CaptureCancellation,
143 stream: crate::platform::process::CaptureStream,
144) -> io::Result<Box<dyn Read + Send>>
145where R: Read + AsRawFd + Send + 'static {
146 set_nonblocking(reader.as_raw_fd())?;
147 let (wake_reader, wake_writer) = UnixStream::pair()?;
148 wake_writer.set_nonblocking(true)?;
149 let mut wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
150 match stream {
151 crate::platform::process::CaptureStream::Stdout => wakers.stdout = Some(wake_writer),
152 crate::platform::process::CaptureStream::Stderr => wakers.stderr = Some(wake_writer),
153 }
154 Ok(Box::new(CancelableCaptureReader { reader, wake_reader }))
155}
156
157pub fn capture_reader_done(cancellation: &CaptureCancellation, stream: crate::platform::process::CaptureStream) {
158 let mut wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
159 match stream {
160 crate::platform::process::CaptureStream::Stdout => wakers.stdout = None,
161 crate::platform::process::CaptureStream::Stderr => wakers.stderr = None,
162 }
163}
164
165pub fn cancel_capture_reader(cancellation: &CaptureCancellation) {
166 let wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
167 let byte = [1_u8; 1];
168 for writer in [&wakers.stdout, &wakers.stderr].into_iter().flatten() {
169 let _ = unsafe { libc::write(writer.as_raw_fd(), byte.as_ptr().cast(), byte.len()) };
171 }
172}
173
174fn set_nonblocking(fd: RawFd) -> io::Result<()> {
175 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
177 if flags < 0 { return Err(io::Error::last_os_error()); }
178 if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
180 return Err(io::Error::last_os_error());
181 }
182 Ok(())
183}
184
185#[path = "platform_linux_file_handles.rs"]
186mod file_handles;
187pub use file_handles::read_process_file_handles;
188#[path = "platform_linux_cmdline.rs"]
189mod cmdline;
190pub use cmdline::read_process_cmdline;
191
192#[path = "platform/process_tree.rs"]
193mod process_tree;
194
195pub fn kill_tree(pid: u32, timeout: std::time::Duration) -> io::Result<u32> {
196 process_tree::kill_tree(pid, timeout, |_pid, process| Ok(process.start_time()))
197}
198
199pub fn exit_code(status: std::process::ExitStatus) -> i32 {
200 use std::os::unix::process::ExitStatusExt;
201 status.code().unwrap_or_else(|| -status.signal().unwrap_or(1))
202}
203
204pub fn set_process_name(name: &str) {
205 let truncated: String = name.chars().take(15).collect();
206 let c_name = std::ffi::CString::new(truncated).unwrap_or_default();
207 unsafe { libc::prctl(libc::PR_SET_NAME, c_name.as_ptr() as libc::c_ulong, 0, 0, 0); }
208}
209
210pub fn configure_trampoline_command(_command: &mut std::process::Command) {}
211
212pub fn configure_process_command(
213 command: &mut std::process::Command,
214 config: crate::platform::process::ProcessCommandConfig,
215) -> io::Result<()> {
216 let create_process_group = config.create_process_group;
217 let nice = config.nice;
218 let address_space_limit_bytes = config.address_space_limit_bytes;
219 if !(create_process_group || nice.is_some() || address_space_limit_bytes.is_some()) {
220 return Ok(());
221 }
222 use std::os::unix::process::CommandExt;
223 unsafe {
224 command.pre_exec(move || {
225 if create_process_group && libc::setpgid(0, 0) == -1 {
226 return Err(io::Error::last_os_error());
227 }
228 if let Some(nice) = nice {
229 if libc::setpriority(libc::PRIO_PROCESS, 0, nice) == -1 {
230 return Err(io::Error::last_os_error());
231 }
232 }
233 if let Some(limit) = address_space_limit_bytes {
234 let rlim = libc::rlimit { rlim_cur: limit, rlim_max: limit };
235 if libc::setrlimit(libc::RLIMIT_AS, &rlim) == -1 {
236 return Err(io::Error::last_os_error());
237 }
238 }
239 Ok(())
240 });
241 }
242 Ok(())
243}
244
245pub fn trampoline_exit_code(status: std::process::ExitStatus) -> i32 {
246 use std::os::unix::process::ExitStatusExt;
247 status.signal().map_or_else(|| status.code().unwrap_or(1), |signal| 128 + signal)
248}
249
250pub fn current_executable_build_id() -> Option<Vec<u8>> {
258 unsafe extern "C" fn visit(
259 info: *mut libc::dl_phdr_info,
260 _size: libc::size_t,
261 output: *mut libc::c_void,
262 ) -> libc::c_int {
263 const MAX_NOTE_BYTES: usize = 1024 * 1024;
264
265 let info = unsafe { &*info };
266 let is_main_executable = info.dlpi_name.is_null()
267 || unsafe { std::ffi::CStr::from_ptr(info.dlpi_name) }
268 .to_bytes()
269 .is_empty();
270 if !is_main_executable || info.dlpi_phdr.is_null() || info.dlpi_phnum == 0 {
271 return 0;
272 }
273 let headers = unsafe {
274 std::slice::from_raw_parts(info.dlpi_phdr, usize::from(info.dlpi_phnum))
275 };
276 #[allow(clippy::unnecessary_cast)]
277 let load_bias = info.dlpi_addr as u64;
278 for header in headers {
279 if header.p_type != libc::PT_NOTE {
280 continue;
281 }
282 let Ok(length) = usize::try_from(header.p_memsz) else {
283 continue;
284 };
285 if length == 0 || length > MAX_NOTE_BYTES {
286 continue;
287 }
288 let Some(address) = load_bias.checked_add(header.p_vaddr) else {
289 continue;
290 };
291 let Some(note_end) = address.checked_add(length as u64) else {
292 continue;
293 };
294 let mapped_read_only = headers.iter().any(|load| {
295 if load.p_type != libc::PT_LOAD || load.p_flags & libc::PF_R == 0 {
296 return false;
297 }
298 let Some(start) = load_bias.checked_add(load.p_vaddr) else {
299 return false;
300 };
301 let Some(end) = start.checked_add(load.p_memsz) else {
302 return false;
303 };
304 address >= start && note_end <= end
305 });
306 if address == 0 || !mapped_read_only {
307 continue;
308 }
309 let notes = unsafe { std::slice::from_raw_parts(address as *const u8, length) };
310 if let Some(build_id) = gnu_build_id_from_notes(notes) {
311 let output = unsafe { &mut *output.cast::<Option<Vec<u8>>>() };
312 *output = Some(build_id.to_vec());
313 return 1;
314 }
315 }
316 0
317 }
318
319 let mut output = None;
320 unsafe {
321 libc::dl_iterate_phdr(
322 Some(visit),
323 (&mut output as *mut Option<Vec<u8>>).cast::<libc::c_void>(),
324 );
325 }
326 output
327}
328
329fn gnu_build_id_from_notes(mut notes: &[u8]) -> Option<&[u8]> {
330 fn aligned(value: usize) -> Option<usize> {
331 value.checked_add(3).map(|value| value & !3)
332 }
333
334 while notes.len() >= 12 {
335 let name_len = usize::try_from(u32::from_ne_bytes(notes[0..4].try_into().ok()?)).ok()?;
336 let desc_len = usize::try_from(u32::from_ne_bytes(notes[4..8].try_into().ok()?)).ok()?;
337 let kind = u32::from_ne_bytes(notes[8..12].try_into().ok()?);
338 let name_end = 12usize.checked_add(name_len)?;
339 let desc_start = 12usize.checked_add(aligned(name_len)?)?;
340 let desc_end = desc_start.checked_add(desc_len)?;
341 let next = desc_start.checked_add(aligned(desc_len)?)?;
342 if next > notes.len() || name_end > notes.len() || desc_end > notes.len() {
343 return None;
344 }
345 if kind == 3 && notes.get(12..name_end)?.starts_with(b"GNU") && desc_len > 0 {
346 return notes.get(desc_start..desc_end);
347 }
348 notes = ¬es[next..];
349 }
350 None
351}
352
353pub fn soft_terminate_process_group(pid: u32) -> io::Result<()> {
355 let result = unsafe { libc::kill(-(pid as i32), libc::SIGTERM) };
358 if result != 0 {
359 let error = io::Error::last_os_error();
360 if error.raw_os_error() != Some(libc::ESRCH) {
361 return Err(error);
362 }
363 }
364 Ok(())
365}
366
367pub fn process_snapshot() -> Vec<crate::platform::process::ProcessSnapshot> {
368 Vec::new()
369}
370
371pub fn process_snapshot_for_pid(_pid: u32) -> Option<crate::platform::process::ProcessSnapshot> {
372 None
373}
374
375pub unsafe fn unix_mark_extra_fds_close_on_exec() {
380 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86", target_arch = "arm", target_arch = "riscv64", target_arch = "powerpc64"))]
381 {
382 const SYS_CLOSE_RANGE: libc::c_long = 436;
383 const CLOSE_RANGE_CLOEXEC: libc::c_uint = 4;
384 if libc::syscall(SYS_CLOSE_RANGE, 3u32, libc::c_uint::MAX, CLOSE_RANGE_CLOEXEC) == 0 {
385 return;
386 }
387 }
388 mark_fds_from_directory_or_range();
389}
390
391pub fn configure_sync_daemon_command(command: &mut std::process::Command) -> io::Result<()> {
392 use std::os::unix::process::CommandExt;
393 unsafe {
394 command.pre_exec(|| {
395 let _ = libc::setsid();
396 unix_mark_extra_fds_close_on_exec();
397 Ok(())
398 });
399 }
400 Ok(())
401}
402
403pub fn configure_sync_contained_command(command: &mut std::process::Command) -> io::Result<()> {
404 use std::os::unix::process::CommandExt;
405 unsafe {
406 command.pre_exec(|| {
407 if libc::setpgid(0, 0) == -1 { return Err(io::Error::last_os_error()); }
408 if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
409 return Err(io::Error::last_os_error());
410 }
411 if libc::getppid() == 1 { libc::_exit(1); }
412 unix_mark_extra_fds_close_on_exec();
413 Ok(())
414 });
415 }
416 Ok(())
417}
418
419pub fn parent_has_console() -> bool { false }
420
421pub fn sync_child_native_handle(_child: &std::process::Child) -> usize { 0 }
422
423unsafe fn mark_fds_from_directory_or_range() {
424 let dir = libc::opendir(c"/dev/fd".as_ptr());
425 if !dir.is_null() {
426 let dir_fd = libc::dirfd(dir);
427 loop {
428 let entry = libc::readdir(dir);
429 if entry.is_null() { break; }
430 let mut fd: libc::c_int = 0;
431 let mut cursor = (*entry).d_name.as_ptr();
432 let mut numeric = false;
433 while *cursor != 0 {
434 let byte = *cursor as u8;
435 if !byte.is_ascii_digit() { numeric = false; break; }
436 fd = fd * 10 + (byte - b'0') as libc::c_int;
437 cursor = cursor.add(1);
438 numeric = true;
439 }
440 if numeric && fd > 2 && fd != dir_fd { set_cloexec(fd); }
441 }
442 libc::closedir(dir);
443 return;
444 }
445 let maximum = libc::sysconf(libc::_SC_OPEN_MAX);
446 for fd in 3..if maximum < 0 { 4096 } else { maximum as libc::c_int } { set_cloexec(fd); }
447}
448
449unsafe fn set_cloexec(fd: libc::c_int) {
450 let flags = libc::fcntl(fd, libc::F_GETFD);
451 if flags != -1 { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC); }
452}
453pub fn observer_backend(scope: crate::platform::process::ObserverScope, category: crate::platform::process::ObserverCategory) -> crate::platform::process::ObserverBackend {
454 use crate::platform::process::{ObserverBackend as B, ObserverCategory as C, ObserverScope as S, ObserverSupport as P};
455 match (scope, category) {
456 (S::SystemWide, C::File) => B { support:P::Unavailable, backend:"seccomp-user-notify", reason:"Phase 3: Linux seccomp user-notify file backend not yet implemented" },
457 (S::SystemWide, C::Network) => B { support:P::Unavailable, backend:"ebpf", reason:"Phase 3: Linux eBPF network backend not yet implemented" },
458 (S::SystemWide, C::Process) => B { support:P::Unavailable, backend:"seccomp-user-notify", reason:"Phase 3: Linux seccomp user-notify process backend not yet implemented" },
459 (S::LaunchedProcessTree, C::File) => B { support:P::Partial, backend:"proc-fd-snapshot", reason:"Linux /proc/<pid>/fd/* snapshot via read_process_file_handles (#539 slice 6 follow-up; no streaming file events)" },
460 (S::LaunchedProcessTree, C::Network) => B { support:P::Unavailable, backend:"none", reason:"#539: no-admin per-child network backend deferred to a follow-up issue" },
461 (S::LaunchedProcessTree, C::Process) => B { support:P::Supported, backend:"subreaper-proc-poll", reason:"Linux PR_SET_CHILD_SUBREAPER + /proc descendant polling (#539 slice 5)" },
462 }
463}
464
465pub fn unix_set_priority(pid: u32, nice: i32) -> io::Result<()> {
466 if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid, nice) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
467}
468pub fn unix_signal_process(pid: u32, signal: crate::platform::process::UnixSignalKind) -> io::Result<()> {
469 if unsafe { libc::kill(pid as i32, unix_signal_raw(signal)) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
470}
471pub fn unix_signal_process_group(pid: i32, signal: crate::platform::process::UnixSignalKind) -> io::Result<()> {
472 if unsafe { libc::killpg(pid, unix_signal_raw(signal)) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
473}
474pub fn unix_signal_raw(signal: crate::platform::process::UnixSignalKind) -> i32 {
475 match signal { crate::platform::process::UnixSignalKind::Interrupt => libc::SIGINT, crate::platform::process::UnixSignalKind::Terminate => libc::SIGTERM, crate::platform::process::UnixSignalKind::Kill => libc::SIGKILL }
476}
477
478pub fn configure_compat_tokio_command(
479 command: &mut Command,
480 _show_console: bool,
481 kill_when_owner_dies: bool,
482) -> io::Result<()> {
483 configure_command(command, false, kill_when_owner_dies)
484}
485
486pub fn after_compat_tokio_spawn(_child: &Child, _kill_when_owner_dies: bool) {}
487
488pub(crate) fn configure_command(
489 command: &mut Command,
490 create_process_group: bool,
491 kill_when_owner_dies: bool,
492) -> io::Result<()> {
493 if create_process_group {
494 command.process_group(0);
495 }
496 if kill_when_owner_dies {
497 let owner_pid = unsafe { libc::getpid() };
498 unsafe {
500 command.pre_exec(move || {
501 if libc::prctl(
502 libc::PR_SET_PDEATHSIG,
503 libc::SIGTERM as libc::c_ulong,
504 0,
505 0,
506 0,
507 ) == -1
508 {
509 return Err(io::Error::last_os_error());
510 }
511 if libc::getppid() != owner_pid {
512 libc::kill(libc::getpid(), libc::SIGTERM);
513 }
514 Ok(())
515 });
516 }
517 }
518 Ok(())
519}
520
521pub(crate) fn after_spawn(_child: &Child, _kill_when_owner_dies: bool) {}
522
523pub(crate) fn signal_process(pid: u32) -> io::Result<()> {
524 unix_kill(pid as i32, libc::SIGKILL)
525}
526
527pub(crate) fn signal_process_group(pid: u32) -> io::Result<()> {
528 unix_kill(-(pid as i32), libc::SIGTERM)
529}
530
531fn unix_kill(target: i32, signal: i32) -> io::Result<()> {
532 let result = unsafe { libc::kill(target, signal) };
533 if result == 0 {
534 return Ok(());
535 }
536 let error = io::Error::last_os_error();
537 if error.raw_os_error() == Some(libc::ESRCH) {
538 Ok(())
539 } else {
540 Err(error)
541 }
542}
543
544pub(crate) fn shell_spec(command: &OsStr) -> SpawnSpec {
545 SpawnSpec::new("/bin/sh").arg("-c").arg(command)
546}
547
548#[cfg(test)]
549mod tests {
550 #[test]
551 fn shell_command_preserves_login_shell_contract_and_ignores_child_path() {
552 use std::ffi::OsStr;
553
554 let command_text = "printf '%s' 'alpha beta;\"gamma\"'";
555 let mut command = super::shell_command(command_text);
556 assert_eq!(command.get_program(), OsStr::new("/bin/sh"));
557 assert_eq!(
558 command.get_args().collect::<Vec<_>>(),
559 [OsStr::new("-lc"), OsStr::new(command_text)]
560 );
561 command
562 .env_clear()
563 .env("PATH", "/caller-supplied-path-override");
564 let output = command
565 .output()
566 .expect("absolute shell command should execute independently of child PATH");
567 assert!(output.status.success());
568 assert_eq!(output.stdout, b"alpha beta;\"gamma\"");
569 }
570
571 #[test]
572 #[cfg(not(target_env = "musl"))]
573 fn current_executable_exposes_a_gnu_build_id() {
574 let build_id = super::current_executable_build_id()
575 .expect("Linux test executable should carry a GNU build ID");
576 assert!(!build_id.is_empty());
577 }
578}
579#[cfg(test)]
580#[path = "tests/platform_linux_coverage.rs"]
581mod coverage_tests;
582#[path = "sync_spawn_group.rs"]
583mod sync_spawn;
584pub use sync_spawn::{spawn_sync, spawn_sync_daemon};