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