Skip to main content

running_process_platform_internal/
platform_linux.rs

1//! Linux implementation root for the process capability.
2
3#[path = "platform_linux/autostart.rs"]
4pub(crate) mod autostart;
5
6#[path = "platform_linux/resources.rs"]
7pub(crate) mod resources;
8pub use resources::{
9    fd_exhaustion_error as resources_fd_exhaustion_error,
10    inode_capacity as resources_inode_capacity,
11    signals_fd_exhaustion as resources_signals_fd_exhaustion,
12    signals_storage_exhaustion as resources_signals_storage_exhaustion,
13    storage_exhaustion_error as resources_storage_exhaustion_error,
14};
15
16pub use autostart::{
17    register as autostart_register,
18    render_registration as autostart_render_registration,
19    unregister as autostart_unregister,
20};
21
22#[path = "platform_linux/process_inspect.rs"]
23pub(crate) mod process_inspect;
24pub use process_inspect::{
25    process_executable_path, process_force_kill, process_same_executable_path,
26    process_signal_terminate, ProcessLiveness,
27};
28
29#[path = "platform_linux/raw_write.rs"]
30pub(crate) mod raw_write;
31pub use raw_write::write_all_to_descriptor as fs_write_all_to_descriptor;
32
33#[path = "platform_linux/shutdown_request.rs"]
34pub(crate) mod shutdown_request;
35pub use shutdown_request::install_shutdown_request_handler as process_install_shutdown_request_handler;
36
37#[path = "platform_linux/process_owner_death.rs"]
38pub(crate) mod process_owner_death;
39pub use process_owner_death::{
40    install_owner_death_cleanup as process_install_owner_death_cleanup,
41    owner_death_cleanup_target as process_owner_death_cleanup_target,
42};
43
44#[path = "platform_linux/host.rs"]
45pub(crate) mod host;
46pub use host::{
47    boot_id as host_boot_id, current_process_privilege as host_current_process_privilege,
48    environment_keys_are_case_insensitive as host_environment_keys_are_case_insensitive,
49    filesystem_device_id as host_filesystem_device_id, hostname as host_hostname,
50    login_environment as host_login_environment, machine_id as host_machine_id,
51    namespace_id as host_namespace_id, user_machine_identity as host_user_machine_identity,
52    PrivilegedIdentity as HostPrivilegedIdentity,
53};
54pub use host::login_environment_block as host_login_environment_block;
55
56#[cfg(feature = "fs")]
57#[path = "platform_linux/fs.rs"]
58pub(crate) mod fs;
59#[cfg(feature = "fs")]
60pub use fs::{
61    create_private_file as fs_create_private_file,
62    decode_path_bytes as fs_decode_path_bytes,
63    replace_file as fs_replace_file, sync_directory as fs_sync_directory,
64    user_config_dir as fs_user_config_dir,
65    user_data_dir as fs_user_data_dir, encode_path_bytes as fs_encode_path_bytes,
66    file_identity as fs_file_identity, is_lock_conflict as fs_is_lock_conflict,
67    open_lock_file as fs_open_lock_file, path_identity as fs_path_identity,
68    try_lock_exclusive as fs_try_lock_exclusive, unlock as fs_unlock,
69    user_run_data_root as fs_user_run_data_root, user_runtime_dir as fs_user_runtime_dir,
70    user_state_dir as fs_user_state_dir, FileIdentity as FsFileIdentity,
71};
72
73#[path = "platform_linux/executable.rs"]
74pub(crate) mod executable;
75pub use executable::{
76    file_name as executable_file_name,
77    sibling_of_current_image as executable_sibling_of_current_image,
78    EXECUTABLE_EXTENSION,
79};
80
81#[cfg(feature = "ipc")]
82#[path = "platform_linux/ipc.rs"]
83pub(crate) mod ipc;
84#[cfg(feature = "ipc")]
85#[path = "platform_linux/ipc_private_dir.rs"]
86mod ipc_private_dir;
87#[cfg(feature = "ipc")]
88pub use ipc::{
89    current_user_id as ipc_current_user_id, Endpoint as IpcEndpoint,
90    endpoint_is_filesystem_backed as ipc_endpoint_is_filesystem_backed,
91    nonblocking_zero_read_is_pending as ipc_nonblocking_zero_read_is_pending,
92    select_endpoint_address as ipc_select_endpoint_address,
93    InheritedListener as IpcInheritedListener, Listener as IpcListener,
94    ListenerNonblockingMode as IpcListenerNonblockingMode, PeerIdentity as IpcPeerIdentity,
95    PeerIdentitySource as IpcPeerIdentitySource, Stream as IpcStream,
96};
97#[cfg(feature = "ipc")]
98pub const LEGACY_SCM_RIGHTS_TRANSPORT_SUPPORTED: bool = true;
99#[cfg(feature = "ipc")]
100pub const LEGACY_DUPLICATE_HANDLE_TRANSPORT_SUPPORTED: bool = false;
101#[cfg(feature = "ipc")]
102pub use ipc::{legacy_send_fd_over, legacy_send_fd_to};
103#[cfg(feature = "ipc")]
104pub fn legacy_duplicate_handle(
105    _source_handle: usize,
106    _backend_pid: u32,
107) -> Result<usize, crate::LegacyHandoffError> {
108    Err(crate::LegacyHandoffError::new(
109        crate::platform::ipc::HandoffTransferErrorKind::Unsupported,
110        None,
111    ))
112}
113#[cfg(feature = "ipc")]
114pub use ipc_private_dir::{
115    ensure_owner_private_directory as ipc_ensure_owner_private_directory,
116    owner_private_directory as ipc_owner_private_directory,
117};
118#[cfg(feature = "ipc")]
119pub fn ipc_broker_endpoint_name(bare_name: &str, path_scoped: bool) -> std::io::Result<String> {
120    use std::fmt::Write as _;
121    use std::path::PathBuf;
122
123    if path_scoped {
124        let mut hash = blake3::Hasher::new();
125        hash.update(b"running-process:path-scoped-socket:v1\0");
126        hash.update(bare_name.as_bytes());
127        let mut leaf = String::with_capacity(32);
128        for byte in hash.finalize().as_bytes().iter().take(16) { let _ = write!(leaf, "{byte:02x}"); }
129        return Ok(PathBuf::from("/tmp").join(format!(".rp-path-{leaf}.sock")).to_string_lossy().into_owned());
130    }
131    let directory = match std::env::var_os("XDG_RUNTIME_DIR") {
132        Some(value) => PathBuf::from(value).join("running-process").join("broker-v2"),
133        None => PathBuf::from(format!("/tmp/running-process-{}/broker-v2", unsafe { libc::getuid() })),
134    };
135    Ok(directory.join(format!("{bare_name}.sock")).to_string_lossy().into_owned())
136}
137
138/// Linux `sun_path` is 108 bytes including the NUL terminator.
139const LINUX_SUN_PATH_MAX: usize = 108;
140
141#[cfg(feature = "ipc")]
142pub fn ipc_endpoint_name_limit() -> crate::platform::ipc::EndpointNameLimit {
143    crate::platform::ipc::EndpointNameLimit {
144        max_bytes: LINUX_SUN_PATH_MAX,
145        label: "Linux sun_path",
146    }
147}
148
149/// Directory holding v1 broker sockets.
150///
151/// Deliberately performs no filesystem writes: name derivation stays pure so
152/// the hash and length-limit tests remain deterministic. Callers that bind
153/// create the parent directory themselves.
154#[cfg(feature = "ipc")]
155fn broker_v1_socket_dir() -> std::path::PathBuf {
156    use std::path::PathBuf;
157
158    match std::env::var_os("XDG_RUNTIME_DIR") {
159        Some(dir) => PathBuf::from(dir).join("running-process").join("broker"),
160        None => PathBuf::from(format!(
161            "/tmp/running-process-{}/broker",
162            unsafe { libc::getuid() }
163        )),
164    }
165}
166
167#[cfg(feature = "ipc")]
168pub fn ipc_broker_v1_endpoint_path(
169    bare_name: &str,
170) -> Result<String, crate::platform::ipc::EndpointNameTooLong> {
171    // Linux gets 108 bytes and a guaranteed $XDG_RUNTIME_DIR (or a short
172    // /tmp fallback), so the full canonical name survives for debuggability.
173    let candidate = broker_v1_socket_dir().join(format!("{bare_name}.sock"));
174    let candidate = candidate.to_string_lossy();
175    // sockaddr_un is NUL-terminated, so the path itself must be strictly
176    // shorter than the field width.
177    if candidate.len() >= LINUX_SUN_PATH_MAX {
178        return Err(crate::platform::ipc::EndpointNameTooLong {
179            len: candidate.len(),
180            max: LINUX_SUN_PATH_MAX - 1,
181            limit_label: "Linux sun_path",
182        });
183    }
184    Ok(candidate.into_owned())
185}
186
187#[cfg(feature = "ipc")]
188pub fn ipc_endpoint_scope_bytes(path: &std::path::Path) -> Vec<u8> {
189    // Linux paths are opaque byte strings; no spelling difference is
190    // meaningless, so the bytes are hashed exactly as the OS reports them.
191    use std::os::unix::ffi::OsStrExt as _;
192
193    path.as_os_str().as_bytes().to_vec()
194}
195
196#[cfg(feature = "ipc")]
197pub fn ipc_broker_v2_runtime_dir() -> std::path::PathBuf {
198    match std::env::var_os("XDG_RUNTIME_DIR") {
199        Some(dir) => std::path::PathBuf::from(dir)
200            .join("running-process")
201            .join("broker-v2"),
202        None => crate::platform::ipc::per_user_runtime_fallback(),
203    }
204}
205#[cfg(feature = "ipc")]
206pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
207    stream.0
208}
209
210#[cfg(feature = "ipc")]
211pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
212    ipc::Stream(stream)
213}
214#[cfg(feature = "ipc")]
215pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
216    ipc::legacy_name(path)
217}
218#[cfg(feature = "ipc-async")]
219pub use ipc::{
220    AsyncListener as IpcAsyncListener, AsyncStream as IpcAsyncStream,
221    IntoAsyncListener as IpcIntoAsyncListener, IntoAsyncStream as IpcIntoAsyncStream,
222};
223
224#[cfg(feature = "session-relay")]
225#[path = "platform_linux_session_relay.rs"]
226mod session_relay;
227#[cfg(feature = "session-relay")]
228pub use session_relay::relay_local_socket_session;
229
230#[path = "platform_linux/terminal.rs"]
231pub mod terminal;
232pub use terminal::active_graphics_probe;
233pub use crate::platform::terminal_input;
234
235#[path = "platform_linux/window_icon.rs"]
236mod window_icon;
237pub use window_icon::{icon_support as window_icon_support_impl, set_icon as set_window_icon_impl};
238
239pub fn shell_command(command: &str) -> std::process::Command {
240    let mut shell = std::process::Command::new("/bin/sh");
241    shell.arg("-lc").arg(command);
242    shell
243}
244
245pub fn compat_shell_command(command: &str) -> std::process::Command {
246    let mut shell = std::process::Command::new("/bin/sh");
247    shell.arg("-lc").arg(command);
248    shell
249}
250
251pub fn canonical_environment_pairs(pairs: Vec<(String, String)>) -> Vec<(String, String)> {
252    pairs
253}
254
255pub fn monitor_console_windows(
256    _duration: std::time::Duration,
257) -> Vec<crate::platform::process::ConsoleWindowInfo> {
258    Vec::new()
259}
260
261#[cfg(feature = "async-process")]
262use std::ffi::OsStr;
263use std::io;
264use std::io::Read;
265use std::os::fd::{AsRawFd, RawFd};
266use std::os::unix::net::UnixStream;
267use std::sync::Mutex;
268
269#[cfg(feature = "async-process")]
270use tokio::process::{Child, Command};
271
272#[cfg(feature = "async-process")]
273use crate::SpawnSpec;
274
275#[path = "platform_linux_descendants.rs"]
276mod descendants;
277pub use descendants::start_descendant_monitor;
278
279#[path = "platform_linux_trace.rs"]
280mod exact_trace;
281pub use exact_trace::{configure_exact_trace, start_exact_trace, TracedChild};
282
283pub fn exact_trace_capability() -> crate::platform::process::ExactTraceCapability {
284    crate::platform::process::ExactTraceCapability {
285        available: true,
286        backend: "linux-ptrace",
287        reason: "launch-time PTRACE_TRACEME with follow-fork/clone/exec/exit supervision",
288        non_invasive_backend: "proc-descendant-snapshot",
289        non_invasive_grade:
290            crate::platform::process::NonInvasiveObservationGrade::SnapshotInferred,
291    }
292}
293
294pub struct WindowsJobHandle;
295
296pub fn assign_child_to_windows_job(
297    _child: &std::process::Child,
298    _direct_pid: u32,
299    _address_space_limit_bytes: Option<u64>,
300    _emit: Option<Box<dyn Fn(crate::platform::process::DescendantEvent) + Send>>,
301) -> io::Result<WindowsJobHandle> {
302    Err(io::Error::new(
303        io::ErrorKind::Unsupported,
304        "Windows Job Objects are unavailable on Linux",
305    ))
306}
307
308#[derive(Default)]
309pub struct CaptureCancellation {
310    wakers: Mutex<CaptureWakers>,
311}
312
313#[derive(Default)]
314struct CaptureWakers {
315    stdout: Option<UnixStream>,
316    stderr: Option<UnixStream>,
317}
318
319struct CancelableCaptureReader<R> {
320    reader: R,
321    wake_reader: UnixStream,
322}
323
324impl<R: Read + AsRawFd> Read for CancelableCaptureReader<R> {
325    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
326        if buf.is_empty() { return Ok(0); }
327        loop {
328            let mut poll_fds = [
329                libc::pollfd { fd: self.reader.as_raw_fd(), events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, revents: 0 },
330                libc::pollfd { fd: self.wake_reader.as_raw_fd(), events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, revents: 0 },
331            ];
332            // SAFETY: both descriptors remain owned by this reader for the call.
333            let polled = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as _, -1) };
334            if polled < 0 {
335                let error = io::Error::last_os_error();
336                if error.kind() == io::ErrorKind::Interrupted { continue; }
337                return Err(error);
338            }
339            if poll_fds[1].revents != 0 {
340                return Err(io::Error::new(io::ErrorKind::Interrupted, "capture reader cancelled"));
341            }
342            if poll_fds[0].revents != 0 {
343                match self.reader.read(buf) {
344                    Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
345                    result => return result,
346                }
347            }
348        }
349    }
350}
351
352pub fn prepare_capture_reader<R>(
353    reader: R,
354    cancellation: &CaptureCancellation,
355    stream: crate::platform::process::CaptureStream,
356) -> io::Result<Box<dyn Read + Send>>
357where R: Read + AsRawFd + Send + 'static {
358    set_nonblocking(reader.as_raw_fd())?;
359    let (wake_reader, wake_writer) = UnixStream::pair()?;
360    wake_writer.set_nonblocking(true)?;
361    let mut wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
362    match stream {
363        crate::platform::process::CaptureStream::Stdout => wakers.stdout = Some(wake_writer),
364        crate::platform::process::CaptureStream::Stderr => wakers.stderr = Some(wake_writer),
365    }
366    Ok(Box::new(CancelableCaptureReader { reader, wake_reader }))
367}
368
369pub fn capture_reader_done(cancellation: &CaptureCancellation, stream: crate::platform::process::CaptureStream) {
370    let mut wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
371    match stream {
372        crate::platform::process::CaptureStream::Stdout => wakers.stdout = None,
373        crate::platform::process::CaptureStream::Stderr => wakers.stderr = None,
374    }
375}
376
377pub fn cancel_capture_reader(cancellation: &CaptureCancellation) {
378    let wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
379    let byte = [1_u8; 1];
380    for writer in [&wakers.stdout, &wakers.stderr].into_iter().flatten() {
381        // SAFETY: the stored wake socket stays alive while the mutex is held.
382        let _ = unsafe { libc::write(writer.as_raw_fd(), byte.as_ptr().cast(), byte.len()) };
383    }
384}
385
386fn set_nonblocking(fd: RawFd) -> io::Result<()> {
387    // SAFETY: `fd` is borrowed from a live reader for both calls.
388    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
389    if flags < 0 { return Err(io::Error::last_os_error()); }
390    // SAFETY: `fd` is borrowed from a live reader for both calls.
391    if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
392        return Err(io::Error::last_os_error());
393    }
394    Ok(())
395}
396
397#[path = "platform_linux_file_handles.rs"]
398mod file_handles;
399pub use file_handles::read_process_file_handles;
400#[path = "platform_linux_cmdline.rs"]
401mod cmdline;
402pub use cmdline::read_process_cmdline;
403
404#[cfg(feature = "process-inspection")]
405#[path = "platform/process_tree.rs"]
406mod process_tree;
407
408#[cfg(feature = "process-inspection")]
409pub fn kill_tree(pid: u32, timeout: std::time::Duration) -> io::Result<u32> {
410    process_tree::kill_tree(pid, timeout, |_pid, process| Ok(process.start_time()))
411}
412
413pub fn exit_code(status: std::process::ExitStatus) -> i32 {
414    use std::os::unix::process::ExitStatusExt;
415    status.code().unwrap_or_else(|| -status.signal().unwrap_or(1))
416}
417
418pub fn set_process_name(name: &str) {
419    let truncated: String = name.chars().take(15).collect();
420    let c_name = std::ffi::CString::new(truncated).unwrap_or_default();
421    unsafe { libc::prctl(libc::PR_SET_NAME, c_name.as_ptr() as libc::c_ulong, 0, 0, 0); }
422}
423
424pub fn configure_trampoline_command(_command: &mut std::process::Command) {}
425
426pub fn configure_process_command(
427    command: &mut std::process::Command,
428    config: crate::platform::process::ProcessCommandConfig,
429) -> io::Result<()> {
430    let create_process_group = config.create_process_group;
431    let nice = config.nice;
432    let address_space_limit_bytes = config.address_space_limit_bytes;
433    if !(create_process_group || nice.is_some() || address_space_limit_bytes.is_some()) {
434        return Ok(());
435    }
436    use std::os::unix::process::CommandExt;
437    unsafe {
438        command.pre_exec(move || {
439            if create_process_group && libc::setpgid(0, 0) == -1 {
440                return Err(io::Error::last_os_error());
441            }
442            if let Some(nice) = nice {
443                if libc::setpriority(libc::PRIO_PROCESS, 0, nice) == -1 {
444                    return Err(io::Error::last_os_error());
445                }
446            }
447            if let Some(limit) = address_space_limit_bytes {
448                let rlim = libc::rlimit { rlim_cur: limit, rlim_max: limit };
449                if libc::setrlimit(libc::RLIMIT_AS, &rlim) == -1 {
450                    return Err(io::Error::last_os_error());
451                }
452            }
453            Ok(())
454        });
455    }
456    Ok(())
457}
458
459pub fn trampoline_exit_code(status: std::process::ExitStatus) -> i32 {
460    use std::os::unix::process::ExitStatusExt;
461    status.signal().map_or_else(|| status.code().unwrap_or(1), |signal| 128 + signal)
462}
463
464/// Return the GNU build ID of the running executable without reading the
465/// executable from disk.
466///
467/// The dynamic loader has already mapped the main image's `PT_NOTE` segment,
468/// so callers that only need an image-generation identity do not need to hash
469/// a potentially large unoptimized binary. `None` preserves a clean fallback
470/// for binaries linked without a GNU build ID.
471pub fn current_executable_build_id() -> Option<Vec<u8>> {
472    unsafe extern "C" fn visit(
473        info: *mut libc::dl_phdr_info,
474        _size: libc::size_t,
475        output: *mut libc::c_void,
476    ) -> libc::c_int {
477        const MAX_NOTE_BYTES: usize = 1024 * 1024;
478
479        let info = unsafe { &*info };
480        let is_main_executable = info.dlpi_name.is_null()
481            || unsafe { std::ffi::CStr::from_ptr(info.dlpi_name) }
482                .to_bytes()
483                .is_empty();
484        if !is_main_executable || info.dlpi_phdr.is_null() || info.dlpi_phnum == 0 {
485            return 0;
486        }
487        let headers = unsafe {
488            std::slice::from_raw_parts(info.dlpi_phdr, usize::from(info.dlpi_phnum))
489        };
490        #[allow(clippy::unnecessary_cast)]
491        let load_bias = info.dlpi_addr as u64;
492        for header in headers {
493            if header.p_type != libc::PT_NOTE {
494                continue;
495            }
496            let Ok(length) = usize::try_from(header.p_memsz) else {
497                continue;
498            };
499            if length == 0 || length > MAX_NOTE_BYTES {
500                continue;
501            }
502            let Some(address) = load_bias.checked_add(header.p_vaddr) else {
503                continue;
504            };
505            let Some(note_end) = address.checked_add(length as u64) else {
506                continue;
507            };
508            let mapped_read_only = headers.iter().any(|load| {
509                if load.p_type != libc::PT_LOAD || load.p_flags & libc::PF_R == 0 {
510                    return false;
511                }
512                let Some(start) = load_bias.checked_add(load.p_vaddr) else {
513                    return false;
514                };
515                let Some(end) = start.checked_add(load.p_memsz) else {
516                    return false;
517                };
518                address >= start && note_end <= end
519            });
520            if address == 0 || !mapped_read_only {
521                continue;
522            }
523            let notes = unsafe { std::slice::from_raw_parts(address as *const u8, length) };
524            if let Some(build_id) = gnu_build_id_from_notes(notes) {
525                let output = unsafe { &mut *output.cast::<Option<Vec<u8>>>() };
526                *output = Some(build_id.to_vec());
527                return 1;
528            }
529        }
530        0
531    }
532
533    let mut output = None;
534    unsafe {
535        libc::dl_iterate_phdr(
536            Some(visit),
537            (&mut output as *mut Option<Vec<u8>>).cast::<libc::c_void>(),
538        );
539    }
540    output
541}
542
543fn gnu_build_id_from_notes(mut notes: &[u8]) -> Option<&[u8]> {
544    fn aligned(value: usize) -> Option<usize> {
545        value.checked_add(3).map(|value| value & !3)
546    }
547
548    while notes.len() >= 12 {
549        let name_len = usize::try_from(u32::from_ne_bytes(notes[0..4].try_into().ok()?)).ok()?;
550        let desc_len = usize::try_from(u32::from_ne_bytes(notes[4..8].try_into().ok()?)).ok()?;
551        let kind = u32::from_ne_bytes(notes[8..12].try_into().ok()?);
552        let name_end = 12usize.checked_add(name_len)?;
553        let desc_start = 12usize.checked_add(aligned(name_len)?)?;
554        let desc_end = desc_start.checked_add(desc_len)?;
555        let next = desc_start.checked_add(aligned(desc_len)?)?;
556        if next > notes.len() || name_end > notes.len() || desc_end > notes.len() {
557            return None;
558        }
559        if kind == 3 && notes.get(12..name_end)?.starts_with(b"GNU") && desc_len > 0 {
560            return notes.get(desc_start..desc_end);
561        }
562        notes = &notes[next..];
563    }
564    None
565}
566
567/// Request a graceful shutdown for a child-owned POSIX process group.
568pub fn soft_terminate_process_group(pid: u32) -> io::Result<()> {
569    // SAFETY: `kill` receives only the numeric child-owned group id; no Rust
570    // references or borrowed state cross the OS boundary.
571    let result = unsafe { libc::kill(-(pid as i32), libc::SIGTERM) };
572    if result != 0 {
573        let error = io::Error::last_os_error();
574        if error.raw_os_error() != Some(libc::ESRCH) {
575            return Err(error);
576        }
577    }
578    Ok(())
579}
580
581pub fn process_snapshot() -> Vec<crate::platform::process::ProcessSnapshot> {
582    Vec::new()
583}
584
585pub fn process_snapshot_for_pid(_pid: u32) -> Option<crate::platform::process::ProcessSnapshot> {
586    None
587}
588
589/// Mark inherited descriptors close-on-exec without breaking std's exec-error pipe.
590///
591/// # Safety
592/// This must only be called from a post-fork `pre_exec` closure.
593pub unsafe fn unix_mark_extra_fds_close_on_exec() {
594    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86", target_arch = "arm", target_arch = "riscv64", target_arch = "powerpc64"))]
595    {
596        const SYS_CLOSE_RANGE: libc::c_long = 436;
597        const CLOSE_RANGE_CLOEXEC: libc::c_uint = 4;
598        if libc::syscall(SYS_CLOSE_RANGE, 3u32, libc::c_uint::MAX, CLOSE_RANGE_CLOEXEC) == 0 {
599            return;
600        }
601    }
602    mark_fds_from_directory_or_range();
603}
604
605pub fn configure_sync_daemon_command(command: &mut std::process::Command) -> io::Result<()> {
606    use std::os::unix::process::CommandExt;
607    unsafe {
608        command.pre_exec(|| {
609            let _ = libc::setsid();
610            unix_mark_extra_fds_close_on_exec();
611            Ok(())
612        });
613    }
614    Ok(())
615}
616
617pub fn configure_sync_contained_command(command: &mut std::process::Command) -> io::Result<()> {
618    use std::os::unix::process::CommandExt;
619    unsafe {
620        command.pre_exec(|| {
621            if libc::setpgid(0, 0) == -1 { return Err(io::Error::last_os_error()); }
622            if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
623                return Err(io::Error::last_os_error());
624            }
625            if libc::getppid() == 1 { libc::_exit(1); }
626            unix_mark_extra_fds_close_on_exec();
627            Ok(())
628        });
629    }
630    Ok(())
631}
632
633pub fn parent_has_console() -> bool { false }
634
635pub fn sync_child_native_handle(_child: &std::process::Child) -> usize { 0 }
636
637unsafe fn mark_fds_from_directory_or_range() {
638    let dir = libc::opendir(c"/dev/fd".as_ptr());
639    if !dir.is_null() {
640        let dir_fd = libc::dirfd(dir);
641        loop {
642            let entry = libc::readdir(dir);
643            if entry.is_null() { break; }
644            let mut fd: libc::c_int = 0;
645            let mut cursor = (*entry).d_name.as_ptr();
646            let mut numeric = false;
647            while *cursor != 0 {
648                let byte = *cursor as u8;
649                if !byte.is_ascii_digit() { numeric = false; break; }
650                fd = fd * 10 + (byte - b'0') as libc::c_int;
651                cursor = cursor.add(1);
652                numeric = true;
653            }
654            if numeric && fd > 2 && fd != dir_fd { set_cloexec(fd); }
655        }
656        libc::closedir(dir);
657        return;
658    }
659    let maximum = libc::sysconf(libc::_SC_OPEN_MAX);
660    for fd in 3..if maximum < 0 { 4096 } else { maximum as libc::c_int } { set_cloexec(fd); }
661}
662
663unsafe fn set_cloexec(fd: libc::c_int) {
664    let flags = libc::fcntl(fd, libc::F_GETFD);
665    if flags != -1 { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC); }
666}
667pub fn observer_backend(scope: crate::platform::process::ObserverScope, category: crate::platform::process::ObserverCategory) -> crate::platform::process::ObserverBackend {
668    use crate::platform::process::{ObserverBackend as B, ObserverCategory as C, ObserverScope as S, ObserverSupport as P};
669    match (scope, category) {
670        (S::SystemWide, C::File) => B { support:P::Unavailable, backend:"seccomp-user-notify", reason:"Phase 3: Linux seccomp user-notify file backend not yet implemented" },
671        (S::SystemWide, C::Network) => B { support:P::Unavailable, backend:"ebpf", reason:"Phase 3: Linux eBPF network backend not yet implemented" },
672        (S::SystemWide, C::Process) => B { support:P::Unavailable, backend:"seccomp-user-notify", reason:"Phase 3: Linux seccomp user-notify process backend not yet implemented" },
673        (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)" },
674        (S::LaunchedProcessTree, C::Network) => B { support:P::Unavailable, backend:"none", reason:"#539: no-admin per-child network backend deferred to a follow-up issue" },
675        (S::LaunchedProcessTree, C::Process) => B { support:P::Supported, backend:"subreaper-proc-poll", reason:"Linux PR_SET_CHILD_SUBREAPER + /proc descendant polling (#539 slice 5)" },
676    }
677}
678
679pub fn unix_set_priority(pid: u32, nice: i32) -> io::Result<()> {
680    if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid, nice) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
681}
682pub fn unix_signal_process(pid: u32, signal: crate::platform::process::UnixSignalKind) -> io::Result<()> {
683    if unsafe { libc::kill(pid as i32, unix_signal_raw(signal)) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
684}
685pub fn unix_signal_process_group(pid: i32, signal: crate::platform::process::UnixSignalKind) -> io::Result<()> {
686    if unsafe { libc::killpg(pid, unix_signal_raw(signal)) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
687}
688pub fn unix_signal_raw(signal: crate::platform::process::UnixSignalKind) -> i32 {
689    match signal { crate::platform::process::UnixSignalKind::Interrupt => libc::SIGINT, crate::platform::process::UnixSignalKind::Terminate => libc::SIGTERM, crate::platform::process::UnixSignalKind::Kill => libc::SIGKILL }
690}
691
692#[cfg(feature = "async-process")]
693pub fn configure_compat_tokio_command(
694    command: &mut Command,
695    _show_console: bool,
696    kill_when_owner_dies: bool,
697) -> io::Result<()> {
698    configure_command(command, false, kill_when_owner_dies)
699}
700
701/// Nothing to do on this host: the parent-death signal is installed in `pre_exec`, before the
702/// child ever runs, so nothing remains to do once it has.
703#[cfg(feature = "async-process")]
704pub fn after_compat_tokio_spawn(
705    _child: &Child,
706    _kill_when_owner_dies: bool,
707) -> io::Result<()> {
708    Ok(())
709}
710
711#[cfg(feature = "async-process")]
712pub(crate) fn configure_command(
713    command: &mut Command,
714    create_process_group: bool,
715    kill_when_owner_dies: bool,
716) -> io::Result<()> {
717    if create_process_group {
718        command.process_group(0);
719    }
720    if kill_when_owner_dies {
721        let owner_pid = unsafe { libc::getpid() };
722        // SAFETY: the closure invokes only async-signal-safe libc calls.
723        unsafe {
724            command.pre_exec(move || {
725                if libc::prctl(
726                    libc::PR_SET_PDEATHSIG,
727                    libc::SIGTERM as libc::c_ulong,
728                    0,
729                    0,
730                    0,
731                ) == -1
732                {
733                    return Err(io::Error::last_os_error());
734                }
735                if libc::getppid() != owner_pid {
736                    libc::kill(libc::getpid(), libc::SIGTERM);
737                }
738                Ok(())
739            });
740        }
741    }
742    Ok(())
743}
744
745#[cfg(feature = "async-process")]
746pub(crate) fn after_spawn(_child: &Child, _kill_when_owner_dies: bool) -> io::Result<()> {
747    Ok(())
748}
749
750pub(crate) fn signal_process(pid: u32) -> io::Result<()> {
751    unix_kill(pid as i32, libc::SIGKILL)
752}
753
754pub(crate) fn signal_process_group(pid: u32) -> io::Result<()> {
755    unix_kill(-(pid as i32), libc::SIGTERM)
756}
757
758fn unix_kill(target: i32, signal: i32) -> io::Result<()> {
759    let result = unsafe { libc::kill(target, signal) };
760    if result == 0 {
761        return Ok(());
762    }
763    let error = io::Error::last_os_error();
764    if error.raw_os_error() == Some(libc::ESRCH) {
765        Ok(())
766    } else {
767        Err(error)
768    }
769}
770
771#[cfg(feature = "async-process")]
772pub(crate) fn shell_spec(command: &OsStr) -> SpawnSpec {
773    SpawnSpec::new("/bin/sh").arg("-c").arg(command)
774}
775
776#[cfg(test)]
777mod tests {
778    #[test]
779    fn shell_command_preserves_login_shell_contract_and_ignores_child_path() {
780        use std::ffi::OsStr;
781
782        let command_text = "printf '%s' 'alpha beta;\"gamma\"'";
783        let mut command = super::shell_command(command_text);
784        assert_eq!(command.get_program(), OsStr::new("/bin/sh"));
785        assert_eq!(
786            command.get_args().collect::<Vec<_>>(),
787            [OsStr::new("-lc"), OsStr::new(command_text)]
788        );
789        command
790            .env_clear()
791            .env("PATH", "/caller-supplied-path-override");
792        let output = command
793            .output()
794            .expect("absolute shell command should execute independently of child PATH");
795        assert!(output.status.success());
796        assert_eq!(output.stdout, b"alpha beta;\"gamma\"");
797    }
798
799    #[test]
800    #[cfg(not(target_env = "musl"))]
801    fn current_executable_exposes_a_gnu_build_id() {
802        let build_id = super::current_executable_build_id()
803            .expect("Linux test executable should carry a GNU build ID");
804        assert!(!build_id.is_empty());
805    }
806}
807#[cfg(test)]
808#[path = "tests/platform_linux_coverage.rs"]
809mod coverage_tests;
810#[path = "sync_spawn_group.rs"]
811mod sync_spawn;
812pub use sync_spawn::{spawn_sync, spawn_sync_daemon};
813
814#[cfg(all(test, feature = "ipc"))]
815mod endpoint_naming_tests {
816    use super::{ipc_broker_v1_endpoint_path, ipc_endpoint_name_limit, LINUX_SUN_PATH_MAX};
817
818    #[test]
819    fn the_v1_address_keeps_the_full_name_for_debuggability() {
820        let address = ipc_broker_v1_endpoint_path("rpb-v1-abc-shared").expect("derive address");
821        assert!(address.contains("rpb-v1-abc-shared"));
822        assert!(address.ends_with("-shared.sock"));
823        assert!(address.contains("/broker/"));
824    }
825
826    #[test]
827    fn an_over_long_name_is_refused_against_sun_path() {
828        let err = ipc_broker_v1_endpoint_path(&"a".repeat(LINUX_SUN_PATH_MAX))
829            .expect_err("must exceed sun_path");
830        assert_eq!(err.max, LINUX_SUN_PATH_MAX - 1);
831        assert_eq!(err.limit_label, "Linux sun_path");
832    }
833
834    #[test]
835    fn an_accepted_address_is_strictly_shorter_than_the_field() {
836        // sockaddr_un is NUL-terminated, so equality with the field width
837        // would truncate the terminator.
838        let address = ipc_broker_v1_endpoint_path("rpb-v1-abc-shared").expect("derive address");
839        assert!(address.len() < LINUX_SUN_PATH_MAX);
840    }
841
842    #[test]
843    fn the_reported_budget_is_sun_path() {
844        let limit = ipc_endpoint_name_limit();
845        assert_eq!(limit.max_bytes, LINUX_SUN_PATH_MAX);
846        assert_eq!(limit.label, "Linux sun_path");
847    }
848
849    #[test]
850    fn the_scope_spelling_is_the_verbatim_path_bytes() {
851        // Paths are opaque byte strings here: no spelling difference is
852        // meaningless, and case is significant. This pins the spelling --
853        // changing it re-scopes every deployed broker, and the stability
854        // tests upstream would not notice.
855        use super::ipc_endpoint_scope_bytes;
856
857        let bytes = ipc_endpoint_scope_bytes(std::path::Path::new("/usr/local/bin/Broker"));
858        assert_eq!(bytes, b"/usr/local/bin/Broker".to_vec());
859
860        let lowered = ipc_endpoint_scope_bytes(std::path::Path::new("/usr/local/bin/broker"));
861        assert_ne!(bytes, lowered, "case must remain significant");
862    }
863
864}
865
866/// Replace this process's image with `command`.
867///
868/// Returns only on failure: on success `execve` has already replaced the
869/// program and there is nothing left to return to. That is why the signature
870/// yields `io::Error` rather than `io::Result<()>` -- an `Ok` would name a
871/// state that cannot be observed.
872pub fn process_replace_current_image(command: &mut std::process::Command) -> std::io::Error {
873    use std::os::unix::process::CommandExt as _;
874    command.exec()
875}
876
877/// This host replaces a running image in place; see the facade for what that
878/// means for a caller that cannot accept a successor instead.
879pub const fn process_can_replace_current_image() -> bool {
880    true
881}