Skip to main content

microsandbox_agentd/
init.rs

1//! PID 1 init: mount filesystems, apply tmpfs mounts, prepare runtime directories.
2//!
3//! This module only performs real work on Linux. On other platforms, `init()` is a no-op
4//! to allow the crate to compile for development purposes.
5
6use crate::error::{AgentdError, AgentdResult};
7
8//--------------------------------------------------------------------------------------------------
9// Types
10//--------------------------------------------------------------------------------------------------
11
12/// Parsed tmpfs mount specification.
13#[derive(Debug)]
14#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
15struct TmpfsSpec<'a> {
16    path: &'a str,
17    size_mib: Option<u32>,
18    mode: Option<u32>,
19    noexec: bool,
20}
21
22/// Parsed block-device root specification.
23#[derive(Debug)]
24#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
25struct BlockRootSpec<'a> {
26    device: &'a str,
27    fstype: Option<&'a str>,
28}
29
30//--------------------------------------------------------------------------------------------------
31// Functions
32//--------------------------------------------------------------------------------------------------
33
34/// Performs synchronous PID 1 initialization.
35///
36/// Mounts essential filesystems, applies tmpfs mounts from `MSB_TMPFS` env var,
37/// configures networking from `MSB_NET*` env vars, and prepares runtime directories.
38#[cfg(target_os = "linux")]
39pub fn init() -> AgentdResult<()> {
40    linux::mount_filesystems()?;
41    linux::mount_runtime()?;
42    linux::mount_block_root()?;
43    linux::apply_tmpfs_mounts()?;
44    crate::network::apply_network_config()?;
45    crate::tls::install_ca_cert()?;
46    linux::create_run_dir()?;
47    Ok(())
48}
49
50/// No-op on non-Linux platforms.
51#[cfg(not(target_os = "linux"))]
52pub fn init() -> AgentdResult<()> {
53    Ok(())
54}
55
56/// Parses a single tmpfs entry: `path[,size=N][,mode=N][,noexec]`
57///
58/// Mode is parsed as octal (e.g. `mode=1777`).
59#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
60fn parse_tmpfs_entry(entry: &str) -> AgentdResult<TmpfsSpec<'_>> {
61    let mut parts = entry.split(',');
62    let path = parts.next().unwrap(); // always at least one element
63    if path.is_empty() {
64        return Err(AgentdError::Init("tmpfs entry has empty path".into()));
65    }
66
67    let mut size_mib = None;
68    let mut mode = None;
69    let mut noexec = false;
70
71    for opt in parts {
72        if opt == "noexec" {
73            noexec = true;
74        } else if let Some(val) = opt.strip_prefix("size=") {
75            size_mib = Some(
76                val.parse::<u32>()
77                    .map_err(|_| AgentdError::Init(format!("invalid tmpfs size: {val}")))?,
78            );
79        } else if let Some(val) = opt.strip_prefix("mode=") {
80            mode = Some(
81                u32::from_str_radix(val, 8)
82                    .map_err(|_| AgentdError::Init(format!("invalid octal tmpfs mode: {val}")))?,
83            );
84        } else {
85            return Err(AgentdError::Init(format!("unknown tmpfs option: {opt}")));
86        }
87    }
88
89    Ok(TmpfsSpec {
90        path,
91        size_mib,
92        mode,
93        noexec,
94    })
95}
96
97/// Parses a block-device root specification: `device[,fstype=TYPE]`
98#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
99fn parse_block_root(val: &str) -> AgentdResult<BlockRootSpec<'_>> {
100    let mut parts = val.split(',');
101    let device = parts.next().unwrap();
102    if device.is_empty() {
103        return Err(AgentdError::Init(
104            "MSB_BLOCK_ROOT has empty device path".into(),
105        ));
106    }
107
108    let mut fstype = None;
109    for opt in parts {
110        if let Some(val) = opt.strip_prefix("fstype=") {
111            if val.is_empty() {
112                return Err(AgentdError::Init(
113                    "MSB_BLOCK_ROOT has empty fstype value".into(),
114                ));
115            }
116            fstype = Some(val);
117        } else {
118            return Err(AgentdError::Init(format!(
119                "unknown MSB_BLOCK_ROOT option: {opt}"
120            )));
121        }
122    }
123
124    Ok(BlockRootSpec { device, fstype })
125}
126
127//--------------------------------------------------------------------------------------------------
128// Modules
129//--------------------------------------------------------------------------------------------------
130
131#[cfg(target_os = "linux")]
132mod linux {
133    use std::{os::unix::fs::symlink, path::Path};
134
135    use nix::{
136        mount::{MsFlags, mount},
137        sys::stat::Mode,
138        unistd::{chdir, chroot, mkdir},
139    };
140
141    use crate::error::{AgentdError, AgentdResult};
142
143    use super::TmpfsSpec;
144
145    /// Mounts essential Linux filesystems.
146    pub fn mount_filesystems() -> AgentdResult<()> {
147        // /dev — devtmpfs
148        mkdir_ignore_exists("/dev")?;
149        mount_ignore_busy(
150            Some("devtmpfs"),
151            "/dev",
152            Some("devtmpfs"),
153            MsFlags::MS_RELATIME,
154            None::<&str>,
155        )?;
156
157        // /proc — proc
158        let nodev_noexec_nosuid =
159            MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
160
161        mkdir_ignore_exists("/proc")?;
162        mount_ignore_busy(
163            Some("proc"),
164            "/proc",
165            Some("proc"),
166            nodev_noexec_nosuid,
167            None::<&str>,
168        )?;
169
170        // /sys — sysfs
171        mkdir_ignore_exists("/sys")?;
172        mount_ignore_busy(
173            Some("sysfs"),
174            "/sys",
175            Some("sysfs"),
176            nodev_noexec_nosuid,
177            None::<&str>,
178        )?;
179
180        // /sys/fs/cgroup — cgroup2
181        mkdir_ignore_exists("/sys/fs/cgroup")?;
182        mount_ignore_busy(
183            Some("cgroup2"),
184            "/sys/fs/cgroup",
185            Some("cgroup2"),
186            nodev_noexec_nosuid,
187            None::<&str>,
188        )?;
189
190        // /dev/pts — devpts
191        let noexec_nosuid = MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
192
193        mkdir_ignore_exists("/dev/pts")?;
194        mount_ignore_busy(
195            Some("devpts"),
196            "/dev/pts",
197            Some("devpts"),
198            noexec_nosuid,
199            None::<&str>,
200        )?;
201
202        // /dev/shm — tmpfs
203        mkdir_ignore_exists("/dev/shm")?;
204        mount_ignore_busy(
205            Some("tmpfs"),
206            "/dev/shm",
207            Some("tmpfs"),
208            noexec_nosuid,
209            None::<&str>,
210        )?;
211
212        // /dev/fd → /proc/self/fd
213        if !Path::new("/dev/fd").exists() {
214            symlink("/proc/self/fd", "/dev/fd")
215                .map_err(|e| AgentdError::Init(format!("failed to symlink /dev/fd: {e}")))?;
216        }
217
218        Ok(())
219    }
220
221    /// Mounts the virtiofs runtime filesystem at the canonical mount point.
222    pub fn mount_runtime() -> AgentdResult<()> {
223        mkdir_ignore_exists(microsandbox_protocol::RUNTIME_MOUNT_POINT)?;
224        mount_ignore_busy(
225            Some(microsandbox_protocol::RUNTIME_FS_TAG),
226            microsandbox_protocol::RUNTIME_MOUNT_POINT,
227            Some("virtiofs"),
228            MsFlags::empty(),
229            None::<&str>,
230        )?;
231        Ok(())
232    }
233
234    /// Mounts a block device as the new root filesystem, if `MSB_BLOCK_ROOT` is set.
235    ///
236    /// Steps: mount block device at `/newroot`, bind-mount `/.msb` into it,
237    /// pivot via `MS_MOVE` + `chroot`, then re-mount essential filesystems.
238    pub fn mount_block_root() -> AgentdResult<()> {
239        let val = match std::env::var(microsandbox_protocol::ENV_BLOCK_ROOT) {
240            Ok(v) if !v.is_empty() => v,
241            _ => return Ok(()),
242        };
243
244        let spec = super::parse_block_root(&val)?;
245
246        // Create the temporary mount point.
247        mkdir_ignore_exists("/newroot")?;
248
249        // Mount the block device.
250        if let Some(fstype) = spec.fstype {
251            mount(
252                Some(spec.device),
253                "/newroot",
254                Some(fstype),
255                MsFlags::empty(),
256                None::<&str>,
257            )
258            .map_err(|e| {
259                AgentdError::Init(format!(
260                    "failed to mount {} at /newroot as {fstype}: {e}",
261                    spec.device
262                ))
263            })?;
264        } else {
265            try_mount(spec.device, "/newroot")?;
266        }
267
268        // Bind-mount the runtime filesystem into the new root.
269        let msb_target = "/newroot/.msb";
270        mkdir_ignore_exists(msb_target)?;
271        mount(
272            Some(microsandbox_protocol::RUNTIME_MOUNT_POINT),
273            msb_target,
274            None::<&str>,
275            MsFlags::MS_BIND,
276            None::<&str>,
277        )
278        .map_err(|e| AgentdError::Init(format!("failed to bind-mount /.msb into /newroot: {e}")))?;
279
280        // Pivot: move the new root on top of /.
281        chdir("/newroot")
282            .map_err(|e| AgentdError::Init(format!("failed to chdir /newroot: {e}")))?;
283
284        mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>)
285            .map_err(|e| AgentdError::Init(format!("failed to MS_MOVE /newroot to /: {e}")))?;
286
287        chroot(".").map_err(|e| AgentdError::Init(format!("failed to chroot: {e}")))?;
288
289        chdir("/")
290            .map_err(|e| AgentdError::Init(format!("failed to chdir / after chroot: {e}")))?;
291
292        // Re-mount essential filesystems in the new root.
293        mount_filesystems()?;
294
295        Ok(())
296    }
297
298    /// Tries every filesystem type listed in `/proc/filesystems` until one succeeds.
299    fn try_mount(device: &str, target: &str) -> AgentdResult<()> {
300        let content = std::fs::read_to_string("/proc/filesystems")
301            .map_err(|e| AgentdError::Init(format!("failed to read /proc/filesystems: {e}")))?;
302
303        for line in content.lines() {
304            // Skip virtual filesystems marked with "nodev".
305            if line.starts_with("nodev") {
306                continue;
307            }
308
309            let fstype = line.trim();
310            if fstype.is_empty() {
311                continue;
312            }
313
314            if mount(
315                Some(device),
316                target,
317                Some(fstype),
318                MsFlags::empty(),
319                None::<&str>,
320            )
321            .is_ok()
322            {
323                return Ok(());
324            }
325        }
326
327        Err(AgentdError::Init(format!(
328            "failed to mount {device} at {target}: no supported filesystem found"
329        )))
330    }
331
332    /// Reads `MSB_TMPFS` env var and mounts each tmpfs entry.
333    ///
334    /// Missing env var is not an error (no tmpfs mounts requested).
335    /// Parse failures and mount failures are hard errors.
336    pub fn apply_tmpfs_mounts() -> AgentdResult<()> {
337        let val = match std::env::var(microsandbox_protocol::ENV_TMPFS) {
338            Ok(v) if !v.is_empty() => v,
339            _ => return Ok(()),
340        };
341
342        for entry in val.split(';') {
343            if entry.is_empty() {
344                continue;
345            }
346
347            let spec = super::parse_tmpfs_entry(entry)?;
348            mount_tmpfs(&spec)?;
349        }
350
351        Ok(())
352    }
353
354    /// Mounts a single tmpfs from a parsed spec.
355    fn mount_tmpfs(spec: &TmpfsSpec<'_>) -> AgentdResult<()> {
356        let path = spec.path;
357
358        // Determine the permission mode.
359        let mode = spec
360            .mode
361            .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
362                0o1777
363            } else {
364                0o755
365            });
366
367        // Create the target directory.
368        std::fs::create_dir_all(path)
369            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
370
371        // Flags: nosuid + nodev (sensible safety defaults).
372        let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME;
373        if spec.noexec {
374            flags |= MsFlags::MS_NOEXEC;
375        }
376
377        // Mount data: size and mode options.
378        let mut data = String::new();
379        if let Some(mib) = spec.size_mib {
380            data.push_str(&format!("size={}", u64::from(mib) * 1024 * 1024));
381        }
382        if !data.is_empty() {
383            data.push(',');
384        }
385        data.push_str(&format!("mode={mode:o}"));
386
387        mount(
388            Some("tmpfs"),
389            path,
390            Some("tmpfs"),
391            flags,
392            Some(data.as_str()),
393        )
394        .map_err(|e| AgentdError::Init(format!("failed to mount tmpfs at {path}: {e}")))?;
395
396        Ok(())
397    }
398
399    /// Creates the `/run` directory.
400    pub fn create_run_dir() -> AgentdResult<()> {
401        mkdir_ignore_exists("/run")?;
402        Ok(())
403    }
404
405    /// Creates a directory, ignoring EEXIST errors.
406    fn mkdir_ignore_exists(path: &str) -> AgentdResult<()> {
407        match mkdir(path, Mode::from_bits_truncate(0o755)) {
408            Ok(()) => Ok(()),
409            Err(nix::Error::EEXIST) => Ok(()),
410            Err(e) => Err(e.into()),
411        }
412    }
413
414    /// Mounts a filesystem, ignoring EBUSY errors (already mounted).
415    fn mount_ignore_busy(
416        source: Option<&str>,
417        target: &str,
418        fstype: Option<&str>,
419        flags: MsFlags,
420        data: Option<&str>,
421    ) -> AgentdResult<()> {
422        match mount(source, target, fstype, flags, data) {
423            Ok(()) => Ok(()),
424            Err(nix::Error::EBUSY) => Ok(()),
425            Err(e) => Err(AgentdError::Init(format!("failed to mount {target}: {e}"))),
426        }
427    }
428}
429
430//--------------------------------------------------------------------------------------------------
431// Tests
432//--------------------------------------------------------------------------------------------------
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_parse_path_only() {
440        let spec = parse_tmpfs_entry("/tmp").unwrap();
441        assert_eq!(spec.path, "/tmp");
442        assert_eq!(spec.size_mib, None);
443        assert_eq!(spec.mode, None);
444        assert!(!spec.noexec);
445    }
446
447    #[test]
448    fn test_parse_with_size() {
449        let spec = parse_tmpfs_entry("/tmp,size=256").unwrap();
450        assert_eq!(spec.path, "/tmp");
451        assert_eq!(spec.size_mib, Some(256));
452    }
453
454    #[test]
455    fn test_parse_with_noexec() {
456        let spec = parse_tmpfs_entry("/tmp,noexec").unwrap();
457        assert_eq!(spec.path, "/tmp");
458        assert!(spec.noexec);
459    }
460
461    #[test]
462    fn test_parse_with_octal_mode() {
463        let spec = parse_tmpfs_entry("/tmp,mode=1777").unwrap();
464        assert_eq!(spec.mode, Some(0o1777));
465
466        let spec = parse_tmpfs_entry("/data,mode=755").unwrap();
467        assert_eq!(spec.mode, Some(0o755));
468    }
469
470    #[test]
471    fn test_parse_multi_options() {
472        let spec = parse_tmpfs_entry("/tmp,size=256,mode=1777,noexec").unwrap();
473        assert_eq!(spec.path, "/tmp");
474        assert_eq!(spec.size_mib, Some(256));
475        assert_eq!(spec.mode, Some(0o1777));
476        assert!(spec.noexec);
477    }
478
479    #[test]
480    fn test_parse_unknown_option_errors() {
481        let err = parse_tmpfs_entry("/tmp,bogus=42").unwrap_err();
482        assert!(err.to_string().contains("unknown tmpfs option"));
483    }
484
485    #[test]
486    fn test_parse_invalid_size_errors() {
487        let err = parse_tmpfs_entry("/tmp,size=abc").unwrap_err();
488        assert!(err.to_string().contains("invalid tmpfs size"));
489    }
490
491    #[test]
492    fn test_parse_invalid_mode_errors() {
493        let err = parse_tmpfs_entry("/tmp,mode=zzz").unwrap_err();
494        assert!(err.to_string().contains("invalid octal tmpfs mode"));
495    }
496
497    #[test]
498    fn test_parse_empty_path_errors() {
499        let err = parse_tmpfs_entry(",size=256").unwrap_err();
500        assert!(err.to_string().contains("empty path"));
501    }
502
503    #[test]
504    fn test_parse_block_root_device_only() {
505        let spec = parse_block_root("/dev/vda").unwrap();
506        assert_eq!(spec.device, "/dev/vda");
507        assert_eq!(spec.fstype, None);
508    }
509
510    #[test]
511    fn test_parse_block_root_with_fstype() {
512        let spec = parse_block_root("/dev/vda,fstype=ext4").unwrap();
513        assert_eq!(spec.device, "/dev/vda");
514        assert_eq!(spec.fstype, Some("ext4"));
515    }
516
517    #[test]
518    fn test_parse_block_root_empty_device_errors() {
519        let err = parse_block_root(",fstype=ext4").unwrap_err();
520        assert!(err.to_string().contains("empty device path"));
521    }
522
523    #[test]
524    fn test_parse_block_root_unknown_option_errors() {
525        let err = parse_block_root("/dev/vda,bogus=42").unwrap_err();
526        assert!(err.to_string().contains("unknown MSB_BLOCK_ROOT option"));
527    }
528
529    #[test]
530    fn test_parse_block_root_empty_fstype_errors() {
531        let err = parse_block_root("/dev/vda,fstype=").unwrap_err();
532        assert!(err.to_string().contains("empty fstype"));
533    }
534}