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")
+}