1#[cfg(any(target_os = "linux", test))]
36use crate::network_namespace::{decode_step_record, STEP_RECORD_LEN};
37use crate::network_namespace::{
38 errno_label, errno_words, EgressNamespaceFailure, NamespaceAttemptError,
39};
40use crate::sealed::{ProfileAttachment, ProfileGap, UsernsGrant};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum MountStep {
48 JoinSession = 1,
49 UnshareMount = 2,
50 MakePrivate = 3,
51 BindTmp = 4,
52 UnshareUserMount = 5,
53 IdentityMaps = 6,
54 MakePrivateNested = 7,
55 Tmpfs = 8,
56 PivotPrepare = 9,
57 PivotRoot = 10,
58 DetachOldRoot = 11,
59 JoinPidNamespace = 12,
61 ForkIntoPidNamespace = 13,
63 MountProc = 14,
65}
66
67impl MountStep {
68 #[cfg(any(target_os = "linux", test))]
70 const ALL: [MountStep; 14] = [
71 Self::JoinSession,
72 Self::UnshareMount,
73 Self::MakePrivate,
74 Self::BindTmp,
75 Self::UnshareUserMount,
76 Self::IdentityMaps,
77 Self::MakePrivateNested,
78 Self::Tmpfs,
79 Self::PivotPrepare,
80 Self::PivotRoot,
81 Self::DetachOldRoot,
82 Self::JoinPidNamespace,
83 Self::ForkIntoPidNamespace,
84 Self::MountProc,
85 ];
86
87 #[cfg(any(target_os = "linux", test))]
89 fn from_code(code: u32) -> Option<Self> {
90 Self::ALL.into_iter().find(|step| *step as u32 == code)
91 }
92
93 #[must_use]
95 pub fn describe(self) -> &'static str {
96 match self {
97 Self::JoinSession => "joining the session's user and network namespaces (setns)",
98 Self::UnshareMount => "unshare(CLONE_NEWNS)",
99 Self::MakePrivate => "making / MS_REC|MS_PRIVATE",
100 Self::BindTmp => "bind-mounting over /tmp",
101 Self::UnshareUserMount => {
102 "unshare(CLONE_NEWUSER | CLONE_NEWNS) inside the session's user namespace"
103 }
104 Self::IdentityMaps => "writing uid_map/gid_map",
105 Self::MakePrivateNested => "making / MS_REC|MS_PRIVATE in the nested namespace",
106 Self::Tmpfs => "mounting the composed root's tmpfs",
107 Self::PivotPrepare => "creating the old root's parking directory and entering the root",
108 Self::PivotRoot => "pivot_root(2)",
109 Self::DetachOldRoot => "detaching the old root",
110 Self::JoinPidNamespace => "joining the session's PID namespace (setns)",
111 Self::ForkIntoPidNamespace => "forking into the session's PID namespace",
112 Self::MountProc => "mounting proc over /proc",
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum MountAttemptError {
120 Step { step: MountStep, errno: i32 },
122 Rehearsal(String),
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum MountAttempt {
130 Created,
132 Failed(MountAttemptError),
134 NotAttempted,
137}
138
139impl MountAttempt {
140 #[must_use]
143 pub fn failure_summary(&self) -> Option<String> {
144 match self {
145 MountAttempt::Failed(MountAttemptError::Step { step, errno }) => Some(format!(
146 "{} failed with {}",
147 step.describe(),
148 errno_label(*errno)
149 )),
150 MountAttempt::Failed(MountAttemptError::Rehearsal(detail)) => Some(detail.clone()),
151 MountAttempt::Created | MountAttempt::NotAttempted => None,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct NamespaceReading {
166 pub network: Result<(), NamespaceAttemptError>,
168 pub mount: MountAttempt,
171 pub sealed_root: MountAttempt,
174 pub private_proc: MountAttempt,
177 pub userns_grant: UsernsGrant,
180 pub attachment: ProfileAttachment,
183 pub profile_gap: ProfileGap,
186}
187
188impl NamespaceReading {
189 #[must_use]
191 pub fn not_linux() -> Self {
192 Self {
193 network: Ok(()),
194 mount: MountAttempt::NotAttempted,
195 sealed_root: MountAttempt::NotAttempted,
196 private_proc: MountAttempt::NotAttempted,
197 userns_grant: UsernsGrant::default(),
198 attachment: ProfileAttachment::NotReported,
199 profile_gap: ProfileGap::Undetermined,
200 }
201 }
202
203 #[must_use]
206 pub fn egress_namespace_failure(&self) -> Option<EgressNamespaceFailure> {
207 crate::network_namespace::egress_namespace_failure(
208 cfg!(target_os = "linux"),
209 self.network.clone(),
210 self.userns_grant,
211 self.attachment.clone(),
212 self.profile_gap.clone(),
213 )
214 }
215
216 #[must_use]
219 pub fn mount_namespace_created(&self) -> bool {
220 self.network.is_ok() && self.mount == MountAttempt::Created
221 }
222
223 #[must_use]
226 pub fn private_proc_created(&self) -> bool {
227 self.network.is_ok() && self.private_proc == MountAttempt::Created
228 }
229
230 #[must_use]
233 pub fn sealed_root_created(&self) -> bool {
234 self.network.is_ok() && self.sealed_root == MountAttempt::Created
235 }
236
237 #[must_use]
242 pub fn user_mount_namespace_usable(&self) -> bool {
243 self.network.is_ok()
244 && match &self.sealed_root {
245 MountAttempt::Created => true,
246 MountAttempt::Failed(MountAttemptError::Step { step, .. }) => matches!(
247 step,
248 MountStep::Tmpfs
249 | MountStep::PivotPrepare
250 | MountStep::PivotRoot
251 | MountStep::DetachOldRoot
252 ),
253 MountAttempt::Failed(MountAttemptError::Rehearsal(_))
254 | MountAttempt::NotAttempted => false,
255 }
256 }
257
258 #[must_use]
261 pub fn first_failure(&self) -> Option<String> {
262 if let Some(failure) = self.egress_namespace_failure() {
263 return Some(failure.summary());
264 }
265 self.sealed_root
266 .failure_summary()
267 .or_else(|| self.mount.failure_summary())
268 }
269}
270
271pub(crate) fn probe_failure_sentence(failed_call: &str, errno: i32) -> String {
274 format!(
275 "The probe's {failed_call} failed with {} ({}).",
276 errno_label(errno),
277 errno_words(errno)
278 )
279}
280
281#[cfg(target_os = "linux")]
286pub(crate) fn probe_namespaces() -> NamespaceReading {
287 use crate::network_namespace::{create_session_netns, CapsuleNetnsPlan};
288
289 let (network, mount, sealed_root, private_proc) =
290 match create_session_netns(&CapsuleNetnsPlan::resolve(Vec::new())) {
291 Err(error) => (
292 Err(error),
293 MountAttempt::NotAttempted,
294 MountAttempt::NotAttempted,
295 MountAttempt::NotAttempted,
296 ),
297 Ok(created) => {
298 let (user_fd, net_fd) = created.namespace_handles();
299 let pid_fd = created.pid_namespace_handle();
300 let handles = linux::Handles {
301 user_fd,
302 net_fd,
303 pid_fd,
304 };
305 let mount = linux::rehearse(linux::Rehearsal::PrivateTmp, handles);
306 let sealed_root = linux::rehearse(linux::Rehearsal::SealedRoot, handles);
307 let private_proc = linux::rehearse(linux::Rehearsal::PrivateProc, handles);
308 drop(created);
310 (Ok(()), mount, sealed_root, private_proc)
311 }
312 };
313 NamespaceReading {
314 network,
315 mount,
316 sealed_root,
317 private_proc,
318 userns_grant: crate::sealed::userns_grant(),
319 attachment: crate::sealed::inspect_profile_attachment(),
320 profile_gap: crate::sealed::inspect_profile_gap(),
321 }
322}
323
324#[cfg(not(target_os = "linux"))]
325pub(crate) fn probe_namespaces() -> NamespaceReading {
326 NamespaceReading::not_linux()
327}
328
329#[cfg(any(target_os = "linux", test))]
332fn decode_record(record: &[u8; STEP_RECORD_LEN]) -> Option<MountAttempt> {
333 let (code, errno) = decode_step_record(record);
334 if code == 0 {
335 return Some(MountAttempt::Created);
336 }
337 MountStep::from_code(code)
338 .map(|step| MountAttempt::Failed(MountAttemptError::Step { step, errno }))
339}
340
341#[cfg(target_os = "linux")]
342#[allow(unsafe_code)]
343mod linux {
344 use std::ffi::CString;
345 use std::io;
346 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
347 use std::os::unix::ffi::OsStrExt;
348
349 use super::{decode_record, MountAttempt, MountAttemptError, MountStep, STEP_RECORD_LEN};
350 use crate::network_namespace::encode_step_record;
351 use crate::sealed::{
352 make_dumpable_for_map_writes, restore_dumpable, write_decimal_map, OLD_ROOT_NAME_C,
353 OLD_ROOT_PIVOTED_C, SEALED_ROOT_BASE_CANDIDATES,
354 };
355
356 #[derive(Debug, Clone, Copy)]
358 pub(super) enum Rehearsal {
359 PrivateTmp,
362 SealedRoot,
364 PrivateProc,
367 }
368
369 impl Rehearsal {
370 fn name(self) -> &'static str {
371 match self {
372 Rehearsal::PrivateTmp => "mount rehearsal",
373 Rehearsal::SealedRoot => "sealed-root rehearsal",
374 Rehearsal::PrivateProc => "private-procfs rehearsal",
375 }
376 }
377 }
378
379 #[derive(Debug, Clone, Copy)]
381 pub(super) struct Handles {
382 pub(super) user_fd: RawFd,
383 pub(super) net_fd: RawFd,
384 pub(super) pid_fd: RawFd,
385 }
386
387 struct Paths {
389 tmp: CString,
391 bases: Vec<(CString, CString)>,
393 uid: u32,
394 gid: u32,
395 }
396
397 impl Paths {
398 fn resolve() -> Result<Self, String> {
399 let tmp = std::fs::canonicalize("/tmp").unwrap_or_else(|_| "/tmp".into());
400 let tmp = CString::new(tmp.as_os_str().as_bytes())
401 .map_err(|_| "the canonical /tmp contains a NUL byte".to_string())?;
402 let mut bases = Vec::with_capacity(SEALED_ROOT_BASE_CANDIDATES.len());
403 for base in SEALED_ROOT_BASE_CANDIDATES {
404 let parked = format!(
405 "{base}/{}",
406 OLD_ROOT_NAME_C.to_str().expect("the parking name is ASCII")
407 );
408 bases.push((
409 CString::new(*base).expect("candidates carry no NUL"),
410 CString::new(parked).expect("candidates carry no NUL"),
411 ));
412 }
413 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
417 Ok(Self {
418 tmp,
419 bases,
420 uid,
421 gid,
422 })
423 }
424 }
425
426 pub(super) fn rehearse(rehearsal: Rehearsal, handles: Handles) -> MountAttempt {
429 let failed = |detail: String| MountAttempt::Failed(MountAttemptError::Rehearsal(detail));
430 let paths = match Paths::resolve() {
431 Ok(paths) => paths,
432 Err(detail) => {
433 return failed(format!("the {} could not run: {detail}", rehearsal.name()))
434 }
435 };
436
437 let mut pipe = [0i32; 2];
438 if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) } != 0 {
440 return failed(format!(
441 "pipe2() for the {} failed: {}",
442 rehearsal.name(),
443 io::Error::last_os_error()
444 ));
445 }
446 let (read_end, write_end) =
449 unsafe { (OwnedFd::from_raw_fd(pipe[0]), OwnedFd::from_raw_fd(pipe[1])) };
450 let write_fd = write_end.as_raw_fd();
451
452 let pid = unsafe { libc::fork() };
458 if pid < 0 {
459 return failed(format!(
460 "fork() for the {} failed: {}",
461 rehearsal.name(),
462 io::Error::last_os_error()
463 ));
464 }
465 if pid == 0 {
466 unsafe {
469 let record = match run(rehearsal, &paths, handles, write_fd) {
470 Ok(()) => encode_step_record(0, 0),
471 Err((step, errno)) => encode_step_record(step as u32, errno),
472 };
473 libc::write(write_fd, record.as_ptr().cast(), record.len());
474 libc::_exit(0);
475 }
476 }
477 drop(write_end);
478
479 let status = match crate::network_namespace::reap_child(pid) {
480 Ok(status) => status,
481 Err(error) => {
482 return failed(format!(
483 "waiting for the {} failed: {error}",
484 rehearsal.name()
485 ))
486 }
487 };
488 let mut record = [0u8; STEP_RECORD_LEN];
491 let read = unsafe {
493 libc::read(
494 read_end.as_raw_fd(),
495 record.as_mut_ptr().cast(),
496 STEP_RECORD_LEN,
497 )
498 };
499 if read == STEP_RECORD_LEN as isize {
500 if let Some(attempt) = decode_record(&record) {
501 return attempt;
502 }
503 }
504 failed(format!(
505 "the {} exited without reporting ({status})",
506 rehearsal.name()
507 ))
508 }
509
510 unsafe fn run(
516 rehearsal: Rehearsal,
517 paths: &Paths,
518 handles: Handles,
519 report_fd: RawFd,
520 ) -> Result<(), (MountStep, i32)> {
521 unsafe {
523 let errno = || *libc::__errno_location();
524 if let Err(error) =
526 crate::network_namespace::join_session_netns(handles.user_fd, handles.net_fd)
527 {
528 return Err((MountStep::JoinSession, error.raw_os_error().unwrap_or(0)));
529 }
530 match rehearsal {
531 Rehearsal::PrivateProc => {
534 if libc::setns(handles.pid_fd, libc::CLONE_NEWPID) != 0 {
535 return Err((MountStep::JoinPidNamespace, errno()));
536 }
537 let member = libc::fork();
538 if member < 0 {
539 return Err((MountStep::ForkIntoPidNamespace, errno()));
540 }
541 if member == 0 {
542 let record = match crate::proc_view::mount_private_proc(true) {
544 Ok(_) => encode_step_record(0, 0),
545 Err((step, step_errno)) => {
546 let step = match step {
547 crate::proc_view::ProcMountStep::UnshareMount => {
548 MountStep::UnshareMount
549 }
550 crate::proc_view::ProcMountStep::MakePrivate => {
551 MountStep::MakePrivate
552 }
553 crate::proc_view::ProcMountStep::MountProc
554 | crate::proc_view::ProcMountStep::OpenProc => {
555 MountStep::MountProc
556 }
557 };
558 encode_step_record(step as u32, step_errno)
559 }
560 };
561 libc::write(report_fd, record.as_ptr().cast(), record.len());
562 libc::_exit(0);
563 }
564 let mut status = 0;
565 loop {
566 let waited = libc::waitpid(member, &mut status, 0);
567 if waited == member {
568 break;
569 }
570 if waited < 0 && errno() == libc::EINTR {
571 continue;
572 }
573 return Err((MountStep::ForkIntoPidNamespace, errno()));
574 }
575 if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
576 libc::_exit(0);
578 }
579 return Err((MountStep::ForkIntoPidNamespace, 0));
580 }
581 Rehearsal::PrivateTmp => {
585 if libc::unshare(libc::CLONE_NEWNS) != 0 {
586 return Err((MountStep::UnshareMount, errno()));
587 }
588 if make_private() != 0 {
589 return Err((MountStep::MakePrivate, errno()));
590 }
591 if libc::mount(
592 paths.tmp.as_ptr(),
593 paths.tmp.as_ptr(),
594 std::ptr::null(),
595 libc::MS_BIND | libc::MS_REC,
596 std::ptr::null(),
597 ) != 0
598 {
599 return Err((MountStep::BindTmp, errno()));
600 }
601 }
602 Rehearsal::SealedRoot => {
605 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
606 return Err((MountStep::UnshareUserMount, errno()));
607 }
608 let previous = make_dumpable_for_map_writes();
611 let _ = write_decimal_map(c"/proc/self/setgroups", None, 0);
612 let mapped =
613 write_decimal_map(c"/proc/self/uid_map", Some(paths.uid), paths.uid)
614 .is_ok()
615 && write_decimal_map(c"/proc/self/gid_map", Some(paths.gid), paths.gid)
616 .is_ok();
617 let map_errno = errno();
618 restore_dumpable(previous);
619 if !mapped {
620 return Err((MountStep::IdentityMaps, map_errno));
621 }
622 if make_private() != 0 {
623 return Err((MountStep::MakePrivateNested, errno()));
624 }
625 let mut based = None;
628 let mut tmpfs_errno = 0;
629 for (base, parked) in &paths.bases {
630 if libc::mount(
631 c"tmpfs".as_ptr(),
632 base.as_ptr(),
633 c"tmpfs".as_ptr(),
634 libc::MS_NOSUID | libc::MS_NODEV,
635 c"mode=0755".as_ptr().cast(),
636 ) == 0
637 {
638 based = Some((base, parked));
639 break;
640 }
641 tmpfs_errno = errno();
642 }
643 let Some((base, parked)) = based else {
644 return Err((MountStep::Tmpfs, tmpfs_errno));
645 };
646 if libc::mkdir(parked.as_ptr(), 0o700) != 0 {
647 return Err((MountStep::PivotPrepare, errno()));
648 }
649 if libc::chdir(base.as_ptr()) != 0 {
650 return Err((MountStep::PivotPrepare, errno()));
651 }
652 if libc::syscall(
653 libc::SYS_pivot_root,
654 c".".as_ptr(),
655 OLD_ROOT_NAME_C.as_ptr(),
656 ) != 0
657 {
658 return Err((MountStep::PivotRoot, errno()));
659 }
660 if libc::chdir(c"/".as_ptr()) != 0 {
661 return Err((MountStep::PivotRoot, errno()));
662 }
663 if libc::umount2(OLD_ROOT_PIVOTED_C.as_ptr(), libc::MNT_DETACH) != 0 {
664 return Err((MountStep::DetachOldRoot, errno()));
665 }
666 }
667 }
668 Ok(())
669 }
670 }
671
672 unsafe fn make_private() -> libc::c_int {
677 unsafe {
679 libc::mount(
680 std::ptr::null(),
681 c"/".as_ptr(),
682 std::ptr::null(),
683 libc::MS_REC | libc::MS_PRIVATE,
684 std::ptr::null(),
685 )
686 }
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use crate::network_namespace::{encode_step_record, CreationStep};
694
695 fn created() -> NamespaceReading {
696 NamespaceReading {
697 network: Ok(()),
698 mount: MountAttempt::Created,
699 sealed_root: MountAttempt::Created,
700 private_proc: MountAttempt::NotAttempted,
701 userns_grant: UsernsGrant::Withheld,
702 attachment: ProfileAttachment::Unconfined,
703 profile_gap: crate::sealed::ProfileGap::Undetermined,
704 }
705 }
706
707 fn failed(step: MountStep, errno: i32) -> MountAttempt {
708 MountAttempt::Failed(MountAttemptError::Step { step, errno })
709 }
710
711 #[test]
712 fn every_record_round_trips_and_an_unknown_code_is_none() {
713 assert_eq!(
714 decode_record(&encode_step_record(0, 0)),
715 Some(MountAttempt::Created)
716 );
717 for step in MountStep::ALL {
718 assert_eq!(
719 decode_record(&encode_step_record(step as u32, libc::EPERM)),
720 Some(failed(step, libc::EPERM))
721 );
722 }
723 assert_eq!(decode_record(&encode_step_record(99, 0)), None);
724 }
725
726 #[test]
727 fn every_step_names_its_own_call() {
728 let mut seen = std::collections::HashSet::new();
729 for step in MountStep::ALL {
730 assert!(
731 seen.insert(step.describe()),
732 "{step:?} repeats a description"
733 );
734 }
735 assert_eq!(MountStep::UnshareMount.describe(), "unshare(CLONE_NEWNS)");
736 assert_eq!(MountStep::PivotRoot.describe(), "pivot_root(2)");
737 }
738
739 #[test]
740 fn the_questions_follow_the_attempts_and_never_the_readings() {
741 for grant in UsernsGrant::ALL {
742 let reading = NamespaceReading {
743 userns_grant: *grant,
744 ..created()
745 };
746 assert!(reading.mount_namespace_created());
747 assert!(reading.sealed_root_created());
748 assert!(reading.user_mount_namespace_usable());
749 assert_eq!(reading.egress_namespace_failure(), None);
750 assert_eq!(reading.first_failure(), None);
751 }
752 }
753
754 #[test]
755 fn a_network_failure_creates_nothing_after_it() {
756 let reading = NamespaceReading {
757 network: Err(NamespaceAttemptError::Step {
758 step: CreationStep::Unshare,
759 errno: libc::EPERM,
760 }),
761 mount: MountAttempt::NotAttempted,
762 sealed_root: MountAttempt::NotAttempted,
763 ..created()
764 };
765 assert!(!reading.mount_namespace_created());
766 assert!(!reading.sealed_root_created());
767 assert!(!reading.user_mount_namespace_usable());
768 assert_eq!(
769 reading.first_failure().as_deref(),
770 cfg!(target_os = "linux")
771 .then_some("unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM")
772 );
773 }
774
775 #[test]
776 fn the_user_mount_question_needs_the_nested_namespace_its_maps_and_propagation() {
777 for (step, usable) in [
778 (MountStep::JoinSession, false),
779 (MountStep::UnshareUserMount, false),
780 (MountStep::IdentityMaps, false),
781 (MountStep::MakePrivateNested, false),
782 (MountStep::Tmpfs, true),
783 (MountStep::PivotPrepare, true),
784 (MountStep::PivotRoot, true),
785 (MountStep::DetachOldRoot, true),
786 ] {
787 let reading = NamespaceReading {
788 sealed_root: failed(step, libc::EPERM),
789 ..created()
790 };
791 assert_eq!(reading.user_mount_namespace_usable(), usable, "{step:?}");
792 assert!(!reading.sealed_root_created());
793 assert!(reading.mount_namespace_created());
794 }
795 let reading = NamespaceReading {
796 sealed_root: MountAttempt::Failed(MountAttemptError::Rehearsal("x".to_string())),
797 ..created()
798 };
799 assert!(!reading.user_mount_namespace_usable());
800 }
801
802 #[test]
803 fn off_linux_nothing_is_created_and_nothing_is_refused() {
804 let reading = NamespaceReading::not_linux();
805 assert!(!reading.mount_namespace_created());
806 assert!(!reading.sealed_root_created());
807 assert!(!reading.user_mount_namespace_usable());
808 assert!(reading.mount.failure_summary().is_none());
809 assert!(reading.sealed_root.failure_summary().is_none());
810 #[cfg(not(target_os = "linux"))]
811 assert_eq!(probe_namespaces(), NamespaceReading::not_linux());
812 }
813
814 #[test]
815 fn a_failed_attempt_summarises_as_its_call_and_errno() {
816 assert_eq!(
817 failed(MountStep::MakePrivate, libc::EACCES)
818 .failure_summary()
819 .as_deref(),
820 Some("making / MS_REC|MS_PRIVATE failed with EACCES")
821 );
822 assert_eq!(
823 probe_failure_sentence(MountStep::PivotRoot.describe(), libc::EPERM),
824 "The probe's pivot_root(2) failed with EPERM (Operation not permitted)."
825 );
826 }
827
828 #[cfg(target_os = "linux")]
829 const INNER_RUN_ENV: &str = "MURMUR_NAMESPACE_PROBE_UNCHANGED_INNER";
830
831 #[cfg(target_os = "linux")]
837 #[test]
838 fn the_probe_leaves_the_calling_process_unchanged() {
839 let exe = std::env::current_exe().expect("test binary path");
840 let output = std::process::Command::new(exe)
841 .args([
842 "--exact",
843 "namespace_probe::tests::inner_the_probe_leaves_the_calling_process_unchanged",
844 "--ignored",
845 "--test-threads=1",
846 "--nocapture",
847 ])
848 .env(INNER_RUN_ENV, "1")
849 .output()
850 .expect("re-exec the test binary for the single-threaded inner run");
851 let stdout = String::from_utf8_lossy(&output.stdout);
852 let stderr = String::from_utf8_lossy(&output.stderr);
853 assert!(
854 output.status.success(),
855 "inner probe run failed\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
856 );
857 assert!(
858 stdout.contains("1 passed"),
859 "inner run did not execute the probe test\n--- stdout ---\n{stdout}"
860 );
861 }
862
863 #[cfg(target_os = "linux")]
864 #[test]
865 #[ignore = "run by the_probe_leaves_the_calling_process_unchanged"]
866 fn inner_the_probe_leaves_the_calling_process_unchanged() {
867 if std::env::var(INNER_RUN_ENV).is_err() {
868 return;
869 }
870 let namespaces = || {
871 ["user", "net", "mnt"].map(|ns| std::fs::read_link(format!("/proc/self/ns/{ns}")).ok())
872 };
873 let fds = || {
874 std::fs::read_dir("/proc/self/fd")
875 .map(|dir| dir.count())
876 .ok()
877 };
878 let before = (namespaces(), fds());
879 let reading = probe_namespaces();
880 assert_eq!((namespaces(), fds()), before, "{reading:?}");
881 if reading.network.is_err() {
882 assert_eq!(reading.mount, MountAttempt::NotAttempted);
883 assert_eq!(reading.sealed_root, MountAttempt::NotAttempted);
884 }
885 }
886}