From 863c03ec4063721c1f6899586aca57d73956ab64 Mon Sep 17 00:00:00 2001 From: dongyuzhen Date: Wed, 26 Nov 2025 15:38:21 +0800 Subject: [PATCH] fix CVE-2025-31133 and CVE-2025-52565 and CVE-2025-52881 --- git-commit | 2 +- patch/0049-runc-fix-CVE-2025-31133.patch | 293 +++++++++++++++++++++++ patch/0050-runc-fix-CVE-2025-52565.patch | 229 ++++++++++++++++++ patch/0051-runc-fix-CVE-2025-52881.patch | 158 ++++++++++++ runc.spec | 8 +- series.conf | 3 + 6 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 patch/0049-runc-fix-CVE-2025-31133.patch create mode 100644 patch/0050-runc-fix-CVE-2025-52565.patch create mode 100644 patch/0051-runc-fix-CVE-2025-52881.patch diff --git a/git-commit b/git-commit index b59dc61..7482b6d 100644 --- a/git-commit +++ b/git-commit @@ -1 +1 @@ -eff48af98905b28ba5f4aa3ed92d1aff97f1ef59 +02ed03710c8e262d89c6bb4761c68abe16b2eb83 diff --git a/patch/0049-runc-fix-CVE-2025-31133.patch b/patch/0049-runc-fix-CVE-2025-31133.patch new file mode 100644 index 0000000..02e4884 --- /dev/null +++ b/patch/0049-runc-fix-CVE-2025-31133.patch @@ -0,0 +1,293 @@ +From 8476df83b534a2522b878c0507b3491def48db9f Mon Sep 17 00:00:00 2001 +From: Kir Kolyshkin +Date: Thu, 6 Mar 2025 08:19:45 -0800 +Subject: [PATCH] libct: add/use isDevNull, verifyDevNull + +The /dev/null in a container should not be trusted, because when /dev +is a bind mount, /dev/null is not created by runc itself. + +1. Add isDevNull which checks the fd minor/major and device type, + and verifyDevNull which does the stat and the check. + +2. Rewrite maskPath to open and check /dev/null, and use its fd to + perform mounts. Move the loop over the MaskPaths into the function, + and rename it to maskPaths. + +3. reOpenDevNull: use verifyDevNull and isDevNull. + +4. fixStdioPermissions: use isDevNull instead of stat. + +Fixes: GHSA-9493-h29p-rfm2 CVE-2025-31133 +Co-authored-by: Rodrigo Campos +Signed-off-by: Kir Kolyshkin +Signed-off-by: Aleksa Sarai +--- + internal/sys/doc.go | 5 ++ + internal/sys/verify_inode_unix.go | 30 ++++++++++++ + libcontainer/init_linux.go | 11 ++--- + libcontainer/rootfs_linux.go | 76 ++++++++++++++++++++++++----- + libcontainer/standard_init_linux.go | 41 ++++++++-------- + 5 files changed, 124 insertions(+), 39 deletions(-) + create mode 100644 internal/sys/doc.go + create mode 100644 internal/sys/verify_inode_unix.go + +diff --git a/internal/sys/doc.go b/internal/sys/doc.go +new file mode 100644 +index 00000000..075387f7 +--- /dev/null ++++ b/internal/sys/doc.go +@@ -0,0 +1,5 @@ ++// Package sys is an internal package that contains helper methods for dealing ++// with Linux that are more complicated than basic wrappers. Basic wrappers ++// usually belong in internal/linux. If you feel something belongs in ++// libcontainer/utils or libcontainer/system, it probably belongs here instead. ++package sys +diff --git a/internal/sys/verify_inode_unix.go b/internal/sys/verify_inode_unix.go +new file mode 100644 +index 00000000..d5019db5 +--- /dev/null ++++ b/internal/sys/verify_inode_unix.go +@@ -0,0 +1,30 @@ ++package sys ++ ++import ( ++ "fmt" ++ "os" ++ "runtime" ++ ++ "golang.org/x/sys/unix" ++) ++ ++// VerifyInodeFunc is the callback passed to [VerifyInode] to check if the ++// inode is the expected type (and on the correct filesystem type, in the case ++// of filesystem-specific inodes). ++type VerifyInodeFunc func(stat *unix.Stat_t, statfs *unix.Statfs_t) error ++ ++// VerifyInode verifies that the underlying inode for the given file matches an ++// expected inode type (possibly on a particular kind of filesystem). This is ++// mainly a wrapper around [VerifyInodeFunc]. ++func VerifyInode(file *os.File, checkFunc VerifyInodeFunc) error { ++ var stat unix.Stat_t ++ if err := unix.Fstat(int(file.Fd()), &stat); err != nil { ++ return fmt.Errorf("fstat %q: %w", file.Name(), err) ++ } ++ var statfs unix.Statfs_t ++ if err := unix.Fstatfs(int(file.Fd()), &statfs); err != nil { ++ return fmt.Errorf("fstatfs %q: %w", file.Name(), err) ++ } ++ runtime.KeepAlive(file) ++ return checkFunc(&stat, &statfs) ++} +diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go +index d9f18139..50c7a129 100644 +--- a/libcontainer/init_linux.go ++++ b/libcontainer/init_linux.go +@@ -432,19 +432,16 @@ func setupUser(config *initConfig) error { + // The ownership needs to match because it is created outside of the container and needs to be + // localized. + func fixStdioPermissions(u *user.ExecUser) error { +- var null unix.Stat_t +- if err := unix.Stat("/dev/null", &null); err != nil { +- return &os.PathError{Op: "stat", Path: "/dev/null", Err: err} +- } + for _, file := range []*os.File{os.Stdin, os.Stdout, os.Stderr} { + var s unix.Stat_t + if err := unix.Fstat(int(file.Fd()), &s); err != nil { + return &os.PathError{Op: "fstat", Path: file.Name(), Err: err} + } + +- // Skip chown if uid is already the one we want or any of the STDIO descriptors +- // were redirected to /dev/null. +- if int(s.Uid) == u.Uid || s.Rdev == null.Rdev { ++ // Skip chown if: ++ // - uid is already the one we want, or ++ // - fd is opened to /dev/null. ++ if int(s.Uid) == u.Uid || isDevNull(&s) { + continue + } + +diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go +index ce5c15ab..50013fa4 100644 +--- a/libcontainer/rootfs_linux.go ++++ b/libcontainer/rootfs_linux.go +@@ -16,6 +16,7 @@ import ( + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/moby/sys/mountinfo" + "github.com/mrunalp/fileutils" ++ "github.com/opencontainers/runc/internal/sys" + "github.com/opencontainers/runc/libcontainer/cgroups" + "github.com/opencontainers/runc/libcontainer/cgroups/fs2" + "github.com/opencontainers/runc/libcontainer/configs" +@@ -331,7 +332,7 @@ func mountCgroupV2(m *configs.Mount, 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.WithProcfd(c.root, m.Destination, func(procfd string) error { +- return maskPath(procfd, c.label) ++ return maskPaths([]string{procfd}, c.label) + }) + } + return err +@@ -677,20 +678,20 @@ func setupDevSymlinks(rootfs string) error { + // needs to be called after we chroot/pivot into the container's rootfs so that any + // symlinks are resolved locally. + func reOpenDevNull() error { +- var stat, devNullStat unix.Stat_t + file, err := os.OpenFile("/dev/null", os.O_RDWR, 0) + if err != nil { + return err + } + defer file.Close() //nolint: errcheck +- if err := unix.Fstat(int(file.Fd()), &devNullStat); err != nil { +- return &os.PathError{Op: "fstat", Path: file.Name(), Err: err} ++ if err := verifyDevNull(file); err != nil { ++ return fmt.Errorf("can't reopen /dev/null: %w", err) + } + for fd := 0; fd < 3; fd++ { ++ var stat unix.Stat_t + if err := unix.Fstat(fd, &stat); err != nil { + return &os.PathError{Op: "fstat", Path: "fd " + strconv.Itoa(fd), Err: err} + } +- if stat.Rdev == devNullStat.Rdev { ++ if isDevNull(&stat) { + // Close and re-open the fd. + if err := unix.Dup3(int(file.Fd()), fd, 0); err != nil { + return &os.PathError{ +@@ -1089,18 +1090,71 @@ func remountReadonly(m *configs.Mount) error { + return fmt.Errorf("unable to mount %s as readonly max retries reached", dest) + } + +-// maskPath masks the top of the specified path inside a container to avoid ++func isDevNull(st *unix.Stat_t) bool { ++ return st.Mode&unix.S_IFMT == unix.S_IFCHR && st.Rdev == unix.Mkdev(1, 3) ++} ++ ++func verifyDevNull(f *os.File) error { ++ return sys.VerifyInode(f, func(st *unix.Stat_t, _ *unix.Statfs_t) error { ++ if !isDevNull(st) { ++ return errors.New("container's /dev/null is invalid") ++ } ++ return nil ++ }) ++} ++ ++// maskPaths masks the top of the specified path 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 maskPath(path string, mountLabel string) error { +- if err := mount("/dev/null", path, "", "", unix.MS_BIND, ""); err != nil && !errors.Is(err, os.ErrNotExist) { +- if errors.Is(err, unix.ENOTDIR) { +- return mount("tmpfs", path, "", "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel)) ++func maskPaths(paths []string, mountLabel string) error { ++ devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0) ++ if err != nil { ++ return fmt.Errorf("can't mask paths: %w", err) ++ } ++ defer devNull.Close() ++ if err := verifyDevNull(devNull); err != nil { ++ return fmt.Errorf("can't mask paths: %w", err) ++ } ++ procSelfFd, closer := utils.ProcThreadSelf("fd/") ++ defer closer() ++ ++ 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) ++ if err != nil { ++ if errors.Is(err, os.ErrNotExist) { ++ continue ++ } ++ return fmt.Errorf("can't mask path %q: %w", path, err) + } +- return err ++ st, err := dstFh.Stat() ++ ++ if err != nil { ++ dstFh.Close() ++ return fmt.Errorf("can't mask path %q: %w", path, err) ++ } ++ 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)) ++ } else { ++ // Destination is a file: mount it to /dev/null. ++ dstType = "path" ++ src, closer := utils.ProcThreadSelfFd(devNull.Fd()) ++ defer closer() ++ dstFd := filepath.Join(procSelfFd, strconv.Itoa(int(dstFh.Fd()))) ++ err = mount(src, path, dstFd, "", unix.MS_BIND, "") ++ } ++ dstFh.Close() ++ if err != nil { ++ return fmt.Errorf("can't mask %s %q: %w", dstType, path, err) ++ } ++ + } ++ + return nil + } + +diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go +index 8f595a44..ee907d82 100644 +--- a/libcontainer/standard_init_linux.go ++++ b/libcontainer/standard_init_linux.go +@@ -153,10 +153,9 @@ func (l *linuxStandardInit) Init() error { + } + } + } +- for _, path := range l.config.Config.MaskPaths { +- if err := maskPath(path, l.config.Config.MountLabel); err != nil { +- return fmt.Errorf("can't mask path %s: %w", path, err) +- } ++ ++ if err := maskPaths(l.config.Config.MaskPaths, l.config.Config.MountLabel); err != nil { ++ return err + } + pdeath, err := system.GetParentDeathSignal() + if err != nil { +@@ -286,22 +285,22 @@ func (l *linuxStandardInit) Init() error { + } + + // Close all file descriptors we are not passing to the container. This is +- // necessary because the execve target could use internal runc fds as the +- // execve path, potentially giving access to binary files from the host +- // (which can then be opened by container processes, leading to container +- // escapes). Note that because this operation will close any open file +- // descriptors that are referenced by (*os.File) handles from underneath +- // the Go runtime, we must not do any file operations after this point +- // (otherwise the (*os.File) finaliser could close the wrong file). See +- // CVE-2024-21626 for more information as to why this protection is +- // necessary. +- // +- // This is not needed for runc-dmz, because the extra execve(2) step means +- // that all O_CLOEXEC file descriptors have already been closed and thus +- // the second execve(2) from runc-dmz cannot access internal file +- // descriptors from runc. +- if err := utils.UnsafeCloseFrom(l.config.PassedFilesCount + 3); err != nil { +- return err +- } ++ // necessary because the execve target could use internal runc fds as the ++ // execve path, potentially giving access to binary files from the host ++ // (which can then be opened by container processes, leading to container ++ // escapes). Note that because this operation will close any open file ++ // descriptors that are referenced by (*os.File) handles from underneath ++ // the Go runtime, we must not do any file operations after this point ++ // (otherwise the (*os.File) finaliser could close the wrong file). See ++ // CVE-2024-21626 for more information as to why this protection is ++ // necessary. ++ // ++ // This is not needed for runc-dmz, because the extra execve(2) step means ++ // that all O_CLOEXEC file descriptors have already been closed and thus ++ // the second execve(2) from runc-dmz cannot access internal file ++ // descriptors from runc. ++ if err := utils.UnsafeCloseFrom(l.config.PassedFilesCount + 3); err != nil { ++ return err ++ } + return system.Exec(name, l.config.Args[0:], os.Environ()) + } +-- +2.33.0 + diff --git a/patch/0050-runc-fix-CVE-2025-52565.patch b/patch/0050-runc-fix-CVE-2025-52565.patch new file mode 100644 index 0000000..81146ce --- /dev/null +++ b/patch/0050-runc-fix-CVE-2025-52565.patch @@ -0,0 +1,229 @@ +From 531ef794e4ecd628006a865ad334a048ee2b4b2e Mon Sep 17 00:00:00 2001 +From: Aleksa Sarai +Date: Thu, 15 May 2025 16:12:21 +1000 +Subject: [PATCH] console: use TIOCGPTPEER when allocating peer PTY + +When opening the peer end of a pty, the old kernel API required us to +open /dev/pts/$num inside the container (at least since we fixed console +handling many years ago in commit 244c9fc426ae ("*: console rewrite")). + +The problem is that in a hostile container it is possible for +/dev/pts/$num to be an attacker-controlled symlink that runc can be +tricked into resolving when doing bind-mounts. This allows the attacker +to (among other things) persist /proc/... entries that are later masked +by runc, allowing an attacker to escape through the kernel.core_pattern +sysctl (/proc/sys/kernel/core_pattern). This is the original issue +reported by Lei Wang and Li Fu Bang in CVE-2025-52565. + +However, it should be noted that this is not entirely a newly-discovered +problem. Way back in Linux 4.13 (2017), I added the TIOCGPTPEER ioctl, +which allows us to get a pty peer without touching the /dev/pts inside +the container. The original threat model was around an attacker +replacing /dev/pts/$n or /dev/pts/ptmx with some malicious inode (a DoS +inode, or possibly a PTY they wanted a confused deputy to operate on). +Unfortunately, there was no practical way for runc to cache a safe +O_PATH handle to /dev/pts/ptmx (unlike other runtimes like LXC, which +switched to TIOCGPTPEER way back in 2017). Since it wasn't clear how we +could protect against the main attack TIOCGPTPEER was meant to protect +against, we never switched to it (even though I implemented it +specifically to harden container runtimes). + +Unfortunately, It turns out that mount *sources* are a threat we didn't +fully consider. Since TIOCGPTPEER already solves this problem entirely +for us in a race free way, we should just use that. In a later patch, we +will add some hardening for /dev/pts/$num opening to maintain support +for very old kernels (Linux 4.13 is very old at this point, but RHEL 7 +is still kicking and is stuck on Linux 3.10). + +Fixes: GHSA-qw9x-cqr3-wc7r CVE-2025-52565 +Reported-by: Lei Wang (CVE-2025-52565) +Reported-by: lfbzhm (CVE-2025-52565) +Reported-by: Aleksa Sarai (TIOCGPTPEER) +Signed-off-by: Aleksa Sarai +--- + internal/linux/linux.go | 47 +++++++++++++++++++++++++++ + libcontainer/console_linux.go | 61 +++++++++++++++++++++++------------ + libcontainer/init_linux.go | 7 ++-- + 3 files changed, 91 insertions(+), 24 deletions(-) + create mode 100644 internal/linux/linux.go + +diff --git a/internal/linux/linux.go b/internal/linux/linux.go +new file mode 100644 +index 00000000..eb9c2ed0 +--- /dev/null ++++ b/internal/linux/linux.go +@@ -0,0 +1,47 @@ ++package linux ++ ++import ( ++ "errors" ++ "os" ++ ++ "golang.org/x/sys/unix" ++) ++ ++// retryOnEINTR takes a function that returns an error and calls it ++// until the error returned is not EINTR. ++func retryOnEINTR(fn func() error) error { ++ for { ++ err := fn() ++ if !errors.Is(err, unix.EINTR) { ++ return err ++ } ++ } ++} ++ ++// Dup3 wraps [unix.Dup3]. ++func Dup3(oldfd, newfd, flags int) error { ++ err := retryOnEINTR(func() error { ++ return unix.Dup3(oldfd, newfd, flags) ++ }) ++ return os.NewSyscallError("dup3", err) ++} ++ ++// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER). ++func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) { ++ // Make sure O_NOCTTY is always set -- otherwise runc might accidentally ++ // gain it as a controlling terminal. O_CLOEXEC also needs to be set to ++ // make sure we don't leak the handle either. ++ flags |= unix.O_NOCTTY | unix.O_CLOEXEC ++ ++ // There is no nice wrapper for this kind of ioctl in unix. ++ peerFd, _, errno := unix.Syscall( ++ unix.SYS_IOCTL, ++ ptyFd, ++ uintptr(unix.TIOCGPTPEER), ++ uintptr(flags), ++ ) ++ if errno != 0 { ++ return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno) ++ } ++ return os.NewFile(peerFd, unsafePeerPath), nil ++} +diff --git a/libcontainer/console_linux.go b/libcontainer/console_linux.go +index 29b9c3b0..9334e10d 100644 +--- a/libcontainer/console_linux.go ++++ b/libcontainer/console_linux.go +@@ -1,41 +1,60 @@ + package libcontainer + + import ( ++ "fmt" + "os" ++ "runtime" + ++ "github.com/containerd/console" ++ "github.com/opencontainers/runc/internal/linux" ++ "github.com/opencontainers/runc/libcontainer/utils" + "golang.org/x/sys/unix" + ) + +-// mount initializes the console inside the rootfs mounting with the specified mount label +-// and applying the correct ownership of the console. +-func mountConsole(slavePath string) error { +- oldMask := unix.Umask(0o000) +- defer unix.Umask(oldMask) +- f, err := os.Create("/dev/console") +- if err != nil && !os.IsExist(err) { +- return err ++// safeAllocPty returns a new (ptmx, peer pty) allocation for use inside a ++// container. ++func safeAllocPty() (pty console.Console, peer *os.File, Err error) { ++ pty, unsafePeerPath, err := console.NewPty() ++ if err != nil { ++ return nil, nil, err + } +- if f != nil { +- f.Close() ++ defer func() { ++ if Err != nil { ++ _ = pty.Close() ++ } ++ }() ++ ++ peer, err = linux.GetPtyPeer(pty.Fd(), unsafePeerPath, unix.O_RDWR|unix.O_NOCTTY) ++ if err != nil { ++ return nil, nil, fmt.Errorf("failed to get peer end of newly-allocated console: %w", err) + } +- return mount(slavePath, "/dev/console", "", "bind", unix.MS_BIND, "") ++ return pty, peer, nil + } + +-// dupStdio opens the slavePath for the console and dups the fds to the current +-// processes stdio, fd 0,1,2. +-func dupStdio(slavePath string) error { +- fd, err := unix.Open(slavePath, unix.O_RDWR, 0) ++// mountConsole bind-mounts the provided pty on top of /dev/console so programs ++// that operate on /dev/console operate on the correct container pty. ++func mountConsole(peerPty *os.File) error { ++ console, err := os.OpenFile("/dev/console", unix.O_NOFOLLOW|unix.O_CREAT|unix.O_CLOEXEC, 0o666) + if err != nil { +- return &os.PathError{ +- Op: "open", +- Path: slavePath, +- Err: err, +- } ++ return fmt.Errorf("create /dev/console mount target: %w", err) + } ++ defer console.Close() ++ ++ dstFd, closer1 := utils.ProcThreadSelfFd(console.Fd()) ++ defer closer1() ++ ++ src, closer := utils.ProcThreadSelfFd(peerPty.Fd()) ++ defer closer() ++ return mount(src, "/dev/console", dstFd, "bind", unix.MS_BIND, "") ++} ++ ++// dupStdio replaces stdio with the given peerPty. ++func dupStdio(peerPty *os.File) error { + for _, i := range []int{0, 1, 2} { +- if err := unix.Dup3(fd, i, 0); err != nil { ++ if err := linux.Dup3(int(peerPty.Fd()), i, 0); err != nil { + return err + } + } ++ runtime.KeepAlive(peerPty) + return nil + } +diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go +index 50c7a129..e4e52612 100644 +--- a/libcontainer/init_linux.go ++++ b/libcontainer/init_linux.go +@@ -248,13 +248,14 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error { + // the UID owner of the console to be the user the process will run as (so + // they can actually control their console). + +- pty, slavePath, err := console.NewPty() ++ pty, peerPty, err := safeAllocPty() + if err != nil { + return err + } + + // After we return from here, we don't need the console anymore. + defer pty.Close() ++ defer peerPty.Close() + + if config.ConsoleHeight != 0 && config.ConsoleWidth != 0 { + err = pty.Resize(console.WinSize{ +@@ -269,7 +270,7 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error { + + // Mount the console inside our rootfs. + if mount { +- if err := mountConsole(slavePath); err != nil { ++ if err := mountConsole(peerPty); err != nil { + return err + } + } +@@ -278,7 +279,7 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error { + return err + } + // Now, dup over all the things. +- return dupStdio(slavePath) ++ return dupStdio(peerPty) + } + + // syncParentReady sends to the given pipe a JSON payload which indicates that +-- +2.33.0 + diff --git a/patch/0051-runc-fix-CVE-2025-52881.patch b/patch/0051-runc-fix-CVE-2025-52881.patch new file mode 100644 index 0000000..a0b91d1 --- /dev/null +++ b/patch/0051-runc-fix-CVE-2025-52881.patch @@ -0,0 +1,158 @@ +From ed6b1693b8b3ae7eb0250a7e76fc888cdacf98c1 Mon Sep 17 00:00:00 2001 +From: Aleksa Sarai +Date: Tue, 7 Oct 2025 22:48:50 +1100 +Subject: [PATCH] selinux: use safe procfs API for labels + +Due to the sensitive nature of these fixes, it was not possible to +submit these upstream and vendor the upstream library. Instead, this +patch uses a fork of github.com/opencontainers/selinux, branched at +commit opencontainers/selinux@879a755db558501df06f4ea59461ebc2d0c4a991. + +In order to permit downstreams to build with this patched version, a +snapshot of the forked version has been included in +internal/third_party/selinux. Note that since we use "go mod vendor", +the patched code is usable even without being "go get"-able. Once the +embargo for this issue is lifted we can submit the patches upstream and +switch back to a proper upstream go.mod entry. + +Also, this requires us to temporarily disable the CI job we have that +disallows "replace" directives. + +Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881 +Signed-off-by: Aleksa Sarai +--- + libcontainer/rootfs_linux.go | 4 ++ + libcontainer/utils/utils_unix.go | 30 +++++++++++++++ + .../selinux/go-selinux/selinux_linux.go | 38 +++++++++++++++++++ + 3 files changed, 72 insertions(+) + +diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go +index 50013fa4..094c0c2a 100644 +--- a/libcontainer/rootfs_linux.go ++++ b/libcontainer/rootfs_linux.go +@@ -1162,6 +1162,10 @@ func maskPaths(paths []string, mountLabel string) error { + // For e.g. net.ipv4.ip_forward translated to /proc/sys/net/ipv4/ip_forward. + func writeSystemProperty(key, value string) error { + keyPath := strings.Replace(key, ".", "/", -1) ++ isMount, err := utils.IsMountPoint(keyPath) ++ if err != nil || isMount { ++ return fmt.Errorf("refusing to write %s: %w", keyPath, err) ++ } + return os.WriteFile(path.Join("/proc/sys", keyPath), []byte(value), 0o644) + } + +diff --git a/libcontainer/utils/utils_unix.go b/libcontainer/utils/utils_unix.go +index 66d12e57..0ffe94c1 100644 +--- a/libcontainer/utils/utils_unix.go ++++ b/libcontainer/utils/utils_unix.go +@@ -13,6 +13,7 @@ import ( + "strconv" + "strings" + "sync" ++ "syscall" + _ "unsafe" // for go:linkname + + "github.com/opencontainers/runc/libcontainer/system" +@@ -388,3 +389,32 @@ func ProcThreadSelf(subpath string) (string, ProcThreadSelfCloser) { + func ProcThreadSelfFd(fd uintptr) (string, ProcThreadSelfCloser) { + return ProcThreadSelf("fd/" + strconv.FormatUint(uint64(fd), 10)) + } ++ ++// Determine if a path has a mount point. ++func IsMountPoint(path string) (bool, error) { ++ newPath := path ++ for newPath != "/" { ++ stat, err := os.Stat(newPath) ++ if err != nil { ++ return false, fmt.Errorf("fail to get %s stat: %v", newPath, err) ++ } ++ ++ parentDir := filepath.Dir(newPath) ++ if parentDir == "/" { ++ return false, nil ++ } ++ ++ parentStat, err := os.Stat(parentDir) ++ if err != nil { ++ return false, fmt.Errorf("fail to get parent path %s stat: %v", parentDir, err) ++ } ++ ++ targetInode := stat.Sys().(*syscall.Stat_t).Dev ++ parentInode := parentStat.Sys().(*syscall.Stat_t).Dev ++ if targetInode != parentInode { ++ return true, nil ++ } ++ newPath = parentDir ++ } ++ return false, nil ++} +diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go +index ee602ab9..0d00067d 100644 +--- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go ++++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go +@@ -16,6 +16,7 @@ import ( + "strconv" + "strings" + "sync" ++ "syscall" + + "golang.org/x/sys/unix" + ) +@@ -277,6 +278,10 @@ func readCon(fpath string) (string, error) { + if fpath == "" { + return "", ErrEmptyPath + } ++ isMount, err := isMountPoint(fpath) ++ if err != nil || isMount { ++ return "", fmt.Errorf("refusing to read %s: %w", fpath, err) ++ } + + in, err := os.Open(fpath) + if err != nil { +@@ -418,10 +423,43 @@ func execLabel() (string, error) { + return readAttr("exec") + } + ++// Determine if a path has a mount point. ++func isMountPoint(path string) (bool, error) { ++ newPath := path ++ for newPath != "/" { ++ stat, err := os.Stat(newPath) ++ if err != nil { ++ return false, fmt.Errorf("fail to get %s stat: %v", newPath, err) ++ } ++ ++ parentDir := filepath.Dir(newPath) ++ if parentDir == "/" { ++ return false, nil ++ } ++ ++ parentStat, err := os.Stat(parentDir) ++ if err != nil { ++ return false, fmt.Errorf("fail to get parent path %s stat: %v", parentDir, err) ++ } ++ ++ targetInode := stat.Sys().(*syscall.Stat_t).Dev ++ parentInode := parentStat.Sys().(*syscall.Stat_t).Dev ++ if targetInode != parentInode { ++ return true, nil ++ } ++ newPath = parentDir ++ } ++ return false, nil ++} ++ + func writeCon(fpath, val string) error { + if fpath == "" { + return ErrEmptyPath + } ++ isMount, err := isMountPoint(fpath) ++ if err != nil || isMount { ++ return fmt.Errorf("refusing to write %s: %w", fpath, err) ++ } + if val == "" { + if !getEnabled() { + return nil +-- +2.33.0 + diff --git a/runc.spec b/runc.spec index faa40fc..d83d43b 100644 --- a/runc.spec +++ b/runc.spec @@ -3,7 +3,7 @@ Name: runc Version: 1.1.8 -Release: 26 +Release: 27 Summary: runc is a CLI tool for spawning and running containers according to the OCI specification. License: ASL 2.0 @@ -56,6 +56,12 @@ install -p -m 755 runc $RPM_BUILD_ROOT/%{_bindir}/runc %{_bindir}/runc %changelog +* Wed Nov 26 2025 dongyuzhen - 1.1.8-27 +- Type:CVE +- CVE:NA +- SUG:NA +- DESC:fix CVE-2025-31133 and CVE-2025-52565 and CVE-2025-52881 + * Wed Mar 26 2025 dongyuzhen - 1.1.8-26 - Type:bugfix - CVE:NA diff --git a/series.conf b/series.conf index d62f689..4fcad30 100644 --- a/series.conf +++ b/series.conf @@ -44,3 +44,6 @@ patch/0045-rootfs-consolidate-mountpoint-creation-logic.patch patch/0046-rootfs-try-to-scope-MkdirAll-to-stay-inside-the-root.patch patch/0047-runc-fix-can-t-set-cpuset-cpus-and-cpuset-mems-at-th.patch patch/0048-runc-libct-cap-allow-New-nil.patch +patch/0049-runc-fix-CVE-2025-31133.patch +patch/0050-runc-fix-CVE-2025-52565.patch +patch/0051-runc-fix-CVE-2025-52881.patch -- Gitee