Files
Ayush RanjanandAlessio Ricci Toniolo 099d05e30a runsc: wire up the RDMA uverbs device proxy
Register a proxied uverbs char device for each /dev/infiniband/uverbs* device
listed in the spec when --rdmaproxy is set. Devices are registered at their
host major:minor (from the spec), so the guest device node, its /sys .../dev
entry, and the VFS registration all agree without any renumbering. The vendor
driver plug-in is selected per device from the PCI driver name in the host
sysfs snapshot; the mlx5 (cxproxy) plug-in is linked in via a blank import.

Install the rdmaproxy seccomp filters (a new Options.RDMAProxy in the boot
filter config, expanded in the precompiled set) whenever RDMA is enabled, and
donate the uverbs device FDs through the dev gofer (ShouldExposeRDMADevice in
SetupDev, and shouldCreateDeviceGofer). GPUDirect RDMA additionally needs the
privileged CapRDMA nvproxy capability to export GPU memory to a dma-buf fd;
since there is no container-facing flag for it, enable it implicitly when
--rdmaproxy is set.

Co-authored-by: Alessio Ricci Toniolo <[email protected]>
2026-08-02 23:31:52 +00:00

587 lines
21 KiB
Go

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sandboxsetup
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/unet"
"gvisor.dev/gvisor/runsc/cmd/util"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/specutils"
)
// ProcFDBindMount is the path where /proc/self/fd is bind-mounted inside the
// gofer's mount tree. Redhat distros don't allow bind-mounts in /proc/self
// directories due to SELinux rules.
const ProcFDBindMount = "/proc/fs"
// vfioPathDir is the directory containing VFIO device nodes.
const vfioPathDir = "/dev/vfio"
// MountOpener is used in two cases:
//
// (1) when the gofer process cannot access a mount source directly (e.g.
// due to permission restrictions in a user namespace). In this case, the
// returned file is just the opened mount source.
//
// (2) when the mount is id-mapped, which the gofer process cannot configure
// on its own. In this case, the returned file is an open_tree() detached mount
// point which has had the requested id mapping configured using mount_setattr().
//
// The caller is responsible for closing the returned file.
type MountOpener func(m *specs.Mount, flags uint32) (*os.File, error)
// NewSocket creates a unet.Socket from a file descriptor.
// It fatally exits if the socket cannot be created.
func NewSocket(ioFD int) *unet.Socket {
socket, err := unet.NewSocket(ioFD)
if err != nil {
util.Fatalf("creating server on FD %d: %v", ioFD, err)
}
return socket
}
// WriteMounts serializes the given mounts as JSON and writes them to the
// given file descriptor.
func WriteMounts(mountsFD int, mounts []specs.Mount) error {
bytes, err := json.Marshal(mounts)
if err != nil {
return err
}
f := os.NewFile(uintptr(mountsFD), "mounts file")
defer f.Close()
for written := 0; written < len(bytes); {
w, err := f.Write(bytes[written:])
if err != nil {
return err
}
written += w
}
return nil
}
// SetupRootFS prepares the filesystem the gofer will serve to the sandbox.
// There are some gVisor-specific quirks:
// - The gofer runs with minimal capabilities (see runsc/cmd/gofer.go).
// However, gofer startup requires more capabilities (for example,
// pivot_root(2) requires CAP_SYS_ADMIN).
// - As a result, the gofer sets up the container rootfs, does pivot_root(2),
// and then re-executes itself while dropping extra capabilities.
// - To re-execute itself, it requires access to `/proc/self/exe` after
// pivot_root(2).
//
// For this reason, we can't just pivot_root(2) into the container rootfs path
// (i.e. spec.Root.Path), as it would mean bind-mounting host /proc inside
// spec.Root.Path, which might be a read-only filesystem.
//
// Furthermore, createContainer hooks from the OCI spec need to run before the
// pivot_root(2) and expect the container rootfs to be prepared at
// spec.Root.Path with all the bind-mounts in place.
//
// To satisfy all of these requirements, this is the approach we take:
// 1. We prepare all the bind-mounts in `spec.Root.Path` and execute the
// createContainer hooks.
// 2. We create a new tmpfs mount at /proc/fs.
// 3. We bind-mount host /proc and spec.Root.Path onto /proc/fs/proc and
// /proc/fs/root, respectively.
// 4. We then pivot_root(2) into /proc/fs. Now host procfs is accessible via
// /proc/ and container rootfs is accessible via /root.
// 5. We re-exec the gofer binary and drop extra capabilities.
// 6. We unmount the /proc bind mount.
// 7. We chroot(2) into /root (note that the gofer runs with CAP_SYS_CHROOT).
//
// This function does steps 1-4.
//
// mountConfs must be indexed such that mountConfs[0] is the root filesystem
// configuration and subsequent entries correspond to spec mounts with
// mount configs.
func SetupRootFS(spec *specs.Spec, conf *config.Config, mountConfs []specutils.GoferMountConf, devIoFD int, mountOpener MountOpener, containerID string, bundleDir string) error {
// Convert all shared mounts into slaves to be sure that nothing will be
// propagated outside of our namespace.
procPath := "/proc"
if err := specutils.SafeMount("", "/", "", unix.MS_SLAVE|unix.MS_REC, "", procPath); err != nil {
util.Fatalf("error converting mounts: %v", err)
}
goferRootFs := spec.Root.Path
containerRootFs := spec.Root.Path
if !conf.TestOnlyAllowRunAsCurrentUserWithoutChroot {
// runsc can't be re-executed without /proc, so we create a tmpfs mount,
// mount ./proc and ./root there, then move this mount to the root and after
// setCapsAndCallSelf, runsc will chroot into /root.
//
// We need a directory to construct a new root and we know that
// runsc can't start without /proc, so we can use it for this.
flags := uintptr(unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC)
if err := specutils.SafeMount("runsc-root", "/proc/fs", "tmpfs", flags, "", procPath); err != nil {
util.Fatalf("error mounting tmpfs: %v", err)
}
if err := unix.Mount("", "/proc/fs", "", unix.MS_UNBINDABLE, ""); err != nil {
util.Fatalf("error setting MS_UNBINDABLE")
}
// Prepare tree structure for pivot_root(2).
if err := os.Mkdir("/proc/fs/proc", 0755); err != nil {
util.Fatalf("error creating /proc/fs/proc: %v", err)
}
if err := os.Mkdir("/proc/fs/root", 0755); err != nil {
util.Fatalf("error creating /proc/fs/root: %v", err)
}
if err := os.Mkdir("/proc/fs/etc", 0755); err != nil {
util.Fatalf("error creating /proc/fs/etc: %v", err)
}
// This cannot use SafeMount because there's no available procfs. But we
// know that /proc/fs is an empty tmpfs mount, so this is safe.
if err := unix.Mount("/proc", "/proc/fs/proc", "", flags|unix.MS_RDONLY|unix.MS_BIND|unix.MS_REC, ""); err != nil {
util.Fatalf("error mounting /proc/fs/proc: %v", err)
}
// self/fd is bind-mounted, so that the FD returned by
// OpenProcSelfFD() does not allow escapes with walking ".." .
if err := unix.Mount("/proc/fs/proc/self/fd", "/proc/fs/"+ProcFDBindMount,
"", unix.MS_RDONLY|unix.MS_BIND|flags, ""); err != nil {
util.Fatalf("error mounting proc/self/fd: %v", err)
}
if err := CopyFile("/proc/fs/etc/localtime", "/etc/localtime"); err != nil {
log.Warningf("Failed to copy /etc/localtime: %v. UTC timezone will be used.", err)
}
goferRootFs = "/proc/fs/root"
procPath = "/proc/fs/proc"
}
rootfsConf := mountConfs[0]
if rootfsConf.ShouldUseLisafs() {
// Some CDI createContainer hooks pivot_root(2) into spec.Root.Path.
// pivot_root(2) requires the target to be a mount point, so we must
// self-bind-mount spec.Root.Path here. runc does this step unconditionally
// in libcontainer/rootfs_linux.go:prepareRoot().
if err := unix.Mount(containerRootFs, containerRootFs, "", unix.MS_BIND|unix.MS_REC, ""); err != nil {
return fmt.Errorf("self-bind-mounting rootfs %q: %w", containerRootFs, err)
}
// Ensure the containerRootFs is set to the RootfsPropagation that the user
// requested. Mounts added under SetupMounts inherit the propagation flags of
// the root.
flags := uint32(unix.MS_SLAVE | unix.MS_REC)
if spec.Linux != nil && spec.Linux.RootfsPropagation != "" {
flags = specutils.PropOptionsToFlags([]string{spec.Linux.RootfsPropagation})
}
if err := unix.Mount("", containerRootFs, "", uintptr(flags), ""); err != nil {
return fmt.Errorf("mounting root (%q) with flags: %#x, err: %v", containerRootFs, flags, err)
}
} else {
// When not using gofer mount for rootfs, spec.Root.Path may not be set or
// may not be a writable directory. So set up the container rootfs directly
// in /proc/fs/root, which is a writable directory.
//
// The self-bind-mount and propagation change above are skipped here: they
// are only needed for lisafs rootfs, and /proc/fs/root is not a mount point
// on the MS_UNBINDABLE /proc/fs tmpfs, so both would fail with EINVAL.
containerRootFs = goferRootFs
}
// Replace the current spec, with the clean spec with symlinks resolved.
if err := SetupMounts(conf, spec.Mounts, containerRootFs, procPath, mountConfs, mountOpener); err != nil {
util.Fatalf("error setting up FS: %v", err)
}
// Set up /dev directory if needed.
if devIoFD >= 0 {
if err := SetupDev(spec, conf, containerRootFs, procPath); err != nil {
util.Fatalf("error setting up /dev: %v", err)
}
}
if rootfsConf.ShouldUseLisafs() {
if spec.Hooks != nil && len(spec.Hooks.CreateContainer) > 0 {
state := specs.State{
Version: specs.Version,
ID: containerID,
Status: specs.StateCreating,
// The sandbox pid is not easily available at this point. We'll set
// it to -1 for now. A future improvement could plumb this value
// through to the gofer if it becomes necessary.
Pid: -1,
Bundle: bundleDir,
Annotations: spec.Annotations,
}
if err := specutils.ExecuteHooks(spec.Hooks.CreateContainer, state); err != nil {
util.Fatalf("error executing CreateContainer hooks: %v", err)
}
}
// Now that spec.Root.Path has been prepared, we can bind-mount it to the
// new root. This will make the container rootfs visible in the gofer root.
if err := specutils.SafeMount(containerRootFs, goferRootFs, "", unix.MS_BIND|unix.MS_REC, "", procPath); err != nil {
return fmt.Errorf("binding prepared rootfs to gofer root: %v", err)
}
// Check if root needs to be remounted as readonly.
if spec.Root.Readonly || rootfsConf.ShouldUseOverlayfs() {
// If root is a mount point but not read-only, we can change mount options
// to make it read-only for extra safety.
// unix.MS_NOSUID and unix.MS_NODEV are included here not only
// for safety reasons but also because they can be locked and
// any attempts to unset them will fail. See
// mount_namespaces(7) for more details.
log.Infof("Remounting root as readonly: %q", goferRootFs)
flags := uintptr(unix.MS_BIND | unix.MS_REMOUNT | unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV)
if err := specutils.SafeMount(goferRootFs, goferRootFs, "bind", flags, "", procPath); err != nil {
return fmt.Errorf("remounting root as read-only with source: %q, target: %q, flags: %#x, err: %v", goferRootFs, goferRootFs, flags, err)
}
}
} else if spec.Hooks != nil && len(spec.Hooks.CreateContainer) > 0 {
log.Warningf("CreateContainer hooks are not executed since container rootfs is not on lisafs")
}
if !conf.TestOnlyAllowRunAsCurrentUserWithoutChroot {
if err := PivotRoot("/proc/fs"); err != nil {
util.Fatalf("failed to change the root file system: %v", err)
}
if err := os.Chdir("/"); err != nil {
util.Fatalf("failed to change working directory")
}
}
return nil
}
func msFlagsToMountAttr(flags uint32) (attrSet uint64, attrClr uint64) {
if flags&unix.MS_RDONLY != 0 {
attrSet |= unix.MOUNT_ATTR_RDONLY
}
if flags&unix.MS_NOSUID != 0 {
attrSet |= unix.MOUNT_ATTR_NOSUID
}
if flags&unix.MS_NODEV != 0 {
attrSet |= unix.MOUNT_ATTR_NODEV
}
if flags&unix.MS_NOEXEC != 0 {
attrSet |= unix.MOUNT_ATTR_NOEXEC
}
if flags&unix.MS_NODIRATIME != 0 {
attrSet |= unix.MOUNT_ATTR_NODIRATIME
}
if flags&unix.MS_NOATIME != 0 {
attrSet |= unix.MOUNT_ATTR_NOATIME
attrClr |= unix.MOUNT_ATTR__ATIME
} else if flags&unix.MS_RELATIME != 0 {
attrSet |= unix.MOUNT_ATTR_RELATIME
attrClr |= unix.MOUNT_ATTR__ATIME
} else if flags&unix.MS_STRICTATIME != 0 {
attrSet |= unix.MOUNT_ATTR_STRICTATIME
attrClr |= unix.MOUNT_ATTR__ATIME
}
return attrSet, attrClr
}
func safeSetupAndMoveMount(srcFileFD int, src, dst, procPath string) error {
fi, err := os.Stat(src)
if err != nil {
return fmt.Errorf("stat(%q) failed: %v", src, err)
}
if fi.IsDir() {
if err := os.MkdirAll(dst, 0777); err != nil {
return fmt.Errorf("mkdir(%q) failed: %v", dst, err)
}
} else {
parent := filepath.Dir(dst)
if err := os.MkdirAll(parent, 0777); err != nil {
return fmt.Errorf("mkdir(%q) failed: %v", parent, err)
}
f, err := os.OpenFile(dst, unix.O_CREAT, 0777)
if err != nil {
return fmt.Errorf("open(%q) failed: %v", dst, err)
}
f.Close()
}
fd, err := unix.Open(dst, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
return fmt.Errorf("failed to safely move mount: Open(%s, _, _): %w", dst, err)
}
defer unix.Close(fd)
if procPath == "" {
procPath = "/proc"
}
fdPath := fmt.Sprintf("%s/self/fd/%d", procPath, fd)
target, err := os.Readlink(fdPath)
if err != nil {
return fmt.Errorf("failed to safely move mount: Readlink(%s): %w", fdPath, err)
}
if dst != target {
return fmt.Errorf("failed to safely move mount: expected to open %s, but found %s", dst, target)
}
if err := unix.MoveMount(srcFileFD, "", fd, "", unix.MOVE_MOUNT_F_EMPTY_PATH|unix.MOVE_MOUNT_T_EMPTY_PATH); err != nil {
return fmt.Errorf("MoveMount(%d, %q) failed: %w", srcFileFD, dst, err)
}
return nil
}
// SetupMounts bind-mounts all mounts specified in the spec in their correct
// location inside root. It resolves relative paths and symlinks, and creates
// directories as needed.
//
// mountConfs is indexed such that mountConfs[0] is the root filesystem
// configuration, and mount iteration starts at index 1.
// mountOpener is called when the gofer process cannot directly access a mount
// source. It may be nil if all mounts are directly accessible.
func SetupMounts(conf *config.Config, mounts []specs.Mount, root, procPath string, mountConfs []specutils.GoferMountConf, mountOpener MountOpener) (retErr error) {
mountIdx := 1 // First index is for rootfs.
for _, m := range mounts {
if !specutils.HasMountConfig(m) {
continue
}
mountConf := mountConfs[mountIdx]
mountIdx++
if !mountConf.ShouldUseLisafs() {
continue
}
dst, err := ResolveSymlinks(root, m.Destination)
if err != nil {
return fmt.Errorf("resolving symlinks to %q: %v", m.Destination, err)
}
flags := specutils.OptionsToFlags(m.Options) | unix.MS_BIND
if mountConf.ShouldUseOverlayfs() {
// Force mount read-only if writes are not going to be sent to it.
flags |= unix.MS_RDONLY
}
log.Infof("Mounting src: %q, dst: %q, flags: %#x, idMapped: %t", m.Source, dst, flags, specutils.IsIDMappedMount(m))
src := m.Source
var srcFile *os.File
if specutils.IsIDMappedMount(m) || unix.Access(src, unix.R_OK) != nil {
if mountOpener == nil {
return fmt.Errorf("cannot access mount source %q (or id mapped) and no mount opener provided", src)
}
// The current process doesn't have enough permissions
// to open the mount (or it is ID mapped), so let's try to open it via the
// caller-provided opener.
srcFile, err = mountOpener(&m, flags)
if err != nil {
return fmt.Errorf("opening %s: %w", m.Source, err)
}
src = fmt.Sprintf("%s/self/fd/%d", procPath, srcFile.Fd())
}
if specutils.IsIDMappedMount(m) {
err = safeSetupAndMoveMount(int(srcFile.Fd()), src, dst, procPath)
} else {
err = specutils.SafeSetupAndMount(src, dst, m.Type, flags, procPath)
}
if srcFile != nil {
srcFile.Close()
}
if err != nil {
return fmt.Errorf("mounting %+v: %v", m, err)
}
dstFD, err := unix.Open(dst, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
return fmt.Errorf("Open(%s, _, _): %w", dst, err)
}
defer unix.Close(dstFD)
// Apply mount options after creating all mount points.
// Otherwise they can be remounted into read-only.
defer func(dstFD int, flags uint32, dst string, isIDMapped bool) {
path := fmt.Sprintf("/proc/self/fd/%d", dstFD)
// The gofer process doesn't execute anything natively.
flags |= unix.MS_NOSUID
statfs := unix.Statfs_t{}
if err := unix.Statfs(path, &statfs); err != nil {
retErr = fmt.Errorf("stat dst: %q", dst)
return
}
lockedFlags := uint32(0)
for _, f := range []struct {
st, ms int
}{
// MS_NOSUID are always set.
{unix.ST_RDONLY, unix.MS_RDONLY},
{unix.ST_NOEXEC, unix.MS_NOEXEC},
{unix.ST_NODEV, unix.MS_NODEV},
{unix.ST_NOATIME, unix.MS_NOATIME},
{unix.ST_NODIRATIME, unix.MS_NODIRATIME},
{unix.ST_RELATIME, unix.MS_RELATIME},
} {
if int(statfs.Flags)&f.st == f.st {
lockedFlags |= uint32(f.ms)
}
}
if lockedFlags&(unix.MS_NOATIME|unix.MS_RELATIME) == 0 {
lockedFlags |= unix.MS_STRICTATIME
}
if isIDMapped {
attrSet, attrClr := msFlagsToMountAttr(flags | lockedFlags)
setattrFlags := uint(unix.AT_EMPTY_PATH)
if flags&unix.MS_REC != 0 {
setattrFlags |= unix.AT_RECURSIVE
}
attr := &unix.MountAttr{
Attr_set: attrSet,
Attr_clr: attrClr,
}
if err := unix.MountSetattr(dstFD, "", setattrFlags, attr); err != nil {
retErr = fmt.Errorf("mount_setattr dst: %q, flags: %#x, err: %v", dst, flags, err)
return
}
} else {
// The previous SafeSetupAndMount creates a new bind-mount, but
// it doesn't change mount flags. A separate MS_BIND|MS_REMOUNT
// has to be done to apply the mount options.
if err := unix.Mount("", path, "", uintptr(flags|lockedFlags|unix.MS_REMOUNT), ""); err != nil {
retErr = fmt.Errorf("mount dst: %q, flags: %#x, err: %v", dst, flags, err)
return
}
}
}(dstFD, flags, dst, specutils.IsIDMappedMount(m))
// Set propagation options that cannot be set together with other options.
flags = specutils.PropOptionsToFlags(m.Options)
if flags != 0 {
if err := specutils.SafeMount("", dst, "", uintptr(flags), "", procPath); err != nil {
return fmt.Errorf("mount dst: %q, flags: %#x, err: %v", dst, flags, err)
}
}
}
return nil
}
// ShouldExposeNvidiaDevice returns true if path refers to an Nvidia device
// which should be exposed to the container.
//
// Precondition: nvproxy is enabled.
func ShouldExposeNvidiaDevice(path string) bool {
if !strings.HasPrefix(path, "/dev/nvidia") {
return false
}
if path == "/dev/nvidiactl" || path == "/dev/nvidia-uvm" {
return true
}
nvidiaDevPathReg := regexp.MustCompile(`^/dev/nvidia(\d+)$`)
return nvidiaDevPathReg.MatchString(path)
}
// ShouldExposeVFIODevice returns true if path refers to a VFIO device
// which should be exposed to the container.
func ShouldExposeVFIODevice(path string) bool {
return strings.HasPrefix(path, vfioPathDir)
}
// ShouldExposeTpuDevice returns true if path refers to a TPU device which
// should be exposed to the container.
//
// Precondition: tpuproxy is enabled.
func ShouldExposeTpuDevice(path string) bool {
valid, _ := util.IsTPUDeviceValid(path)
return valid || ShouldExposeVFIODevice(path)
}
// ShouldExposeRDMADevice returns true if path refers to an RDMA uverbs device.
//
// Precondition: rdmaproxy is enabled.
func ShouldExposeRDMADevice(path string) bool {
return strings.HasPrefix(path, "/dev/infiniband/uverbs")
}
// SetupDev mounts devices from the OCI spec into the gofer's /dev directory.
func SetupDev(spec *specs.Spec, conf *config.Config, root, procPath string) error {
if err := os.MkdirAll(filepath.Join(root, "dev"), 0777); err != nil {
return fmt.Errorf("creating dev directory: %v", err)
}
// Mount any devices specified in the spec.
if spec.Linux == nil {
return nil
}
nvproxyEnabled := specutils.NVProxyEnabled(spec, conf)
tpuproxyEnabled := specutils.TPUProxyEnabled(spec, conf)
rdmaproxyEnabled := specutils.RDMAEnabled(spec, conf)
for _, dev := range spec.Linux.Devices {
shouldMount := (nvproxyEnabled && ShouldExposeNvidiaDevice(dev.Path)) ||
(tpuproxyEnabled && ShouldExposeTpuDevice(dev.Path)) ||
(rdmaproxyEnabled && ShouldExposeRDMADevice(dev.Path))
if !shouldMount {
continue
}
dst := filepath.Join(root, dev.Path)
log.Infof("Mounting device %q as bind mount at %q", dev.Path, dst)
if err := specutils.SafeSetupAndMount(dev.Path, dst, "bind", unix.MS_BIND, procPath); err != nil {
return fmt.Errorf("mounting %q: %v", dev.Path, err)
}
}
return nil
}
// ResolveMounts resolves relative paths and symlinks in mount point
// destinations. It also adjusts mount options based on the underlying
// filesystem type.
//
// Note: mount points must already be in place for resolution to work.
// Otherwise, it may follow symlinks to locations that would be overwritten
// with another mount point and return the wrong location. In short, make sure
// SetupMounts() has been called before.
func ResolveMounts(conf *config.Config, mounts []specs.Mount, root string, mountConfs []specutils.GoferMountConf) ([]specs.Mount, error) {
mountIdx := 1 // First index is for rootfs.
cleanMounts := make([]specs.Mount, 0, len(mounts))
for _, m := range mounts {
if !specutils.HasMountConfig(m) {
cleanMounts = append(cleanMounts, m)
continue
}
mountConf := mountConfs[mountIdx]
mountIdx++
if !mountConf.ShouldUseLisafs() {
cleanMounts = append(cleanMounts, m)
continue
}
dst, err := ResolveSymlinks(root, m.Destination)
if err != nil {
return nil, fmt.Errorf("resolving symlinks to %q: %v", m.Destination, err)
}
relDst, err := filepath.Rel(root, dst)
if err != nil {
panic(fmt.Sprintf("%q could not be made relative to %q: %v", dst, root, err))
}
opts, err := AdjustMountOptions(conf, filepath.Join(root, relDst), m.Options)
if err != nil {
return nil, err
}
cpy := m
cpy.Destination = filepath.Join("/", relDst)
cpy.Options = opts
cleanMounts = append(cleanMounts, cpy)
}
return cleanMounts, nil
}