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, 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}
184
185impl NamespaceReading {
186 #[must_use]
188 pub fn not_linux() -> Self {
189 Self {
190 network: Ok(()),
191 mount: MountAttempt::NotAttempted,
192 sealed_root: MountAttempt::NotAttempted,
193 private_proc: MountAttempt::NotAttempted,
194 userns_grant: UsernsGrant::default(),
195 attachment: ProfileAttachment::NotReported,
196 }
197 }
198
199 #[must_use]
202 pub fn egress_namespace_failure(&self) -> Option<EgressNamespaceFailure> {
203 crate::network_namespace::egress_namespace_failure(
204 cfg!(target_os = "linux"),
205 self.network.clone(),
206 self.userns_grant,
207 self.attachment.clone(),
208 )
209 }
210
211 #[must_use]
214 pub fn mount_namespace_created(&self) -> bool {
215 self.network.is_ok() && self.mount == MountAttempt::Created
216 }
217
218 #[must_use]
221 pub fn private_proc_created(&self) -> bool {
222 self.network.is_ok() && self.private_proc == MountAttempt::Created
223 }
224
225 #[must_use]
228 pub fn sealed_root_created(&self) -> bool {
229 self.network.is_ok() && self.sealed_root == MountAttempt::Created
230 }
231
232 #[must_use]
237 pub fn user_mount_namespace_usable(&self) -> bool {
238 self.network.is_ok()
239 && match &self.sealed_root {
240 MountAttempt::Created => true,
241 MountAttempt::Failed(MountAttemptError::Step { step, .. }) => matches!(
242 step,
243 MountStep::Tmpfs
244 | MountStep::PivotPrepare
245 | MountStep::PivotRoot
246 | MountStep::DetachOldRoot
247 ),
248 MountAttempt::Failed(MountAttemptError::Rehearsal(_))
249 | MountAttempt::NotAttempted => false,
250 }
251 }
252
253 #[must_use]
256 pub fn first_failure(&self) -> Option<String> {
257 if let Some(failure) = self.egress_namespace_failure() {
258 return Some(failure.summary());
259 }
260 self.sealed_root
261 .failure_summary()
262 .or_else(|| self.mount.failure_summary())
263 }
264}
265
266pub(crate) fn probe_failure_sentence(failed_call: &str, errno: i32) -> String {
269 format!(
270 "The probe's {failed_call} failed with {} ({}).",
271 errno_label(errno),
272 errno_words(errno)
273 )
274}
275
276#[cfg(target_os = "linux")]
281pub(crate) fn probe_namespaces() -> NamespaceReading {
282 use crate::network_namespace::{create_session_netns, CapsuleNetnsPlan};
283
284 let (network, mount, sealed_root, private_proc) =
285 match create_session_netns(&CapsuleNetnsPlan::resolve(Vec::new())) {
286 Err(error) => (
287 Err(error),
288 MountAttempt::NotAttempted,
289 MountAttempt::NotAttempted,
290 MountAttempt::NotAttempted,
291 ),
292 Ok(created) => {
293 let (user_fd, net_fd) = created.namespace_handles();
294 let pid_fd = created.pid_namespace_handle();
295 let handles = linux::Handles {
296 user_fd,
297 net_fd,
298 pid_fd,
299 };
300 let mount = linux::rehearse(linux::Rehearsal::PrivateTmp, handles);
301 let sealed_root = linux::rehearse(linux::Rehearsal::SealedRoot, handles);
302 let private_proc = linux::rehearse(linux::Rehearsal::PrivateProc, handles);
303 drop(created);
305 (Ok(()), mount, sealed_root, private_proc)
306 }
307 };
308 NamespaceReading {
309 network,
310 mount,
311 sealed_root,
312 private_proc,
313 userns_grant: crate::sealed::userns_grant(),
314 attachment: crate::sealed::inspect_profile_attachment(),
315 }
316}
317
318#[cfg(not(target_os = "linux"))]
319pub(crate) fn probe_namespaces() -> NamespaceReading {
320 NamespaceReading::not_linux()
321}
322
323#[cfg(any(target_os = "linux", test))]
326fn decode_record(record: &[u8; STEP_RECORD_LEN]) -> Option<MountAttempt> {
327 let (code, errno) = decode_step_record(record);
328 if code == 0 {
329 return Some(MountAttempt::Created);
330 }
331 MountStep::from_code(code)
332 .map(|step| MountAttempt::Failed(MountAttemptError::Step { step, errno }))
333}
334
335#[cfg(target_os = "linux")]
336#[allow(unsafe_code)]
337mod linux {
338 use std::ffi::CString;
339 use std::io;
340 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
341 use std::os::unix::ffi::OsStrExt;
342
343 use super::{decode_record, MountAttempt, MountAttemptError, MountStep, STEP_RECORD_LEN};
344 use crate::network_namespace::encode_step_record;
345 use crate::sealed::{
346 make_dumpable_for_map_writes, restore_dumpable, write_decimal_map, OLD_ROOT_NAME_C,
347 OLD_ROOT_PIVOTED_C, SEALED_ROOT_BASE_CANDIDATES,
348 };
349
350 #[derive(Debug, Clone, Copy)]
352 pub(super) enum Rehearsal {
353 PrivateTmp,
356 SealedRoot,
358 PrivateProc,
361 }
362
363 impl Rehearsal {
364 fn name(self) -> &'static str {
365 match self {
366 Rehearsal::PrivateTmp => "mount rehearsal",
367 Rehearsal::SealedRoot => "sealed-root rehearsal",
368 Rehearsal::PrivateProc => "private-procfs rehearsal",
369 }
370 }
371 }
372
373 #[derive(Debug, Clone, Copy)]
375 pub(super) struct Handles {
376 pub(super) user_fd: RawFd,
377 pub(super) net_fd: RawFd,
378 pub(super) pid_fd: RawFd,
379 }
380
381 struct Paths {
383 tmp: CString,
385 bases: Vec<(CString, CString)>,
387 uid: u32,
388 gid: u32,
389 }
390
391 impl Paths {
392 fn resolve() -> Result<Self, String> {
393 let tmp = std::fs::canonicalize("/tmp").unwrap_or_else(|_| "/tmp".into());
394 let tmp = CString::new(tmp.as_os_str().as_bytes())
395 .map_err(|_| "the canonical /tmp contains a NUL byte".to_string())?;
396 let mut bases = Vec::with_capacity(SEALED_ROOT_BASE_CANDIDATES.len());
397 for base in SEALED_ROOT_BASE_CANDIDATES {
398 let parked = format!(
399 "{base}/{}",
400 OLD_ROOT_NAME_C.to_str().expect("the parking name is ASCII")
401 );
402 bases.push((
403 CString::new(*base).expect("candidates carry no NUL"),
404 CString::new(parked).expect("candidates carry no NUL"),
405 ));
406 }
407 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
411 Ok(Self {
412 tmp,
413 bases,
414 uid,
415 gid,
416 })
417 }
418 }
419
420 pub(super) fn rehearse(rehearsal: Rehearsal, handles: Handles) -> MountAttempt {
423 let failed = |detail: String| MountAttempt::Failed(MountAttemptError::Rehearsal(detail));
424 let paths = match Paths::resolve() {
425 Ok(paths) => paths,
426 Err(detail) => {
427 return failed(format!("the {} could not run: {detail}", rehearsal.name()))
428 }
429 };
430
431 let mut pipe = [0i32; 2];
432 if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) } != 0 {
434 return failed(format!(
435 "pipe2() for the {} failed: {}",
436 rehearsal.name(),
437 io::Error::last_os_error()
438 ));
439 }
440 let (read_end, write_end) =
443 unsafe { (OwnedFd::from_raw_fd(pipe[0]), OwnedFd::from_raw_fd(pipe[1])) };
444 let write_fd = write_end.as_raw_fd();
445
446 let pid = unsafe { libc::fork() };
452 if pid < 0 {
453 return failed(format!(
454 "fork() for the {} failed: {}",
455 rehearsal.name(),
456 io::Error::last_os_error()
457 ));
458 }
459 if pid == 0 {
460 unsafe {
463 let record = match run(rehearsal, &paths, handles, write_fd) {
464 Ok(()) => encode_step_record(0, 0),
465 Err((step, errno)) => encode_step_record(step as u32, errno),
466 };
467 libc::write(write_fd, record.as_ptr().cast(), record.len());
468 libc::_exit(0);
469 }
470 }
471 drop(write_end);
472
473 let status = match crate::network_namespace::reap_child(pid) {
474 Ok(status) => status,
475 Err(error) => {
476 return failed(format!(
477 "waiting for the {} failed: {error}",
478 rehearsal.name()
479 ))
480 }
481 };
482 let mut record = [0u8; STEP_RECORD_LEN];
485 let read = unsafe {
487 libc::read(
488 read_end.as_raw_fd(),
489 record.as_mut_ptr().cast(),
490 STEP_RECORD_LEN,
491 )
492 };
493 if read == STEP_RECORD_LEN as isize {
494 if let Some(attempt) = decode_record(&record) {
495 return attempt;
496 }
497 }
498 failed(format!(
499 "the {} exited without reporting ({status})",
500 rehearsal.name()
501 ))
502 }
503
504 unsafe fn run(
510 rehearsal: Rehearsal,
511 paths: &Paths,
512 handles: Handles,
513 report_fd: RawFd,
514 ) -> Result<(), (MountStep, i32)> {
515 unsafe {
517 let errno = || *libc::__errno_location();
518 if let Err(error) =
520 crate::network_namespace::join_session_netns(handles.user_fd, handles.net_fd)
521 {
522 return Err((MountStep::JoinSession, error.raw_os_error().unwrap_or(0)));
523 }
524 match rehearsal {
525 Rehearsal::PrivateProc => {
528 if libc::setns(handles.pid_fd, libc::CLONE_NEWPID) != 0 {
529 return Err((MountStep::JoinPidNamespace, errno()));
530 }
531 let member = libc::fork();
532 if member < 0 {
533 return Err((MountStep::ForkIntoPidNamespace, errno()));
534 }
535 if member == 0 {
536 let record = match crate::proc_view::mount_private_proc(true) {
538 Ok(_) => encode_step_record(0, 0),
539 Err((step, step_errno)) => {
540 let step = match step {
541 crate::proc_view::ProcMountStep::UnshareMount => {
542 MountStep::UnshareMount
543 }
544 crate::proc_view::ProcMountStep::MakePrivate => {
545 MountStep::MakePrivate
546 }
547 crate::proc_view::ProcMountStep::MountProc
548 | crate::proc_view::ProcMountStep::OpenProc => {
549 MountStep::MountProc
550 }
551 };
552 encode_step_record(step as u32, step_errno)
553 }
554 };
555 libc::write(report_fd, record.as_ptr().cast(), record.len());
556 libc::_exit(0);
557 }
558 let mut status = 0;
559 loop {
560 let waited = libc::waitpid(member, &mut status, 0);
561 if waited == member {
562 break;
563 }
564 if waited < 0 && errno() == libc::EINTR {
565 continue;
566 }
567 return Err((MountStep::ForkIntoPidNamespace, errno()));
568 }
569 if libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0 {
570 libc::_exit(0);
572 }
573 return Err((MountStep::ForkIntoPidNamespace, 0));
574 }
575 Rehearsal::PrivateTmp => {
579 if libc::unshare(libc::CLONE_NEWNS) != 0 {
580 return Err((MountStep::UnshareMount, errno()));
581 }
582 if make_private() != 0 {
583 return Err((MountStep::MakePrivate, errno()));
584 }
585 if libc::mount(
586 paths.tmp.as_ptr(),
587 paths.tmp.as_ptr(),
588 std::ptr::null(),
589 libc::MS_BIND | libc::MS_REC,
590 std::ptr::null(),
591 ) != 0
592 {
593 return Err((MountStep::BindTmp, errno()));
594 }
595 }
596 Rehearsal::SealedRoot => {
599 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
600 return Err((MountStep::UnshareUserMount, errno()));
601 }
602 let previous = make_dumpable_for_map_writes();
605 let _ = write_decimal_map(c"/proc/self/setgroups", None, 0);
606 let mapped =
607 write_decimal_map(c"/proc/self/uid_map", Some(paths.uid), paths.uid)
608 .is_ok()
609 && write_decimal_map(c"/proc/self/gid_map", Some(paths.gid), paths.gid)
610 .is_ok();
611 let map_errno = errno();
612 restore_dumpable(previous);
613 if !mapped {
614 return Err((MountStep::IdentityMaps, map_errno));
615 }
616 if make_private() != 0 {
617 return Err((MountStep::MakePrivateNested, errno()));
618 }
619 let mut based = None;
622 let mut tmpfs_errno = 0;
623 for (base, parked) in &paths.bases {
624 if libc::mount(
625 c"tmpfs".as_ptr(),
626 base.as_ptr(),
627 c"tmpfs".as_ptr(),
628 libc::MS_NOSUID | libc::MS_NODEV,
629 c"mode=0755".as_ptr().cast(),
630 ) == 0
631 {
632 based = Some((base, parked));
633 break;
634 }
635 tmpfs_errno = errno();
636 }
637 let Some((base, parked)) = based else {
638 return Err((MountStep::Tmpfs, tmpfs_errno));
639 };
640 if libc::mkdir(parked.as_ptr(), 0o700) != 0 {
641 return Err((MountStep::PivotPrepare, errno()));
642 }
643 if libc::chdir(base.as_ptr()) != 0 {
644 return Err((MountStep::PivotPrepare, errno()));
645 }
646 if libc::syscall(
647 libc::SYS_pivot_root,
648 c".".as_ptr(),
649 OLD_ROOT_NAME_C.as_ptr(),
650 ) != 0
651 {
652 return Err((MountStep::PivotRoot, errno()));
653 }
654 if libc::chdir(c"/".as_ptr()) != 0 {
655 return Err((MountStep::PivotRoot, errno()));
656 }
657 if libc::umount2(OLD_ROOT_PIVOTED_C.as_ptr(), libc::MNT_DETACH) != 0 {
658 return Err((MountStep::DetachOldRoot, errno()));
659 }
660 }
661 }
662 Ok(())
663 }
664 }
665
666 unsafe fn make_private() -> libc::c_int {
671 unsafe {
673 libc::mount(
674 std::ptr::null(),
675 c"/".as_ptr(),
676 std::ptr::null(),
677 libc::MS_REC | libc::MS_PRIVATE,
678 std::ptr::null(),
679 )
680 }
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687 use crate::network_namespace::{encode_step_record, CreationStep};
688
689 fn created() -> NamespaceReading {
690 NamespaceReading {
691 network: Ok(()),
692 mount: MountAttempt::Created,
693 sealed_root: MountAttempt::Created,
694 private_proc: MountAttempt::NotAttempted,
695 userns_grant: UsernsGrant::Withheld,
696 attachment: ProfileAttachment::Unconfined,
697 }
698 }
699
700 fn failed(step: MountStep, errno: i32) -> MountAttempt {
701 MountAttempt::Failed(MountAttemptError::Step { step, errno })
702 }
703
704 #[test]
705 fn every_record_round_trips_and_an_unknown_code_is_none() {
706 assert_eq!(
707 decode_record(&encode_step_record(0, 0)),
708 Some(MountAttempt::Created)
709 );
710 for step in MountStep::ALL {
711 assert_eq!(
712 decode_record(&encode_step_record(step as u32, libc::EPERM)),
713 Some(failed(step, libc::EPERM))
714 );
715 }
716 assert_eq!(decode_record(&encode_step_record(99, 0)), None);
717 }
718
719 #[test]
720 fn every_step_names_its_own_call() {
721 let mut seen = std::collections::HashSet::new();
722 for step in MountStep::ALL {
723 assert!(
724 seen.insert(step.describe()),
725 "{step:?} repeats a description"
726 );
727 }
728 assert_eq!(MountStep::UnshareMount.describe(), "unshare(CLONE_NEWNS)");
729 assert_eq!(MountStep::PivotRoot.describe(), "pivot_root(2)");
730 }
731
732 #[test]
733 fn the_questions_follow_the_attempts_and_never_the_readings() {
734 for grant in UsernsGrant::ALL {
735 let reading = NamespaceReading {
736 userns_grant: *grant,
737 ..created()
738 };
739 assert!(reading.mount_namespace_created());
740 assert!(reading.sealed_root_created());
741 assert!(reading.user_mount_namespace_usable());
742 assert_eq!(reading.egress_namespace_failure(), None);
743 assert_eq!(reading.first_failure(), None);
744 }
745 }
746
747 #[test]
748 fn a_network_failure_creates_nothing_after_it() {
749 let reading = NamespaceReading {
750 network: Err(NamespaceAttemptError::Step {
751 step: CreationStep::Unshare,
752 errno: libc::EPERM,
753 }),
754 mount: MountAttempt::NotAttempted,
755 sealed_root: MountAttempt::NotAttempted,
756 ..created()
757 };
758 assert!(!reading.mount_namespace_created());
759 assert!(!reading.sealed_root_created());
760 assert!(!reading.user_mount_namespace_usable());
761 assert_eq!(
762 reading.first_failure().as_deref(),
763 cfg!(target_os = "linux")
764 .then_some("unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM")
765 );
766 }
767
768 #[test]
769 fn the_user_mount_question_needs_the_nested_namespace_its_maps_and_propagation() {
770 for (step, usable) in [
771 (MountStep::JoinSession, false),
772 (MountStep::UnshareUserMount, false),
773 (MountStep::IdentityMaps, false),
774 (MountStep::MakePrivateNested, false),
775 (MountStep::Tmpfs, true),
776 (MountStep::PivotPrepare, true),
777 (MountStep::PivotRoot, true),
778 (MountStep::DetachOldRoot, true),
779 ] {
780 let reading = NamespaceReading {
781 sealed_root: failed(step, libc::EPERM),
782 ..created()
783 };
784 assert_eq!(reading.user_mount_namespace_usable(), usable, "{step:?}");
785 assert!(!reading.sealed_root_created());
786 assert!(reading.mount_namespace_created());
787 }
788 let reading = NamespaceReading {
789 sealed_root: MountAttempt::Failed(MountAttemptError::Rehearsal("x".to_string())),
790 ..created()
791 };
792 assert!(!reading.user_mount_namespace_usable());
793 }
794
795 #[test]
796 fn off_linux_nothing_is_created_and_nothing_is_refused() {
797 let reading = NamespaceReading::not_linux();
798 assert!(!reading.mount_namespace_created());
799 assert!(!reading.sealed_root_created());
800 assert!(!reading.user_mount_namespace_usable());
801 assert!(reading.mount.failure_summary().is_none());
802 assert!(reading.sealed_root.failure_summary().is_none());
803 #[cfg(not(target_os = "linux"))]
804 assert_eq!(probe_namespaces(), NamespaceReading::not_linux());
805 }
806
807 #[test]
808 fn a_failed_attempt_summarises_as_its_call_and_errno() {
809 assert_eq!(
810 failed(MountStep::MakePrivate, libc::EACCES)
811 .failure_summary()
812 .as_deref(),
813 Some("making / MS_REC|MS_PRIVATE failed with EACCES")
814 );
815 assert_eq!(
816 probe_failure_sentence(MountStep::PivotRoot.describe(), libc::EPERM),
817 "The probe's pivot_root(2) failed with EPERM (Operation not permitted)."
818 );
819 }
820
821 #[cfg(target_os = "linux")]
822 const INNER_RUN_ENV: &str = "MURMUR_NAMESPACE_PROBE_UNCHANGED_INNER";
823
824 #[cfg(target_os = "linux")]
830 #[test]
831 fn the_probe_leaves_the_calling_process_unchanged() {
832 let exe = std::env::current_exe().expect("test binary path");
833 let output = std::process::Command::new(exe)
834 .args([
835 "--exact",
836 "namespace_probe::tests::inner_the_probe_leaves_the_calling_process_unchanged",
837 "--ignored",
838 "--test-threads=1",
839 "--nocapture",
840 ])
841 .env(INNER_RUN_ENV, "1")
842 .output()
843 .expect("re-exec the test binary for the single-threaded inner run");
844 let stdout = String::from_utf8_lossy(&output.stdout);
845 let stderr = String::from_utf8_lossy(&output.stderr);
846 assert!(
847 output.status.success(),
848 "inner probe run failed\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
849 );
850 assert!(
851 stdout.contains("1 passed"),
852 "inner run did not execute the probe test\n--- stdout ---\n{stdout}"
853 );
854 }
855
856 #[cfg(target_os = "linux")]
857 #[test]
858 #[ignore = "run by the_probe_leaves_the_calling_process_unchanged"]
859 fn inner_the_probe_leaves_the_calling_process_unchanged() {
860 if std::env::var(INNER_RUN_ENV).is_err() {
861 return;
862 }
863 let namespaces = || {
864 ["user", "net", "mnt"].map(|ns| std::fs::read_link(format!("/proc/self/ns/{ns}")).ok())
865 };
866 let fds = || {
867 std::fs::read_dir("/proc/self/fd")
868 .map(|dir| dir.count())
869 .ok()
870 };
871 let before = (namespaces(), fds());
872 let reading = probe_namespaces();
873 assert_eq!((namespaces(), fds()), before, "{reading:?}");
874 if reading.network.is_err() {
875 assert_eq!(reading.mount, MountAttempt::NotAttempted);
876 assert_eq!(reading.sealed_root, MountAttempt::NotAttempted);
877 }
878 }
879}