diff --git a/git-commit b/git-commit index e9525fd4f3fb3e12ca4cf225d4a9a12b4d10bc62..92d8930d80524c3b323e809f3a260a1dbd7091d6 100644 --- a/git-commit +++ b/git-commit @@ -1 +1 @@ -757473cdeea474eec67d70dcefa3b9c90ba51ce1 +1251c89d252bb9f8136d47c5892497829e78683f diff --git a/patch/0057-rootfs-consolidate-mountpoint-creation-logic.patch b/patch/0057-rootfs-consolidate-mountpoint-creation-logic.patch new file mode 100644 index 0000000000000000000000000000000000000000..0bec2b8cebecdc2d121e53c6cbdaebe8164baf57 --- /dev/null +++ b/patch/0057-rootfs-consolidate-mountpoint-creation-logic.patch @@ -0,0 +1,344 @@ +From 161ddbfb05c69e010d50d788533a19d51607b937 Mon Sep 17 00:00:00 2001 +From: Aleksa Sarai +Date: Mon, 1 Jul 2024 15:12:01 +1000 +Subject: [PATCH 1/2] rootfs: consolidate mountpoint creation logic + +The logic for how we create mountpoints is spread over each mountpoint +preparation function, when in reality the behaviour is pretty uniform +with only a handful of exceptions. So just move it all to one function +that is easier to understand. + +Signed-off-by: Aleksa Sarai +--- + libcontainer/container_linux.go | 28 ++---- + libcontainer/rootfs_linux.go | 160 ++++++++++++++----------------- + libcontainer/utils/utils_unix.go | 15 +++ + 3 files changed, 94 insertions(+), 109 deletions(-) + +diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go +index 5086d50..e8b00c8 100644 +--- a/libcontainer/container_linux.go ++++ b/libcontainer/container_linux.go +@@ -1278,8 +1278,7 @@ func (c *linuxContainer) restoreNetwork(req *criurpc.CriuReq, criuOpts *CriuOpts + // restore using CRIU. This function is inspired from the code in + // rootfs_linux.go + func (c *linuxContainer) makeCriuRestoreMountpoints(m *configs.Mount) error { +- switch m.Device { +- case "cgroup": ++ if m.Device == "cgroup" { + // No mount point(s) need to be created: + // + // * for v1, mount points are saved by CRIU because +@@ -1288,26 +1287,11 @@ func (c *linuxContainer) makeCriuRestoreMountpoints(m *configs.Mount) error { + // * for v2, /sys/fs/cgroup is a real mount, but + // the mountpoint appears as soon as /sys is mounted + return nil +- case "bind": +- // The prepareBindMount() function checks if source +- // exists. So it cannot be used for other filesystem types. +- // TODO: pass something else than nil? Not sure if criu is +- // impacted by issue #2484 +- if err := prepareBindMount(m, c.config.Rootfs, nil); err != nil { +- return err +- } +- default: +- // for all other filesystems just create the mountpoints +- dest, err := securejoin.SecureJoin(c.config.Rootfs, m.Destination) +- if err != nil { +- return err +- } +- if err := checkProcMount(c.config.Rootfs, dest, ""); err != nil { +- return err +- } +- if err := os.MkdirAll(dest, 0o755); err != nil { +- return err +- } ++ } ++ // TODO: pass something else than nil? Not sure if criu is ++ // impacted by issue #2484 ++ if _, err := createMountpoint(c.config.Rootfs, m, nil, ""); err != nil { ++ return fmt.Errorf("create criu restore mount for %s mount: %w", m.Destination, err) + } + return nil + } +diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go +index 499d753..ea554d3 100644 +--- a/libcontainer/rootfs_linux.go ++++ b/libcontainer/rootfs_linux.go +@@ -225,36 +225,6 @@ func mountCmd(cmd configs.Command) error { + return nil + } + +-func prepareBindMount(m *configs.Mount, rootfs string, mountFd *int) error { +- source := m.Source +- if mountFd != nil { +- source = "/proc/self/fd/" + strconv.Itoa(*mountFd) +- } +- +- stat, err := os.Stat(source) +- 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 +- } +- // ensure that the destination of the bind mount is resolved of symlinks at mount time because +- // any previous mounts can invalidate the next mount's destination. +- // this can happen when a user specifies mounts within other mounts to cause breakouts or other +- // evil stuff to try to escape the container's rootfs. +- var dest string +- if dest, err = securejoin.SecureJoin(rootfs, m.Destination); err != nil { +- return err +- } +- if err := checkProcMount(rootfs, dest, source); err != nil { +- return err +- } +- if err := createIfNotExists(dest, stat.IsDir()); err != nil { +- return err +- } +- +- return nil +-} +- + func mountCgroupV1(m *configs.Mount, c *mountConfig) error { + binds, err := getCgroupMounts(m) + if err != nil { +@@ -283,6 +253,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error { + for _, b := range binds { + if c.cgroupns { + subsystemPath := filepath.Join(c.root, b.Destination) ++ subsystemName := filepath.Base(b.Destination) + if err := os.MkdirAll(subsystemPath, 0o755); err != nil { + return err + } +@@ -293,7 +264,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error { + } + var ( + source = "cgroup" +- data = filepath.Base(subsystemPath) ++ data = subsystemName + ) + if data == "systemd" { + data = cgroups.CgroupNamePrefix + data +@@ -323,14 +294,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error { + } + + func mountCgroupV2(m *configs.Mount, c *mountConfig) error { +- dest, err := securejoin.SecureJoin(c.root, m.Destination) +- if err != nil { +- return err +- } +- if err := os.MkdirAll(dest, 0o755); err != nil { +- return err +- } +- err = utils.WithProcfd(c.root, m.Destination, func(procfd string) error { ++ err := utils.WithProcfd(c.root, m.Destination, func(procfd string) error { + return mount(m.Source, m.Destination, procfd, "cgroup2", uintptr(m.Flags), m.Data) + }) + if err == nil || !(errors.Is(err, unix.EPERM) || errors.Is(err, unix.EBUSY)) { +@@ -412,6 +376,70 @@ func doTmpfsCopyUp(m *configs.Mount, rootfs, mountLabel string) (Err error) { + }) + } + ++var errRootfsToFile = errors.New("config tries to change rootfs to file") ++ ++func createMountpoint(rootfs string, m *configs.Mount, mountFd *int, source string) (string, error) { ++ dest, err := securejoin.SecureJoin(rootfs, m.Destination) ++ if err != nil { ++ return "", err ++ } ++ if err := checkProcMount(rootfs, dest, m, source); err != nil { ++ return "", fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err) ++ } ++ ++ switch m.Device { ++ case "bind": ++ source := m.Source ++ if mountFd != nil { ++ source = "/proc/self/fd/" + strconv.Itoa(*mountFd) ++ } ++ ++ fi, err := os.Stat(source) ++ 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 "", fmt.Errorf("bind mount source stat: %w", err) ++ } ++ // If the original source is not a directory, make the target a file. ++ if !fi.IsDir() { ++ // Make sure we aren't tricked into trying to make the root a file. ++ if rootfs == dest { ++ return "", fmt.Errorf("%w: file bind mount over rootfs", errRootfsToFile) ++ } ++ // Make the parent directory. ++ if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { ++ return "", fmt.Errorf("make parent dir of file bind-mount: %w", err) ++ } ++ // Make the target file. ++ f, err := os.OpenFile(dest, os.O_CREATE, 0o755) ++ if err != nil { ++ return "", fmt.Errorf("create target of file bind-mount: %w", err) ++ } ++ _ = f.Close() ++ // Nothing left to do. ++ return dest, nil ++ } ++ ++ case "tmpfs": ++ // If the original target exists, copy the mode for the tmpfs mount. ++ if stat, err := os.Stat(dest); err == nil { ++ dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode())) ++ if m.Data != "" { ++ dt = dt + "," + m.Data ++ } ++ m.Data = dt ++ ++ // Nothing left to do. ++ return dest, nil ++ } ++ } ++ ++ if err := os.MkdirAll(dest, 0o755); err != nil { ++ return "", err ++ } ++ return dest, nil ++} ++ + func mountToRootfs(m *configs.Mount, c *mountConfig) error { + rootfs := c.root + +@@ -446,46 +474,27 @@ func mountToRootfs(m *configs.Mount, c *mountConfig) error { + return mountPropagate(m, rootfs, "", nil) + } + +- mountLabel := c.label + mountFd := c.fd +- dest, err := securejoin.SecureJoin(rootfs, m.Destination) ++ dest, err := createMountpoint(rootfs, m, mountFd, m.Source) + if err != nil { +- return err ++ return fmt.Errorf("create mount destination for %s mount: %w", m.Destination, err) + } ++ mountLabel := c.label + + switch m.Device { + case "mqueue": +- if err := os.MkdirAll(dest, 0o755); err != nil { +- return err +- } + if err := mountPropagate(m, rootfs, "", nil); err != nil { + return err + } + return label.SetFileLabel(dest, mountLabel) + case "tmpfs": +- if stat, err := os.Stat(dest); err != nil { +- if err := os.MkdirAll(dest, 0o755); err != nil { +- return err +- } +- } else { +- dt := fmt.Sprintf("mode=%04o", stat.Mode()) +- if m.Data != "" { +- dt = dt + "," + m.Data +- } +- m.Data = dt +- } +- + if m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP { + err = doTmpfsCopyUp(m, rootfs, mountLabel) + } else { + err = mountPropagate(m, rootfs, mountLabel, nil) + } +- + return err + case "bind": +- if err := prepareBindMount(m, rootfs, mountFd); err != nil { +- return err +- } + if err := mountPropagate(m, rootfs, mountLabel, mountFd); err != nil { + return err + } +@@ -513,12 +522,6 @@ func mountToRootfs(m *configs.Mount, c *mountConfig) error { + } + return mountCgroupV1(m, c) + default: +- if err := checkProcMount(rootfs, dest, m.Source); err != nil { +- return err +- } +- if err := os.MkdirAll(dest, 0o755); err != nil { +- return err +- } + return mountPropagate(m, rootfs, mountLabel, mountFd) + } + if err := setRecAttr(m, rootfs); err != nil { +@@ -729,6 +732,9 @@ func createDeviceNode(rootfs string, node *devices.Device, bind bool) error { + if err != nil { + return err + } ++ if dest == rootfs { ++ return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile) ++ } + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return err + } +@@ -995,26 +1001,6 @@ func chroot() error { + return nil + } + +-// createIfNotExists creates a file or a directory only if it does not already exist. +-func createIfNotExists(path string, isDir bool) error { +- if _, err := os.Stat(path); err != nil { +- if os.IsNotExist(err) { +- if isDir { +- return os.MkdirAll(path, 0o755) +- } +- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { +- return err +- } +- f, err := os.OpenFile(path, os.O_CREATE, 0o755) +- if err != nil { +- return err +- } +- _ = f.Close() +- } +- } +- return nil +-} +- + // readonlyPath will make a path read only. + func readonlyPath(path string) error { + if err := mount(path, path, "", "", unix.MS_BIND|unix.MS_REC, ""); err != nil { +diff --git a/libcontainer/utils/utils_unix.go b/libcontainer/utils/utils_unix.go +index f57f087..6fe0096 100644 +--- a/libcontainer/utils/utils_unix.go ++++ b/libcontainer/utils/utils_unix.go +@@ -10,6 +10,7 @@ import ( + "path/filepath" + "runtime" + "strconv" ++ "strings" + "sync" + _ "unsafe" // for go:linkname + +@@ -156,6 +157,20 @@ func NewSockPair(name string) (parent, child *os.File, err error) { + return os.NewFile(uintptr(fds[1]), name+"-p"), os.NewFile(uintptr(fds[0]), name+"-c"), nil + } + ++// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"), ++// but properly handling the case where path or root are "/". ++// ++// NOTE: The return value only make sense if the path doesn't contain "..". ++func IsLexicallyInRoot(root, path string) bool { ++ if root != "/" { ++ root += "/" ++ } ++ if path != "/" { ++ path += "/" ++ } ++ return strings.HasPrefix(path, root) ++} ++ + // WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...) + // corresponding to the unsafePath resolved within the root. Before passing the + // fd, this path is verified to have been inside the root -- so operating on it +-- +2.33.0 + diff --git a/patch/0058-rootfs-try-to-scope-MkdirAll-to-stay-inside-the-root.patch b/patch/0058-rootfs-try-to-scope-MkdirAll-to-stay-inside-the-root.patch new file mode 100644 index 0000000000000000000000000000000000000000..7376329d176fac7b07f456d9eecbb0215f7b9eb8 --- /dev/null +++ b/patch/0058-rootfs-try-to-scope-MkdirAll-to-stay-inside-the-root.patch @@ -0,0 +1,356 @@ +From ef3e5b4ea8f0b2eaac05df8b16f6656aebf05998 Mon Sep 17 00:00:00 2001 +From: Aleksa Sarai +Date: Tue, 2 Jul 2024 20:58:43 +1000 +Subject: [PATCH 2/2] rootfs: try to scope MkdirAll to stay inside the rootfs + +While we use SecureJoin to try to make all of our target paths inside +the container safe, SecureJoin is not safe against an attacker than can +change the path after we "resolve" it. + +os.MkdirAll can inadvertently follow symlinks and thus an attacker could +end up tricking runc into creating empty directories on the host (note +that the container doesn't get access to these directories, and the host +just sees empty directories). However, this could potentially cause DoS +issues by (for instance) creating a directory in a conf.d directory for +a daemon that doesn't handle subdirectories properly. + +In addition, the handling for creating file bind-mounts did a plain +open(O_CREAT) on the SecureJoin'd path, which is even more obviously +unsafe (luckily we didn't use O_TRUNC, or this bug could've allowed an +attacker to cause data loss...). Regardless of the symlink issue, +opening an untrusted file could result in a DoS if the file is a hung +tty or some other "nasty" file. We can use mknodat to safely create a +regular file without opening anything anyway (O_CREAT|O_EXCL would also +work but it makes the logic a bit more complicated, and we don't want to +open the file for any particular reason anyway). + +libpathrs[1] is the long-term solution for these kinds of problems, but +for now we can patch this particular issue by creating a more restricted +MkdirAll that refuses to resolve symlinks and does the creation using +file descriptors. This is loosely based on a more secure version that +filepath-securejoin now has[2] and will be added to libpathrs soon[3]. + +[1]: https://github.com/openSUSE/libpathrs +[2]: https://github.com/cyphar/filepath-securejoin/releases/tag/v0.3.0 +[3]: https://github.com/openSUSE/libpathrs/issues/10 + +Fixes: CVE-2024-45310 +Signed-off-by: Aleksa Sarai +--- + libcontainer/mount_linux.go | 18 +++++ + libcontainer/rootfs_linux.go | 33 ++++++--- + libcontainer/system/linux.go | 41 +++++++++++ + libcontainer/utils/utils_unix.go | 112 +++++++++++++++++++++++++++++++ + 4 files changed, 193 insertions(+), 11 deletions(-) + +diff --git a/libcontainer/mount_linux.go b/libcontainer/mount_linux.go +index 5f49de9..948b6c0 100644 +--- a/libcontainer/mount_linux.go ++++ b/libcontainer/mount_linux.go +@@ -1,6 +1,7 @@ + package libcontainer + + import ( ++ "io/fs" + "strconv" + + "golang.org/x/sys/unix" +@@ -81,3 +82,20 @@ func unmount(target string, flags int) error { + } + return nil + } ++ ++// syscallMode returns the syscall-specific mode bits from Go's portable mode bits. ++// Copy from https://cs.opensource.google/go/go/+/refs/tags/go1.20.7:src/os/file_posix.go;l=61-75 ++func syscallMode(i fs.FileMode) (o uint32) { ++ o |= uint32(i.Perm()) ++ if i&fs.ModeSetuid != 0 { ++ o |= unix.S_ISUID ++ } ++ if i&fs.ModeSetgid != 0 { ++ o |= unix.S_ISGID ++ } ++ if i&fs.ModeSticky != 0 { ++ o |= unix.S_ISVTX ++ } ++ // No mapping for Go's ModeTemporary (plan9 only). ++ return ++} +diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go +index ea554d3..4678c76 100644 +--- a/libcontainer/rootfs_linux.go ++++ b/libcontainer/rootfs_linux.go +@@ -254,7 +254,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error { + if c.cgroupns { + subsystemPath := filepath.Join(c.root, b.Destination) + subsystemName := filepath.Base(b.Destination) +- if err := os.MkdirAll(subsystemPath, 0o755); err != nil { ++ if err := utils.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil { + return err + } + if err := utils.WithProcfd(c.root, b.Destination, func(procfd string) error { +@@ -383,7 +383,7 @@ func createMountpoint(rootfs string, m *configs.Mount, mountFd *int, source stri + if err != nil { + return "", err + } +- if err := checkProcMount(rootfs, dest, m, source); err != nil { ++ if err := checkProcMount(rootfs, dest, source); err != nil { + return "", fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err) + } + +@@ -407,15 +407,26 @@ func createMountpoint(rootfs string, m *configs.Mount, mountFd *int, source stri + return "", fmt.Errorf("%w: file bind mount over rootfs", errRootfsToFile) + } + // Make the parent directory. +- if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { ++ destDir, destBase := filepath.Split(dest) ++ destDirFd, err := utils.MkdirAllInRootOpen(rootfs, destDir, 0o755) ++ if err != nil { + return "", fmt.Errorf("make parent dir of file bind-mount: %w", err) + } +- // Make the target file. +- f, err := os.OpenFile(dest, os.O_CREATE, 0o755) +- if err != nil { +- return "", fmt.Errorf("create target of file bind-mount: %w", err) ++ defer destDirFd.Close() ++ // Make the target file. We want to avoid opening any file that is ++ // already there because it could be a "bad" file like an invalid ++ // device or hung tty that might cause a DoS, so we use mknodat. ++ // destBase does not contain any "/" components, and mknodat does ++ // not follow trailing symlinks, so we can safely just call mknodat ++ // here. ++ if err := unix.Mknodat(int(destDirFd.Fd()), destBase, unix.S_IFREG|0o644, 0); err != nil { ++ // If we get EEXIST, there was already an inode there and ++ // we can consider that a success. ++ if !errors.Is(err, unix.EEXIST) { ++ err = &os.PathError{Op: "mknod regular file", Path: dest, Err: err} ++ return "", fmt.Errorf("create target of file bind-mount: %w", err) ++ } + } +- _ = f.Close() + // Nothing left to do. + return dest, nil + } +@@ -434,7 +445,7 @@ func createMountpoint(rootfs string, m *configs.Mount, mountFd *int, source stri + } + } + +- if err := os.MkdirAll(dest, 0o755); err != nil { ++ if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil { + return "", err + } + return dest, nil +@@ -467,7 +478,7 @@ func mountToRootfs(m *configs.Mount, c *mountConfig) error { + if strings.HasPrefix(m.Destination, "/proc/sys/") { + return nil + } +- if err := os.MkdirAll(dest, 0o755); err != nil { ++ if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil { + return err + } + // Selinux kernels do not support labeling of /proc or /sys. +@@ -735,7 +746,7 @@ func createDeviceNode(rootfs string, node *devices.Device, bind bool) error { + if dest == rootfs { + return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile) + } +- if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { ++ if err := utils.MkdirAllInRoot(rootfs, filepath.Dir(dest), 0o755); err != nil { + return err + } + if bind { +diff --git a/libcontainer/system/linux.go b/libcontainer/system/linux.go +index e1d6eb1..0f97045 100644 +--- a/libcontainer/system/linux.go ++++ b/libcontainer/system/linux.go +@@ -6,6 +6,8 @@ package system + import ( + "os" + "os/exec" ++ "runtime" ++ "strings" + "unsafe" + + "golang.org/x/sys/unix" +@@ -102,3 +104,42 @@ func GetSubreaper() (int, error) { + + return int(i), nil + } ++ ++func prepareAt(dir *os.File, path string) (int, string) { ++ if dir == nil { ++ return unix.AT_FDCWD, path ++ } ++ ++ // Rather than just filepath.Join-ing path here, do it manually so the ++ // error and handle correctly indicate cases like path=".." as being ++ // relative to the correct directory. The handle.Name() might end up being ++ // wrong but because this is (currently) only used in MkdirAllInRoot, that ++ // isn't a problem. ++ dirName := dir.Name() ++ if !strings.HasSuffix(dirName, "/") { ++ dirName += "/" ++ } ++ fullPath := dirName + path ++ ++ return int(dir.Fd()), fullPath ++} ++ ++func Openat(dir *os.File, path string, flags int, mode uint32) (*os.File, error) { ++ dirFd, fullPath := prepareAt(dir, path) ++ fd, err := unix.Openat(dirFd, path, flags, mode) ++ if err != nil { ++ return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err} ++ } ++ runtime.KeepAlive(dir) ++ return os.NewFile(uintptr(fd), fullPath), nil ++} ++ ++func Mkdirat(dir *os.File, path string, mode uint32) error { ++ dirFd, fullPath := prepareAt(dir, path) ++ err := unix.Mkdirat(dirFd, path, mode) ++ if err != nil { ++ err = &os.PathError{Op: "mkdirat", Path: fullPath, Err: err} ++ } ++ runtime.KeepAlive(dir) ++ return err ++} +diff --git a/libcontainer/utils/utils_unix.go b/libcontainer/utils/utils_unix.go +index 6fe0096..66d12e5 100644 +--- a/libcontainer/utils/utils_unix.go ++++ b/libcontainer/utils/utils_unix.go +@@ -4,6 +4,7 @@ + package utils + + import ( ++ "errors" + "fmt" + "math" + "os" +@@ -14,6 +15,8 @@ import ( + "sync" + _ "unsafe" // for go:linkname + ++ "github.com/opencontainers/runc/libcontainer/system" ++ + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +@@ -171,6 +174,115 @@ func IsLexicallyInRoot(root, path string) bool { + return strings.HasPrefix(path, root) + } + ++// MkdirAllInRootOpen attempts to make ++// ++// path, _ := securejoin.SecureJoin(root, unsafePath) ++// os.MkdirAll(path, mode) ++// os.Open(path) ++// ++// safer against attacks where components in the path are changed between ++// SecureJoin returning and MkdirAll (or Open) being called. In particular, we ++// try to detect any symlink components in the path while we are doing the ++// MkdirAll. ++// ++// NOTE: Unlike os.MkdirAll, mode is not Go's os.FileMode, it is the unix mode ++// (the suid/sgid/sticky bits are not the same as for os.FileMode). ++// ++// NOTE: If unsafePath is a subpath of root, we assume that you have already ++// called SecureJoin and so we use the provided path verbatim without resolving ++// any symlinks (this is done in a way that avoids symlink-exchange races). ++// This means that the path also must not contain ".." elements, otherwise an ++// error will occur. ++// ++// This is a somewhat less safe alternative to ++// , but it should ++// detect attempts to trick us into creating directories outside of the root. ++// We should migrate to securejoin.MkdirAll once it is merged. ++func MkdirAllInRootOpen(root, unsafePath string, mode uint32) (_ *os.File, Err error) { ++ // If the path is already "within" the root, use it verbatim. ++ fullPath := unsafePath ++ if !IsLexicallyInRoot(root, unsafePath) { ++ var err error ++ fullPath, err = securejoin.SecureJoin(root, unsafePath) ++ if err != nil { ++ return nil, err ++ } ++ } ++ subPath, err := filepath.Rel(root, fullPath) ++ if err != nil { ++ return nil, err ++ } ++ ++ // Check for any silly mode bits. ++ if mode&^0o7777 != 0 { ++ return nil, fmt.Errorf("tried to include non-mode bits in MkdirAll mode: 0o%.3o", mode) ++ } ++ ++ currentDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0) ++ if err != nil { ++ return nil, fmt.Errorf("open root handle: %w", err) ++ } ++ defer func() { ++ if Err != nil { ++ currentDir.Close() ++ } ++ }() ++ ++ for _, part := range strings.Split(subPath, string(filepath.Separator)) { ++ switch part { ++ case "", ".": ++ // Skip over no-op components. ++ continue ++ case "..": ++ return nil, fmt.Errorf("possible breakout detected: found %q component in SecureJoin subpath %s", part, subPath) ++ } ++ ++ nextDir, err := system.Openat(currentDir, part, unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) ++ switch { ++ case err == nil: ++ // Update the currentDir. ++ _ = currentDir.Close() ++ currentDir = nextDir ++ ++ case errors.Is(err, unix.ENOTDIR): ++ // This might be a symlink or some other random file. Either way, ++ // error out. ++ return nil, fmt.Errorf("cannot mkdir in %s/%s: %w", currentDir.Name(), part, unix.ENOTDIR) ++ ++ case errors.Is(err, os.ErrNotExist): ++ // Luckily, mkdirat will not follow trailing symlinks, so this is ++ // safe to do as-is. ++ if err := system.Mkdirat(currentDir, part, mode); err != nil { ++ return nil, err ++ } ++ // Open the new directory. There is a race here where an attacker ++ // could swap the directory with a different directory, but ++ // MkdirAll's fuzzy semantics mean we don't care about that. ++ nextDir, err := system.Openat(currentDir, part, unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) ++ if err != nil { ++ return nil, fmt.Errorf("open newly created directory: %w", err) ++ } ++ // Update the currentDir. ++ _ = currentDir.Close() ++ currentDir = nextDir ++ ++ default: ++ return nil, err ++ } ++ } ++ return currentDir, nil ++} ++ ++// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the ++// returned handle, for callers that don't need to use it. ++func MkdirAllInRoot(root, unsafePath string, mode uint32) error { ++ f, err := MkdirAllInRootOpen(root, unsafePath, mode) ++ if err == nil { ++ _ = f.Close() ++ } ++ return err ++} ++ + // WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...) + // corresponding to the unsafePath resolved within the root. Before passing the + // fd, this path is verified to have been inside the root -- so operating on it +-- +2.33.0 + diff --git a/runc.spec b/runc.spec index 3009533bb91d1aae33234938a0ca9998695a2133..ce9d3b32973915c295e9fbb5a6405645f747ca44 100644 --- a/runc.spec +++ b/runc.spec @@ -3,7 +3,7 @@ Name: docker-runc Version: 1.1.3 -Release: 29 +Release: 30 Summary: runc is a CLI tool for spawning and running containers according to the OCI specification. License: ASL 2.0 @@ -54,6 +54,12 @@ install -p -m 755 runc $RPM_BUILD_ROOT/%{_bindir}/runc %{_bindir}/runc %changelog +* Tue Sep 10 2024 Song Zhang - 1.1.3-30 +- Type:CVE +- CVE:CVE-2024-45310 +- SUG:NA +- DESC:fix CVE-2024-45310 + * Fri Aug 30 2024 zhongjiawei - 1.1.3-29 - Type:bugfix - CVE:NA diff --git a/series.conf b/series.conf index 64a79bf6077e72d6a18b0250389f11997f5f8d22..f6d29d3c1acdd01ba89e393876ef9fbce83e5d31 100644 --- a/series.conf +++ b/series.conf @@ -54,3 +54,5 @@ patch/0053-runc-fix-CVE-2024-3154.patch patch/0054-runc-Set-temporary-single-CPU-affinity-before-cgroup-cpus.patch patch/0055-runc-do-not-support-set-umask-through-native.umask.patch patch/0056-runc-format-log-instead-panic-when-procError-missing.patch +patch/0057-rootfs-consolidate-mountpoint-creation-logic.patch +patch/0058-rootfs-try-to-scope-MkdirAll-to-stay-inside-the-root.patch