diff --git a/unix/syscall_linux.go b/unix/syscall_linux.go index 558f07b5..c302f01b 100644 --- a/unix/syscall_linux.go +++ b/unix/syscall_linux.go @@ -1675,6 +1675,69 @@ type fileHandle struct { 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 3c3bd816..9962fa1f 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 @@ func TestClockNanosleep(t *testing.T) { 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") +}