diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 8c4803338c..9a23d35806 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -292,7 +292,7 @@ func PartitionsWithContext(ctx context.Context, all bool) ([]PartitionStat, erro } // use mountinfo - ret, err = parseFieldsOnMountinfo(ctx, lines, all, fs, filename) + ret, err = parseFieldsOnMountinfo(ctx, lines, all, filename) if err != nil { return nil, fmt.Errorf("error parsing mountinfo file %s: %w", filename, err) } @@ -323,46 +323,77 @@ func parseFieldsOnMounts(lines []string, all bool, fs []string) []PartitionStat return ret } -func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs []string, filename string) ([]PartitionStat, error) { +func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, filename string) ([]PartitionStat, error) { ret := make([]PartitionStat, 0, len(lines)) + seenDevIDs := make(map[string]string) for _, line := range lines { - // a line of 1/mountinfo has the following structure: - // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue - // (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) - - // split the mountinfo line by the separator hyphen - parts := strings.Split(line, " - ") + // See proc_pid_mountinfo(5) (proc(5) on EL) + // A line of (//mountinfo) has the following structure: + // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + // (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) + // Documentation is unclear if (6) is optional/may not be present, so it is conditionally parsed if present. + // (7) is optional and may not be present, but this function does not currently use it. + // Documentation is unclear if (11) is optional or not but this function does not currently use it. + + // split the mountinfo line by the separator hyphen (`(8)` above) + parts := strings.SplitN(line, " - ", 2) if len(parts) != 2 { - return nil, fmt.Errorf("found invalid mountinfo line in file %s: %s ", filename, line) + return nil, fmt.Errorf("found invalid mountinfo line in file %s (bad parts len): %s ", filename, line) } fields := strings.Fields(parts[0]) + if len(fields) < 5 { // field (7) is optional, field (6) may(?) be optional + return nil, fmt.Errorf("found invalid mountinfo line in file %s (bad fields(1) len): %s ", filename, line) + } blockDeviceID := fields[2] + rootDir := fields[3] mountPoint := fields[4] - mountOpts := strings.Split(fields[5], ",") + mountOpts := []string{} + if len(fields) >= 6 { + mountOpts = strings.Split(fields[5], ",") + } + fields = strings.Fields(parts[1]) + if len(fields) < 2 { + return nil, fmt.Errorf("found invalid mountinfo line in file %s (bad fields(2) len): %s ", filename, line) + } + fsType := fields[0] + mntSrc := fields[1] + isBind := false + // Per fstab(5), the device can be any string for non-storage-backed filesystems. + if !all && !strings.HasPrefix(mntSrc, "/") { + continue + } + // Some virtual/non-storage filesystems do still have real sources (e.g. nsfs binds), + // but need to use the "root" field (field 4) instead of the "source" field (field 10). + // The "source" field is actually "*filesystem-specific" information". + device := rootDir + if strings.HasPrefix(mntSrc, "/") { + device = mntSrc + } else if rootDir == "/" { + device = "none" + } - if rootDir := fields[3]; rootDir != "" && rootDir != "/" { + if _, ok := seenDevIDs[blockDeviceID]; ok { + // Bind mount; set the underlying mount path as the device. + device = seenDevIDs[blockDeviceID] + isBind = true mountOpts = append(mountOpts, "bind") } - fields = strings.Fields(parts[1]) - fstype := fields[0] - device := fields[1] + seenDevIDs[blockDeviceID] = mountPoint + + if !all && isBind { + continue + } d := PartitionStat{ Device: device, Mountpoint: unescapeFstab(mountPoint), - Fstype: fstype, + Fstype: fsType, Opts: mountOpts, } - if !all { - if d.Device == "none" || !common.StringsHas(fs, d.Fstype) { - continue - } - } - if strings.HasPrefix(d.Device, "/dev/mapper/") { devpath, err := filepath.EvalSymlinks(common.HostDevWithContext(ctx, strings.Replace(d.Device, "/dev", "", 1))) if err == nil { diff --git a/disk/disk_linux_test.go b/disk/disk_linux_test.go index ec9467f719..9474948cf2 100644 --- a/disk/disk_linux_test.go +++ b/disk/disk_linux_test.go @@ -12,11 +12,14 @@ import ( ) func Test_parseFieldsOnMountinfo(t *testing.T) { - fs := []string{"sysfs", "tmpfs"} - lines := []string{ - "111 80 0:22 / /sys rw,nosuid,nodev,noexec,noatime shared:15 - sysfs sysfs rw", - "114 80 0:61 / /run rw,nosuid,nodev shared:18 - tmpfs none rw,mode=755", + "05 2 9:126 / / rw,noatime shared:1 - ext4 /dev/sda1 rw", + "06 3 9:127 / /foo rw,noatime shared:1 - ext4 /dev/sda2 rw", + "07 3 9:127 /bar /foo/bar rw,noatime shared:1 - ext4 /dev/sda2 rw", // "bind mount" on /foo + "22 13 0:19 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs - rw", + "37 29 0:4 net:[12345] /run/netns/foo rw shared:552 - nsfs nsfs rw", + "111 80 0:22 / /sys rw,nosuid,nodev,noexec,noatime shared:15 - sysfs sysfs rw", + "114 80 0:61 / /run rw,nosuid,nodev shared:18 - tmpfs none rw,mode=755", } cases := map[string]struct { @@ -26,21 +29,27 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { "all": { all: true, expect: []PartitionStat{ - {Device: "sysfs", Mountpoint: "/sys", Fstype: "sysfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "noatime"}}, + {Device: "/dev/sda1", Mountpoint: "/", Fstype: "ext4", Opts: []string{"rw", "noatime"}}, + {Device: "/dev/sda2", Mountpoint: "/foo", Fstype: "ext4", Opts: []string{"rw", "noatime"}}, + {Device: "/foo", Mountpoint: "/foo/bar", Fstype: "ext4", Opts: []string{"rw", "noatime", "bind"}}, + {Device: "none", Mountpoint: "/dev/shm", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}}, + {Device: "net:[12345]", Mountpoint: "/run/netns/foo", Fstype: "nsfs", Opts: []string{"rw"}}, + {Device: "none", Mountpoint: "/sys", Fstype: "sysfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "noatime"}}, {Device: "none", Mountpoint: "/run", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev"}}, }, }, "not all": { all: false, expect: []PartitionStat{ - {Device: "sysfs", Mountpoint: "/sys", Fstype: "sysfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "noatime"}}, + {Device: "/dev/sda1", Mountpoint: "/", Fstype: "ext4", Opts: []string{"rw", "noatime"}}, + {Device: "/dev/sda2", Mountpoint: "/foo", Fstype: "ext4", Opts: []string{"rw", "noatime"}}, }, }, } for name, c := range cases { t.Run(name, func(t *testing.T) { - actual, err := parseFieldsOnMountinfo(context.Background(), lines, c.all, fs, "") + actual, err := parseFieldsOnMountinfo(context.Background(), lines, c.all, "") require.NoError(t, err) assert.Equal(t, c.expect, actual) })