diff --git a/unix/linux/types.go b/unix/linux/types.go index 308d65a1f..29c80934d 100644 --- a/unix/linux/types.go +++ b/unix/linux/types.go @@ -1047,6 +1047,10 @@ const ( AT_STATX_FORCE_SYNC = C.AT_STATX_FORCE_SYNC AT_STATX_DONT_SYNC = C.AT_STATX_DONT_SYNC + AT_HANDLE_FID = C.AT_HANDLE_FID + AT_HANDLE_MNT_ID_UNIQUE = C.AT_HANDLE_MNT_ID_UNIQUE + AT_HANDLE_CONNECTABLE = C.AT_HANDLE_CONNECTABLE + AT_RECURSIVE = C.AT_RECURSIVE AT_SYMLINK_FOLLOW = C.AT_SYMLINK_FOLLOW diff --git a/unix/syscall_linux.go b/unix/syscall_linux.go index f7b82bcca..3a5df78b9 100644 --- a/unix/syscall_linux.go +++ b/unix/syscall_linux.go @@ -2449,8 +2449,28 @@ func (fh *FileHandle) Bytes() []byte { // NameToHandleAt wraps the name_to_handle_at system call; it obtains // a handle for a path name. +// +// Deprecated: This method can only return int-sized mount IDs and will thus +// silently ignore [AT_HANDLE_MNT_ID_UNIQUE]. Use [NameToHandleAt64] instead. func NameToHandleAt(dirfd int, path string, flags int) (handle FileHandle, mountID int, err error) { - var mid _C_int + flags &= ^AT_HANDLE_MNT_ID_UNIQUE + handle, mid, err := NameToHandleAt64(dirfd, path, flags) + return handle, int(mid), err +} + +// NameToHandleAt64 is effectively identical to [NameToHandleAt] except that it +// supports returning 64-bit mount IDs with [AT_HANDLE_MNT_ID_UNIQUE]. +func NameToHandleAt64(dirfd int, path string, flags int) (handle FileHandle, mountID uint64, err error) { + uniqueMntId := flags&AT_HANDLE_MNT_ID_UNIQUE != 0 + var ( + midInt _C_int + mid64 uint64 + ) + midPtr := &midInt + if uniqueMntId { + midPtr = (*_C_int)(unsafe.Pointer(&mid64)) + } + // Try first with a small buffer, assuming the handle will // only be 32 bytes. size := uint32(32 + unsafe.Sizeof(fileHandle{})) @@ -2459,7 +2479,7 @@ func NameToHandleAt(dirfd int, path string, flags int) (handle FileHandle, mount buf := make([]byte, size) fh := (*fileHandle)(unsafe.Pointer(&buf[0])) fh.Bytes = size - uint32(unsafe.Sizeof(fileHandle{})) - err = nameToHandleAt(dirfd, path, fh, &mid, flags) + err = nameToHandleAt(dirfd, path, fh, midPtr, flags) if err == EOVERFLOW { if didResize { // We shouldn't need to resize more than once @@ -2472,7 +2492,11 @@ func NameToHandleAt(dirfd int, path string, flags int) (handle FileHandle, mount if err != nil { return } - return FileHandle{fh}, int(mid), nil + mountID = uint64(midInt) + if uniqueMntId { + mountID = mid64 + } + return FileHandle{fh}, mountID, nil } } diff --git a/unix/syscall_linux_test.go b/unix/syscall_linux_test.go index 99332eace..b6d23f1d5 100644 --- a/unix/syscall_linux_test.go +++ b/unix/syscall_linux_test.go @@ -913,7 +913,7 @@ func TestClockNanosleep(t *testing.T) { } func TestOpenByHandleAt(t *testing.T) { - skipIfNotSupported := func(t *testing.T, name string, err error) { + skipIfNotSupported := func(t *testing.T, name, flags string, err error) { if err == unix.EPERM { t.Skipf("skipping %s test without CAP_DAC_READ_SEARCH", name) } @@ -923,15 +923,18 @@ func TestOpenByHandleAt(t *testing.T) { if err == unix.EOPNOTSUPP { t.Skipf("%s not supported on this filesystem", name) } + if err == unix.EINVAL { + t.Skipf("%s flags %s are not supported", name, flags) + } } h, mountID, err := unix.NameToHandleAt(unix.AT_FDCWD, "syscall_linux_test.go", 0) if err != nil { - skipIfNotSupported(t, "name_to_handle_at", err) + skipIfNotSupported(t, "name_to_handle_at", "", err) t.Fatalf("NameToHandleAt: %v", err) } - t.Logf("mountID: %v, handle: size=%d, type=%d, bytes=%q", mountID, - h.Size(), h.Type(), h.Bytes()) + t.Logf("mountID: %v (%#x), handle: size=%d, type=%d, bytes=%q", + mountID, mountID, h.Size(), h.Type(), h.Bytes()) mount, err := openMountByID(mountID) if err != nil { t.Fatalf("openMountByID: %v", err) @@ -944,8 +947,8 @@ func TestOpenByHandleAt(t *testing.T) { h = unix.NewFileHandle(h.Type(), h.Bytes()) } fd, err := unix.OpenByHandleAt(int(mount.Fd()), h, unix.O_RDONLY) - skipIfNotSupported(t, "open_by_handle_at", err) if err != nil { + skipIfNotSupported(t, "open_by_handle_at", "O_RDONLY", err) t.Fatalf("OpenByHandleAt: %v", err) } t.Logf("opened fd %v", fd) @@ -962,6 +965,86 @@ func TestOpenByHandleAt(t *testing.T) { } }) } + + t.Run("AT_HANDLE_MNT_ID_UNIQUE", func(t *testing.T) { + // AT_HANDLE_MNT_ID_UNIQUE needs to be implicitly cleared as it would + // produce unusable information (and on pre-AT_HANDLE_MNT_ID_UNIQUE + // implementations, possible stack corruption). + h, mountID, err := unix.NameToHandleAt(unix.AT_FDCWD, ".", unix.AT_HANDLE_MNT_ID_UNIQUE) + if err != nil { + skipIfNotSupported(t, "name_to_handle_at", "AT_HANDLE_MNT_ID_UNIQUE", err) + t.Fatalf("NameToHandleAt: %v", err) + } + t.Logf("mountID: %v (%#x), handle: size=%d, type=%d, bytes=%q", + mountID, mountID, h.Size(), h.Type(), h.Bytes()) + if uint64(mountID) >= 1<<31 { + t.Errorf("classic NameToHandleAt must return classic mount id, but got %d (%#x) which is larger than 2^31 (%#x)", mountID, mountID, 1<<31) + } + }) +} + +func TestNameToHandleAt64(t *testing.T) { + skipIfNotSupported := func(t *testing.T, name, flags string, err error) { + if err == unix.EPERM { + t.Skipf("skipping %s test without CAP_DAC_READ_SEARCH", name) + } + if err == unix.ENOSYS { + t.Skipf("%s system call not available", name) + } + if err == unix.EOPNOTSUPP { + t.Skipf("%s not supported on this filesystem", name) + } + if err == unix.EINVAL { + t.Skipf("%s flags %s are not supported", name, flags) + } + } + t.Run("0", func(t *testing.T) { + h, mountID, err := unix.NameToHandleAt64(unix.AT_FDCWD, ".", 0) + if err != nil { + skipIfNotSupported(t, "name_to_handle_at", "", err) + t.Fatalf("NameToHandleAt64: %v", err) + } + t.Logf("mountID: %v (%#x), handle: size=%d, type=%d, bytes=%q", + mountID, mountID, h.Size(), h.Type(), h.Bytes()) + if mountID >= 1<<31 { + t.Errorf("classic mount id %d (%#x) must not be larger than 2^31 (%#x)", mountID, mountID, 1<<31) + } + if len(h.Bytes()) != h.Size() { + t.Errorf("file handle bytes should match expected length %d but got %d", h.Size(), len(h.Bytes())) + } + }) + t.Run("AT_HANDLE_FID", func(t *testing.T) { + h, mountID, err := unix.NameToHandleAt64(unix.AT_FDCWD, ".", unix.AT_HANDLE_FID) + if err != nil { + // EOPNOTSUPP is not valid for AT_HANDLE_FID. + if err != unix.EOPNOTSUPP { + skipIfNotSupported(t, "name_to_handle_at", "AT_HANDLE_FID", err) + } + t.Fatalf("NameToHandleAt64: %v", err) + } + t.Logf("mountID: %d (%#x) handle: size=%d type=%d bytes=%q", + mountID, mountID, h.Size(), h.Type(), h.Bytes()) + if len(h.Bytes()) != h.Size() { + t.Errorf("file handle bytes should match expected length %d but got %d", h.Size(), len(h.Bytes())) + } + if mountID >= 1<<31 { + t.Fatalf("classic mount id %d (%#x) must not be larger than 2^31 (%#x)", mountID, mountID, 1<<31) + } + }) + t.Run("AT_HANDLE_MNT_ID_UNIQUE", func(t *testing.T) { + h, mountID, err := unix.NameToHandleAt64(unix.AT_FDCWD, ".", unix.AT_HANDLE_FID|unix.AT_HANDLE_MNT_ID_UNIQUE) + if err != nil { + t.Skip("name_to_handle_at(AT_HANDLE_FID):", err) + } + t.Logf("mountID: %d (%#x) handle: size=%d type=%d bytes=%q", + mountID, mountID, h.Size(), h.Type(), h.Bytes()) + if len(h.Bytes()) != h.Size() { + t.Errorf("file handle bytes should match expected length %d but got %d", h.Size(), len(h.Bytes())) + } + if mountID < 1<<31 { + t.Fatalf("unique mount id %d (%#x) must be at least 2^31 (%#x)", mountID, mountID, 1<<31) + } + }) } func openMountByID(mountID int) (f *os.File, err error) { diff --git a/unix/ztypes_linux.go b/unix/ztypes_linux.go index 45476a73c..36db8ba49 100644 --- a/unix/ztypes_linux.go +++ b/unix/ztypes_linux.go @@ -876,6 +876,10 @@ const ( AT_STATX_FORCE_SYNC = 0x2000 AT_STATX_DONT_SYNC = 0x4000 + AT_HANDLE_FID = 0x200 + AT_HANDLE_MNT_ID_UNIQUE = 0x1 + AT_HANDLE_CONNECTABLE = 0x2 + AT_RECURSIVE = 0x8000 AT_SYMLINK_FOLLOW = 0x400