1use std::{
2 collections::BTreeMap,
3 error::Error,
4 ffi::{CString, OsStr, OsString},
5 fmt::{self, Display, Formatter},
6 fs::File,
7 io::{self, Read},
8 os::{
9 fd::{AsRawFd, OwnedFd, RawFd},
10 unix::ffi::OsStrExt,
11 },
12 thread,
13 time::{Duration, Instant},
14};
15
16use crate::{
17 ChildSignalState, ProcessGroupId, ProcessId, Signal,
18 child_signal::Prepared as PreparedSignalState,
19 descriptor::{Actions as DescriptorActions, Prepared as PreparedDescriptors, close_raw},
20 pipe_cloexec,
21 raw::{last_errno, write_all},
22 signal_process, wait_event_nohang,
23};
24
25const FAILURE_MAGIC: u32 = 0x494d_4d4f;
26const CHILD_FAILURE_EXIT: libc::c_int = 127;
27const CLEANUP_TIMEOUT: Duration = Duration::from_secs(1);
28const CLEANUP_POLL_INTERVAL: Duration = Duration::from_millis(5);
29
30#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
32pub enum ProcessGroup {
33 #[default]
35 Inherit,
36 New,
38 Join(ProcessGroupId),
40}
41
42#[derive(Debug, Clone, Eq, PartialEq)]
44pub enum SupplementaryGroups {
45 Preserve,
47 Set(Vec<libc::gid_t>),
49}
50
51#[derive(Debug, Clone, Eq, PartialEq)]
57pub struct ProcessCredentials {
58 user: libc::uid_t,
59 group: libc::gid_t,
60 supplementary_groups: SupplementaryGroups,
61}
62
63impl ProcessCredentials {
64 #[must_use]
66 pub const fn new(
67 user: libc::uid_t,
68 group: libc::gid_t,
69 supplementary_groups: SupplementaryGroups,
70 ) -> Self {
71 Self {
72 user,
73 group,
74 supplementary_groups,
75 }
76 }
77
78 #[must_use]
80 pub const fn user(&self) -> libc::uid_t {
81 self.user
82 }
83
84 #[must_use]
86 pub const fn group(&self) -> libc::gid_t {
87 self.group
88 }
89
90 #[must_use]
92 pub const fn supplementary_groups(&self) -> &SupplementaryGroups {
93 &self.supplementary_groups
94 }
95}
96
97#[derive(Debug, Clone, Copy, Eq, PartialEq)]
99#[repr(u8)]
100pub enum SpawnStage {
101 Fork = 1,
102 ProcessGroup = 2,
103 CurrentDirectory = 3,
104 DescriptorDuplication = 4,
105 Execute = 5,
106 StartupHandshake = 6,
107 Identity = 7,
108 SignalState = 8,
109}
110
111impl Display for SpawnStage {
112 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
113 let name = match self {
114 Self::Fork => "fork",
115 Self::ProcessGroup => "process-group setup",
116 Self::CurrentDirectory => "working-directory change",
117 Self::DescriptorDuplication => "descriptor mapping",
118 Self::Execute => "execve",
119 Self::StartupHandshake => "startup handshake",
120 Self::Identity => "identity transition",
121 Self::SignalState => "signal-state reset",
122 };
123 formatter.write_str(name)
124 }
125}
126
127#[derive(Debug)]
129pub enum SpawnError {
130 OperatingSystem {
132 stage: SpawnStage,
133 error: io::Error,
134 cleanup_pending: Option<ProcessId>,
135 },
136 TimedOut {
138 timeout: Duration,
139 cleanup_pending: Option<ProcessId>,
140 },
141 InvalidHandshake { cleanup_pending: Option<ProcessId> },
143}
144
145impl SpawnError {
146 fn operating_system(stage: SpawnStage, error: io::Error) -> Self {
147 Self::OperatingSystem {
148 stage,
149 error,
150 cleanup_pending: None,
151 }
152 }
153
154 fn with_cleanup_pending(self, process: Option<ProcessId>) -> Self {
155 match self {
156 Self::OperatingSystem { stage, error, .. } => Self::OperatingSystem {
157 stage,
158 error,
159 cleanup_pending: process,
160 },
161 Self::TimedOut { timeout, .. } => Self::TimedOut {
162 timeout,
163 cleanup_pending: process,
164 },
165 Self::InvalidHandshake { .. } => Self::InvalidHandshake {
166 cleanup_pending: process,
167 },
168 }
169 }
170
171 #[must_use]
178 pub const fn cleanup_pending(&self) -> Option<ProcessId> {
179 match self {
180 Self::OperatingSystem {
181 cleanup_pending, ..
182 }
183 | Self::TimedOut {
184 cleanup_pending, ..
185 }
186 | Self::InvalidHandshake { cleanup_pending } => *cleanup_pending,
187 }
188 }
189}
190
191impl Display for SpawnError {
192 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
193 match self {
194 Self::OperatingSystem { stage, error, .. } => {
195 write!(formatter, "child {stage} failed: {error}")
196 }
197 Self::TimedOut { timeout, .. } => {
198 write!(formatter, "child startup timed out after {timeout:?}")
199 }
200 Self::InvalidHandshake { .. } => {
201 formatter.write_str("child returned an invalid handshake")
202 }
203 }
204 }
205}
206
207impl Error for SpawnError {
208 fn source(&self) -> Option<&(dyn Error + 'static)> {
209 match self {
210 Self::OperatingSystem { error, .. } => Some(error),
211 Self::TimedOut { .. } | Self::InvalidHandshake { .. } => None,
212 }
213 }
214}
215
216#[derive(Debug, Clone, Copy, Eq, PartialEq)]
218pub struct SpawnedChild {
219 process: ProcessId,
220 group: Option<ProcessGroupId>,
221}
222
223impl SpawnedChild {
224 #[must_use]
226 pub const fn process(&self) -> ProcessId {
227 self.process
228 }
229
230 #[must_use]
232 pub const fn process_group(&self) -> Option<ProcessGroupId> {
233 self.group
234 }
235}
236
237#[derive(Debug)]
245pub struct PreparedCommand {
246 program: CString,
247 arguments: Vec<CString>,
248 environment: BTreeMap<OsString, OsString>,
249 current_directory: Option<CString>,
250 process_group: ProcessGroup,
251 credentials: Option<ProcessCredentials>,
252 signal_state: ChildSignalState,
253 descriptors: DescriptorActions,
254}
255
256impl PreparedCommand {
257 pub fn new(program: impl AsRef<OsStr>) -> io::Result<Self> {
263 let program = os_string_to_cstring(program.as_ref(), "program")?;
264 if program.as_bytes().is_empty() {
265 return Err(io::Error::new(
266 io::ErrorKind::InvalidInput,
267 "program must not be empty",
268 ));
269 }
270 Ok(Self {
271 program,
272 arguments: Vec::new(),
273 environment: std::env::vars_os().collect(),
274 current_directory: None,
275 process_group: ProcessGroup::Inherit,
276 credentials: None,
277 signal_state: ChildSignalState::Reset,
278 descriptors: DescriptorActions::default(),
279 })
280 }
281
282 pub fn arg(&mut self, argument: impl AsRef<OsStr>) -> io::Result<&mut Self> {
288 self.arguments
289 .push(os_string_to_cstring(argument.as_ref(), "argument")?);
290 Ok(self)
291 }
292
293 pub fn clear_environment(&mut self) -> &mut Self {
295 self.environment.clear();
296 self
297 }
298
299 pub fn environment(
306 &mut self,
307 key: impl AsRef<OsStr>,
308 value: impl AsRef<OsStr>,
309 ) -> io::Result<&mut Self> {
310 let key = key.as_ref();
311 let key_bytes = key.as_bytes();
312 if key_bytes.is_empty() || key_bytes.contains(&b'=') || key_bytes.contains(&0) {
313 return Err(io::Error::new(
314 io::ErrorKind::InvalidInput,
315 "environment key must be nonempty and contain neither '=' nor NUL",
316 ));
317 }
318 if value.as_ref().as_bytes().contains(&0) {
319 return Err(io::Error::new(
320 io::ErrorKind::InvalidInput,
321 "environment value must not contain NUL",
322 ));
323 }
324 self.environment
325 .insert(key.to_os_string(), value.as_ref().to_os_string());
326 Ok(self)
327 }
328
329 pub fn current_directory(&mut self, path: impl AsRef<OsStr>) -> io::Result<&mut Self> {
335 self.current_directory = Some(os_string_to_cstring(path.as_ref(), "working directory")?);
336 Ok(self)
337 }
338
339 pub fn process_group(&mut self, group: ProcessGroup) -> &mut Self {
341 self.process_group = group;
342 self
343 }
344
345 pub fn credentials(&mut self, credentials: ProcessCredentials) -> &mut Self {
347 self.credentials = Some(credentials);
348 self
349 }
350
351 pub fn signal_state(&mut self, state: ChildSignalState) -> &mut Self {
353 self.signal_state = state;
354 self
355 }
356
357 pub fn map_descriptor(&mut self, source: OwnedFd, target: RawFd) -> io::Result<&mut Self> {
367 self.descriptors.map(source, target)?;
368 Ok(self)
369 }
370
371 pub fn inherit_descriptor(&mut self, descriptor: OwnedFd) -> io::Result<&mut Self> {
377 self.descriptors.inherit(descriptor)?;
378 Ok(self)
379 }
380
381 pub fn close_descriptor(&mut self, descriptor: RawFd) -> io::Result<&mut Self> {
391 self.descriptors.close(descriptor)?;
392 Ok(self)
393 }
394
395 pub fn spawn(self, startup_timeout: Duration) -> Result<SpawnedChild, SpawnError> {
406 SpawnPreparation::new(self)?.spawn(startup_timeout)
407 }
408}
409
410struct SpawnPreparation {
411 program: CString,
412 _arguments: Vec<CString>,
413 argument_pointers: Vec<*const libc::c_char>,
414 _environment: Vec<CString>,
415 environment_pointers: Vec<*const libc::c_char>,
416 current_directory: Option<CString>,
417 process_group: ProcessGroup,
418 credentials: Option<ProcessCredentials>,
419 signal_state: Option<PreparedSignalState>,
420 descriptors: PreparedDescriptors,
421 status_reader: OwnedFd,
422 status_writer: OwnedFd,
423}
424
425impl SpawnPreparation {
426 fn new(command: PreparedCommand) -> Result<Self, SpawnError> {
427 let pipe = pipe_cloexec()
428 .map_err(|error| SpawnError::operating_system(SpawnStage::StartupHandshake, error))?;
429 let (status_reader, initial_status_writer) = pipe.into_parts();
430 let (descriptors, status_writer) = command
431 .descriptors
432 .prepare(initial_status_writer)
433 .map_err(|error| {
434 SpawnError::operating_system(SpawnStage::DescriptorDuplication, error)
435 })?;
436
437 let environment = materialize_environment(command.environment)
438 .map_err(|error| SpawnError::operating_system(SpawnStage::Execute, error))?;
439 let mut arguments = Vec::with_capacity(command.arguments.len() + 1);
440 arguments.push(command.program.clone());
441 arguments.extend(command.arguments);
442 let argument_pointers = cstring_pointers(&arguments);
443 let environment_pointers = cstring_pointers(&environment);
444
445 validate_credentials(command.credentials.as_ref())
446 .map_err(|error| SpawnError::operating_system(SpawnStage::Identity, error))?;
447 let signal_state = PreparedSignalState::new(command.signal_state)
448 .map_err(|error| SpawnError::operating_system(SpawnStage::SignalState, error))?;
449
450 Ok(Self {
451 program: command.program,
452 _arguments: arguments,
453 argument_pointers,
454 _environment: environment,
455 environment_pointers,
456 current_directory: command.current_directory,
457 process_group: command.process_group,
458 credentials: command.credentials,
459 signal_state,
460 descriptors,
461 status_reader,
462 status_writer,
463 })
464 }
465
466 fn spawn(self, startup_timeout: Duration) -> Result<SpawnedChild, SpawnError> {
467 let raw_child = unsafe { libc::fork() };
470 if raw_child == -1 {
471 return Err(SpawnError::operating_system(
472 SpawnStage::Fork,
473 io::Error::last_os_error(),
474 ));
475 }
476 if raw_child == 0 {
477 self.run_child();
478 }
479
480 let process = ProcessId::new(raw_child).ok_or_else(|| {
481 SpawnError::operating_system(
482 SpawnStage::Fork,
483 io::Error::new(
484 io::ErrorKind::InvalidData,
485 "fork returned a nonpositive child PID",
486 ),
487 )
488 })?;
489 drop(self.status_writer);
490 parent_set_process_group(process, self.process_group);
491
492 let group = match self.process_group {
493 ProcessGroup::Inherit => None,
494 ProcessGroup::New => ProcessGroupId::new(process.get()),
495 ProcessGroup::Join(group) => Some(group),
496 };
497 let result = read_startup_handshake(self.status_reader, startup_timeout);
498 match result {
499 Ok(()) => Ok(SpawnedChild { process, group }),
500 Err(error) => {
501 let cleanup_pending = terminate_and_reap_bounded(process);
502 Err(error.with_cleanup_pending(cleanup_pending))
503 }
504 }
505 }
506
507 fn run_child(&self) -> ! {
508 close_raw(self.status_reader.as_raw_fd());
509
510 if let Some(signal_state) = &self.signal_state
511 && let Err(errno) = signal_state.apply_in_child()
512 {
513 child_fail(
514 self.status_writer.as_raw_fd(),
515 SpawnStage::SignalState,
516 errno,
517 );
518 }
519
520 if let Err(errno) = child_set_process_group(self.process_group) {
521 child_fail(
522 self.status_writer.as_raw_fd(),
523 SpawnStage::ProcessGroup,
524 errno,
525 );
526 }
527 if let Some(directory) = &self.current_directory {
528 if unsafe { libc::chdir(directory.as_ptr()) } == -1 {
530 child_fail(
531 self.status_writer.as_raw_fd(),
532 SpawnStage::CurrentDirectory,
533 last_errno(),
534 );
535 }
536 }
537 if let Err(errno) = self.descriptors.apply_in_child() {
538 child_fail(
539 self.status_writer.as_raw_fd(),
540 SpawnStage::DescriptorDuplication,
541 errno,
542 );
543 }
544
545 if let Some(credentials) = &self.credentials
546 && let Err(errno) = apply_credentials(credentials)
547 {
548 child_fail(self.status_writer.as_raw_fd(), SpawnStage::Identity, errno);
549 }
550
551 unsafe {
555 libc::execve(
556 self.program.as_ptr(),
557 self.argument_pointers.as_ptr(),
558 self.environment_pointers.as_ptr(),
559 );
560 }
561 child_fail(
562 self.status_writer.as_raw_fd(),
563 SpawnStage::Execute,
564 last_errno(),
565 );
566 }
567}
568
569#[repr(C)]
570#[derive(Clone, Copy)]
571struct FailureRecord {
572 magic: u32,
573 errno: i32,
574 stage: u8,
575 reserved: [u8; 3],
576}
577
578fn os_string_to_cstring(value: &OsStr, label: &str) -> io::Result<CString> {
579 CString::new(value.as_bytes()).map_err(|_| {
580 io::Error::new(
581 io::ErrorKind::InvalidInput,
582 format!("{label} must not contain NUL"),
583 )
584 })
585}
586
587fn materialize_environment(environment: BTreeMap<OsString, OsString>) -> io::Result<Vec<CString>> {
588 environment
589 .into_iter()
590 .map(|(key, value)| {
591 let mut entry = Vec::with_capacity(key.as_bytes().len() + value.as_bytes().len() + 1);
592 entry.extend_from_slice(key.as_bytes());
593 entry.push(b'=');
594 entry.extend_from_slice(value.as_bytes());
595 CString::new(entry).map_err(|_| {
596 io::Error::new(io::ErrorKind::InvalidInput, "environment contains NUL")
597 })
598 })
599 .collect()
600}
601
602fn cstring_pointers(strings: &[CString]) -> Vec<*const libc::c_char> {
603 let mut pointers = Vec::with_capacity(strings.len() + 1);
604 pointers.extend(strings.iter().map(|value| value.as_ptr()));
605 pointers.push(std::ptr::null());
606 pointers
607}
608
609fn parent_set_process_group(process: ProcessId, group: ProcessGroup) {
610 let target = match group {
611 ProcessGroup::Inherit => return,
612 ProcessGroup::New => process.get(),
613 ProcessGroup::Join(group) => group.get(),
614 };
615 loop {
616 if unsafe { libc::setpgid(process.get(), target) } == 0 {
618 return;
619 }
620 let error = io::Error::last_os_error();
621 if error.kind() == io::ErrorKind::Interrupted {
622 continue;
623 }
624 return;
627 }
628}
629
630fn read_startup_handshake(descriptor: OwnedFd, timeout: Duration) -> Result<(), SpawnError> {
631 let deadline = Instant::now()
632 .checked_add(timeout)
633 .unwrap_or_else(Instant::now);
634 let mut poll_descriptor = libc::pollfd {
635 fd: descriptor.as_raw_fd(),
636 events: libc::POLLIN | libc::POLLHUP,
637 revents: 0,
638 };
639 loop {
640 let remaining = deadline.saturating_duration_since(Instant::now());
641 let timeout_millis = duration_to_poll_timeout(remaining);
642 let result = unsafe { libc::poll(&raw mut poll_descriptor, 1, timeout_millis) };
644 if result > 0 {
645 break;
646 }
647 if result == 0 {
648 return Err(SpawnError::TimedOut {
649 timeout,
650 cleanup_pending: None,
651 });
652 }
653 let error = io::Error::last_os_error();
654 if error.kind() != io::ErrorKind::Interrupted {
655 return Err(SpawnError::operating_system(
656 SpawnStage::StartupHandshake,
657 error,
658 ));
659 }
660 }
661
662 let mut file = File::from(descriptor);
663 let mut bytes = [0_u8; size_of::<FailureRecord>()];
664 let mut read = 0;
665 while read < bytes.len() {
666 let remaining = bytes.get_mut(read..).ok_or(SpawnError::InvalidHandshake {
667 cleanup_pending: None,
668 })?;
669 match file.read(remaining) {
670 Ok(0) if read == 0 => return Ok(()),
671 Ok(0) => {
672 return Err(SpawnError::InvalidHandshake {
673 cleanup_pending: None,
674 });
675 }
676 Ok(count) => read += count,
677 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
678 Err(error) => {
679 return Err(SpawnError::operating_system(
680 SpawnStage::StartupHandshake,
681 error,
682 ));
683 }
684 }
685 }
686
687 let record = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<FailureRecord>()) };
690 if record.magic != FAILURE_MAGIC {
691 return Err(SpawnError::InvalidHandshake {
692 cleanup_pending: None,
693 });
694 }
695 let stage = spawn_stage_from_wire(record.stage).ok_or(SpawnError::InvalidHandshake {
696 cleanup_pending: None,
697 })?;
698 Err(SpawnError::operating_system(
699 stage,
700 io::Error::from_raw_os_error(record.errno),
701 ))
702}
703
704fn duration_to_poll_timeout(duration: Duration) -> libc::c_int {
705 if duration.is_zero() {
706 return 0;
707 }
708 let millis = duration.as_millis().max(1);
709 libc::c_int::try_from(millis).unwrap_or(libc::c_int::MAX)
710}
711
712const fn spawn_stage_from_wire(stage: u8) -> Option<SpawnStage> {
713 match stage {
714 2 => Some(SpawnStage::ProcessGroup),
715 3 => Some(SpawnStage::CurrentDirectory),
716 4 => Some(SpawnStage::DescriptorDuplication),
717 5 => Some(SpawnStage::Execute),
718 7 => Some(SpawnStage::Identity),
719 8 => Some(SpawnStage::SignalState),
720 _ => None,
721 }
722}
723
724fn validate_credentials(credentials: Option<&ProcessCredentials>) -> io::Result<()> {
725 let Some(ProcessCredentials {
726 supplementary_groups: SupplementaryGroups::Set(groups),
727 ..
728 }) = credentials
729 else {
730 return Ok(());
731 };
732 let maximum = loop {
733 let result = unsafe { libc::sysconf(libc::_SC_NGROUPS_MAX) };
735 if result >= 0 {
736 break usize::try_from(result).unwrap_or(usize::MAX);
737 }
738 let error = io::Error::last_os_error();
739 if error.kind() != io::ErrorKind::Interrupted {
740 return Err(error);
741 }
742 };
743 if groups.len() > maximum {
744 return Err(io::Error::new(
745 io::ErrorKind::InvalidInput,
746 "supplementary group count exceeds the operating-system limit",
747 ));
748 }
749 Ok(())
750}
751
752fn terminate_and_reap_bounded(process: ProcessId) -> Option<ProcessId> {
753 let _ = signal_process(process, Signal::KILL);
754 let deadline = Instant::now() + CLEANUP_TIMEOUT;
755 loop {
756 match wait_event_nohang(process) {
757 Ok(Some(event)) if event.is_terminal() => return None,
758 Err(error) if error.raw_os_error() == Some(libc::ECHILD) => return None,
759 Err(_) => return Some(process),
760 Ok(Some(_) | None) => {}
761 }
762 if Instant::now() >= deadline {
763 return Some(process);
764 }
765 thread::sleep(CLEANUP_POLL_INTERVAL);
766 }
767}
768
769fn child_set_process_group(group: ProcessGroup) -> Result<(), libc::c_int> {
770 let target = match group {
771 ProcessGroup::Inherit => return Ok(()),
772 ProcessGroup::New => 0,
773 ProcessGroup::Join(group) => group.get(),
774 };
775 loop {
776 if unsafe { libc::setpgid(0, target) } == 0 {
778 return Ok(());
779 }
780 let errno = last_errno();
781 if errno != libc::EINTR {
782 return Err(errno);
783 }
784 }
785}
786
787fn apply_credentials(credentials: &ProcessCredentials) -> Result<(), libc::c_int> {
788 if let SupplementaryGroups::Set(groups) = &credentials.supplementary_groups {
789 loop {
790 if set_supplementary_groups(groups) == 0 {
791 break;
792 }
793 let errno = last_errno();
794 if errno != libc::EINTR {
795 return Err(errno);
796 }
797 }
798 }
799 loop {
800 if unsafe { libc::setgid(credentials.group) } == 0 {
802 break;
803 }
804 let errno = last_errno();
805 if errno != libc::EINTR {
806 return Err(errno);
807 }
808 }
809 loop {
810 if unsafe { libc::setuid(credentials.user) } == 0 {
812 return Ok(());
813 }
814 let errno = last_errno();
815 if errno != libc::EINTR {
816 return Err(errno);
817 }
818 }
819}
820
821#[cfg(any(target_os = "linux", target_os = "android"))]
822fn set_supplementary_groups(groups: &[libc::gid_t]) -> libc::c_int {
823 unsafe { libc::setgroups(groups.len(), groups.as_ptr()) }
825}
826
827#[cfg(not(any(target_os = "linux", target_os = "android")))]
828fn set_supplementary_groups(groups: &[libc::gid_t]) -> libc::c_int {
829 let Ok(count) = libc::c_int::try_from(groups.len()) else {
830 return -1;
831 };
832 unsafe { libc::setgroups(count, groups.as_ptr()) }
834}
835
836fn child_fail(descriptor: RawFd, stage: SpawnStage, errno: libc::c_int) -> ! {
837 let record = FailureRecord {
838 magic: FAILURE_MAGIC,
839 errno,
840 stage: stage as u8,
841 reserved: [0; 3],
842 };
843 let bytes = unsafe {
845 std::slice::from_raw_parts((&raw const record).cast::<u8>(), size_of::<FailureRecord>())
846 };
847 let _ = write_all(descriptor, bytes);
848 unsafe { libc::_exit(CHILD_FAILURE_EXIT) }
850}
851
852#[cfg(test)]
853mod tests {
854 use super::{
855 SpawnError, SpawnStage, duration_to_poll_timeout, read_startup_handshake,
856 spawn_stage_from_wire,
857 };
858 use crate::pipe_cloexec;
859 use std::{error::Error, thread, time::Duration};
860
861 #[test]
862 fn wire_stages_reject_parent_only_values() {
863 assert_eq!(spawn_stage_from_wire(2), Some(SpawnStage::ProcessGroup));
864 assert_eq!(spawn_stage_from_wire(5), Some(SpawnStage::Execute));
865 assert_eq!(spawn_stage_from_wire(7), Some(SpawnStage::Identity));
866 assert_eq!(spawn_stage_from_wire(8), Some(SpawnStage::SignalState));
867 assert_eq!(spawn_stage_from_wire(1), None);
868 assert_eq!(spawn_stage_from_wire(255), None);
869 }
870
871 #[test]
872 fn poll_timeout_rounds_up_and_saturates() {
873 assert_eq!(duration_to_poll_timeout(Duration::ZERO), 0);
874 assert_eq!(duration_to_poll_timeout(Duration::from_nanos(1)), 1);
875 assert_eq!(
876 duration_to_poll_timeout(Duration::from_secs(u64::MAX)),
877 libc::c_int::MAX
878 );
879 }
880
881 #[test]
882 fn startup_handshake_obeys_timeout() -> Result<(), Box<dyn Error>> {
883 let pipe = pipe_cloexec()?;
884 let (reader, writer) = pipe.into_parts();
885 let holder = thread::spawn(move || {
886 thread::sleep(Duration::from_millis(25));
887 drop(writer);
888 });
889 let Err(error) = read_startup_handshake(reader, Duration::from_millis(1)) else {
890 return Err(std::io::Error::other("open handshake did not time out").into());
891 };
892 assert!(matches!(error, SpawnError::TimedOut { .. }));
893 holder
894 .join()
895 .map_err(|_| std::io::Error::other("handshake holder panicked"))?;
896 Ok(())
897 }
898}