crates/appliance-pid1/src/mounts.rs
//! **The kernel pseudo-filesystems every distroless PID 1 mounts for itself —
//! with flags, and with its results read.**
//!
//! Written once here because three tunnr payloads (`wg-appliance`,
//! `demo-container-appliance`, `rdp-wayland-launcher`) each carried a
//! hand-copied version of the same eleven lines, and all three shared the same
//! two defects:
//!
//! 1. **The results were discarded.** `libc::mkdir(…)` and `libc::mount(…)` were
//! called as bare statements, so PID 1 could not tell whether `/proc`, `/sys`
//! or `/dev` had mounted and carried on as if they had. A missing
//! `/dev/net/tun` then looked like "the pump refused" and a missing `/proc`
//! silently turned kernel-cmdline handling off.
//! 2. **No flags.** Every mount passed `0`: no `MS_NOSUID`, no `MS_NODEV`, no
//! `MS_NOEXEC`, on the highest-privilege process in the appliance.
//!
//! The order matters and is the reason they are one module: a `mount(2)` whose
//! result is thrown away will ignore new flags just as happily, and the flags
//! would be a claim nothing checks. Results first, then flags.
//!
//! ## What this does NOT do
//!
//! It does not give the appliance a read-only root, a size-bounded `rootfs`, or
//! a hand-populated `/dev`. Those change what the appliance can do at runtime
//! (something still writes `/etc/resolv.conf`) or need kernel configuration, and
//! they are decisions for the payload, not defaults to inherit silently.
use std::fmt;
/// One kernel pseudo-filesystem a PID 1 mounts for itself, and the flags it must
/// carry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PseudoFs {
/// The `source` argument to `mount(2)` (a pseudo-filesystem ignores it, but
/// it is what shows up in `/proc/mounts`).
pub source: &'static str,
/// Where it is mounted.
pub target: &'static str,
/// The filesystem type.
pub fstype: &'static str,
/// Mode for the mount point, created before mounting over it.
pub dir_mode: libc::mode_t,
/// `MS_*` flags handed to `mount(2)`.
pub flags: libc::c_ulong,
}
/// The three pseudo-filesystems, and the flags each is mounted with.
///
/// `nosuid` and `noexec` everywhere: a distroless appliance execs nothing and
/// owns no setuid binary, so allowing either is granting a post-exploit
/// primitive for free. `nodev` everywhere **except `/dev`** — `/dev` *is* the
/// device tree, and `MS_NODEV` there would make `/dev/net/tun` unopenable and
/// the tunnel impossible. `/sys` is additionally read-only: no nordisk payload
/// writes to sysfs (interfaces are configured over netlink and ioctls).
pub const PSEUDO_FILESYSTEMS: [PseudoFs; 3] = [
PseudoFs {
source: "proc",
target: "/proc",
fstype: "proc",
dir_mode: 0o555,
flags: libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
},
PseudoFs {
source: "sysfs",
target: "/sys",
fstype: "sysfs",
dir_mode: 0o555,
flags: libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC | libc::MS_RDONLY,
},
PseudoFs {
source: "devtmpfs",
target: "/dev",
fstype: "devtmpfs",
dir_mode: 0o755,
flags: libc::MS_NOSUID | libc::MS_NOEXEC,
},
];
/// A pseudo-filesystem that could not be created or mounted.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountFailure {
/// The mount point (or directory) that failed.
pub target: &'static str,
/// The filesystem type, or `"mkdir"` when it was the mount point that could
/// not be created.
pub kind: &'static str,
/// The errno the syscall reported — the value that used to be thrown away.
pub errno: i32,
}
impl fmt::Display for MountFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({}): errno {}", self.target, self.kind, self.errno)
}
}
/// The flag set spelled the way an operator reads it in `/proc/mounts`, so the
/// posture the appliance actually mounted with can be printed on the serial log
/// and asserted by a boot contract — rather than being a claim only the source
/// makes.
#[must_use]
pub fn flag_names(flags: libc::c_ulong) -> String {
let mut v = Vec::new();
for (bit, name) in [
(libc::MS_RDONLY, "ro"),
(libc::MS_NOSUID, "nosuid"),
(libc::MS_NODEV, "nodev"),
(libc::MS_NOEXEC, "noexec"),
] {
if flags & bit != 0 {
v.push(name);
}
}
if v.is_empty() {
"(none)".to_string()
} else {
v.join(",")
}
}
/// `mkdir(2)`, treating "it is already there" as success.
///
/// `EEXIST` is the normal case for `/proc` and `/etc` in a populated initramfs,
/// so treating it as a failure would make a checked PID 1 refuse every boot.
/// Every other errno is returned.
///
/// # Errors
/// The raw errno, when the directory could not be created and did not exist.
pub fn mkdir_checked(dir: &str, mode: libc::mode_t) -> Result<(), i32> {
let c = std::ffi::CString::new(dir).map_err(|_| libc::EINVAL)?;
// SAFETY: `c` is a NUL-terminated C string owned by a local that outlives
// the call, and `mkdir(2)` only READS through that pointer — it writes
// nothing back, keeps no reference after returning, and `mode` is a
// `mode_t` by type. The only reason this is `unsafe` is the `extern "C"`
// boundary.
let rc = unsafe { libc::mkdir(c.as_ptr(), mode) };
if rc == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if err == libc::EEXIST {
Ok(())
} else {
Err(err)
}
}
/// `mount(2)` one pseudo-filesystem, **reading the result**.
///
/// # Errors
/// The raw errno the kernel reported.
pub fn mount_one(fs: &PseudoFs) -> Result<(), i32> {
let s = std::ffi::CString::new(fs.source).map_err(|_| libc::EINVAL)?;
let t = std::ffi::CString::new(fs.target).map_err(|_| libc::EINVAL)?;
let f = std::ffi::CString::new(fs.fstype).map_err(|_| libc::EINVAL)?;
// SAFETY: the three pointers are NUL-terminated C strings owned by locals
// that outlive the call, and `mount(2)` only READS through them. The `data`
// argument is NULL, which each of these filesystem types accepts as "no
// fs-specific options" (it is the same argument the payloads already passed
// before this crate existed), and `flags` is a bitwise OR of `libc`'s own
// `MS_*` constants, so it cannot name a flag the kernel does not have.
// Nothing is written back through any pointer and no pointer outlives the
// call.
let rc = unsafe {
libc::mount(
s.as_ptr(),
t.as_ptr(),
f.as_ptr(),
fs.flags,
std::ptr::null(),
)
};
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))
}
}
/// **Mount `/proc`, `/sys` and `/dev`, reading every result**, plus any extra
/// directories the payload needs (`/dev/net` for `/dev/net/tun`, `/etc` for
/// `resolv.conf`) — each given as `(path, mode)`.
///
/// Returns one `"<target> <fstype> <flags>"` line per filesystem, for the caller
/// to prefix and print: the posture is then on the serial log, where a boot
/// contract can assert it.
///
/// # Errors
/// The first filesystem that could not be created or mounted. A PID 1 should
/// treat this as a **refusal**: a pseudo-filesystem that did not mount means
/// this is not the environment the appliance was assured in, and coming up
/// degraded-but-green is the failure mode the whole exercise is about.
pub fn mount_pseudo_filesystems(
extra_dirs: &[(&'static str, libc::mode_t)],
) -> Result<Vec<String>, MountFailure> {
let mut lines = Vec::with_capacity(PSEUDO_FILESYSTEMS.len());
for fs in &PSEUDO_FILESYSTEMS {
mkdir_checked(fs.target, fs.dir_mode).map_err(|errno| MountFailure {
target: fs.target,
kind: "mkdir",
errno,
})?;
mount_one(fs).map_err(|errno| MountFailure {
target: fs.target,
kind: fs.fstype,
errno,
})?;
lines.push(format!(
"{} {} {}",
fs.target,
fs.fstype,
flag_names(fs.flags)
));
}
for (dir, mode) in extra_dirs {
mkdir_checked(dir, *mode).map_err(|errno| MountFailure {
target: dir,
kind: "mkdir",
errno,
})?;
}
Ok(lines)
}
#[cfg(test)]
mod tests {
use super::*;
/// RED-WHEN-BROKEN — every pseudo-filesystem is `nosuid` and `noexec`. A
/// distroless appliance execs nothing and owns no setuid binary, so a mount
/// that allows either hands a post-exploit primitive over for nothing.
#[test]
fn every_pseudo_filesystem_is_mounted_nosuid_and_noexec() {
for fs in &PSEUDO_FILESYSTEMS {
assert!(
fs.flags & libc::MS_NOSUID != 0,
"{} ({}) is mounted without MS_NOSUID",
fs.target,
fs.fstype
);
assert!(
fs.flags & libc::MS_NOEXEC != 0,
"{} ({}) is mounted without MS_NOEXEC",
fs.target,
fs.fstype
);
}
}
/// RED-WHEN-BROKEN — `nodev` everywhere except `/dev`, and the exception is
/// ASSERTED rather than left to a reader's goodwill: `MS_NODEV` on `/dev`
/// would make `/dev/net/tun` unopenable and the tunnel impossible.
#[test]
fn nodev_everywhere_except_dev_itself() {
for fs in &PSEUDO_FILESYSTEMS {
if fs.target == "/dev" {
assert!(
fs.flags & libc::MS_NODEV == 0,
"/dev must NOT be MS_NODEV — /dev/net/tun lives there"
);
} else {
assert!(
fs.flags & libc::MS_NODEV != 0,
"{} ({}) is mounted without MS_NODEV",
fs.target,
fs.fstype
);
}
}
}
/// `/sys` is read-only and `/dev` is not: the appliance configures
/// interfaces over netlink and ioctls and never writes sysfs, while
/// `devtmpfs` must stay writable for `mkdir /dev/net`.
#[test]
fn sysfs_is_read_only_and_dev_is_not() {
let get = |t: &str| {
PSEUDO_FILESYSTEMS
.iter()
.find(|f| f.target == t)
.unwrap_or_else(|| panic!("{t} is in the table"))
.flags
};
assert!(get("/sys") & libc::MS_RDONLY != 0, "/sys must be ro");
assert!(
get("/dev") & libc::MS_RDONLY == 0,
"/dev must stay writable — PID 1 creates /dev/net"
);
}
/// RED-WHEN-BROKEN — the syscall result is READ. This runs the real
/// `mount(2)`: an unprivileged process gets `EPERM`, and before this crate
/// existed that errno was discarded and the caller carried on as if the
/// filesystem had mounted.
#[test]
fn a_failed_mount_surfaces_its_errno_instead_of_being_discarded() {
// SAFETY: `geteuid(2)` takes no arguments, touches no memory of ours and
// cannot fail; it is `unsafe` only for crossing the `extern "C"`
// boundary.
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipped: running as root, where the mount would succeed");
return;
}
let procfs = PSEUDO_FILESYSTEMS
.iter()
.find(|f| f.target == "/proc")
.expect("/proc is in the table");
assert_eq!(
mount_one(procfs),
Err(libc::EPERM),
"the errno must reach the caller, not be thrown away"
);
}
/// An existing directory is not a failure — `EEXIST` is the normal case for
/// `/proc` and `/etc`, and treating it as an error would refuse every boot.
#[test]
fn an_existing_directory_is_not_a_mkdir_failure() {
assert_eq!(mkdir_checked("/proc", 0o555), Ok(()));
}
/// The failure carries the three things an operator staring at a serial log
/// needs, and a discarded return value could never give them.
#[test]
fn a_failure_names_the_target_the_kind_and_the_errno() {
let f = MountFailure {
target: "/dev",
kind: "devtmpfs",
errno: libc::ENODEV,
};
let s = f.to_string();
assert!(s.contains("/dev"), "which mount point: {s}");
assert!(s.contains("devtmpfs"), "which filesystem: {s}");
assert!(s.contains(&format!("errno {}", libc::ENODEV)), "errno: {s}");
}
/// The posture line spells the flags the way `/proc/mounts` does, so what a
/// boot prints can be compared with what the kernel reports.
#[test]
fn the_posture_line_spells_the_flags_the_way_proc_mounts_does() {
assert_eq!(
flag_names(libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC),
"nosuid,nodev,noexec"
);
assert_eq!(
flag_names(libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC | libc::MS_RDONLY),
"ro,nosuid,nodev,noexec"
);
assert_eq!(flag_names(0), "(none)");
}
}