Skip to main content

capsule_runtime/
namespace_probe.rs

1//! The one place the host's namespaces are probed.
2//!
3//! Every decision that depends on what namespaces this host can give a capsule reads a
4//! [`NamespaceReading`]: the `E-CAP-005` network-namespace check, whether a command's literal
5//! `/tmp` can be mapped onto the session's temporary folder, whether `sealed` is refused
6//! (`E-CAP-003`), the enforcement tier, `mur run --explain-scope` and `mur doctor`. A reading is
7//! taken once per [`crate::sandbox::HostProbe`], so one staged session judges one reading.
8//!
9//! The probe tries, in order, exactly what a session does:
10//!
11//!   1. **network** — the session's own creator ([`crate::network_namespace`]):
12//!      `unshare(CLONE_NEWUSER | CLONE_NEWNET)`, identity maps, `lo`, the local route, the
13//!      sockets and the hand-off.
14//!   2. **mount** — in a child that joins that namespace the way every command does
15//!      (`join_session_netns`), the private mount namespace a command's `/tmp` mapping builds:
16//!      `unshare(CLONE_NEWNS)`, `MS_REC | MS_PRIVATE`, a recursive bind over `/tmp`.
17//!   3. **sealed root** — in a second joining child, the composed root's nested construction:
18//!      `unshare(CLONE_NEWUSER | CLONE_NEWNS)`, identity maps, `MS_REC | MS_PRIVATE`, a `tmpfs`,
19//!      `pivot_root(2)`, detaching the old root.
20//!   4. **private proc** — in a third joining child that also enters the session's PID namespace
21//!      and forks into it, as a command's relay does: `unshare(CLONE_NEWNS)`, `MS_REC |
22//!      MS_PRIVATE`, and `mount -t proc` over `/proc` with the `hidepid` spellings
23//!      [`crate::proc_view`] mounts with.
24//!
25//! The creator the network attempt runs also creates the session's PID namespace and its holder,
26//! so that attempt exercises the PID namespace too; dropping its handles ends the holder.
27//!
28//! The mount attempts run inside the probe's network namespace because no command runs outside
29//! one: a command joins its session's namespaces first, on every tier, and only then builds its
30//! mount namespace. A host that cannot create the network namespace attempts none of them.
31//!
32//! AppArmor's unprivileged-userns restriction and the profile confining `mur` are read with the
33//! attempts, and only ever choose the words of a refusal: no decision reads them.
34
35#[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/// One step of a mount rehearsal, as the child reports it when that step fails.
43///
44/// `JoinSession` is shared by both rehearsals; `UnshareMount`, `MakePrivate` and `BindTmp` are the
45/// `/tmp` mapping's; the rest are the composed root's.
46#[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    /// `setns(2)` into the session's PID namespace.
60    JoinPidNamespace = 12,
61    /// Forking into the session's PID namespace.
62    ForkIntoPidNamespace = 13,
63    /// `mount -t proc` over `/proc`, every `hidepid` spelling refused.
64    MountProc = 14,
65}
66
67impl MountStep {
68    // Only the Linux probe decodes a step; tests on every OS check the encoding round-trips.
69    #[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    /// The step a rehearsal record's code names, or `None` for a code no step carries.
88    #[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    /// The call or operation, as a refusal names it.
94    #[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/// Why a mount rehearsal produced no namespace.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum MountAttemptError {
120    /// The rehearsal ran, and `step` failed with `errno`.
121    Step { step: MountStep, errno: i32 },
122    /// The rehearsal child could not be forked, piped or waited for, or exited without reporting;
123    /// the runtime's own account of it.
124    Rehearsal(String),
125}
126
127/// What one mount rehearsal did.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum MountAttempt {
130    /// Every step succeeded.
131    Created,
132    /// A step failed, or the rehearsal could not run.
133    Failed(MountAttemptError),
134    /// Not tried: the network namespace it nests in could not be created, or the host is not
135    /// Linux.
136    NotAttempted,
137}
138
139impl MountAttempt {
140    /// `<call> failed with <ERRNO>`, the rehearsal's own account, or `None` when nothing failed:
141    /// the one-line answer `mur doctor` prints.
142    #[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/// The host's namespaces as one probe found them: the three attempts, in the order a session
157/// makes them, and the two readings a refusal is worded from.
158///
159/// Off Linux nothing is attempted and nothing fails: `network` is `Ok(())` because no attempt
160/// failed — there is no network namespace to create — and both mount attempts are
161/// [`MountAttempt::NotAttempted`]. Every question asked of the reading therefore answers that no
162/// namespace was created and none was refused: [`Self::mount_namespace_created`] and
163/// [`Self::sealed_root_created`] are `false` and [`Self::egress_namespace_failure`] is `None`.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct NamespaceReading {
166    /// The session's network namespace creation.
167    pub network: Result<(), NamespaceAttemptError>,
168    /// The private mount namespace a command's `/tmp` mapping builds, inside the network
169    /// namespace.
170    pub mount: MountAttempt,
171    /// The composed root's nested user+mount namespace and `pivot_root(2)`, inside the network
172    /// namespace.
173    pub sealed_root: MountAttempt,
174    /// A private `procfs` mounted over `/proc` by a process forked into the session's PID
175    /// namespace, in its own mount namespace: what decides [`crate::proc_view::proc_view`].
176    pub private_proc: MountAttempt,
177    /// AppArmor's unprivileged-userns restriction as read with the attempts. Words a refusal;
178    /// decides nothing.
179    pub userns_grant: UsernsGrant,
180    /// The profile the kernel reports as confining this process, read with the attempts. Words a
181    /// refusal; decides nothing.
182    pub attachment: ProfileAttachment,
183    /// Why no `mur-sealed` profile confines this process, read with the attempts. Words a
184    /// refusal's remedy; decides nothing.
185    pub profile_gap: ProfileGap,
186}
187
188impl NamespaceReading {
189    /// The reading of a host that is not Linux. See the type's documentation.
190    #[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    /// The `E-CAP-005` failure the network attempt is, or `None` when it created the namespace
204    /// (or, off Linux, when there was none to create).
205    #[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    /// Whether a command can get a private mount namespace inside the session's user namespace,
217    /// which is what maps its literal `/tmp`.
218    #[must_use]
219    pub fn mount_namespace_created(&self) -> bool {
220        self.network.is_ok() && self.mount == MountAttempt::Created
221    }
222
223    /// Whether a command can mount a private `procfs` of the session's PID namespace over
224    /// `/proc`.
225    #[must_use]
226    pub fn private_proc_created(&self) -> bool {
227        self.network.is_ok() && self.private_proc == MountAttempt::Created
228    }
229
230    /// Whether the composed root `sealed` runs in was built: the nested user+mount namespace,
231    /// its mounts and `pivot_root(2)`.
232    #[must_use]
233    pub fn sealed_root_created(&self) -> bool {
234        self.network.is_ok() && self.sealed_root == MountAttempt::Created
235    }
236
237    /// Whether a user+mount namespace, its identity maps and private propagation can be created
238    /// inside the session's namespace — the sealed-root rehearsal got past
239    /// [`MountStep::MakePrivateNested`]. What a test that builds a bind mount in its own
240    /// user+mount namespace needs to ask.
241    #[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    /// The first attempt that failed, as `<call> failed with <ERRNO>` or the runtime's account,
259    /// or `None` when none did.
260    #[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
271/// `The probe's <call> failed with <ERRNO> (<words>).`, the sentence an `E-CAP-003` for `sealed`
272/// names the failed attempt with.
273pub(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/// Reads the host's namespaces: the three attempts, in order, and the readings.
282///
283/// Every `unshare` and `setns` happens in a child, so the calling process is unchanged, and the
284/// namespace handles are closed before this returns.
285#[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                // Closes the holder's lifeline too, which ends the probe's PID namespace.
309                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/// What a rehearsal's step report says: `Created` for code `0`, the failed step otherwise, or
330/// `None` for a code no step carries.
331#[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    /// Which of the three mount rehearsals to run.
357    #[derive(Debug, Clone, Copy)]
358    pub(super) enum Rehearsal {
359        /// A command's `/tmp` mapping: `unshare(CLONE_NEWNS)`, private propagation, a bind over
360        /// `/tmp`.
361        PrivateTmp,
362        /// The composed root's nested namespace, maps, mounts and pivot.
363        SealedRoot,
364        /// A command's private `/proc`: `setns` into the PID namespace, a fork into it, then in
365        /// the forked process `unshare(CLONE_NEWNS)`, private propagation and `mount -t proc`.
366        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    /// The session namespace handles a rehearsal joins, held open by the caller.
380    #[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    /// Every path the child uses, built in the parent before `fork()`.
388    struct Paths {
389        /// `/tmp`, canonicalised.
390        tmp: CString,
391        /// Each root base candidate with its parking directory's full path.
392        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            // SAFETY: `getuid`/`getgid` take no arguments, dereference nothing and cannot fail.
414            // Read here, before the fork: inside a fresh user namespace with no map written yet
415            // they report the overflow id, which no `uid_map` accepts.
416            let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
417            Ok(Self {
418                tmp,
419                bases,
420                uid,
421                gid,
422            })
423        }
424    }
425
426    /// Runs `rehearsal` in a child that first joins the user and network namespaces `handles`
427    /// name, and reports what it did.
428    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        // SAFETY: `pipe` is a 2-element stack array, exactly the size `pipe2` writes into.
439        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        // SAFETY: both fds were just returned by the successful `pipe2` and nothing else owns
447        // them.
448        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        // SAFETY: `fork()` from a possibly-multithreaded process is sound as long as the child
453        // touches nothing but async-signal-safe primitives. The child runs only `setns`,
454        // `unshare`, `prctl`, `mount`, `open`/`write`/`close`, `mkdir`, `chdir`, `pivot_root`
455        // via `syscall`, `umount2`, `fork` and `waitpid` over paths built above, then `write`
456        // and `_exit`: no allocation, no locks, no stdio.
457        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            // SAFETY: forked-child context, syscalls only; the block ends in `_exit`, so no
467            // destructor of the parent's state runs here.
468            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        // Read after the child is reaped and without blocking: the record it wrote stays in the
489        // pipe, and another thread's child forked in the meantime may still hold the write end.
490        let mut record = [0u8; STEP_RECORD_LEN];
491        // SAFETY: `record` is a live stack buffer of `STEP_RECORD_LEN` bytes.
492        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    /// The rehearsal's steps, each failure the step and the `errno` it left.
511    ///
512    /// # Safety
513    /// Post-`fork()` child context only. Every call is async-signal-safe and nothing here
514    /// allocates: every path is in `paths`, built before the fork.
515    unsafe fn run(
516        rehearsal: Rehearsal,
517        paths: &Paths,
518        handles: Handles,
519        report_fd: RawFd,
520    ) -> Result<(), (MountStep, i32)> {
521        // SAFETY: the caller's contract covers this whole body.
522        unsafe {
523            let errno = || *libc::__errno_location();
524            // Every command joins its session's namespaces before anything else, on every tier.
525            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                // A command's relay and command: `setns` into the PID namespace, then a fork, so
532                // the mounting process is a member of the namespace its `procfs` describes.
533                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                        // The member reports for the whole rehearsal, then exits `0`.
543                        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                        // The member wrote the rehearsal's record; this child adds none.
577                        libc::_exit(0);
578                    }
579                    return Err((MountStep::ForkIntoPidNamespace, 0));
580                }
581                // `temp_dir::map_private_tmp`'s namespace and propagation, then a bind over `/tmp`
582                // with its flags: recursive, because a mount namespace owned by a less privileged
583                // user namespace refuses a plain bind of a tree with mounts beneath it (`EINVAL`).
584                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                // `sealed::construct_composed_root`'s steps 1–4, 6 and 7, with the same syscalls
603                // in the same order.
604                Rehearsal::SealedRoot => {
605                    if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
606                        return Err((MountStep::UnshareUserMount, errno()));
607                    }
608                    // Dumpable for the map writes only: a non-dumpable task's `/proc/self/*`
609                    // entries are root-owned (see `sealed::make_dumpable_for_map_writes`).
610                    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                    // The first candidate that takes a `tmpfs`; the real construction picks one
626                    // that is no ancestor of the workdir, which a rehearsal has none of.
627                    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    /// `mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)`.
673    ///
674    /// # Safety
675    /// Post-`fork()` child context only.
676    unsafe fn make_private() -> libc::c_int {
677        // SAFETY: every pointer is null or a static C string.
678        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    /// The probe runs every attempt in a child: the calling process keeps its namespaces, and no
832    /// descriptor it opened outlives it.
833    ///
834    /// `/proc/self/fd` counts every thread's descriptors, so the count is exact only with no
835    /// other test running: the check runs in a single-threaded re-exec of this binary.
836    #[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}