Skip to main content

microsandbox_agentd/
init.rs

1//! PID 1 init: mount filesystems, apply tmpfs mounts, prepare runtime directories.
2
3use crate::config::{BootParams, SecurityProfile};
4use crate::error::AgentdResult;
5use crate::{network, rlimit, tls};
6
7//--------------------------------------------------------------------------------------------------
8// Functions
9//--------------------------------------------------------------------------------------------------
10
11/// Performs synchronous PID 1 initialization.
12///
13/// Applies sandbox-wide resource limits first so every later guest process
14/// inherits the raised baseline, then mounts filesystems, applies directory
15/// mounts, file mounts, and tmpfs mounts from the parsed params. Configures
16/// networking and prepares runtime directories.
17///
18/// Consumes the [`BootParams`] by value — the data is one-shot and not
19/// needed after init returns.
20pub fn init(
21    mut params: BootParams,
22    before_user_mounts: impl FnOnce() -> AgentdResult<()>,
23) -> AgentdResult<()> {
24    rlimit::apply_baseline(&params.rlimits)?;
25    linux::mount_filesystems()?;
26    linux::mount_runtime()?;
27    if let Some(spec) = &params.block_root {
28        linux::mount_block_root(spec)?;
29    }
30    before_user_mounts()?;
31    if params.security_profile == SecurityProfile::Restricted {
32        force_restricted_mount_flags(&mut params);
33    }
34    linux::apply_dir_mounts(&params.dir_mounts)?;
35    linux::apply_file_mounts(&params.file_mounts)?;
36    linux::apply_disk_mounts(&params.disk_mounts)?;
37    network::apply_hostname(
38        params.hostname.as_deref(),
39        params.host_alias.as_deref(),
40        params.net_ipv4.as_ref().map(|v4| v4.gateway),
41        params.net_ipv6.as_ref().map(|v6| v6.gateway),
42    )?;
43    linux::apply_tmpfs_mounts(&params.tmpfs)?;
44    linux::ensure_standard_tmp_permissions()?;
45    network::apply_network_config(params.network())?;
46    tls::install_ca_cert()?;
47    tls::install_host_cas()?;
48    linux::ensure_scripts_path_in_profile()?;
49    linux::create_run_dir()?;
50    Ok(())
51}
52
53fn force_restricted_mount_flags(params: &mut BootParams) {
54    for spec in &mut params.dir_mounts {
55        spec.nosuid = true;
56        spec.nodev = true;
57    }
58    for spec in &mut params.file_mounts {
59        spec.nosuid = true;
60        spec.nodev = true;
61    }
62    for spec in &mut params.disk_mounts {
63        spec.nosuid = true;
64        spec.nodev = true;
65    }
66    for spec in &mut params.tmpfs {
67        spec.nosuid = true;
68        spec.nodev = true;
69    }
70}
71
72fn ensure_scripts_profile_block(profile: &str) -> String {
73    const START_MARKER: &str = "# >>> microsandbox scripts path >>>";
74    const END_MARKER: &str = "# <<< microsandbox scripts path <<<";
75    const BLOCK: &str = "# >>> microsandbox scripts path >>>\ncase \":$PATH:\" in\n  *:/.msb/scripts:*) ;;\n  *) export PATH=\"/.msb/scripts:$PATH\" ;;\nesac\n# <<< microsandbox scripts path <<<\n";
76
77    if profile.contains(START_MARKER) && profile.contains(END_MARKER) {
78        return profile.to_string();
79    }
80
81    let mut updated = profile.to_string();
82    if !updated.is_empty() && !updated.ends_with('\n') {
83        updated.push('\n');
84    }
85    updated.push_str(BLOCK);
86    updated
87}
88
89//--------------------------------------------------------------------------------------------------
90// Modules
91//--------------------------------------------------------------------------------------------------
92
93mod linux {
94    use std::fs;
95    use std::os::unix::fs::{self as unix_fs, PermissionsExt};
96    use std::path::Path;
97
98    use nix::mount::{self, MntFlags, MsFlags};
99    use nix::sys::stat::Mode;
100    use nix::unistd;
101
102    use crate::config::{BlockRootSpec, DirMountSpec, DiskMountSpec, FileMountSpec, TmpfsSpec};
103    use crate::error::{AgentdError, AgentdResult};
104
105    /// Mounts essential Linux filesystems.
106    pub fn mount_filesystems() -> AgentdResult<()> {
107        // /dev — devtmpfs
108        mkdir_ignore_exists("/dev")?;
109        mount_ignore_busy(
110            Some("devtmpfs"),
111            "/dev",
112            Some("devtmpfs"),
113            MsFlags::MS_RELATIME,
114            None::<&str>,
115        )?;
116
117        // /proc — proc
118        let nodev_noexec_nosuid =
119            MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
120
121        mkdir_ignore_exists("/proc")?;
122        mount_ignore_busy(
123            Some("proc"),
124            "/proc",
125            Some("proc"),
126            nodev_noexec_nosuid,
127            None::<&str>,
128        )?;
129
130        // /sys — sysfs
131        mkdir_ignore_exists("/sys")?;
132        mount_ignore_busy(
133            Some("sysfs"),
134            "/sys",
135            Some("sysfs"),
136            nodev_noexec_nosuid,
137            None::<&str>,
138        )?;
139
140        // /sys/fs/cgroup — cgroup2
141        mkdir_ignore_exists("/sys/fs/cgroup")?;
142        mount_ignore_busy(
143            Some("cgroup2"),
144            "/sys/fs/cgroup",
145            Some("cgroup2"),
146            nodev_noexec_nosuid,
147            None::<&str>,
148        )?;
149
150        // /dev/pts — devpts
151        let noexec_nosuid = MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
152
153        mkdir_ignore_exists("/dev/pts")?;
154        mount_ignore_busy(
155            Some("devpts"),
156            "/dev/pts",
157            Some("devpts"),
158            noexec_nosuid,
159            None::<&str>,
160        )?;
161
162        // /dev/shm — tmpfs
163        mkdir_ignore_exists("/dev/shm")?;
164        mount_ignore_busy(
165            Some("tmpfs"),
166            "/dev/shm",
167            Some("tmpfs"),
168            noexec_nosuid,
169            None::<&str>,
170        )?;
171
172        // /dev/fd → /proc/self/fd
173        if !Path::new("/dev/fd").exists() {
174            unix_fs::symlink("/proc/self/fd", "/dev/fd")
175                .map_err(|e| AgentdError::Init(format!("failed to symlink /dev/fd: {e}")))?;
176        }
177
178        Ok(())
179    }
180
181    /// Mounts the virtiofs runtime filesystem at the canonical mount point.
182    pub fn mount_runtime() -> AgentdResult<()> {
183        mkdir_ignore_exists(microsandbox_protocol::RUNTIME_MOUNT_POINT)?;
184        mount_ignore_busy(
185            Some(microsandbox_protocol::RUNTIME_FS_TAG),
186            microsandbox_protocol::RUNTIME_MOUNT_POINT,
187            Some("virtiofs"),
188            MsFlags::empty(),
189            None::<&str>,
190        )?;
191        Ok(())
192    }
193
194    /// Assembles the root filesystem from the parsed block-root spec.
195    ///
196    /// Dispatches on the spec variant, then pivots `/newroot` into `/`.
197    pub fn mount_block_root(spec: &BlockRootSpec) -> AgentdResult<()> {
198        mkdir_ignore_exists("/newroot")?;
199
200        match spec {
201            BlockRootSpec::DiskImage { device, fstype } => {
202                mount_disk_image(device, fstype.as_deref())?;
203            }
204            BlockRootSpec::OciErofs {
205                lower,
206                upper,
207                upper_fstype,
208            } => {
209                mount_oci_erofs(lower, upper, upper_fstype)?;
210            }
211        }
212
213        pivot_to_newroot()?;
214
215        Ok(())
216    }
217
218    /// Mount a single disk image at /newroot.
219    fn mount_disk_image(device: &str, fstype: Option<&str>) -> AgentdResult<()> {
220        if let Some(fstype) = fstype {
221            mount::mount(
222                Some(device),
223                "/newroot",
224                Some(fstype),
225                MsFlags::empty(),
226                None::<&str>,
227            )
228            .map_err(|e| {
229                AgentdError::Init(format!(
230                    "failed to mount {device} at /newroot as {fstype}: {e}"
231                ))
232            })?;
233        } else {
234            let fstypes = read_proc_filesystems()?;
235            try_mount_any(device, "/newroot", MsFlags::empty(), &fstypes)?;
236        }
237        Ok(())
238    }
239
240    /// Mount merged EROFS lower + writable upper + overlayfs at /newroot.
241    fn mount_oci_erofs(
242        lower_device: &str,
243        upper_device: &str,
244        upper_fstype: &str,
245    ) -> AgentdResult<()> {
246        // Mount the EROFS lower device read-only.
247        let lower_dir = "/.msb/rootfs/lower";
248        mkdir_ignore_exists("/.msb/rootfs")?;
249        mkdir_ignore_exists("/.msb/rootfs/lower")?;
250        mount::mount(
251            Some(lower_device),
252            lower_dir,
253            Some("erofs"),
254            MsFlags::MS_RDONLY,
255            None::<&str>,
256        )
257        .map_err(|e| AgentdError::Init(format!("mount {lower_device} at {lower_dir}: {e}")))?;
258
259        // Mount the writable upper device.
260        let upperfs_dir = "/.msb/rootfs/upperfs";
261        mkdir_ignore_exists("/.msb/rootfs/upperfs")?;
262        mount::mount(
263            Some(upper_device),
264            upperfs_dir,
265            Some(upper_fstype),
266            MsFlags::empty(),
267            None::<&str>,
268        )
269        .map_err(|e| AgentdError::Init(format!("mount {upper_device} at {upperfs_dir}: {e}")))?;
270
271        // Create upper and work subdirs on the writable device.
272        let upper_dir = format!("{upperfs_dir}/upper");
273        let work_dir = format!("{upperfs_dir}/work");
274        fs::create_dir_all(&upper_dir)
275            .map_err(|e| AgentdError::Init(format!("mkdir {upper_dir}: {e}")))?;
276        fs::create_dir_all(&work_dir)
277            .map_err(|e| AgentdError::Init(format!("mkdir {work_dir}: {e}")))?;
278
279        // Assemble overlayfs mount.
280        let mount_data = format!("lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}");
281
282        mount::mount(
283            Some("overlay"),
284            "/newroot",
285            Some("overlay"),
286            MsFlags::empty(),
287            Some(mount_data.as_str()),
288        )
289        .map_err(|e| AgentdError::Init(format!("mount overlay at /newroot: {e}")))?;
290
291        Ok(())
292    }
293
294    /// Bind-mount /.msb into /newroot, then MS_MOVE + chroot + re-mount essentials.
295    fn pivot_to_newroot() -> AgentdResult<()> {
296        let msb_target = "/newroot/.msb";
297        mkdir_ignore_exists(msb_target)?;
298        mount::mount(
299            Some(microsandbox_protocol::RUNTIME_MOUNT_POINT),
300            msb_target,
301            None::<&str>,
302            MsFlags::MS_BIND,
303            None::<&str>,
304        )
305        .map_err(|e| AgentdError::Init(format!("failed to bind-mount /.msb into /newroot: {e}")))?;
306
307        unistd::chdir("/newroot")
308            .map_err(|e| AgentdError::Init(format!("failed to chdir /newroot: {e}")))?;
309
310        mount::mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>)
311            .map_err(|e| AgentdError::Init(format!("failed to MS_MOVE /newroot to /: {e}")))?;
312
313        unistd::chroot(".").map_err(|e| AgentdError::Init(format!("failed to chroot: {e}")))?;
314
315        unistd::chdir("/")
316            .map_err(|e| AgentdError::Init(format!("failed to chdir / after chroot: {e}")))?;
317
318        mount_filesystems()?;
319
320        Ok(())
321    }
322
323    /// Read native filesystem types from `/proc/filesystems`, skipping
324    /// `nodev` entries (virtual filesystems that can't back a real device).
325    fn read_proc_filesystems() -> AgentdResult<Vec<String>> {
326        let content = fs::read_to_string("/proc/filesystems")
327            .map_err(|e| AgentdError::Init(format!("failed to read /proc/filesystems: {e}")))?;
328        Ok(content
329            .lines()
330            .filter_map(|line| {
331                if line.starts_with("nodev") {
332                    return None;
333                }
334                let fstype = line.trim();
335                if fstype.is_empty() {
336                    None
337                } else {
338                    Some(fstype.to_string())
339                }
340            })
341            .collect())
342    }
343
344    /// Try mounting `device` at `target` with `flags`, walking the supplied
345    /// candidate filesystem list until one succeeds. Use
346    /// `read_proc_filesystems` to build the candidate list (typically once
347    /// per init phase) and reuse it across multiple mount attempts.
348    fn try_mount_any(
349        device: &str,
350        target: &str,
351        flags: MsFlags,
352        fstypes: &[String],
353    ) -> AgentdResult<()> {
354        for fstype in fstypes {
355            if mount::mount(
356                Some(device),
357                target,
358                Some(fstype.as_str()),
359                flags,
360                None::<&str>,
361            )
362            .is_ok()
363            {
364                return Ok(());
365            }
366        }
367        Err(AgentdError::Init(format!(
368            "failed to mount {device} at {target}: no supported filesystem found"
369        )))
370    }
371
372    /// Filesystem-specific mount data for disk-image volume mounts.
373    fn disk_mount_data(fstype: &str, readonly: bool) -> Option<&'static str> {
374        if readonly && fstype == "ext4" {
375            // A read-only block device cannot replay an ext4 journal. `noload`
376            // lets seeded or intentionally read-only ext4 images mount without
377            // attempting journal recovery.
378            Some("noload")
379        } else {
380            None
381        }
382    }
383
384    /// Try mounting a disk-image volume, adding filesystem-specific options
385    /// where read-only block devices need them.
386    fn try_mount_disk_any(
387        device: &str,
388        target: &str,
389        flags: MsFlags,
390        readonly: bool,
391        fstypes: &[String],
392    ) -> AgentdResult<()> {
393        for fstype in fstypes {
394            let data = disk_mount_data(fstype, readonly);
395            if mount::mount(Some(device), target, Some(fstype.as_str()), flags, data).is_ok() {
396                return Ok(());
397            }
398        }
399        Err(AgentdError::Init(format!(
400            "disk mount: failed to mount {device} at {target}: no supported filesystem found"
401        )))
402    }
403
404    /// Mounts each virtiofs directory volume from the parsed specs.
405    pub fn apply_dir_mounts(specs: &[DirMountSpec]) -> AgentdResult<()> {
406        for spec in specs {
407            mount_dir(spec)?;
408        }
409        Ok(())
410    }
411
412    /// Mounts a single virtiofs directory share from a parsed spec.
413    fn mount_dir(spec: &DirMountSpec) -> AgentdResult<()> {
414        let path = spec.guest_path.as_str();
415
416        // Create the mount point directory.
417        fs::create_dir_all(path)
418            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
419
420        let mut flags = MsFlags::MS_RELATIME;
421        if spec.nosuid {
422            flags |= MsFlags::MS_NOSUID;
423        }
424        if spec.nodev {
425            flags |= MsFlags::MS_NODEV;
426        }
427        if spec.noexec {
428            flags |= MsFlags::MS_NOEXEC;
429        }
430        if spec.readonly {
431            flags |= MsFlags::MS_RDONLY;
432        }
433
434        mount::mount(
435            Some(spec.tag.as_str()),
436            path,
437            Some("virtiofs"),
438            flags,
439            None::<&str>,
440        )
441        .map_err(|e| {
442            AgentdError::Init(format!(
443                "failed to mount virtiofs tag '{}' at {path}: {e}",
444                spec.tag
445            ))
446        })?;
447
448        Ok(())
449    }
450
451    /// Bind-mounts each file from virtiofs shares.
452    pub fn apply_file_mounts(specs: &[FileMountSpec]) -> AgentdResult<()> {
453        if specs.is_empty() {
454            return Ok(());
455        }
456
457        // Create the staging root directory.
458        fs::create_dir_all(microsandbox_protocol::FILE_MOUNTS_DIR).map_err(|e| {
459            AgentdError::Init(format!(
460                "failed to create file mounts dir {}: {e}",
461                microsandbox_protocol::FILE_MOUNTS_DIR
462            ))
463        })?;
464
465        for spec in specs {
466            mount_file(spec)?;
467        }
468
469        // Best-effort cleanup of the staging root (succeeds only if all
470        // per-tag subdirs were already removed inside mount_file).
471        let _ = fs::remove_dir(microsandbox_protocol::FILE_MOUNTS_DIR);
472
473        Ok(())
474    }
475
476    /// Mounts a single file from a virtiofs share via bind mount.
477    fn mount_file(spec: &FileMountSpec) -> AgentdResult<()> {
478        let staging_path = format!("{}/{}", microsandbox_protocol::FILE_MOUNTS_DIR, spec.tag);
479
480        // 1. Create the staging mount point directory.
481        fs::create_dir_all(&staging_path).map_err(|e| {
482            AgentdError::Init(format!("failed to create staging dir {staging_path}: {e}"))
483        })?;
484
485        // 2. Mount the virtiofs share at the staging directory.
486        let mut flags = MsFlags::MS_RELATIME;
487        if spec.nosuid {
488            flags |= MsFlags::MS_NOSUID;
489        }
490        if spec.nodev {
491            flags |= MsFlags::MS_NODEV;
492        }
493        if spec.noexec {
494            flags |= MsFlags::MS_NOEXEC;
495        }
496        if spec.readonly {
497            flags |= MsFlags::MS_RDONLY;
498        }
499
500        mount::mount(
501            Some(spec.tag.as_str()),
502            staging_path.as_str(),
503            Some("virtiofs"),
504            flags,
505            None::<&str>,
506        )
507        .map_err(|e| {
508            AgentdError::Init(format!(
509                "failed to mount virtiofs tag '{}' at {staging_path}: {e}",
510                spec.tag
511            ))
512        })?;
513
514        let bind_result = (|| {
515            // 3. Create parent directories for the guest path.
516            let guest = Path::new(&spec.guest_path);
517            if let Some(parent) = guest.parent() {
518                fs::create_dir_all(parent).map_err(|e| {
519                    AgentdError::Init(format!(
520                        "failed to create parent dirs for {}: {e}",
521                        spec.guest_path
522                    ))
523                })?;
524            }
525
526            // 4. Create the target file (touch) as a bind mount target.
527            fs::OpenOptions::new()
528                .create(true)
529                .truncate(false)
530                .write(true)
531                .open(&spec.guest_path)
532                .map_err(|e| {
533                    AgentdError::Init(format!(
534                        "failed to create bind target {}: {e}",
535                        spec.guest_path
536                    ))
537                })?;
538
539            // 5. Bind mount the file from staging to the guest path.
540            let source_path = format!("{staging_path}/{}", spec.filename);
541            mount::mount(
542                Some(source_path.as_str()),
543                spec.guest_path.as_str(),
544                None::<&str>,
545                MsFlags::MS_BIND,
546                None::<&str>,
547            )
548            .map_err(|e| {
549                AgentdError::Init(format!(
550                    "failed to bind mount {source_path} to {}: {e}",
551                    spec.guest_path
552                ))
553            })?;
554
555            // 6. Remount the file bind with the guest-facing VFS flags.
556            let mut remount_flags = MsFlags::MS_BIND | MsFlags::MS_REMOUNT;
557            if spec.nosuid {
558                remount_flags |= MsFlags::MS_NOSUID;
559            }
560            if spec.nodev {
561                remount_flags |= MsFlags::MS_NODEV;
562            }
563            if spec.noexec {
564                remount_flags |= MsFlags::MS_NOEXEC;
565            }
566            if spec.readonly {
567                remount_flags |= MsFlags::MS_RDONLY;
568            }
569            mount::mount(
570                None::<&str>,
571                spec.guest_path.as_str(),
572                None::<&str>,
573                remount_flags,
574                None::<&str>,
575            )
576            .map_err(|e| {
577                AgentdError::Init(format!(
578                    "failed to remount {} with volume flags: {e}",
579                    spec.guest_path
580                ))
581            })?;
582
583            Ok(())
584        })();
585
586        let cleanup_result = cleanup_file_mount_staging(&staging_path);
587        match (bind_result, cleanup_result) {
588            (Ok(()), Ok(())) => Ok(()),
589            (Err(err), Ok(())) => Err(err),
590            (Ok(()), Err(err)) => Err(err),
591            (Err(err), Err(cleanup_err)) => Err(AgentdError::Init(format!(
592                "{err}; additionally failed to cleanup file mount staging {staging_path}: {cleanup_err}"
593            ))),
594        }
595    }
596
597    fn cleanup_file_mount_staging(staging_path: &str) -> AgentdResult<()> {
598        // The bind mount keeps the file accessible at the guest path; removing
599        // the share prevents alternate-path access through the staging tree.
600        mount::umount2(staging_path, MntFlags::MNT_DETACH).map_err(|e| {
601            AgentdError::Init(format!(
602                "failed to unmount file mount staging {staging_path}: {e}"
603            ))
604        })?;
605        fs::remove_dir(staging_path).map_err(|e| {
606            AgentdError::Init(format!(
607                "failed to remove file mount staging {staging_path}: {e}"
608            ))
609        })?;
610        Ok(())
611    }
612
613    /// Mounts each disk-image volume at its guest path.
614    pub fn apply_disk_mounts(specs: &[DiskMountSpec]) -> AgentdResult<()> {
615        if specs.is_empty() {
616            return Ok(());
617        }
618        // Read /proc/filesystems only when at least one mount needs
619        // autodetection, then reuse the candidate list across the batch.
620        let fstypes = if specs.iter().any(|spec| spec.fstype.is_none()) {
621            Some(read_proc_filesystems()?)
622        } else {
623            None
624        };
625        for spec in specs {
626            mount_disk(spec, fstypes.as_deref())?;
627        }
628        Ok(())
629    }
630
631    /// Resolve the block device for a disk-image mount id.
632    ///
633    /// Primary path: `/dev/disk/by-id/virtio-<id>`, which udev/kernel
634    /// create when the VMM sets `virtio_blk_config.serial`.
635    /// Fallback: scan `/sys/block/*/serial` for a match, which works
636    /// even when udev is unavailable or has not yet populated the
637    /// symlink.
638    fn resolve_disk_device(id: &str) -> AgentdResult<String> {
639        use std::{thread::sleep, time::Duration};
640        const RETRIES: u32 = 20;
641        const INTERVAL: Duration = Duration::from_millis(10);
642
643        let by_id = format!("/dev/disk/by-id/virtio-{id}");
644        for attempt in 0..RETRIES {
645            if Path::new(&by_id).exists() {
646                return Ok(by_id);
647            }
648            if let Some(dev) = scan_block_serial(id) {
649                return Ok(dev);
650            }
651            // Skip the sleep after the last check so the failure path
652            // doesn't pay 10ms it can't use.
653            if attempt + 1 < RETRIES {
654                sleep(INTERVAL);
655            }
656        }
657        Err(AgentdError::Init(format!(
658            "disk mount: no block device found for id '{id}' \
659             (checked /dev/disk/by-id/virtio-{id} and /sys/block/*/serial)"
660        )))
661    }
662
663    /// Walk `/sys/block/*` for an entry whose `serial` file matches `id`.
664    fn scan_block_serial(id: &str) -> Option<String> {
665        let entries = fs::read_dir("/sys/block").ok()?;
666        for entry in entries.flatten() {
667            let name = entry.file_name();
668            let Some(name_str) = name.to_str() else {
669                continue;
670            };
671            if !name_str.starts_with("vd") {
672                continue;
673            }
674            let serial_path = entry.path().join("serial");
675            let Ok(serial) = fs::read_to_string(&serial_path) else {
676                continue;
677            };
678            if serial.trim() == id {
679                return Some(format!("/dev/{name_str}"));
680            }
681        }
682        None
683    }
684
685    fn mount_disk(spec: &DiskMountSpec, fstypes: Option<&[String]>) -> AgentdResult<()> {
686        let path = spec.guest_path.as_str();
687        fs::create_dir_all(path)
688            .map_err(|e| AgentdError::Init(format!("disk mount: create dir {path}: {e}")))?;
689
690        let device = resolve_disk_device(&spec.id)?;
691
692        let mut flags = MsFlags::MS_RELATIME;
693        if spec.nosuid {
694            flags |= MsFlags::MS_NOSUID;
695        }
696        if spec.nodev {
697            flags |= MsFlags::MS_NODEV;
698        }
699        if spec.noexec {
700            flags |= MsFlags::MS_NOEXEC;
701        }
702        if spec.readonly {
703            flags |= MsFlags::MS_RDONLY;
704        }
705
706        if let Some(fstype) = spec.fstype.as_deref() {
707            let data = disk_mount_data(fstype, spec.readonly);
708            mount::mount(Some(device.as_str()), path, Some(fstype), flags, data).map_err(|e| {
709                AgentdError::Init(format!(
710                    "disk mount: failed to mount {device} at {path} as {fstype}: {e}"
711                ))
712            })?;
713        } else {
714            let fstypes = fstypes.ok_or_else(|| {
715                AgentdError::Init("disk mount: missing filesystem autodetect list".into())
716            })?;
717            try_mount_disk_any(&device, path, flags, spec.readonly, fstypes)?;
718        }
719
720        Ok(())
721    }
722
723    /// Mounts each tmpfs from the parsed specs.
724    pub fn apply_tmpfs_mounts(specs: &[TmpfsSpec]) -> AgentdResult<()> {
725        for spec in specs {
726            mount_tmpfs(spec)?;
727        }
728        Ok(())
729    }
730
731    /// Ensure standard temporary directories are writable and sticky.
732    pub fn ensure_standard_tmp_permissions() -> AgentdResult<()> {
733        ensure_directory_mode("/tmp", 0o1777)?;
734        ensure_directory_mode("/var/tmp", 0o1777)?;
735        Ok(())
736    }
737
738    /// Mounts a single tmpfs from a parsed spec.
739    fn mount_tmpfs(spec: &TmpfsSpec) -> AgentdResult<()> {
740        let path = spec.path.as_str();
741
742        // Determine the permission mode.
743        let mode = spec
744            .mode
745            .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
746                0o1777
747            } else {
748                0o755
749            });
750
751        // Create the target directory.
752        fs::create_dir_all(path)
753            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
754
755        let mut flags = MsFlags::MS_RELATIME;
756        if spec.nosuid {
757            flags |= MsFlags::MS_NOSUID;
758        }
759        if spec.nodev {
760            flags |= MsFlags::MS_NODEV;
761        }
762        if spec.noexec {
763            flags |= MsFlags::MS_NOEXEC;
764        }
765        if spec.readonly {
766            flags |= MsFlags::MS_RDONLY;
767        }
768
769        // Mount data: size and mode options.
770        let mut data = String::new();
771        if let Some(mib) = spec.size_mib {
772            data.push_str(&format!("size={}", u64::from(mib) * 1024 * 1024));
773        }
774        if !data.is_empty() {
775            data.push(',');
776        }
777        data.push_str(&format!("mode={mode:o}"));
778
779        mount::mount(
780            Some("tmpfs"),
781            path,
782            Some("tmpfs"),
783            flags,
784            Some(data.as_str()),
785        )
786        .map_err(|e| AgentdError::Init(format!("failed to mount tmpfs at {path}: {e}")))?;
787
788        Ok(())
789    }
790
791    /// Creates `/run` and `/run/microsandbox` directories.
792    ///
793    /// `/run/microsandbox` is the canonical directory for agentd-owned
794    /// runtime files (e.g. the post-handoff stderr log). Creating it
795    /// here keeps the ownership in `init::init` regardless of whether
796    /// handoff is configured.
797    pub fn create_run_dir() -> AgentdResult<()> {
798        mkdir_ignore_exists("/run")?;
799        mkdir_ignore_exists("/run/microsandbox")?;
800        Ok(())
801    }
802
803    /// Ensure login shells preserve `/.msb/scripts` on PATH.
804    pub fn ensure_scripts_path_in_profile() -> AgentdResult<()> {
805        let profile_path = Path::new("/etc/profile");
806        let existing = match fs::read_to_string(profile_path) {
807            Ok(contents) => contents,
808            Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
809            Err(err) => {
810                return Err(AgentdError::Init(format!(
811                    "failed to read {}: {err}",
812                    profile_path.display()
813                )));
814            }
815        };
816
817        let updated = super::ensure_scripts_profile_block(&existing);
818        if updated != existing {
819            if let Some(parent) = profile_path.parent() {
820                fs::create_dir_all(parent).map_err(|err| {
821                    AgentdError::Init(format!("failed to create {}: {err}", parent.display()))
822                })?;
823            }
824            fs::write(profile_path, updated).map_err(|err| {
825                AgentdError::Init(format!("failed to write {}: {err}", profile_path.display()))
826            })?;
827        }
828
829        Ok(())
830    }
831
832    /// Creates a directory, ignoring EEXIST errors.
833    fn mkdir_ignore_exists(path: &str) -> AgentdResult<()> {
834        match unistd::mkdir(path, Mode::from_bits_truncate(0o755)) {
835            Ok(()) => Ok(()),
836            Err(nix::Error::EEXIST) => Ok(()),
837            Err(e) => Err(e.into()),
838        }
839    }
840
841    fn ensure_directory_mode(path: &str, mode: u32) -> AgentdResult<()> {
842        fs::create_dir_all(path)
843            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
844
845        let metadata = fs::metadata(path)
846            .map_err(|e| AgentdError::Init(format!("failed to stat {path}: {e}")))?;
847        if !metadata.is_dir() {
848            return Err(AgentdError::Init(format!(
849                "expected directory at {path}, found non-directory"
850            )));
851        }
852
853        let current_mode = metadata.permissions().mode() & 0o7777;
854        if current_mode != mode {
855            fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|e| {
856                AgentdError::Init(format!("failed to chmod {path} to {mode:o}: {e}"))
857            })?;
858        }
859
860        Ok(())
861    }
862
863    /// Mounts a filesystem, ignoring EBUSY errors (already mounted).
864    fn mount_ignore_busy(
865        source: Option<&str>,
866        target: &str,
867        fstype: Option<&str>,
868        flags: MsFlags,
869        data: Option<&str>,
870    ) -> AgentdResult<()> {
871        match mount::mount(source, target, fstype, flags, data) {
872            Ok(()) => Ok(()),
873            Err(nix::Error::EBUSY) => Ok(()),
874            Err(e) => Err(AgentdError::Init(format!("failed to mount {target}: {e}"))),
875        }
876    }
877}
878
879//--------------------------------------------------------------------------------------------------
880// Tests
881//--------------------------------------------------------------------------------------------------
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886
887    #[test]
888    fn test_ensure_scripts_profile_block_appends_block() {
889        let updated = ensure_scripts_profile_block("export PATH=/usr/bin:/bin\n");
890        assert!(updated.contains("# >>> microsandbox scripts path >>>"));
891        assert!(updated.contains("export PATH=\"/.msb/scripts:$PATH\""));
892    }
893
894    #[test]
895    fn test_ensure_scripts_profile_block_adds_newline_when_missing() {
896        let updated = ensure_scripts_profile_block("export PATH=/usr/bin:/bin");
897        assert!(updated.contains("/usr/bin:/bin\n# >>> microsandbox scripts path >>>"));
898    }
899
900    #[test]
901    fn test_ensure_scripts_profile_block_is_idempotent() {
902        let profile = ensure_scripts_profile_block("");
903        let updated = ensure_scripts_profile_block(&profile);
904        assert_eq!(profile, updated);
905    }
906}