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/// Mount only the filesystems needed to discover and open the agent console.
12///
13/// The console descriptor remains valid when a block-backed root later pivots
14/// and remounts the essential filesystems inside the final guest root.
15pub fn prepare_bootstrap_console() -> AgentdResult<()> {
16    linux::mount_bootstrap_filesystems()
17}
18
19/// Performs synchronous PID 1 initialization.
20///
21/// Applies sandbox-wide resource limits first so every later guest process
22/// inherits the raised baseline, then mounts filesystems, applies directory
23/// mounts, file mounts, and tmpfs mounts from the parsed params. Configures
24/// networking and prepares runtime directories.
25///
26/// Consumes the [`BootParams`] by value — the data is one-shot and not
27/// needed after init returns.
28pub fn init(
29    mut params: BootParams,
30    before_user_mounts: impl FnOnce() -> AgentdResult<()>,
31) -> AgentdResult<()> {
32    rlimit::apply_baseline(&params.rlimits)?;
33    linux::mount_filesystems()?;
34    linux::mount_runtime()?;
35    if let Some(spec) = &params.block_root {
36        linux::mount_block_root(spec)?;
37    }
38    before_user_mounts()?;
39    if params.security_profile == SecurityProfile::Restricted {
40        force_restricted_mount_flags(&mut params);
41    }
42    linux::apply_user_mounts(
43        &params.dir_mounts,
44        &params.file_mounts,
45        &params.disk_mounts,
46        &params.tmpfs,
47    )?;
48    network::apply_hostname(
49        params.hostname.as_deref(),
50        params.host_alias.as_deref(),
51        params.net_ipv4.as_ref().map(|v4| v4.gateway),
52        params.net_ipv6.as_ref().map(|v6| v6.gateway),
53    )?;
54    linux::ensure_standard_tmp_permissions()?;
55    network::apply_network_config(params.network())?;
56    tls::install_ca_cert()?;
57    tls::install_host_cas()?;
58    linux::ensure_scripts_path_in_profile()?;
59    linux::create_run_dir()?;
60    Ok(())
61}
62
63fn force_restricted_mount_flags(params: &mut BootParams) {
64    for spec in &mut params.dir_mounts {
65        spec.nosuid = true;
66        spec.nodev = true;
67    }
68    for spec in &mut params.file_mounts {
69        spec.nosuid = true;
70        spec.nodev = true;
71    }
72    for spec in &mut params.disk_mounts {
73        spec.nosuid = true;
74        spec.nodev = true;
75    }
76    for spec in &mut params.tmpfs {
77        spec.nosuid = true;
78        spec.nodev = true;
79    }
80}
81
82fn ensure_scripts_profile_block(profile: &str) -> String {
83    const START_MARKER: &str = "# >>> microsandbox scripts path >>>";
84    const END_MARKER: &str = "# <<< microsandbox scripts path <<<";
85    const BLOCK: &str = "# >>> microsandbox scripts path >>>\ncase \":$PATH:\" in\n  *:/.msb/scripts:*) ;;\n  *) export PATH=\"/.msb/scripts:$PATH\" ;;\nesac\n# <<< microsandbox scripts path <<<\n";
86
87    if profile.contains(START_MARKER) && profile.contains(END_MARKER) {
88        return profile.to_string();
89    }
90
91    let mut updated = profile.to_string();
92    if !updated.is_empty() && !updated.ends_with('\n') {
93        updated.push('\n');
94    }
95    updated.push_str(BLOCK);
96    updated
97}
98
99//--------------------------------------------------------------------------------------------------
100// Modules
101//--------------------------------------------------------------------------------------------------
102
103mod linux {
104    use std::os::unix::fs::{self as unix_fs, PermissionsExt};
105    use std::path::Path;
106    use std::{fs, thread, time::Duration};
107
108    use nix::mount::{self, MntFlags, MsFlags};
109    use nix::sys::stat::Mode;
110    use nix::unistd;
111    use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath};
112
113    use crate::config::{
114        BlockRootSpec, BlockRootUpper, DirMountSpec, DiskMountSpec, FileMountSpec, TmpfsSpec,
115    };
116    use crate::error::{AgentdError, AgentdResult};
117
118    const UPPER_METRICS_PATH: &str = "/sys/kernel/msb_metrics/upper_path";
119    const UPPER_METRICS_REGISTER_ATTEMPTS: usize = 100;
120    const UPPER_METRICS_REGISTER_RETRY: Duration = Duration::from_millis(10);
121
122    //--------------------------------------------------------------------------------------------------
123    // Types
124    //--------------------------------------------------------------------------------------------------
125
126    /// A mount from any user-facing volume transport.
127    ///
128    /// Keeping the variants together is essential: mounting by transport
129    /// group can let a later parent hide a child from an earlier group.
130    enum UserMount<'a> {
131        Dir(&'a DirMountSpec),
132        File(&'a FileMountSpec),
133        Disk(&'a DiskMountSpec),
134        Tmpfs(&'a TmpfsSpec),
135    }
136
137    struct PlannedUserMount<'a> {
138        depth: usize,
139        canonical_path: String,
140        mount: UserMount<'a>,
141    }
142
143    //--------------------------------------------------------------------------------------------------
144    // Methods
145    //--------------------------------------------------------------------------------------------------
146
147    impl UserMount<'_> {
148        fn guest_path(&self) -> &str {
149            match self {
150                Self::Dir(spec) => &spec.guest_path,
151                Self::File(spec) => &spec.guest_path,
152                Self::Disk(spec) => &spec.guest_path,
153                Self::Tmpfs(spec) => &spec.path,
154            }
155        }
156
157        fn is_file(&self) -> bool {
158            matches!(self, Self::File(_))
159        }
160    }
161
162    /// Mount the minimum filesystems needed for virtio-console discovery.
163    pub fn mount_bootstrap_filesystems() -> AgentdResult<()> {
164        mount_dev()?;
165        mount_sys()?;
166        Ok(())
167    }
168
169    /// Mounts essential Linux filesystems.
170    pub fn mount_filesystems() -> AgentdResult<()> {
171        mount_dev()?;
172
173        // /proc — proc
174        let nodev_noexec_nosuid =
175            MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
176
177        mkdir_ignore_exists("/proc")?;
178        mount_ignore_busy(
179            Some("proc"),
180            "/proc",
181            Some("proc"),
182            nodev_noexec_nosuid,
183            None::<&str>,
184        )?;
185
186        mount_sys()?;
187
188        // /sys/fs/cgroup — cgroup2
189        mkdir_ignore_exists("/sys/fs/cgroup")?;
190        mount_ignore_busy(
191            Some("cgroup2"),
192            "/sys/fs/cgroup",
193            Some("cgroup2"),
194            nodev_noexec_nosuid,
195            None::<&str>,
196        )?;
197
198        // /dev/pts — devpts
199        let noexec_nosuid = MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
200
201        mkdir_ignore_exists("/dev/pts")?;
202        mount_ignore_busy(
203            Some("devpts"),
204            "/dev/pts",
205            Some("devpts"),
206            noexec_nosuid,
207            None::<&str>,
208        )?;
209
210        // /dev/shm — tmpfs
211        mkdir_ignore_exists("/dev/shm")?;
212        mount_ignore_busy(
213            Some("tmpfs"),
214            "/dev/shm",
215            Some("tmpfs"),
216            noexec_nosuid,
217            None::<&str>,
218        )?;
219
220        // devtmpfs hides any links from the image and does not create these aliases.
221        for (target, link) in [
222            ("/proc/self/fd", "/dev/fd"),
223            ("/proc/self/fd/0", "/dev/stdin"),
224            ("/proc/self/fd/1", "/dev/stdout"),
225            ("/proc/self/fd/2", "/dev/stderr"),
226        ] {
227            match unix_fs::symlink(target, link) {
228                Ok(()) => {}
229                // A link may already exist even when its descriptor is closed.
230                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
231                Err(e) => {
232                    return Err(AgentdError::Init(format!("failed to symlink {link}: {e}")));
233                }
234            }
235        }
236
237        Ok(())
238    }
239
240    fn mount_dev() -> AgentdResult<()> {
241        mkdir_ignore_exists("/dev")?;
242        mount_ignore_busy(
243            Some("devtmpfs"),
244            "/dev",
245            Some("devtmpfs"),
246            MsFlags::MS_RELATIME,
247            None::<&str>,
248        )
249    }
250
251    fn mount_sys() -> AgentdResult<()> {
252        let flags =
253            MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
254        mkdir_ignore_exists("/sys")?;
255        mount_ignore_busy(Some("sysfs"), "/sys", Some("sysfs"), flags, None::<&str>)
256    }
257
258    /// Mounts the virtiofs runtime filesystem at the canonical mount point.
259    pub fn mount_runtime() -> AgentdResult<()> {
260        mkdir_ignore_exists(microsandbox_protocol::RUNTIME_MOUNT_POINT)?;
261        mount_ignore_busy(
262            Some(microsandbox_protocol::RUNTIME_FS_TAG),
263            microsandbox_protocol::RUNTIME_MOUNT_POINT,
264            Some("virtiofs"),
265            MsFlags::empty(),
266            None::<&str>,
267        )?;
268        Ok(())
269    }
270
271    /// Assembles the root filesystem from the parsed block-root spec.
272    ///
273    /// Dispatches on the spec variant, then pivots `/newroot` into `/`.
274    pub fn mount_block_root(spec: &BlockRootSpec) -> AgentdResult<()> {
275        mkdir_ignore_exists("/newroot")?;
276
277        match spec {
278            BlockRootSpec::DiskImage { device, fstype } => {
279                mount_disk_image(device, fstype.as_deref())?;
280                crate::root_disk::register("/newroot", device);
281            }
282            BlockRootSpec::OciErofs { lower, upper } => {
283                mount_oci_erofs(lower, upper)?;
284            }
285        }
286
287        pivot_to_newroot()?;
288
289        Ok(())
290    }
291
292    /// Mount a single disk image at /newroot.
293    fn mount_disk_image(device: &str, fstype: Option<&str>) -> AgentdResult<()> {
294        if let Some(fstype) = fstype {
295            mount::mount(
296                Some(device),
297                "/newroot",
298                Some(fstype),
299                MsFlags::empty(),
300                None::<&str>,
301            )
302            .map_err(|e| {
303                AgentdError::Init(format!(
304                    "failed to mount {device} at /newroot as {fstype}: {e}"
305                ))
306            })?;
307        } else {
308            let fstypes = read_proc_filesystems()?;
309            try_mount_any(device, "/newroot", MsFlags::empty(), &fstypes)?;
310        }
311        Ok(())
312    }
313
314    /// Mount merged EROFS lower + writable upper + overlayfs at /newroot.
315    fn mount_oci_erofs(lower_device: &str, upper: &BlockRootUpper) -> AgentdResult<()> {
316        // Mount the EROFS lower device read-only.
317        let lower_dir = "/.msb/rootfs/lower";
318        mkdir_ignore_exists("/.msb/rootfs")?;
319        mkdir_ignore_exists("/.msb/rootfs/lower")?;
320        mount::mount(
321            Some(lower_device),
322            lower_dir,
323            Some("erofs"),
324            MsFlags::MS_RDONLY,
325            None::<&str>,
326        )
327        .map_err(|e| AgentdError::Init(format!("mount {lower_device} at {lower_dir}: {e}")))?;
328
329        // Mount the writable upper: a block-device filesystem (managed ext4
330        // or user disk image), or a RAM-backed tmpfs for tmpfs root disks.
331        let upperfs_dir = "/.msb/rootfs/upperfs";
332        mkdir_ignore_exists("/.msb/rootfs/upperfs")?;
333        match upper {
334            BlockRootUpper::Device { device, fstype } => {
335                mount::mount(
336                    Some(device.as_str()),
337                    upperfs_dir,
338                    Some(fstype.as_str()),
339                    MsFlags::empty(),
340                    None::<&str>,
341                )
342                .map_err(|e| AgentdError::Init(format!("mount {device} at {upperfs_dir}: {e}")))?;
343                crate::root_disk::register(upperfs_dir, device);
344            }
345            BlockRootUpper::Tmpfs { size_mib } => {
346                let data = size_mib
347                    .map(|mib| format!("size={},mode=755", u64::from(mib) * 1024 * 1024))
348                    .unwrap_or_else(|| "mode=755".to_owned());
349                mount::mount(
350                    Some("tmpfs"),
351                    upperfs_dir,
352                    Some("tmpfs"),
353                    MsFlags::MS_RELATIME,
354                    Some(data.as_str()),
355                )
356                .map_err(|e| {
357                    AgentdError::Init(format!("mount tmpfs upper at {upperfs_dir}: {e}"))
358                })?;
359            }
360        }
361        register_upper_metrics(upperfs_dir);
362        // The pivot below makes this mount unreachable by path; pin a fd now so poweroff teardown can still remount it read-only.
363        crate::teardown::register_upper_fs(upperfs_dir);
364
365        // Create upper and work subdirs on the writable device.
366        let upper_dir = format!("{upperfs_dir}/upper");
367        let work_dir = format!("{upperfs_dir}/work");
368        fs::create_dir_all(&upper_dir)
369            .map_err(|e| AgentdError::Init(format!("mkdir {upper_dir}: {e}")))?;
370        fs::create_dir_all(&work_dir)
371            .map_err(|e| AgentdError::Init(format!("mkdir {work_dir}: {e}")))?;
372
373        // Assemble overlayfs mount.
374        let mount_data = format!("lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}");
375
376        mount::mount(
377            Some("overlay"),
378            "/newroot",
379            Some("overlay"),
380            MsFlags::empty(),
381            Some(mount_data.as_str()),
382        )
383        .map_err(|e| AgentdError::Init(format!("mount overlay at /newroot: {e}")))?;
384
385        Ok(())
386    }
387
388    fn register_upper_metrics(upperfs_dir: &str) {
389        for attempt in 0..UPPER_METRICS_REGISTER_ATTEMPTS {
390            match fs::write(UPPER_METRICS_PATH, upperfs_dir) {
391                Ok(()) => return,
392                Err(err)
393                    if err.kind() == std::io::ErrorKind::NotFound
394                        && attempt + 1 < UPPER_METRICS_REGISTER_ATTEMPTS =>
395                {
396                    thread::sleep(UPPER_METRICS_REGISTER_RETRY);
397                }
398                Err(err) if err.kind() == std::io::ErrorKind::NotFound => return,
399                Err(err) => {
400                    eprintln!("agentd: upper metrics registration failed: {err}");
401                    return;
402                }
403            }
404        }
405    }
406
407    /// Bind-mount /.msb into /newroot, then MS_MOVE + chroot + re-mount essentials.
408    fn pivot_to_newroot() -> AgentdResult<()> {
409        let msb_target = "/newroot/.msb";
410        mkdir_ignore_exists(msb_target)?;
411        mount::mount(
412            Some(microsandbox_protocol::RUNTIME_MOUNT_POINT),
413            msb_target,
414            None::<&str>,
415            MsFlags::MS_BIND,
416            None::<&str>,
417        )
418        .map_err(|e| AgentdError::Init(format!("failed to bind-mount /.msb into /newroot: {e}")))?;
419
420        unistd::chdir("/newroot")
421            .map_err(|e| AgentdError::Init(format!("failed to chdir /newroot: {e}")))?;
422
423        mount::mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>)
424            .map_err(|e| AgentdError::Init(format!("failed to MS_MOVE /newroot to /: {e}")))?;
425
426        unistd::chroot(".").map_err(|e| AgentdError::Init(format!("failed to chroot: {e}")))?;
427
428        unistd::chdir("/")
429            .map_err(|e| AgentdError::Init(format!("failed to chdir / after chroot: {e}")))?;
430
431        mount_filesystems()?;
432
433        Ok(())
434    }
435
436    /// Read native filesystem types from `/proc/filesystems`, skipping
437    /// `nodev` entries (virtual filesystems that can't back a real device).
438    fn read_proc_filesystems() -> AgentdResult<Vec<String>> {
439        let content = fs::read_to_string("/proc/filesystems")
440            .map_err(|e| AgentdError::Init(format!("failed to read /proc/filesystems: {e}")))?;
441        Ok(content
442            .lines()
443            .filter_map(|line| {
444                if line.starts_with("nodev") {
445                    return None;
446                }
447                let fstype = line.trim();
448                if fstype.is_empty() {
449                    None
450                } else {
451                    Some(fstype.to_string())
452                }
453            })
454            .collect())
455    }
456
457    /// Try mounting `device` at `target` with `flags`, walking the supplied
458    /// candidate filesystem list until one succeeds. Use
459    /// `read_proc_filesystems` to build the candidate list (typically once
460    /// per init phase) and reuse it across multiple mount attempts.
461    fn try_mount_any(
462        device: &str,
463        target: &str,
464        flags: MsFlags,
465        fstypes: &[String],
466    ) -> AgentdResult<()> {
467        for fstype in fstypes {
468            if mount::mount(
469                Some(device),
470                target,
471                Some(fstype.as_str()),
472                flags,
473                None::<&str>,
474            )
475            .is_ok()
476            {
477                return Ok(());
478            }
479        }
480        Err(AgentdError::Init(format!(
481            "failed to mount {device} at {target}: no supported filesystem found"
482        )))
483    }
484
485    /// Filesystem-specific mount data for disk-image volume mounts.
486    fn disk_mount_data(fstype: &str, readonly: bool) -> Option<&'static str> {
487        if readonly && fstype == "ext4" {
488            // A read-only block device cannot replay an ext4 journal. `noload`
489            // lets seeded or intentionally read-only ext4 images mount without
490            // attempting journal recovery.
491            Some("noload")
492        } else {
493            None
494        }
495    }
496
497    /// Try mounting a disk-image volume, adding filesystem-specific options
498    /// where read-only block devices need them.
499    fn try_mount_disk_any(
500        device: &str,
501        target: &str,
502        flags: MsFlags,
503        readonly: bool,
504        fstypes: &[String],
505    ) -> AgentdResult<()> {
506        for fstype in fstypes {
507            let data = disk_mount_data(fstype, readonly);
508            if mount::mount(Some(device), target, Some(fstype.as_str()), flags, data).is_ok() {
509                return Ok(());
510            }
511        }
512        Err(AgentdError::Init(format!(
513            "disk mount: failed to mount {device} at {target}: no supported filesystem found"
514        )))
515    }
516
517    /// Applies every user mount in one parent-before-child plan.
518    pub fn apply_user_mounts(
519        dir_specs: &[DirMountSpec],
520        file_specs: &[FileMountSpec],
521        disk_specs: &[DiskMountSpec],
522        tmpfs_specs: &[TmpfsSpec],
523    ) -> AgentdResult<()> {
524        let plan = plan_user_mounts(dir_specs, file_specs, disk_specs, tmpfs_specs)?;
525
526        // Read the autodetection candidates once even when disk mounts are
527        // interleaved with other kinds in the final plan.
528        let fstypes = if disk_specs.iter().any(|spec| spec.fstype.is_none()) {
529            Some(read_proc_filesystems()?)
530        } else {
531            None
532        };
533
534        if !file_specs.is_empty() {
535            fs::create_dir_all(microsandbox_protocol::FILE_MOUNTS_DIR).map_err(|e| {
536                AgentdError::Init(format!(
537                    "failed to create file mounts dir {}: {e}",
538                    microsandbox_protocol::FILE_MOUNTS_DIR
539                ))
540            })?;
541        }
542
543        let result = (|| {
544            for planned in plan {
545                match planned.mount {
546                    UserMount::Dir(spec) => mount_dir(spec)?,
547                    UserMount::File(spec) => mount_file(spec)?,
548                    UserMount::Disk(spec) => mount_disk(spec, fstypes.as_deref())?,
549                    UserMount::Tmpfs(spec) => mount_tmpfs(spec)?,
550                }
551            }
552            Ok(())
553        })();
554
555        // Each file share is detached by mount_file; remove the common
556        // staging root after the complete cross-kind plan finishes.
557        if !file_specs.is_empty() {
558            let _ = fs::remove_dir(microsandbox_protocol::FILE_MOUNTS_DIR);
559        }
560
561        result
562    }
563
564    fn plan_user_mounts<'a>(
565        dir_specs: &'a [DirMountSpec],
566        file_specs: &'a [FileMountSpec],
567        disk_specs: &'a [DiskMountSpec],
568        tmpfs_specs: &'a [TmpfsSpec],
569    ) -> AgentdResult<Vec<PlannedUserMount<'a>>> {
570        let mounts = dir_specs
571            .iter()
572            .map(UserMount::Dir)
573            .chain(file_specs.iter().map(UserMount::File))
574            .chain(disk_specs.iter().map(UserMount::Disk))
575            .chain(tmpfs_specs.iter().map(UserMount::Tmpfs));
576        let mut plan = Vec::with_capacity(
577            dir_specs.len() + file_specs.len() + disk_specs.len() + tmpfs_specs.len(),
578        );
579
580        for mount in mounts {
581            let (depth, canonical_path) = mount_order_key(mount.guest_path())?;
582            plan.push(PlannedUserMount {
583                depth,
584                canonical_path,
585                mount,
586            });
587        }
588
589        plan.sort_by(|left, right| {
590            (left.depth, left.canonical_path.as_str())
591                .cmp(&(right.depth, right.canonical_path.as_str()))
592        });
593
594        for pair in plan.windows(2) {
595            if pair[0].canonical_path == pair[1].canonical_path {
596                return Err(AgentdError::Init(format!(
597                    "multiple volumes cannot mount the same guest path: {}",
598                    pair[0].canonical_path
599                )));
600            }
601        }
602
603        // A file can be a mount leaf, but it cannot contain another mount.
604        // Reject the complete plan before executing its first mount so this
605        // configuration cannot fail later with ENOTDIR after partial setup.
606        for file in plan.iter().filter(|planned| planned.mount.is_file()) {
607            let file_path = Utf8UnixPath::new(&file.canonical_path);
608            if let Some(descendant) = plan.iter().find(|candidate| {
609                candidate.depth > file.depth
610                    && Utf8UnixPath::new(&candidate.canonical_path).starts_with(file_path)
611            }) {
612                return Err(AgentdError::Init(format!(
613                    "file mount cannot contain another mount: {} is an ancestor of {}",
614                    file.canonical_path, descendant.canonical_path
615                )));
616            }
617        }
618
619        Ok(plan)
620    }
621
622    fn mount_order_key(guest: &str) -> AgentdResult<(usize, String)> {
623        let path = Utf8UnixPath::new(guest);
624        if !path.is_valid() || !path.is_absolute() {
625            return Err(AgentdError::Init(format!(
626                "invalid guest mount path: {guest}"
627            )));
628        }
629        if path
630            .components()
631            .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
632        {
633            return Err(AgentdError::Init(format!(
634                "guest mount path must not contain '..': {guest}"
635            )));
636        }
637
638        let canonical = path.normalize();
639        if canonical.as_str() == "/" {
640            return Err(AgentdError::Init(
641                "cannot mount a volume at guest root /".into(),
642            ));
643        }
644        let depth = canonical
645            .components()
646            .filter(Utf8Component::is_normal)
647            .count();
648        Ok((depth, canonical.to_string()))
649    }
650
651    #[cfg(test)]
652    pub(super) fn planned_user_mounts_for_test<'a>(
653        dir_specs: &'a [DirMountSpec],
654        file_specs: &'a [FileMountSpec],
655        disk_specs: &'a [DiskMountSpec],
656        tmpfs_specs: &'a [TmpfsSpec],
657    ) -> AgentdResult<Vec<(&'static str, String)>> {
658        plan_user_mounts(dir_specs, file_specs, disk_specs, tmpfs_specs).map(|plan| {
659            plan.into_iter()
660                .map(|planned| {
661                    let kind = match planned.mount {
662                        UserMount::Dir(_) => "dir",
663                        UserMount::File(_) => "file",
664                        UserMount::Disk(_) => "disk",
665                        UserMount::Tmpfs(_) => "tmpfs",
666                    };
667                    (kind, planned.canonical_path)
668                })
669                .collect()
670        })
671    }
672
673    /// Mounts a single virtiofs directory share from a parsed spec.
674    fn mount_dir(spec: &DirMountSpec) -> AgentdResult<()> {
675        let path = spec.guest_path.as_str();
676
677        // Create the mount point directory.
678        fs::create_dir_all(path)
679            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
680
681        let mut flags = MsFlags::MS_RELATIME;
682        if spec.nosuid {
683            flags |= MsFlags::MS_NOSUID;
684        }
685        if spec.nodev {
686            flags |= MsFlags::MS_NODEV;
687        }
688        if spec.noexec {
689            flags |= MsFlags::MS_NOEXEC;
690        }
691        if spec.readonly {
692            flags |= MsFlags::MS_RDONLY;
693        }
694
695        mount::mount(
696            Some(spec.tag.as_str()),
697            path,
698            Some("virtiofs"),
699            flags,
700            None::<&str>,
701        )
702        .map_err(|e| {
703            AgentdError::Init(format!(
704                "failed to mount virtiofs tag '{}' at {path}: {e}",
705                spec.tag
706            ))
707        })?;
708
709        Ok(())
710    }
711
712    /// Mounts a single file from a virtiofs share via bind mount.
713    fn mount_file(spec: &FileMountSpec) -> AgentdResult<()> {
714        let staging_path = format!("{}/{}", microsandbox_protocol::FILE_MOUNTS_DIR, spec.tag);
715
716        // 1. Create the staging mount point directory.
717        fs::create_dir_all(&staging_path).map_err(|e| {
718            AgentdError::Init(format!("failed to create staging dir {staging_path}: {e}"))
719        })?;
720
721        // 2. Mount the virtiofs share at the staging directory.
722        let mut flags = MsFlags::MS_RELATIME;
723        if spec.nosuid {
724            flags |= MsFlags::MS_NOSUID;
725        }
726        if spec.nodev {
727            flags |= MsFlags::MS_NODEV;
728        }
729        if spec.noexec {
730            flags |= MsFlags::MS_NOEXEC;
731        }
732        if spec.readonly {
733            flags |= MsFlags::MS_RDONLY;
734        }
735
736        mount::mount(
737            Some(spec.tag.as_str()),
738            staging_path.as_str(),
739            Some("virtiofs"),
740            flags,
741            None::<&str>,
742        )
743        .map_err(|e| {
744            AgentdError::Init(format!(
745                "failed to mount virtiofs tag '{}' at {staging_path}: {e}",
746                spec.tag
747            ))
748        })?;
749
750        let bind_result = (|| {
751            // 3. Create parent directories for the guest path.
752            let guest = Path::new(&spec.guest_path);
753            if let Some(parent) = guest.parent() {
754                fs::create_dir_all(parent).map_err(|e| {
755                    AgentdError::Init(format!(
756                        "failed to create parent dirs for {}: {e}",
757                        spec.guest_path
758                    ))
759                })?;
760            }
761
762            // 4. Create the target file (touch) as a bind mount target.
763            fs::OpenOptions::new()
764                .create(true)
765                .truncate(false)
766                .write(true)
767                .open(&spec.guest_path)
768                .map_err(|e| {
769                    AgentdError::Init(format!(
770                        "failed to create bind target {}: {e}",
771                        spec.guest_path
772                    ))
773                })?;
774
775            // 5. Bind mount the file from staging to the guest path.
776            let source_path = format!("{staging_path}/{}", spec.filename);
777            mount::mount(
778                Some(source_path.as_str()),
779                spec.guest_path.as_str(),
780                None::<&str>,
781                MsFlags::MS_BIND,
782                None::<&str>,
783            )
784            .map_err(|e| {
785                AgentdError::Init(format!(
786                    "failed to bind mount {source_path} to {}: {e}",
787                    spec.guest_path
788                ))
789            })?;
790
791            // 6. Remount the file bind with the guest-facing VFS flags.
792            let mut remount_flags = MsFlags::MS_BIND | MsFlags::MS_REMOUNT;
793            if spec.nosuid {
794                remount_flags |= MsFlags::MS_NOSUID;
795            }
796            if spec.nodev {
797                remount_flags |= MsFlags::MS_NODEV;
798            }
799            if spec.noexec {
800                remount_flags |= MsFlags::MS_NOEXEC;
801            }
802            if spec.readonly {
803                remount_flags |= MsFlags::MS_RDONLY;
804            }
805            mount::mount(
806                None::<&str>,
807                spec.guest_path.as_str(),
808                None::<&str>,
809                remount_flags,
810                None::<&str>,
811            )
812            .map_err(|e| {
813                AgentdError::Init(format!(
814                    "failed to remount {} with volume flags: {e}",
815                    spec.guest_path
816                ))
817            })?;
818
819            Ok(())
820        })();
821
822        let cleanup_result = cleanup_file_mount_staging(&staging_path);
823        match (bind_result, cleanup_result) {
824            (Ok(()), Ok(())) => Ok(()),
825            (Err(err), Ok(())) => Err(err),
826            (Ok(()), Err(err)) => Err(err),
827            (Err(err), Err(cleanup_err)) => Err(AgentdError::Init(format!(
828                "{err}; additionally failed to cleanup file mount staging {staging_path}: {cleanup_err}"
829            ))),
830        }
831    }
832
833    fn cleanup_file_mount_staging(staging_path: &str) -> AgentdResult<()> {
834        // The bind mount keeps the file accessible at the guest path; removing
835        // the share prevents alternate-path access through the staging tree.
836        mount::umount2(staging_path, MntFlags::MNT_DETACH).map_err(|e| {
837            AgentdError::Init(format!(
838                "failed to unmount file mount staging {staging_path}: {e}"
839            ))
840        })?;
841        fs::remove_dir(staging_path).map_err(|e| {
842            AgentdError::Init(format!(
843                "failed to remove file mount staging {staging_path}: {e}"
844            ))
845        })?;
846        Ok(())
847    }
848
849    /// Resolve the block device for a disk-image mount id.
850    ///
851    /// Primary path: `/dev/disk/by-id/virtio-<id>`, which udev/kernel
852    /// create when the VMM sets `virtio_blk_config.serial`.
853    /// Fallback: scan `/sys/block/*/serial` for a match, which works
854    /// even when udev is unavailable or has not yet populated the
855    /// symlink.
856    fn resolve_disk_device(id: &str) -> AgentdResult<String> {
857        use std::{thread::sleep, time::Duration};
858        const RETRIES: u32 = 20;
859        const INTERVAL: Duration = Duration::from_millis(10);
860
861        let by_id = format!("/dev/disk/by-id/virtio-{id}");
862        for attempt in 0..RETRIES {
863            if Path::new(&by_id).exists() {
864                return Ok(by_id);
865            }
866            if let Some(dev) = scan_block_serial(id) {
867                return Ok(dev);
868            }
869            // Skip the sleep after the last check so the failure path
870            // doesn't pay 10ms it can't use.
871            if attempt + 1 < RETRIES {
872                sleep(INTERVAL);
873            }
874        }
875        Err(AgentdError::Init(format!(
876            "disk mount: no block device found for id '{id}' \
877             (checked /dev/disk/by-id/virtio-{id} and /sys/block/*/serial)"
878        )))
879    }
880
881    /// Walk `/sys/block/*` for an entry whose `serial` file matches `id`.
882    fn scan_block_serial(id: &str) -> Option<String> {
883        let entries = fs::read_dir("/sys/block").ok()?;
884        for entry in entries.flatten() {
885            let name = entry.file_name();
886            let Some(name_str) = name.to_str() else {
887                continue;
888            };
889            if !name_str.starts_with("vd") {
890                continue;
891            }
892            let serial_path = entry.path().join("serial");
893            let Ok(serial) = fs::read_to_string(&serial_path) else {
894                continue;
895            };
896            if serial.trim() == id {
897                return Some(format!("/dev/{name_str}"));
898            }
899        }
900        None
901    }
902
903    fn mount_disk(spec: &DiskMountSpec, fstypes: Option<&[String]>) -> AgentdResult<()> {
904        let path = spec.guest_path.as_str();
905        fs::create_dir_all(path)
906            .map_err(|e| AgentdError::Init(format!("disk mount: create dir {path}: {e}")))?;
907
908        let device = resolve_disk_device(&spec.id)?;
909
910        let mut flags = MsFlags::MS_RELATIME;
911        if spec.nosuid {
912            flags |= MsFlags::MS_NOSUID;
913        }
914        if spec.nodev {
915            flags |= MsFlags::MS_NODEV;
916        }
917        if spec.noexec {
918            flags |= MsFlags::MS_NOEXEC;
919        }
920        if spec.readonly {
921            flags |= MsFlags::MS_RDONLY;
922        }
923
924        if let Some(fstype) = spec.fstype.as_deref() {
925            let data = disk_mount_data(fstype, spec.readonly);
926            mount::mount(Some(device.as_str()), path, Some(fstype), flags, data).map_err(|e| {
927                AgentdError::Init(format!(
928                    "disk mount: failed to mount {device} at {path} as {fstype}: {e}"
929                ))
930            })?;
931        } else {
932            let fstypes = fstypes.ok_or_else(|| {
933                AgentdError::Init("disk mount: missing filesystem autodetect list".into())
934            })?;
935            try_mount_disk_any(&device, path, flags, spec.readonly, fstypes)?;
936        }
937
938        Ok(())
939    }
940
941    /// Ensure standard temporary directories are writable and sticky.
942    pub fn ensure_standard_tmp_permissions() -> AgentdResult<()> {
943        ensure_directory_mode("/tmp", 0o1777)?;
944        ensure_directory_mode("/var/tmp", 0o1777)?;
945        Ok(())
946    }
947
948    /// Mounts a single tmpfs from a parsed spec.
949    fn mount_tmpfs(spec: &TmpfsSpec) -> AgentdResult<()> {
950        let path = spec.path.as_str();
951
952        // Determine the permission mode.
953        let mode = spec
954            .mode
955            .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
956                0o1777
957            } else {
958                0o755
959            });
960
961        // Create the target directory.
962        fs::create_dir_all(path)
963            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
964
965        let mut flags = MsFlags::MS_RELATIME;
966        if spec.nosuid {
967            flags |= MsFlags::MS_NOSUID;
968        }
969        if spec.nodev {
970            flags |= MsFlags::MS_NODEV;
971        }
972        if spec.noexec {
973            flags |= MsFlags::MS_NOEXEC;
974        }
975        if spec.readonly {
976            flags |= MsFlags::MS_RDONLY;
977        }
978
979        // Mount data: size and mode options.
980        let mut data = String::new();
981        if let Some(mib) = spec.size_mib {
982            data.push_str(&format!("size={}", u64::from(mib) * 1024 * 1024));
983        }
984        if !data.is_empty() {
985            data.push(',');
986        }
987        data.push_str(&format!("mode={mode:o}"));
988
989        mount::mount(
990            Some("tmpfs"),
991            path,
992            Some("tmpfs"),
993            flags,
994            Some(data.as_str()),
995        )
996        .map_err(|e| AgentdError::Init(format!("failed to mount tmpfs at {path}: {e}")))?;
997
998        Ok(())
999    }
1000
1001    /// Creates `/run` and `/run/microsandbox` directories.
1002    ///
1003    /// `/run/microsandbox` is the canonical directory for agentd-owned
1004    /// runtime files (e.g. the post-handoff stderr log). Creating it
1005    /// here keeps the ownership in `init::init` regardless of whether
1006    /// handoff is configured.
1007    pub fn create_run_dir() -> AgentdResult<()> {
1008        mkdir_ignore_exists("/run")?;
1009        mkdir_ignore_exists("/run/microsandbox")?;
1010        Ok(())
1011    }
1012
1013    /// Ensure login shells preserve `/.msb/scripts` on PATH.
1014    pub fn ensure_scripts_path_in_profile() -> AgentdResult<()> {
1015        let profile_path = Path::new("/etc/profile");
1016        let existing = match fs::read_to_string(profile_path) {
1017            Ok(contents) => contents,
1018            Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
1019            Err(err) => {
1020                return Err(AgentdError::Init(format!(
1021                    "failed to read {}: {err}",
1022                    profile_path.display()
1023                )));
1024            }
1025        };
1026
1027        let updated = super::ensure_scripts_profile_block(&existing);
1028        if updated != existing {
1029            if let Some(parent) = profile_path.parent() {
1030                fs::create_dir_all(parent).map_err(|err| {
1031                    AgentdError::Init(format!("failed to create {}: {err}", parent.display()))
1032                })?;
1033            }
1034            fs::write(profile_path, updated).map_err(|err| {
1035                AgentdError::Init(format!("failed to write {}: {err}", profile_path.display()))
1036            })?;
1037        }
1038
1039        Ok(())
1040    }
1041
1042    /// Creates a directory, ignoring EEXIST errors.
1043    fn mkdir_ignore_exists(path: &str) -> AgentdResult<()> {
1044        match unistd::mkdir(path, Mode::from_bits_truncate(0o755)) {
1045            Ok(()) => Ok(()),
1046            Err(nix::Error::EEXIST) => Ok(()),
1047            Err(e) => Err(e.into()),
1048        }
1049    }
1050
1051    fn ensure_directory_mode(path: &str, mode: u32) -> AgentdResult<()> {
1052        fs::create_dir_all(path)
1053            .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
1054
1055        let metadata = fs::metadata(path)
1056            .map_err(|e| AgentdError::Init(format!("failed to stat {path}: {e}")))?;
1057        if !metadata.is_dir() {
1058            return Err(AgentdError::Init(format!(
1059                "expected directory at {path}, found non-directory"
1060            )));
1061        }
1062
1063        let current_mode = metadata.permissions().mode() & 0o7777;
1064        if current_mode != mode {
1065            fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|e| {
1066                AgentdError::Init(format!("failed to chmod {path} to {mode:o}: {e}"))
1067            })?;
1068        }
1069
1070        Ok(())
1071    }
1072
1073    /// Mounts a filesystem, ignoring EBUSY errors (already mounted).
1074    fn mount_ignore_busy(
1075        source: Option<&str>,
1076        target: &str,
1077        fstype: Option<&str>,
1078        flags: MsFlags,
1079        data: Option<&str>,
1080    ) -> AgentdResult<()> {
1081        match mount::mount(source, target, fstype, flags, data) {
1082            Ok(()) => Ok(()),
1083            Err(nix::Error::EBUSY) => Ok(()),
1084            Err(e) => Err(AgentdError::Init(format!("failed to mount {target}: {e}"))),
1085        }
1086    }
1087}
1088
1089//--------------------------------------------------------------------------------------------------
1090// Tests
1091//--------------------------------------------------------------------------------------------------
1092
1093#[cfg(test)]
1094mod tests {
1095    use super::*;
1096    use crate::config::{DirMountSpec, DiskMountSpec, FileMountSpec, TmpfsSpec};
1097
1098    #[test]
1099    fn test_ensure_scripts_profile_block_appends_block() {
1100        let updated = ensure_scripts_profile_block("export PATH=/usr/bin:/bin\n");
1101        assert!(updated.contains("# >>> microsandbox scripts path >>>"));
1102        assert!(updated.contains("export PATH=\"/.msb/scripts:$PATH\""));
1103    }
1104
1105    #[test]
1106    fn test_ensure_scripts_profile_block_adds_newline_when_missing() {
1107        let updated = ensure_scripts_profile_block("export PATH=/usr/bin:/bin");
1108        assert!(updated.contains("/usr/bin:/bin\n# >>> microsandbox scripts path >>>"));
1109    }
1110
1111    #[test]
1112    fn test_ensure_scripts_profile_block_is_idempotent() {
1113        let profile = ensure_scripts_profile_block("");
1114        let updated = ensure_scripts_profile_block(&profile);
1115        assert_eq!(profile, updated);
1116    }
1117
1118    #[test]
1119    fn test_user_mount_plan_orders_mixed_kinds_parent_first() {
1120        let dirs = vec![DirMountSpec {
1121            tag: "workspace".into(),
1122            guest_path: "/workspace".into(),
1123            readonly: false,
1124            noexec: false,
1125            nosuid: false,
1126            nodev: false,
1127        }];
1128        let files = vec![FileMountSpec {
1129            tag: "config".into(),
1130            filename: "app.toml".into(),
1131            guest_path: "/workspace/persist/app.toml".into(),
1132            readonly: true,
1133            noexec: false,
1134            nosuid: false,
1135            nodev: false,
1136        }];
1137        let disks = vec![DiskMountSpec {
1138            id: "durable".into(),
1139            guest_path: "/workspace/persist".into(),
1140            fstype: Some("ext4".into()),
1141            readonly: false,
1142            noexec: false,
1143            nosuid: false,
1144            nodev: false,
1145        }];
1146        let tmpfs = vec![TmpfsSpec {
1147            path: "/workspace/persist/cache".into(),
1148            size_mib: None,
1149            mode: None,
1150            noexec: false,
1151            nosuid: false,
1152            nodev: false,
1153            readonly: false,
1154        }];
1155
1156        let plan = linux::planned_user_mounts_for_test(&dirs, &files, &disks, &tmpfs).unwrap();
1157
1158        assert_eq!(
1159            plan,
1160            vec![
1161                ("dir", "/workspace".into()),
1162                ("disk", "/workspace/persist".into()),
1163                ("file", "/workspace/persist/app.toml".into()),
1164                ("tmpfs", "/workspace/persist/cache".into()),
1165            ]
1166        );
1167    }
1168
1169    #[test]
1170    fn test_user_mount_plan_rejects_file_mount_as_parent() {
1171        let dirs = vec![DirMountSpec {
1172            tag: "persist".into(),
1173            guest_path: "/workspace/persist".into(),
1174            readonly: false,
1175            noexec: false,
1176            nosuid: false,
1177            nodev: false,
1178        }];
1179        let files = vec![FileMountSpec {
1180            tag: "workspace".into(),
1181            filename: "workspace".into(),
1182            guest_path: "/workspace".into(),
1183            readonly: true,
1184            noexec: false,
1185            nosuid: false,
1186            nodev: false,
1187        }];
1188
1189        let error = linux::planned_user_mounts_for_test(&dirs, &files, &[], &[]).unwrap_err();
1190
1191        assert!(error.to_string().contains("file mount cannot contain"));
1192    }
1193}