unix: add FileHandle, NewFileHandle, NameToHandleAt, OpenByHandleAt
This adds wrappers around name_to_handle_at and open_by_handle_at.
Requires root (or CAP_DAC_READ_SEARCH, rather) to run tests, which at
least some of our builders have.
bradfitz@go:~/src/golang.org/x/sys/unix$ go test -c && sudo ./unix.test -test.run=OpenBy -test.v=true
=== RUN TestOpenByHandleAt
=== RUN TestOpenByHandleAt/clone=false
=== RUN TestOpenByHandleAt/clone=true
--- PASS: TestOpenByHandleAt (0.00s)
syscall_linux_test.go:546: mountID: 22, handle: size=8, type=1, bytes="\x9e\x1e\b\x00~\x8c\xe5\x9d"
--- PASS: TestOpenByHandleAt/clone=false (0.00s)
syscall_linux_test.go:568: opened fd 3
--- PASS: TestOpenByHandleAt/clone=true (0.00s)
syscall_linux_test.go:568: opened fd 3
PASS
Fixes golang/go#30537
Change-Id: Ia48a8faab2fee665d88a16d81a3a0c1504b129ce
Reviewed-on: https://go-review.googlesource.com/c/sys/+/173357
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/unix/syscall_linux.go b/unix/syscall_linux.go
index 558f07b..c302f01 100644
--- a/unix/syscall_linux.go
+++ b/unix/syscall_linux.go
@@ -1675,6 +1675,69 @@
Type int32
}
+// FileHandle represents the C struct file_handle used by
+// name_to_handle_at (see NameToHandleAt) and open_by_handle_at (see
+// OpenByHandleAt).
+type FileHandle struct {
+ *fileHandle
+}
+
+// NewFileHandle constructs a FileHandle.
+func NewFileHandle(handleType int32, handle []byte) FileHandle {
+ const hdrSize = unsafe.Sizeof(fileHandle{})
+ buf := make([]byte, hdrSize+uintptr(len(handle)))
+ copy(buf[hdrSize:], handle)
+ fh := (*fileHandle)(unsafe.Pointer(&buf[0]))
+ fh.Type = handleType
+ fh.Bytes = uint32(len(handle))
+ return FileHandle{fh}
+}
+
+func (fh *FileHandle) Size() int { return int(fh.fileHandle.Bytes) }
+func (fh *FileHandle) Type() int32 { return fh.fileHandle.Type }
+func (fh *FileHandle) Bytes() []byte {
+ n := fh.Size()
+ if n == 0 {
+ return nil
+ }
+ return (*[1 << 30]byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&fh.fileHandle.Type)) + 4))[:n:n]
+}
+
+// NameToHandleAt wraps the name_to_handle_at system call; it obtains
+// a handle for a path name.
+func NameToHandleAt(dirfd int, path string, flags int) (handle FileHandle, mountID int, err error) {
+ var mid _C_int
+ // Try first with a small buffer, assuming the handle will
+ // only be 32 bytes.
+ size := uint32(32 + unsafe.Sizeof(fileHandle{}))
+ didResize := false
+ for {
+ buf := make([]byte, size)
+ fh := (*fileHandle)(unsafe.Pointer(&buf[0]))
+ fh.Bytes = size - uint32(unsafe.Sizeof(fileHandle{}))
+ err = nameToHandleAt(dirfd, path, fh, &mid, flags)
+ if err == EOVERFLOW {
+ if didResize {
+ // We shouldn't need to resize more than once
+ return
+ }
+ didResize = true
+ size = fh.Bytes + uint32(unsafe.Sizeof(fileHandle{}))
+ continue
+ }
+ if err != nil {
+ return
+ }
+ return FileHandle{fh}, int(mid), nil
+ }
+}
+
+// OpenByHandleAt wraps the open_by_handle_at system call; it opens a
+// file via a handle as previously returned by NameToHandleAt.
+func OpenByHandleAt(mountFD int, handle FileHandle, flags int) (fd int, err error) {
+ return openByHandleAt(mountFD, handle.fileHandle, flags)
+}
+
/*
* Unimplemented
*/
diff --git a/unix/syscall_linux_test.go b/unix/syscall_linux_test.go
index 3c3bd81..9962fa1 100644
--- a/unix/syscall_linux_test.go
+++ b/unix/syscall_linux_test.go
@@ -7,10 +7,16 @@
package unix_test
import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
"io/ioutil"
"os"
"runtime"
"runtime/debug"
+ "strconv"
+ "strings"
"testing"
"time"
@@ -531,3 +537,72 @@
t.Errorf("ClockNanosleep(CLOCK_THREAD_CPUTIME_ID, 0, %#v, nil) = %v, want EINVAL or EOPNOTSUPP", &rel, err)
}
}
+
+func TestOpenByHandleAt(t *testing.T) {
+ h, mountID, err := unix.NameToHandleAt(unix.AT_FDCWD, "syscall_linux_test.go", 0)
+ if err != nil {
+ t.Fatalf("NameToHandleAt: %v", err)
+ }
+ t.Logf("mountID: %v, handle: size=%d, type=%d, bytes=%q", mountID,
+ h.Size(), h.Type(), h.Bytes())
+ mount, err := openMountByID(mountID)
+ if err != nil {
+ t.Fatalf("openMountByID: %v", err)
+ }
+ defer mount.Close()
+
+ for _, clone := range []bool{false, true} {
+ t.Run("clone="+strconv.FormatBool(clone), func(t *testing.T) {
+ if clone {
+ h = unix.NewFileHandle(h.Type(), h.Bytes())
+ }
+ fd, err := unix.OpenByHandleAt(int(mount.Fd()), h, unix.O_RDONLY)
+ if err == unix.EPERM {
+ t.Skipf("skipping OpenByHandleAt without CAP_DAC_READ_SEARCH")
+ }
+ if err == unix.ENOSYS {
+ t.Skipf("name_to_handle_at system call not available")
+ }
+ if err == unix.EOPNOTSUPP {
+ t.Skipf("name_to_handle_at not supported on this filesystem")
+ }
+ if err != nil {
+ t.Fatalf("OpenByHandleAt: %v", err)
+ }
+ defer unix.Close(fd)
+
+ t.Logf("opened fd %v", fd)
+ f := os.NewFile(uintptr(fd), "")
+ slurp, err := ioutil.ReadAll(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ const substr = "Some substring for a test."
+ if !strings.Contains(string(slurp), substr) {
+ t.Errorf("didn't find substring %q in opened file; read %d bytes", substr, len(slurp))
+ }
+ })
+ }
+}
+
+func openMountByID(mountID int) (f *os.File, err error) {
+ mi, err := os.Open("/proc/self/mountinfo")
+ if err != nil {
+ return nil, err
+ }
+ defer mi.Close()
+ bs := bufio.NewScanner(mi)
+ wantPrefix := []byte(fmt.Sprintf("%v ", mountID))
+ for bs.Scan() {
+ if !bytes.HasPrefix(bs.Bytes(), wantPrefix) {
+ continue
+ }
+ fields := strings.Fields(bs.Text())
+ dev := fields[4]
+ return os.Open(dev)
+ }
+ if err := bs.Err(); err != nil {
+ return nil, err
+ }
+ return nil, errors.New("mountID not found")
+}