Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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)");
    }
}