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