1use crate::config::BootParams;
4use crate::error::AgentdResult;
5use crate::{network, rlimit, tls};
6
7pub fn init(params: BootParams) -> AgentdResult<()> {
21 rlimit::apply_baseline(¶ms.rlimits)?;
22 linux::mount_filesystems()?;
23 linux::mount_runtime()?;
24 if let Some(spec) = ¶ms.block_root {
25 linux::mount_block_root(spec)?;
26 }
27 linux::apply_dir_mounts(¶ms.dir_mounts)?;
28 linux::apply_file_mounts(¶ms.file_mounts)?;
29 linux::apply_disk_mounts(¶ms.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(¶ms.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
63mod 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 pub fn mount_filesystems() -> AgentdResult<()> {
81 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 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 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 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 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 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 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 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 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 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 fn mount_oci_erofs(
216 lower_device: &str,
217 upper_device: &str,
218 upper_fstype: &str,
219 ) -> AgentdResult<()> {
220 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 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 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 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 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 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 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 pub fn apply_dir_mounts(specs: &[DirMountSpec]) -> AgentdResult<()> {
348 for spec in specs {
349 mount_dir(spec)?;
350 }
351 Ok(())
352 }
353
354 fn mount_dir(spec: &DirMountSpec) -> AgentdResult<()> {
356 let path = spec.guest_path.as_str();
357
358 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 pub fn apply_file_mounts(specs: &[FileMountSpec]) -> AgentdResult<()> {
386 if specs.is_empty() {
387 return Ok(());
388 }
389
390 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 let _ = fs::remove_dir(microsandbox_protocol::FILE_MOUNTS_DIR);
405
406 Ok(())
407 }
408
409 fn mount_file(spec: &FileMountSpec) -> AgentdResult<()> {
411 let staging_path = format!("{}/{}", microsandbox_protocol::FILE_MOUNTS_DIR, spec.tag);
412
413 fs::create_dir_all(&staging_path).map_err(|e| {
415 AgentdError::Init(format!("failed to create staging dir {staging_path}: {e}"))
416 })?;
417
418 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 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 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 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 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 let _ = mount::umount2(staging_path.as_str(), MntFlags::MNT_DETACH);
499 let _ = fs::remove_dir(&staging_path);
500
501 Ok(())
502 }
503
504 pub fn apply_disk_mounts(specs: &[DiskMountSpec]) -> AgentdResult<()> {
506 if specs.is_empty() {
507 return Ok(());
508 }
509 let fstypes = read_proc_filesystems()?;
512 for spec in specs {
513 mount_disk(spec, &fstypes)?;
514 }
515 Ok(())
516 }
517
518 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 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 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 pub fn apply_tmpfs_mounts(specs: &[TmpfsSpec]) -> AgentdResult<()> {
606 for spec in specs {
607 mount_tmpfs(spec)?;
608 }
609 Ok(())
610 }
611
612 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 fn mount_tmpfs(spec: &TmpfsSpec) -> AgentdResult<()> {
621 let path = spec.path.as_str();
622
623 let mode = spec
625 .mode
626 .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
627 0o1777
628 } else {
629 0o755
630 });
631
632 fs::create_dir_all(path)
634 .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
635
636 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 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 pub fn create_run_dir() -> AgentdResult<()> {
674 mkdir_ignore_exists("/run")?;
675 mkdir_ignore_exists("/run/microsandbox")?;
676 Ok(())
677 }
678
679 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 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 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#[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}