runc: fix CVE-2024-45310
Signed-off-by: Song Zhang <zhangsong34@huawei.com> (cherry picked from commit ab4cc0729c31453147018b290e97d51db51f3c13)
This commit is contained in:
parent
4172c93f2e
commit
db968d3370
@ -1 +1 @@
|
|||||||
b5df7029488e0b42b65b5df8e23c7bd9e8884099
|
1251c89d252bb9f8136d47c5892497829e78683f
|
||||||
|
|||||||
344
patch/0045-rootfs-consolidate-mountpoint-creation-logic.patch
Normal file
344
patch/0045-rootfs-consolidate-mountpoint-creation-logic.patch
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
From 161ddbfb05c69e010d50d788533a19d51607b937 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Aleksa Sarai <cyphar@cyphar.com>
|
||||||
|
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 <cyphar@cyphar.com>
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
@ -0,0 +1,356 @@
|
|||||||
|
From ef3e5b4ea8f0b2eaac05df8b16f6656aebf05998 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Aleksa Sarai <cyphar@cyphar.com>
|
||||||
|
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 <cyphar@cyphar.com>
|
||||||
|
---
|
||||||
|
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
|
||||||
|
+// <https://github.com/cyphar/filepath-securejoin/pull/13>, 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
|
||||||
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Name: runc
|
Name: runc
|
||||||
Version: 1.1.8
|
Version: 1.1.8
|
||||||
Release: 20
|
Release: 21
|
||||||
Summary: runc is a CLI tool for spawning and running containers according to the OCI specification.
|
Summary: runc is a CLI tool for spawning and running containers according to the OCI specification.
|
||||||
|
|
||||||
License: ASL 2.0
|
License: ASL 2.0
|
||||||
@ -57,6 +57,12 @@ install -p -m 755 runc $RPM_BUILD_ROOT/%{_bindir}/runc
|
|||||||
%{_bindir}/runc
|
%{_bindir}/runc
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Tue Sep 10 2024 Song Zhang<zhangsong34@huawei.com> - 1.1.8-21
|
||||||
|
- Type:CVE
|
||||||
|
- CVE:CVE-2024-45310
|
||||||
|
- SUG:NA
|
||||||
|
- DESC:fix CVE-2024-45310
|
||||||
|
|
||||||
* Fri Aug 30 2024 zhongjiawei<zhongjiawei1@huawei.com> - 1.1.8-20
|
* Fri Aug 30 2024 zhongjiawei<zhongjiawei1@huawei.com> - 1.1.8-20
|
||||||
- Type:bugfix
|
- Type:bugfix
|
||||||
- CVE:NA
|
- CVE:NA
|
||||||
|
|||||||
@ -41,3 +41,5 @@ patch/0041-runc-Set-temporary-single-CPU-affinity-before-cgroup-cpus.patch
|
|||||||
patch/0042-runc-fix-a-data-race.patch
|
patch/0042-runc-fix-a-data-race.patch
|
||||||
patch/0043-runc-do-not-support-set-umask-through-native.umask.patch
|
patch/0043-runc-do-not-support-set-umask-through-native.umask.patch
|
||||||
patch/0044-runc-format-log-instead-panic-when-procError-missing.patch
|
patch/0044-runc-format-log-instead-panic-when-procError-missing.patch
|
||||||
|
patch/0045-rootfs-consolidate-mountpoint-creation-logic.patch
|
||||||
|
patch/0046-rootfs-try-to-scope-MkdirAll-to-stay-inside-the-root.patch
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user