Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions procfs-core/src/mounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,26 @@ impl super::FromBufRead for Vec<MountEntry> {

for line in r.lines() {
let line = expect!(line);
let mut s = line.split(' '); // not using split_whitespace because we might have empty fields

let fs_spec = unmangle_octal(expect!(s.next()));
let fs_file = unmangle_octal(expect!(s.next()));
let fs_vfstype = unmangle_octal(expect!(s.next()));
let fs_mntops = unmangle_octal(expect!(s.next()));
// The format is `fs_spec fs_file fs_vfstype fs_mntops fs_freq fs_passno`.
// Each field is normally space-mangled (e.g. `\040` for a literal space),
// so a plain split on ' ' would yield six fields. In practice some
// drivers emit `fs_mntops` with unescaped spaces inside option values
// (notably the 9p mount Docker Desktop on WSL2 creates, where the
// option string contains `path=C:\Program Files\...`). To stay robust
// to that, take the first three fields from the front and the last two
// from the back; whatever is left in between is `fs_mntops`.
let mut head = line.splitn(4, ' ');
let fs_spec = unmangle_octal(expect!(head.next()));
let fs_file = unmangle_octal(expect!(head.next()));
let fs_vfstype = unmangle_octal(expect!(head.next()));
let rest = expect!(head.next());

let mut tail = rest.rsplitn(3, ' ');
let fs_passno = expect!(u8::from_str(expect!(tail.next())));
let fs_freq = expect!(u8::from_str(expect!(tail.next())));
let fs_mntops = unmangle_octal(expect!(tail.next()));

let fs_mntops: HashMap<String, Option<String>> = fs_mntops
.split(',')
.map(|s| {
Expand All @@ -47,8 +61,6 @@ impl super::FromBufRead for Vec<MountEntry> {
(k, v)
})
.collect();
let fs_freq = expect!(u8::from_str(expect!(s.next())));
let fs_passno = expect!(u8::from_str(expect!(s.next())));

let mount_entry = MountEntry {
fs_spec,
Expand All @@ -66,13 +78,14 @@ impl super::FromBufRead for Vec<MountEntry> {
}
}


/// Unmangle spaces ' ', tabs '\t', line breaks '\n', backslashes '\\', and hashes '#'
///
/// See https://elixir.bootlin.com/linux/v6.2.8/source/fs/proc_namespace.c#L89
pub(crate) fn unmangle_octal(input: &str) -> String {
let mut input = input.to_string();

for (octal, c) in [(r"\011", "\t"), (r"\012", "\n"), (r"\134", "\\"), (r"\043", "#")] {
for (octal, c) in [(r"\040", " "), (r"\011", "\t"), (r"\012", "\n"), (r"\134", "\\"), (r"\043", "#")] {
input = input.replace(octal, c);
}

Expand Down Expand Up @@ -114,3 +127,21 @@ Downloads /media/sf_downloads vboxsf rw,nodev,relatime,iocharset=utf8,uid=0,gid=
assert_eq!(mounts[0].fs_vfstype, "tmpfs");
assert!(mounts[0].fs_mntops.contains_key("ro"));
}

#[test]
fn test_unescaped_spaces_in_mntops() {
// Docker Desktop on WSL2 emits 9p mount lines with literal spaces inside
// `path=...` in fs_mntops; the parser must tolerate them.
let raw = b"C:\\134Program\\040Files\\134Docker\\134Docker\\134resources /Docker/host 9p rw,noatime,aname=drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/,cache=5,access=client,msize=65536,trans=fd,rfd=3,wfd=3 0 0\n";
let mounts = <Vec<MountEntry> as crate::FromBufRead>::from_buf_read(&raw[..]).unwrap();
assert_eq!(mounts.len(), 1);
let m = &mounts[0];
assert_eq!(m.fs_spec, r"C:\Program Files\Docker\Docker\resources");
assert_eq!(m.fs_file, "/Docker/host");
assert_eq!(m.fs_vfstype, "9p");
assert_eq!(m.fs_freq, 0);
assert_eq!(m.fs_passno, 0);
assert_eq!(m.fs_mntops.get("cache").unwrap().as_deref(), Some("5"));
// The space-bearing value is preserved end-to-end.
assert!(m.fs_mntops.get("aname").unwrap().as_deref().unwrap().contains("C:\\Program Files\\Docker"));
}
Loading