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 }