subc_os/lib.rs
1//! Operating-system primitives the subc daemon needs and cannot reach without
2//! unsafe code, each behind a small safe API.
3//!
4//! The daemon crates forbid unsafe code. This crate is the one deliberate
5//! exception (like `subc-uptime` and `subc-cgroup`): every `unsafe` block here
6//! is a single foreign call with its preconditions stated beside it, and nothing
7//! unsafe is exported.
8//!
9//! Today it answers one question: is the process now holding pid N the same
10//! process the daemon spawned earlier? A pid alone cannot say, because the
11//! kernel reuses pids once a process has been reaped. [`Process`] reads the two
12//! facts that tell processes apart, the kernel's start time for the pid and the
13//! file identity (device and inode) of the executable image it runs, and sends
14//! signals to it.
15//!
16//! Sources, per platform:
17//!
18//! - Linux: the start time is field 22 of `/proc/<pid>/stat` (clock ticks since
19//! boot), and the executable is `stat` through `/proc/<pid>/exe`, which
20//! resolves to the running image even if its file has since been replaced or
21//! deleted. A pidfd is opened before either is read and signals go through it
22//! (`pidfd_send_signal`), so the process that was checked is the process that
23//! is signalled. No unsafe code is needed: rustix wraps both calls.
24//! - macOS: the start time is `kp_proc.p_starttime` from `sysctl`
25//! `KERN_PROC_PID` (microseconds since the epoch), and the executable is the
26//! path `proc_pidpath` reports, then `stat` on that path. These two calls are
27//! unsafe. macOS has no pidfd, so a signal is a plain
28//! `kill` sent right after the checks; see [`Process::signal`].
29//! - Windows: creation time and forced stops use one retained process handle.
30//! `ExecutableCapture` pins the resolved executable until a suspended spawn
31//! binds its volume and 128-bit file ID to that handle and creation time.
32//! - Anywhere else: [`Process::open`] reports [`std::io::ErrorKind::Unsupported`].
33//!
34//! For persisted PID owners, [`process_identity`] reads versioned kernel start
35//! identities and distinguishes alive, dead and unknown without spawning a
36//! process. Its foreign calls are signal-zero `kill` on Unix and `proc_pidinfo`
37//! on macOS. Only dead owners may be reclaimed; unknown owners stay protected.
38//!
39//! It also reads how much memory and CPU time one process is using, for
40//! reporting only; see [`resource_usage`]. On Linux that is procfs again; on
41//! macOS it is `proc_pid_rusage`, plus `mach_timebase_info` to convert its CPU
42//! times to nanoseconds, the other two unsafe calls in the crate.
43//!
44//! And it carries the launch nonce from the daemon to each module it spawns
45//! over an inherited Unix pipe or a PID-authenticated Windows named pipe:
46//! [`launch_nonce`] is the one cached reader every module uses. Windows keeps
47//! an environment copy for old Windows readers until live source reports show
48//! every module consuming the named pipe.
49
50#![deny(unsafe_code)]
51
52#[cfg(all(unix, feature = "test-support"))]
53pub mod fork_exec_test;
54pub mod launch_nonce;
55pub mod privacy_identity;
56pub mod process_identity;
57#[cfg(windows)]
58pub mod windows_acl;
59#[cfg(unix)]
60pub use launch_nonce::LaunchNonceHandoff;
61pub use launch_nonce::{
62 launch_nonce, LaunchNonce, LaunchNonceError, LaunchNonceSource, LAUNCH_NONCE_ENV,
63 LAUNCH_NONCE_FD, LAUNCH_NONCE_FD_ENV, LAUNCH_NONCE_PIPE_ENV, LAUNCH_NONCE_PIPE_FALLBACK_ENV,
64};
65
66#[cfg(windows)]
67pub use launch_nonce::{LaunchNoncePipeDelivery, LaunchNoncePipeHandoff};
68
69#[cfg(target_os = "linux")]
70mod linux;
71#[cfg(target_os = "macos")]
72mod macos;
73#[cfg(windows)]
74mod windows;
75#[cfg(all(test, windows))]
76mod windows_tests;
77#[cfg(windows)]
78pub use windows::{
79 ExecutableCapture, ImageAgreement, ImageUnavailable, SpawnedImage, WindowsFileIdentity,
80};
81
82#[cfg(target_os = "linux")]
83use linux as platform;
84#[cfg(target_os = "macos")]
85use macos as platform;
86
87use std::{io, path::Path};
88
89/// True where [`Process`] can identify and stop a process by pid.
90pub const PROCESS_IDENTITY_SUPPORTED: bool =
91 cfg!(any(target_os = "linux", target_os = "macos", windows));
92
93/// Device and inode of a file: which file, independent of the name used to
94/// reach it.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub struct FileIdentity {
97 pub device: u64,
98 pub inode: u64,
99}
100
101/// The device and inode of the file at `path`, following symlinks. `None` if it
102/// cannot be read or the platform has no inode numbers.
103pub fn file_identity(path: &Path) -> Option<FileIdentity> {
104 #[cfg(unix)]
105 {
106 use std::os::unix::fs::MetadataExt;
107
108 std::fs::metadata(path).ok().map(|metadata| FileIdentity {
109 device: metadata.dev(),
110 inode: metadata.ino(),
111 })
112 }
113 #[cfg(not(unix))]
114 {
115 let _ = path;
116 None
117 }
118}
119
120/// What a live process looks like right now.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct Observation {
123 /// The kernel's start time for the process. Opaque: compare it only with a
124 /// value read on the same host by this crate. Linux counts clock ticks since
125 /// boot; macOS counts microseconds since the epoch; Windows counts 100 ns
126 /// intervals since the Windows epoch.
127 pub start_time: u64,
128 /// The file the process is executing, or `None` if it could not be read
129 /// (for example, a process owned by another user).
130 /// Windows uses the full file ID in `SpawnedImage` instead of a Unix inode;
131 /// this field is always `None` there.
132 pub executable: Option<FileIdentity>,
133}
134
135/// A signal [`Process::signal`] can send.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum Signal {
138 /// SIGTERM: a request to exit, which the process may handle or ignore.
139 Terminate,
140 /// SIGKILL: ends the process; it cannot be handled or ignored.
141 Kill,
142}
143
144/// The kernel start time of the process holding `pid`, or `None` if there is
145/// none, it has already exited (a zombie waiting to be reaped counts as exited),
146/// or the platform has no source.
147pub fn start_time(pid: u32) -> Option<u64> {
148 #[cfg(any(target_os = "linux", target_os = "macos"))]
149 {
150 platform::start_time(pid)
151 }
152 #[cfg(windows)]
153 {
154 Process::open(pid)
155 .ok()??
156 .observe()
157 .map(|observation| observation.start_time)
158 }
159 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
160 {
161 let _ = pid;
162 None
163 }
164}
165
166/// True where [`resource_usage`] can read a live process. Elsewhere it always
167/// answers `None`, and a caller can use this to say "not supported here"
168/// rather than "could not read".
169pub const RESOURCE_USAGE_SUPPORTED: bool =
170 cfg!(any(target_os = "linux", target_os = "macos", windows));
171
172/// What [`ResourceUsage::memory_bytes`] measures. The platforms offer
173/// different figures, and they are not interchangeable.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum MemoryKind {
176 /// macOS `phys_footprint`: the memory the kernel charges to the process
177 /// (dirty and compressed pages, among others), which is also what jetsam
178 /// acts on. Pages an allocator has released with `MADV_FREE` do not count.
179 PhysFootprint,
180 /// Linux `VmRSS`: pages of the process resident in RAM, including shared
181 /// file-backed pages. Swapped-out pages are not included; see
182 /// [`ResourceUsage::swap_bytes`].
183 ResidentSet,
184 /// Windows `WorkingSetSize`: pageable memory currently resident in RAM,
185 /// including shared pages. This is not Unix RSS or private committed memory.
186 WindowsWorkingSet,
187}
188
189/// One reading of a process's memory and cumulative CPU time.
190///
191/// It covers the process named by the pid alone: its threads are included,
192/// processes it has started are not.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct ResourceUsage {
195 /// Memory in bytes, measured as [`Self::memory_kind`] says.
196 pub memory_bytes: u64,
197 pub memory_kind: MemoryKind,
198 /// Bytes swapped out (Linux `VmSwap`). `None` where the platform does not
199 /// report it for a single process, which is not the same as zero.
200 pub swap_bytes: Option<u64>,
201 /// CPU time spent in user mode since the process started.
202 pub cpu_user: std::time::Duration,
203 /// CPU time spent in the kernel on the process's behalf since it started.
204 pub cpu_system: std::time::Duration,
205}
206
207/// Memory and cumulative CPU time of the process holding `pid`, read now.
208///
209/// `None` when there is no such process, it has exited (a zombie awaiting its
210/// reap counts as exited), it cannot be read (for example, another user's
211/// process on macOS), or the platform has no source
212/// (see [`RESOURCE_USAGE_SUPPORTED`]). Never a reading of zeros in place of
213/// one of those.
214///
215/// Like any pid-based read, this describes whatever process holds `pid` now;
216/// a caller that needs it to be a particular process should confirm that
217/// process's [`start_time`] around the call.
218pub fn resource_usage(pid: u32) -> Option<ResourceUsage> {
219 #[cfg(any(target_os = "linux", target_os = "macos"))]
220 {
221 platform::resource_usage(pid)
222 }
223 #[cfg(windows)]
224 {
225 Process::open(pid).ok()??.resource_usage()
226 }
227 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
228 {
229 let _ = pid;
230 None
231 }
232}
233
234/// A handle on the process holding one pid at the moment it was opened.
235#[derive(Debug)]
236pub struct Process {
237 pid: u32,
238 #[cfg(target_os = "linux")]
239 pidfd: Option<std::os::fd::OwnedFd>,
240 #[cfg(windows)]
241 handle: std::os::windows::io::OwnedHandle,
242}
243
244impl Process {
245 /// Open a handle on the process now holding `pid`.
246 ///
247 /// `Ok(None)` means no process holds that pid (on macOS, also a zombie
248 /// awaiting its reap; on Linux a zombie opens, and [`Self::observe`] then
249 /// reports it as exited). On Linux this opens a pidfd,
250 /// which from then on refers to this exact process even if it exits and the
251 /// pid is reused; when the kernel cannot open one (older than 5.3, or a
252 /// seccomp policy refusing the call) the handle falls back to the pid, as
253 /// on macOS.
254 pub fn open(pid: u32) -> io::Result<Option<Self>> {
255 #[cfg(target_os = "linux")]
256 {
257 linux::open(pid).map(|opened| opened.map(|pidfd| Self { pid, pidfd }))
258 }
259 #[cfg(target_os = "macos")]
260 {
261 Ok(platform::exists(pid).then_some(Self { pid }))
262 }
263 #[cfg(windows)]
264 {
265 windows::open(pid).map(|opened| opened.map(|handle| Self { pid, handle }))
266 }
267 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
268 {
269 let _ = pid;
270 Err(io::Error::new(
271 io::ErrorKind::Unsupported,
272 "process identity is not available on this platform",
273 ))
274 }
275 }
276
277 pub fn pid(&self) -> u32 {
278 self.pid
279 }
280
281 /// True when signals go through a pidfd, so they cannot reach a different
282 /// process that has since reused this pid.
283 pub fn signals_through_pidfd(&self) -> bool {
284 #[cfg(target_os = "linux")]
285 {
286 self.pidfd.is_some()
287 }
288 #[cfg(not(target_os = "linux"))]
289 {
290 false
291 }
292 }
293
294 /// The process's start time and executable, or `None` once it has exited
295 /// (including as a zombie not yet reaped by its parent).
296 pub fn observe(&self) -> Option<Observation> {
297 #[cfg(any(target_os = "linux", target_os = "macos"))]
298 {
299 #[cfg(target_os = "linux")]
300 if !linux::pidfd_alive(self.pidfd.as_ref()) {
301 return None;
302 }
303 let start_time = platform::start_time(self.pid)?;
304 Some(Observation {
305 start_time,
306 executable: platform::executable_identity(self.pid),
307 })
308 }
309 #[cfg(windows)]
310 {
311 windows::observe(self)
312 }
313 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
314 {
315 None
316 }
317 }
318
319 /// Wait on the retained Windows handle. `Ok(false)` means the bound elapsed,
320 /// not that a reused PID was observed.
321 #[cfg(windows)]
322 pub fn wait_for_exit(&self, timeout: std::time::Duration) -> io::Result<bool> {
323 windows::wait(self, timeout)
324 }
325
326 /// Force the confirmed Windows process to stop, then wait on the same handle.
327 /// This is not a graceful termination signal. A different creation time
328 /// refuses before any action; `Ok(false)` means the wait bound elapsed.
329 #[cfg(windows)]
330 pub fn force_stop(
331 &self,
332 expected_start_time: u64,
333 timeout: std::time::Duration,
334 ) -> io::Result<bool> {
335 windows::force_stop(self, expected_start_time, timeout)
336 }
337
338 /// Windows resources read through the retained handle, not by reopening its PID.
339 #[cfg(windows)]
340 pub fn resource_usage(&self) -> Option<ResourceUsage> {
341 windows::resource_usage(self)
342 }
343
344 /// Send `signal` to the process.
345 ///
346 /// With a pidfd the signal can only reach the process this handle was
347 /// opened on: if that process has exited, the call fails with `ESRCH` even
348 /// if the pid has been reused. Without one (macOS, or a Linux kernel with no
349 /// pidfd) the signal goes to whatever holds the pid now, so callers should
350 /// [`Self::observe`] immediately before signalling. What remains is the
351 /// time between that check and this call; for a different process to be
352 /// hit, the checked one must exit, be reaped, and have its pid handed to a
353 /// new process inside that window, and both kernels hand out pids in
354 /// increasing order, so a reuse needs the whole pid space to wrap first.
355 ///
356 /// `Ok(false)` means the process had already exited (`ESRCH`).
357 pub fn signal(&self, signal: Signal) -> io::Result<bool> {
358 #[cfg(any(target_os = "linux", target_os = "macos"))]
359 {
360 #[cfg(target_os = "linux")]
361 let result = linux::signal(self.pid, self.pidfd.as_ref(), signal);
362 #[cfg(target_os = "macos")]
363 let result = macos::signal(self.pid, signal);
364 match result {
365 Ok(()) => Ok(true),
366 Err(rustix::io::Errno::SRCH) => Ok(false),
367 Err(error) => Err(error.into()),
368 }
369 }
370 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
371 {
372 let _ = signal;
373 Err(io::Error::new(
374 io::ErrorKind::Unsupported,
375 "process signalling is not available on this platform",
376 ))
377 }
378 }
379}
380
381#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
382mod tests {
383 use std::{
384 process::{Child, Command},
385 time::{Duration, Instant},
386 };
387
388 use super::*;
389
390 fn spawn_sleep() -> Child {
391 Command::new("sleep")
392 .arg("60")
393 .spawn()
394 .expect("spawn sleep")
395 }
396
397 /// The executable a spawned `sleep` runs, resolved the way `Command` found it.
398 fn sleep_identity() -> FileIdentity {
399 let path = ["/bin/sleep", "/usr/bin/sleep"]
400 .into_iter()
401 .find(|path| Path::new(path).exists())
402 .expect("sleep is installed");
403 file_identity(Path::new(path)).expect("stat sleep")
404 }
405
406 /// Right after `spawn` returns the child may not have finished exec yet,
407 /// and until then it still runs the test binary's image.
408 fn wait_for_executable(process: &Process, expected: FileIdentity) -> Observation {
409 let deadline = Instant::now() + Duration::from_secs(5);
410 loop {
411 let observation = process.observe().expect("child is alive");
412 if observation.executable == Some(expected) || Instant::now() > deadline {
413 return observation;
414 }
415 std::thread::sleep(Duration::from_millis(10));
416 }
417 }
418
419 #[test]
420 fn own_process_is_observable_with_its_own_image() {
421 let process = Process::open(std::process::id())
422 .expect("open own process")
423 .expect("own process exists");
424 let observation = process.observe().expect("own process is alive");
425 let own_image = file_identity(&std::env::current_exe().unwrap()).unwrap();
426 assert_eq!(observation.executable, Some(own_image));
427 assert_eq!(start_time(std::process::id()), Some(observation.start_time));
428 }
429
430 #[test]
431 fn child_start_time_is_stable_and_differs_from_ours() {
432 let mut child = spawn_sleep();
433 let pid = child.id();
434 let process = Process::open(pid).unwrap().unwrap();
435 let observation = wait_for_executable(&process, sleep_identity());
436 assert_eq!(observation.executable, Some(sleep_identity()));
437 assert_eq!(start_time(pid), Some(observation.start_time));
438 child.kill().unwrap();
439 child.wait().unwrap();
440 }
441
442 #[test]
443 fn a_signalled_and_unreaped_child_reads_as_exited() {
444 let mut child = spawn_sleep();
445 let process = Process::open(child.id()).unwrap().unwrap();
446 assert!(process.signal(Signal::Terminate).unwrap());
447 let deadline = Instant::now() + Duration::from_secs(5);
448 while process.observe().is_some() {
449 assert!(Instant::now() < deadline, "child still observed as alive");
450 std::thread::sleep(Duration::from_millis(10));
451 }
452 // Not yet reaped: the pid is still a zombie here, and still reads as exited.
453 assert_eq!(start_time(child.id()), None);
454 child.wait().unwrap();
455 }
456
457 #[test]
458 fn a_reaped_child_cannot_be_opened_or_observed() {
459 let mut child = spawn_sleep();
460 let pid = child.id();
461 child.kill().unwrap();
462 child.wait().unwrap();
463 // The pid could in principle be reused by now; either way it is not the child.
464 if let Some(process) = Process::open(pid).unwrap() {
465 if let Some(observation) = process.observe() {
466 assert_ne!(observation.executable, Some(sleep_identity()));
467 }
468 }
469 }
470
471 /// The macOS fields are read at fixed offsets, so check the value is a
472 /// plausible start time and not some other field: our own process started
473 /// in the past, and not long ago.
474 #[cfg(target_os = "macos")]
475 #[test]
476 fn macos_start_time_is_microseconds_since_the_epoch() {
477 let now = std::time::SystemTime::now()
478 .duration_since(std::time::UNIX_EPOCH)
479 .unwrap()
480 .as_micros() as u64;
481 let started = start_time(std::process::id()).unwrap();
482 assert!(started <= now, "start time {started} is after now {now}");
483 assert!(
484 now - started < 3_600 * 1_000_000,
485 "start time {started} is more than an hour before now {now}"
486 );
487 }
488
489 /// Keeps one core busy for at least `wall` of wall-clock time.
490 /// This thread's CPU time, from the thread CPU clock rather than the
491 /// process-usage API under test.
492 fn thread_cpu_time() -> Duration {
493 let now = rustix::time::clock_gettime(rustix::time::ClockId::ThreadCPUTime);
494 Duration::new(now.tv_sec as u64, now.tv_nsec as u32)
495 }
496
497 /// Spend `cpu` of this thread's CPU time. Measured on CPU time, not wall
498 /// time: on a loaded machine the thread is descheduled for part of any
499 /// wall interval, so a wall-timed loop can do far less work than its
500 /// duration suggests. A generous wall cap keeps a stalled clock from
501 /// hanging the test.
502 fn burn_cpu(cpu: Duration) {
503 let start = thread_cpu_time();
504 let give_up = Instant::now() + Duration::from_secs(60);
505 let mut value = 0u64;
506 while thread_cpu_time().saturating_sub(start) < cpu {
507 assert!(
508 Instant::now() < give_up,
509 "thread CPU clock stopped advancing"
510 );
511 for step in 0..10_000u64 {
512 value = std::hint::black_box(value.wrapping_mul(31).wrapping_add(step));
513 }
514 }
515 std::hint::black_box(value);
516 }
517
518 #[test]
519 fn own_resource_usage_is_present_and_plausible() {
520 // Clean executable pages need not count toward physical footprint, and
521 // nextest runs this case in a fresh process with little private memory.
522 // Touch and retain private pages so the byte/unit check has a known
523 // lower bound instead of assuming a minimum footprint for the binary.
524 let pages = vec![0xa5u8; 8 * 1024 * 1024];
525 std::hint::black_box(&pages);
526 let usage = resource_usage(std::process::id()).expect("own process is readable");
527 assert!(
528 usage.memory_bytes >= pages.len() as u64,
529 "memory {} bytes cannot account for {} touched private bytes",
530 usage.memory_bytes,
531 pages.len()
532 );
533 std::hint::black_box(&pages);
534 assert!(
535 usage.memory_bytes < 64 * 1024 * 1024 * 1024,
536 "memory {} bytes is implausibly large",
537 usage.memory_bytes
538 );
539 #[cfg(target_os = "macos")]
540 assert_eq!(usage.memory_kind, MemoryKind::PhysFootprint);
541 #[cfg(target_os = "linux")]
542 {
543 assert_eq!(usage.memory_kind, MemoryKind::ResidentSet);
544 assert!(usage.swap_bytes.is_some(), "Linux reports VmSwap");
545 }
546 }
547
548 /// CPU time must grow with busy work, and by roughly the amount of work
549 /// done: a reading in the wrong unit (for example Mach ticks taken as
550 /// nanoseconds on Apple silicon, about 24 times too small) grows too, but
551 /// not by enough.
552 #[test]
553 fn own_cpu_time_grows_by_about_the_busy_work_done() {
554 let pid = std::process::id();
555 let total = |usage: ResourceUsage| usage.cpu_user + usage.cpu_system;
556 let before = total(resource_usage(pid).unwrap());
557 let busy = Duration::from_millis(400);
558 burn_cpu(busy);
559 let after = total(resource_usage(pid).unwrap());
560 let grown = after.saturating_sub(before);
561 // This thread alone spent `busy` of CPU time, so the process total
562 // grew by at least that much; other tests' threads only add to it.
563 // The 10% allowance covers tick rounding in the reading, and is far
564 // tighter than the ~24x a unit error would cause.
565 assert!(
566 grown >= busy * 9 / 10,
567 "cpu time grew by {grown:?} over {busy:?} of busy work"
568 );
569 }
570
571 #[test]
572 fn a_child_reads_its_own_usage_not_ours() {
573 let mut child = spawn_sleep();
574 let process = Process::open(child.id()).unwrap().unwrap();
575 wait_for_executable(&process, sleep_identity());
576 let ours = resource_usage(std::process::id()).unwrap();
577 let usage = resource_usage(child.id()).expect("live child is readable");
578 assert!(usage.memory_bytes > 0);
579 assert!(
580 usage.memory_bytes < ours.memory_bytes,
581 "a sleeping child ({} bytes) should be smaller than the test binary ({} bytes)",
582 usage.memory_bytes,
583 ours.memory_bytes
584 );
585 child.kill().unwrap();
586 child.wait().unwrap();
587 }
588
589 #[test]
590 fn an_exited_child_reads_as_unavailable_not_zero() {
591 let mut child = spawn_sleep();
592 let pid = child.id();
593 child.kill().unwrap();
594 // Killed but not reaped: a zombie, which still has a pid.
595 let deadline = Instant::now() + Duration::from_secs(5);
596 while start_time(pid).is_some() {
597 assert!(Instant::now() < deadline, "child still observed as alive");
598 std::thread::sleep(Duration::from_millis(10));
599 }
600 assert_eq!(resource_usage(pid), None, "a zombie reads as unavailable");
601 child.wait().unwrap();
602 // Reaped: the pid names nothing (barring reuse, which would be some
603 // other live process and so still not a reading of zeros).
604 if let Some(usage) = resource_usage(pid) {
605 assert!(usage.memory_bytes > 0, "a reused pid is some live process");
606 }
607 }
608
609 #[test]
610 fn a_pid_with_no_process_reads_as_unavailable() {
611 // Above both kernels' pid limits (Linux caps pid_max at 2^22, macOS at
612 // 99998), so nothing can hold it.
613 assert_eq!(resource_usage(i32::MAX as u32), None);
614 // Not a representable pid at all.
615 assert_eq!(resource_usage(u32::MAX), None);
616 }
617
618 #[cfg(target_os = "linux")]
619 #[test]
620 fn linux_signals_go_through_a_pidfd() {
621 let mut child = spawn_sleep();
622 let process = Process::open(child.id()).unwrap().unwrap();
623 assert!(process.signals_through_pidfd());
624 child.kill().unwrap();
625 child.wait().unwrap();
626 // The pidfd still names the reaped child, so a signal cannot reach anything else.
627 assert!(!process.signal(Signal::Kill).unwrap());
628 }
629}