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