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