Skip to content
Merged
Show file tree
Hide file tree
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
44 changes: 41 additions & 3 deletions internal/collector/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ func parseMounts(data string) ([]mountEntry, error) {
continue
}
entries = append(entries, mountEntry{
device: fields[0],
mountpoint: fields[1],
fstype: fields[2],
device: unescapeMountField(fields[0]),
mountpoint: unescapeMountField(fields[1]),
fstype: unescapeMountField(fields[2]),
})
}
if err := scanner.Err(); err != nil {
Expand All @@ -101,6 +101,44 @@ 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, 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. 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
}
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
Expand Down
83 changes: 83 additions & 0 deletions internal/collector/disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,89 @@ 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)), 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) {
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",
},
}
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)
}
})
}
}
Comment thread
LarsLaskowski marked this conversation as resolved.

func TestDefaultExcludedFSTypes_FiltersPseudoFS(t *testing.T) {
for _, fstype := range []string{"proc", "sysfs", "tmpfs", "overlay"} {
if !defaultExcludedFSTypes[fstype] {
Expand Down
Loading