From f85db4350c79befb398933526db29c21f6399cb0 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Sat, 14 Nov 2026 12:54:18 -0800 Subject: [PATCH] mountinfo: use statx in Mounted and MountedFast Since Linux v5.8, statx(2) reports STATX_ATTR_MOUNT_ROOT attribute, which tells whether the path is a root of a mount. This works for all kinds of mounts, including bind mounts, and requires a single syscall. Use it as the first method in Mounted and MountedFast, falling back to existing methods (openat2, stat, mountinfo) if statx fails for any reason, or the kernel does not report STATX_ATTR_MOUNT_ROOT. The errors are not returned from statx to keep the existing error semantics (those come from normalizePath used by the fallback code). Since the kernel resolves symlinks, there is no need to normalize the path (which is relatively costly, as filepath.EvalSymlinks does lstat on every path component), only to make it absolute and clean (so that ".." is handled the same way as before). AT_NO_AUTOMOUNT is used to avoid triggering an automount, which is consistent with the other methods (and avoids side effects). As a result, Mounted is about 10x faster (523 vs 5056 ns/op on a path with 6 components). Add the new method to the tests. Signed-off-by: Kir Kolyshkin --- mountinfo/mounted_linux.go | 59 +++++++++++++++++++++++++++++++-- mountinfo/mounted_linux_test.go | 43 +++++++++++++++++++----- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/mountinfo/mounted_linux.go b/mountinfo/mounted_linux.go index 58f13c26..febdf693 100644 --- a/mountinfo/mounted_linux.go +++ b/mountinfo/mounted_linux.go @@ -1,6 +1,7 @@ package mountinfo import ( + "errors" "os" "path/filepath" @@ -18,15 +19,20 @@ import ( // be handled separately using e.g. errors.Is(err, fs.ErrNotExist). // // This function is only available on Linux. When available (since kernel -// v5.6), openat2(2) syscall is used to reliably detect all mounts. Otherwise, -// the implementation falls back to using stat(2), which can reliably detect -// normal (but not bind) mounts. +// v5.8), statx(2) syscall is used to reliably detect all mounts, followed +// by openat2(2) (since kernel v5.6). Otherwise, the implementation falls back +// to using stat(2), which can reliably detect normal (but not bind) mounts. func MountedFast(path string) (mounted, sure bool, err error) { // Root is always mounted. if path == string(os.PathSeparator) { return true, true, nil } + // Fastest path: statx, which does not need a normalized path. + if mounted, ok := tryMountedByStatx(path); ok { + return mounted, true, nil + } + path, err = normalizePath(path) if err != nil { return false, false, err @@ -35,6 +41,48 @@ func MountedFast(path string) (mounted, sure bool, err error) { return } +var errNoMountRoot = errors.New("statx: STATX_ATTR_MOUNT_ROOT not supported") + +// mountedByStatx is a method of detecting a mount that works for all kinds +// of mounts (incl. bind mounts), but requires a recent (v5.8+) linux kernel. +// Unlike other mountedBy* methods, it does not require path to be normalized +// (symlinks are resolved by the kernel), but it must be absolute and clean. +func mountedByStatx(path string) (bool, error) { + var st unix.Statx_t + + // Use AT_NO_AUTOMOUNT to not trigger an automount: we want to know + // whether path is a mount point now, without causing any side effects + // (this is also consistent with what other methods do). + err := unix.Statx(unix.AT_FDCWD, path, unix.AT_NO_AUTOMOUNT, 0, &st) + if err != nil { + return false, &os.PathError{Op: "statx", Path: path, Err: err} + } + if st.Attributes_mask&unix.STATX_ATTR_MOUNT_ROOT == 0 { + return false, errNoMountRoot + } + return st.Attributes&unix.STATX_ATTR_MOUNT_ROOT != 0, nil +} + +// tryMountedByStatx is a wrapper around mountedByStatx which accepts any path. +// If ok is false, other methods need to be used. In particular, no errors are +// returned: in case of an error (such as non-existent path), a fallback method +// should return the error to the caller. +func tryMountedByStatx(path string) (mounted, ok bool) { + // Do not use normalizePath here as it is relatively slow, and + // statx resolves symlinks anyway. Yet, make the path absolute and + // clean, so ".." is treated the same way as by normalizePath. + path, err := filepath.Abs(path) + if err != nil { + return false, false + } + // Root is always mounted (even if it is not a mount root, e.g. chroot). + if path == string(os.PathSeparator) { + return true, true + } + mounted, err = mountedByStatx(path) + return mounted, err == nil +} + // mountedByOpenat2 is a method of detecting a mount that works for all kinds // of mounts (incl. bind mounts), but requires a recent (v5.6+) linux kernel. func mountedByOpenat2(path string) (bool, error) { @@ -87,6 +135,11 @@ func mountedFast(path string) (mounted, sure bool, err error) { } func mounted(path string) (bool, error) { + // Fastest path: statx, which does not need a normalized path. + if mounted, ok := tryMountedByStatx(path); ok { + return mounted, nil + } + path, err := normalizePath(path) if err != nil { return false, err diff --git a/mountinfo/mounted_linux_test.go b/mountinfo/mounted_linux_test.go index 50667d8a..ac7a5fa5 100644 --- a/mountinfo/mounted_linux_test.go +++ b/mountinfo/mounted_linux_test.go @@ -267,6 +267,18 @@ func requireOpenat2(t *testing.T) { } } +func requireStatx(t *testing.T) { + t.Helper() + if err := tryStatx(); err != nil { + t.Skipf("statx: %v (old kernel? need Linux 5.8+)", err) + } +} + +func tryStatx() error { + _, err := mountedByStatx("/") + return err +} + func tryOpenat2() error { fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{Flags: unix.O_RDONLY}) if err == nil { @@ -275,7 +287,7 @@ func tryOpenat2() error { return err } -func testMountedFast(t *testing.T, path string, tc *testMount, openat2Supported bool) { +func testMountedFast(t *testing.T, path string, tc *testMount, fastSupported bool) { mounted, sure, err := MountedFast(path) if err != nil { // Got an error; is it expected? @@ -295,7 +307,7 @@ func testMountedFast(t *testing.T, path string, tc *testMount, openat2Supported return } - if openat2Supported { + if fastSupported { if mounted != tc.isMount { t.Errorf("MountedFast: expected mounted to be %v, got %v", tc.isMount, mounted) } @@ -305,7 +317,7 @@ func testMountedFast(t *testing.T, path string, tc *testMount, openat2Supported } if tc.isBind { - // For bind mounts, in case openat2 is not supported, + // For bind mounts, in case neither statx nor openat2 is supported, // sure and mounted must be false. if sure { t.Error("MountedFast: expected sure to be false for a bind mount") @@ -328,14 +340,19 @@ func testMountedFast(t *testing.T, path string, tc *testMount, openat2Supported func TestMountedBy(t *testing.T) { checked := false - openat2Supported := false + // Whether MountedFast can reliably detect bind mounts. + fastSupported := false // List of individual implementations to check. toCheck := []func(string) (bool, error){mountedByMountinfo, mountedByStat} if tryOpenat2() == nil { - openat2Supported = true + fastSupported = true toCheck = append(toCheck, mountedByOpenat2) } + if tryStatx() == nil { + fastSupported = true + toCheck = append(toCheck, mountedByStatx) + } for _, tc := range testMounts { tc := tc @@ -360,7 +377,7 @@ func TestMountedBy(t *testing.T) { } // Check the public MountedFast() function as a whole. - testMountedFast(t, m, &tc, openat2Supported) + testMountedFast(t, m, &tc, fastSupported) // Check individual mountedBy* implementations. @@ -403,7 +420,15 @@ func TestMountedBy(t *testing.T) { func TestMountedByOpenat2VsMountinfo(t *testing.T) { requireOpenat2(t) + testMountedByVsMountinfo(t, "mountedByOpenat2", mountedByOpenat2) +} + +func TestMountedByStatxVsMountinfo(t *testing.T) { + requireStatx(t) + testMountedByVsMountinfo(t, "mountedByStatx", mountedByStatx) +} +func testMountedByVsMountinfo(t *testing.T, name string, fn func(string) (bool, error)) { mounts, err := GetMounts(nil) if err != nil { t.Fatalf("GetMounts error: %v", err) @@ -416,13 +441,13 @@ func TestMountedByOpenat2VsMountinfo(t *testing.T) { // (this special case is handled by Mounted). continue } - mounted, err := mountedByOpenat2(m) + mounted, err := fn(m) if err != nil { if !errors.Is(err, os.ErrPermission) { - t.Errorf("mountedByOpenat2(%q) error: %+v", m, err) + t.Errorf("%s(%q) error: %+v", name, m, err) } } else if !mounted { - t.Errorf("mountedByOpenat2(%q): expected true, got false", m) + t.Errorf("%s(%q): expected true, got false", name, m) } } }