From a4b2bc9c56e6c2b719cf0f238582bbfe5a05e5ea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 09:59:23 +0000 Subject: [PATCH 1/2] Decode /proc/mounts octal escapes in parseMounts The kernel encodes space, tab, newline, and backslash as octal escapes (\040, \011, \012, \134) in /proc/mounts, since those characters would otherwise be ambiguous in its whitespace-separated format. parseMounts used the raw whitespace-split fields verbatim, so a mountpoint or device path containing one of these characters (e.g. a USB drive labeled "My Drive", mounted at /media/pi/My\040Drive) never matched its real path. Add unescapeMountField to decode the four escapes proc(5) documents for this format, applied to both the device and mountpoint fields. Any other backslash sequence is left untouched. --- internal/collector/disk.go | 38 ++++++++++++++++++++-- internal/collector/disk_test.go | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/internal/collector/disk.go b/internal/collector/disk.go index 3b45c25..df61e9c 100644 --- a/internal/collector/disk.go +++ b/internal/collector/disk.go @@ -90,8 +90,8 @@ func parseMounts(data string) ([]mountEntry, error) { continue } entries = append(entries, mountEntry{ - device: fields[0], - mountpoint: fields[1], + device: unescapeMountField(fields[0]), + mountpoint: unescapeMountField(fields[1]), fstype: fields[2], }) } @@ -101,6 +101,40 @@ func parseMounts(data string) ([]mountEntry, error) { return entries, nil } +// mountFieldEscapes maps the octal escapes /proc/mounts uses for characters +// that would otherwise be ambiguous in its whitespace-separated format (see +// proc(5)) to the literal character they represent. +var mountFieldEscapes = map[string]byte{ + "040": ' ', + "011": '\t', + "012": '\n', + "134": '\\', +} + +// unescapeMountField decodes the octal escapes /proc/mounts uses in the +// device and mountpoint fields, e.g. "\040" for a space in a USB drive label +// such as "My\040Drive". Any other "\NNN" sequence (not one of the four +// escapes the kernel emits) is left untouched, as is a lone backslash not +// followed by three octal digits. +func unescapeMountField(s string) string { + if !strings.Contains(s, `\`) { + return s + } + var b strings.Builder + b.Grow(len(s)) + for i := 0; i < len(s); i++ { + if s[i] == '\\' && i+3 < len(s) { + if c, ok := mountFieldEscapes[s[i+1:i+4]]; ok { + b.WriteByte(c) + i += 3 + continue + } + } + b.WriteByte(s[i]) + } + return b.String() +} + // statfsFunc matches syscall.Statfs's signature so tests can inject a fake // implementation instead of touching the real filesystem. type statfsFunc func(path string, buf *syscall.Statfs_t) error diff --git a/internal/collector/disk_test.go b/internal/collector/disk_test.go index a337e3b..f8ff74a 100644 --- a/internal/collector/disk_test.go +++ b/internal/collector/disk_test.go @@ -42,6 +42,63 @@ func TestParseMounts(t *testing.T) { } } +// /proc/mounts encodes space, tab, newline, and backslash as octal escapes +// since those characters would otherwise be ambiguous in the +// whitespace-separated format (see proc(5)). parseMounts must decode them +// back to the real path, e.g. for a USB drive labeled "My Drive". +func TestParseMounts_DecodesOctalEscapes(t *testing.T) { + data := `/dev/sda1 /media/pi/My\040Drive\011\012\134end vfat rw,relatime 0 0 +` + entries, err := parseMounts(data) + if err != nil { + t.Fatalf("parseMounts: %v", err) + } + if len(entries) != 1 { + t.Fatalf("expected 1 entry, got %d", len(entries)) + } + want := "/media/pi/My Drive\t\n\\end" + if entries[0].mountpoint != want { + t.Fatalf("mountpoint = %q, want %q", entries[0].mountpoint, want) + } +} + +// The device field can carry the same escapes, e.g. for a labeled device +// node under /dev/disk/by-label. +func TestParseMounts_DecodesOctalEscapesInDevice(t *testing.T) { + data := `/dev/disk/by-label/My\040Label /mnt/usb ext4 rw,relatime 0 0 +` + entries, err := parseMounts(data) + if err != nil { + t.Fatalf("parseMounts: %v", err) + } + if len(entries) != 1 { + t.Fatalf("expected 1 entry, got %d", len(entries)) + } + want := "/dev/disk/by-label/My Label" + if entries[0].device != want { + t.Fatalf("device = %q, want %q", entries[0].device, want) + } +} + +// A backslash sequence that isn't one of the four escapes /proc/mounts +// actually emits, or a lone trailing backslash, must be passed through +// unchanged rather than misinterpreted or panicking on a short slice. +func TestParseMounts_LeavesUnknownBackslashSequencesUntouched(t *testing.T) { + data := `/dev/sda1 /mnt/weird\x41\ vfat rw,relatime 0 0 +` + entries, err := parseMounts(data) + if err != nil { + t.Fatalf("parseMounts: %v", err) + } + if len(entries) != 1 { + t.Fatalf("expected 1 entry, got %d", len(entries)) + } + want := `/mnt/weird\x41\` + if entries[0].mountpoint != want { + t.Fatalf("mountpoint = %q, want %q", entries[0].mountpoint, want) + } +} + func TestDefaultExcludedFSTypes_FiltersPseudoFS(t *testing.T) { for _, fstype := range []string{"proc", "sysfs", "tmpfs", "overlay"} { if !defaultExcludedFSTypes[fstype] { From 37d534282af401ca1a999bf5682f02e8efb44b53 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 10:50:55 +0000 Subject: [PATCH 2/2] Decode fstype escapes too and table-drive the escape tests fstype is mangled by the kernel with the same octal escapes as device and mountpoint (e.g. a fuse subtype derived from user input can be "fuse.My\040FS"), but only the other two fields were being decoded. Apply unescapeMountField to fstype as well, since it's used for the excludedFSType lookup and surfaced verbatim in /api/v1/metrics and the dashboard. Collapse the three separate escape-decoding test functions into one table-driven TestParseMounts_DecodesOctalEscapes per docs/TESTS.md, and add a case pinning that the single left-to-right decoding pass doesn't re-scan bytes it already emitted: an escaped backslash ("\134") immediately followed by digits that look like another escape ("040") must decode to a literal "\040", not a space. --- internal/collector/disk.go | 12 ++- internal/collector/disk_test.go | 126 +++++++++++++++++++------------- 2 files changed, 84 insertions(+), 54 deletions(-) diff --git a/internal/collector/disk.go b/internal/collector/disk.go index df61e9c..fd03da1 100644 --- a/internal/collector/disk.go +++ b/internal/collector/disk.go @@ -92,7 +92,7 @@ func parseMounts(data string) ([]mountEntry, error) { entries = append(entries, mountEntry{ device: unescapeMountField(fields[0]), mountpoint: unescapeMountField(fields[1]), - fstype: fields[2], + fstype: unescapeMountField(fields[2]), }) } if err := scanner.Err(); err != nil { @@ -112,10 +112,14 @@ var mountFieldEscapes = map[string]byte{ } // unescapeMountField decodes the octal escapes /proc/mounts uses in the -// device and mountpoint fields, e.g. "\040" for a space in a USB drive label -// such as "My\040Drive". Any other "\NNN" sequence (not one of the four +// device, mountpoint, and fstype fields (fstype is mangled too, e.g. a fuse +// subtype derived from user input), e.g. "\040" for a space in a USB drive +// label such as "My\040Drive". Any other "\NNN" sequence (not one of the four // escapes the kernel emits) is left untouched, as is a lone backslash not -// followed by three octal digits. +// followed by three octal digits. The single left-to-right pass only +// consumes the three digits of a matched escape, so it never re-scans bytes +// it already emitted — a literal "\134040" (an escaped backslash followed by +// literal "040") decodes to "\040", not to a space. func unescapeMountField(s string) string { if !strings.Contains(s, `\`) { return s diff --git a/internal/collector/disk_test.go b/internal/collector/disk_test.go index f8ff74a..474236d 100644 --- a/internal/collector/disk_test.go +++ b/internal/collector/disk_test.go @@ -44,58 +44,84 @@ func TestParseMounts(t *testing.T) { // /proc/mounts encodes space, tab, newline, and backslash as octal escapes // since those characters would otherwise be ambiguous in the -// whitespace-separated format (see proc(5)). parseMounts must decode them -// back to the real path, e.g. for a USB drive labeled "My Drive". +// whitespace-separated format (see proc(5)), for all three of the device, +// mountpoint, and fstype fields (e.g. a USB drive labeled "My Drive", or a +// fuse subtype derived from user input). parseMounts must decode them back +// to the real value in each field. func TestParseMounts_DecodesOctalEscapes(t *testing.T) { - data := `/dev/sda1 /media/pi/My\040Drive\011\012\134end vfat rw,relatime 0 0 -` - entries, err := parseMounts(data) - if err != nil { - t.Fatalf("parseMounts: %v", err) - } - if len(entries) != 1 { - t.Fatalf("expected 1 entry, got %d", len(entries)) - } - want := "/media/pi/My Drive\t\n\\end" - if entries[0].mountpoint != want { - t.Fatalf("mountpoint = %q, want %q", entries[0].mountpoint, want) - } -} - -// The device field can carry the same escapes, e.g. for a labeled device -// node under /dev/disk/by-label. -func TestParseMounts_DecodesOctalEscapesInDevice(t *testing.T) { - data := `/dev/disk/by-label/My\040Label /mnt/usb ext4 rw,relatime 0 0 -` - entries, err := parseMounts(data) - if err != nil { - t.Fatalf("parseMounts: %v", err) - } - if len(entries) != 1 { - t.Fatalf("expected 1 entry, got %d", len(entries)) - } - want := "/dev/disk/by-label/My Label" - if entries[0].device != want { - t.Fatalf("device = %q, want %q", entries[0].device, want) - } -} - -// A backslash sequence that isn't one of the four escapes /proc/mounts -// actually emits, or a lone trailing backslash, must be passed through -// unchanged rather than misinterpreted or panicking on a short slice. -func TestParseMounts_LeavesUnknownBackslashSequencesUntouched(t *testing.T) { - data := `/dev/sda1 /mnt/weird\x41\ vfat rw,relatime 0 0 -` - entries, err := parseMounts(data) - if err != nil { - t.Fatalf("parseMounts: %v", err) - } - if len(entries) != 1 { - t.Fatalf("expected 1 entry, got %d", len(entries)) + tests := []struct { + name string + line string + wantDevice string + wantMountpoint string + wantFSType string + }{ + { + name: "escapes in mountpoint", + line: `/dev/sda1 /media/pi/My\040Drive\011\012\134end vfat rw,relatime 0 0`, + wantDevice: "/dev/sda1", + wantMountpoint: "/media/pi/My Drive\t\n\\end", + wantFSType: "vfat", + }, + { + name: "escapes in device", + line: `/dev/disk/by-label/My\040Label /mnt/usb ext4 rw,relatime 0 0`, + wantDevice: "/dev/disk/by-label/My Label", + wantMountpoint: "/mnt/usb", + wantFSType: "ext4", + }, + { + name: "escapes in fstype subtype", + line: `fusefs /mnt/x fuse.My\040FS rw 0 0`, + wantDevice: "fusefs", + wantMountpoint: "/mnt/x", + wantFSType: "fuse.My FS", + }, + { + name: "unknown backslash sequences left untouched", + // Neither "\x41" nor a lone trailing "\" is one of the four + // escapes /proc/mounts actually emits, so both must pass + // through unchanged rather than being misinterpreted or + // panicking on a short slice. + line: `/dev/sda1 /mnt/weird\x41\ vfat rw,relatime 0 0`, + wantDevice: "/dev/sda1", + wantMountpoint: `/mnt/weird\x41\`, + wantFSType: "vfat", + }, + { + name: "escaped backslash is not re-scanned with what follows it", + // The kernel emits a literal backslash in a path as "\134". + // A single left-to-right pass that only consumes the three + // digits of a matched escape must decode "\134040" (an + // escaped backslash followed by literal "040") to "\040", not + // to a space — i.e. it must not re-scan the "040" that + // follows the emitted backslash as a fresh escape. + line: `/dev/sda1 /mnt/a\134040b vfat rw,relatime 0 0`, + wantDevice: "/dev/sda1", + wantMountpoint: `/mnt/a\040b`, + wantFSType: "vfat", + }, } - want := `/mnt/weird\x41\` - if entries[0].mountpoint != want { - t.Fatalf("mountpoint = %q, want %q", entries[0].mountpoint, want) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entries, err := parseMounts(tt.line + "\n") + if err != nil { + t.Fatalf("parseMounts: %v", err) + } + if len(entries) != 1 { + t.Fatalf("expected 1 entry, got %d", len(entries)) + } + got := entries[0] + if got.device != tt.wantDevice { + t.Errorf("device = %q, want %q", got.device, tt.wantDevice) + } + if got.mountpoint != tt.wantMountpoint { + t.Errorf("mountpoint = %q, want %q", got.mountpoint, tt.wantMountpoint) + } + if got.fstype != tt.wantFSType { + t.Errorf("fstype = %q, want %q", got.fstype, tt.wantFSType) + } + }) } }