Skip to content
Open
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
59 changes: 56 additions & 3 deletions mountinfo/mounted_linux.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mountinfo

import (
"errors"
"os"
"path/filepath"

Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
43 changes: 34 additions & 9 deletions mountinfo/mounted_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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?
Expand All @@ -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)
}
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
}
Expand Down
Loading