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