1use crate::config::BootParams;
4use crate::error::AgentdResult;
5use crate::{network, rlimit, tls};
6
7pub fn init(
21 params: BootParams,
22 before_user_mounts: impl FnOnce() -> AgentdResult<()>,
23) -> AgentdResult<()> {
24 rlimit::apply_baseline(¶ms.rlimits)?;
25 linux::mount_filesystems()?;
26 linux::mount_runtime()?;
27 if let Some(spec) = ¶ms.block_root {
28 linux::mount_block_root(spec)?;
29 }
30 before_user_mounts()?;
31 linux::apply_dir_mounts(¶ms.dir_mounts)?;
32 linux::apply_file_mounts(¶ms.file_mounts)?;
33 linux::apply_disk_mounts(¶ms.disk_mounts)?;
34 network::apply_hostname(
35 params.hostname.as_deref(),
36 params.host_alias.as_deref(),
37 params.net_ipv4.as_ref().map(|v4| v4.gateway),
38 params.net_ipv6.as_ref().map(|v6| v6.gateway),
39 )?;
40 linux::apply_tmpfs_mounts(¶ms.tmpfs)?;
41 linux::ensure_standard_tmp_permissions()?;
42 network::apply_network_config(params.network())?;
43 tls::install_ca_cert()?;
44 tls::install_host_cas()?;
45 linux::ensure_scripts_path_in_profile()?;
46 linux::create_run_dir()?;
47 Ok(())
48}
49
50fn ensure_scripts_profile_block(profile: &str) -> String {
51 const START_MARKER: &str = "# >>> microsandbox scripts path >>>";
52 const END_MARKER: &str = "# <<< microsandbox scripts path <<<";
53 const BLOCK: &str = "# >>> microsandbox scripts path >>>\ncase \":$PATH:\" in\n *:/.msb/scripts:*) ;;\n *) export PATH=\"/.msb/scripts:$PATH\" ;;\nesac\n# <<< microsandbox scripts path <<<\n";
54
55 if profile.contains(START_MARKER) && profile.contains(END_MARKER) {
56 return profile.to_string();
57 }
58
59 let mut updated = profile.to_string();
60 if !updated.is_empty() && !updated.ends_with('\n') {
61 updated.push('\n');
62 }
63 updated.push_str(BLOCK);
64 updated
65}
66
67mod linux {
72 use std::fs;
73 use std::os::unix::fs::{self as unix_fs, PermissionsExt};
74 use std::path::Path;
75
76 use nix::mount::{self, MntFlags, MsFlags};
77 use nix::sys::stat::Mode;
78 use nix::unistd;
79
80 use crate::config::{BlockRootSpec, DirMountSpec, DiskMountSpec, FileMountSpec, TmpfsSpec};
81 use crate::error::{AgentdError, AgentdResult};
82
83 pub fn mount_filesystems() -> AgentdResult<()> {
85 mkdir_ignore_exists("/dev")?;
87 mount_ignore_busy(
88 Some("devtmpfs"),
89 "/dev",
90 Some("devtmpfs"),
91 MsFlags::MS_RELATIME,
92 None::<&str>,
93 )?;
94
95 let nodev_noexec_nosuid =
97 MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
98
99 mkdir_ignore_exists("/proc")?;
100 mount_ignore_busy(
101 Some("proc"),
102 "/proc",
103 Some("proc"),
104 nodev_noexec_nosuid,
105 None::<&str>,
106 )?;
107
108 mkdir_ignore_exists("/sys")?;
110 mount_ignore_busy(
111 Some("sysfs"),
112 "/sys",
113 Some("sysfs"),
114 nodev_noexec_nosuid,
115 None::<&str>,
116 )?;
117
118 mkdir_ignore_exists("/sys/fs/cgroup")?;
120 mount_ignore_busy(
121 Some("cgroup2"),
122 "/sys/fs/cgroup",
123 Some("cgroup2"),
124 nodev_noexec_nosuid,
125 None::<&str>,
126 )?;
127
128 let noexec_nosuid = MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME;
130
131 mkdir_ignore_exists("/dev/pts")?;
132 mount_ignore_busy(
133 Some("devpts"),
134 "/dev/pts",
135 Some("devpts"),
136 noexec_nosuid,
137 None::<&str>,
138 )?;
139
140 mkdir_ignore_exists("/dev/shm")?;
142 mount_ignore_busy(
143 Some("tmpfs"),
144 "/dev/shm",
145 Some("tmpfs"),
146 noexec_nosuid,
147 None::<&str>,
148 )?;
149
150 if !Path::new("/dev/fd").exists() {
152 unix_fs::symlink("/proc/self/fd", "/dev/fd")
153 .map_err(|e| AgentdError::Init(format!("failed to symlink /dev/fd: {e}")))?;
154 }
155
156 Ok(())
157 }
158
159 pub fn mount_runtime() -> AgentdResult<()> {
161 mkdir_ignore_exists(microsandbox_protocol::RUNTIME_MOUNT_POINT)?;
162 mount_ignore_busy(
163 Some(microsandbox_protocol::RUNTIME_FS_TAG),
164 microsandbox_protocol::RUNTIME_MOUNT_POINT,
165 Some("virtiofs"),
166 MsFlags::empty(),
167 None::<&str>,
168 )?;
169 Ok(())
170 }
171
172 pub fn mount_block_root(spec: &BlockRootSpec) -> AgentdResult<()> {
176 mkdir_ignore_exists("/newroot")?;
177
178 match spec {
179 BlockRootSpec::DiskImage { device, fstype } => {
180 mount_disk_image(device, fstype.as_deref())?;
181 }
182 BlockRootSpec::OciErofs {
183 lower,
184 upper,
185 upper_fstype,
186 } => {
187 mount_oci_erofs(lower, upper, upper_fstype)?;
188 }
189 }
190
191 pivot_to_newroot()?;
192
193 Ok(())
194 }
195
196 fn mount_disk_image(device: &str, fstype: Option<&str>) -> AgentdResult<()> {
198 if let Some(fstype) = fstype {
199 mount::mount(
200 Some(device),
201 "/newroot",
202 Some(fstype),
203 MsFlags::empty(),
204 None::<&str>,
205 )
206 .map_err(|e| {
207 AgentdError::Init(format!(
208 "failed to mount {device} at /newroot as {fstype}: {e}"
209 ))
210 })?;
211 } else {
212 let fstypes = read_proc_filesystems()?;
213 try_mount_any(device, "/newroot", MsFlags::empty(), &fstypes)?;
214 }
215 Ok(())
216 }
217
218 fn mount_oci_erofs(
220 lower_device: &str,
221 upper_device: &str,
222 upper_fstype: &str,
223 ) -> AgentdResult<()> {
224 let lower_dir = "/.msb/rootfs/lower";
226 mkdir_ignore_exists("/.msb/rootfs")?;
227 mkdir_ignore_exists("/.msb/rootfs/lower")?;
228 mount::mount(
229 Some(lower_device),
230 lower_dir,
231 Some("erofs"),
232 MsFlags::MS_RDONLY,
233 None::<&str>,
234 )
235 .map_err(|e| AgentdError::Init(format!("mount {lower_device} at {lower_dir}: {e}")))?;
236
237 let upperfs_dir = "/.msb/rootfs/upperfs";
239 mkdir_ignore_exists("/.msb/rootfs/upperfs")?;
240 mount::mount(
241 Some(upper_device),
242 upperfs_dir,
243 Some(upper_fstype),
244 MsFlags::empty(),
245 None::<&str>,
246 )
247 .map_err(|e| AgentdError::Init(format!("mount {upper_device} at {upperfs_dir}: {e}")))?;
248
249 let upper_dir = format!("{upperfs_dir}/upper");
251 let work_dir = format!("{upperfs_dir}/work");
252 fs::create_dir_all(&upper_dir)
253 .map_err(|e| AgentdError::Init(format!("mkdir {upper_dir}: {e}")))?;
254 fs::create_dir_all(&work_dir)
255 .map_err(|e| AgentdError::Init(format!("mkdir {work_dir}: {e}")))?;
256
257 let mount_data = format!("lowerdir={lower_dir},upperdir={upper_dir},workdir={work_dir}");
259
260 mount::mount(
261 Some("overlay"),
262 "/newroot",
263 Some("overlay"),
264 MsFlags::empty(),
265 Some(mount_data.as_str()),
266 )
267 .map_err(|e| AgentdError::Init(format!("mount overlay at /newroot: {e}")))?;
268
269 Ok(())
270 }
271
272 fn pivot_to_newroot() -> AgentdResult<()> {
274 let msb_target = "/newroot/.msb";
275 mkdir_ignore_exists(msb_target)?;
276 mount::mount(
277 Some(microsandbox_protocol::RUNTIME_MOUNT_POINT),
278 msb_target,
279 None::<&str>,
280 MsFlags::MS_BIND,
281 None::<&str>,
282 )
283 .map_err(|e| AgentdError::Init(format!("failed to bind-mount /.msb into /newroot: {e}")))?;
284
285 unistd::chdir("/newroot")
286 .map_err(|e| AgentdError::Init(format!("failed to chdir /newroot: {e}")))?;
287
288 mount::mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>)
289 .map_err(|e| AgentdError::Init(format!("failed to MS_MOVE /newroot to /: {e}")))?;
290
291 unistd::chroot(".").map_err(|e| AgentdError::Init(format!("failed to chroot: {e}")))?;
292
293 unistd::chdir("/")
294 .map_err(|e| AgentdError::Init(format!("failed to chdir / after chroot: {e}")))?;
295
296 mount_filesystems()?;
297
298 Ok(())
299 }
300
301 fn read_proc_filesystems() -> AgentdResult<Vec<String>> {
304 let content = fs::read_to_string("/proc/filesystems")
305 .map_err(|e| AgentdError::Init(format!("failed to read /proc/filesystems: {e}")))?;
306 Ok(content
307 .lines()
308 .filter_map(|line| {
309 if line.starts_with("nodev") {
310 return None;
311 }
312 let fstype = line.trim();
313 if fstype.is_empty() {
314 None
315 } else {
316 Some(fstype.to_string())
317 }
318 })
319 .collect())
320 }
321
322 fn try_mount_any(
327 device: &str,
328 target: &str,
329 flags: MsFlags,
330 fstypes: &[String],
331 ) -> AgentdResult<()> {
332 for fstype in fstypes {
333 if mount::mount(
334 Some(device),
335 target,
336 Some(fstype.as_str()),
337 flags,
338 None::<&str>,
339 )
340 .is_ok()
341 {
342 return Ok(());
343 }
344 }
345 Err(AgentdError::Init(format!(
346 "failed to mount {device} at {target}: no supported filesystem found"
347 )))
348 }
349
350 pub fn apply_dir_mounts(specs: &[DirMountSpec]) -> AgentdResult<()> {
352 for spec in specs {
353 mount_dir(spec)?;
354 }
355 Ok(())
356 }
357
358 fn mount_dir(spec: &DirMountSpec) -> AgentdResult<()> {
360 let path = spec.guest_path.as_str();
361
362 fs::create_dir_all(path)
364 .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
365
366 let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME;
367 if spec.noexec {
368 flags |= MsFlags::MS_NOEXEC;
369 }
370 if spec.readonly {
371 flags |= MsFlags::MS_RDONLY;
372 }
373
374 mount::mount(
375 Some(spec.tag.as_str()),
376 path,
377 Some("virtiofs"),
378 flags,
379 None::<&str>,
380 )
381 .map_err(|e| {
382 AgentdError::Init(format!(
383 "failed to mount virtiofs tag '{}' at {path}: {e}",
384 spec.tag
385 ))
386 })?;
387
388 Ok(())
389 }
390
391 pub fn apply_file_mounts(specs: &[FileMountSpec]) -> AgentdResult<()> {
393 if specs.is_empty() {
394 return Ok(());
395 }
396
397 fs::create_dir_all(microsandbox_protocol::FILE_MOUNTS_DIR).map_err(|e| {
399 AgentdError::Init(format!(
400 "failed to create file mounts dir {}: {e}",
401 microsandbox_protocol::FILE_MOUNTS_DIR
402 ))
403 })?;
404
405 for spec in specs {
406 mount_file(spec)?;
407 }
408
409 let _ = fs::remove_dir(microsandbox_protocol::FILE_MOUNTS_DIR);
412
413 Ok(())
414 }
415
416 fn mount_file(spec: &FileMountSpec) -> AgentdResult<()> {
418 let staging_path = format!("{}/{}", microsandbox_protocol::FILE_MOUNTS_DIR, spec.tag);
419
420 fs::create_dir_all(&staging_path).map_err(|e| {
422 AgentdError::Init(format!("failed to create staging dir {staging_path}: {e}"))
423 })?;
424
425 let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME;
427 if spec.noexec {
428 flags |= MsFlags::MS_NOEXEC;
429 }
430 if spec.readonly {
431 flags |= MsFlags::MS_RDONLY;
432 }
433
434 mount::mount(
435 Some(spec.tag.as_str()),
436 staging_path.as_str(),
437 Some("virtiofs"),
438 flags,
439 None::<&str>,
440 )
441 .map_err(|e| {
442 AgentdError::Init(format!(
443 "failed to mount virtiofs tag '{}' at {staging_path}: {e}",
444 spec.tag
445 ))
446 })?;
447
448 let bind_result = (|| {
449 let guest = Path::new(&spec.guest_path);
451 if let Some(parent) = guest.parent() {
452 fs::create_dir_all(parent).map_err(|e| {
453 AgentdError::Init(format!(
454 "failed to create parent dirs for {}: {e}",
455 spec.guest_path
456 ))
457 })?;
458 }
459
460 fs::OpenOptions::new()
462 .create(true)
463 .truncate(false)
464 .write(true)
465 .open(&spec.guest_path)
466 .map_err(|e| {
467 AgentdError::Init(format!(
468 "failed to create bind target {}: {e}",
469 spec.guest_path
470 ))
471 })?;
472
473 let source_path = format!("{staging_path}/{}", spec.filename);
475 mount::mount(
476 Some(source_path.as_str()),
477 spec.guest_path.as_str(),
478 None::<&str>,
479 MsFlags::MS_BIND,
480 None::<&str>,
481 )
482 .map_err(|e| {
483 AgentdError::Init(format!(
484 "failed to bind mount {source_path} to {}: {e}",
485 spec.guest_path
486 ))
487 })?;
488
489 let mut remount_flags =
491 MsFlags::MS_BIND | MsFlags::MS_REMOUNT | MsFlags::MS_NOSUID | MsFlags::MS_NODEV;
492 if spec.noexec {
493 remount_flags |= MsFlags::MS_NOEXEC;
494 }
495 if spec.readonly {
496 remount_flags |= MsFlags::MS_RDONLY;
497 }
498 mount::mount(
499 None::<&str>,
500 spec.guest_path.as_str(),
501 None::<&str>,
502 remount_flags,
503 None::<&str>,
504 )
505 .map_err(|e| {
506 AgentdError::Init(format!(
507 "failed to remount {} with volume flags: {e}",
508 spec.guest_path
509 ))
510 })?;
511
512 Ok(())
513 })();
514
515 let cleanup_result = cleanup_file_mount_staging(&staging_path);
516 match (bind_result, cleanup_result) {
517 (Ok(()), Ok(())) => Ok(()),
518 (Err(err), Ok(())) => Err(err),
519 (Ok(()), Err(err)) => Err(err),
520 (Err(err), Err(cleanup_err)) => Err(AgentdError::Init(format!(
521 "{err}; additionally failed to cleanup file mount staging {staging_path}: {cleanup_err}"
522 ))),
523 }
524 }
525
526 fn cleanup_file_mount_staging(staging_path: &str) -> AgentdResult<()> {
527 mount::umount2(staging_path, MntFlags::MNT_DETACH).map_err(|e| {
530 AgentdError::Init(format!(
531 "failed to unmount file mount staging {staging_path}: {e}"
532 ))
533 })?;
534 fs::remove_dir(staging_path).map_err(|e| {
535 AgentdError::Init(format!(
536 "failed to remove file mount staging {staging_path}: {e}"
537 ))
538 })?;
539 Ok(())
540 }
541
542 pub fn apply_disk_mounts(specs: &[DiskMountSpec]) -> AgentdResult<()> {
544 if specs.is_empty() {
545 return Ok(());
546 }
547 let fstypes = read_proc_filesystems()?;
550 for spec in specs {
551 mount_disk(spec, &fstypes)?;
552 }
553 Ok(())
554 }
555
556 fn resolve_disk_device(id: &str) -> AgentdResult<String> {
564 use std::{thread::sleep, time::Duration};
565 const RETRIES: u32 = 20;
566 const INTERVAL: Duration = Duration::from_millis(10);
567
568 let by_id = format!("/dev/disk/by-id/virtio-{id}");
569 for attempt in 0..RETRIES {
570 if Path::new(&by_id).exists() {
571 return Ok(by_id);
572 }
573 if let Some(dev) = scan_block_serial(id) {
574 return Ok(dev);
575 }
576 if attempt + 1 < RETRIES {
579 sleep(INTERVAL);
580 }
581 }
582 Err(AgentdError::Init(format!(
583 "disk mount: no block device found for id '{id}' \
584 (checked /dev/disk/by-id/virtio-{id} and /sys/block/*/serial)"
585 )))
586 }
587
588 fn scan_block_serial(id: &str) -> Option<String> {
590 let entries = fs::read_dir("/sys/block").ok()?;
591 for entry in entries.flatten() {
592 let name = entry.file_name();
593 let Some(name_str) = name.to_str() else {
594 continue;
595 };
596 if !name_str.starts_with("vd") {
597 continue;
598 }
599 let serial_path = entry.path().join("serial");
600 let Ok(serial) = fs::read_to_string(&serial_path) else {
601 continue;
602 };
603 if serial.trim() == id {
604 return Some(format!("/dev/{name_str}"));
605 }
606 }
607 None
608 }
609
610 fn mount_disk(spec: &DiskMountSpec, fstypes: &[String]) -> AgentdResult<()> {
611 let path = spec.guest_path.as_str();
612 fs::create_dir_all(path)
613 .map_err(|e| AgentdError::Init(format!("disk mount: create dir {path}: {e}")))?;
614
615 let device = resolve_disk_device(&spec.id)?;
616
617 let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME;
618 if spec.noexec {
619 flags |= MsFlags::MS_NOEXEC;
620 }
621 if spec.readonly {
622 flags |= MsFlags::MS_RDONLY;
623 }
624
625 if let Some(fstype) = spec.fstype.as_deref() {
626 mount::mount(
627 Some(device.as_str()),
628 path,
629 Some(fstype),
630 flags,
631 None::<&str>,
632 )
633 .map_err(|e| {
634 AgentdError::Init(format!(
635 "disk mount: failed to mount {device} at {path} as {fstype}: {e}"
636 ))
637 })?;
638 } else {
639 try_mount_any(&device, path, flags, fstypes)?;
640 }
641
642 Ok(())
643 }
644
645 pub fn apply_tmpfs_mounts(specs: &[TmpfsSpec]) -> AgentdResult<()> {
647 for spec in specs {
648 mount_tmpfs(spec)?;
649 }
650 Ok(())
651 }
652
653 pub fn ensure_standard_tmp_permissions() -> AgentdResult<()> {
655 ensure_directory_mode("/tmp", 0o1777)?;
656 ensure_directory_mode("/var/tmp", 0o1777)?;
657 Ok(())
658 }
659
660 fn mount_tmpfs(spec: &TmpfsSpec) -> AgentdResult<()> {
662 let path = spec.path.as_str();
663
664 let mode = spec
666 .mode
667 .unwrap_or(if path == "/tmp" || path == "/var/tmp" {
668 0o1777
669 } else {
670 0o755
671 });
672
673 fs::create_dir_all(path)
675 .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
676
677 let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME;
679 if spec.noexec {
680 flags |= MsFlags::MS_NOEXEC;
681 }
682 if spec.readonly {
683 flags |= MsFlags::MS_RDONLY;
684 }
685
686 let mut data = String::new();
688 if let Some(mib) = spec.size_mib {
689 data.push_str(&format!("size={}", u64::from(mib) * 1024 * 1024));
690 }
691 if !data.is_empty() {
692 data.push(',');
693 }
694 data.push_str(&format!("mode={mode:o}"));
695
696 mount::mount(
697 Some("tmpfs"),
698 path,
699 Some("tmpfs"),
700 flags,
701 Some(data.as_str()),
702 )
703 .map_err(|e| AgentdError::Init(format!("failed to mount tmpfs at {path}: {e}")))?;
704
705 Ok(())
706 }
707
708 pub fn create_run_dir() -> AgentdResult<()> {
715 mkdir_ignore_exists("/run")?;
716 mkdir_ignore_exists("/run/microsandbox")?;
717 Ok(())
718 }
719
720 pub fn ensure_scripts_path_in_profile() -> AgentdResult<()> {
722 let profile_path = Path::new("/etc/profile");
723 let existing = match fs::read_to_string(profile_path) {
724 Ok(contents) => contents,
725 Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
726 Err(err) => {
727 return Err(AgentdError::Init(format!(
728 "failed to read {}: {err}",
729 profile_path.display()
730 )));
731 }
732 };
733
734 let updated = super::ensure_scripts_profile_block(&existing);
735 if updated != existing {
736 if let Some(parent) = profile_path.parent() {
737 fs::create_dir_all(parent).map_err(|err| {
738 AgentdError::Init(format!("failed to create {}: {err}", parent.display()))
739 })?;
740 }
741 fs::write(profile_path, updated).map_err(|err| {
742 AgentdError::Init(format!("failed to write {}: {err}", profile_path.display()))
743 })?;
744 }
745
746 Ok(())
747 }
748
749 fn mkdir_ignore_exists(path: &str) -> AgentdResult<()> {
751 match unistd::mkdir(path, Mode::from_bits_truncate(0o755)) {
752 Ok(()) => Ok(()),
753 Err(nix::Error::EEXIST) => Ok(()),
754 Err(e) => Err(e.into()),
755 }
756 }
757
758 fn ensure_directory_mode(path: &str, mode: u32) -> AgentdResult<()> {
759 fs::create_dir_all(path)
760 .map_err(|e| AgentdError::Init(format!("failed to create directory {path}: {e}")))?;
761
762 let metadata = fs::metadata(path)
763 .map_err(|e| AgentdError::Init(format!("failed to stat {path}: {e}")))?;
764 if !metadata.is_dir() {
765 return Err(AgentdError::Init(format!(
766 "expected directory at {path}, found non-directory"
767 )));
768 }
769
770 let current_mode = metadata.permissions().mode() & 0o7777;
771 if current_mode != mode {
772 fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|e| {
773 AgentdError::Init(format!("failed to chmod {path} to {mode:o}: {e}"))
774 })?;
775 }
776
777 Ok(())
778 }
779
780 fn mount_ignore_busy(
782 source: Option<&str>,
783 target: &str,
784 fstype: Option<&str>,
785 flags: MsFlags,
786 data: Option<&str>,
787 ) -> AgentdResult<()> {
788 match mount::mount(source, target, fstype, flags, data) {
789 Ok(()) => Ok(()),
790 Err(nix::Error::EBUSY) => Ok(()),
791 Err(e) => Err(AgentdError::Init(format!("failed to mount {target}: {e}"))),
792 }
793 }
794}
795
796#[cfg(test)]
801mod tests {
802 use super::*;
803
804 #[test]
805 fn test_ensure_scripts_profile_block_appends_block() {
806 let updated = ensure_scripts_profile_block("export PATH=/usr/bin:/bin\n");
807 assert!(updated.contains("# >>> microsandbox scripts path >>>"));
808 assert!(updated.contains("export PATH=\"/.msb/scripts:$PATH\""));
809 }
810
811 #[test]
812 fn test_ensure_scripts_profile_block_adds_newline_when_missing() {
813 let updated = ensure_scripts_profile_block("export PATH=/usr/bin:/bin");
814 assert!(updated.contains("/usr/bin:/bin\n# >>> microsandbox scripts path >>>"));
815 }
816
817 #[test]
818 fn test_ensure_scripts_profile_block_is_idempotent() {
819 let profile = ensure_scripts_profile_block("");
820 let updated = ensure_scripts_profile_block(&profile);
821 assert_eq!(profile, updated);
822 }
823}