Skip to main content

running_process_platform_internal/
platform_linux.rs

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