Skip to main content

tatara_init/
mounts.rs

1//! Early-boot filesystem mounts — `/proc`, `/sys`, `/dev`, `/run`, `/tmp`.
2//!
3//! Linux initramfs starts with just the cpio contents on a tmpfs — no
4//! virtual filesystems mounted. Userspace tools that open `/dev/null`,
5//! `/proc/self`, `/sys/class/*` etc. fail without these. tatara-init calls
6//! `mount_early_filesystems()` before spawning any supervised service.
7//!
8//! Non-Linux builds are no-ops so the crate compiles on macOS for dev.
9
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub enum EarlyMountError {
14    #[error("mount({target}): {reason}")]
15    Mount { target: String, reason: String },
16}
17
18/// One declarative mount description.
19#[derive(Debug, Clone)]
20pub struct EarlyMount {
21    pub source: &'static str,
22    pub target: &'static str,
23    pub fstype: &'static str,
24    pub flags: u64,
25    pub data: &'static str,
26}
27
28/// The canonical set a Linux guest needs before any service starts.
29pub const CANONICAL_MOUNTS: &[EarlyMount] = &[
30    EarlyMount {
31        source: "proc",
32        target: "/proc",
33        fstype: "proc",
34        flags: 0,
35        data: "",
36    },
37    EarlyMount {
38        source: "sysfs",
39        target: "/sys",
40        fstype: "sysfs",
41        flags: 0,
42        data: "",
43    },
44    EarlyMount {
45        source: "devtmpfs",
46        target: "/dev",
47        fstype: "devtmpfs",
48        flags: 0,
49        data: "mode=0755",
50    },
51    EarlyMount {
52        source: "tmpfs",
53        target: "/run",
54        fstype: "tmpfs",
55        flags: 0,
56        data: "mode=0755",
57    },
58    EarlyMount {
59        source: "tmpfs",
60        target: "/tmp",
61        fstype: "tmpfs",
62        flags: 0,
63        data: "mode=1777",
64    },
65];
66
67/// Mount each entry from `CANONICAL_MOUNTS`. Failures are returned as errors
68/// but don't abort — the caller logs + continues so a missing kernel module
69/// (say `devtmpfs`) doesn't wedge the whole boot.
70pub fn mount_early_filesystems() -> Vec<Result<EarlyMount, EarlyMountError>> {
71    CANONICAL_MOUNTS
72        .iter()
73        .map(|m| mount_one(m).map(|()| m.clone()))
74        .collect()
75}
76
77/// Mount an extra filesystem declared via `(definit :mounts (…))`. Unlike
78/// CANONICAL_MOUNTS the fields here are owned strings (the lisp form is
79/// dynamic), so we take `&str` values and build C strings on the spot.
80///
81/// Typical use: virtiofs shares from the host.
82///
83/// ```no_run
84/// # use tatara_init::mounts::mount_extra;
85/// mount_extra("nixstore", "/nix/store", "virtiofs", Some("ro"));
86/// ```
87pub fn mount_extra(
88    source: &str,
89    target: &str,
90    fstype: &str,
91    options: Option<&str>,
92) -> Result<(), EarlyMountError> {
93    mount_extra_impl(source, target, fstype, options)
94}
95
96#[cfg(target_os = "linux")]
97fn mount_extra_impl(
98    source: &str,
99    target: &str,
100    fstype: &str,
101    options: Option<&str>,
102) -> Result<(), EarlyMountError> {
103    use std::ffi::CString;
104    let src = CString::new(source).map_err(|e| err(target, e))?;
105    let tgt = CString::new(target).map_err(|e| err(target, e))?;
106    let fst = CString::new(fstype).map_err(|e| err(target, e))?;
107    let opts_raw = options.unwrap_or("");
108    let opts = CString::new(opts_raw).map_err(|e| err(target, e))?;
109    // mount(2) flags come encoded in the options string — we pass 0 to
110    // `flags` and let virtiofs/ext4/etc. consume `data` as the options
111    // string. Keeps the lisp surface simple (one string).
112    let _ = std::fs::create_dir_all(target);
113    let r = unsafe {
114        libc::mount(
115            src.as_ptr(),
116            tgt.as_ptr(),
117            fst.as_ptr(),
118            0,
119            opts.as_ptr() as *const libc::c_void,
120        )
121    };
122    if r == 0 {
123        return Ok(());
124    }
125    let e = std::io::Error::last_os_error();
126    if e.raw_os_error() == Some(libc::EBUSY) {
127        return Ok(());
128    }
129    Err(EarlyMountError::Mount {
130        target: target.into(),
131        reason: e.to_string(),
132    })
133}
134
135#[cfg(not(target_os = "linux"))]
136fn mount_extra_impl(
137    _source: &str,
138    _target: &str,
139    _fstype: &str,
140    _options: Option<&str>,
141) -> Result<(), EarlyMountError> {
142    Ok(())
143}
144
145#[cfg(target_os = "linux")]
146fn mount_one(m: &EarlyMount) -> Result<(), EarlyMountError> {
147    use std::ffi::CString;
148    // Idempotent: if the target already has something mounted on it,
149    // mount(2) returns EBUSY — we treat that as success (boot-loop safe).
150    // SAFETY: single-threaded at PID-1 bringup; pointers are valid C strings.
151    let source = CString::new(m.source).map_err(|e| err(m.target, e))?;
152    let target = CString::new(m.target).map_err(|e| err(m.target, e))?;
153    let fstype = CString::new(m.fstype).map_err(|e| err(m.target, e))?;
154    let data = CString::new(m.data).map_err(|e| err(m.target, e))?;
155    // Ensure the mount point exists.
156    let _ = std::fs::create_dir_all(m.target);
157    let r = unsafe {
158        libc::mount(
159            source.as_ptr(),
160            target.as_ptr(),
161            fstype.as_ptr(),
162            m.flags,
163            data.as_ptr() as *const libc::c_void,
164        )
165    };
166    if r == 0 {
167        return Ok(());
168    }
169    let e = std::io::Error::last_os_error();
170    if e.raw_os_error() == Some(libc::EBUSY) {
171        return Ok(());
172    }
173    Err(EarlyMountError::Mount {
174        target: m.target.into(),
175        reason: e.to_string(),
176    })
177}
178
179#[cfg(not(target_os = "linux"))]
180fn mount_one(_m: &EarlyMount) -> Result<(), EarlyMountError> {
181    // No-op: tatara-init compiles on macOS for dev but there's no
182    // procfs/sysfs/devtmpfs to mount here. The real mount happens at
183    // PID 1 inside the Linux guest.
184    Ok(())
185}
186
187#[cfg(target_os = "linux")]
188fn err<E: std::fmt::Display>(target: &str, e: E) -> EarlyMountError {
189    EarlyMountError::Mount {
190        target: target.into(),
191        reason: e.to_string(),
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn canonical_set_is_in_boot_order() {
201        let targets: Vec<_> = CANONICAL_MOUNTS.iter().map(|m| m.target).collect();
202        assert_eq!(targets, ["/proc", "/sys", "/dev", "/run", "/tmp"]);
203    }
204
205    #[test]
206    fn every_canonical_mount_has_nonempty_fstype() {
207        for m in CANONICAL_MOUNTS {
208            assert!(!m.fstype.is_empty(), "{} has empty fstype", m.target);
209            assert!(m.target.starts_with('/'), "{} not absolute", m.target);
210        }
211    }
212
213    #[test]
214    fn mount_one_is_a_no_op_on_non_linux() {
215        // Darwin dev path: every call returns Ok.
216        for m in CANONICAL_MOUNTS {
217            #[cfg(not(target_os = "linux"))]
218            assert!(mount_one(m).is_ok());
219            #[cfg(target_os = "linux")]
220            {
221                // On Linux we can't exercise real mounts in unit tests — they
222                // need CAP_SYS_ADMIN. Just verify the function is callable.
223                let _ = m;
224            }
225        }
226    }
227}