Skip to main content

reverie_process/
mount.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use core::convert::Infallible;
10use core::fmt;
11use core::ptr;
12use core::str::FromStr;
13use std::collections::HashMap;
14use std::ffi::CString;
15use std::ffi::OsStr;
16use std::os::unix::ffi::OsStrExt;
17use std::path::Path;
18
19pub use nix::mount::MsFlags as MountFlags;
20use syscalls::Errno;
21
22use super::fd::FileType;
23use super::fd::create_dir_all;
24use super::fd::touch_path;
25
26/// A mount.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct Mount {
29    source: Option<CString>,
30    target: CString,
31    fstype: Option<CString>,
32    flags: MountFlags,
33    data: Option<CString>,
34    touch_target: bool,
35    allow_readonly_fallback: bool,
36    /// A path, fstype or data string that could not be represented as a C
37    /// string, recorded at BUILD time and reported at [`Mount::mount`] time.
38    ///
39    /// ⚠️ WHY A FLAG AND NOT A `Result` FROM THE BUILDER. `mount()` runs AFTER
40    /// FORK, where this module documents that it cannot allocate -- so the
41    /// failure cannot be discovered or described there. And making the builder
42    /// fallible would change `Mount::new(..) -> Self` into
43    /// `-> Result<Self, _>` for every caller, including hermit, to report a
44    /// condition none of them can do anything about except refuse the mount.
45    /// Recording one bool costs nothing after fork and turns a panic into the
46    /// `Errno` the caller already handles.
47    unrepresentable: bool,
48}
49
50/// Represents a bind mount. Can be converted into a [`Mount`].
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct Bind {
53    /// The source path of the bind mount. This path must exist. It can be either
54    /// a file or directory.
55    pub source: CString,
56
57    /// The target of the bind mount. This does not need to exist and can be
58    /// created when performing the bind mount.
59    pub target: CString,
60}
61
62/// `util::to_cstring` panics on an interior NUL. Every mount path, source,
63/// fstype and data string went through it, so a recoverable "this is not a
64/// valid C string" was a panic inside the builder. Measured on reverie main
65/// 200439dc8de9:
66///
67/// ```text
68/// Mount::new(OsStr::from_bytes(b"/test/work\0dir"))
69///   panicked at reverie-process/src/util.rs:17:41:
70///   called `Result::unwrap()` on an `Err` value: NulError(10, [...])
71/// ```
72///
73/// This returns the empty string on failure and says so, letting the caller
74/// record it and refuse the mount instead of aborting the process.
75fn checked_cstring<S: AsRef<OsStr>>(s: S) -> (CString, bool) {
76    match CString::new(s.as_ref().as_bytes()) {
77        Ok(c) => (c, true),
78        Err(_) => (CString::default(), false),
79    }
80}
81
82impl Mount {
83    /// Creates a new mount at the path `target`.
84    pub fn new<S: AsRef<OsStr>>(target: S) -> Self {
85        let (t, ok) = checked_cstring(target);
86        Self {
87            unrepresentable: !ok,
88            source: None,
89            target: t,
90            fstype: None,
91            flags: MountFlags::empty(),
92            data: None,
93            touch_target: false,
94            allow_readonly_fallback: false,
95        }
96    }
97
98    /// Creates a bind mount. This effectively creates hardlink of a directory,
99    /// making the contents accessible at both places.
100    ///
101    /// By default, none of the mounts in the `source` directory are visible in
102    /// `destination`. To make all mounts recursively visible, combine this with
103    /// [`Mount::recursive`]. Can also be used with [`Mount::readonly`] to make
104    /// the contents of `destination` read-only.
105    pub fn bind<S: AsRef<OsStr>, D: AsRef<OsStr>>(source: S, destination: D) -> Self {
106        Self::new(destination)
107            .source(source)
108            .flags(MountFlags::MS_BIND)
109    }
110
111    /// Move/rename a mount.
112    pub fn rename<S: AsRef<OsStr>, D: AsRef<OsStr>>(source: S, destination: D) -> Self {
113        Self::new(destination)
114            .source(source)
115            .flags(MountFlags::MS_MOVE)
116    }
117
118    /// Mount a fresh devpts file system. The target is usually `/dev/pts`.
119    ///
120    /// In order for this devpts to be private and independent of other devpts
121    /// (i.e., for containers), use:
122    /// ```no_compile
123    /// Mount::devpts("/dev/pts").data("newinstance,ptmxmode=0666")
124    /// ```
125    /// And either make `/dev/ptmx` a symlink pointing to `/dev/pts/ptmx` or
126    /// bind-mount it.
127    ///
128    /// See also: <https://www.kernel.org/doc/Documentation/filesystems/devpts.txt>
129    pub fn devpts<S: AsRef<OsStr>>(target: S) -> Self {
130        Self::new(target).fstype("devpts")
131    }
132
133    /// Mount a fresh proc file system at `/proc`.
134    pub fn proc() -> Self {
135        Self::new("/proc").fstype("proc")
136    }
137
138    /// Mount an overlay file system.
139    ///
140    /// NOTE: This only works in Linux 5.11 or newer when mounted from a user
141    /// namespace. Otherwise, you need real root privileges to mount an
142    /// overlayfs.
143    ///
144    /// An overlay filesystem combines two filesystems - an upper filesystem and
145    /// a lower filesystem. When a name exists in both filesystems, the object
146    /// in the upper filesystem is visible while the object in the lower
147    /// filesystem is either hidden or, in the case of directories, merged with
148    /// the upper object.
149    ///
150    /// In other words, the `lowerdir` and `upperdir` are combined into a
151    /// directory `merged` using `workdir` as a temporary work area.
152    ///
153    /// The lower filesystem can be any filesystem supported by Linux and does
154    /// not need to be writable. The lower filesystem can even be another
155    /// overlayfs. The upper filesystem should be writable.
156    ///
157    /// See <https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html> for
158    /// more information.
159    ///
160    /// # Arguments
161    ///
162    /// * `lowerdir` - The lower directory of the overlay. Can be any filesystem
163    ///   and does not need to be writable. This directory is never
164    ///   modified by writes to `merged`.
165    /// * `upperdir` - The upper directory of the overlay. This is where all
166    ///   changes to `merged` are collected. Does not need to be
167    ///   empty, but should be when starting a new overlay from
168    ///   scratch.
169    /// * `workdir`  - The work directory. This should always be empty.
170    /// * `merged`   - The combination of `lowerdir` and `upperdir`.
171    pub fn overlay(lowerdir: &Path, upperdir: &Path, workdir: &Path, merged: &Path) -> Self {
172        // TODO: Since there can actually be multiple lowerdirs, it might be
173        // more ergonomic to return an `OverlayBuilder` instead.
174        let options = format!(
175            "lowerdir={},upperdir={},workdir={}",
176            lowerdir.display(),
177            upperdir.display(),
178            workdir.display()
179        );
180
181        Self::new(merged)
182            .fstype("overlay")
183            .source("overlay")
184            .data(options)
185    }
186
187    /// Creates a temporary file system at the location specified.
188    pub fn tmpfs<S: AsRef<OsStr>>(target: S) -> Self {
189        Self::new(target).fstype("tmpfs")
190    }
191
192    /// Creates a sys file system at the location specified. The target directory
193    /// is usually `/sys`. This is useful when creating a network namespace.
194    pub fn sysfs<S: AsRef<OsStr>>(target: S) -> Self {
195        Self::new(target).fstype("sysfs")
196    }
197
198    /// Sets the mount point target.
199    pub fn target<S: AsRef<OsStr>>(mut self, target: S) -> Self {
200        let (t, ok) = checked_cstring(target);
201        self.target = t;
202        self.unrepresentable |= !ok;
203        self
204    }
205
206    /// Returns the mount point target path.
207    pub fn get_target(&self) -> &Path {
208        Path::new(OsStr::from_bytes(self.target.to_bytes()))
209    }
210
211    /// Sets the source of the mount.
212    pub fn source<S: AsRef<OsStr>>(mut self, path: S) -> Self {
213        let (v, ok) = checked_cstring(path);
214        self.source = Some(v);
215        self.unrepresentable |= !ok;
216        self
217    }
218
219    /// Returns the mount point source path (if any).
220    pub fn get_source(&self) -> Option<&Path> {
221        self.source
222            .as_ref()
223            .map(|s| Path::new(OsStr::from_bytes(s.to_bytes())))
224    }
225
226    /// Indicates that the target of a bind mount should be created
227    /// automatically.
228    pub fn touch_target(mut self) -> Self {
229        self.touch_target = true;
230        self
231    }
232
233    /// Adds mount flags.
234    pub fn flags(mut self, flags: MountFlags) -> Self {
235        self.flags |= flags;
236        self
237    }
238
239    /// Make the file system read-only.
240    pub fn readonly(mut self) -> Self {
241        self.flags |= MountFlags::MS_RDONLY;
242        self
243    }
244
245    // TODO-HUMAN-REVIEW(PR-615)
246    /// Allows a new writable proc mount that fails with `EPERM` to retry read-only.
247    ///
248    /// This explicitly permits the resulting mount to be less capable than
249    /// requested. It has no effect on non-proc or already-read-only mounts,
250    /// remounts, bind mounts, moves, or propagation changes.
251    pub fn allow_readonly_fallback(mut self) -> Self {
252        self.allow_readonly_fallback = true;
253        self
254    }
255
256    /// Makes a bind mount recursive.
257    pub fn recursive(mut self) -> Self {
258        self.flags |= MountFlags::MS_REC;
259        self
260    }
261
262    /// Makes this mount point private. Mount and unmount events do not propagate
263    /// into or out of this mount point.
264    pub fn private(mut self) -> Self {
265        self.flags |= MountFlags::MS_PRIVATE;
266        self
267    }
268
269    /// Make this mount point shared. Mount and unmount events immediately under
270    /// this mount point will propagate to the other mount points that are
271    /// members of this mount's peer group. Propagation here means that the same
272    /// mount or unmount will automatically occur under all of the other mount
273    /// points in the peer group. Conversely, mount and unmount events that take
274    /// place under peer mount points will propagate to this mount point.
275    pub fn shared(mut self) -> Self {
276        self.flags |= MountFlags::MS_SHARED;
277        self
278    }
279
280    /// Same as specifying both [`Mount::recursive`] and [`Mount::private`].
281    pub fn rprivate(mut self) -> Self {
282        self.flags |= MountFlags::MS_REC | MountFlags::MS_PRIVATE;
283        self
284    }
285
286    /// Same as specifying both [`Mount::recursive`] and [`Mount::shared`].
287    pub fn rshared(mut self) -> Self {
288        self.flags |= MountFlags::MS_REC | MountFlags::MS_SHARED;
289        self
290    }
291
292    /// Sets the filesystem type.
293    pub fn fstype<S: AsRef<OsStr>>(mut self, fstype: S) -> Self {
294        let (v, ok) = checked_cstring(fstype);
295        self.fstype = Some(v);
296        self.unrepresentable |= !ok;
297        self
298    }
299
300    /// Sets any additional data required by the mount.
301    pub fn data<S: AsRef<OsStr>>(mut self, data: S) -> Self {
302        let (v, ok) = checked_cstring(data);
303        self.data = Some(v);
304        self.unrepresentable |= !ok;
305        self
306    }
307
308    fn source_ptr(&self) -> *const libc::c_char {
309        self.source.as_ref().map_or(ptr::null(), |s| s.as_ptr())
310    }
311
312    fn target_ptr(&self) -> *const libc::c_char {
313        self.target.as_ptr()
314    }
315
316    fn fstype_ptr(&self) -> *const libc::c_char {
317        self.fstype.as_ref().map_or(ptr::null(), |s| s.as_ptr())
318    }
319
320    fn data_ptr(&self) -> *const libc::c_void {
321        self.data
322            .as_ref()
323            .map_or(ptr::null(), |s| s.as_ptr() as *const libc::c_void)
324    }
325
326    /// Performs the mount. For bind-mount operations, the target directory or
327    /// file is created if [`touch_target`] was used.
328    ///
329    /// NOTE: This function *must* not allocate since it is called after `fork`
330    /// (or `clone`) and before `execve`. Any allocations could cause deadlocks
331    /// (which are hard to track down).
332    /// The flags the kernel will not let a read-only bind remount drop, read back
333    /// from the mount that now exists at our target.
334    ///
335    /// ⚠️ WITHOUT THIS, A READ-ONLY BIND OF A `nosuid` OR `nodev` SOURCE FAILS
336    /// WITH EPERM AND THE WHOLE CONTAINER NEVER SPAWNS. Inside a user namespace
337    /// the kernel locks these flags, and `do_remount` refuses any remount that
338    /// would clear one; passing only `MS_RDONLY` asks to clear every other flag
339    /// the source had. Re-supplying them asks for exactly what is already there,
340    /// which is permitted.
341    ///
342    /// Measured 2026-08-27: this cost a whole validate arm. Hermit places its
343    /// frozen `/etc/group` and empty nscd directory in TMPDIR and binds each
344    /// read-only, so a TMPDIR on `/run/user/<uid>` -- `nosuid,nodev` on any
345    /// systemd host -- failed every container spawn. 610 of that arm's 612 e2e
346    /// rows came from this single mount, each one reading as a test result while
347    /// measuring nothing. `nosuid` alone and `nodev` alone were each sufficient.
348    ///
349    /// ⚠️ `statfs` RATHER THAN `/proc/self/mountinfo` BECAUSE THIS RUNS AFTER
350    /// FORK, where the surrounding contract forbids allocation. `statfs` is one
351    /// syscall onto a caller-owned buffer and parses nothing.
352    ///
353    /// ⚠️ AND ONLY THE FLAGS WHOSE `ST_` AND `MS_` VALUES COINCIDE. That is true
354    /// for `NOSUID`, `NODEV`, `NOEXEC`, `NOATIME` and `NODIRATIME`, and FALSE for
355    /// `RELATIME`: `ST_RELATIME` is 0x1000 while `MS_RELATIME` is 0x200000, so
356    /// copying the raw bits across would set `MS_SYNCHRONOUS`-adjacent garbage
357    /// rather than the flag intended. Relatime is therefore left out; the kernel
358    /// keeps the existing atime policy when a remount names none.
359    fn locked_source_flags(&self) -> MountFlags {
360        // SAFETY: `statfs` writes only into `buffer`, and `target_ptr` is a
361        // NUL-terminated C string owned by `self` for the whole call.
362        let mut buffer = std::mem::MaybeUninit::<libc::statvfs>::uninit();
363        let flags = unsafe {
364            if libc::statvfs(self.target_ptr(), buffer.as_mut_ptr()) != 0 {
365                // Cannot read the source, so add nothing: the remount then
366                // behaves exactly as it did before this function existed.
367                return MountFlags::empty();
368            }
369            buffer.assume_init().f_flag
370        };
371        let mut preserved = MountFlags::empty();
372        for (probe, flag) in [
373            (libc::ST_NOSUID, MountFlags::MS_NOSUID),
374            (libc::ST_NODEV, MountFlags::MS_NODEV),
375            (libc::ST_NOEXEC, MountFlags::MS_NOEXEC),
376            (libc::ST_NOATIME, MountFlags::MS_NOATIME),
377            (libc::ST_NODIRATIME, MountFlags::MS_NODIRATIME),
378        ] {
379            if flags & probe != 0 {
380                preserved |= flag;
381            }
382        }
383        preserved
384    }
385
386    fn mount_with_flags(&self, flags: MountFlags) -> Result<(), Errno> {
387        // SAFETY: Every non-null pointer comes from a live `CString` owned by
388        // `self`, and `target` is always present. `mount` only borrows these
389        // buffers for the duration of the syscall.
390        Errno::result(unsafe {
391            libc::mount(
392                self.source_ptr(),
393                self.target_ptr(),
394                self.fstype_ptr(),
395                flags.bits(),
396                self.data_ptr(),
397            )
398        })?;
399
400        Ok(())
401    }
402
403    fn readonly_proc_fallback(&self, error: Errno) -> Option<MountFlags> {
404        let is_proc = self
405            .fstype
406            .as_ref()
407            .is_some_and(|fstype| fstype.as_bytes() == b"proc");
408        // These operations ignore the filesystem type, so `proc` does not
409        // identify the filesystem being changed. Only a new mount can retry.
410        let other_operations = MountFlags::MS_REMOUNT
411            | MountFlags::MS_BIND
412            | MountFlags::MS_MOVE
413            | MountFlags::MS_SHARED
414            | MountFlags::MS_PRIVATE
415            | MountFlags::MS_SLAVE
416            | MountFlags::MS_UNBINDABLE;
417        (self.allow_readonly_fallback
418            && error == Errno::EPERM
419            && is_proc
420            && !self.flags.intersects(other_operations)
421            && !self.flags.contains(MountFlags::MS_RDONLY))
422        .then_some(self.flags | MountFlags::MS_RDONLY)
423    }
424
425    pub(super) fn mount(&mut self) -> Result<(), Errno> {
426        // ⚠️ REFUSE, DO NOT PANIC. A path/fstype/data string that is not a valid
427        // C string was recorded at build time (see `unrepresentable`). This is
428        // the first point that can report it, and it is a plain flag test
429        // because this runs after fork where allocation is not available.
430        // EINVAL is what the kernel returns for a malformed mount argument, so
431        // the caller's existing error path already knows what to do with it.
432        if self.unrepresentable {
433            return Err(Errno::EINVAL);
434        }
435        // NOTE: Although we can't allocate here, we can safely *modify* `self`.
436        // When this function is called, we have forked virtual memory and any
437        // modifications we make are copy-on-write and lost when `execve` is
438        // called. Thus, this function takes `self` by mutable reference.
439        if self.flags.contains(MountFlags::MS_BIND) && self.touch_target {
440            // Bind mounts will fail unless the destination path exists, so it
441            // is convenient to create it automatically.
442            //
443            // One reason for doing this here instead of the parent process is
444            // because the target may not yet exist until we mount it. For
445            // example, if we want to create a `/tmp` (tmpfs) folder and then
446            // bind-mount some files or directories into it, pre-creating the
447            // destination directories won't work because they'll get created in
448            // a different tmpfs.
449            if let Some(src) = &self.source {
450                if FileType::new(src.as_ptr())?.is_dir() {
451                    create_dir_all(&self.target, 0o777)?;
452                } else {
453                    touch_path(&self.target, 0o666, 0o777)?;
454                }
455            }
456        }
457
458        match self.mount_with_flags(self.flags) {
459            Err(error) => match self.readonly_proc_fallback(error) {
460                Some(flags) => self.mount_with_flags(flags),
461                None => Err(error),
462            },
463            result => result,
464        }?;
465
466        // Linux ignores MS_RDONLY on the initial bind mount. Apply per-mount flags with the
467        // required bind remount so a read-only bind cannot mutate its source inode.
468        if self.flags.contains(MountFlags::MS_BIND) && self.flags.contains(MountFlags::MS_RDONLY) {
469            Errno::result(unsafe {
470                libc::mount(
471                    ptr::null(),
472                    self.target_ptr(),
473                    ptr::null(),
474                    (self.flags | MountFlags::MS_REMOUNT | self.locked_source_flags()).bits(),
475                    ptr::null(),
476                )
477            })?;
478        }
479
480        Ok(())
481    }
482}
483
484impl Bind {
485    /// Creates a new bind mount. The `target` is optional because it is often
486    /// convenient to use an identical `source` and `target` directory. If
487    /// `target` is `None`, then it is interpretted as being the same as
488    /// `source`.
489    pub fn new<S, T>(source: S, target: T) -> Self
490    where
491        S: AsRef<OsStr>,
492        T: AsRef<OsStr>,
493    {
494        let (src, src_ok) = checked_cstring(source);
495        let (tgt, tgt_ok) = checked_cstring(target);
496        debug_assert!(
497            src_ok && tgt_ok,
498            "Bind path not representable as a C string; the Mount it converts \
499             into is marked unrepresentable and its mount() will fail EINVAL"
500        );
501        Self {
502            source: src,
503            target: tgt,
504        }
505    }
506}
507
508impl From<Bind> for Mount {
509    fn from(b: Bind) -> Self {
510        let src_empty = b.source.as_bytes().is_empty();
511        let tgt_empty = b.target.as_bytes().is_empty();
512        Self {
513            source: Some(b.source),
514            target: b.target,
515            fstype: None,
516            flags: MountFlags::MS_BIND,
517            data: None,
518            touch_target: false,
519            allow_readonly_fallback: false,
520            // A Bind built from a path that was not representable as a C string
521            // holds an EMPTY CString (see `checked_cstring`). Empty source or
522            // target is never a valid bind mount, so it carries the refusal
523            // forward rather than attempting mount(2) with "".
524            unrepresentable: src_empty || tgt_empty,
525        }
526    }
527}
528
529impl From<&str> for Bind {
530    fn from(s: &str) -> Self {
531        if let Some((source, target)) = s.split_once(':') {
532            Self {
533                source: checked_cstring(source).0,
534                target: checked_cstring(target).0,
535            }
536        } else {
537            // A Rust `&str` may contain an interior NUL, so this path panicked
538            // too. An unrepresentable path becomes empty here and the Mount it
539            // converts into refuses with EINVAL.
540            let source = checked_cstring(s).0;
541            let target = source.clone();
542            Self { source, target }
543        }
544    }
545}
546
547impl FromStr for Bind {
548    type Err = Infallible;
549
550    /// Parses bind mounts of the following forms:
551    ///  1. "path/to/source"
552    ///  2. "path/to/source:path/to/dest"
553    fn from_str(s: &str) -> Result<Self, Self::Err> {
554        Ok(Self::from(s))
555    }
556}
557
558/// An error from parsing a mount.
559#[derive(thiserror::Error, Debug, Eq, PartialEq)]
560pub enum MountParseError {
561    /// The `target` key is missing. This is always required.
562    MissingTarget,
563
564    /// An invalid mount option was specified.
565    Invalid(String, Option<String>),
566}
567
568impl fmt::Display for MountParseError {
569    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
570        match self {
571            Self::MissingTarget => write!(f, "missing mount target"),
572            Self::Invalid(k, v) => match v {
573                Some(v) => write!(f, "invalid mount option '{}={}'", k, v),
574                None => write!(f, "invalid mount option '{}'", k),
575            },
576        }
577    }
578}
579
580impl FromStr for Mount {
581    type Err = MountParseError;
582
583    /// Parses a [`Mount`]. This accepts the same syntax as Docker mounts where
584    /// each mount consists of a comma-separated key-value list.
585    ///
586    /// See <https://docs.docker.com/storage/bind-mounts/> for more information.
587    fn from_str(s: &str) -> Result<Self, Self::Err> {
588        let mut map: HashMap<&str, Option<&str>> = HashMap::new();
589
590        for item in s.split(',') {
591            let item = item.trim();
592
593            if item.is_empty() {
594                continue;
595            }
596
597            let (key, value) = match item.split_once('=') {
598                Some((key, value)) => (key, Some(value)),
599                None => (item, None),
600            };
601
602            map.insert(key, value);
603        }
604
605        // The mount target is always required.
606        let mut mount = match map
607            .remove("target")
608            .or_else(|| map.remove("destination"))
609            .or_else(|| map.remove("dest"))
610            .or_else(|| map.remove("dst"))
611            .flatten()
612        {
613            Some(target) => Mount::new(target),
614            None => {
615                return Err(MountParseError::MissingTarget);
616            }
617        };
618
619        if let Some(source) = map.remove("source").or_else(|| map.remove("src")).flatten() {
620            mount = mount.source(source);
621        }
622
623        let is_bind_mount = if let Some(fstype) = map.remove("type").flatten() {
624            if fstype == "bind" {
625                true
626            } else {
627                mount = mount.fstype(fstype);
628                false
629            }
630        } else {
631            true
632        };
633
634        if is_bind_mount {
635            mount = mount.flags(MountFlags::MS_BIND);
636        }
637
638        if let Some((key, value)) = map.remove_entry("readonly") {
639            if let Some(value) = value {
640                // No value should have been specified.
641                return Err(MountParseError::Invalid(key.into(), Some(value.to_owned())));
642            }
643
644            mount = mount.readonly();
645        }
646
647        if let Some(propagation) = map.remove("bind-propagation").flatten() {
648            if !is_bind_mount {
649                return Err(MountParseError::Invalid(
650                    "bind-propagation".into(),
651                    Some(propagation.into()),
652                ));
653            }
654            let flags = match propagation {
655                "shared" => MountFlags::MS_SHARED,
656                "slave" => MountFlags::MS_SLAVE,
657                "private" => MountFlags::MS_PRIVATE,
658                "rshared" => MountFlags::MS_REC | MountFlags::MS_SHARED,
659                "rslave" => MountFlags::MS_REC | MountFlags::MS_SLAVE,
660                "rprivate" => MountFlags::MS_REC | MountFlags::MS_PRIVATE,
661                _ => {
662                    return Err(MountParseError::Invalid(
663                        "bind-propagation".into(),
664                        Some(propagation.into()),
665                    ));
666                }
667            };
668
669            mount = mount.flags(flags);
670        } else if is_bind_mount {
671            // Bind mounts are private by default. Propagation flags are a
672            // separate mount operation and are invalid on a fresh tmpfs mount.
673            mount = mount.flags(MountFlags::MS_REC | MountFlags::MS_PRIVATE);
674        }
675
676        // Any left over keys are invalid.
677        if let Some((k, v)) = map.into_iter().next() {
678            return Err(MountParseError::Invalid(k.into(), v.map(ToOwned::to_owned)));
679        }
680
681        Ok(mount)
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688
689    #[test]
690    fn getters_and_setters() {
691        let m = Mount::bind("/foo", "/bar");
692        assert_eq!(m.get_target(), Path::new("/bar"));
693        assert_eq!(m.get_source(), Some(Path::new("/foo")));
694
695        let m = m.target("/baz");
696        assert_eq!(m.get_target(), Path::new("/baz"));
697    }
698
699    #[test]
700    fn proc_mount_retries_readonly_only_after_permission_denial() {
701        let proc_mount = Mount::proc();
702        assert_eq!(proc_mount.readonly_proc_fallback(Errno::EPERM), None);
703        assert_eq!(
704            proc_mount
705                .clone()
706                .allow_readonly_fallback()
707                .readonly_proc_fallback(Errno::EPERM),
708            Some(MountFlags::MS_RDONLY)
709        );
710        assert_eq!(
711            proc_mount
712                .clone()
713                .allow_readonly_fallback()
714                .readonly_proc_fallback(Errno::ENOENT),
715            None
716        );
717        assert_eq!(
718            Mount::proc()
719                .readonly()
720                .allow_readonly_fallback()
721                .readonly_proc_fallback(Errno::EPERM),
722            None
723        );
724        assert_eq!(
725            Mount::tmpfs("/tmp")
726                .allow_readonly_fallback()
727                .readonly_proc_fallback(Errno::EPERM),
728            None
729        );
730    }
731
732    #[test]
733    fn proc_mount_readonly_fallback_excludes_other_mount_operations() {
734        let mount = Mount::proc().allow_readonly_fallback();
735        let ordinary_flags = MountFlags::MS_NOSUID | MountFlags::MS_NODEV | MountFlags::MS_NOEXEC;
736        assert_eq!(
737            mount
738                .clone()
739                .flags(ordinary_flags)
740                .readonly_proc_fallback(Errno::EPERM),
741            Some(ordinary_flags | MountFlags::MS_RDONLY)
742        );
743        for flags in [
744            MountFlags::MS_REMOUNT,
745            MountFlags::MS_BIND,
746            MountFlags::MS_MOVE,
747            MountFlags::MS_SHARED,
748            MountFlags::MS_PRIVATE,
749            MountFlags::MS_SLAVE,
750            MountFlags::MS_UNBINDABLE,
751            MountFlags::MS_REMOUNT | MountFlags::MS_BIND,
752            MountFlags::MS_BIND | MountFlags::MS_REC,
753            MountFlags::MS_SHARED | MountFlags::MS_REC,
754            MountFlags::MS_PRIVATE | MountFlags::MS_REC,
755            MountFlags::MS_SLAVE | MountFlags::MS_REC,
756            MountFlags::MS_UNBINDABLE | MountFlags::MS_REC,
757        ] {
758            assert_eq!(
759                mount
760                    .clone()
761                    .flags(flags)
762                    .readonly_proc_fallback(Errno::EPERM),
763                None,
764                "must not retry a non-creation mount operation: {flags:?}"
765            );
766        }
767    }
768
769    #[test]
770    fn parse_mount() {
771        assert_eq!(
772            Mount::from_str("type=bind,source=/foo,target=/bar,readonly"),
773            Ok(Mount::bind("/foo", "/bar").readonly().rprivate())
774        );
775        assert_eq!(
776            Mount::from_str("src=/foo,target=/bar,readonly"),
777            Ok(Mount::bind("/foo", "/bar").readonly().rprivate())
778        );
779        assert_eq!(
780            Mount::from_str("src=/foo,target=/bar,bind-propagation=rshared"),
781            Ok(Mount::bind("/foo", "/bar").rshared())
782        );
783        assert_eq!(
784            Mount::from_str("type=tmpfs,target=/tmp"),
785            Ok(Mount::tmpfs("/tmp"))
786        );
787        assert_eq!(
788            Mount::from_str("type=tmpfs,target=/tmp,bind-propagation=rprivate"),
789            Err(MountParseError::Invalid(
790                "bind-propagation".into(),
791                Some("rprivate".into())
792            ))
793        );
794        assert_eq!(
795            Mount::from_str("target=foo, ,,,"),
796            Ok(Mount::new("foo").flags(MountFlags::MS_BIND).rprivate())
797        );
798
799        assert_eq!(Mount::from_str(""), Err(MountParseError::MissingTarget));
800        assert_eq!(
801            Mount::from_str("type=bind,source=/foo,readonly"),
802            Err(MountParseError::MissingTarget)
803        );
804        assert_eq!(
805            Mount::from_str("type=tmpfs,target=/foo,wat"),
806            Err(MountParseError::Invalid("wat".into(), None))
807        );
808        assert_eq!(
809            Mount::from_str("type=tmpfs,target=/foo,readonly=wat"),
810            Err(MountParseError::Invalid(
811                "readonly".into(),
812                Some("wat".into())
813            ))
814        );
815    }
816
817    #[test]
818    fn parse_bind() {
819        assert_eq!(Bind::from("source:target"), Bind::new("source", "target"));
820        assert_eq!(Bind::from("source"), Bind::new("source", "source"));
821
822        assert_eq!(
823            Mount::from(Bind::from("source:target")),
824            Mount::bind("source", "target")
825        );
826    }
827}
828
829#[cfg(test)]
830mod unrepresentable_paths_are_refused_not_panics {
831    use std::ffi::OsStr;
832    use std::os::unix::ffi::OsStrExt;
833
834    use syscalls::Errno;
835
836    use super::Bind;
837    use super::Mount;
838
839    /// ⚠️ THIS PANICKED BEFORE, AND THE PANIC WAS THE WHOLE DEFECT. Every mount
840    /// path went through `util::to_cstring`, which unwraps `CString::new`.
841    /// Measured on reverie main 200439dc8de9:
842    ///   panicked at reverie-process/src/util.rs:17:41:
843    ///   called `Result::unwrap()` on an `Err` value: NulError(10, [...])
844    /// A recoverable "this path is not a valid C string" aborted the process.
845    #[test]
846    fn a_target_with_an_interior_nul_refuses_instead_of_panicking() {
847        let bad = OsStr::from_bytes(b"/test/work\0dir");
848        let mut m = Mount::new(bad);
849        assert_eq!(m.mount(), Err(Errno::EINVAL));
850    }
851
852    #[test]
853    fn an_unrepresentable_source_fstype_or_data_also_refuses() {
854        let bad = OsStr::from_bytes(b"bad\0value");
855        for m in [
856            Mount::new("/test").source(bad),
857            Mount::new("/test").fstype(bad),
858            Mount::new("/test").data(bad),
859        ] {
860            let mut m = m;
861            assert_eq!(m.mount(), Err(Errno::EINVAL));
862        }
863    }
864
865    /// ⚠️ THE CONTROL, WITHOUT WHICH THE THREE ABOVE PROVE NOTHING. A refusal
866    /// that fired on every mount would pass them and break every real mount.
867    /// A representable path must NOT be marked unrepresentable -- it must get
868    /// past the flag test and fail (or succeed) on the real syscall instead.
869    #[test]
870    fn a_representable_path_is_not_refused_by_this_check() {
871        let mut m = Mount::new("/test/workdir").fstype("tmpfs");
872        let got = m.mount();
873        assert_ne!(
874            got,
875            Err(Errno::EINVAL),
876            "a valid path must reach mount(2); EINVAL here means the refusal \
877             over-fired and no mount would ever work"
878        );
879    }
880
881    #[test]
882    fn a_bind_from_a_str_with_an_interior_nul_refuses() {
883        let b = Bind::from("/test/a\0b");
884        let mut m: Mount = b.into();
885        assert_eq!(m.mount(), Err(Errno::EINVAL));
886    }
887}