From d0d593bac51417e669b49fd5fa4e6209d054130c Mon Sep 17 00:00:00 2001 From: dongyuzhen Date: Mon, 17 Nov 2025 18:37:14 +0800 Subject: [PATCH] fix CVE-2025-31133 --- git-commit | 2 +- patch/0049-runc-fix-CVE-2025-31133.patch | 268 +++++++++++++++++++++++ runc.spec | 8 +- series.conf | 1 + 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 patch/0049-runc-fix-CVE-2025-31133.patch diff --git a/git-commit b/git-commit index b59dc61..941822f 100644 --- a/git-commit +++ b/git-commit @@ -1 +1 @@ -eff48af98905b28ba5f4aa3ed92d1aff97f1ef59 +9018fc222c11b01d106b9f7d4f6cc38572d1a374 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..a5e6c12 --- /dev/null +++ b/patch/0049-runc-fix-CVE-2025-31133.patch @@ -0,0 +1,268 @@ +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 | 68 ++++++++++++++++++++++++++--- + libcontainer/standard_init_linux.go | 41 +++++++++-------- + 5 files changed, 120 insertions(+), 35 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 0000000..075387f +--- /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 0000000..d5019db +--- /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 d9f1813..50c7a12 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 4678c76..364ecfc 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" +@@ -325,7 +326,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 +@@ -1083,18 +1084,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 8f595a4..ee907d8 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/runc.spec b/runc.spec index faa40fc..50dc50a 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 +* Mon Nov 17 2025 dongyuzhen - 1.1.8-27 +- Type:CVE +- CVE:NA +- SUG:NA +- DESC:fix CVE-2025-31133 + * Wed Mar 26 2025 dongyuzhen - 1.1.8-26 - Type:bugfix - CVE:NA diff --git a/series.conf b/series.conf index d62f689..2bee6b1 100644 --- a/series.conf +++ b/series.conf @@ -44,3 +44,4 @@ 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 -- Gitee