1use std::collections::BTreeMap;
42use std::collections::BTreeSet;
43
44use serde::Deserialize;
45use serde::Serialize;
46
47use crate::config::MountInfoRootRewrite;
48
49pub const BASE_MOUNTPOINTS: [&str; 9] = [
54 "/",
55 "/proc",
56 "/dev",
57 "/dev/pts",
58 "/dev/shm",
59 "/dev/mqueue",
60 "/sys",
61 "/sys/fs/cgroup",
62 "/run",
63];
64
65#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73pub struct ContainerMount {
74 pub raw_mount_id: u64,
76 pub raw_parent_id: u64,
78 pub major: u32,
80 pub minor: u32,
82 pub root: Vec<u8>,
84 pub mountpoint: Vec<u8>,
86 pub mount_options: Vec<u8>,
88 pub optional_fields: Vec<Vec<u8>>,
91 pub fstype: Vec<u8>,
93 pub source: Vec<u8>,
95 pub super_options: Vec<u8>,
97}
98
99const SUPERBLOCK_FLAG_OPTIONS: [&[u8]; 4] = [b"sync", b"dirsync", b"mand", b"lazytime"];
103
104impl ContainerMount {
105 fn parse(line: &[u8]) -> Option<Self> {
108 let fields = line.split(|byte| *byte == b' ').collect::<Vec<_>>();
109 let separator = fields.iter().position(|field| *field == b"-")?;
110 if separator < 6 || separator + 4 != fields.len() {
111 return None;
112 }
113 let decimal = |field: &[u8]| std::str::from_utf8(field).ok()?.parse::<u64>().ok();
114 let (major, minor) = {
115 let text = std::str::from_utf8(fields[2]).ok()?;
116 let (major, minor) = text.split_once(':')?;
117 (major.parse().ok()?, minor.parse().ok()?)
118 };
119 Some(Self {
120 raw_mount_id: decimal(fields[0])?,
121 raw_parent_id: decimal(fields[1])?,
122 major,
123 minor,
124 root: fields[3].to_vec(),
125 mountpoint: fields[4].to_vec(),
126 mount_options: fields[5].to_vec(),
127 optional_fields: fields[6..separator].iter().map(|f| f.to_vec()).collect(),
128 fstype: fields[separator + 1].to_vec(),
129 source: fields[separator + 2].to_vec(),
130 super_options: fields[separator + 3].to_vec(),
131 })
132 }
133
134 pub fn is_ephemeral_host_seed(&self) -> bool {
138 crate::procfs::is_ephemeral_host_seed_fields(&self.mountpoint, &self.fstype)
141 }
142
143 fn write_mountinfo(&self, out: &mut Vec<u8>) {
145 out.extend_from_slice(
146 format!(
147 "{} {} {}:{} ",
148 self.raw_mount_id, self.raw_parent_id, self.major, self.minor
149 )
150 .as_bytes(),
151 );
152 for field in [&self.root, &self.mountpoint, &self.mount_options] {
153 out.extend_from_slice(field);
154 out.push(b' ');
155 }
156 for field in &self.optional_fields {
157 out.extend_from_slice(field);
158 out.push(b' ');
159 }
160 out.extend_from_slice(b"- ");
161 out.extend_from_slice(&self.fstype);
162 out.push(b' ');
163 out.extend_from_slice(&self.source);
164 out.push(b' ');
165 out.extend_from_slice(&self.super_options);
166 out.push(b'\n');
167 }
168
169 fn mounts_options(&self) -> Vec<u8> {
182 let mut mount = self.mount_options.split(|byte| *byte == b',');
183 let mut superblock = self.super_options.split(|byte| *byte == b',');
184 let mount_read_only = mount.next() == Some(b"ro".as_slice());
185 let superblock_read_only = superblock.next() == Some(b"ro".as_slice());
186 let superblock = superblock.collect::<Vec<_>>();
187 let flags = superblock
188 .iter()
189 .take_while(|option| SUPERBLOCK_FLAG_OPTIONS.contains(option))
190 .count();
191 let mut options: Vec<&[u8]> = vec![if mount_read_only || superblock_read_only {
192 b"ro"
193 } else {
194 b"rw"
195 }];
196 options.extend(&superblock[..flags]);
197 options.extend(mount.filter(|option| !option.is_empty()));
198 options.extend(
199 superblock[flags..]
200 .iter()
201 .filter(|option| !option.is_empty()),
202 );
203 options.join(&b","[..])
204 }
205}
206
207pub fn parse_container_mounts(contents: &[u8]) -> Option<Vec<ContainerMount>> {
211 crate::procfs::parse_mountinfo(contents)?;
212 if contents.is_empty() {
213 return Some(Vec::new());
214 }
215 contents
216 .strip_suffix(b"\n")
217 .unwrap_or(contents)
218 .split(|byte| *byte == b'\n')
219 .map(ContainerMount::parse)
220 .collect()
221}
222
223#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
225pub struct ContainerMountTable {
226 pub mounts: Vec<ContainerMount>,
228 pub hidden_ancestors: Vec<(u64, u64)>,
232}
233
234impl std::fmt::Debug for ContainerMountTable {
242 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 formatter
244 .debug_struct("ContainerMountTable")
245 .field("rows", &self.mounts.len())
246 .field("hidden", &self.hidden_ancestors.len())
247 .finish()
248 }
249}
250
251impl ContainerMountTable {
252 pub fn select(
289 namespace: &[ContainerMount],
290 installed: &BTreeSet<u64>,
291 base: &[(Vec<u8>, u64)],
292 never: &BTreeSet<u64>,
293 ) -> Self {
294 let by_id = namespace
295 .iter()
296 .map(|row| (row.raw_mount_id, row))
297 .collect::<BTreeMap<_, _>>();
298 let mut shown = installed
299 .iter()
300 .copied()
301 .filter(|id| by_id.contains_key(id))
302 .collect::<BTreeSet<_>>();
303 for (mountpoint, id) in base {
304 if !never.contains(id)
305 && by_id
306 .get(id)
307 .is_some_and(|row| row.mountpoint == *mountpoint)
308 {
309 shown.insert(*id);
310 }
311 }
312 for id in installed {
313 let mut visited = BTreeSet::new();
314 let mut current = by_id.get(id).copied();
315 while let Some(row) = current {
316 if !visited.insert(row.raw_mount_id) {
317 break;
318 }
319 current = by_id.get(&row.raw_parent_id).copied();
320 if let Some(parent) = current
321 && !never.contains(&parent.raw_mount_id)
322 {
323 shown.insert(parent.raw_mount_id);
324 }
325 }
326 }
327 let nearest_shown = |row: &ContainerMount| -> Result<u64, u64> {
331 let mut visited = BTreeSet::from([row.raw_mount_id]);
332 let mut parent = row.raw_parent_id;
333 loop {
334 if shown.contains(&parent) {
335 return Ok(parent);
336 }
337 match by_id.get(&parent) {
338 Some(above) if visited.insert(parent) => parent = above.raw_parent_id,
339 _ => return Err(parent),
340 }
341 }
342 };
343 let mut mounts = Vec::new();
344 let mut hidden_ancestors = Vec::new();
345 for row in namespace {
346 if shown.contains(&row.raw_mount_id) {
347 let mut row = row.clone();
348 row.raw_parent_id = match nearest_shown(&row) {
349 Ok(parent) => parent,
350 Err(_) => row.raw_parent_id,
353 };
354 mounts.push(row);
355 } else if let Ok(ancestor) = nearest_shown(row) {
356 hidden_ancestors.push((row.raw_mount_id, ancestor));
357 }
358 }
359 Self {
360 mounts,
361 hidden_ancestors,
362 }
363 }
364
365 pub fn shown_mount_id(&self, raw_mount_id: u64) -> u64 {
370 self.hidden_ancestors
371 .iter()
372 .find(|(hidden, _)| *hidden == raw_mount_id)
373 .map_or(raw_mount_id, |(_, ancestor)| *ancestor)
374 }
375
376 pub fn shown_mount_ids(&self) -> BTreeSet<u64> {
378 self.mounts.iter().map(|row| row.raw_mount_id).collect()
379 }
380
381 pub fn mountinfo(&self) -> Vec<u8> {
385 let mut out = Vec::new();
386 for row in &self.mounts {
387 row.write_mountinfo(&mut out);
388 }
389 out
390 }
391
392 pub fn mounts(&self, rewrites: &[MountInfoRootRewrite]) -> Vec<u8> {
397 let mut prefixes = rewrites
398 .iter()
399 .filter_map(|rewrite| {
400 Some((
401 rewrite.raw_mountpoint_prefix.as_deref()?,
402 rewrite.deterministic_mountpoint_prefix.as_deref()?,
403 ))
404 })
405 .collect::<Vec<_>>();
406 prefixes.sort_by_key(|(raw, _)| std::cmp::Reverse(raw.len()));
407 let mut out = Vec::new();
408 for row in &self.mounts {
409 out.extend_from_slice(&row.source);
410 out.push(b' ');
411 out.extend_from_slice(&rewrite_prefix(&row.mountpoint, &prefixes));
412 out.push(b' ');
413 out.extend_from_slice(&row.fstype);
414 out.push(b' ');
415 out.extend_from_slice(&row.mounts_options());
416 out.extend_from_slice(b" 0 0\n");
417 }
418 out
419 }
420}
421
422#[derive(Clone, Debug, Default, Eq, PartialEq)]
425pub struct Installation {
426 pub ids: Vec<u64>,
430 pub mountpoints: Vec<Vec<u8>>,
436 pub sources: Vec<Vec<u8>>,
439 pub backend: bool,
444}
445
446pub fn owned_mounts(
455 namespace: &[ContainerMount],
456 installations: &[Installation],
457) -> (BTreeSet<u64>, BTreeSet<u64>) {
458 let by_id = namespace
459 .iter()
460 .map(|row| (row.raw_mount_id, row))
461 .collect::<BTreeMap<_, _>>();
462 let mut hermit = BTreeSet::new();
463 let mut backend = BTreeSet::new();
464 for installation in installations {
465 let created = installation.ids.iter().copied().collect::<BTreeSet<_>>();
466 for id in &installation.ids {
467 let Some(row) = by_id.get(id) else {
468 continue;
469 };
470 let owner_is_backend = if installation.is_at(row) {
471 installation.backend
472 } else if installation.below(row).is_some() {
473 let original = installation
476 .original_of(namespace, row, |candidate| !created.contains(&candidate));
477 installation.backend
478 || original.is_some_and(|original| backend.contains(&original.raw_mount_id))
479 } else {
480 continue;
481 };
482 if owner_is_backend {
483 backend.insert(*id);
484 } else {
485 hermit.insert(*id);
486 }
487 }
488 }
489 (hermit, backend)
490}
491
492impl Installation {
493 fn is_at(&self, row: &ContainerMount) -> bool {
495 self.mountpoints.contains(&row.mountpoint)
496 }
497
498 fn below(&self, row: &ContainerMount) -> Option<Vec<u8>> {
501 self.mountpoints.iter().find_map(|mountpoint| {
502 if mountpoint.as_slice() == b"/" {
503 return (row.mountpoint.as_slice() != b"/").then(|| row.mountpoint.clone());
504 }
505 let rest = row.mountpoint.strip_prefix(mountpoint.as_slice())?;
506 (rest.first() == Some(&b'/')).then(|| rest.to_vec())
507 })
508 }
509
510 fn original_of<'a>(
515 &self,
516 namespace: &'a [ContainerMount],
517 row: &ContainerMount,
518 eligible: impl Fn(u64) -> bool,
519 ) -> Option<&'a ContainerMount> {
520 let rest = self.below(row)?;
521 self.sources.iter().find_map(|source| {
522 let path = if source.as_slice() == b"/" {
523 rest.clone()
524 } else {
525 let mut path = source.clone();
526 path.extend_from_slice(&rest);
527 path
528 };
529 namespace.iter().find(|candidate| {
530 candidate.raw_mount_id != row.raw_mount_id
531 && eligible(candidate.raw_mount_id)
532 && candidate.mountpoint == path
533 && (candidate.major, candidate.minor, &candidate.root)
534 == (row.major, row.minor, &row.root)
535 })
536 })
537 }
538}
539
540pub fn assign_installed_rows(
561 namespace: &[ContainerMount],
562 installations: &mut [Installation],
563) -> bool {
564 if installations.is_empty() {
565 return true;
566 }
567 let mut open = installations.len();
569 for row in namespace.iter().rev() {
570 let first_row = (0..open)
571 .rev()
572 .find(|&index| installations[index].is_at(row));
573 let copy = (0..open).rev().find(|&index| {
574 installations[index]
575 .original_of(namespace, row, |_| true)
576 .is_some()
577 });
578 let closes = match (first_row, copy) {
580 (Some(index), Some(copy)) => (index > copy).then_some(index),
581 (first_row, None) => first_row,
582 (None, Some(_)) => None,
583 };
584 if let Some(index) = closes {
585 installations[index].ids.push(row.raw_mount_id);
586 if index == 0 {
587 return true;
588 }
589 open = index;
590 } else if let Some(index) = copy {
591 installations[index].ids.push(row.raw_mount_id);
592 }
593 }
594 false
595}
596
597pub fn check_installed_rows(
605 namespace: &[ContainerMount],
606 installations: &[Installation],
607 is_plans_first_row: impl Fn(&ContainerMount) -> bool,
608) -> Result<(), String> {
609 let row_at_target = |installation: &Installation| {
610 installation.ids.iter().find_map(|id| {
611 namespace
612 .iter()
613 .find(|row| row.raw_mount_id == *id && installation.is_at(row))
614 })
615 };
616 for (index, installation) in installations.iter().enumerate() {
617 if installation.mountpoints.is_empty() {
618 continue;
619 }
620 let Some(row) = row_at_target(installation) else {
621 return Err(format!(
622 "the plan's mount {index}, at {}, has no row at its target",
623 String::from_utf8_lossy(&installation.mountpoints[0])
624 ));
625 };
626 if index == 0 && !is_plans_first_row(row) {
627 return Err(format!(
628 "the row taken for the plan's first mount, {} at {}, is not the container's own",
629 row.raw_mount_id,
630 String::from_utf8_lossy(&row.mountpoint)
631 ));
632 }
633 }
634 Ok(())
635}
636
637fn rewrite_prefix(field: &[u8], prefixes: &[(&[u8], &[u8])]) -> Vec<u8> {
641 for (raw, deterministic) in prefixes {
642 let Some(suffix) = field.strip_prefix(*raw) else {
643 continue;
644 };
645 if suffix.is_empty() || suffix.first() == Some(&b'/') {
646 let mut rewritten = deterministic.to_vec();
647 rewritten.extend_from_slice(suffix);
648 return rewritten;
649 }
650 }
651 field.to_vec()
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 const HOST: &[u8] = b"\
66320 1 259:4 / / rw,relatime shared:1 - btrfs /dev/nvme0n1p4 rw,ssd,subvol=/\n\
66421 20 0:22 / /proc rw,nosuid,nodev,noexec,relatime shared:2 - proc proc rw\n\
66522 20 0:5 / /dev rw,nosuid master:3 - devtmpfs devtmpfs rw,size=4096k,mode=755\n\
66623 22 0:24 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000\n\
66724 20 0:25 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw\n\
66825 20 259:5 / /data rw,relatime - ext4 /dev/nvme1n1 rw\n\
66926 25 0:40 / /data/a rw,nosuid,nodev,relatime - fuse.edenfs edenfs rw,user_id=0\n\
67027 20 0:26 / /tmp rw,nosuid,nodev - tmpfs tmpfs rw,size=1024k\n\
67128 20 259:4 /tmp/tmpAbC/group /etc/group ro,relatime - btrfs /dev/nvme0n1p4 rw,ssd,subvol=/\n\
67229 21 0:41 / /proc rw,relatime - proc proc rw\n\
67330 27 0:26 /tmpAbC /tmp rw,nosuid,nodev - tmpfs tmpfs rw,size=1024k\n\
67431 25 0:42 / /data/work rw,relatime - tmpfs none rw\n";
675
676 fn base(top_at: &[(&str, u64)]) -> Vec<(Vec<u8>, u64)> {
679 top_at
680 .iter()
681 .map(|(path, id)| (path.as_bytes().to_vec(), *id))
682 .collect()
683 }
684
685 fn host_table(contents: &[u8]) -> ContainerMountTable {
686 let rows = parse_container_mounts(contents).expect("the host table parses");
687 let installed = BTreeSet::from([28, 29, 30, 31]);
692 let base = base(&[
693 ("/", 20),
694 ("/proc", 29),
695 ("/dev", 22),
696 ("/dev/pts", 23),
697 ("/dev/shm", 22),
698 ("/dev/mqueue", 22),
699 ("/sys", 24),
700 ("/sys/fs/cgroup", 24),
701 ("/run", 20),
702 ]);
703 ContainerMountTable::select(&rows, &installed, &base, &BTreeSet::new())
704 }
705
706 #[test]
707 fn the_table_shows_hermits_mounts_the_base_set_and_their_ancestors_only() {
708 let table = host_table(HOST);
709 let shown = table
710 .mounts
711 .iter()
712 .map(|row| row.raw_mount_id)
713 .collect::<Vec<_>>();
714 assert_eq!(shown, [20, 21, 22, 23, 24, 25, 27, 28, 29, 30, 31]);
719 let named = parse_container_mounts(HOST).unwrap();
721 for row in &table.mounts {
722 let kernel = named
723 .iter()
724 .find(|kernel| kernel.raw_mount_id == row.raw_mount_id)
725 .unwrap();
726 assert_eq!(row, kernel);
727 }
728 assert_eq!(table.hidden_ancestors, [(26, 25)]);
729 }
730
731 #[test]
732 fn a_shown_row_hangs_from_its_nearest_shown_ancestor() {
733 let rows = parse_container_mounts(HOST).unwrap();
734 let table = ContainerMountTable::select(
737 &rows,
738 &BTreeSet::new(),
739 &base(&[("/", 20), ("/dev/pts", 23)]),
740 &BTreeSet::new(),
741 );
742 let shown = table
743 .mounts
744 .iter()
745 .map(|row| (row.raw_mount_id, row.raw_parent_id))
746 .collect::<Vec<_>>();
747 assert_eq!(shown, [(20, 1), (23, 20)]);
748 assert_eq!(table.shown_mount_id(22), 20);
749 assert_eq!(table.shown_mount_id(31), 20);
750 }
751
752 #[test]
753 fn a_hidden_mount_reports_its_nearest_shown_ancestor() {
754 let table = host_table(HOST);
755 assert_eq!(
756 table.shown_mount_id(26),
757 25,
758 "the FUSE checkout under /data"
759 );
760 assert_eq!(
761 table.shown_mount_id(21),
762 21,
763 "the covered host /proc is shown"
764 );
765 assert_eq!(table.shown_mount_id(30), 30, "a shown mount is itself");
766 assert_eq!(table.shown_mount_id(7), 7, "pipefs is no row");
767 }
768
769 #[test]
770 fn the_rendering_is_a_pure_function_of_the_table() {
771 let table = host_table(HOST);
772 assert_eq!(table.mountinfo(), host_table(HOST).mountinfo());
773 assert_eq!(
774 table.mountinfo(),
775 b"\
77620 1 259:4 / / rw,relatime shared:1 - btrfs /dev/nvme0n1p4 rw,ssd,subvol=/\n\
77721 20 0:22 / /proc rw,nosuid,nodev,noexec,relatime shared:2 - proc proc rw\n\
77822 20 0:5 / /dev rw,nosuid master:3 - devtmpfs devtmpfs rw,size=4096k,mode=755\n\
77923 22 0:24 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000\n\
78024 20 0:25 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs rw\n\
78125 20 259:5 / /data rw,relatime - ext4 /dev/nvme1n1 rw\n\
78227 20 0:26 / /tmp rw,nosuid,nodev - tmpfs tmpfs rw,size=1024k\n\
78328 20 259:4 /tmp/tmpAbC/group /etc/group ro,relatime - btrfs /dev/nvme0n1p4 rw,ssd,subvol=/\n\
78429 21 0:41 / /proc rw,relatime - proc proc rw\n\
78530 27 0:26 /tmpAbC /tmp rw,nosuid,nodev - tmpfs tmpfs rw,size=1024k\n\
78631 25 0:42 / /data/work rw,relatime - tmpfs none rw\n"
787 .as_slice()
788 );
789 assert_eq!(
791 parse_container_mounts(&table.mountinfo()).unwrap(),
792 table.mounts
793 );
794 let restored: ContainerMountTable =
796 serde_json::from_str(&serde_json::to_string(&table).unwrap()).unwrap();
797 assert_eq!(restored.mountinfo(), table.mountinfo());
798 }
799
800 #[test]
806 fn a_host_mount_outside_the_rule_changes_no_byte() {
807 let mut churned = Vec::new();
808 for line in HOST.split_inclusive(|byte| *byte == b'\n') {
809 churned.extend_from_slice(line);
810 if line.starts_with(b"25 ") {
811 churned.extend_from_slice(
812 b"95 25 0:93 / /data/b rw,nosuid,nodev,relatime - \
813 fuse.edenfs edenfs rw,user_id=1\n",
814 );
815 churned.extend_from_slice(
816 b"96 20 0:94 / /run/user/1000 rw,nosuid,nodev,relatime - tmpfs tmpfs rw\n",
817 );
818 }
819 }
820 let quiet = host_table(HOST);
821 let churned = host_table(&churned);
822 assert_eq!(churned.mountinfo(), quiet.mountinfo());
823 assert_eq!(churned.mounts(&[]), quiet.mounts(&[]));
824 assert_eq!(churned.shown_mount_id(95), 25);
825 assert_eq!(churned.shown_mount_id(96), 20);
826 }
827
828 #[test]
829 fn a_base_mountpoint_counts_only_where_its_descriptor_reaches_a_mount_there() {
830 let rows = parse_container_mounts(HOST).unwrap();
831 let table = ContainerMountTable::select(
834 &rows,
835 &BTreeSet::new(),
836 &base(&[("/sys/fs/cgroup", 24), ("/data", 26)]),
837 &BTreeSet::new(),
838 );
839 assert!(table.mounts.is_empty(), "{:?}", table.mounts);
840 assert!(table.hidden_ancestors.is_empty());
843 }
844
845 fn installation(
848 ids: &[u64],
849 mountpoints: &[&str],
850 sources: &[&str],
851 backend: bool,
852 ) -> Installation {
853 Installation {
854 ids: ids.to_vec(),
855 mountpoints: mountpoints.iter().map(|m| m.as_bytes().to_vec()).collect(),
856 sources: sources.iter().map(|m| m.as_bytes().to_vec()).collect(),
857 backend,
858 }
859 }
860
861 fn shown(table: &ContainerMountTable) -> Vec<u64> {
862 table.mounts.iter().map(|row| row.raw_mount_id).collect()
863 }
864
865 #[test]
872 fn a_later_bind_redirecting_a_target_admits_no_host_row() {
873 let rows = parse_container_mounts(
874 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
875 50 20 8:2 / /data rw - ext4 /dev/data rw\n\
876 51 50 0:40 / /data/foo rw - fuse.edenfs edenfs rw\n\
877 60 20 8:1 /srv/a /opt/x rw - ext4 /dev/root rw\n\
878 61 20 8:1 /srv/b /opt rw - ext4 /dev/root rw\n",
879 )
880 .unwrap();
881 let installations = [
882 installation(&[60], &["/opt/x"], &["/srv/a"], false),
883 installation(&[61], &["/opt"], &["/srv/b"], false),
884 installation(&[51], &["/opt/x"], &[], false),
887 ];
888 let (hermit, backend) = owned_mounts(&rows, &installations);
889 assert_eq!(hermit, BTreeSet::from([60, 61]));
890 assert!(backend.is_empty());
891 let table = ContainerMountTable::select(&rows, &hermit, &[], &BTreeSet::new());
892 assert_eq!(shown(&table), [20, 60, 61]);
893 assert_eq!(table.shown_mount_id(50), 20);
894 assert_eq!(table.shown_mount_id(51), 20);
895 }
896
897 #[test]
900 fn a_recursive_bind_owns_the_copies_it_makes() {
901 let rows = parse_container_mounts(
902 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
903 40 20 8:3 / /src rw - ext4 /dev/src rw\n\
904 41 40 0:41 / /src/sub rw - tmpfs tmpfs rw\n\
905 70 20 8:3 / /dst rw - ext4 /dev/src rw\n\
906 71 70 0:41 / /dst/sub rw - tmpfs tmpfs rw\n",
907 )
908 .unwrap();
909 let (hermit, backend) = owned_mounts(
910 &rows,
911 &[installation(&[70, 71], &["/dst"], &["/src"], false)],
912 );
913 assert_eq!(hermit, BTreeSet::from([70, 71]));
914 assert!(backend.is_empty());
915 let table = ContainerMountTable::select(&rows, &hermit, &[], &BTreeSet::new());
916 assert_eq!(shown(&table), [20, 70, 71]);
917 assert_eq!(
918 table.shown_mount_id(41),
919 20,
920 "the host's /src/sub stays hidden"
921 );
922 }
923
924 fn assigned(rows: &[ContainerMount], plan: &[Installation]) -> (Vec<Vec<u64>>, bool) {
927 let mut plan = plan.to_vec();
928 let found = assign_installed_rows(rows, &mut plan);
929 (
930 plan.into_iter()
931 .map(|installation| installation.ids)
932 .collect(),
933 found,
934 )
935 }
936
937 #[test]
943 fn rows_at_one_target_go_to_their_installations_in_order() {
944 let rows = parse_container_mounts(
945 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
946 21 20 0:20 / /proc rw - proc proc rw\n\
947 50 21 0:50 / /proc rw - proc proc rw\n\
948 60 20 8:1 /p /tmp/prog rw - ext4 /dev/root rw\n\
949 61 60 8:1 /q /tmp/prog ro - ext4 /dev/root rw\n",
950 )
951 .unwrap();
952 let plan = [
953 installation(&[], &["/proc"], &[], false),
954 installation(&[], &["/tmp/prog"], &["/p"], false),
955 installation(&[], &["/tmp/prog"], &["/q"], true),
956 installation(&[], &[], &[], true),
957 ];
958 assert_eq!(
959 assigned(&rows, &plan),
960 (vec![vec![50], vec![60], vec![61], vec![]], true)
961 );
962 }
963
964 #[test]
968 fn a_recursive_binds_copies_are_assigned_to_it() {
969 let rows = parse_container_mounts(
970 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
971 49 20 0:49 / /proc rw - proc proc rw\n\
972 50 20 0:60 / /scratch/t/x rw - tmpfs tmpfs rw\n\
973 51 20 8:1 /scratch/t /tmp rw - ext4 /dev/root rw\n\
974 52 51 0:60 / /tmp/x rw - tmpfs tmpfs rw\n",
975 )
976 .unwrap();
977 let plan = [
978 installation(&[], &["/proc"], &[], false),
979 installation(&[], &["/scratch/t/x"], &[], true),
980 installation(&[], &["/tmp"], &["/scratch/t"], false),
981 ];
982 let (plan_ids, found) = assigned(&rows, &plan);
983 assert!(found);
984 assert_eq!(plan_ids, [vec![49], vec![50], vec![52, 51]]);
985 let mut plan = plan.to_vec();
986 for (installation, ids) in plan.iter_mut().zip(plan_ids) {
987 installation.ids = ids;
988 }
989 let (hermit, backend) = owned_mounts(&rows, &plan);
990 assert_eq!(hermit, BTreeSet::from([49, 51]));
991 assert_eq!(
992 backend,
993 BTreeSet::from([50, 52]),
994 "a backend's mount stays the backend's in the copy"
995 );
996 }
997
998 #[test]
1006 fn a_row_stays_where_its_install_time_target_was() {
1007 let rows = parse_container_mounts(
1008 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1009 30 20 8:2 / /data rw - ext4 /dev/data rw\n\
1010 31 30 8:2 /x /opt/x rw - ext4 /dev/data rw\n\
1011 49 20 0:49 / /proc rw - proc proc rw\n\
1012 60 20 8:1 /srv/a /opt rw - ext4 /dev/root rw\n\
1013 61 30 8:1 /item /data/foo rw - ext4 /dev/root rw\n\
1014 62 20 8:1 /stray /stray rw - ext4 /dev/root rw\n\
1015 63 60 8:1 /srv/c /opt rw - ext4 /dev/root rw\n",
1016 )
1017 .unwrap();
1018 let plan = [
1019 installation(&[], &["/proc"], &[], false),
1020 installation(&[], &["/opt"], &[], false),
1021 installation(&[], &["/data/foo"], &[], false),
1022 installation(&[], &["/opt"], &[], false),
1023 ];
1024 let (plan_ids, found) = assigned(&rows, &plan);
1025 assert!(found);
1026 assert_eq!(plan_ids, [vec![49], vec![60], vec![61], vec![63]]);
1027 let mut plan = plan.to_vec();
1028 for (installation, ids) in plan.iter_mut().zip(plan_ids) {
1029 installation.ids = ids;
1030 }
1031 assert_eq!(
1032 check_installed_rows(&rows, &plan, |row| row.raw_mount_id == 49),
1033 Ok(())
1034 );
1035 let (hermit, _) = owned_mounts(&rows, &plan);
1036 let table = ContainerMountTable::select(&rows, &hermit, &[], &BTreeSet::new());
1037 assert!(shown(&table).contains(&61), "the genuine row stays visible");
1038 assert!(!shown(&table).contains(&62) && !shown(&table).contains(&31));
1039 }
1040
1041 #[test]
1046 fn a_walk_that_takes_the_hosts_proc_or_misses_a_mount_is_refused() {
1047 let rows = parse_container_mounts(
1048 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1049 21 20 0:22 / /proc rw - proc proc rw\n\
1050 27 20 0:26 / /tmp rw - tmpfs tmpfs rw\n\
1051 70 27 8:1 /scratch/t /tmp rw - ext4 /dev/root rw\n",
1052 )
1053 .unwrap();
1054 let plan = [
1055 installation(&[], &["/proc"], &[], false),
1056 installation(&[], &["/tmp"], &[], false),
1057 ];
1058 let (plan_ids, found) = assigned(&rows, &plan);
1059 assert!(found, "the walk alone cannot tell");
1060 let mut walked = plan.to_vec();
1061 for (installation, ids) in walked.iter_mut().zip(plan_ids) {
1062 installation.ids = ids;
1063 }
1064 let ours = |row: &ContainerMount| (row.major, row.minor) == (0, 4095);
1066 assert!(check_installed_rows(&rows, &walked, ours).is_err());
1067 let mut missing = walked.clone();
1068 missing[1].ids.clear();
1069 assert!(
1070 check_installed_rows(&rows, &missing, |_| true)
1071 .unwrap_err()
1072 .contains("no row at its target")
1073 );
1074 }
1075
1076 #[test]
1079 fn a_plan_whose_first_row_is_missing_is_refused() {
1080 let rows = parse_container_mounts(
1081 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1082 60 20 8:1 /srv/b /opt rw - ext4 /dev/root rw\n",
1083 )
1084 .unwrap();
1085 let plan = [
1086 installation(&[], &["/proc"], &[], false),
1087 installation(&[], &["/opt"], &["/srv/b"], false),
1088 ];
1089 assert!(!assigned(&rows, &plan).1);
1090 }
1091
1092 #[test]
1097 fn a_user_mount_at_a_backend_target_stays_shown() {
1098 let rows = parse_container_mounts(
1099 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1100 80 20 0:50 / /test rw - tmpfs tmpfs rw\n\
1101 81 80 0:51 / /test rw - tmpfs tmpfs rw\n",
1102 )
1103 .unwrap();
1104 let (hermit, backend) = owned_mounts(
1105 &rows,
1106 &[
1107 installation(&[80], &["/test"], &[], false),
1108 installation(&[81], &["/test"], &[], true),
1109 ],
1110 );
1111 assert_eq!(hermit, BTreeSet::from([80]));
1112 assert_eq!(backend, BTreeSet::from([81]));
1113 let table = ContainerMountTable::select(&rows, &hermit, &[], &backend);
1114 assert_eq!(shown(&table), [20, 80]);
1115 assert_eq!(table.shown_mount_id(81), 80);
1116 }
1117
1118 #[test]
1121 fn a_covered_hermit_proc_stays_shown() {
1122 let rows = parse_container_mounts(
1123 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1124 21 20 0:22 / /proc rw - proc proc rw\n\
1125 90 21 0:4095 / /proc rw - proc proc rw\n\
1126 91 20 8:4 /guest-root / rw - ext4 /dev/other rw\n",
1127 )
1128 .unwrap();
1129 let plan = [
1130 installation(&[], &["/proc"], &[], false),
1131 installation(&[], &["/"], &["/guest-root"], false),
1132 ];
1133 let (plan_ids, found) = assigned(&rows, &plan);
1134 assert_eq!((plan_ids.clone(), found), (vec![vec![90], vec![91]], true));
1135 let mut plan = plan.to_vec();
1136 for (installation, ids) in plan.iter_mut().zip(plan_ids) {
1137 installation.ids = ids;
1138 }
1139 assert_eq!(
1140 check_installed_rows(&rows, &plan, |row| row.raw_mount_id == 90),
1141 Ok(())
1142 );
1143 let (hermit, _) = owned_mounts(&rows, &plan);
1144 assert_eq!(hermit, BTreeSet::from([90, 91]));
1145 let table = ContainerMountTable::select(&rows, &hermit, &[], &BTreeSet::new());
1146 assert_eq!(shown(&table), [20, 21, 90, 91]);
1147 }
1148
1149 #[test]
1153 fn a_copy_of_a_backend_mount_is_the_backends() {
1154 let rows = parse_container_mounts(
1155 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1156 27 20 0:26 / /tmp rw - tmpfs tmpfs rw\n\
1157 100 27 8:1 /stats /tmp/tmpX/.stats rw - ext4 /dev/root rw\n\
1158 101 27 0:26 /tmpX /tmp rw - tmpfs tmpfs rw\n\
1159 102 101 8:1 /stats /tmp/.stats rw - ext4 /dev/root rw\n",
1160 )
1161 .unwrap();
1162 let (hermit, backend) = owned_mounts(
1163 &rows,
1164 &[
1165 installation(&[100], &["/tmp/tmpX/.stats"], &["/stats"], true),
1166 installation(&[101, 102], &["/tmp"], &["/tmp/tmpX"], false),
1167 ],
1168 );
1169 assert_eq!(hermit, BTreeSet::from([101]));
1170 assert_eq!(backend, BTreeSet::from([100, 102]));
1171 let table = ContainerMountTable::select(&rows, &hermit, &[], &backend);
1172 assert_eq!(shown(&table), [20, 27, 101]);
1173 assert_eq!(table.shown_mount_id(102), 101);
1174 }
1175
1176 #[test]
1180 fn a_seed_is_hidden_but_kept_in_the_topology() {
1181 let rows = parse_container_mounts(
1182 b"20 1 8:1 / / rw - ext4 /dev/root rw\n\
1183 110 20 0:60 / /mnt/x/0a-seed-chef-ns-1 rw - fuse.squashfuse_ll s rw\n\
1184 111 110 0:61 / /mnt/x/0a-seed-chef-ns-1/in rw - tmpfs tmpfs rw\n\
1185 112 20 0:62 / /opt/0b-seed-plan-ns-2 rw - fuse.squashfuse_ll s rw\n",
1186 )
1187 .unwrap();
1188 let installed = BTreeSet::from([112]);
1189 let never = BTreeSet::from([110]);
1190 let table = ContainerMountTable::select(&rows, &installed, &base(&[("/", 20)]), &never);
1191 assert_eq!(shown(&table), [20, 112]);
1192 assert_eq!(table.shown_mount_id(110), 20);
1193 assert_eq!(table.shown_mount_id(111), 20);
1194 }
1195
1196 #[test]
1197 fn proc_mounts_merges_the_options_as_show_vfsmnt_does() {
1198 let table = host_table(HOST);
1199 let mounts = table.mounts(&[]);
1200 let text = String::from_utf8(mounts).unwrap();
1201 let lines = text.lines().collect::<Vec<_>>();
1202 assert_eq!(
1203 lines[0],
1204 "/dev/nvme0n1p4 / btrfs rw,relatime,ssd,subvol=/ 0 0"
1205 );
1206 assert_eq!(
1207 lines[4], "sysfs /sys sysfs ro,nosuid,nodev,noexec,relatime 0 0",
1208 "a read-only mount of a writable superblock is ro"
1209 );
1210 let row = |mount_options: &[u8], super_options: &[u8]| ContainerMount {
1211 raw_mount_id: 1,
1212 raw_parent_id: 0,
1213 major: 0,
1214 minor: 1,
1215 root: b"/".to_vec(),
1216 mountpoint: b"/m".to_vec(),
1217 mount_options: mount_options.to_vec(),
1218 optional_fields: Vec::new(),
1219 fstype: b"ext4".to_vec(),
1220 source: b"/dev/x".to_vec(),
1221 super_options: super_options.to_vec(),
1222 };
1223 assert_eq!(
1224 row(b"rw,nosuid,noatime", b"rw,sync,lazytime,errors=remount-ro").mounts_options(),
1225 b"rw,sync,lazytime,nosuid,noatime,errors=remount-ro"
1226 );
1227 assert_eq!(
1228 row(b"rw,relatime", b"ro").mounts_options(),
1229 b"ro,relatime",
1230 "a writable mount of a read-only superblock is ro"
1231 );
1232 }
1233
1234 #[test]
1235 fn proc_mounts_shows_staged_mountpoints_where_the_guest_sees_them() {
1236 let table = ContainerMountTable {
1237 mounts: parse_container_mounts(
1238 b"40 30 0:26 /tmpAbC/x /tmp/tmpAbC/x rw - tmpfs tmpfs rw\n\
1239 41 30 0:26 /tmpAbC/xy /tmp/tmpAbCdef rw - tmpfs tmpfs rw\n",
1240 )
1241 .unwrap(),
1242 hidden_ancestors: Vec::new(),
1243 };
1244 let rewrite = MountInfoRootRewrite {
1245 raw_mount_id: 30,
1246 deterministic_root: b"/".to_vec(),
1247 raw_root_prefix: None,
1248 deterministic_root_prefix: None,
1249 raw_mountpoint_prefix: Some(b"/tmp/tmpAbC".to_vec()),
1250 deterministic_mountpoint_prefix: Some(b"/tmp".to_vec()),
1251 };
1252 assert_eq!(
1253 table.mounts(&[rewrite]),
1254 b"tmpfs /tmp/x tmpfs rw 0 0\ntmpfs /tmp/tmpAbCdef tmpfs rw 0 0\n"
1255 );
1256 }
1257
1258 #[test]
1259 fn the_parser_refuses_what_parse_mountinfo_refuses() {
1260 assert_eq!(parse_container_mounts(b""), Some(Vec::new()));
1261 for malformed in [
1262 b"20 1 259:4 / / rw - btrfs\n".as_slice(),
1263 b"20 1 259:4 / / rw unknown:7 - btrfs /dev/x rw\n".as_slice(),
1264 b"20 1 259:4 / / rw - btrfs /dev/x rw\n20 1 259:4 / /a rw - btrfs /dev/x rw\n"
1265 .as_slice(),
1266 ] {
1267 assert_eq!(parse_container_mounts(malformed), None, "{malformed:?}");
1268 }
1269 }
1270}