1use crate::config::{BootParams, SecurityProfile};
4use crate::error::AgentdResult;
5use crate::{network, rlimit, tls};
6
7pub fn prepare_bootstrap_console() -> AgentdResult<()> {
16 linux::mount_bootstrap_filesystems()
17}
18
19pub fn init(
29 mut params: BootParams,
30 before_user_mounts: impl FnOnce() -> AgentdResult<()>,
31) -> AgentdResult<()> {
32 rlimit::apply_baseline(¶ms.rlimits)?;
33 linux::mount_filesystems()?;
34 linux::mount_runtime()?;
35 if let Some(spec) = ¶ms.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 ¶ms.dir_mounts,
44 ¶ms.file_mounts,
45 ¶ms.disk_mounts,
46 ¶ms.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
99mod 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 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 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 pub fn mount_bootstrap_filesystems() -> AgentdResult<()> {
164 mount_dev()?;
165 mount_sys()?;
166 Ok(())
167 }
168
169 pub fn mount_filesystems() -> AgentdResult<()> {
171 mount_dev()?;
172
173 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 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 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 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 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 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 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 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 fn mount_oci_erofs(lower_device: &str, upper: &BlockRootUpper) -> AgentdResult<()> {
304 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 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 crate::teardown::register_upper_fs(upperfs_dir);
351
352 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 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 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 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 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 fn disk_mount_data(fstype: &str, readonly: bool) -> Option<&'static str> {
474 if readonly && fstype == "ext4" {
475 Some("noload")
479 } else {
480 None
481 }
482 }
483
484 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 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 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 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 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 fn mount_dir(spec: &DirMountSpec) -> AgentdResult<()> {
662 let path = spec.guest_path.as_str();
663
664 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 fn mount_file(spec: &FileMountSpec) -> AgentdResult<()> {
701 let staging_path = format!("{}/{}", microsandbox_protocol::FILE_MOUNTS_DIR, spec.tag);
702
703 fs::create_dir_all(&staging_path).map_err(|e| {
705 AgentdError::Init(format!("failed to create staging dir {staging_path}: {e}"))
706 })?;
707
708 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 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 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 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 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 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 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 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 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 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 fn mount_tmpfs(spec: &TmpfsSpec) -> AgentdResult<()> {
937 let path = spec.path.as_str();
938
939 let mode = spec
941 .mode
942 .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
943 0o1777
944 } else {
945 0o755
946 });
947
948 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 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 pub fn create_run_dir() -> AgentdResult<()> {
995 mkdir_ignore_exists("/run")?;
996 mkdir_ignore_exists("/run/microsandbox")?;
997 Ok(())
998 }
999
1000 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 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 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#[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}