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