Merge pull request #5281 from lifubang/backport-5275-1.4

[1.4] libct: reuse tmpfs for directory masks
This commit is contained in:
Kir Kolyshkin
2026-05-21 16:05:45 -07:00
committed by GitHub
7 changed files with 176 additions and 65 deletions
+2 -2
View File
@@ -31,12 +31,12 @@ import (
// Callers need to be very careful operating on the trailing path, as trivial // Callers need to be very careful operating on the trailing path, as trivial
// mistakes like following symlinks can cause security bugs. Most people // mistakes like following symlinks can cause security bugs. Most people
// should probably just use [MkdirAllInRoot] or [CreateInRoot]. // 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 // MkdirAllInRoot also does hallucinateUnsafePath, but we need to do it
// here first because when we split unsafePath into (dir, file) components // here first because when we split unsafePath into (dir, file) components
// we want to be doing so with the hallucinated path (so that trailing // we want to be doing so with the hallucinated path (so that trailing
// dangling symlinks are treated correctly). // dangling symlinks are treated correctly).
unsafePath, err := hallucinateUnsafePath(root, unsafePath) unsafePath, err := hallucinateUnsafePath(root.Name(), unsafePath)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to construct hallucinated target path: %w", err) return nil, "", fmt.Errorf("failed to construct hallucinated target path: %w", err)
} }
+4 -11
View File
@@ -24,12 +24,11 @@ import (
"github.com/cyphar/filepath-securejoin/pathrs-lite" "github.com/cyphar/filepath-securejoin/pathrs-lite"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
) )
// MkdirAllInRoot attempts to make // MkdirAllInRoot attempts to make
// //
// path, _ := securejoin.SecureJoin(root, unsafePath) // path, _ := securejoin.SecureJoin(root.Name(), unsafePath)
// os.MkdirAll(path, mode) // os.MkdirAll(path, mode)
// os.Open(path) // os.Open(path)
// //
@@ -48,8 +47,8 @@ import (
// handling if unsafePath has already been scoped within the rootfs (this is // 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 // needed for a lot of runc callers and fixing this would require reworking a
// lot of path logic). // lot of path logic).
func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) (*os.File, error) { func MkdirAllInRoot(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) {
unsafePath, err := hallucinateUnsafePath(root, unsafePath) unsafePath, err := hallucinateUnsafePath(root.Name(), unsafePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to construct hallucinated target path: %w", err) 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 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 retryEAGAIN(func() (*os.File, error) {
return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) return pathrs.MkdirAllHandle(root, unsafePath, mode)
}) })
} }
+5 -5
View File
@@ -28,11 +28,11 @@ import (
) )
// OpenInRoot opens the given path inside the root with the provided flags. It // 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]. // [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) { handle, err := retryEAGAIN(func() (*os.File, error) {
return pathrs.OpenInRoot(root, subpath) return pathrs.OpenatInRoot(root, subpath)
}) })
if err != nil { if err != nil {
return nil, err 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, // 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, // include it in the passed flags. The fileMode argument uses unix.* mode bits,
// *not* os.FileMode. // *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) dirFd, filename, err := MkdirAllParentInRoot(root, subpath, 0o755)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -63,5 +63,5 @@ func CreateInRoot(root, subpath string, flags int, fileMode uint32) (*os.File, e
if err != nil { if err != nil {
return nil, err return nil, err
} }
return os.NewFile(uintptr(fd), root+"/"+subpath), nil return os.NewFile(uintptr(fd), root.Name()+"/"+subpath), nil
} }
+8 -2
View File
@@ -540,10 +540,16 @@ func isOnTmpfs(path string, mounts []*configs.Mount) bool {
// This function also creates missing mountpoints as long as they // This function also creates missing mountpoints as long as they
// are not on top of a tmpfs, as CRIU will restore tmpfs content anyway. // are not on top of a tmpfs, as CRIU will restore tmpfs content anyway.
func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error { 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{} umounts := []string{}
defer func() { defer func() {
for _, u := range umounts { 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 { if err != nil {
logrus.Warnf("Error during cleanup unmounting %s: open handle: %v", u, err) logrus.Warnf("Error during cleanup unmounting %s: open handle: %v", u, err)
continue continue
@@ -577,7 +583,7 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
continue continue
} }
me := mountEntry{Mount: m} 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) return fmt.Errorf("create criu restore mountpoint for %s mount: %w", me.Destination, err)
} }
if me.dstFile != nil { if me.dstFile != nil {
+110 -43
View File
@@ -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. // mountConfig contains mount data not specific to a mount point.
type mountConfig struct { type mountConfig struct {
root string root *os.File
label string label string
cgroup2Path string cgroup2Path string
rootlessCgroups bool rootlessCgroups bool
@@ -160,8 +160,17 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) {
return fmt.Errorf("error preparing rootfs: %w", err) 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{ mountConfig := &mountConfig{
root: config.Rootfs, root: rootFd,
label: config.MountLabel, label: config.MountLabel,
cgroup2Path: iConfig.Cgroup2Path, cgroup2Path: iConfig.Cgroup2Path,
rootlessCgroups: config.RootlessCgroups, rootlessCgroups: config.RootlessCgroups,
@@ -175,7 +184,7 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) {
setupDev := needsSetupDev(config) setupDev := needsSetupDev(config)
if setupDev { if setupDev {
if err := createDevices(config); err != nil { if err := createDevices(config, rootFd); err != nil {
return fmt.Errorf("error creating device nodes: %w", err) return fmt.Errorf("error creating device nodes: %w", err)
} }
if err := setupPtmx(config); err != nil { 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 // container. It's just cleaner to do this here (at the expense of the
// operation not being perfectly split). // 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} 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 { if config.NoPivotRoot {
err = msMoveRoot(config.Rootfs) err = msMoveRoot(config.Rootfs)
} else if config.Namespaces.Contains(configs.NEWNS) { } else if config.Namespaces.Contains(configs.NEWNS) {
err = pivotRoot(config.Rootfs) err = pivotRoot(rootFd)
} else { } else {
err = chroot() err = chroot()
} }
if err != nil { if err != nil {
return fmt.Errorf("error jailing process inside rootfs: %w", err) return fmt.Errorf("error jailing process inside rootfs: %w", err)
} }
rootFd.Close()
// Apply root mount propagation flags. // Apply root mount propagation flags.
// This must be done after pivot_root/chroot because the mount propagation flag is applied // 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 // 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 // here (not to mention we want to make sure we create the path
// inside the tmpfs, so we don't want to resolve symlinks). // 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) 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 { if err != nil {
return err 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 // symlink(2) is very dumb, it will just shove the path into
// the link and doesn't do any checks or relative path // the link and doesn't do any checks or relative path
// conversion. Also, don't error out if the cgroup already exists. // 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 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 // 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`). // with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`).
err = utils.WithProcfdFile(m.dstFile, func(procfd string) error { err = utils.WithProcfdFile(m.dstFile, func(procfd string) error {
return maskPaths([]string{procfd}, c.label) return maskDir(procfd, c.label)
}) })
} }
return err return err
@@ -436,15 +444,22 @@ func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) {
} }
defer tmpDirFile.Close() 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 // Configure the *host* tmpdir as if it's the container mount. We change
// m.dstFile since we are going to mount *on the host*. // m.dstFile since we are going to mount *on the host*.
hostMount := mountEntry{ hostMount := mountEntry{
Mount: m.Mount, Mount: m.Mount,
dstFile: tmpDirFile, dstFile: tmpDirFile,
} }
if err := hostMount.mountPropagate("/", mountLabel); err != nil { if err := hostMount.mountPropagate(hostRootFd, mountLabel); err != nil {
return err return err
} }
hostRootFd.Close()
defer func() { defer func() {
if Err != nil { if Err != nil {
if err := unmount(tmpDir, unix.MNT_DETACH); err != nil { if err := unmount(tmpDir, unix.MNT_DETACH); err != nil {
@@ -513,9 +528,10 @@ func statfsToMountFlags(st unix.Statfs_t) int {
return flags 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) 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() { defer func() {
if dstFile != nil && Err != nil { if dstFile != nil && Err != nil {
_ = dstFile.Close() _ = dstFile.Close()
@@ -552,9 +568,9 @@ func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) {
dstIsFile = !fi.IsDir() dstIsFile = !fi.IsDir()
} }
if dstIsFile { 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 { } else {
dstFile, err = pathrs.MkdirAllInRoot(rootfs, unsafePath, 0o755) dstFile, err = pathrs.MkdirAllInRoot(root, unsafePath, 0o755)
} }
if err != nil { if err != nil {
return fmt.Errorf("make mountpoint %q: %w", m.Destination, err) 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 { func mountToRootfs(c *mountConfig, m mountEntry) error {
rootfs := c.root
defer func() { defer func() {
if m.dstFile != nil { if m.dstFile != nil {
_ = m.dstFile.Close() _ = m.dstFile.Close()
@@ -601,6 +616,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
// has been a "fun" attack scenario in the past. // has been a "fun" attack scenario in the past.
// TODO: This won't be necessary once we switch to libpathrs and we can // TODO: This won't be necessary once we switch to libpathrs and we can
// stop all of these symlink-exchange attacks. // stop all of these symlink-exchange attacks.
rootfs := c.root.Name()
dest := filepath.Clean(m.Destination) dest := filepath.Clean(m.Destination)
if !pathrs.IsLexicallyInRoot(rootfs, dest) { if !pathrs.IsLexicallyInRoot(rootfs, dest) {
// Do not use securejoin as it resolves symlinks. // Do not use securejoin as it resolves symlinks.
@@ -616,7 +632,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
} else if !fi.IsDir() { } else if !fi.IsDir() {
return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device) 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 { if err != nil {
return err return err
} }
@@ -624,17 +640,17 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
// "proc" and "sys" mounts need special handling (without resolving the // "proc" and "sys" mounts need special handling (without resolving the
// destination) to avoid attacks. // destination) to avoid attacks.
m.dstFile = dstFile m.dstFile = dstFile
return m.mountPropagate(rootfs, "") return m.mountPropagate(c.root, "")
} }
mountLabel := c.label 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) return fmt.Errorf("create mountpoint for %s mount: %w", m.Destination, err)
} }
switch m.Device { switch m.Device {
case "mqueue": case "mqueue":
if err := m.mountPropagate(rootfs, ""); err != nil { if err := m.mountPropagate(c.root, ""); err != nil {
return err return err
} }
return utils.WithProcfdFile(m.dstFile, func(dstFd string) error { 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 { if m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP {
err = doTmpfsCopyUp(m, mountLabel) err = doTmpfsCopyUp(m, mountLabel)
} else { } else {
err = m.mountPropagate(rootfs, mountLabel) err = m.mountPropagate(c.root, mountLabel)
} }
return err return err
case "bind": case "bind":
// open_tree()-related shenanigans are all handled in mountViaFds. // 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 return err
} }
@@ -772,7 +788,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
} }
return mountCgroupV1(m, c) return mountCgroupV1(m, c)
default: 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. // 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) useBindMount := userns.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER)
for _, node := range config.Devices { 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 // containers running in a user namespace are not allowed to mknod
// devices so we can just bind mount it from the host. // 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 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. // 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 == "" { if node.Path == "" {
// The node only exists for cgroup reasons, ignore it here. // The node only exists for cgroup reasons, ignore it here.
return nil return nil
} }
destDir, destName, err := pathrs.MkdirAllParentInRoot(rootfs, node.Path, 0o755) destDir, destName, err := pathrs.MkdirAllParentInRoot(rootFd, node.Path, 0o755)
if err != nil { if err != nil {
return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err) 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 // pivotRoot will call pivot_root such that rootfs becomes the new root
// filesystem, and everything else is cleaned up. // 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 // While the documentation may claim otherwise, pivot_root(".", ".") is
// actually valid. What this results in is / being the new root but // 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 // /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 // 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. // 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 { if err != nil {
return err return err
} }
defer unix.Close(oldroot) 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. // Change to the new root so that the pivot_root actually acts on it.
if err := unix.Fchdir(newroot); err != nil { if err := unix.Fchdir(int(root.Fd())); err != nil {
return &os.PathError{Op: "fchdir", Path: "fd " + strconv.Itoa(newroot), Err: err} return &os.PathError{Op: "chdir", Path: root.Name(), Err: err}
} }
if err := unix.PivotRoot(".", "."); err != nil { 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 // maskPaths masks the top of the specified paths inside a container to avoid
// security issues from processes reading information from non-namespace aware // security issues from processes reading information from non-namespace aware
// mounts ( proc/kcore ). // mounts ( proc/kcore ).
// For files, maskPath bind mounts /dev/null over the top of the specified path. // 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. // 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) devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0)
if err != nil { if err != nil {
return fmt.Errorf("can't mask paths: %w", err) 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/") procSelfFd, closer := utils.ProcThreadSelf("fd/")
defer closer() 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 { for _, path := range paths {
// Open the target path; skip if it doesn't exist. // Open the target path; skip if it doesn't exist.
dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0) 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() dstFh.Close()
return fmt.Errorf("can't mask path %q: %w", path, err) 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 var dstType string
if st.IsDir() { if st.IsDir() {
// Destination is a directory: bind mount a ro tmpfs over it. // Destination is a directory: bind mount a ro tmpfs over it.
dstType = "dir" 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 { } else {
// Destination is a file: mount it to /dev/null. // Destination is a file: mount it to /dev/null.
dstType = "path" dstType = "path"
@@ -1372,16 +1438,17 @@ func maskPaths(paths []string, mountLabel string) error {
return nil 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) fullPath, err := procfs.ProcSelfFdReadlink(f)
if err != nil { if err != nil {
return nil, fmt.Errorf("get full path: %w", err) return nil, fmt.Errorf("get full path: %w", err)
} }
rootfs := rootFd.Name()
if !pathrs.IsLexicallyInRoot(rootfs, fullPath) { if !pathrs.IsLexicallyInRoot(rootfs, fullPath) {
return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs) return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs)
} }
unsafePath := pathrs.LexicallyStripRoot(rootfs, fullPath) unsafePath := pathrs.LexicallyStripRoot(rootfs, fullPath)
reopened, err := pathrs.OpenInRoot(rootfs, unsafePath, flags) reopened, err := pathrs.OpenInRoot(rootFd, unsafePath, flags)
if err != nil { if err != nil {
return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err) 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 // Do the mount operation followed by additional mounts required to take care
// of propagation flags. This will always be scoped inside the container rootfs. // 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 ( var (
data = label.FormatMountLabel(m.Data, mountLabel) data = label.FormatMountLabel(m.Data, mountLabel)
flags = m.Flags 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 // TODO: Use move_mount(2) on newer kernels so that this is no longer
// necessary on modern systems. // 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 { if err != nil {
return fmt.Errorf("reopen mountpoint after mount: %w", err) return fmt.Errorf("reopen mountpoint after mount: %w", err)
} }
+2 -1
View File
@@ -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 return err
} }
pdeath, err := system.GetParentDeathSignal() pdeath, err := system.GetParentDeathSignal()
if err != nil { if err != nil {
return fmt.Errorf("can't get pdeath signal: %w", err) return fmt.Errorf("can't get pdeath signal: %w", err)
+45 -1
View File
@@ -6,7 +6,7 @@ function setup() {
setup_busybox setup_busybox
# Create fake rootfs. # Create fake rootfs.
mkdir rootfs/testdir mkdir rootfs/testdir rootfs/testdir2 rootfs/testdir3
echo "Forbidden information!" >rootfs/testfile echo "Forbidden information!" >rootfs/testfile
# add extra masked paths # add extra masked paths
@@ -55,6 +55,20 @@ function teardown() {
[[ "${output}" == *"Operation not permitted"* ]] [[ "${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]" { @test "mask paths [prohibit symlink /proc]" {
ln -s /symlink rootfs/proc ln -s /symlink rootfs/proc
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox 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 # so we merely check that it fails, and do not check the exact error
# message like for /proc above. # 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" ]
}