diff --git a/libcontainer/mount_entry_linux.go b/libcontainer/mount_entry_linux.go
new file mode 100644
index 00000000000..b55ece9c457
--- /dev/null
+++ b/libcontainer/mount_entry_linux.go
@@ -0,0 +1,294 @@
+package libcontainer
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "syscall"
+
+ "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
+ "github.com/opencontainers/runc/internal/pathrs"
+ "github.com/opencontainers/runc/libcontainer/configs"
+ "github.com/opencontainers/runc/libcontainer/utils"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/sys/unix"
+)
+
+// mountEntry contains mount data specific to a mount point.
+type mountEntry struct {
+ *configs.Mount
+ srcFile *mountSource
+ dstFile *os.File
+}
+
+// srcName is only meant for error messages, it returns a "friendly" name.
+func (m mountEntry) srcName() string {
+ if m.srcFile != nil {
+ return m.srcFile.file.Name()
+ }
+ return m.Source
+}
+
+func (m mountEntry) srcStat() (os.FileInfo, *syscall.Stat_t, error) {
+ var (
+ st os.FileInfo
+ err error
+ )
+ if m.srcFile != nil {
+ st, err = m.srcFile.file.Stat()
+ } else {
+ st, err = os.Stat(m.Source)
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ return st, st.Sys().(*syscall.Stat_t), nil
+}
+
+func (m mountEntry) srcStatfs() (*unix.Statfs_t, error) {
+ var st unix.Statfs_t
+ if m.srcFile != nil {
+ if err := unix.Fstatfs(int(m.srcFile.file.Fd()), &st); err != nil {
+ return nil, os.NewSyscallError("fstatfs", err)
+ }
+ } else {
+ if err := unix.Statfs(m.Source, &st); err != nil {
+ return nil, &os.PathError{Op: "statfs", Path: m.Source, Err: err}
+ }
+ }
+ return &st, nil
+}
+
+func (m mountEntry) isDetachedMount() bool {
+ return m.srcFile != nil && (m.srcFile.Type == mountSourceOpenTree || m.srcFile.Type == mountSourceFsOpen)
+}
+
+func (m *mountEntry) createOpenMountpoint(root *os.File) (Err error) {
+ rootfs := root.Name()
+ unsafePath := pathrs.LexicallyStripRoot(rootfs, m.Destination)
+ dstFile, err := pathrs.OpenInRoot(root, unsafePath, unix.O_PATH)
+ defer func() {
+ if dstFile != nil && Err != nil {
+ _ = dstFile.Close()
+ }
+ }()
+ if err == nil && m.Device == "tmpfs" {
+ // If the original target exists, copy the mode for the tmpfs mount.
+ stat, err := dstFile.Stat()
+ if err != nil {
+ return fmt.Errorf("check tmpfs source mode: %w", err)
+ }
+ dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode()))
+ if m.Data != "" {
+ dt = dt + "," + m.Data
+ }
+ m.Data = dt
+ }
+ if err != nil {
+ if !errors.Is(err, unix.ENOENT) {
+ return fmt.Errorf("lookup mountpoint target: %w", err)
+ }
+
+ // If the mountpoint doesn't already exist, we want to create a mountpoint
+ // that makes sense for the source. For file bind-mounts this is an empty
+ // file, for everything else it's a directory.
+ dstIsFile := false
+ if m.Device == "bind" {
+ fi, _, err := m.srcStat()
+ if err != nil {
+ // Error out if the source of a bind mount does not exist as we
+ // will be unable to bind anything to it.
+ return err
+ }
+ dstIsFile = !fi.IsDir()
+ }
+ if dstIsFile {
+ dstFile, err = pathrs.CreateInRoot(root, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644)
+ } else {
+ dstFile, err = pathrs.MkdirAllInRoot(root, unsafePath, 0o755)
+ }
+ if err != nil {
+ return fmt.Errorf("make mountpoint %q: %w", m.Destination, err)
+ }
+ }
+
+ dstFullPath, err := procfs.ProcSelfFdReadlink(dstFile)
+ if err != nil {
+ return fmt.Errorf("get mount destination real path: %w", err)
+ }
+ if !pathrs.IsLexicallyInRoot(rootfs, dstFullPath) {
+ return fmt.Errorf("mountpoint %q is outside of rootfs %q", dstFullPath, rootfs)
+ }
+ if relPath, err := filepath.Rel(rootfs, dstFullPath); err != nil {
+ return fmt.Errorf("get relative path of %q: %w", dstFullPath, err)
+ } else if relPath == "." {
+ return fmt.Errorf("mountpoint %q is on the top of rootfs %q", dstFullPath, rootfs)
+ }
+ // TODO: Make checkProcMount use dstFile directly to avoid the need to
+ // operate on paths here.
+ if err := checkProcMount(rootfs, dstFullPath, *m); err != nil {
+ return fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err)
+ }
+ // Update mountEntry.
+ m.dstFile = dstFile
+ return nil
+}
+
+func reopenAfterMount(rootFd, f *os.File, flags int) (_ *os.File, Err error) {
+ fullPath, err := procfs.ProcSelfFdReadlink(f)
+ if err != nil {
+ return nil, fmt.Errorf("get full path: %w", err)
+ }
+ rootfs := rootFd.Name()
+ if !pathrs.IsLexicallyInRoot(rootfs, fullPath) {
+ return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs)
+ }
+ unsafePath := pathrs.LexicallyStripRoot(rootfs, fullPath)
+ reopened, err := pathrs.OpenInRoot(rootFd, unsafePath, flags)
+ if err != nil {
+ return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err)
+ }
+ defer func() {
+ if Err != nil {
+ _ = reopened.Close()
+ }
+ }()
+
+ // NOTE: The best we can do here is confirm that the new mountpoint handle
+ // matches the original target handle, but an attacker could've swapped a
+ // different path to replace it. In the worst case this could result in us
+ // applying later vfsmount flags onto the wrong mount.
+ //
+ // This is far from ideal, but the only way of doing this in a race-free
+ // way is to switch the new mount API (move_mount(2) does not require this
+ // re-opening step, and thus no such races are possible).
+ reopenedFullPath, err := procfs.ProcSelfFdReadlink(reopened)
+ if err != nil {
+ return nil, fmt.Errorf("check full path of re-opened mountpoint: %w", err)
+ }
+ if reopenedFullPath != fullPath {
+ return nil, fmt.Errorf("mountpoint %q was moved while re-opening", unsafePath)
+ }
+ return reopened, nil
+}
+
+// initMountSourceFd initializes the mount source file descriptor if it is nil and the new mount API is available.
+func (m *mountEntry) initMountSourceFd(flags int, mountLabel string) {
+ if hasNewMountAPI() && (m.srcFile == nil || m.srcFile.file == nil) {
+ if source, err := createDetachedMountSource(m.Mount, flags, mountLabel); err == nil {
+ m.srcFile = source
+ } else {
+ logrus.Debugf("new mount api error: %v", err)
+ }
+ }
+}
+
+// Do the mount operation followed by additional mounts required to take care
+// of propagation flags. This will always be scoped inside the container rootfs.
+func (m *mountEntry) mountPropagate(rootFd *os.File, mountLabel string) error {
+ var (
+ data = label.FormatMountLabel(m.Data, mountLabel)
+ flags = m.Flags
+ )
+ // Delay mounting the filesystem read-only if we need to do further
+ // operations on it. We need to set up files in "/dev", and other tmpfs
+ // mounts may need to be chmod-ed after mounting. These mounts will be
+ // remounted ro later in finalizeRootfs(), if necessary.
+ flagsMask := 0
+ if m.Device == "tmpfs" || pathrs.LexicallyCleanPath(m.Destination) == "/dev" {
+ flagsMask = ^unix.MS_RDONLY
+ }
+ m.initMountSourceFd(flagsMask, mountLabel)
+
+ if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
+ if flagsMask != 0 {
+ flags &= flagsMask
+ }
+ return mountViaFds(m.Source, m.srcFile, m.Destination, dstFd, m.Device, uintptr(flags), data)
+ }); err != nil {
+ return err
+ }
+
+ if m.isDetachedMount() {
+ _ = m.dstFile.Close()
+ m.dstFile = m.srcFile.file
+ } else {
+ // We need to re-open the mountpoint after doing the mount, in order for us
+ // to operate on the new mount we just created. However, we cannot use
+ // pathrs.Reopen because we need to re-resolve from the parent directory to
+ // get a new handle to the top mount.
+ newDstFile, err := reopenAfterMount(rootFd, m.dstFile, unix.O_PATH)
+ if err != nil {
+ return fmt.Errorf("reopen mountpoint after mount: %w", err)
+ }
+ _ = m.dstFile.Close()
+ m.dstFile = newDstFile
+ }
+
+ // Apply the propagation flags on the new mount using mount_setattr(2) if
+ // available, otherwise fall back to mount(2).
+ if len(m.PropagationFlags) > 0 {
+ // Try to use mount_setattr(2) for propagation flags. This is more
+ // secure and efficient than using mount(2).
+ //
+ // PropagationFlags can contain MS_REC combined with propagation types
+ // (e.g., "rprivate" = MS_PRIVATE | MS_REC). For mount_setattr, we need
+ // to handle MS_REC separately as the AT_RECURSIVE flag parameter.
+ var propagation uint64
+ // Always use AT_EMPTY_PATH since we're operating on the fd directly
+ pflags := uint(unix.AT_EMPTY_PATH)
+ for _, pflag := range m.PropagationFlags {
+ if pflag&unix.MS_REC != 0 {
+ pflags |= unix.AT_RECURSIVE
+ }
+ // Only include the propagation type, not MS_REC
+ propagation |= uint64(pflag & ^unix.MS_REC)
+ }
+ attr := unix.MountAttr{
+ Propagation: propagation,
+ }
+ err := unix.MountSetattr(int(m.dstFile.Fd()), "", pflags, &attr)
+ if err == nil {
+ return nil
+ }
+ // fall back to mount(2).
+ logrus.Debugf("mount_setattr failed for %s: %v, falling back to mount(2)", m.Destination, err)
+ if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
+ for _, pflag := range m.PropagationFlags {
+ if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil {
+ return err
+ }
+ }
+ return nil
+ }); err != nil {
+ return fmt.Errorf("change mount propagation through procfd: %w", err)
+ }
+ }
+ return nil
+}
+
+func setRecAttr(m *mountEntry) error {
+ if m.RecAttr == nil {
+ return nil
+ }
+ return utils.WithProcfdFile(m.dstFile, func(procfd string) error {
+ return unix.MountSetattr(-1, procfd, unix.AT_RECURSIVE, m.RecAttr)
+ })
+}
+
+// cleanup closes any open file descriptors in the mountEntry. This is called
+// after the mount has been completed and the mountEntry is no longer needed.
+func (m *mountEntry) cleanup() {
+ if m.srcFile != nil && m.srcFile.file != nil {
+ if m.srcFile.file != m.dstFile {
+ _ = m.srcFile.file.Close()
+ }
+ m.srcFile.file = nil
+ }
+ if m.dstFile != nil {
+ _ = m.dstFile.Close()
+ m.dstFile = nil
+ }
+}
diff --git a/libcontainer/mount_linux.go b/libcontainer/mount_linux.go
index 82ef0fdf1b5..af1fa63efdd 100644
--- a/libcontainer/mount_linux.go
+++ b/libcontainer/mount_linux.go
@@ -7,12 +7,14 @@ import (
"os"
"strconv"
"strings"
+ "sync"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/internal/userns"
+ "github.com/opencontainers/runc/libcontainer/system/kernelversion"
"github.com/opencontainers/runc/libcontainer/utils"
)
@@ -27,6 +29,9 @@ const (
mountSourceOpenTree mountSourceType = "open_tree"
// A plain file descriptor that can be mounted through /proc/thread-self/fd.
mountSourcePlain mountSourceType = "plain-open"
+ // A file descriptor returned by fsopen(2) that needs to be installed using
+ // fsmount(2) and move_mount(2).
+ mountSourceFsOpen mountSourceType = "fsopen"
)
type mountSource struct {
@@ -166,7 +171,7 @@ func mountViaFds(source string, srcFile *mountSource, target, dstFd, fstype stri
dst = dstFd
}
src := source
- isMoveMount := srcFile != nil && srcFile.Type == mountSourceOpenTree
+ isMoveMount := srcFile != nil && (srcFile.Type == mountSourceOpenTree || srcFile.Type == mountSourceFsOpen)
if srcFile != nil {
// If we're going to use the /proc/thread-self/... path for classic
// mount(2), we need to get a safe handle to /proc/thread-self. This
@@ -342,3 +347,184 @@ func mountFd(nsHandles *userns.Handles, m *configs.Mount) (_ *mountSource, retEr
file: mountFile,
}, nil
}
+
+// hasNewMountAPI reports whether the running kernel supports the new mount API
+// (open_tree(2), move_mount(2), fsopen(2), fsmount(2)), which was introduced
+// in Linux 5.2.
+var hasNewMountAPI = sync.OnceValue(func() bool {
+ // All of the pieces of the new mount API we use (fsopen, fsconfig,
+ // fsmount, open_tree) were added together in Linux 5.2[1,2], so we can
+ // just check for one of the syscalls and the others should also be
+ // available.
+ //
+ // Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE.
+ // This is equivalent to openat(2), but tells us if open_tree is
+ // available (and thus all of the other basic new mount API syscalls).
+ // open_tree(2) is most light-weight syscall to test here.
+ //
+ // [1]: merge commit 400913252d09
+ // [2]:
+ fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC)
+ if err != nil {
+ return false
+ }
+ _ = unix.Close(fd)
+
+ // RHEL 8 has a backport of fsopen(2) that appears to have some very
+ // difficult to debug performance pathology. As such, it seems prudent to
+ // simply reject pre-5.2 kernels.
+ isNotBackport, _ := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{Kernel: 5, Major: 2})
+ return isNotBackport
+})
+
+// createDetachedBindMount creates a detached bind mount using open_tree(2).
+func createDetachedBindMount(m *configs.Mount) (_ *mountSource, retErr error) {
+ openTreeFlags := uint(unix.OPEN_TREE_CLONE | unix.OPEN_TREE_CLOEXEC)
+ if m.Flags&unix.MS_REC != 0 {
+ openTreeFlags |= unix.AT_RECURSIVE
+ }
+ fd, err := unix.OpenTree(unix.AT_FDCWD, m.Source, openTreeFlags)
+ if err != nil {
+ return nil, &os.PathError{Op: "open_tree", Path: m.Source, Err: err}
+ }
+ return &mountSource{
+ Type: mountSourceOpenTree,
+ file: os.NewFile(uintptr(fd), m.Source),
+ }, nil
+}
+
+// fsconfigApplyOptions applies the options specified in opts to the filesystem
+// file descriptor fsfd using fsconfig(2). The options are expected to be a
+// comma-separated list of key=value pairs or flags.
+func fsconfigApplyOptions(fsfd int, opts string) error {
+ fields := strings.SplitSeq(opts, ",")
+ for opt := range fields {
+ opt = strings.TrimSpace(opt)
+ if opt == "" {
+ continue
+ }
+ k, v, found := strings.Cut(opt, "=")
+ if found {
+ if err := unix.FsconfigSetString(fsfd, k, v); err != nil {
+ return fmt.Errorf("failed to set fsconfig option %s=%s: %w", k, v, err)
+ }
+ } else {
+ if err := unix.FsconfigSetFlag(fsfd, opt); err != nil {
+ return fmt.Errorf("failed to set fsconfig flag %s: %w", opt, err)
+ }
+ }
+ }
+ return nil
+}
+
+// msFlagsToMountAttr converts mount(2) flags to mount attributes for
+// mount_setattr(2). It returns the set and clear bits for the mount attributes.
+func msFlagsToMountAttr(flags int) (set, cls uint64) {
+ // Binary mount flags: if the flag is set, add it to "set"; otherwise add
+ // it to "cls" so that mount_setattr(2) clears it.
+ for _, f := range []struct {
+ msFlag int
+ mountAttr uint64
+ }{
+ {unix.MS_RDONLY, unix.MOUNT_ATTR_RDONLY},
+ {unix.MS_NOSUID, unix.MOUNT_ATTR_NOSUID},
+ {unix.MS_NODEV, unix.MOUNT_ATTR_NODEV},
+ {unix.MS_NOEXEC, unix.MOUNT_ATTR_NOEXEC},
+ {unix.MS_NODIRATIME, unix.MOUNT_ATTR_NODIRATIME},
+ {unix.MS_NOSYMFOLLOW, unix.MOUNT_ATTR_NOSYMFOLLOW},
+ } {
+ if flags&f.msFlag != 0 {
+ set |= f.mountAttr
+ } else {
+ cls |= f.mountAttr
+ }
+ }
+ // Unlike the binary flags above, the atime behaviour is a multi-valued
+ // field, not an independent on/off bit. MOUNT_ATTR__ATIME (0x70) is the
+ // bitmask that covers this 3-bit field, with these possible values:
+ // - MOUNT_ATTR_RELATIME (0x00) — the default; update atime relative to mtime/ctime
+ // - MOUNT_ATTR_NOATIME (0x10) — do not update access times
+ // - MOUNT_ATTR_STRICTATIME (0x20) — always update access times
+ switch {
+ case flags&unix.MS_NOATIME != 0:
+ set |= unix.MOUNT_ATTR_NOATIME
+ case flags&unix.MS_STRICTATIME != 0:
+ set |= unix.MOUNT_ATTR_STRICTATIME
+ case flags&unix.MS_RELATIME != 0:
+ set |= unix.MOUNT_ATTR_RELATIME
+ }
+ if flags&unix.MS_LAZYTIME != 0 {
+ logrus.Warnf("MS_LAZYTIME mount flag specified, but kernel does not support MOUNT_ATTR_LAZYTIME; ignoring")
+ }
+ // Always clear the atime mask in attr_clr: whether an atime option was
+ // specified or not, mount_setattr(2) needs the mask to modify this
+ // multi-valued field -- either to set a new value or reset to default.
+ cls |= unix.MOUNT_ATTR__ATIME
+
+ return set, cls
+}
+
+// createDetachedFsMount creates a detached filesystem mount using fsopen(2),
+// fsconfig(2), fsmount(2), and move_mount(2).
+func createDetachedFsMount(m *configs.Mount, flagsMask int, mountLabel string) (_ *mountSource, retErr error) {
+ fstype := m.Device
+ if fstype == "cgroup" {
+ fstype = "cgroup2"
+ }
+ fsfd, err := unix.Fsopen(fstype, unix.FSOPEN_CLOEXEC)
+ if err != nil {
+ return nil, &os.PathError{Op: "fsopen", Path: fstype, Err: err}
+ }
+ defer unix.Close(fsfd)
+
+ if err := fsconfigApplyOptions(fsfd, m.Data); err != nil {
+ return nil, &os.PathError{Op: "fsconfigset", Path: fstype, Err: err}
+ }
+ if mountLabel != "" {
+ if err := unix.FsconfigSetString(fsfd, "context", mountLabel); err != nil {
+ return nil, &os.PathError{Op: "fsconfigset(context)", Path: fstype, Err: err}
+ }
+ }
+ source := m.Source
+ if m.Device == "cgroup" {
+ source = fstype
+ }
+ if err := unix.FsconfigSetString(fsfd, "source", source); err != nil {
+ return nil, &os.PathError{Op: "fsconfigset(source)", Path: fstype, Err: err}
+ }
+ if err := unix.FsconfigCreate(fsfd); err != nil {
+ return nil, &os.PathError{Op: "fsconfig", Path: fstype, Err: err}
+ }
+
+ flags := m.Flags
+ if flagsMask != 0 {
+ flags &= flagsMask
+ }
+ mountAttrs, _ := msFlagsToMountAttr(flags)
+ fd, err := unix.Fsmount(fsfd, unix.FSMOUNT_CLOEXEC, int(mountAttrs))
+ if err != nil {
+ return nil, &os.PathError{Op: "fsmount", Path: fstype, Err: err}
+ }
+ return &mountSource{
+ Type: mountSourceFsOpen,
+ file: os.NewFile(uintptr(fd), fstype),
+ }, nil
+}
+
+// createDetachedMountSource creates a detached mount source for the given mount
+// configuration. If the mount is a bind-mount, it will use open_tree(2). If it
+// is a filesystem mount, it will use fsopen(2), fsconfig(2), and fsmount(2).
+func createDetachedMountSource(m *configs.Mount, flagsMask int, mountLabel string) (_ *mountSource, retErr error) {
+ if m.IsBind() {
+ // 1. For mount flags:
+ // Since we create a detached bind mount, there is no need to set mount attributes upfront.
+ // They can be applied later using move_mount(2) and mount_setattr(2).
+ //
+ // 2. For mountLabel:
+ // Mount labels cannot be set on a detached bind mount, so we skip setting it here.
+ // As documented in mount(2): if MS_BIND is included in mountflags,
+ // the filesystem type and mount data arguments are ignored.
+ return createDetachedBindMount(m)
+ }
+ return createDetachedFsMount(m, flagsMask, mountLabel)
+}
diff --git a/libcontainer/mount_linux_test.go b/libcontainer/mount_linux_test.go
index 6bddfc2f08a..fe922b4c33a 100644
--- a/libcontainer/mount_linux_test.go
+++ b/libcontainer/mount_linux_test.go
@@ -52,3 +52,126 @@ func TestStringifyMountFlags(t *testing.T) {
}
}
}
+
+func TestMsFlagsToMountAttr(t *testing.T) {
+ // allBinaryAttrs is the bitmask of all binary (non-atime) mount attributes.
+ allBinaryAttrs := uint64(unix.MOUNT_ATTR_RDONLY | unix.MOUNT_ATTR_NOSUID |
+ unix.MOUNT_ATTR_NODEV | unix.MOUNT_ATTR_NOEXEC |
+ unix.MOUNT_ATTR_NODIRATIME | unix.MOUNT_ATTR_NOSYMFOLLOW)
+
+ // Helper to compute the expected "cls" when only a subset of binary attrs
+ // is set: the set attrs are removed from cls, the rest stay, and the atime
+ // field is always cleared.
+ expectCls := func(setAttrs uint64) uint64 {
+ return (allBinaryAttrs &^ setAttrs) | unix.MOUNT_ATTR__ATIME
+ }
+
+ for _, test := range []struct {
+ name string
+ flags int
+ wantSet uint64
+ wantCls uint64
+ }{
+ // No flags: all binary attrs cleared, atime defaults to RELATIME.
+ {"empty", 0, 0, allBinaryAttrs | unix.MOUNT_ATTR__ATIME},
+
+ // --- Individual binary flags ---
+ // Each set flag moves its attr from "cls" to "set"; the rest stay in "cls".
+ {
+ "rdonly", unix.MS_RDONLY,
+ unix.MOUNT_ATTR_RDONLY, expectCls(unix.MOUNT_ATTR_RDONLY),
+ },
+ {
+ "nosuid", unix.MS_NOSUID,
+ unix.MOUNT_ATTR_NOSUID, expectCls(unix.MOUNT_ATTR_NOSUID),
+ },
+ {
+ "nodev", unix.MS_NODEV,
+ unix.MOUNT_ATTR_NODEV, expectCls(unix.MOUNT_ATTR_NODEV),
+ },
+ {
+ "noexec", unix.MS_NOEXEC,
+ unix.MOUNT_ATTR_NOEXEC, expectCls(unix.MOUNT_ATTR_NOEXEC),
+ },
+ {
+ "nodiratime", unix.MS_NODIRATIME,
+ unix.MOUNT_ATTR_NODIRATIME, expectCls(unix.MOUNT_ATTR_NODIRATIME),
+ },
+ {
+ "nosymfollow", unix.MS_NOSYMFOLLOW,
+ unix.MOUNT_ATTR_NOSYMFOLLOW, expectCls(unix.MOUNT_ATTR_NOSYMFOLLOW),
+ },
+
+ // --- Individual atime modes ---
+ // MOUNT_ATTR__ATIME is always in "cls" to clear the atime field first.
+ {
+ "noatime", unix.MS_NOATIME,
+ unix.MOUNT_ATTR_NOATIME, expectCls(0),
+ },
+ {
+ "strictatime", unix.MS_STRICTATIME,
+ unix.MOUNT_ATTR_STRICTATIME, expectCls(0),
+ },
+ {
+ "relatime", unix.MS_RELATIME,
+ 0, // MOUNT_ATTR_RELATIME is 0x00, clearing the field is sufficient
+ expectCls(0),
+ },
+
+ // --- Combinations ---
+ {
+ "all-binary-flags",
+ unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
+ unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW,
+ allBinaryAttrs, unix.MOUNT_ATTR__ATIME,
+ },
+
+ {
+ "all-binary-plus-noatime",
+ unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
+ unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW | unix.MS_NOATIME,
+ allBinaryAttrs | unix.MOUNT_ATTR_NOATIME, unix.MOUNT_ATTR__ATIME,
+ },
+
+ {
+ "all-binary-plus-strictatime",
+ unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
+ unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW | unix.MS_STRICTATIME,
+ allBinaryAttrs | unix.MOUNT_ATTR_STRICTATIME, unix.MOUNT_ATTR__ATIME,
+ },
+
+ {
+ "all-binary-plus-relatime",
+ unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NODEV | unix.MS_NOEXEC |
+ unix.MS_NODIRATIME | unix.MS_NOSYMFOLLOW | unix.MS_RELATIME,
+ allBinaryAttrs, // RELATIME is 0x00, so nothing extra in "set"
+ unix.MOUNT_ATTR__ATIME,
+ },
+
+ {
+ "rdonly-plus-noatime",
+ unix.MS_RDONLY | unix.MS_NOATIME,
+ unix.MOUNT_ATTR_RDONLY | unix.MOUNT_ATTR_NOATIME,
+ expectCls(unix.MOUNT_ATTR_RDONLY),
+ },
+
+ // Multiple atime flags set (should not happen in practice, but the
+ // switch picks the first: NOATIME > STRICTATIME > RELATIME).
+ {
+ "noatime-plus-strictatime",
+ unix.MS_NOATIME | unix.MS_STRICTATIME,
+ unix.MOUNT_ATTR_NOATIME, expectCls(0),
+ },
+ {
+ "strictatime-plus-relatime",
+ unix.MS_STRICTATIME | unix.MS_RELATIME,
+ unix.MOUNT_ATTR_STRICTATIME, expectCls(0),
+ },
+ } {
+ gotSet, gotCls := msFlagsToMountAttr(test.flags)
+ if gotSet != test.wantSet || gotCls != test.wantCls {
+ t.Errorf("%s: msFlagsToMountAttr(0x%x) = (set=0x%x, cls=0x%x), want (set=0x%x, cls=0x%x)",
+ test.name, test.flags, gotSet, gotCls, test.wantSet, test.wantCls)
+ }
+ }
+}
diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go
index e4a4df20c78..7ceef42f963 100644
--- a/libcontainer/rootfs_linux.go
+++ b/libcontainer/rootfs_linux.go
@@ -9,10 +9,8 @@ import (
"runtime"
"strconv"
"strings"
- "syscall"
"time"
- "github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
"github.com/moby/sys/mountinfo"
"github.com/moby/sys/userns"
"github.com/mrunalp/fileutils"
@@ -42,51 +40,6 @@ type mountConfig struct {
cgroupns bool
}
-// mountEntry contains mount data specific to a mount point.
-type mountEntry struct {
- *configs.Mount
- srcFile *mountSource
- dstFile *os.File
-}
-
-// srcName is only meant for error messages, it returns a "friendly" name.
-func (m mountEntry) srcName() string {
- if m.srcFile != nil {
- return m.srcFile.file.Name()
- }
- return m.Source
-}
-
-func (m mountEntry) srcStat() (os.FileInfo, *syscall.Stat_t, error) {
- var (
- st os.FileInfo
- err error
- )
- if m.srcFile != nil {
- st, err = m.srcFile.file.Stat()
- } else {
- st, err = os.Stat(m.Source)
- }
- if err != nil {
- return nil, nil, err
- }
- return st, st.Sys().(*syscall.Stat_t), nil
-}
-
-func (m mountEntry) srcStatfs() (*unix.Statfs_t, error) {
- var st unix.Statfs_t
- if m.srcFile != nil {
- if err := unix.Fstatfs(int(m.srcFile.file.Fd()), &st); err != nil {
- return nil, os.NewSyscallError("fstatfs", err)
- }
- } else {
- if err := unix.Statfs(m.Source, &st); err != nil {
- return nil, &os.PathError{Op: "statfs", Path: m.Source, Err: err}
- }
- }
- return &st, nil
-}
-
// needsSetupDev returns true if /dev needs to be set up.
func needsSetupDev(config *configs.Config) bool {
for _, m := range config.Mounts {
@@ -111,8 +64,9 @@ func doSetupDev(rootFd *os.File, config *configs.Config) error {
}
// setupAndMountToRootfs sets up the mount for a single mount point and mounts it to the rootfs.
-func setupAndMountToRootfs(pipe *syncSocket, config *configs.Config, mountConfig *mountConfig, m *configs.Mount) error {
+func setupAndMountToRootfs(pipe *syncSocket, config *configs.Config, mountConfig *mountConfig, m *configs.Mount) (retErr error) {
entry := mountEntry{Mount: m}
+ defer entry.cleanup()
// Figure out whether we need to request runc to give us an
// open_tree(2)-style mountfd. For idmapped mounts, this is always
// necessary. For bind-mounts, this is only necessary if we cannot
@@ -136,7 +90,11 @@ func setupAndMountToRootfs(pipe *syncSocket, config *configs.Config, mountConfig
if sync.File == nil {
return fmt.Errorf("mountfd request for %q: response missing attached fd", m.Source)
}
- defer sync.File.Close()
+ defer func() {
+ if retErr != nil && entry.srcFile == nil {
+ _ = sync.File.Close()
+ }
+ }()
// Sanity-check to make sure we didn't get the wrong fd back. Note
// that while m.Source might contain symlinks, the (*os.File).Name
// is based on the path provided to os.OpenFile, not what it
@@ -158,7 +116,7 @@ func setupAndMountToRootfs(pipe *syncSocket, config *configs.Config, mountConfig
src.file = sync.File
entry.srcFile = src
}
- if err := mountToRootfs(mountConfig, entry); err != nil {
+ if err := mountToRootfs(mountConfig, &entry); err != nil {
return fmt.Errorf("error mounting %q to rootfs at %q: %w", m.Source, m.Destination, err)
}
return nil
@@ -323,7 +281,7 @@ func cleanupTmp(tmpdir string) {
_ = os.RemoveAll(tmpdir)
}
-func mountCgroupV1(m mountEntry, c *mountConfig) error {
+func mountCgroupV1(m *mountEntry, c *mountConfig) error {
binds, err := getCgroupMounts(m.Mount)
if err != nil {
return err
@@ -343,8 +301,10 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error {
Data: "mode=755",
PropagationFlags: m.PropagationFlags,
}
+ entry := mountEntry{Mount: tmpfs}
+ defer entry.cleanup()
- if err := mountToRootfs(c, mountEntry{Mount: tmpfs}); err != nil {
+ if err := mountToRootfs(c, &entry); err != nil {
return err
}
@@ -377,7 +337,10 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error {
return err
}
} else {
- if err := mountToRootfs(c, mountEntry{Mount: b}); err != nil {
+ subEntry := mountEntry{Mount: b}
+ err := mountToRootfs(c, &subEntry)
+ subEntry.cleanup()
+ if err != nil {
return err
}
}
@@ -396,10 +359,9 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error {
return nil
}
-func mountCgroupV2(m mountEntry, c *mountConfig) error {
- err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
- return mountViaFds(m.Source, nil, m.Destination, dstFd, "cgroup2", uintptr(m.Flags), m.Data)
- })
+func mountCgroupV2(m *mountEntry, c *mountConfig) error {
+ // We can't set mount label on cgroup2 mounts, so we just pass an empty string here.
+ err := m.mountPropagate(c.root, "")
if err == nil || (!errors.Is(err, unix.EPERM) && !errors.Is(err, unix.EBUSY)) {
return err
}
@@ -419,7 +381,9 @@ func mountCgroupV2(m mountEntry, c *mountConfig) error {
bindM.Source = c.cgroup2Path
}
// mountToRootfs() handles remounting for MS_RDONLY.
- err = mountToRootfs(c, mountEntry{Mount: bindM})
+ entry := mountEntry{Mount: bindM}
+ defer entry.cleanup()
+ err = mountToRootfs(c, &entry)
if c.rootlessCgroups && errors.Is(err, unix.ENOENT) {
// ENOENT (for `src = c.cgroup2Path`) happens when rootless runc is being executed
// outside the userns+mountns.
@@ -427,13 +391,17 @@ func mountCgroupV2(m mountEntry, c *mountConfig) error {
// Mask `/sys/fs/cgroup` to ensure it is read-only, even when `/sys` is mounted
// with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`).
err = utils.WithProcfdFile(m.dstFile, func(procfd string) error {
- return maskDir(procfd, c.label)
+ fd, err := maskDir(m.dstFile, procfd, c.label)
+ if fd != nil {
+ _ = fd.Close()
+ }
+ return err
})
}
return err
}
-func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) {
+func doTmpfsCopyUp(m *mountEntry, mountLabel string) (Err error) {
// Set up a scratch dir for the tmpfs on the host.
tmpdir, err := prepareTmp("/tmp")
if err != nil {
@@ -464,6 +432,7 @@ func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) {
Mount: m.Mount,
dstFile: tmpDirFile,
}
+ defer hostMount.cleanup()
if err := hostMount.mountPropagate(hostRootFd, mountLabel); err != nil {
return err
}
@@ -536,85 +505,7 @@ func statfsToMountFlags(st unix.Statfs_t) int {
return flags
}
-func (m *mountEntry) createOpenMountpoint(root *os.File) (Err error) {
- rootfs := root.Name()
- unsafePath := pathrs.LexicallyStripRoot(rootfs, m.Destination)
- dstFile, err := pathrs.OpenInRoot(root, unsafePath, unix.O_PATH)
- defer func() {
- if dstFile != nil && Err != nil {
- _ = dstFile.Close()
- }
- }()
- if err == nil && m.Device == "tmpfs" {
- // If the original target exists, copy the mode for the tmpfs mount.
- stat, err := dstFile.Stat()
- if err != nil {
- return fmt.Errorf("check tmpfs source mode: %w", err)
- }
- dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode()))
- if m.Data != "" {
- dt = dt + "," + m.Data
- }
- m.Data = dt
- }
- if err != nil {
- if !errors.Is(err, unix.ENOENT) {
- return fmt.Errorf("lookup mountpoint target: %w", err)
- }
-
- // If the mountpoint doesn't already exist, we want to create a mountpoint
- // that makes sense for the source. For file bind-mounts this is an empty
- // file, for everything else it's a directory.
- dstIsFile := false
- if m.Device == "bind" {
- fi, _, err := m.srcStat()
- if err != nil {
- // Error out if the source of a bind mount does not exist as we
- // will be unable to bind anything to it.
- return err
- }
- dstIsFile = !fi.IsDir()
- }
- if dstIsFile {
- dstFile, err = pathrs.CreateInRoot(root, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644)
- } else {
- dstFile, err = pathrs.MkdirAllInRoot(root, unsafePath, 0o755)
- }
- if err != nil {
- return fmt.Errorf("make mountpoint %q: %w", m.Destination, err)
- }
- }
-
- dstFullPath, err := procfs.ProcSelfFdReadlink(dstFile)
- if err != nil {
- return fmt.Errorf("get mount destination real path: %w", err)
- }
- if !pathrs.IsLexicallyInRoot(rootfs, dstFullPath) {
- return fmt.Errorf("mountpoint %q is outside of rootfs %q", dstFullPath, rootfs)
- }
- if relPath, err := filepath.Rel(rootfs, dstFullPath); err != nil {
- return fmt.Errorf("get relative path of %q: %w", dstFullPath, err)
- } else if relPath == "." {
- return fmt.Errorf("mountpoint %q is on the top of rootfs %q", dstFullPath, rootfs)
- }
- // TODO: Make checkProcMount use dstFile directly to avoid the need to
- // operate on paths here.
- if err := checkProcMount(rootfs, dstFullPath, *m); err != nil {
- return fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err)
- }
- // Update mountEntry.
- m.dstFile = dstFile
- return nil
-}
-
-func mountToRootfs(c *mountConfig, m mountEntry) error {
- defer func() {
- if m.dstFile != nil {
- _ = m.dstFile.Close()
- m.dstFile = nil
- }
- }()
-
+func mountToRootfs(c *mountConfig, m *mountEntry) error {
// procfs and sysfs are special because we need to ensure they are actually
// mounted on a specific path in a container without any funny business.
switch m.Device {
@@ -630,7 +521,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
// Do not use securejoin as it resolves symlinks.
dest = filepath.Join(rootfs, dest)
}
- if err := checkProcMount(rootfs, dest, m); err != nil {
+ if err := checkProcMount(rootfs, dest, *m); err != nil {
return err
}
if fi, err := os.Lstat(dest); err != nil {
@@ -674,12 +565,15 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
return err
case "bind":
// open_tree()-related shenanigans are all handled in mountViaFds.
- if err := m.mountPropagate(c.root, mountLabel); err != nil {
+ // As documented in mount(2): if MS_BIND is included in mountflags,
+ // the filesystem type and mount data arguments are ignored.
+ // Therefore, keep mountLabel empty for bind mounts.
+ if err := m.mountPropagate(c.root, ""); err != nil {
return err
}
// The initial MS_BIND won't change the mount options, we need to do a
- // separate MS_BIND|MS_REMOUNT to apply the mount options. We skip
+ // separate mount_setattr(2) to apply the mount options. We skip
// doing this if the user has not specified any mount flags at all
// (including cleared flags) -- in which case we just keep the original
// mount flags.
@@ -688,6 +582,26 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
// contrast to mount(8)'s current behaviour, but is what users probably
// expect. See .
if m.Flags & ^(unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT) != 0 || m.ClearedFlags != 0 {
+ // The kernel does not yet support MOUNT_ATTR_LAZYTIME.
+ if m.Flags&unix.MS_LAZYTIME == 0 {
+ // Try to use mount_setattr(2) with AT_EMPTY_PATH to apply the flags
+ // directly to the detached mount fd. This is more secure and efficient
+ // than the old MS_BIND|MS_REMOUNT approach.
+ attrSet, attrCls := msFlagsToMountAttr(m.Flags)
+ attr := unix.MountAttr{
+ Attr_set: attrSet,
+ Attr_clr: attrCls,
+ }
+ mountErr := unix.MountSetattr(int(m.dstFile.Fd()), "", unix.AT_EMPTY_PATH, &attr)
+ if mountErr == nil {
+ return setRecAttr(m)
+ }
+ if !errors.Is(mountErr, unix.ENOSYS) && !errors.Is(mountErr, unix.EOPNOTSUPP) && !errors.Is(mountErr, unix.EPERM) {
+ return mountErr
+ }
+ // fallback to old school MS_BIND|MS_REMOUNT.
+ logrus.Debugf("mount_setattr(2) failed for bind-mount %s, falling back to MS_BIND|MS_REMOUNT: %v", m.Destination, mountErr)
+ }
if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
flags := m.Flags | unix.MS_BIND | unix.MS_REMOUNT
// The runtime-spec says we SHOULD map to the relevant mount(8)
@@ -1330,17 +1244,39 @@ func verifyDevNull(f *os.File) error {
}
// maskDir mounts a read-only tmpfs on top of the specified path.
-func maskDir(path, mountLabel string) error {
- err := mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("nr_blocks=1,nr_inodes=1", mountLabel))
- if err != nil {
+// This returns the file descriptor of the tmpfs mount if the new mount api is used, or nil if the old mount api is used.
+// The caller is responsible for closing the returned file descriptor if it is not nil.
+func maskDir(dstFile *os.File, dstPath, mountLabel string) (*os.File, error) {
+ tmpfs := &configs.Mount{
+ Source: "tmpfs",
+ Device: "tmpfs",
+ Destination: dstPath,
+ Flags: unix.MS_RDONLY,
+ Data: "nr_blocks=1,nr_inodes=1",
+ }
+ entry := mountEntry{
+ Mount: tmpfs,
+ dstFile: dstFile,
+ }
+ entry.initMountSourceFd(entry.Flags, mountLabel)
+ if err := utils.WithProcfdFile(entry.dstFile, func(dstFd string) error {
+ return mountViaFds(entry.Mount.Source, entry.srcFile, entry.Mount.Destination, dstFd, entry.Mount.Device, uintptr(entry.Mount.Flags), label.FormatMountLabel(entry.Mount.Data, mountLabel))
+ }); err != nil {
+ if entry.isDetachedMount() {
+ _ = entry.srcFile.file.Close()
+ }
// On most kernels `nr_inodes=1` works fine. However, Ubuntu 20.04 (Focal) with
// the official 5.4 kernel carries a private patch in "mm/shmem.c" that rejects
// `nr_inodes<2`, so retry with `nr_inodes=2` here.
// For reference, search for "case Opt_nr_inodes" in:
// https://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/focal/plain/mm/shmem.c?h=Ubuntu-5.4.0-216.236
- err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("nr_blocks=1,nr_inodes=2", mountLabel))
+ err = mount("tmpfs", dstPath, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("nr_blocks=1,nr_inodes=2", mountLabel))
+ return nil, err
}
- return err
+ if entry.isDetachedMount() {
+ return entry.srcFile.file, nil
+ }
+ return nil, nil
}
// maskPaths masks the top of the specified paths inside a container to avoid
@@ -1412,23 +1348,29 @@ func maskPaths(rootFs string, paths []string, mountLabel string) error {
}
}
if bindFailed || sharedMaskSrc == nil {
- err = maskDir(path, mountLabel)
+ fd, err := maskDir(dstFh, path, mountLabel)
if err == nil && !bindFailed && sharedMaskSrc == nil {
// Establish this mount as the reusable shared source. reopenAfterMount
// resolves the underlying inode via procfs and re-opens it through
// rootFd, so the resulting fd is anchored to the real path inside the
// container rootfs even if path was a /proc/self/fd/N alias.
- rootFd, err := os.OpenFile(rootFs, unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_PATH, 0)
- if err != nil {
- return fmt.Errorf("open rootfs handle for masked paths: %w", err)
- }
- reopened, err := reopenAfterMount(rootFd, dstFh, unix.O_PATH|unix.O_CLOEXEC)
- rootFd.Close()
- if err != nil {
- return fmt.Errorf("can't reopen shared directory mask: %w", err)
+ if fd != nil {
+ sharedMaskFile = fd
+ } else {
+ rootFd, err := os.OpenFile(rootFs, unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_PATH, 0)
+ if err != nil {
+ return fmt.Errorf("open rootfs handle for masked paths: %w", err)
+ }
+ reopened, err := reopenAfterMount(rootFd, dstFh, unix.O_PATH|unix.O_CLOEXEC)
+ rootFd.Close()
+ if err != nil {
+ return fmt.Errorf("can't reopen shared directory mask: %w", err)
+ }
+ sharedMaskFile = reopened
}
- sharedMaskFile = reopened
sharedMaskSrc = &mountSource{Type: mountSourcePlain, file: sharedMaskFile}
+ } else if fd != nil {
+ _ = fd.Close()
}
}
} else {
@@ -1445,99 +1387,3 @@ func maskPaths(rootFs string, paths []string, mountLabel string) error {
return nil
}
-
-func reopenAfterMount(rootFd, f *os.File, flags int) (_ *os.File, Err error) {
- fullPath, err := procfs.ProcSelfFdReadlink(f)
- if err != nil {
- return nil, fmt.Errorf("get full path: %w", err)
- }
- rootfs := rootFd.Name()
- if !pathrs.IsLexicallyInRoot(rootfs, fullPath) {
- return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs)
- }
- unsafePath := pathrs.LexicallyStripRoot(rootfs, fullPath)
- reopened, err := pathrs.OpenInRoot(rootFd, unsafePath, flags)
- if err != nil {
- return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err)
- }
- defer func() {
- if Err != nil {
- _ = reopened.Close()
- }
- }()
-
- // NOTE: The best we can do here is confirm that the new mountpoint handle
- // matches the original target handle, but an attacker could've swapped a
- // different path to replace it. In the worst case this could result in us
- // applying later vfsmount flags onto the wrong mount.
- //
- // This is far from ideal, but the only way of doing this in a race-free
- // way is to switch the new mount API (move_mount(2) does not require this
- // re-opening step, and thus no such races are possible).
- reopenedFullPath, err := procfs.ProcSelfFdReadlink(reopened)
- if err != nil {
- return nil, fmt.Errorf("check full path of re-opened mountpoint: %w", err)
- }
- if reopenedFullPath != fullPath {
- return nil, fmt.Errorf("mountpoint %q was moved while re-opening", unsafePath)
- }
- return reopened, nil
-}
-
-// Do the mount operation followed by additional mounts required to take care
-// of propagation flags. This will always be scoped inside the container rootfs.
-func (m *mountEntry) mountPropagate(rootFd *os.File, mountLabel string) error {
- var (
- data = label.FormatMountLabel(m.Data, mountLabel)
- flags = m.Flags
- )
- // Delay mounting the filesystem read-only if we need to do further
- // operations on it. We need to set up files in "/dev", and other tmpfs
- // mounts may need to be chmod-ed after mounting. These mounts will be
- // remounted ro later in finalizeRootfs(), if necessary.
- if m.Device == "tmpfs" || pathrs.LexicallyCleanPath(m.Destination) == "/dev" {
- flags &= ^unix.MS_RDONLY
- }
-
- if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
- return mountViaFds(m.Source, m.srcFile, m.Destination, dstFd, m.Device, uintptr(flags), data)
- }); err != nil {
- return err
- }
-
- // We need to re-open the mountpoint after doing the mount, in order for us
- // to operate on the new mount we just created. However, we cannot use
- // pathrs.Reopen because we need to re-resolve from the parent directory to
- // get a new handle to the top mount.
- //
- // TODO: Use move_mount(2) on newer kernels so that this is no longer
- // necessary on modern systems.
- newDstFile, err := reopenAfterMount(rootFd, m.dstFile, unix.O_PATH)
- if err != nil {
- return fmt.Errorf("reopen mountpoint after mount: %w", err)
- }
- _ = m.dstFile.Close()
- m.dstFile = newDstFile
-
- // Apply the propagation flags on the new mount.
- if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
- for _, pflag := range m.PropagationFlags {
- if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil {
- return err
- }
- }
- return nil
- }); err != nil {
- return fmt.Errorf("change mount propagation through procfd: %w", err)
- }
- return nil
-}
-
-func setRecAttr(m mountEntry) error {
- if m.RecAttr == nil {
- return nil
- }
- return utils.WithProcfdFile(m.dstFile, func(procfd string) error {
- return unix.MountSetattr(-1, procfd, unix.AT_RECURSIVE, m.RecAttr)
- })
-}
diff --git a/tests/integration/mounts_sshfs.bats b/tests/integration/mounts_sshfs.bats
index ad7c215c5c1..fbdf63b978b 100644
--- a/tests/integration/mounts_sshfs.bats
+++ b/tests/integration/mounts_sshfs.bats
@@ -133,9 +133,6 @@ function fail_sshfs_bind_flags() {
run -0 grep -wq rw <<<"$mnt_flags"
run ! grep -wq noexec <<<"$mnt_flags"
run ! grep -wq nosymfollow <<<"$mnt_flags"
- # FIXME FIXME: As with mount(8), trying to clear an atime flag the "naive"
- # way will be ignored!
- run -0 grep -wq nodiratime <<<"$mnt_flags"
# Now try with a user namespace.
update_config ' .linux.namespaces += [{"type": "user"}]
@@ -212,9 +209,6 @@ function fail_sshfs_bind_flags() {
run ! grep -wq nosuid <<<"$mnt_flags"
run ! grep -wq nodev <<<"$mnt_flags"
run ! grep -wq noexec <<<"$mnt_flags"
- # FIXME FIXME: As with mount(8), trying to clear an atime flag the "naive"
- # way will be ignored!
- run -0 grep -wq noatime <<<"$mnt_flags"
# Now try with a user namespace.
update_config ' .linux.namespaces += [{"type": "user"}]
@@ -300,8 +294,6 @@ function fail_sshfs_bind_flags() {
# Unspecified flags must be cleared.
run ! grep -wq nosuid <<<"$mnt_flags"
run ! grep -wq nodev <<<"$mnt_flags"
- # FIXME: As with mount(8), runc keeps the old atime setting by default.
- run -0 grep -wq noatime <<<"$mnt_flags"
# Now try with a user namespace.
update_config ' .linux.namespaces += [{"type": "user"}]
@@ -329,22 +321,6 @@ function fail_sshfs_bind_flags() {
run ! grep -wq nodiratime <<<"${1:-$mnt_flags}"
}
- # FIXME: As with mount(8), runc keeps the old atime setting by default.
- pass_sshfs_bind_flags "noatime" "bind"
- run -0 grep -wq noatime <<<"$mnt_flags"
- run ! grep -wq relatime <<<"$mnt_flags"
-
- # FIXME: As with mount(8), runc keeps the old atime setting by default.
- pass_sshfs_bind_flags "noatime" "bind,norelatime"
- run -0 grep -wq noatime <<<"$mnt_flags"
- run ! grep -wq relatime <<<"$mnt_flags"
-
- # FIXME FIXME: As with mount(8), trying to clear an atime flag the "naive"
- # way will be ignored!
- pass_sshfs_bind_flags "noatime" "bind,atime"
- run -0 grep -wq noatime <<<"$mnt_flags"
- run ! grep -wq relatime <<<"$mnt_flags"
-
# ... but explicitly setting a different flag works.
pass_sshfs_bind_flags "noatime" "bind,relatime"
run ! grep -wq noatime <<<"$mnt_flags"
@@ -370,11 +346,8 @@ function fail_sshfs_bind_flags() {
run ! grep -wq noatime <<<"$mnt_flags"
run -0 grep -wq nodiratime <<<"$mnt_flags"
run ! grep -wq relatime <<<"$mnt_flags"
- # FIXME FIXME: relatime should not be set in this case.
pass_sshfs_bind_flags "noatime" "bind,nodiratime,norelatime"
- run ! grep -wq noatime <<<"$mnt_flags"
run -0 grep -wq nodiratime <<<"$mnt_flags"
- run -0 grep -wq relatime <<<"$mnt_flags"
# Now try with a user namespace.
update_config ' .linux.namespaces += [{"type": "user"}]
diff --git a/tests/integration/selinux.bats b/tests/integration/selinux.bats
index 66665c45059..4ce65e6f39d 100644
--- a/tests/integration/selinux.bats
+++ b/tests/integration/selinux.bats
@@ -87,6 +87,23 @@ function enable_userns() {
[ "$status" -eq 0 ]
}
+@test "runc run (custom mount label)" {
+ MOUNTLABEL="system_u:object_r:container_file_t:s0:c344,c805"
+ update_config ' .process.selinuxLabel |= "system_u:system_r:container_t:s0:c4,c5"
+ | .process.args = ["/bin/sleep", "infinite"]
+ | .linux.mountLabel="'"$MOUNTLABEL"'"'
+ runc run -d --console-socket "$CONSOLE_SOCKET" tst
+ [ "$status" -eq 0 ]
+
+ runc exec tst getfattr -n security.selinux /dev
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"$MOUNTLABEL"* ]]
+
+ runc exec tst getfattr -n security.selinux /proc
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"system_u:object_r:proc_t:s0"* ]]
+}
+
@test "runc run (session keyring security label)" {
run_check_label
}