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