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