Skip to content

Commit

Permalink
unix: add FileHandle, NewFileHandle, NameToHandleAt, OpenByHandleAt
Browse files Browse the repository at this point in the history
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 <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
Reviewed-by: Ian Lance Taylor <[email protected]>
  • Loading branch information
bradfitz committed Apr 25, 2019
1 parent 18eb32c commit 9f0b1ff
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 0 deletions.
63 changes: 63 additions & 0 deletions unix/syscall_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
75 changes: 75 additions & 0 deletions unix/syscall_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
package unix_test

import (
"bufio"
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"runtime"
"runtime/debug"
"strconv"
"strings"
"testing"
"time"

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

0 comments on commit 9f0b1ff

Please sign in to comment.