1#[cfg(any(target_os = "linux", test))]
29use crate::network_namespace::{decode_step_record, STEP_RECORD_LEN};
30use crate::network_namespace::{
31 errno_label, errno_words, EgressNamespaceFailure, NamespaceAttemptError,
32};
33use crate::sealed::{ProfileAttachment, UsernsGrant};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum MountStep {
41 JoinSession = 1,
42 UnshareMount = 2,
43 MakePrivate = 3,
44 BindTmp = 4,
45 UnshareUserMount = 5,
46 IdentityMaps = 6,
47 MakePrivateNested = 7,
48 Tmpfs = 8,
49 PivotPrepare = 9,
50 PivotRoot = 10,
51 DetachOldRoot = 11,
52}
53
54impl MountStep {
55 #[cfg(any(target_os = "linux", test))]
57 const ALL: [MountStep; 11] = [
58 Self::JoinSession,
59 Self::UnshareMount,
60 Self::MakePrivate,
61 Self::BindTmp,
62 Self::UnshareUserMount,
63 Self::IdentityMaps,
64 Self::MakePrivateNested,
65 Self::Tmpfs,
66 Self::PivotPrepare,
67 Self::PivotRoot,
68 Self::DetachOldRoot,
69 ];
70
71 #[cfg(any(target_os = "linux", test))]
73 fn from_code(code: u32) -> Option<Self> {
74 Self::ALL.into_iter().find(|step| *step as u32 == code)
75 }
76
77 #[must_use]
79 pub fn describe(self) -> &'static str {
80 match self {
81 Self::JoinSession => "joining the session's user and network namespaces (setns)",
82 Self::UnshareMount => "unshare(CLONE_NEWNS)",
83 Self::MakePrivate => "making / MS_REC|MS_PRIVATE",
84 Self::BindTmp => "bind-mounting over /tmp",
85 Self::UnshareUserMount => {
86 "unshare(CLONE_NEWUSER | CLONE_NEWNS) inside the session's user namespace"
87 }
88 Self::IdentityMaps => "writing uid_map/gid_map",
89 Self::MakePrivateNested => "making / MS_REC|MS_PRIVATE in the nested namespace",
90 Self::Tmpfs => "mounting the composed root's tmpfs",
91 Self::PivotPrepare => "creating the old root's parking directory and entering the root",
92 Self::PivotRoot => "pivot_root(2)",
93 Self::DetachOldRoot => "detaching the old root",
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum MountAttemptError {
101 Step { step: MountStep, errno: i32 },
103 Rehearsal(String),
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum MountAttempt {
111 Created,
113 Failed(MountAttemptError),
115 NotAttempted,
118}
119
120impl MountAttempt {
121 #[must_use]
124 pub fn failure_summary(&self) -> Option<String> {
125 match self {
126 MountAttempt::Failed(MountAttemptError::Step { step, errno }) => Some(format!(
127 "{} failed with {}",
128 step.describe(),
129 errno_label(*errno)
130 )),
131 MountAttempt::Failed(MountAttemptError::Rehearsal(detail)) => Some(detail.clone()),
132 MountAttempt::Created | MountAttempt::NotAttempted => None,
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct NamespaceReading {
147 pub network: Result<(), NamespaceAttemptError>,
149 pub mount: MountAttempt,
152 pub sealed_root: MountAttempt,
155 pub userns_grant: UsernsGrant,
158 pub attachment: ProfileAttachment,
161}
162
163impl NamespaceReading {
164 #[must_use]
166 pub fn not_linux() -> Self {
167 Self {
168 network: Ok(()),
169 mount: MountAttempt::NotAttempted,
170 sealed_root: MountAttempt::NotAttempted,
171 userns_grant: UsernsGrant::default(),
172 attachment: ProfileAttachment::NotReported,
173 }
174 }
175
176 #[must_use]
179 pub fn egress_namespace_failure(&self) -> Option<EgressNamespaceFailure> {
180 crate::network_namespace::egress_namespace_failure(
181 cfg!(target_os = "linux"),
182 self.network.clone(),
183 self.userns_grant,
184 self.attachment.clone(),
185 )
186 }
187
188 #[must_use]
191 pub fn mount_namespace_created(&self) -> bool {
192 self.network.is_ok() && self.mount == MountAttempt::Created
193 }
194
195 #[must_use]
198 pub fn sealed_root_created(&self) -> bool {
199 self.network.is_ok() && self.sealed_root == MountAttempt::Created
200 }
201
202 #[must_use]
207 pub fn user_mount_namespace_usable(&self) -> bool {
208 self.network.is_ok()
209 && match &self.sealed_root {
210 MountAttempt::Created => true,
211 MountAttempt::Failed(MountAttemptError::Step { step, .. }) => matches!(
212 step,
213 MountStep::Tmpfs
214 | MountStep::PivotPrepare
215 | MountStep::PivotRoot
216 | MountStep::DetachOldRoot
217 ),
218 MountAttempt::Failed(MountAttemptError::Rehearsal(_))
219 | MountAttempt::NotAttempted => false,
220 }
221 }
222
223 #[must_use]
226 pub fn first_failure(&self) -> Option<String> {
227 if let Some(failure) = self.egress_namespace_failure() {
228 return Some(failure.summary());
229 }
230 self.sealed_root
231 .failure_summary()
232 .or_else(|| self.mount.failure_summary())
233 }
234}
235
236pub(crate) fn probe_failure_sentence(failed_call: &str, errno: i32) -> String {
239 format!(
240 "The probe's {failed_call} failed with {} ({}).",
241 errno_label(errno),
242 errno_words(errno)
243 )
244}
245
246#[cfg(target_os = "linux")]
251pub(crate) fn probe_namespaces() -> NamespaceReading {
252 use crate::network_namespace::{create_session_netns, CapsuleNetnsPlan};
253
254 let (network, mount, sealed_root) =
255 match create_session_netns(&CapsuleNetnsPlan::resolve(Vec::new())) {
256 Err(error) => (
257 Err(error),
258 MountAttempt::NotAttempted,
259 MountAttempt::NotAttempted,
260 ),
261 Ok(created) => {
262 let (user_fd, net_fd) = created.namespace_handles();
263 let mount = linux::rehearse(linux::Rehearsal::PrivateTmp, user_fd, net_fd);
264 let sealed_root = linux::rehearse(linux::Rehearsal::SealedRoot, user_fd, net_fd);
265 drop(created);
266 (Ok(()), mount, sealed_root)
267 }
268 };
269 NamespaceReading {
270 network,
271 mount,
272 sealed_root,
273 userns_grant: crate::sealed::userns_grant(),
274 attachment: crate::sealed::inspect_profile_attachment(),
275 }
276}
277
278#[cfg(not(target_os = "linux"))]
279pub(crate) fn probe_namespaces() -> NamespaceReading {
280 NamespaceReading::not_linux()
281}
282
283#[cfg(any(target_os = "linux", test))]
286fn decode_record(record: &[u8; STEP_RECORD_LEN]) -> Option<MountAttempt> {
287 let (code, errno) = decode_step_record(record);
288 if code == 0 {
289 return Some(MountAttempt::Created);
290 }
291 MountStep::from_code(code)
292 .map(|step| MountAttempt::Failed(MountAttemptError::Step { step, errno }))
293}
294
295#[cfg(target_os = "linux")]
296#[allow(unsafe_code)]
297mod linux {
298 use std::ffi::CString;
299 use std::io;
300 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
301 use std::os::unix::ffi::OsStrExt;
302
303 use super::{decode_record, MountAttempt, MountAttemptError, MountStep, STEP_RECORD_LEN};
304 use crate::network_namespace::encode_step_record;
305 use crate::sealed::{
306 make_dumpable_for_map_writes, restore_dumpable, write_decimal_map, OLD_ROOT_NAME_C,
307 OLD_ROOT_PIVOTED_C, SEALED_ROOT_BASE_CANDIDATES,
308 };
309
310 #[derive(Debug, Clone, Copy)]
312 pub(super) enum Rehearsal {
313 PrivateTmp,
316 SealedRoot,
318 }
319
320 impl Rehearsal {
321 fn name(self) -> &'static str {
322 match self {
323 Rehearsal::PrivateTmp => "mount rehearsal",
324 Rehearsal::SealedRoot => "sealed-root rehearsal",
325 }
326 }
327 }
328
329 struct Paths {
331 tmp: CString,
333 bases: Vec<(CString, CString)>,
335 uid: u32,
336 gid: u32,
337 }
338
339 impl Paths {
340 fn resolve() -> Result<Self, String> {
341 let tmp = std::fs::canonicalize("/tmp").unwrap_or_else(|_| "/tmp".into());
342 let tmp = CString::new(tmp.as_os_str().as_bytes())
343 .map_err(|_| "the canonical /tmp contains a NUL byte".to_string())?;
344 let mut bases = Vec::with_capacity(SEALED_ROOT_BASE_CANDIDATES.len());
345 for base in SEALED_ROOT_BASE_CANDIDATES {
346 let parked = format!(
347 "{base}/{}",
348 OLD_ROOT_NAME_C.to_str().expect("the parking name is ASCII")
349 );
350 bases.push((
351 CString::new(*base).expect("candidates carry no NUL"),
352 CString::new(parked).expect("candidates carry no NUL"),
353 ));
354 }
355 let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
359 Ok(Self {
360 tmp,
361 bases,
362 uid,
363 gid,
364 })
365 }
366 }
367
368 pub(super) fn rehearse(rehearsal: Rehearsal, user_fd: RawFd, net_fd: RawFd) -> MountAttempt {
371 let failed = |detail: String| MountAttempt::Failed(MountAttemptError::Rehearsal(detail));
372 let paths = match Paths::resolve() {
373 Ok(paths) => paths,
374 Err(detail) => {
375 return failed(format!("the {} could not run: {detail}", rehearsal.name()))
376 }
377 };
378
379 let mut pipe = [0i32; 2];
380 if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) } != 0 {
382 return failed(format!(
383 "pipe2() for the {} failed: {}",
384 rehearsal.name(),
385 io::Error::last_os_error()
386 ));
387 }
388 let (read_end, write_end) =
391 unsafe { (OwnedFd::from_raw_fd(pipe[0]), OwnedFd::from_raw_fd(pipe[1])) };
392 let write_fd = write_end.as_raw_fd();
393
394 let pid = unsafe { libc::fork() };
400 if pid < 0 {
401 return failed(format!(
402 "fork() for the {} failed: {}",
403 rehearsal.name(),
404 io::Error::last_os_error()
405 ));
406 }
407 if pid == 0 {
408 unsafe {
411 let record = match run(rehearsal, &paths, user_fd, net_fd) {
412 Ok(()) => encode_step_record(0, 0),
413 Err((step, errno)) => encode_step_record(step as u32, errno),
414 };
415 libc::write(write_fd, record.as_ptr().cast(), record.len());
416 libc::_exit(0);
417 }
418 }
419 drop(write_end);
420
421 let status = match crate::network_namespace::reap_child(pid) {
422 Ok(status) => status,
423 Err(error) => {
424 return failed(format!(
425 "waiting for the {} failed: {error}",
426 rehearsal.name()
427 ))
428 }
429 };
430 let mut record = [0u8; STEP_RECORD_LEN];
433 let read = unsafe {
435 libc::read(
436 read_end.as_raw_fd(),
437 record.as_mut_ptr().cast(),
438 STEP_RECORD_LEN,
439 )
440 };
441 if read == STEP_RECORD_LEN as isize {
442 if let Some(attempt) = decode_record(&record) {
443 return attempt;
444 }
445 }
446 failed(format!(
447 "the {} exited without reporting ({status})",
448 rehearsal.name()
449 ))
450 }
451
452 unsafe fn run(
458 rehearsal: Rehearsal,
459 paths: &Paths,
460 user_fd: RawFd,
461 net_fd: RawFd,
462 ) -> Result<(), (MountStep, i32)> {
463 unsafe {
465 let errno = || *libc::__errno_location();
466 if let Err(error) = crate::network_namespace::join_session_netns(user_fd, net_fd) {
468 return Err((MountStep::JoinSession, error.raw_os_error().unwrap_or(0)));
469 }
470 match rehearsal {
471 Rehearsal::PrivateTmp => {
475 if libc::unshare(libc::CLONE_NEWNS) != 0 {
476 return Err((MountStep::UnshareMount, errno()));
477 }
478 if make_private() != 0 {
479 return Err((MountStep::MakePrivate, errno()));
480 }
481 if libc::mount(
482 paths.tmp.as_ptr(),
483 paths.tmp.as_ptr(),
484 std::ptr::null(),
485 libc::MS_BIND | libc::MS_REC,
486 std::ptr::null(),
487 ) != 0
488 {
489 return Err((MountStep::BindTmp, errno()));
490 }
491 }
492 Rehearsal::SealedRoot => {
495 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
496 return Err((MountStep::UnshareUserMount, errno()));
497 }
498 let previous = make_dumpable_for_map_writes();
501 let _ = write_decimal_map(c"/proc/self/setgroups", None, 0);
502 let mapped =
503 write_decimal_map(c"/proc/self/uid_map", Some(paths.uid), paths.uid)
504 .is_ok()
505 && write_decimal_map(c"/proc/self/gid_map", Some(paths.gid), paths.gid)
506 .is_ok();
507 let map_errno = errno();
508 restore_dumpable(previous);
509 if !mapped {
510 return Err((MountStep::IdentityMaps, map_errno));
511 }
512 if make_private() != 0 {
513 return Err((MountStep::MakePrivateNested, errno()));
514 }
515 let mut based = None;
518 let mut tmpfs_errno = 0;
519 for (base, parked) in &paths.bases {
520 if libc::mount(
521 c"tmpfs".as_ptr(),
522 base.as_ptr(),
523 c"tmpfs".as_ptr(),
524 libc::MS_NOSUID | libc::MS_NODEV,
525 c"mode=0755".as_ptr().cast(),
526 ) == 0
527 {
528 based = Some((base, parked));
529 break;
530 }
531 tmpfs_errno = errno();
532 }
533 let Some((base, parked)) = based else {
534 return Err((MountStep::Tmpfs, tmpfs_errno));
535 };
536 if libc::mkdir(parked.as_ptr(), 0o700) != 0 {
537 return Err((MountStep::PivotPrepare, errno()));
538 }
539 if libc::chdir(base.as_ptr()) != 0 {
540 return Err((MountStep::PivotPrepare, errno()));
541 }
542 if libc::syscall(
543 libc::SYS_pivot_root,
544 c".".as_ptr(),
545 OLD_ROOT_NAME_C.as_ptr(),
546 ) != 0
547 {
548 return Err((MountStep::PivotRoot, errno()));
549 }
550 if libc::chdir(c"/".as_ptr()) != 0 {
551 return Err((MountStep::PivotRoot, errno()));
552 }
553 if libc::umount2(OLD_ROOT_PIVOTED_C.as_ptr(), libc::MNT_DETACH) != 0 {
554 return Err((MountStep::DetachOldRoot, errno()));
555 }
556 }
557 }
558 Ok(())
559 }
560 }
561
562 unsafe fn make_private() -> libc::c_int {
567 unsafe {
569 libc::mount(
570 std::ptr::null(),
571 c"/".as_ptr(),
572 std::ptr::null(),
573 libc::MS_REC | libc::MS_PRIVATE,
574 std::ptr::null(),
575 )
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use crate::network_namespace::{encode_step_record, CreationStep};
584
585 fn created() -> NamespaceReading {
586 NamespaceReading {
587 network: Ok(()),
588 mount: MountAttempt::Created,
589 sealed_root: MountAttempt::Created,
590 userns_grant: UsernsGrant::Withheld,
591 attachment: ProfileAttachment::Unconfined,
592 }
593 }
594
595 fn failed(step: MountStep, errno: i32) -> MountAttempt {
596 MountAttempt::Failed(MountAttemptError::Step { step, errno })
597 }
598
599 #[test]
600 fn every_record_round_trips_and_an_unknown_code_is_none() {
601 assert_eq!(
602 decode_record(&encode_step_record(0, 0)),
603 Some(MountAttempt::Created)
604 );
605 for step in MountStep::ALL {
606 assert_eq!(
607 decode_record(&encode_step_record(step as u32, libc::EPERM)),
608 Some(failed(step, libc::EPERM))
609 );
610 }
611 assert_eq!(decode_record(&encode_step_record(99, 0)), None);
612 }
613
614 #[test]
615 fn every_step_names_its_own_call() {
616 let mut seen = std::collections::HashSet::new();
617 for step in MountStep::ALL {
618 assert!(
619 seen.insert(step.describe()),
620 "{step:?} repeats a description"
621 );
622 }
623 assert_eq!(MountStep::UnshareMount.describe(), "unshare(CLONE_NEWNS)");
624 assert_eq!(MountStep::PivotRoot.describe(), "pivot_root(2)");
625 }
626
627 #[test]
628 fn the_questions_follow_the_attempts_and_never_the_readings() {
629 for grant in UsernsGrant::ALL {
630 let reading = NamespaceReading {
631 userns_grant: *grant,
632 ..created()
633 };
634 assert!(reading.mount_namespace_created());
635 assert!(reading.sealed_root_created());
636 assert!(reading.user_mount_namespace_usable());
637 assert_eq!(reading.egress_namespace_failure(), None);
638 assert_eq!(reading.first_failure(), None);
639 }
640 }
641
642 #[test]
643 fn a_network_failure_creates_nothing_after_it() {
644 let reading = NamespaceReading {
645 network: Err(NamespaceAttemptError::Step {
646 step: CreationStep::Unshare,
647 errno: libc::EPERM,
648 }),
649 mount: MountAttempt::NotAttempted,
650 sealed_root: MountAttempt::NotAttempted,
651 ..created()
652 };
653 assert!(!reading.mount_namespace_created());
654 assert!(!reading.sealed_root_created());
655 assert!(!reading.user_mount_namespace_usable());
656 assert_eq!(
657 reading.first_failure().as_deref(),
658 cfg!(target_os = "linux")
659 .then_some("unshare(CLONE_NEWUSER | CLONE_NEWNET) failed with EPERM")
660 );
661 }
662
663 #[test]
664 fn the_user_mount_question_needs_the_nested_namespace_its_maps_and_propagation() {
665 for (step, usable) in [
666 (MountStep::JoinSession, false),
667 (MountStep::UnshareUserMount, false),
668 (MountStep::IdentityMaps, false),
669 (MountStep::MakePrivateNested, false),
670 (MountStep::Tmpfs, true),
671 (MountStep::PivotPrepare, true),
672 (MountStep::PivotRoot, true),
673 (MountStep::DetachOldRoot, true),
674 ] {
675 let reading = NamespaceReading {
676 sealed_root: failed(step, libc::EPERM),
677 ..created()
678 };
679 assert_eq!(reading.user_mount_namespace_usable(), usable, "{step:?}");
680 assert!(!reading.sealed_root_created());
681 assert!(reading.mount_namespace_created());
682 }
683 let reading = NamespaceReading {
684 sealed_root: MountAttempt::Failed(MountAttemptError::Rehearsal("x".to_string())),
685 ..created()
686 };
687 assert!(!reading.user_mount_namespace_usable());
688 }
689
690 #[test]
691 fn off_linux_nothing_is_created_and_nothing_is_refused() {
692 let reading = NamespaceReading::not_linux();
693 assert!(!reading.mount_namespace_created());
694 assert!(!reading.sealed_root_created());
695 assert!(!reading.user_mount_namespace_usable());
696 assert!(reading.mount.failure_summary().is_none());
697 assert!(reading.sealed_root.failure_summary().is_none());
698 #[cfg(not(target_os = "linux"))]
699 assert_eq!(probe_namespaces(), NamespaceReading::not_linux());
700 }
701
702 #[test]
703 fn a_failed_attempt_summarises_as_its_call_and_errno() {
704 assert_eq!(
705 failed(MountStep::MakePrivate, libc::EACCES)
706 .failure_summary()
707 .as_deref(),
708 Some("making / MS_REC|MS_PRIVATE failed with EACCES")
709 );
710 assert_eq!(
711 probe_failure_sentence(MountStep::PivotRoot.describe(), libc::EPERM),
712 "The probe's pivot_root(2) failed with EPERM (Operation not permitted)."
713 );
714 }
715
716 #[cfg(target_os = "linux")]
717 const INNER_RUN_ENV: &str = "MURMUR_NAMESPACE_PROBE_UNCHANGED_INNER";
718
719 #[cfg(target_os = "linux")]
725 #[test]
726 fn the_probe_leaves_the_calling_process_unchanged() {
727 let exe = std::env::current_exe().expect("test binary path");
728 let output = std::process::Command::new(exe)
729 .args([
730 "--exact",
731 "namespace_probe::tests::inner_the_probe_leaves_the_calling_process_unchanged",
732 "--ignored",
733 "--test-threads=1",
734 "--nocapture",
735 ])
736 .env(INNER_RUN_ENV, "1")
737 .output()
738 .expect("re-exec the test binary for the single-threaded inner run");
739 let stdout = String::from_utf8_lossy(&output.stdout);
740 let stderr = String::from_utf8_lossy(&output.stderr);
741 assert!(
742 output.status.success(),
743 "inner probe run failed\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}"
744 );
745 assert!(
746 stdout.contains("1 passed"),
747 "inner run did not execute the probe test\n--- stdout ---\n{stdout}"
748 );
749 }
750
751 #[cfg(target_os = "linux")]
752 #[test]
753 #[ignore = "run by the_probe_leaves_the_calling_process_unchanged"]
754 fn inner_the_probe_leaves_the_calling_process_unchanged() {
755 if std::env::var(INNER_RUN_ENV).is_err() {
756 return;
757 }
758 let namespaces = || {
759 ["user", "net", "mnt"].map(|ns| std::fs::read_link(format!("/proc/self/ns/{ns}")).ok())
760 };
761 let fds = || {
762 std::fs::read_dir("/proc/self/fd")
763 .map(|dir| dir.count())
764 .ok()
765 };
766 let before = (namespaces(), fds());
767 let reading = probe_namespaces();
768 assert_eq!((namespaces(), fds()), before, "{reading:?}");
769 if reading.network.is_err() {
770 assert_eq!(reading.mount, MountAttempt::NotAttempted);
771 assert_eq!(reading.sealed_root, MountAttempt::NotAttempted);
772 }
773 }
774}