Skip to main content

running_process_platform_internal/
platform_linux.rs

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