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, 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}
184
185impl NamespaceReading {
186    /// The reading of a host that is not Linux. See the type's documentation.
187    #[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    /// The `E-CAP-005` failure the network attempt is, or `None` when it created the namespace
200    /// (or, off Linux, when there was none to create).
201    #[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    /// Whether a command can get a private mount namespace inside the session's user namespace,
212    /// which is what maps its literal `/tmp`.
213    #[must_use]
214    pub fn mount_namespace_created(&self) -> bool {
215        self.network.is_ok() && self.mount == MountAttempt::Created
216    }
217
218    /// Whether a command can mount a private `procfs` of the session's PID namespace over
219    /// `/proc`.
220    #[must_use]
221    pub fn private_proc_created(&self) -> bool {
222        self.network.is_ok() && self.private_proc == MountAttempt::Created
223    }
224
225    /// Whether the composed root `sealed` runs in was built: the nested user+mount namespace,
226    /// its mounts and `pivot_root(2)`.
227    #[must_use]
228    pub fn sealed_root_created(&self) -> bool {
229        self.network.is_ok() && self.sealed_root == MountAttempt::Created
230    }
231
232    /// Whether a user+mount namespace, its identity maps and private propagation can be created
233    /// inside the session's namespace — the sealed-root rehearsal got past
234    /// [`MountStep::MakePrivateNested`]. What a test that builds a bind mount in its own
235    /// user+mount namespace needs to ask.
236    #[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    /// The first attempt that failed, as `<call> failed with <ERRNO>` or the runtime's account,
254    /// or `None` when none did.
255    #[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
266/// `The probe's <call> failed with <ERRNO> (<words>).`, the sentence an `E-CAP-003` for `sealed`
267/// names the failed attempt with.
268pub(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/// Reads the host's namespaces: the three attempts, in order, and the readings.
277///
278/// Every `unshare` and `setns` happens in a child, so the calling process is unchanged, and the
279/// namespace handles are closed before this returns.
280#[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                // Closes the holder's lifeline too, which ends the probe's PID namespace.
304                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/// What a rehearsal's step report says: `Created` for code `0`, the failed step otherwise, or
324/// `None` for a code no step carries.
325#[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    /// Which of the three mount rehearsals to run.
351    #[derive(Debug, Clone, Copy)]
352    pub(super) enum Rehearsal {
353        /// A command's `/tmp` mapping: `unshare(CLONE_NEWNS)`, private propagation, a bind over
354        /// `/tmp`.
355        PrivateTmp,
356        /// The composed root's nested namespace, maps, mounts and pivot.
357        SealedRoot,
358        /// A command's private `/proc`: `setns` into the PID namespace, a fork into it, then in
359        /// the forked process `unshare(CLONE_NEWNS)`, private propagation and `mount -t proc`.
360        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    /// The session namespace handles a rehearsal joins, held open by the caller.
374    #[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    /// Every path the child uses, built in the parent before `fork()`.
382    struct Paths {
383        /// `/tmp`, canonicalised.
384        tmp: CString,
385        /// Each root base candidate with its parking directory's full path.
386        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            // SAFETY: `getuid`/`getgid` take no arguments, dereference nothing and cannot fail.
408            // Read here, before the fork: inside a fresh user namespace with no map written yet
409            // they report the overflow id, which no `uid_map` accepts.
410            let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
411            Ok(Self {
412                tmp,
413                bases,
414                uid,
415                gid,
416            })
417        }
418    }
419
420    /// Runs `rehearsal` in a child that first joins the user and network namespaces `handles`
421    /// name, and reports what it did.
422    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        // SAFETY: `pipe` is a 2-element stack array, exactly the size `pipe2` writes into.
433        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        // SAFETY: both fds were just returned by the successful `pipe2` and nothing else owns
441        // them.
442        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        // SAFETY: `fork()` from a possibly-multithreaded process is sound as long as the child
447        // touches nothing but async-signal-safe primitives. The child runs only `setns`,
448        // `unshare`, `prctl`, `mount`, `open`/`write`/`close`, `mkdir`, `chdir`, `pivot_root`
449        // via `syscall`, `umount2`, `fork` and `waitpid` over paths built above, then `write`
450        // and `_exit`: no allocation, no locks, no stdio.
451        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            // SAFETY: forked-child context, syscalls only; the block ends in `_exit`, so no
461            // destructor of the parent's state runs here.
462            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        // Read after the child is reaped and without blocking: the record it wrote stays in the
483        // pipe, and another thread's child forked in the meantime may still hold the write end.
484        let mut record = [0u8; STEP_RECORD_LEN];
485        // SAFETY: `record` is a live stack buffer of `STEP_RECORD_LEN` bytes.
486        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    /// The rehearsal's steps, each failure the step and the `errno` it left.
505    ///
506    /// # Safety
507    /// Post-`fork()` child context only. Every call is async-signal-safe and nothing here
508    /// allocates: every path is in `paths`, built before the fork.
509    unsafe fn run(
510        rehearsal: Rehearsal,
511        paths: &Paths,
512        handles: Handles,
513        report_fd: RawFd,
514    ) -> Result<(), (MountStep, i32)> {
515        // SAFETY: the caller's contract covers this whole body.
516        unsafe {
517            let errno = || *libc::__errno_location();
518            // Every command joins its session's namespaces before anything else, on every tier.
519            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                // A command's relay and command: `setns` into the PID namespace, then a fork, so
526                // the mounting process is a member of the namespace its `procfs` describes.
527                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                        // The member reports for the whole rehearsal, then exits `0`.
537                        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                        // The member wrote the rehearsal's record; this child adds none.
571                        libc::_exit(0);
572                    }
573                    return Err((MountStep::ForkIntoPidNamespace, 0));
574                }
575                // `temp_dir::map_private_tmp`'s namespace and propagation, then a bind over `/tmp`
576                // with its flags: recursive, because a mount namespace owned by a less privileged
577                // user namespace refuses a plain bind of a tree with mounts beneath it (`EINVAL`).
578                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                // `sealed::construct_composed_root`'s steps 1–4, 6 and 7, with the same syscalls
597                // in the same order.
598                Rehearsal::SealedRoot => {
599                    if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
600                        return Err((MountStep::UnshareUserMount, errno()));
601                    }
602                    // Dumpable for the map writes only: a non-dumpable task's `/proc/self/*`
603                    // entries are root-owned (see `sealed::make_dumpable_for_map_writes`).
604                    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                    // The first candidate that takes a `tmpfs`; the real construction picks one
620                    // that is no ancestor of the workdir, which a rehearsal has none of.
621                    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    /// `mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)`.
667    ///
668    /// # Safety
669    /// Post-`fork()` child context only.
670    unsafe fn make_private() -> libc::c_int {
671        // SAFETY: every pointer is null or a static C string.
672        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    /// The probe runs every attempt in a child: the calling process keeps its namespaces, and no
825    /// descriptor it opened outlives it.
826    ///
827    /// `/proc/self/fd` counts every thread's descriptors, so the count is exact only with no
828    /// other test running: the check runs in a single-threaded re-exec of this binary.
829    #[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}