diff --git a/internal/pathrs/mkdirall.go b/internal/pathrs/mkdirall.go index 3a896f48415..81c9a022c74 100644 --- a/internal/pathrs/mkdirall.go +++ b/internal/pathrs/mkdirall.go @@ -31,12 +31,12 @@ import ( // Callers need to be very careful operating on the trailing path, as trivial // mistakes like following symlinks can cause security bugs. Most people // should probably just use [MkdirAllInRoot] or [CreateInRoot]. -func MkdirAllParentInRoot(root, unsafePath string, mode os.FileMode) (*os.File, string, error) { +func MkdirAllParentInRoot(root *os.File, unsafePath string, mode os.FileMode) (*os.File, string, error) { // MkdirAllInRoot also does hallucinateUnsafePath, but we need to do it // here first because when we split unsafePath into (dir, file) components // we want to be doing so with the hallucinated path (so that trailing // dangling symlinks are treated correctly). - unsafePath, err := hallucinateUnsafePath(root, unsafePath) + unsafePath, err := hallucinateUnsafePath(root.Name(), unsafePath) if err != nil { return nil, "", fmt.Errorf("failed to construct hallucinated target path: %w", err) } diff --git a/internal/pathrs/mkdirall_pathrslite.go b/internal/pathrs/mkdirall_pathrslite.go index c2578e051fe..0a6f25ebdc2 100644 --- a/internal/pathrs/mkdirall_pathrslite.go +++ b/internal/pathrs/mkdirall_pathrslite.go @@ -24,12 +24,11 @@ import ( "github.com/cyphar/filepath-securejoin/pathrs-lite" "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" ) // MkdirAllInRoot attempts to make // -// path, _ := securejoin.SecureJoin(root, unsafePath) +// path, _ := securejoin.SecureJoin(root.Name(), unsafePath) // os.MkdirAll(path, mode) // os.Open(path) // @@ -48,8 +47,8 @@ import ( // handling if unsafePath has already been scoped within the rootfs (this is // needed for a lot of runc callers and fixing this would require reworking a // lot of path logic). -func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) { - unsafePath, err := hallucinateUnsafePath(root, unsafePath) +func MkdirAllInRoot(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) { + unsafePath, err := hallucinateUnsafePath(root.Name(), unsafePath) if err != nil { return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err) } @@ -67,13 +66,7 @@ func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) mode &= 0o1777 } - rootDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0) - if err != nil { - return nil, fmt.Errorf("open root handle: %w", err) - } - defer rootDir.Close() - return retryEAGAIN(func() (*os.File, error) { - return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) + return pathrs.MkdirAllHandle(root, unsafePath, mode) }) } diff --git a/internal/pathrs/root_pathrslite.go b/internal/pathrs/root_pathrslite.go index 51db77440d7..0ddabf80429 100644 --- a/internal/pathrs/root_pathrslite.go +++ b/internal/pathrs/root_pathrslite.go @@ -28,11 +28,11 @@ import ( ) // OpenInRoot opens the given path inside the root with the provided flags. It -// is effectively shorthand for [securejoin.OpenInRoot] followed by +// is effectively shorthand for [securejoin.OpenatInRoot] followed by // [securejoin.Reopen]. -func OpenInRoot(root, subpath string, flags int) (*os.File, error) { +func OpenInRoot(root *os.File, subpath string, flags int) (*os.File, error) { handle, err := retryEAGAIN(func() (*os.File, error) { - return pathrs.OpenInRoot(root, subpath) + return pathrs.OpenatInRoot(root, subpath) }) if err != nil { return nil, err @@ -47,7 +47,7 @@ func OpenInRoot(root, subpath string, flags int) (*os.File, error) { // open(O_CREAT|O_NOFOLLOW) semantics. If you want the creation to use O_EXCL, // include it in the passed flags. The fileMode argument uses unix.* mode bits, // *not* os.FileMode. -func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, error) { +func CreateInRoot(root *os.File, subpath string, flags int, fileMode uint32) (*os.File, error) { dirFd, filename, err := MkdirAllParentInRoot(root, subpath, 0o755) if err != nil { return nil, err @@ -63,5 +63,5 @@ func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, e if err != nil { return nil, err } - return os.NewFile(uintptr(fd), root+"/"+subpath), nil + return os.NewFile(uintptr(fd), root.Name()+"/"+subpath), nil } diff --git a/libcontainer/criu_linux.go b/libcontainer/criu_linux.go index 322e439578b..736743833c9 100644 --- a/libcontainer/criu_linux.go +++ b/libcontainer/criu_linux.go @@ -540,10 +540,16 @@ func isOnTmpfs(path string, mounts []*configs.Mount) bool { // This function also creates missing mountpoints as long as they // are not on top of a tmpfs, as CRIU will restore tmpfs content anyway. func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error { + rootFd, err := os.OpenFile(c.config.Rootfs, unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_PATH, 0) + if err != nil { + return fmt.Errorf("open rootfs handle: %w", err) + } + defer rootFd.Close() + umounts := []string{} defer func() { for _, u := range umounts { - mntFile, err := pathrs.OpenInRoot(c.config.Rootfs, u, unix.O_PATH) + mntFile, err := pathrs.OpenInRoot(rootFd, u, unix.O_PATH) if err != nil { logrus.Warnf("Error during cleanup unmounting %s: open handle: %v", u, err) continue @@ -577,7 +583,7 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error { continue } me := mountEntry{Mount: m} - if err := me.createOpenMountpoint(c.config.Rootfs); err != nil { + if err := me.createOpenMountpoint(rootFd); err != nil { return fmt.Errorf("create criu restore mountpoint for %s mount: %w", me.Destination, err) } if me.dstFile != nil { diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 7aaa70d109f..4eb1cedafac 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -35,7 +35,7 @@ const defaultMountFlags = unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV // mountConfig contains mount data not specific to a mount point. type mountConfig struct { - root string + root *os.File label string cgroup2Path string rootlessCgroups bool @@ -160,8 +160,17 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) { return fmt.Errorf("error preparing rootfs: %w", err) } + // Pre-open rootfs. NOTE that if we need to re-enable support for mounting + // on top of container root (see issue 5070), we will need to reopen rootFd + // after such mounts. + rootFd, err := os.OpenFile(config.Rootfs, unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_PATH, 0) + if err != nil { + return fmt.Errorf("open rootfs handle: %w", err) + } + defer rootFd.Close() + mountConfig := &mountConfig{ - root: config.Rootfs, + root: rootFd, label: config.MountLabel, cgroup2Path: iConfig.Cgroup2Path, rootlessCgroups: config.RootlessCgroups, @@ -175,7 +184,7 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) { setupDev := needsSetupDev(config) if setupDev { - if err := createDevices(config); err != nil { + if err := createDevices(config, rootFd); err != nil { return fmt.Errorf("error creating device nodes: %w", err) } if err := setupPtmx(config); err != nil { @@ -203,7 +212,7 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) { // container. It's just cleaner to do this here (at the expense of the // operation not being perfectly split). - if err := unix.Chdir(config.Rootfs); err != nil { + if err := unix.Fchdir(int(rootFd.Fd())); err != nil { return &os.PathError{Op: "chdir", Path: config.Rootfs, Err: err} } @@ -218,13 +227,14 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) { if config.NoPivotRoot { err = msMoveRoot(config.Rootfs) } else if config.Namespaces.Contains(configs.NEWNS) { - err = pivotRoot(config.Rootfs) + err = pivotRoot(rootFd) } else { err = chroot() } if err != nil { return fmt.Errorf("error jailing process inside rootfs: %w", err) } + rootFd.Close() // Apply root mount propagation flags. // This must be done after pivot_root/chroot because the mount propagation flag is applied @@ -336,10 +346,8 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error { // We just created the tmpfs, and so we can just use filepath.Join // here (not to mention we want to make sure we create the path // inside the tmpfs, so we don't want to resolve symlinks). - // TODO: Why not just use b.Destination (c.root is the root here)? - subsystemPath := filepath.Join(c.root, b.Destination) subsystemName := filepath.Base(b.Destination) - subsystemDir, err := pathrs.MkdirAllInRoot(c.root, subsystemPath, 0o755) + subsystemDir, err := pathrs.MkdirAllInRoot(c.root, b.Destination, 0o755) if err != nil { return err } @@ -372,7 +380,7 @@ func mountCgroupV1(m mountEntry, c *mountConfig) error { // symlink(2) is very dumb, it will just shove the path into // the link and doesn't do any checks or relative path // conversion. Also, don't error out if the cgroup already exists. - if err := os.Symlink(mc, filepath.Join(c.root, m.Destination, ss)); err != nil && !errors.Is(err, os.ErrExist) { + if err := os.Symlink(mc, filepath.Join(c.root.Name(), m.Destination, ss)); err != nil && !errors.Is(err, os.ErrExist) { return err } } @@ -411,7 +419,7 @@ 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 maskPaths([]string{procfd}, c.label) + return maskDir(procfd, c.label) }) } return err @@ -436,15 +444,22 @@ func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) { } defer tmpDirFile.Close() + hostRootFd, err := os.OpenFile("/", unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_PATH, 0) + if err != nil { + return fmt.Errorf("tmpcopyup: open host root: %w", err) + } + defer hostRootFd.Close() + // Configure the *host* tmpdir as if it's the container mount. We change // m.dstFile since we are going to mount *on the host*. hostMount := mountEntry{ Mount: m.Mount, dstFile: tmpDirFile, } - if err := hostMount.mountPropagate("/", mountLabel); err != nil { + if err := hostMount.mountPropagate(hostRootFd, mountLabel); err != nil { return err } + hostRootFd.Close() defer func() { if Err != nil { if err := unmount(tmpDir, unix.MNT_DETACH); err != nil { @@ -513,9 +528,10 @@ func statfsToMountFlags(st unix.Statfs_t) int { return flags } -func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { +func (m *mountEntry) createOpenMountpoint(root *os.File) (Err error) { + rootfs := root.Name() unsafePath := pathrs.LexicallyStripRoot(rootfs, m.Destination) - dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH) + dstFile, err := pathrs.OpenInRoot(root, unsafePath, unix.O_PATH) defer func() { if dstFile != nil && Err != nil { _ = dstFile.Close() @@ -552,9 +568,9 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { dstIsFile = !fi.IsDir() } if dstIsFile { - dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) + dstFile, err = pathrs.CreateInRoot(root, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644) } else { - dstFile, err = pathrs.MkdirAllInRoot(rootfs, unsafePath, 0o755) + dstFile, err = pathrs.MkdirAllInRoot(root, unsafePath, 0o755) } if err != nil { return fmt.Errorf("make mountpoint %q: %w", m.Destination, err) @@ -584,7 +600,6 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) { } func mountToRootfs(c *mountConfig, m mountEntry) error { - rootfs := c.root defer func() { if m.dstFile != nil { _ = m.dstFile.Close() @@ -601,6 +616,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { // has been a "fun" attack scenario in the past. // TODO: This won't be necessary once we switch to libpathrs and we can // stop all of these symlink-exchange attacks. + rootfs := c.root.Name() dest := filepath.Clean(m.Destination) if !pathrs.IsLexicallyInRoot(rootfs, dest) { // Do not use securejoin as it resolves symlinks. @@ -616,7 +632,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { } else if !fi.IsDir() { return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device) } - dstFile, err := pathrs.MkdirAllInRoot(rootfs, dest, 0o755) + dstFile, err := pathrs.MkdirAllInRoot(c.root, dest, 0o755) if err != nil { return err } @@ -624,17 +640,17 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { // "proc" and "sys" mounts need special handling (without resolving the // destination) to avoid attacks. m.dstFile = dstFile - return m.mountPropagate(rootfs, "") + return m.mountPropagate(c.root, "") } mountLabel := c.label - if err := m.createOpenMountpoint(rootfs); err != nil { + if err := m.createOpenMountpoint(c.root); err != nil { return fmt.Errorf("create mountpoint for %s mount: %w", m.Destination, err) } switch m.Device { case "mqueue": - if err := m.mountPropagate(rootfs, ""); err != nil { + if err := m.mountPropagate(c.root, ""); err != nil { return err } return utils.WithProcfdFile(m.dstFile, func(dstFd string) error { @@ -645,12 +661,12 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { if m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP { err = doTmpfsCopyUp(m, mountLabel) } else { - err = m.mountPropagate(rootfs, mountLabel) + err = m.mountPropagate(c.root, mountLabel) } return err case "bind": // open_tree()-related shenanigans are all handled in mountViaFds. - if err := m.mountPropagate(rootfs, mountLabel); err != nil { + if err := m.mountPropagate(c.root, mountLabel); err != nil { return err } @@ -772,7 +788,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error { } return mountCgroupV1(m, c) default: - return m.mountPropagate(rootfs, mountLabel) + return m.mountPropagate(c.root, mountLabel) } } @@ -943,7 +959,7 @@ func reOpenDevNull() error { } // Create the device nodes in the container. -func createDevices(config *configs.Config) error { +func createDevices(config *configs.Config, rootFd *os.File) error { useBindMount := userns.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER) for _, node := range config.Devices { @@ -954,7 +970,7 @@ func createDevices(config *configs.Config) error { // containers running in a user namespace are not allowed to mknod // devices so we can just bind mount it from the host. - if err := createDeviceNode(config.Rootfs, node, useBindMount); err != nil { + if err := createDeviceNode(rootFd, node, useBindMount); err != nil { return err } } @@ -974,12 +990,12 @@ func bindMountDeviceNode(destDir *os.File, destName string, node *devices.Device } // Creates the device node in the rootfs of the container. -func createDeviceNode(rootfs string, node *devices.Device, bind bool) error { +func createDeviceNode(rootFd *os.File, node *devices.Device, bind bool) error { if node.Path == "" { // The node only exists for cgroup reasons, ignore it here. return nil } - destDir, destName, err := pathrs.MkdirAllParentInRoot(rootfs, node.Path, 0o755) + destDir, destName, err := pathrs.MkdirAllParentInRoot(rootFd, node.Path, 0o755) if err != nil { return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err) } @@ -1128,28 +1144,22 @@ func setupPtmx(config *configs.Config) error { // pivotRoot will call pivot_root such that rootfs becomes the new root // filesystem, and everything else is cleaned up. -func pivotRoot(rootfs string) error { +func pivotRoot(root *os.File) error { // While the documentation may claim otherwise, pivot_root(".", ".") is // actually valid. What this results in is / being the new root but // /proc/self/cwd being the old root. Since we can play around with the cwd // with pivot_root this allows us to pivot without creating directories in // the rootfs. Shout-outs to the LXC developers for giving us this idea. - oldroot, err := linux.Open("/", unix.O_DIRECTORY|unix.O_RDONLY, 0) + oldroot, err := linux.Open("/", unix.O_DIRECTORY|unix.O_RDONLY|unix.O_PATH, 0) if err != nil { return err } defer unix.Close(oldroot) - newroot, err := linux.Open(rootfs, unix.O_DIRECTORY|unix.O_RDONLY, 0) - if err != nil { - return err - } - defer unix.Close(newroot) - // Change to the new root so that the pivot_root actually acts on it. - if err := unix.Fchdir(newroot); err != nil { - return &os.PathError{Op: "fchdir", Path: "fd " + strconv.Itoa(newroot), Err: err} + if err := unix.Fchdir(int(root.Fd())); err != nil { + return &os.PathError{Op: "chdir", Path: root.Name(), Err: err} } if err := unix.PivotRoot(".", "."); err != nil { @@ -1320,12 +1330,20 @@ func verifyDevNull(f *os.File) error { }) } +// maskDir mounts a read-only tmpfs on top of the specified path. +func maskDir(path, mountLabel string) error { + return mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("nr_blocks=1,nr_inodes=1", mountLabel)) +} + // maskPaths masks the top of the specified paths inside a container to avoid // security issues from processes reading information from non-namespace aware // mounts ( proc/kcore ). // For files, maskPath bind mounts /dev/null over the top of the specified path. // For directories, maskPath mounts read-only tmpfs over the top of the specified path. -func maskPaths(paths []string, mountLabel string) error { +func maskPaths(rootFs string, paths []string, mountLabel string) error { + if len(paths) == 0 { + return nil + } devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0) if err != nil { return fmt.Errorf("can't mask paths: %w", err) @@ -1338,6 +1356,17 @@ func maskPaths(paths []string, mountLabel string) error { procSelfFd, closer := utils.ProcThreadSelf("fd/") defer closer() + var ( + sharedMaskFile *os.File + sharedMaskSrc *mountSource + bindFailed bool + ) + defer func() { + if sharedMaskFile != nil { + _ = sharedMaskFile.Close() + } + }() + maskedPaths := make(map[string]struct{}) for _, path := range paths { // Open the target path; skip if it doesn't exist. dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0) @@ -1352,11 +1381,48 @@ func maskPaths(paths []string, mountLabel string) error { dstFh.Close() return fmt.Errorf("can't mask path %q: %w", path, err) } + // skip duplicate masked paths. + cleanPath := pathrs.LexicallyCleanPath(path) + if _, ok := maskedPaths[cleanPath]; ok { + dstFh.Close() + continue + } + maskedPaths[cleanPath] = struct{}{} + var dstType string if st.IsDir() { // Destination is a directory: bind mount a ro tmpfs over it. dstType = "dir" - err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel)) + if !bindFailed && sharedMaskSrc != nil { + dstFd := filepath.Join(procSelfFd, strconv.Itoa(int(dstFh.Fd()))) + err = mountViaFds("", sharedMaskSrc, path, dstFd, "", unix.MS_BIND, "") + if err != nil { + // A bind-mount inherits MNT_READONLY from the source vfsmount, + // but if it fails fall back to individual tmpfs mounts. + bindFailed = true + logrus.WithError(err).Warn("maskPaths: shared tmpfs bind-mount failed, falling back to per-directory tmpfs") + } + } + if bindFailed || sharedMaskSrc == nil { + err = maskDir(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) + } + sharedMaskFile = reopened + sharedMaskSrc = &mountSource{Type: mountSourcePlain, file: sharedMaskFile} + } + } } else { // Destination is a file: mount it to /dev/null. dstType = "path" @@ -1372,16 +1438,17 @@ func maskPaths(paths []string, mountLabel string) error { return nil } -func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err error) { +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(rootfs, unsafePath, flags) + reopened, err := pathrs.OpenInRoot(rootFd, unsafePath, flags) if err != nil { return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err) } @@ -1411,7 +1478,7 @@ func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err 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(rootfs, mountLabel string) error { +func (m *mountEntry) mountPropagate(rootFd *os.File, mountLabel string) error { var ( data = label.FormatMountLabel(m.Data, mountLabel) flags = m.Flags @@ -1437,7 +1504,7 @@ func (m *mountEntry) mountPropagate(rootfs, mountLabel string) error { // // TODO: Use move_mount(2) on newer kernels so that this is no longer // necessary on modern systems. - newDstFile, err := reopenAfterMount(rootfs, m.dstFile, unix.O_PATH) + newDstFile, err := reopenAfterMount(rootFd, m.dstFile, unix.O_PATH) if err != nil { return fmt.Errorf("reopen mountpoint after mount: %w", err) } diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go index 570472bd41f..933c1de3441 100644 --- a/libcontainer/standard_init_linux.go +++ b/libcontainer/standard_init_linux.go @@ -142,9 +142,10 @@ func (l *linuxStandardInit) Init() error { } } - if err := maskPaths(l.config.Config.MaskPaths, l.config.Config.MountLabel); err != nil { + if err := maskPaths("/", l.config.Config.MaskPaths, l.config.Config.MountLabel); err != nil { return err } + pdeath, err := system.GetParentDeathSignal() if err != nil { return fmt.Errorf("can't get pdeath signal: %w", err) diff --git a/tests/integration/mask.bats b/tests/integration/mask.bats index 5783332ea18..782edbb3033 100644 --- a/tests/integration/mask.bats +++ b/tests/integration/mask.bats @@ -6,7 +6,7 @@ function setup() { setup_busybox # Create fake rootfs. - mkdir rootfs/testdir + mkdir rootfs/testdir rootfs/testdir2 rootfs/testdir3 echo "Forbidden information!" >rootfs/testfile # add extra masked paths @@ -55,6 +55,20 @@ function teardown() { [[ "${output}" == *"Operation not permitted"* ]] } +@test "mask paths [duplicate paths]" { + update_config '(.. | select(.maskedPaths? != null)) .maskedPaths += ["/testdir", "/testfile"]' + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox + [ "$status" -eq 0 ] + + runc exec test_busybox sh -c "mount | grep /testdir -c" + [ "$status" -eq 0 ] + [[ "${output}" == "1" ]] + + runc exec test_busybox sh -c "mount | grep /testfile -c" + [ "$status" -eq 0 ] + [[ "${output}" == "1" ]] +} + @test "mask paths [prohibit symlink /proc]" { ln -s /symlink rootfs/proc runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox @@ -73,3 +87,33 @@ function teardown() { # so we merely check that it fails, and do not check the exact error # message like for /proc above. } + +@test "mask paths [directories share tmpfs]" { + update_config '(.. | select(.maskedPaths? != null)) .maskedPaths += ["/testdir2", "/testdir3"]' + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox + [ "$status" -eq 0 ] + + # shellcheck disable=SC2016 + runc exec test_busybox sh -euc ' + set -- $(stat -c %d /testdir /testdir2 /testdir3) + [ "$1" = "$2" ] + [ "$2" = "$3" ] + ' + [ "$status" -eq 0 ] + + runc exec test_busybox touch /testdir2/foo + [ "$status" -eq 1 ] + [[ "${output}" == *"Read-only file system"* ]] +} + +@test "mask paths [directory with read-only rootfs]" { + update_config '(.. | select(.maskedPaths? != null)) .maskedPaths += ["/testdir2", "/testdir3"]' + update_config '.root.readonly = true' + + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox + [ "$status" -eq 0 ] + + runc exec test_busybox ls /testdir + [ "$status" -eq 0 ] + [ -z "$output" ] +}