1use std::collections::BTreeSet;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct MountInfoRow {
16 pub raw_mount_id: u64,
17 pub raw_parent_id: u64,
18 pub raw_device: u64,
19 pub root: Vec<u8>,
20 pub raw_peer_groups: Vec<u64>,
21}
22
23pub const MOUNT_PEER_PREFIXES: [&[u8]; 3] = [b"shared:", b"master:", b"propagate_from:"];
26
27pub fn mount_identity_provenance_is_well_formed(order: &[u64], assignment: &[u64]) -> bool {
34 let mut rows = std::collections::BTreeSet::new();
35 if !order.iter().all(|raw| rows.insert(*raw)) {
36 return false;
37 }
38 if assignment.contains(&0) && !rows.contains(&0) {
39 return false;
40 }
41 let mut numbered = std::collections::BTreeSet::new();
42 assignment.iter().all(|raw| numbered.insert(*raw))
43}
44
45pub fn is_ephemeral_host_seed_mount(line: &[u8]) -> bool {
81 let fields: Vec<&[u8]> = line.split(|byte| *byte == b' ').collect();
82 let Some(separator) = fields.iter().position(|field| *field == b"-") else {
83 return false;
84 };
85 let mount_point = fields.get(4).copied().unwrap_or_default();
86 let fs_type = fields.get(separator + 1).copied().unwrap_or_default();
87 is_ephemeral_host_seed_fields(mount_point, fs_type)
88}
89
90pub fn is_ephemeral_host_seed_fields(mount_point: &[u8], fs_type: &[u8]) -> bool {
93 let name = mount_point
94 .rsplit(|byte| *byte == b'/')
95 .next()
96 .unwrap_or_default();
97 fs_type == b"fuse.squashfuse_ll" && is_host_seed_name(name)
98}
99
100pub fn exclude_ephemeral_host_seed_mounts(contents: &[u8]) -> Vec<u8> {
106 let mut out = Vec::with_capacity(contents.len());
107 for line in contents.split_inclusive(|byte| *byte == b'\n') {
108 let body = line.strip_suffix(b"\n").unwrap_or(line);
109 if !is_ephemeral_host_seed_mount(body) {
110 out.extend_from_slice(line);
111 }
112 }
113 out
114}
115
116fn is_host_seed_name(name: &[u8]) -> bool {
119 fn parse(name: &[u8]) -> Option<()> {
120 let hex = name
121 .iter()
122 .take_while(|byte| byte.is_ascii_hexdigit())
123 .count();
124 let rest = name
125 .get(hex..)
126 .filter(|_| hex > 0)?
127 .strip_prefix(b"-seed-")?;
128 let digits = rest
129 .iter()
130 .rev()
131 .take_while(|byte| byte.is_ascii_digit())
132 .count();
133 let seed = rest
134 .get(..rest.len().checked_sub(digits).filter(|_| digits > 0)?)?
135 .strip_suffix(b"-ns-")?;
136 (!seed.is_empty()
137 && seed
138 .iter()
139 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')))
140 .then_some(())
141 }
142 parse(name).is_some()
143}
144
145pub fn mount_ids_are_ordered_subset(visible: &[u64], captured: &[u64]) -> bool {
148 let mut seen = BTreeSet::new();
149 if !visible.iter().all(|raw| seen.insert(*raw)) {
150 return false;
151 }
152 let mut captured = captured.iter();
153 visible
154 .iter()
155 .all(|raw| captured.by_ref().any(|candidate| candidate == raw))
156}
157
158fn decimal(field: &[u8]) -> Option<u64> {
159 if field.is_empty() || field.iter().any(|byte| !byte.is_ascii_digit()) {
160 return None;
161 }
162 std::str::from_utf8(field).ok()?.parse().ok()
163}
164
165fn mount_peer_group(field: &[u8]) -> Option<Option<u64>> {
166 for prefix in MOUNT_PEER_PREFIXES {
167 if let Some(raw) = field.strip_prefix(prefix) {
168 return Some(Some(decimal(raw)?));
169 }
170 }
171 Some(None)
172}
173
174fn parse_mountinfo_row(line: &[u8]) -> Option<MountInfoRow> {
175 let fields = line.split(|byte| *byte == b' ').collect::<Vec<_>>();
176 if fields.iter().any(|field| field.is_empty()) {
177 return None;
178 }
179 let separator = fields.iter().position(|field| *field == b"-")?;
180 if separator < 6 || separator + 4 != fields.len() {
181 return None;
182 }
183 let mut device = fields[2].split(|byte| *byte == b':');
184 let major = u32::try_from(decimal(device.next()?)?).ok()?;
185 let minor = u32::try_from(decimal(device.next()?)?).ok()?;
186 if device.next().is_some() {
187 return None;
188 }
189 let mut raw_peer_groups = Vec::new();
190 for field in &fields[6..separator] {
191 if let Some(raw) = mount_peer_group(field)? {
192 raw_peer_groups.push(raw);
193 } else if field
194 .iter()
195 .position(|byte| *byte == b':')
196 .is_some_and(|separator| decimal(&field[separator + 1..]).is_some())
197 {
198 return None;
203 }
204 }
205 Some(MountInfoRow {
206 raw_mount_id: decimal(fields[0])?,
207 raw_parent_id: decimal(fields[1])?,
208 raw_device: libc::makedev(major, minor),
209 root: fields[3].to_vec(),
210 raw_peer_groups,
211 })
212}
213
214pub fn parse_mountinfo(contents: &[u8]) -> Option<Vec<MountInfoRow>> {
219 if contents.is_empty() {
220 return Some(Vec::new());
221 }
222 let body = contents.strip_suffix(b"\n").unwrap_or(contents);
223 if body.is_empty() {
224 return None;
225 }
226 let rows = body
227 .split(|byte| *byte == b'\n')
228 .map(parse_mountinfo_row)
229 .collect::<Option<Vec<_>>>()?;
230 let mut seen = BTreeSet::new();
231 rows.iter()
232 .all(|row| seen.insert(row.raw_mount_id))
233 .then_some(rows)
234}
235
236pub fn parse_fdinfo_mount_id(contents: &[u8]) -> Option<u64> {
243 let mut mount_id = None;
244 for line in contents.split(|byte| *byte == b'\n') {
245 let Some(value) = line.strip_prefix(b"mnt_id:") else {
246 continue;
247 };
248 let value = value
249 .strip_prefix(b"\t")
250 .or_else(|| value.strip_prefix(b" "))?;
251 if value.is_empty() || value.iter().any(|byte| !byte.is_ascii_digit()) || mount_id.is_some()
252 {
253 return None;
254 }
255 let text = std::str::from_utf8(value).ok()?;
256 mount_id = Some(text.parse().ok()?);
257 }
258 mount_id
259}
260
261#[cfg(test)]
262mod tests {
263 use super::exclude_ephemeral_host_seed_mounts;
264 use super::is_ephemeral_host_seed_mount;
265 use super::mount_ids_are_ordered_subset;
266 use super::parse_fdinfo_mount_id;
267 use super::parse_mountinfo;
268
269 #[test]
270 fn fdinfo_mount_id_is_strict_and_unique() {
271 assert_eq!(parse_fdinfo_mount_id(b"pos:\t0\nmnt_id:\t37\n"), Some(37));
272 assert_eq!(parse_fdinfo_mount_id(b"mnt_id: 0\n"), Some(0));
273 for malformed in [
274 b"pos:\t0\n".as_slice(),
275 b"mnt_id:\tbad\nmnt_id:\t37\n".as_slice(),
276 b"mnt_id:\t37\nmnt_id:\t38\n".as_slice(),
277 b"mnt_id:\t37 trailing\n".as_slice(),
278 b"mnt_id:\t18446744073709551616\n".as_slice(),
279 b"mnt_id:37\n".as_slice(),
280 ] {
281 assert_eq!(parse_fdinfo_mount_id(malformed), None, "{malformed:?}");
282 }
283 }
284
285 #[test]
286 fn mountinfo_parser_accepts_empty_and_refuses_malformed_or_duplicate_rows() {
287 assert_eq!(parse_mountinfo(b""), Some(Vec::new()));
288 assert_eq!(parse_mountinfo(b"\n"), None);
289 let row = b"37 1 8:1 / / rw shared:9 - ext4 /dev/root rw\n";
290 let parsed = parse_mountinfo(row).expect("valid mountinfo row");
291 assert_eq!(parsed.len(), 1);
292 assert_eq!(parsed[0].raw_mount_id, 37);
293 assert_eq!(parsed[0].raw_parent_id, 1);
294 assert_eq!(parsed[0].root, b"/");
295 assert_eq!(parsed[0].raw_peer_groups, [9]);
296 assert!(parse_mountinfo(b"37 1 bad / / rw - ext4 /dev/root rw\n").is_none());
297 assert!(
298 parse_mountinfo(b"37 1 8:1 / / rw unbindable nosymfollow - ext4 /dev/root rw\n")
299 .is_some(),
300 "known bare optional flags must remain accepted"
301 );
302 assert!(
303 parse_mountinfo(b"37 1 8:1 / / rw future_peer:19 - ext4 /dev/root rw\n").is_none(),
304 "unknown numeric optional fields must fail closed"
305 );
306 assert!(
307 parse_mountinfo(b"37 1 8:1 / / rw future_flag:value - ext4 /dev/root rw\n").is_some(),
308 "unknown nonnumeric flags carry no raw numeric identity"
309 );
310 let duplicate = [row.as_slice(), row.as_slice()].concat();
311 assert!(parse_mountinfo(&duplicate).is_none());
312 }
313
314 #[test]
315 fn mount_id_subset_requires_unique_members_in_captured_order() {
316 assert!(mount_ids_are_ordered_subset(&[], &[10, 20, 30]));
317 assert!(mount_ids_are_ordered_subset(&[10, 30], &[10, 20, 30]));
318 assert!(!mount_ids_are_ordered_subset(&[30, 10], &[10, 20, 30]));
319 assert!(!mount_ids_are_ordered_subset(&[10, 99], &[10, 20, 30]));
320 assert!(!mount_ids_are_ordered_subset(&[10, 10], &[10, 20, 30]));
321 }
322
323 const SEED: &[u8] = b"76 1 0:50 / /mnt/xarfuse/uid-1000/e62a203d-seed-nspid4026531836_cgpid16161-ns-4026531832 rw,nosuid,nodev,relatime master:48 - fuse.squashfuse_ll squashfuse_ll rw,user_id=1000,group_id=100,allow_other";
324
325 #[test]
330 fn ephemeral_host_seed_mount_class_is_the_seed_name() {
331 assert!(is_ephemeral_host_seed_mount(SEED));
332 assert!(!is_ephemeral_host_seed_mount(
333 b"77 1 0:51 / /mnt/xarfuse/stable-release rw,relatime - fuse.squashfuse_ll squashfuse_ll rw"
334 ));
335 assert!(is_ephemeral_host_seed_mount(
336 b"76 1 0:50 / /xarfuse/uid-1/e62a203d-seed-nspid4026531836_cgpid16161-ns-4026531832 rw - fuse.squashfuse_ll squashfuse_ll rw"
337 ));
338 assert!(!is_ephemeral_host_seed_mount(
339 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-nspid4026531836_cgpid16161-ns-4026531832 rw - tmpfs none rw"
340 ));
341 for near_miss in [
342 b"76 1 0:50 / /var/releases/www rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
343 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-nspid1_cgpid2-ns-3x rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
344 b"76 1 0:50 / /mnt/xarfuse/uid-1/-seed-nspid1_cgpid2-ns-3 rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
345 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed--ns-3 rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
346 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-chef-ns- rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
347 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-a.b-ns-3 rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
348 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-chef-ns-3 rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
349 b"76 1 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-nspid1_cgpid2-ns-3/sub rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
350 b"18 1 0:21 / /proc rw,nosuid - proc proc rw".as_slice(),
351 b"squashfuse_ll /mnt/xarfuse/uid-1/e62a203d-seed-nspid1_cgpid2-ns-3 fuse.squashfuse_ll rw 0 0".as_slice(),
352 ] {
353 assert!(!is_ephemeral_host_seed_mount(near_miss), "{near_miss:?}");
354 }
355 }
356
357 #[test]
363 fn named_host_seeds_are_excluded() {
364 for named_seed in [
365 b"683 2845 0:111 / /mnt/xarfuse/uid-0/8cce7402-seed-fb-pcie-error-log-ns-4026531832 rw,nosuid,nodev,relatime shared:3548 - fuse.squashfuse_ll squashfuse_ll rw,user_id=0,group_id=0".as_slice(),
366 b"405 2845 0:52 / /mnt/xarfuse/uid-0/de637de8-seed-devserver-cleanup_hg_cache-ns-4026531832 rw,nosuid,nodev,relatime shared:246 - fuse.squashfuse_ll squashfuse_ll rw,user_id=0,group_id=0,allow_other".as_slice(),
367 b"401 2845 0:52 / /mnt/xarfuse/uid-0/06178150-seed-chef-ns-4026531832 rw,nosuid,nodev,relatime shared:246 - fuse.squashfuse_ll squashfuse_ll rw,user_id=0,group_id=0,allow_other".as_slice(),
368 ] {
369 assert!(is_ephemeral_host_seed_mount(named_seed), "{named_seed:?}");
370 }
371 for kept in [
372 b"77 2845 0:51 / /mnt/xarfuse/stable-release rw,relatime - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
373 b"401 2845 0:52 / /mnt/xarfuse/uid-0/06178150-seed-chef-ns-4026531832 rw,nosuid,nodev,relatime shared:246 - tmpfs tmpfs rw".as_slice(),
374 ] {
375 assert!(!is_ephemeral_host_seed_mount(kept), "{kept:?}");
376 }
377 }
378
379 #[test]
380 fn seed_filter_drops_only_seed_rows_and_keeps_order() {
381 let contents = [
382 b"18 1 0:21 / /proc rw - proc proc rw\n".as_slice(),
383 SEED,
384 b"\n401 1 0:52 / /mnt/xarfuse/uid-0/06178150-seed-chef-ns-4026531832 rw - fuse.squashfuse_ll squashfuse_ll rw".as_slice(),
385 b"\n100 1 0:70 / /test rw - tmpfs none rw\n".as_slice(),
386 b"77 1 0:51 / /mnt/xarfuse/stable-release rw - fuse.squashfuse_ll squashfuse_ll rw"
387 .as_slice(),
388 ]
389 .concat();
390 assert_eq!(
391 exclude_ephemeral_host_seed_mounts(&contents),
392 b"18 1 0:21 / /proc rw - proc proc rw\n100 1 0:70 / /test rw - tmpfs none rw\n77 1 0:51 / /mnt/xarfuse/stable-release rw - fuse.squashfuse_ll squashfuse_ll rw"
393 );
394 }
395}