From 03950d30392ab30d0bc79e764bb5b11634ab9dd7 Mon Sep 17 00:00:00 2001 From: Nils Wireklint Date: Mon, 4 Sep 2023 11:40:05 +0200 Subject: [PATCH] Add mount functionality to 'Directory' This is needed for 'chroot' runners that must mount the special filesystems '/proc' and '/sys' in the input root. --- pkg/filesystem/BUILD.bazel | 1 + pkg/filesystem/directory.go | 9 ++ pkg/filesystem/local_directory_unix.go | 53 ++++++++++ pkg/filesystem/mount_unix.go | 130 +++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 pkg/filesystem/mount_unix.go diff --git a/pkg/filesystem/BUILD.bazel b/pkg/filesystem/BUILD.bazel index 868897e7..9694b3a3 100644 --- a/pkg/filesystem/BUILD.bazel +++ b/pkg/filesystem/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "local_directory_linux.go", "local_directory_unix.go", "local_directory_windows.go", + "mount_unix.go", ], importpath = "github.com/buildbarn/bb-storage/pkg/filesystem", visibility = ["//visibility:public"], diff --git a/pkg/filesystem/directory.go b/pkg/filesystem/directory.go index 030861b3..0758128a 100644 --- a/pkg/filesystem/directory.go +++ b/pkg/filesystem/directory.go @@ -3,6 +3,7 @@ package filesystem import ( "io" "os" + "sync" "time" "github.com/buildbarn/bb-storage/pkg/filesystem/path" @@ -112,6 +113,14 @@ type Directory interface { // Function that base types may use to implement calls that // require double dispatching, such as hardlinking and renaming. Apply(arg interface{}) error + + // Mount and Unmount. + // This uses `Mountat` functionality, but not `Unmountat`. + // see https://github.com/buildbarn/bb-remote-execution/issues/115 for information. + Mount(mountpoint path.Component, source string, fstype string, options int) error + // The Unmount call changes the working directory internally + // and must use a program-scope mutex to isolate this side effect. + Unmount(lock *sync.Mutex, mountpoint path.Component) error } // DirectoryCloser is a Directory handle that can be released. diff --git a/pkg/filesystem/local_directory_unix.go b/pkg/filesystem/local_directory_unix.go index 6fb3c609..015f34e1 100644 --- a/pkg/filesystem/local_directory_unix.go +++ b/pkg/filesystem/local_directory_unix.go @@ -4,7 +4,9 @@ package filesystem import ( + "fmt" "io" + "io/fs" "os" "runtime" "sort" @@ -17,6 +19,7 @@ import ( "golang.org/x/sys/unix" "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type localDirectory struct { @@ -460,3 +463,53 @@ func (d *localDirectory) Apply(arg interface{}) error { return syscall.EXDEV } } + +func (d *localDirectory) Mount(mountpoint path.Component, source string, fstype string, options int) error { + if options != 0 { + return status.Error(codes.InvalidArgument, "Options are not yet supported in `mountat`.") + } + mfd, err := mountat(d.fd, fstype, source, mountpoint.String()) + if err != nil { + return util.StatusWrap(err, "Mountat") + } + defer unix.Close(mfd) + + return nil +} + +// Fchdir changes the current working directory to the directory file descriptor. +// If there is an error, it will be of type *PathError. +func Fchdir(dfd int) error { + if e := syscall.Fchdir(dfd); e != nil { + return &fs.PathError{Op: "fchdir", Path: fmt.Sprintf("%d", dfd), Err: e} + } + return nil +} + +// Uses `unmount` on relative pathnames with `fchdir`. +func fchdir_unmountat(lock *sync.Mutex, dfd int, mountname string) error { + lock.Lock() + defer lock.Unlock() + + store, err := os.Getwd() + if err != nil { + return util.StatusWrap(err, "Could not store the working directory %#v.") + } + defer os.Chdir(store) + + err = Fchdir(dfd) + if err != nil { + return util.StatusWrapf(err, "Could not change working directory to %#v.", dfd) + } + + err = unix.Unmount(mountname, 0) + if err != nil { + return util.StatusWrap(err, "Unmount") + } + + return nil +} + +func (d *localDirectory) Unmount(lock *sync.Mutex, mountpoint path.Component) error { + return fchdir_unmountat(lock, d.fd, mountpoint.String()) +} diff --git a/pkg/filesystem/mount_unix.go b/pkg/filesystem/mount_unix.go new file mode 100644 index 00000000..9d2f7ae4 --- /dev/null +++ b/pkg/filesystem/mount_unix.go @@ -0,0 +1,130 @@ +//go:build darwin || freebsd || linux +// +build darwin freebsd linux + +package filesystem + +import ( + "errors" + "fmt" + "unsafe" + + "github.com/buildbarn/bb-storage/pkg/util" + + "golang.org/x/sys/unix" +) + +const ( + FSCONFIG_SET_FLAG = 0 + FSCONFIG_SET_STRING = 1 + FSCONFIG_SET_BINARY = 2 + FSCONFIG_SET_PATH = 3 + FSCONFIG_SET_PATH_EMPTY = 4 + FSCONFIG_SET_FD = 5 + FSCONFIG_CMD_CREATE = 6 + FSCONFIG_CMD_RECONFIGURE = 7 +) + +// Rudimentary function wrapper for the `fsconfig` syscall. +// +// This is implemented as a stop gap solution until real support is merged into the `unix` library. +// See this patchset: https://go-review.googlesource.com/c/sys/+/399995/ . +// This only implements the two commands needed for basic `mountat` functionality. +// And will just exit if any other command is called. +// +// TODO(nils): construct proper `syscall.E*` errors. +// like `unix.errnoErr`, but the function is not exported. +func fsconfig(fsfd int, cmd int, key string, value string, flags int) (err error) { + switch cmd { + case FSCONFIG_SET_STRING: + if len(key) == 0 || len(value) == 0 { + err = errors.New("`key` and `value` must be provided") + return + } + case FSCONFIG_CMD_CREATE: + if len(key) != 0 || len(value) != 0 { + err = errors.New("`key` and `value` must be empty") + return + } + default: + err = errors.New("not implemented: " + fmt.Sprintf("%d", cmd)) + return + } + + var _p0 *byte + var _p1 *byte + + _p0, err = unix.BytePtrFromString(key) + if err != nil { + return + } + if key == "" { + _p0 = nil + } + + _p1, err = unix.BytePtrFromString(value) + if err != nil { + return + } + if value == "" { + _p1 = nil + } + + r0, _, e1 := unix.Syscall6( + unix.SYS_FSCONFIG, + uintptr(fsfd), + uintptr(cmd), + uintptr(unsafe.Pointer(_p0)), + uintptr(unsafe.Pointer(_p1)), + uintptr(flags), + 0, + ) + ret := int(r0) + if e1 != 0 { + err = e1 + return + } + if ret < 0 { + err = errors.New("negative return code, not converted to an error in `Syscall`: " + fmt.Sprintf("%d", ret)) + return + } + + return +} + +// Mounts the `source` filesystem on `mountname` inside a directory +// given as `dfd` file descriptor, +// using the `fstype` filesystem type. +// This returns a file descriptor to the mount object, +// that can be used to move it again. +// Remember to close it before unmounting, +// or unmount will fail with EBUSY. +// +// TODO: Options cannot be sent to the syscalls. +func mountat(dfd int, fstype, source, mountname string) (int, error) { + + fd, err := unix.Fsopen(fstype, unix.FSOPEN_CLOEXEC) + if err != nil { + return -1, util.StatusWrapf(err, "Fsopen '%s'", fstype) + } + + err = fsconfig(fd, FSCONFIG_SET_STRING, "source", source, 0) + if err != nil { + return -1, util.StatusWrapf(err, "Fsconfig source '%s'", source) + } + + err = fsconfig(fd, FSCONFIG_CMD_CREATE, "", "", 0) + if err != nil { + return -1, util.StatusWrap(err, "Fsconfig create") + } + + mfd, err := unix.Fsmount(fd, unix.FSMOUNT_CLOEXEC, unix.MS_NOEXEC) + if err != nil { + return -1, util.StatusWrap(err, "Fsmount") + } + err = unix.MoveMount(mfd, "", dfd, mountname, unix.MOVE_MOUNT_F_EMPTY_PATH) + if err != nil { + return -1, util.StatusWrapf(err, "Movemount mountname '%s' in file descriptor %d", mountname, dfd) + } + + return mfd, nil +}