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