[meta-virtualization][scarthgap][PATCH 2/2] docker-moby: Fix CVE-2026-42306
"Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)" <[email protected]> Wed, 15 Jul 2026 22:55:49 +0530
| Newsgroups | org.yoctoproject.lists.meta-virtualization |
|---|---|
| Message-ID | <[email protected]> |
From: Deepak Rathore <[email protected]> This patch applies the upstream 25.0 backport for CVE-2026-42306. The upstream fix commits are referenced in [1] and [2], and the public CVE advisory is referenced in [3]. The Scarthgap-specific Go 1.22 API adaptation is documented in the embedded patch headers. [1] https://github.com/moby/moby/commit/63772fe8630ee74f7c45d20637f3161cc750f4da [2] https://github.com/moby/moby/commit/f82f5a92d2ca3a2337b167bdc0a91d99a89756ef [3] https://github.com/moby/moby/security/advisories/GHSA-rg2x-37c3-w2rh Signed-off-by: Deepak Rathore <[email protected]> --- recipes-containers/docker/docker-moby_git.bb | 2 + .../docker/files/CVE-2026-42306_p1.patch | 183 ++++++++++++++++++ .../docker/files/CVE-2026-42306_p2.patch | 161 +++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 recipes-containers/docker/files/CVE-2026-42306_p1.patch create mode 100644 recipes-containers/docker/files/CVE-2026-42306_p2.patch diff --git a/recipes-containers/docker/docker-moby_git.bb b/recipes-containers/docker/docker-moby_git.bb index f9121b3..68e53b0 100644 --- a/recipes-containers/docker/docker-moby_git.bb +++ b/recipes-containers/docker/docker-moby_git.bb @@ -60,6 +60,8 @@ SRC_URI = "\ file://CVE-2026-34040_p1.patch;patchdir=src/import \ file://CVE-2026-34040_p2.patch;patchdir=src/import \ file://CVE-2026-41568.patch;patchdir=src/import \ + file://CVE-2026-42306_p1.patch;patchdir=src/import \ + file://CVE-2026-42306_p2.patch;patchdir=src/import \ " DOCKER_COMMIT = "${SRCREV_moby}" diff --git a/recipes-containers/docker/files/CVE-2026-42306_p1.patch b/recipes-containers/docker/files/CVE-2026-42306_p1.patch new file mode 100644 index 0000000..c07a74a --- /dev/null +++ b/recipes-containers/docker/files/CVE-2026-42306_p1.patch @@ -0,0 +1,183 @@ +From b4eb1144ac13d0a8e1b7d7e6a3d35ce97865d036 Mon Sep 17 00:00:00 2001 +From: Deepak Rathore <[email protected]> +Date: Tue, 30 Jun 2026 05:17:49 -0700 +Subject: [PATCH] Fix bind mount target redirection via symlink swap during + docker cp +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Pin mount targets via /proc/self/fd file descriptors to prevent TOCTOU +attacks. + +Previously, a container process could swap a path component with a +symlink between GetResourcePath resolution and directory/file creation +or mount, allowing writes to arbitrary host paths outside the container. + +Open the resolved destination through os.Root to get a pinned fd, then +mount onto /proc/self/fd/<fd> instead of re-resolving the +container-relative path. This closes the TOCTOU window between +createIfNotExists and mount. + +Because the kernel rejects remount and propagation-change syscalls on +/proc/self/fd paths, the initial bind mount uses the fd path for safety, +then Readlink resolves the real path for the subsequent read-only +remount and rprivate propagation change. + +CVE: CVE-2026-42306 +Upstream-Status: Backport [https://github.com/moby/moby/commit/63772fe8630ee74f7c45d20637f3161cc750f4da] + +Backport Changes: +- Replaced upstream root.Open with the Scarthgap openInRoot helper, + which uses openat(2) and O_NOFOLLOW from a pinned container-root fd + because Go 1.22 does not provide os.Root. +- Used a raw O_PATH fd and /proc/self/fd/<fd> for the initial bind + mount, then kept the upstream real-path remount and propagation flow. +- Kept raw fd close handling explicit: fatal paths close before + returning, while non-fatal makeMountRRO errors fall through to the + common close so targetFd is closed exactly once. +- Retained the strings import because pathComponents still uses + strings.Split in the Scarthgap openat(2) helper; upstream removes it + because os.Root avoids that helper. + +Signed-off-by: Paweł Gronowski <[email protected]> +(cherry picked from commit 43fa458a9c40873867e75221454de10709b04236) +Signed-off-by: Cory Snider <[email protected]> +(cherry picked from commit 63772fe8630ee74f7c45d20637f3161cc750f4da) +Signed-off-by: Deepak Rathore <[email protected]> +--- + daemon/containerfs_linux.go | 67 ++++++++++++++++++++++---- + 1 file changed, 57 insertions(+), 10 deletions(-) + +diff --git a/daemon/containerfs_linux.go b/daemon/containerfs_linux.go +index ab6b199..3ed4a88 100644 +--- a/daemon/containerfs_linux.go ++++ b/daemon/containerfs_linux.go +@@ -7,6 +7,7 @@ import ( + "os" + "path/filepath" + "runtime" ++ "strconv" + "strings" + + "github.com/containerd/log" +@@ -89,10 +90,14 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + } + defer unix.Close(rootFd) + ++ // TODO(vvoland): Refactor this after security release. + for _, m := range mounts { +- dest, err := container.GetResourcePath(m.Destination) ++ // Destination is an absolute path within container ++ // filesystem. For the root-scoped openat operations to ++ // work, convert it to a path relative to root fs /. ++ relDest, err := filepath.Rel("/", m.Destination) + if err != nil { +- return err ++ return fmt.Errorf("make destination relative: %w", err) + } + + var stat os.FileInfo +@@ -100,7 +105,7 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + if err != nil { + return err + } +- if err := createIfNotExists(rootFd, strings.TrimPrefix(m.Destination, "/"), stat.IsDir()); err != nil { ++ if err := createIfNotExists(rootFd, relDest, stat.IsDir()); err != nil { + return err + } + +@@ -108,9 +113,7 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + if m.NonRecursive { + bindMode = "bind" + } +- writeMode := "ro" + if m.Writable { +- writeMode = "rw" + if m.ReadOnlyNonRecursive { + return errors.New("options conflict: Writable && ReadOnlyNonRecursive") + } +@@ -122,6 +125,37 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + return errors.New("options conflict: ReadOnlyNonRecursive && ReadOnlyForceRecursive") + } + ++ // Open the mount target through the container root so we ++ // have a file descriptor pinning the resolved inode. Using ++ // /proc/self/fd/<fd> as the mount target prevents any ++ // subsequent symlink swap from redirecting the mount. ++ targetFd, err := openInRoot(rootFd, relDest) ++ if err != nil { ++ return fmt.Errorf("open mount target %q: %w", m.Destination, err) ++ } ++ targetPath := "/proc/self/fd/" + strconv.Itoa(targetFd) ++ ++ // The kernel rejects remount and propagation-change syscalls ++ // when the target is a /proc/self/fd path. Only the initial ++ // bind mount works on such paths, so we perform that via the ++ // fd path for TOCTOU safety and then resolve the real path for ++ // the read-only remount and propagation change. ++ if err := mount.Mount(m.Source, targetPath, "", bindMode); err != nil { ++ unix.Close(targetFd) ++ return err ++ } ++ realPath, err := os.Readlink(targetPath) ++ if err != nil { ++ unix.Close(targetFd) ++ return fmt.Errorf("readlink %s: %w", targetPath, err) ++ } ++ if !m.Writable { ++ if err := mount.Mount("", realPath, "", "ro,remount,bind"); err != nil { ++ unix.Close(targetFd) ++ return err ++ } ++ } ++ + // openContainerFS() is called for temporary mounts + // outside the container. Soon these will be unmounted + // with lazy unmount option and given we have mounted +@@ -132,20 +166,21 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + // all these mounts rprivate. Do not use propagation + // property of volume as that should apply only when + // mounting happens inside the container. +- opts := strings.Join([]string{bindMode, writeMode, "rprivate"}, ",") +- if err := mount.Mount(m.Source, dest, "", opts); err != nil { ++ if err := mount.MakeRPrivate(realPath); err != nil { ++ unix.Close(targetFd) + return err + } + + if !m.Writable && !m.ReadOnlyNonRecursive { +- if err := makeMountRRO(dest); err != nil { ++ if err := makeMountRRO(realPath); err != nil { + if m.ReadOnlyForceRecursive { ++ unix.Close(targetFd) + return err +- } else { +- log.G(context.TODO()).WithError(err).Debugf("Failed to make %q recursively read-only", dest) + } ++ log.G(context.TODO()).WithError(err).Debugf("Failed to make %q recursively read-only", m.Destination) + } + } ++ unix.Close(targetFd) + } + + return mounttree.SwitchRoot(container.BaseFS) +@@ -278,6 +313,18 @@ func createIfNotExists(rootFd int, unsafePath string, isDir bool) error { + return unix.Close(fd) + } + ++func openInRoot(rootFd int, unsafePath string) (int, error) { ++ parentFd, base, closeParent, err := openParentInRoot(rootFd, unsafePath) ++ if err != nil { ++ return -1, err ++ } ++ if closeParent { ++ defer unix.Close(parentFd) ++ } ++ ++ return unix.Openat(parentFd, base, unix.O_PATH|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) ++} ++ + func mkdirAllInRoot(rootFd int, unsafePath string) error { + parts, err := pathComponents(unsafePath) + if err != nil { diff --git a/recipes-containers/docker/files/CVE-2026-42306_p2.patch b/recipes-containers/docker/files/CVE-2026-42306_p2.patch new file mode 100644 index 0000000..ef02668 --- /dev/null +++ b/recipes-containers/docker/files/CVE-2026-42306_p2.patch @@ -0,0 +1,161 @@ +From 5ec5db4aa88385b86df0f4e109ecabde25498b2c Mon Sep 17 00:00:00 2001 +From: Deepak Rathore <[email protected]> +Date: Tue, 30 Jun 2026 05:19:01 -0700 +Subject: [PATCH] daemon: resolve in-container symlinks before os.Root mount ops +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The security fix in GHSA-vp62-88p7-qqf5 switched openContainerFS to +os.Root for mount-destination operations, but stopped walking the +destination through in-container symlinks. + +os.Root refuses to follow absolute symlinks, so any container whose +image had an absolute symlink along the mount target's path (e.g. the +common /var/run -> /run in ubuntu/alpine/busybox) broke `docker cp`. + +Walk m.Destination through ctr.GetResourcePath first which follows +symlinks to get a path relative to BaseFS, then keep using os.Root for +the actual MkdirAll/OpenFile/Open calls. + +CVE: CVE-2026-42306 +Upstream-Status: Backport [https://github.com/moby/moby/commit/f82f5a92d2ca3a2337b167bdc0a91d99a89756ef] + +Backport Changes: +- Reworded upstream os.Root comments only where this branch uses the + Scarthgap openat(2)-based root-scoped helpers from p1. +- Added an Fstat check after O_PATH|O_NOFOLLOW so a final symlink fd is + rejected, matching the intended os.Root.Open behavior. +- Ported upstream TestCopyWithAbsoluteSymlinkedMountTarget from + integration/container/copy_linux_test.go into Scarthgap's + integration/container/copy_test.go, where the copy integration tests + live in this branch. + +Signed-off-by: Paweł Gronowski <[email protected]> +(cherry picked from commit fb3702d033601bb0e767f2ca398e3909a15be29c) +Signed-off-by: Cory Snider <[email protected]> +(cherry picked from commit f82f5a92d2ca3a2337b167bdc0a91d99a89756ef) +Signed-off-by: Deepak Rathore <[email protected]> +--- + daemon/containerfs_linux.go | 29 +++++++++++++++++++++----- + integration/container/copy_test.go | 49 ++++++++++++++++++++++++++++++++++++++ + 2 files changed, 73 insertions(+), 5 deletions(-) + +diff --git a/daemon/containerfs_linux.go b/daemon/containerfs_linux.go +index 3ed4a88..f7ed847 100644 +--- a/daemon/containerfs_linux.go ++++ b/daemon/containerfs_linux.go +@@ -92,10 +92,16 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + + // TODO(vvoland): Refactor this after security release. + for _, m := range mounts { +- // Destination is an absolute path within container +- // filesystem. For the root-scoped openat operations to +- // work, convert it to a path relative to root fs /. +- relDest, err := filepath.Rel("/", m.Destination) ++ // Walk m.Destination through the container's symlinks ++ // before passing it to root-scoped openat operations. ++ // This preserves common absolute symlinks such as ++ // /var/run -> /run while later fd-based operations still ++ // enforce the mount destination protections. ++ resolved, err := container.GetResourcePath(m.Destination) ++ if err != nil { ++ return fmt.Errorf("resolve mount destination %q: %w", m.Destination, err) ++ } ++ relDest, err := filepath.Rel(container.BaseFS, resolved) + if err != nil { + return fmt.Errorf("make destination relative: %w", err) + } +@@ -322,7 +328,20 @@ func openInRoot(rootFd int, unsafePath string) (int, error) { + defer unix.Close(parentFd) + } + +- return unix.Openat(parentFd, base, unix.O_PATH|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) ++ fd, err := unix.Openat(parentFd, base, unix.O_PATH|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) ++ if err != nil { ++ return -1, err ++ } ++ var st unix.Stat_t ++ if err := unix.Fstat(fd, &st); err != nil { ++ unix.Close(fd) ++ return -1, err ++ } ++ if st.Mode&unix.S_IFMT == unix.S_IFLNK { ++ unix.Close(fd) ++ return -1, fmt.Errorf("mount target %q is a symlink", unsafePath) ++ } ++ return fd, nil + } + + func mkdirAllInRoot(rootFd int, unsafePath string) error { +diff --git a/integration/container/copy_test.go b/integration/container/copy_test.go +index 38d2c799f2..a03cc38b03 100644 +--- a/integration/container/copy_test.go ++++ b/integration/container/copy_test.go +@@ -10,10 +10,13 @@ import ( + "testing" + + "github.com/docker/docker/api/types" ++ mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/errdefs" ++ "github.com/docker/docker/integration/internal/build" + "github.com/docker/docker/integration/internal/container" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/jsonmessage" ++ "github.com/docker/docker/testutil" + "github.com/docker/docker/testutil/fakecontext" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +@@ -129,6 +132,52 @@ func TestCopyToContainerPathIsNotDir(t *testing.T) { + assert.Check(t, is.ErrorContains(err, "not a directory")) + } + ++// TestCopyWithAbsoluteSymlinkedMountTarget was introduced as a regression test ++// for https://github.com/moby/moby/issues/52653. ++// ++// The security fix in GHSA-vp62-88p7-qqf5 switched openContainerFS to use ++// os.Root for the mount-destination operations. ++// os.Root refuses to follow absolute symlinks, but distro images commonly ship ++// /var/run as an absolute symlink to /run. ++// As a result, any container with a bind mount whose target traversed such a ++// symlink (e.g. -v /host/sock:/var/run/docker.sock) made `docker cp` fail. ++func TestCopyWithAbsoluteSymlinkedMountTarget(t *testing.T) { ++ skip.If(t, testEnv.DaemonInfo.OSType != "linux") ++ ctx := setupTest(t) ++ apiClient := testEnv.APIClient() ++ ++ // Build an image with an absolute in-container symlink along the mount ++ // target path. ++ // Stock distro images expose this shape via /var/run -> /run, but we set ++ // up our own /sockets -> /root pair so the test does not depend on any ++ // particular base image's layout. ++ buildCtx := fakecontext.New(t, "", ++ fakecontext.WithDockerfile(`FROM busybox ++RUN touch /root/nil && ln -s /root /sockets ++`), ++ ) ++ defer buildCtx.Close() ++ imgID := build.Do(ctx, t, apiClient, buildCtx) ++ ++ // Use testutil.TempDir so the rootless daemon can access the bind-mount ++ // source: t.TempDir() creates a 0700 parent that the fake-root user ++ // cannot stat. ++ srcDir := testutil.TempDir(t) ++ assert.NilError(t, os.WriteFile(filepath.Join(srcDir, "sock"), nil, 0o644)) ++ ++ cid := container.Create(ctx, t, apiClient, ++ container.WithImage(imgID), ++ container.WithMount(mounttypes.Mount{ ++ Type: mounttypes.TypeBind, ++ Source: filepath.Join(srcDir, "sock"), ++ Target: "/sockets/docker.sock", ++ }), ++ ) ++ ++ err := apiClient.CopyToContainer(ctx, cid, "/sockets/", bytes.NewReader(nil), types.CopyToContainerOptions{}) ++ assert.NilError(t, err) ++} ++ + func TestCopyFromContainer(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType == "windows") + ctx := setupTest(t) -- 2.35.6