Skip to main content

microsandbox_agentd/
init.rs

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