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