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 = "private-dir")]
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 = "private-dir")]
114pub use ipc_private_dir::{
115    ensure_owner_private_directory as private_dir_ensure_owner_private_directory,
116    owner_private_directory as private_dir_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.
139#[cfg(feature = "ipc")]
140const LINUX_SUN_PATH_MAX: usize = 108;
141
142#[cfg(feature = "ipc")]
143pub fn ipc_endpoint_name_limit() -> crate::platform::ipc::EndpointNameLimit {
144    crate::platform::ipc::EndpointNameLimit {
145        max_bytes: LINUX_SUN_PATH_MAX,
146        label: "Linux sun_path",
147    }
148}
149
150/// Directory holding v1 broker sockets.
151///
152/// Deliberately performs no filesystem writes: name derivation stays pure so
153/// the hash and length-limit tests remain deterministic. Callers that bind
154/// create the parent directory themselves.
155#[cfg(feature = "ipc")]
156fn broker_v1_socket_dir() -> std::path::PathBuf {
157    use std::path::PathBuf;
158
159    match std::env::var_os("XDG_RUNTIME_DIR") {
160        Some(dir) => PathBuf::from(dir).join("running-process").join("broker"),
161        None => PathBuf::from(format!(
162            "/tmp/running-process-{}/broker",
163            unsafe { libc::getuid() }
164        )),
165    }
166}
167
168#[cfg(feature = "ipc")]
169pub fn ipc_broker_v1_endpoint_path(
170    bare_name: &str,
171) -> Result<String, crate::platform::ipc::EndpointNameTooLong> {
172    // Linux gets 108 bytes and a guaranteed $XDG_RUNTIME_DIR (or a short
173    // /tmp fallback), so the full canonical name survives for debuggability.
174    let candidate = broker_v1_socket_dir().join(format!("{bare_name}.sock"));
175    let candidate = candidate.to_string_lossy();
176    // sockaddr_un is NUL-terminated, so the path itself must be strictly
177    // shorter than the field width.
178    if candidate.len() >= LINUX_SUN_PATH_MAX {
179        return Err(crate::platform::ipc::EndpointNameTooLong {
180            len: candidate.len(),
181            max: LINUX_SUN_PATH_MAX - 1,
182            limit_label: "Linux sun_path",
183        });
184    }
185    Ok(candidate.into_owned())
186}
187
188#[cfg(feature = "ipc")]
189pub fn ipc_endpoint_scope_bytes(path: &std::path::Path) -> Vec<u8> {
190    // Linux paths are opaque byte strings; no spelling difference is
191    // meaningless, so the bytes are hashed exactly as the OS reports them.
192    use std::os::unix::ffi::OsStrExt as _;
193
194    path.as_os_str().as_bytes().to_vec()
195}
196
197#[cfg(feature = "ipc")]
198pub fn ipc_broker_v2_runtime_dir() -> std::path::PathBuf {
199    match std::env::var_os("XDG_RUNTIME_DIR") {
200        Some(dir) => std::path::PathBuf::from(dir)
201            .join("running-process")
202            .join("broker-v2"),
203        None => crate::platform::ipc::per_user_runtime_fallback(),
204    }
205}
206#[cfg(feature = "ipc")]
207pub fn into_legacy_ipc_stream(stream: IpcStream) -> interprocess::local_socket::Stream {
208    stream.0
209}
210
211#[cfg(feature = "ipc")]
212pub fn from_legacy_ipc_stream(stream: interprocess::local_socket::Stream) -> IpcStream {
213    ipc::Stream(stream)
214}
215#[cfg(feature = "ipc")]
216pub fn legacy_ipc_name(path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
217    ipc::legacy_name(path)
218}
219#[cfg(feature = "ipc-async")]
220pub use ipc::{
221    AsyncListener as IpcAsyncListener, AsyncStream as IpcAsyncStream,
222    IntoAsyncListener as IpcIntoAsyncListener, IntoAsyncStream as IpcIntoAsyncStream,
223};
224
225#[cfg(feature = "session-relay")]
226#[path = "platform_linux_session_relay.rs"]
227mod session_relay;
228#[cfg(feature = "session-relay")]
229pub use session_relay::relay_local_socket_session;
230
231#[cfg(feature = "pty")]
232#[path = "platform_linux/terminal.rs"]
233pub mod terminal;
234#[cfg(feature = "terminal-graphics")]
235#[path = "platform_linux/terminal_graphics.rs"]
236mod terminal_graphics;
237#[cfg(feature = "terminal-graphics")]
238pub use terminal_graphics::active_graphics_probe;
239pub use crate::platform::terminal_input;
240
241#[path = "platform_linux/window_icon.rs"]
242mod window_icon;
243pub use window_icon::{icon_support as window_icon_support_impl, set_icon as set_window_icon_impl};
244
245pub fn 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 compat_shell_command(command: &str) -> std::process::Command {
252    let mut shell = std::process::Command::new("/bin/sh");
253    shell.arg("-lc").arg(command);
254    shell
255}
256
257pub fn canonical_environment_pairs(pairs: Vec<(String, String)>) -> Vec<(String, String)> {
258    pairs
259}
260
261pub fn monitor_console_windows(
262    _duration: std::time::Duration,
263) -> Vec<crate::platform::process::ConsoleWindowInfo> {
264    Vec::new()
265}
266
267#[cfg(feature = "async-process")]
268use std::ffi::OsStr;
269use std::io;
270use std::io::Read;
271use std::os::fd::{AsRawFd, RawFd};
272use std::os::unix::net::UnixStream;
273use std::sync::Mutex;
274
275#[cfg(feature = "async-process")]
276use tokio::process::{Child, Command};
277
278#[cfg(feature = "async-process")]
279use crate::SpawnSpec;
280
281#[path = "platform_linux_descendants.rs"]
282mod descendants;
283pub use descendants::start_descendant_monitor;
284
285#[path = "platform_linux_trace.rs"]
286mod exact_trace;
287pub use exact_trace::{configure_exact_trace, start_exact_trace, TracedChild};
288
289pub fn exact_trace_capability() -> crate::platform::process::ExactTraceCapability {
290    crate::platform::process::ExactTraceCapability {
291        available: true,
292        backend: "linux-ptrace",
293        reason: "launch-time PTRACE_TRACEME with follow-fork/clone/exec/exit supervision",
294        non_invasive_backend: "proc-descendant-snapshot",
295        non_invasive_grade:
296            crate::platform::process::NonInvasiveObservationGrade::SnapshotInferred,
297    }
298}
299
300pub struct WindowsJobHandle;
301
302pub fn assign_child_to_windows_job(
303    _child: &std::process::Child,
304    _direct_pid: u32,
305    _address_space_limit_bytes: Option<u64>,
306    _emit: Option<Box<dyn Fn(crate::platform::process::DescendantEvent) + Send>>,
307) -> io::Result<WindowsJobHandle> {
308    Err(io::Error::new(
309        io::ErrorKind::Unsupported,
310        "Windows Job Objects are unavailable on Linux",
311    ))
312}
313
314#[derive(Default)]
315pub struct CaptureCancellation {
316    wakers: Mutex<CaptureWakers>,
317}
318
319#[derive(Default)]
320struct CaptureWakers {
321    stdout: Option<UnixStream>,
322    stderr: Option<UnixStream>,
323}
324
325struct CancelableCaptureReader<R> {
326    reader: R,
327    wake_reader: UnixStream,
328}
329
330impl<R: Read + AsRawFd> Read for CancelableCaptureReader<R> {
331    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
332        if buf.is_empty() { return Ok(0); }
333        loop {
334            let mut poll_fds = [
335                libc::pollfd { fd: self.reader.as_raw_fd(), events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, revents: 0 },
336                libc::pollfd { fd: self.wake_reader.as_raw_fd(), events: libc::POLLIN | libc::POLLHUP | libc::POLLERR, revents: 0 },
337            ];
338            // SAFETY: both descriptors remain owned by this reader for the call.
339            let polled = unsafe { libc::poll(poll_fds.as_mut_ptr(), poll_fds.len() as _, -1) };
340            if polled < 0 {
341                let error = io::Error::last_os_error();
342                if error.kind() == io::ErrorKind::Interrupted { continue; }
343                return Err(error);
344            }
345            if poll_fds[1].revents != 0 {
346                return Err(io::Error::new(io::ErrorKind::Interrupted, "capture reader cancelled"));
347            }
348            if poll_fds[0].revents != 0 {
349                match self.reader.read(buf) {
350                    Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
351                    result => return result,
352                }
353            }
354        }
355    }
356}
357
358pub fn prepare_capture_reader<R>(
359    reader: R,
360    cancellation: &CaptureCancellation,
361    stream: crate::platform::process::CaptureStream,
362) -> io::Result<Box<dyn Read + Send>>
363where R: Read + AsRawFd + Send + 'static {
364    set_nonblocking(reader.as_raw_fd())?;
365    let (wake_reader, wake_writer) = UnixStream::pair()?;
366    wake_writer.set_nonblocking(true)?;
367    let mut wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
368    match stream {
369        crate::platform::process::CaptureStream::Stdout => wakers.stdout = Some(wake_writer),
370        crate::platform::process::CaptureStream::Stderr => wakers.stderr = Some(wake_writer),
371    }
372    Ok(Box::new(CancelableCaptureReader { reader, wake_reader }))
373}
374
375pub fn capture_reader_done(cancellation: &CaptureCancellation, stream: crate::platform::process::CaptureStream) {
376    let mut wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
377    match stream {
378        crate::platform::process::CaptureStream::Stdout => wakers.stdout = None,
379        crate::platform::process::CaptureStream::Stderr => wakers.stderr = None,
380    }
381}
382
383pub fn cancel_capture_reader(cancellation: &CaptureCancellation) {
384    let wakers = cancellation.wakers.lock().expect("capture wakers mutex poisoned");
385    let byte = [1_u8; 1];
386    for writer in [&wakers.stdout, &wakers.stderr].into_iter().flatten() {
387        // SAFETY: the stored wake socket stays alive while the mutex is held.
388        let _ = unsafe { libc::write(writer.as_raw_fd(), byte.as_ptr().cast(), byte.len()) };
389    }
390}
391
392fn set_nonblocking(fd: RawFd) -> io::Result<()> {
393    // SAFETY: `fd` is borrowed from a live reader for both calls.
394    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
395    if flags < 0 { return Err(io::Error::last_os_error()); }
396    // SAFETY: `fd` is borrowed from a live reader for both calls.
397    if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
398        return Err(io::Error::last_os_error());
399    }
400    Ok(())
401}
402
403#[path = "platform_linux_file_handles.rs"]
404mod file_handles;
405pub use file_handles::read_process_file_handles;
406#[path = "platform_linux_cmdline.rs"]
407mod cmdline;
408pub use cmdline::{read_process_argv, read_process_cmdline};
409
410#[cfg(feature = "process-inspection")]
411#[path = "platform/process_tree.rs"]
412mod process_tree;
413
414#[cfg(feature = "process-inspection")]
415pub fn kill_tree(pid: u32, timeout: std::time::Duration) -> io::Result<u32> {
416    process_tree::kill_tree(pid, timeout, |_pid, process| Ok(process.start_time()))
417}
418
419pub fn exit_code(status: std::process::ExitStatus) -> i32 {
420    use std::os::unix::process::ExitStatusExt;
421    status.code().unwrap_or_else(|| -status.signal().unwrap_or(1))
422}
423
424pub fn set_process_name(name: &str) {
425    let truncated: String = name.chars().take(15).collect();
426    let c_name = std::ffi::CString::new(truncated).unwrap_or_default();
427    unsafe { libc::prctl(libc::PR_SET_NAME, c_name.as_ptr() as libc::c_ulong, 0, 0, 0); }
428}
429
430pub fn configure_trampoline_command(_command: &mut std::process::Command) {}
431
432pub fn configure_process_command(
433    command: &mut std::process::Command,
434    config: crate::platform::process::ProcessCommandConfig,
435) -> io::Result<()> {
436    configure_process_command_inner(command, config, false)
437}
438
439/// Root-facade-only launch seam for bounded owner-death containment.
440///
441/// This must be `pub` because `running-process` is a separate package, but
442/// applications should use its semantic bounded-run options instead of this
443/// implementation-detail function.
444#[doc(hidden)]
445pub fn configure_process_command_for_bounded_owner_death(
446    command: &mut std::process::Command,
447    config: crate::platform::process::ProcessCommandConfig,
448) -> io::Result<()> {
449    configure_process_command_inner(command, config, true)
450}
451
452fn configure_process_command_inner(
453    command: &mut std::process::Command,
454    config: crate::platform::process::ProcessCommandConfig,
455    kill_when_owner_dies: bool,
456) -> io::Result<()> {
457    let create_process_group = config.create_process_group;
458    let nice = config.nice;
459    let address_space_limit_bytes = config.address_space_limit_bytes;
460    if !(create_process_group
461        || nice.is_some()
462        || address_space_limit_bytes.is_some()
463        || kill_when_owner_dies)
464    {
465        return Ok(());
466    }
467    let owner_pid = if kill_when_owner_dies {
468        // Read before `Command` forks so the child can detect the narrow race
469        // where this process exits between fork and PR_SET_PDEATHSIG.
470        unsafe { libc::getpid() }
471    } else {
472        0
473    };
474    use std::os::unix::process::CommandExt;
475    unsafe {
476        command.pre_exec(move || {
477            if create_process_group && libc::setpgid(0, 0) == -1 {
478                return Err(io::Error::last_os_error());
479            }
480            if let Some(nice) = nice {
481                if libc::setpriority(libc::PRIO_PROCESS, 0, nice) == -1 {
482                    return Err(io::Error::last_os_error());
483                }
484            }
485            if let Some(limit) = address_space_limit_bytes {
486                let rlim = libc::rlimit { rlim_cur: limit, rlim_max: limit };
487                if libc::setrlimit(libc::RLIMIT_AS, &rlim) == -1 {
488                    return Err(io::Error::last_os_error());
489                }
490            }
491            if kill_when_owner_dies {
492                install_parent_death_signal_with_race_guard(owner_pid)?;
493            }
494            Ok(())
495        });
496    }
497    Ok(())
498}
499
500/// Install PDEATHSIG and close the fork-to-prctl owner-death race.
501///
502/// This runs only in `Command::pre_exec`, after fork and before exec.
503fn install_parent_death_signal_with_race_guard(owner_pid: libc::pid_t) -> io::Result<()> {
504    if unsafe {
505        libc::prctl(
506            libc::PR_SET_PDEATHSIG,
507            libc::SIGTERM as libc::c_ulong,
508            0,
509            0,
510            0,
511        )
512    } == -1
513    {
514        return Err(io::Error::last_os_error());
515    }
516    if unsafe { libc::getppid() } != owner_pid {
517        // The owner died after fork but before PDEATHSIG was armed. A
518        // caller-provided pre_exec hook may have ignored SIGTERM, so sending
519        // that signal then returning would let this orphan exec. SAFETY:
520        // `_exit` is async-signal-safe and bypasses Rust destructors and
521        // allocation in this post-fork child.
522        unsafe { libc::_exit(128 + libc::SIGTERM) };
523    }
524    Ok(())
525}
526
527pub fn trampoline_exit_code(status: std::process::ExitStatus) -> i32 {
528    use std::os::unix::process::ExitStatusExt;
529    status.signal().map_or_else(|| status.code().unwrap_or(1), |signal| 128 + signal)
530}
531
532/// Return the GNU build ID of the running executable without reading the
533/// executable from disk.
534///
535/// The dynamic loader has already mapped the main image's `PT_NOTE` segment,
536/// so callers that only need an image-generation identity do not need to hash
537/// a potentially large unoptimized binary. `None` preserves a clean fallback
538/// for binaries linked without a GNU build ID.
539pub fn current_executable_build_id() -> Option<Vec<u8>> {
540    unsafe extern "C" fn visit(
541        info: *mut libc::dl_phdr_info,
542        _size: libc::size_t,
543        output: *mut libc::c_void,
544    ) -> libc::c_int {
545        const MAX_NOTE_BYTES: usize = 1024 * 1024;
546
547        let info = unsafe { &*info };
548        let is_main_executable = info.dlpi_name.is_null()
549            || unsafe { std::ffi::CStr::from_ptr(info.dlpi_name) }
550                .to_bytes()
551                .is_empty();
552        if !is_main_executable || info.dlpi_phdr.is_null() || info.dlpi_phnum == 0 {
553            return 0;
554        }
555        let headers = unsafe {
556            std::slice::from_raw_parts(info.dlpi_phdr, usize::from(info.dlpi_phnum))
557        };
558        #[allow(clippy::unnecessary_cast)]
559        let load_bias = info.dlpi_addr as u64;
560        for header in headers {
561            if header.p_type != libc::PT_NOTE {
562                continue;
563            }
564            let Ok(length) = usize::try_from(header.p_memsz) else {
565                continue;
566            };
567            if length == 0 || length > MAX_NOTE_BYTES {
568                continue;
569            }
570            let Some(address) = load_bias.checked_add(header.p_vaddr) else {
571                continue;
572            };
573            let Some(note_end) = address.checked_add(length as u64) else {
574                continue;
575            };
576            let mapped_read_only = headers.iter().any(|load| {
577                if load.p_type != libc::PT_LOAD || load.p_flags & libc::PF_R == 0 {
578                    return false;
579                }
580                let Some(start) = load_bias.checked_add(load.p_vaddr) else {
581                    return false;
582                };
583                let Some(end) = start.checked_add(load.p_memsz) else {
584                    return false;
585                };
586                address >= start && note_end <= end
587            });
588            if address == 0 || !mapped_read_only {
589                continue;
590            }
591            let notes = unsafe { std::slice::from_raw_parts(address as *const u8, length) };
592            if let Some(build_id) = gnu_build_id_from_notes(notes) {
593                let output = unsafe { &mut *output.cast::<Option<Vec<u8>>>() };
594                *output = Some(build_id.to_vec());
595                return 1;
596            }
597        }
598        0
599    }
600
601    let mut output = None;
602    unsafe {
603        libc::dl_iterate_phdr(
604            Some(visit),
605            (&mut output as *mut Option<Vec<u8>>).cast::<libc::c_void>(),
606        );
607    }
608    output
609}
610
611fn gnu_build_id_from_notes(mut notes: &[u8]) -> Option<&[u8]> {
612    fn aligned(value: usize) -> Option<usize> {
613        value.checked_add(3).map(|value| value & !3)
614    }
615
616    while notes.len() >= 12 {
617        let name_len = usize::try_from(u32::from_ne_bytes(notes[0..4].try_into().ok()?)).ok()?;
618        let desc_len = usize::try_from(u32::from_ne_bytes(notes[4..8].try_into().ok()?)).ok()?;
619        let kind = u32::from_ne_bytes(notes[8..12].try_into().ok()?);
620        let name_end = 12usize.checked_add(name_len)?;
621        let desc_start = 12usize.checked_add(aligned(name_len)?)?;
622        let desc_end = desc_start.checked_add(desc_len)?;
623        let next = desc_start.checked_add(aligned(desc_len)?)?;
624        if next > notes.len() || name_end > notes.len() || desc_end > notes.len() {
625            return None;
626        }
627        if kind == 3 && notes.get(12..name_end)?.starts_with(b"GNU") && desc_len > 0 {
628            return notes.get(desc_start..desc_end);
629        }
630        notes = &notes[next..];
631    }
632    None
633}
634
635/// Request a graceful shutdown for a child-owned POSIX process group.
636pub fn soft_terminate_process_group(pid: u32) -> io::Result<()> {
637    // SAFETY: `kill` receives only the numeric child-owned group id; no Rust
638    // references or borrowed state cross the OS boundary.
639    let result = unsafe { libc::kill(-(pid as i32), libc::SIGTERM) };
640    if result != 0 {
641        let error = io::Error::last_os_error();
642        if error.raw_os_error() != Some(libc::ESRCH) {
643            return Err(error);
644        }
645    }
646    Ok(())
647}
648
649pub fn process_snapshot() -> Vec<crate::platform::process::ProcessSnapshot> {
650    Vec::new()
651}
652
653pub fn process_snapshot_for_pid(_pid: u32) -> Option<crate::platform::process::ProcessSnapshot> {
654    None
655}
656
657/// Mark inherited descriptors close-on-exec without breaking std's exec-error pipe.
658///
659/// # Safety
660/// This must only be called from a post-fork `pre_exec` closure.
661pub unsafe fn unix_mark_extra_fds_close_on_exec() {
662    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86", target_arch = "arm", target_arch = "riscv64", target_arch = "powerpc64"))]
663    {
664        const SYS_CLOSE_RANGE: libc::c_long = 436;
665        const CLOSE_RANGE_CLOEXEC: libc::c_uint = 4;
666        if libc::syscall(SYS_CLOSE_RANGE, 3u32, libc::c_uint::MAX, CLOSE_RANGE_CLOEXEC) == 0 {
667            return;
668        }
669    }
670    mark_fds_from_directory_or_range();
671}
672
673pub fn configure_sync_daemon_command(command: &mut std::process::Command) -> io::Result<()> {
674    configure_sync_daemon_command_inner(command, None)
675}
676
677pub fn configure_sync_daemon_command_with_inheritance(
678    command: &mut std::process::Command,
679    inheritance: crate::platform::process::DaemonExecInheritance,
680) -> io::Result<()> {
681    configure_sync_daemon_command_inner(command, Some(inheritance))
682}
683
684fn configure_sync_daemon_command_inner(
685    command: &mut std::process::Command,
686    inheritance: Option<crate::platform::process::DaemonExecInheritance>,
687) -> io::Result<()> {
688    use std::os::unix::process::CommandExt;
689    unsafe {
690        command.pre_exec(move || {
691            let _ = libc::setsid();
692            unix_mark_extra_fds_close_on_exec();
693            if let Some(inheritance) = inheritance {
694                clear_cloexec_after_sweep(inheritance.descriptor())?;
695            }
696            Ok(())
697        });
698    }
699    Ok(())
700}
701
702unsafe fn clear_cloexec_after_sweep(fd: libc::c_int) -> io::Result<()> {
703    let flags = libc::fcntl(fd, libc::F_GETFD);
704    if flags == -1 {
705        return Err(io::Error::last_os_error());
706    }
707    if libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) == -1 {
708        return Err(io::Error::last_os_error());
709    }
710    Ok(())
711}
712
713pub fn configure_sync_contained_command(command: &mut std::process::Command) -> io::Result<()> {
714    use std::os::unix::process::CommandExt;
715    unsafe {
716        command.pre_exec(|| {
717            if libc::setpgid(0, 0) == -1 { return Err(io::Error::last_os_error()); }
718            if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
719                return Err(io::Error::last_os_error());
720            }
721            if libc::getppid() == 1 { libc::_exit(1); }
722            unix_mark_extra_fds_close_on_exec();
723            Ok(())
724        });
725    }
726    Ok(())
727}
728
729pub fn parent_has_console() -> bool { false }
730
731pub fn sync_child_native_handle(_child: &std::process::Child) -> usize { 0 }
732
733unsafe fn mark_fds_from_directory_or_range() {
734    let dir = libc::opendir(c"/dev/fd".as_ptr());
735    if !dir.is_null() {
736        let dir_fd = libc::dirfd(dir);
737        loop {
738            let entry = libc::readdir(dir);
739            if entry.is_null() { break; }
740            let mut fd: libc::c_int = 0;
741            let mut cursor = (*entry).d_name.as_ptr();
742            let mut numeric = false;
743            while *cursor != 0 {
744                let byte = *cursor as u8;
745                if !byte.is_ascii_digit() { numeric = false; break; }
746                fd = fd * 10 + (byte - b'0') as libc::c_int;
747                cursor = cursor.add(1);
748                numeric = true;
749            }
750            if numeric && fd > 2 && fd != dir_fd { set_cloexec(fd); }
751        }
752        libc::closedir(dir);
753        return;
754    }
755    let maximum = libc::sysconf(libc::_SC_OPEN_MAX);
756    for fd in 3..if maximum < 0 { 4096 } else { maximum as libc::c_int } { set_cloexec(fd); }
757}
758
759unsafe fn set_cloexec(fd: libc::c_int) {
760    let flags = libc::fcntl(fd, libc::F_GETFD);
761    if flags != -1 { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC); }
762}
763pub fn observer_backend(scope: crate::platform::process::ObserverScope, category: crate::platform::process::ObserverCategory) -> crate::platform::process::ObserverBackend {
764    use crate::platform::process::{ObserverBackend as B, ObserverCategory as C, ObserverScope as S, ObserverSupport as P};
765    match (scope, category) {
766        (S::SystemWide, C::File) => B { support:P::Unavailable, backend:"seccomp-user-notify", reason:"Phase 3: Linux seccomp user-notify file backend not yet implemented" },
767        (S::SystemWide, C::Network) => B { support:P::Unavailable, backend:"ebpf", reason:"Phase 3: Linux eBPF network backend not yet implemented" },
768        (S::SystemWide, C::Process) => B { support:P::Unavailable, backend:"seccomp-user-notify", reason:"Phase 3: Linux seccomp user-notify process backend not yet implemented" },
769        (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)" },
770        (S::LaunchedProcessTree, C::Network) => B { support:P::Unavailable, backend:"none", reason:"#539: no-admin per-child network backend deferred to a follow-up issue" },
771        (S::LaunchedProcessTree, C::Process) => B { support:P::Supported, backend:"subreaper-proc-poll", reason:"Linux PR_SET_CHILD_SUBREAPER + /proc descendant polling (#539 slice 5)" },
772    }
773}
774
775pub fn unix_set_priority(pid: u32, nice: i32) -> io::Result<()> {
776    if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid, nice) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
777}
778pub fn unix_signal_process(pid: u32, signal: crate::platform::process::UnixSignalKind) -> io::Result<()> {
779    if unsafe { libc::kill(pid as i32, unix_signal_raw(signal)) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
780}
781pub fn unix_signal_process_group(pid: i32, signal: crate::platform::process::UnixSignalKind) -> io::Result<()> {
782    if unsafe { libc::killpg(pid, unix_signal_raw(signal)) } == -1 { Err(io::Error::last_os_error()) } else { Ok(()) }
783}
784pub fn unix_signal_raw(signal: crate::platform::process::UnixSignalKind) -> i32 {
785    match signal { crate::platform::process::UnixSignalKind::Interrupt => libc::SIGINT, crate::platform::process::UnixSignalKind::Terminate => libc::SIGTERM, crate::platform::process::UnixSignalKind::Kill => libc::SIGKILL }
786}
787
788#[cfg(feature = "async-process")]
789pub fn configure_compat_tokio_command(
790    command: &mut Command,
791    _show_console: bool,
792    kill_when_owner_dies: bool,
793) -> io::Result<()> {
794    configure_command(command, false, kill_when_owner_dies, None)
795}
796
797/// Nothing to do on this host: the parent-death signal is installed in `pre_exec`, before the
798/// child ever runs, so nothing remains to do once it has.
799#[cfg(feature = "async-process")]
800pub fn after_compat_tokio_spawn(
801    _child: &Child,
802    _kill_when_owner_dies: bool,
803) -> io::Result<()> {
804    Ok(())
805}
806
807#[cfg(feature = "async-process")]
808pub(crate) fn configure_command(
809    command: &mut Command,
810    create_process_group: bool,
811    kill_when_owner_dies: bool,
812    nice: Option<i32>,
813) -> io::Result<()> {
814    if create_process_group {
815        command.process_group(0);
816    }
817    if kill_when_owner_dies || nice.is_some() {
818        let owner_pid = unsafe { libc::getpid() };
819        // SAFETY: the closure invokes only async-signal-safe libc calls.
820        unsafe {
821            command.pre_exec(move || {
822                if let Some(nice) = nice {
823                    if libc::setpriority(libc::PRIO_PROCESS, 0, nice) == -1 {
824                        return Err(io::Error::last_os_error());
825                    }
826                }
827                if kill_when_owner_dies {
828                    install_parent_death_signal_with_race_guard(owner_pid)?;
829                }
830                Ok(())
831            });
832        }
833    }
834    Ok(())
835}
836
837#[cfg(feature = "async-process")]
838pub(crate) fn after_spawn(_child: &Child, _kill_when_owner_dies: bool) -> io::Result<()> {
839    Ok(())
840}
841
842/// Launch-bound identity for private async controls.
843///
844/// `pidfd_open` supplies the race-free direct-control capability where the
845/// kernel permits it. `/proc/<pid>/stat` start ticks remain available for CPU
846/// accounting on older or restricted hosts, but are never a raw-PID control
847/// fallback.
848#[cfg(feature = "async-process")]
849pub(crate) struct AsyncChildIdentity {
850    pid: u32,
851    start_ticks: u64,
852    pidfd: Option<std::os::fd::OwnedFd>,
853}
854
855#[cfg(feature = "async-process")]
856pub(crate) fn async_child_identity(child: &Child) -> Option<AsyncChildIdentity> {
857    let pid = child.id()?;
858    let (start_ticks, _, _) = proc_stat(pid).ok()?;
859    let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::c_int, 0) } as libc::c_int;
860    let pidfd = (fd >= 0).then(|| {
861        // SAFETY: pidfd_open returned a newly owned descriptor above.
862        unsafe { <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(fd) }
863    });
864    Some(AsyncChildIdentity {
865        pid,
866        start_ticks,
867        pidfd,
868    })
869}
870
871#[cfg(feature = "async-process")]
872pub(crate) fn signal_async_child(identity: &AsyncChildIdentity) -> io::Result<()> {
873    if identity_matches(identity) {
874        pidfd_send_signal(identity, libc::SIGKILL)
875    } else {
876        Err(io::Error::new(
877            io::ErrorKind::BrokenPipe,
878            "child process launch identity no longer matches",
879        ))
880    }
881}
882
883#[cfg(feature = "async-process")]
884pub(crate) fn signal_async_child_group(identity: &AsyncChildIdentity) -> io::Result<()> {
885    if !identity_matches(identity) || !pidfd_is_live(identity)? {
886        return Err(io::Error::new(
887            io::ErrorKind::BrokenPipe,
888            "child process launch identity no longer matches",
889        ));
890    }
891    if unsafe { libc::kill(-(identity.pid as i32), libc::SIGTERM) } == 0 {
892        Ok(())
893    } else {
894        Err(io::Error::last_os_error())
895    }
896}
897
898#[cfg(feature = "async-process")]
899pub(crate) fn async_child_cpu_time(
900    identity: &AsyncChildIdentity,
901) -> io::Result<Option<std::time::Duration>> {
902    let Ok((start_ticks, user_ticks, system_ticks)) = proc_stat(identity.pid) else {
903        return Ok(None);
904    };
905    if start_ticks != identity.start_ticks {
906        return Ok(None);
907    }
908    let ticks_per_second = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
909    if ticks_per_second <= 0 {
910        return Ok(None);
911    }
912    let ticks = user_ticks.saturating_add(system_ticks);
913    let hz = ticks_per_second as u64;
914    Ok(Some(
915        std::time::Duration::from_secs(ticks / hz)
916            + std::time::Duration::from_nanos(
917                ticks
918                    % hz
919                    .saturating_mul(1_000_000_000)
920                    / hz,
921            ),
922    ))
923}
924
925#[cfg(feature = "async-process")]
926fn identity_matches(identity: &AsyncChildIdentity) -> bool {
927    matches!(proc_stat(identity.pid), Ok((start_ticks, _, _)) if start_ticks == identity.start_ticks)
928}
929
930#[cfg(feature = "async-process")]
931fn pidfd_is_live(identity: &AsyncChildIdentity) -> io::Result<bool> {
932    let Some(pidfd) = identity.pidfd.as_ref() else {
933        return Err(io::Error::new(
934            io::ErrorKind::Unsupported,
935            "pidfd control is unavailable for this child",
936        ));
937    };
938    let result = unsafe {
939        libc::syscall(
940            libc::SYS_pidfd_send_signal,
941            std::os::fd::AsRawFd::as_raw_fd(pidfd),
942            0,
943            std::ptr::null::<libc::siginfo_t>(),
944            0,
945        )
946    };
947    if result == 0 {
948        return Ok(true);
949    }
950    let error = io::Error::last_os_error();
951    if error.raw_os_error() == Some(libc::ESRCH) {
952        Ok(false)
953    } else {
954        Err(error)
955    }
956}
957
958#[cfg(feature = "async-process")]
959fn pidfd_send_signal(identity: &AsyncChildIdentity, signal: libc::c_int) -> io::Result<()> {
960    let Some(pidfd) = identity.pidfd.as_ref() else {
961        return Err(io::Error::new(
962            io::ErrorKind::Unsupported,
963            "pidfd control is unavailable for this child",
964        ));
965    };
966    let result = unsafe {
967        libc::syscall(
968            libc::SYS_pidfd_send_signal,
969            std::os::fd::AsRawFd::as_raw_fd(pidfd),
970            signal,
971            std::ptr::null::<libc::siginfo_t>(),
972            0,
973        )
974    };
975    if result == 0 {
976        return Ok(());
977    }
978    let error = io::Error::last_os_error();
979    if error.raw_os_error() == Some(libc::ESRCH) {
980        Ok(())
981    } else {
982        Err(error)
983    }
984}
985
986#[cfg(feature = "async-process")]
987fn proc_stat(pid: u32) -> io::Result<(u64, u64, u64)> {
988    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat"))?;
989    let fields = stat
990        .rsplit_once(')')
991        .map(|(_, fields)| fields.split_ascii_whitespace().collect::<Vec<_>>())
992        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "malformed /proc stat"))?;
993    let parse = |index: usize| -> io::Result<u64> {
994        fields
995            .get(index)
996            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "short /proc stat"))?
997            .parse::<u64>()
998            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid /proc stat"))
999    };
1000    // Fields after the closing command name begin at stat field 3: utime=11,
1001    // stime=12, starttime=19 in this zero-based tail.
1002    Ok((parse(19)?, parse(11)?, parse(12)?))
1003}
1004
1005#[cfg(feature = "async-process")]
1006pub(crate) fn shell_spec(command: &OsStr) -> SpawnSpec {
1007    SpawnSpec::new("/bin/sh").arg("-c").arg(command)
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    #[cfg(feature = "async-process")]
1013    #[test]
1014    fn async_identity_mismatch_fails_closed_without_pid_signal() {
1015        let pid = unsafe { libc::getpid() as u32 };
1016        let (start_ticks, _, _) = super::proc_stat(pid).expect("read this process start key");
1017        let identity = super::AsyncChildIdentity {
1018            pid,
1019            start_ticks: start_ticks.saturating_add(1),
1020            pidfd: None,
1021        };
1022        assert!(!super::identity_matches(&identity));
1023        let error = super::signal_async_child(&identity)
1024            .expect_err("mismatched launch identity must not signal a reused PID");
1025        assert_eq!(error.kind(), std::io::ErrorKind::BrokenPipe);
1026        assert_eq!(super::async_child_cpu_time(&identity).unwrap(), None);
1027    }
1028
1029    #[cfg(feature = "async-process")]
1030    #[test]
1031    fn async_identity_without_pidfd_keeps_cpu_but_refuses_pid_control() {
1032        let pid = unsafe { libc::getpid() as u32 };
1033        let (start_ticks, _, _) = super::proc_stat(pid).expect("read this process start key");
1034        let identity = super::AsyncChildIdentity {
1035            pid,
1036            start_ticks,
1037            pidfd: None,
1038        };
1039        assert!(super::async_child_cpu_time(&identity).unwrap().is_some());
1040        let error = super::signal_async_child(&identity).expect_err("no raw-PID kill fallback");
1041        assert_eq!(error.kind(), std::io::ErrorKind::Unsupported);
1042    }
1043
1044    #[test]
1045    fn owner_death_race_guard_exits_when_sigterm_is_ignored() {
1046        let child = unsafe { libc::fork() };
1047        assert!(child >= 0, "fork owner-death race fixture");
1048        if child == 0 {
1049            // Model a caller-supplied pre_exec hook which ignored SIGTERM
1050            // before the bounded runner's hook is appended.
1051            if unsafe { libc::signal(libc::SIGTERM, libc::SIG_IGN) } == libc::SIG_ERR {
1052                unsafe { libc::_exit(98) };
1053            }
1054            let owner_pid = unsafe { libc::getppid() }.saturating_add(1);
1055            // A deliberately mismatched owner PID models the post-prctl
1056            // parent-death race. The helper must not return even though the
1057            // SIGTERM disposition above would ignore a signal-based guard.
1058            if super::install_parent_death_signal_with_race_guard(owner_pid).is_err() {
1059                unsafe { libc::_exit(99) };
1060            }
1061            unsafe { libc::_exit(100) };
1062        }
1063
1064        let mut status = 0;
1065        assert_eq!(unsafe { libc::waitpid(child, &mut status, 0) }, child);
1066        assert!(libc::WIFEXITED(status), "race fixture must _exit");
1067        assert_eq!(
1068            libc::WEXITSTATUS(status),
1069            128 + libc::SIGTERM,
1070            "ignored SIGTERM must not permit the owner-dead child to continue"
1071        );
1072    }
1073
1074    #[test]
1075    fn shell_command_preserves_login_shell_contract_and_ignores_child_path() {
1076        use std::ffi::OsStr;
1077
1078        let command_text = "printf '%s' 'alpha beta;\"gamma\"'";
1079        let mut command = super::shell_command(command_text);
1080        assert_eq!(command.get_program(), OsStr::new("/bin/sh"));
1081        assert_eq!(
1082            command.get_args().collect::<Vec<_>>(),
1083            [OsStr::new("-lc"), OsStr::new(command_text)]
1084        );
1085        command
1086            .env_clear()
1087            .env("PATH", "/caller-supplied-path-override");
1088        let output = command
1089            .output()
1090            .expect("absolute shell command should execute independently of child PATH");
1091        assert!(output.status.success());
1092        assert_eq!(output.stdout, b"alpha beta;\"gamma\"");
1093    }
1094
1095    #[test]
1096    #[cfg(not(target_env = "musl"))]
1097    fn current_executable_exposes_a_gnu_build_id() {
1098        let build_id = super::current_executable_build_id()
1099            .expect("Linux test executable should carry a GNU build ID");
1100        assert!(!build_id.is_empty());
1101    }
1102}
1103#[cfg(test)]
1104#[path = "tests/platform_linux_coverage.rs"]
1105mod coverage_tests;
1106#[path = "sync_spawn_group.rs"]
1107mod sync_spawn;
1108pub use sync_spawn::{spawn_sync, spawn_sync_daemon, spawn_sync_daemon_with_inheritance};
1109
1110#[cfg(all(test, feature = "ipc"))]
1111mod endpoint_naming_tests {
1112    use super::{ipc_broker_v1_endpoint_path, ipc_endpoint_name_limit, LINUX_SUN_PATH_MAX};
1113
1114    #[test]
1115    fn the_v1_address_keeps_the_full_name_for_debuggability() {
1116        let address = ipc_broker_v1_endpoint_path("rpb-v1-abc-shared").expect("derive address");
1117        assert!(address.contains("rpb-v1-abc-shared"));
1118        assert!(address.ends_with("-shared.sock"));
1119        assert!(address.contains("/broker/"));
1120    }
1121
1122    #[test]
1123    fn an_over_long_name_is_refused_against_sun_path() {
1124        let err = ipc_broker_v1_endpoint_path(&"a".repeat(LINUX_SUN_PATH_MAX))
1125            .expect_err("must exceed sun_path");
1126        assert_eq!(err.max, LINUX_SUN_PATH_MAX - 1);
1127        assert_eq!(err.limit_label, "Linux sun_path");
1128    }
1129
1130    #[test]
1131    fn an_accepted_address_is_strictly_shorter_than_the_field() {
1132        // sockaddr_un is NUL-terminated, so equality with the field width
1133        // would truncate the terminator.
1134        let address = ipc_broker_v1_endpoint_path("rpb-v1-abc-shared").expect("derive address");
1135        assert!(address.len() < LINUX_SUN_PATH_MAX);
1136    }
1137
1138    #[test]
1139    fn the_reported_budget_is_sun_path() {
1140        let limit = ipc_endpoint_name_limit();
1141        assert_eq!(limit.max_bytes, LINUX_SUN_PATH_MAX);
1142        assert_eq!(limit.label, "Linux sun_path");
1143    }
1144
1145    #[test]
1146    fn the_scope_spelling_is_the_verbatim_path_bytes() {
1147        // Paths are opaque byte strings here: no spelling difference is
1148        // meaningless, and case is significant. This pins the spelling --
1149        // changing it re-scopes every deployed broker, and the stability
1150        // tests upstream would not notice.
1151        use super::ipc_endpoint_scope_bytes;
1152
1153        let bytes = ipc_endpoint_scope_bytes(std::path::Path::new("/usr/local/bin/Broker"));
1154        assert_eq!(bytes, b"/usr/local/bin/Broker".to_vec());
1155
1156        let lowered = ipc_endpoint_scope_bytes(std::path::Path::new("/usr/local/bin/broker"));
1157        assert_ne!(bytes, lowered, "case must remain significant");
1158    }
1159
1160}
1161
1162/// Replace this process's image with `command`.
1163///
1164/// Returns only on failure: on success `execve` has already replaced the
1165/// program and there is nothing left to return to. That is why the signature
1166/// yields `io::Error` rather than `io::Result<()>` -- an `Ok` would name a
1167/// state that cannot be observed.
1168pub fn process_replace_current_image(command: &mut std::process::Command) -> std::io::Error {
1169    use std::os::unix::process::CommandExt as _;
1170    command.exec()
1171}
1172
1173/// This host replaces a running image in place; see the facade for what that
1174/// means for a caller that cannot accept a successor instead.
1175pub const fn process_can_replace_current_image() -> bool {
1176    true
1177}