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}