From 7783cdf5cc91a88e3d1d105ba282288a1386c5d1 Mon Sep 17 00:00:00 2001 From: brent saner Date: Tue, 7 Oct 2025 13:15:28 -0400 Subject: [PATCH 1/8] Fixes #1284 I know this issue was closed, but it's literally a 2 second fix to bring it in line with proper documented parsing of the mountinfo format. --- disk/disk_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 8c4803338c..bbc58d68f0 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -332,7 +332,7 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] // (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) // split the mountinfo line by the separator hyphen - parts := strings.Split(line, " - ") + parts := strings.SplitN(line, " - ", 2) if len(parts) != 2 { return nil, fmt.Errorf("found invalid mountinfo line in file %s: %s ", filename, line) } From 2aff976c76d52a36d9084b23337c88c85ef5c009 Mon Sep 17 00:00:00 2001 From: brent saner Date: Tue, 7 Oct 2025 15:52:49 -0400 Subject: [PATCH 2/8] Fixes #1932 Certain virtual filesystems (e.g. nsfs, SOME tmpfs, overlayfs, etc.) should NOT duplicate the fstype as the source, as they have a *real* source. This PR applies logic to determine whether to use mountinfo field 10 as the device ('source') or to use the mouninfo field 3 ('root') as the device. --- disk/disk_linux.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 8c4803338c..029e7fc578 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -341,6 +341,7 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] blockDeviceID := fields[2] mountPoint := fields[4] mountOpts := strings.Split(fields[5], ",") + device := fields[3] if rootDir := fields[3]; rootDir != "" && rootDir != "/" { mountOpts = append(mountOpts, "bind") @@ -348,7 +349,9 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] fields = strings.Fields(parts[1]) fstype := fields[0] - device := fields[1] + if (fields[0] != fields[1]) || (fields[0] == fields[1] && device == "/") { + device = fields[1] + } d := PartitionStat{ Device: device, From 564c4617cb65294a0ca982bf4a502a89bd8ed96c Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 9 Oct 2025 13:15:07 -0400 Subject: [PATCH 3/8] add test case --- disk/disk_linux_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/disk/disk_linux_test.go b/disk/disk_linux_test.go index ec9467f719..94e9ce5f3e 100644 --- a/disk/disk_linux_test.go +++ b/disk/disk_linux_test.go @@ -15,6 +15,7 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { fs := []string{"sysfs", "tmpfs"} lines := []string{ + "22 13 0:19 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs - rw", // PR #1931 issue #1284 "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", } @@ -26,6 +27,7 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { "all": { all: true, expect: []PartitionStat{ + {Device: "-", Mountpoint: "/dev/shm", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}}, {Device: "sysfs", Mountpoint: "/sys", Fstype: "sysfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "noatime"}}, {Device: "none", Mountpoint: "/run", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev"}}, }, @@ -33,6 +35,7 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { "not all": { all: false, expect: []PartitionStat{ + {Device: "-", Mountpoint: "/dev/shm", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}}, {Device: "sysfs", Mountpoint: "/sys", Fstype: "sysfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "noatime"}}, }, }, From 4421ee806b52bdd001976042210324c49171e9e5 Mon Sep 17 00:00:00 2001 From: brent saner Date: Thu, 9 Oct 2025 17:40:17 -0400 Subject: [PATCH 4/8] prep for merge into PR #1931 --- disk/disk_linux.go | 19 ++++++++++++++++--- disk/disk_linux_test.go | 7 +++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 029e7fc578..857d20584b 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -327,6 +327,7 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] ret := make([]PartitionStat, 0, len(lines)) for _, line := range lines { + // See proc_pid_mountinfo(5) (proc(5) on EL) // 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) @@ -339,16 +340,27 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] fields := strings.Fields(parts[0]) blockDeviceID := fields[2] + rootDir := fields[3] mountPoint := fields[4] mountOpts := strings.Split(fields[5], ",") + fields = strings.Fields(parts[1]) + fsType := fields[0] + mntSrc := fields[1] + // 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(rootDir, "/") && strings.HasPrefix(mntSrc, "/")) { + device = mntSrc + } device := fields[3] - if rootDir := fields[3]; rootDir != "" && rootDir != "/" { + if rootDir != "" && rootDir != "/" { mountOpts = append(mountOpts, "bind") } - fields = strings.Fields(parts[1]) - fstype := fields[0] + // Use the "source" field for non-virtual real-sourced mounts instad of "root". + // The "source" field is actually "*filesystem-specific* information". if (fields[0] != fields[1]) || (fields[0] == fields[1] && device == "/") { device = fields[1] } @@ -361,6 +373,7 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] } if !all { + // Per fstab(5), if d.Device == "none" || !common.StringsHas(fs, d.Fstype) { continue } diff --git a/disk/disk_linux_test.go b/disk/disk_linux_test.go index ec9467f719..ae39f835b6 100644 --- a/disk/disk_linux_test.go +++ b/disk/disk_linux_test.go @@ -15,8 +15,11 @@ 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 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue", + "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 { From e370cf64ade6646d44f98afa266b0ff19819f44d Mon Sep 17 00:00:00 2001 From: brent saner Date: Fri, 10 Oct 2025 01:45:25 -0400 Subject: [PATCH 5/8] Proper device sourcing This commit properly handles logic to determine the real device, it properly filters out non-"real"/non-physical mounts, and it adds more test cases for mountfile parsing. --- disk/disk_linux.go | 36 ++++++++++++++++++++---------------- disk/disk_linux_test.go | 16 +++++++++++----- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/disk/disk_linux.go b/disk/disk_linux.go index fdd4fa891e..9184ba74b1 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -325,7 +325,9 @@ func parseFieldsOnMounts(lines []string, all bool, fs []string) []PartitionStat func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs []string, filename string) ([]PartitionStat, error) { ret := make([]PartitionStat, 0, len(lines)) + seenDevIDs := make(map[string]string) + fmt.Printf("all: %v\nfs: %#v\n", all, fs) for _, line := range lines { // See proc_pid_mountinfo(5) (proc(5) on EL) // a line of 1/mountinfo has the following structure: @@ -346,39 +348,41 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] fields = strings.Fields(parts[1]) 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(rootDir, "/") && strings.HasPrefix(mntSrc, "/")) { + if strings.HasPrefix(mntSrc, "/") { device = mntSrc + } else if rootDir == "/" { + device = "none" } - device := fields[3] - if 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") } - - // Use the "source" field for non-virtual real-sourced mounts instad of "root". - // The "source" field is actually "*filesystem-specific* information". - if (fields[0] != fields[1]) || (fields[0] == fields[1] && device == "/") { - 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 { - // Per fstab(5), - 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 454cbd0439..f144b3cff1 100644 --- a/disk/disk_linux_test.go +++ b/disk/disk_linux_test.go @@ -16,7 +16,8 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { lines := []string{ "05 2 9:126 / / rw,noatime shared:1 - ext4 /dev/sda1 rw", - "06 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue", + "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", @@ -30,20 +31,25 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { "all": { all: true, expect: []PartitionStat{ - {Device: "-", Mountpoint: "/dev/shm", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}}, - {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: "-", Mountpoint: "/dev/shm", Fstype: "tmpfs", Opts: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}}, - {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"}}, }, }, } + spew.Config.DisableMethods = true for name, c := range cases { t.Run(name, func(t *testing.T) { actual, err := parseFieldsOnMountinfo(context.Background(), lines, c.all, fs, "") From 981569dd733a4704541547bf1db776d3aa446a58 Mon Sep 17 00:00:00 2001 From: brent saner Date: Fri, 10 Oct 2025 01:55:38 -0400 Subject: [PATCH 6/8] remove artifact from test dumping --- disk/disk_linux_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/disk/disk_linux_test.go b/disk/disk_linux_test.go index f144b3cff1..97aa073fbb 100644 --- a/disk/disk_linux_test.go +++ b/disk/disk_linux_test.go @@ -49,7 +49,6 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { }, } - spew.Config.DisableMethods = true for name, c := range cases { t.Run(name, func(t *testing.T) { actual, err := parseFieldsOnMountinfo(context.Background(), lines, c.all, fs, "") From 0fddc5656d05ed87e671c55ab7ce3ee8b96fb4e4 Mon Sep 17 00:00:00 2001 From: brent saner Date: Fri, 10 Oct 2025 01:56:17 -0400 Subject: [PATCH 7/8] ditto --- disk/disk_linux.go | 1 - 1 file changed, 1 deletion(-) diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 9184ba74b1..837426eeb3 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -327,7 +327,6 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] ret := make([]PartitionStat, 0, len(lines)) seenDevIDs := make(map[string]string) - fmt.Printf("all: %v\nfs: %#v\n", all, fs) for _, line := range lines { // See proc_pid_mountinfo(5) (proc(5) on EL) // a line of 1/mountinfo has the following structure: From 7b002dfe0fe15363ac92219e7c1550ba30e46258 Mon Sep 17 00:00:00 2001 From: brent saner Date: Sat, 1 Nov 2025 14:48:00 -0400 Subject: [PATCH 8/8] Review fixes (@shirou) - Removed `fs` param from parseFieldsOnMountinfo sig entirely as it's unused with new logic-driven classification, and callers updated. - Added length check for both `fields` splits. --- disk/disk_linux.go | 34 +++++++++++++++++++++++----------- disk/disk_linux_test.go | 16 +++++++--------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/disk/disk_linux.go b/disk/disk_linux.go index 837426eeb3..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,28 +323,40 @@ 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 { // See proc_pid_mountinfo(5) (proc(5) on EL) - // 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 + // 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 @@ -368,9 +380,9 @@ func parseFieldsOnMountinfo(ctx context.Context, lines []string, all bool, fs [] isBind = true mountOpts = append(mountOpts, "bind") } - + seenDevIDs[blockDeviceID] = mountPoint - + if !all && isBind { continue } diff --git a/disk/disk_linux_test.go b/disk/disk_linux_test.go index 97aa073fbb..9474948cf2 100644 --- a/disk/disk_linux_test.go +++ b/disk/disk_linux_test.go @@ -12,8 +12,6 @@ import ( ) func Test_parseFieldsOnMountinfo(t *testing.T) { - fs := []string{"sysfs", "tmpfs"} - lines := []string{ "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", @@ -31,11 +29,11 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { "all": { all: true, expect: []PartitionStat{ - {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: "/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: "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"}}, }, @@ -43,15 +41,15 @@ func Test_parseFieldsOnMountinfo(t *testing.T) { "not all": { all: false, expect: []PartitionStat{ - {Device: "/dev/sda1", Mountpoint: "/", Fstype: "ext4", Opts:[]string{"rw", "noatime"}}, - {Device: "/dev/sda2", Mountpoint: "/foo", Fstype: "ext4", Opts:[]string{"rw", "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) })