[meta-virtualization][scarthgap][PATCH 1/2] docker-moby: Fix CVE-2026-41568
"Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)" <[email protected]> Wed, 15 Jul 2026 22:55:48 +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-41568. The upstream fix commit is referenced in [1], and the public CVE advisory is referenced in [2]. The Scarthgap-specific Go 1.22 API adaptation is documented in the embedded patch header. [1] https://github.com/moby/moby/commit/742e28ed8d654372f0aa5b549da74c2f6e674194 [2] https://github.com/moby/moby/security/advisories/GHSA-vp62-88p7-qqf5 Signed-off-by: Deepak Rathore <[email protected]> --- recipes-containers/docker/docker-moby_git.bb | 1 + .../docker/files/CVE-2026-41568.patch | 275 ++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 recipes-containers/docker/files/CVE-2026-41568.patch diff --git a/recipes-containers/docker/docker-moby_git.bb b/recipes-containers/docker/docker-moby_git.bb index eb493a4..f9121b3 100644 --- a/recipes-containers/docker/docker-moby_git.bb +++ b/recipes-containers/docker/docker-moby_git.bb @@ -59,6 +59,7 @@ SRC_URI = "\ file://CVE-2026-33997.patch;patchdir=src/import \ 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 \ " DOCKER_COMMIT = "${SRCREV_moby}" diff --git a/recipes-containers/docker/files/CVE-2026-41568.patch b/recipes-containers/docker/files/CVE-2026-41568.patch new file mode 100644 index 0000000..8dffd72 --- /dev/null +++ b/recipes-containers/docker/files/CVE-2026-41568.patch @@ -0,0 +1,275 @@ +From 9afb2d06f1d14b71be59d6662d4da2deacfb45fc Mon Sep 17 00:00:00 2001 +From: Deepak Rathore <[email protected]> +Date: Tue, 30 Jun 2026 05:16:06 -0700 +Subject: [PATCH] daemon/copy: Fix symlink escape in mount destination creation +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Use os.Root to scope all filesystem operations in createIfNotExists to +the container root directory. + +This prevents a TOCTOU attack where a container process swaps a path +component with a symlink between GetResourcePath resolution and +directory/file creation, which could allow writing to arbitrary host +paths outside the container. + +CVE: CVE-2026-41568 +Upstream-Status: Backport [https://github.com/moby/moby/commit/742e28ed8d654372f0aa5b549da74c2f6e674194] + +Backport Changes: +- Replaced upstream os.Root operations with dirfd-relative + openat(2)/mkdirat(2) helpers, because Scarthgap uses Go 1.22 and + does not provide os.Root. +- Kept O_NOFOLLOW while walking each path component so the backport + preserves the upstream root-scoped symlink protection. +- Adapted the createIfNotExists unit test to open the container root + with unix.Open. + +(cherry picked from commit 64a22d80b93ddc1416b501b5145df02947312249) +Signed-off-by: Paweł Gronowski <[email protected]> +(cherry picked from commit 742e28ed8d654372f0aa5b549da74c2f6e674194) +Signed-off-by: Deepak Rathore <[email protected]> +--- + daemon/containerfs_linux.go | 148 +++++++++++++++++++- + daemon/containerfs_linux_test.go | 51 +++++++ + 2 files changed, 197 insertions(+), 2 deletions(-) + create mode 100644 daemon/containerfs_linux_test.go + +diff --git a/daemon/containerfs_linux.go b/daemon/containerfs_linux.go +index 384c86b..ab6b199 100644 +--- a/daemon/containerfs_linux.go ++++ b/daemon/containerfs_linux.go +@@ -19,7 +19,6 @@ import ( + "github.com/docker/docker/container" + "github.com/docker/docker/internal/mounttree" + "github.com/docker/docker/internal/unshare" +- "github.com/docker/docker/pkg/fileutils" + ) + + type future struct { +@@ -83,6 +82,13 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + if err := mount.MakeRSlave("/"); err != nil { + return err + } ++ ++ rootFd, err := unix.Open(container.BaseFS, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) ++ if err != nil { ++ return fmt.Errorf("open container root: %w", err) ++ } ++ defer unix.Close(rootFd) ++ + for _, m := range mounts { + dest, err := container.GetResourcePath(m.Destination) + if err != nil { +@@ -94,7 +100,7 @@ func (daemon *Daemon) openContainerFS(container *container.Container) (_ *contai + if err != nil { + return err + } +- if err := fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil { ++ if err := createIfNotExists(rootFd, strings.TrimPrefix(m.Destination, "/"), stat.IsDir()); err != nil { + return err + } + +@@ -242,6 +248,144 @@ func (vw *containerFSView) Stat(ctx context.Context, path string) (*types.Contai + return stat, err + } + ++// createIfNotExists creates a file or directory below rootFd only if it does ++// not already exist. Path components are opened dirfd-relative with ++// O_NOFOLLOW so a container cannot swap in a symlink that escapes BaseFS. ++func createIfNotExists(rootFd int, unsafePath string, isDir bool) error { ++ if isDir { ++ return mkdirAllInRoot(rootFd, unsafePath) ++ } ++ ++ parentPath := filepath.Dir(unsafePath) ++ if parentPath != "." && parentPath != "/" { ++ if err := mkdirAllInRoot(rootFd, parentPath); err != nil { ++ return err ++ } ++ } ++ ++ parentFd, base, closeParent, err := openParentInRoot(rootFd, unsafePath) ++ if err != nil { ++ return err ++ } ++ if closeParent { ++ defer unix.Close(parentFd) ++ } ++ ++ fd, err := unix.Openat(parentFd, base, unix.O_CREAT|unix.O_WRONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0o755) ++ if err != nil { ++ return err ++ } ++ return unix.Close(fd) ++} ++ ++func mkdirAllInRoot(rootFd int, unsafePath string) error { ++ parts, err := pathComponents(unsafePath) ++ if err != nil { ++ return err ++ } ++ ++ currentFd := rootFd ++ closeCurrent := false ++ for _, part := range parts { ++ nextFd, err := openDirAtNoFollow(currentFd, part) ++ if os.IsNotExist(err) { ++ if err := unix.Mkdirat(currentFd, part, 0o755); err != nil && !os.IsExist(err) { ++ if closeCurrent { ++ unix.Close(currentFd) ++ } ++ return err ++ } ++ nextFd, err = openDirAtNoFollow(currentFd, part) ++ } ++ if err != nil { ++ if closeCurrent { ++ unix.Close(currentFd) ++ } ++ return err ++ } ++ if closeCurrent { ++ unix.Close(currentFd) ++ } ++ currentFd = nextFd ++ closeCurrent = true ++ } ++ if closeCurrent { ++ return unix.Close(currentFd) ++ } ++ return nil ++} ++ ++func openParentInRoot(rootFd int, unsafePath string) (parentFd int, base string, closeParent bool, err error) { ++ parts, err := pathComponents(unsafePath) ++ if err != nil { ++ return -1, "", false, err ++ } ++ if len(parts) == 0 { ++ return -1, "", false, fmt.Errorf("empty path") ++ } ++ base = parts[len(parts)-1] ++ if len(parts) == 1 { ++ return rootFd, base, false, nil ++ } ++ ++ parentFd, err = openDirInRoot(rootFd, filepath.Join(parts[:len(parts)-1]...)) ++ if err != nil { ++ return -1, "", false, err ++ } ++ return parentFd, base, true, nil ++} ++ ++func openDirInRoot(rootFd int, unsafePath string) (int, error) { ++ parts, err := pathComponents(unsafePath) ++ if err != nil { ++ return -1, err ++ } ++ if len(parts) == 0 { ++ return rootFd, nil ++ } ++ ++ currentFd := rootFd ++ closeCurrent := false ++ for _, part := range parts { ++ nextFd, err := openDirAtNoFollow(currentFd, part) ++ if err != nil { ++ if closeCurrent { ++ unix.Close(currentFd) ++ } ++ return -1, err ++ } ++ if closeCurrent { ++ unix.Close(currentFd) ++ } ++ currentFd = nextFd ++ closeCurrent = true ++ } ++ return currentFd, nil ++} ++ ++func openDirAtNoFollow(rootFd int, name string) (int, error) { ++ return unix.Openat(rootFd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) ++} ++ ++func pathComponents(unsafePath string) ([]string, error) { ++ if filepath.IsAbs(unsafePath) { ++ return nil, fmt.Errorf("absolute path %q is not scoped to container root", unsafePath) ++ } ++ ++ cleanPath := filepath.Clean(unsafePath) ++ if cleanPath == "." { ++ return nil, nil ++ } ++ ++ parts := strings.Split(cleanPath, string(os.PathSeparator)) ++ for _, part := range parts { ++ if part == "" || part == "." || part == ".." { ++ return nil, fmt.Errorf("path %q escapes container root", unsafePath) ++ } ++ } ++ return parts, nil ++} ++ + // makeMountRRO makes the mount recursively read-only. + func makeMountRRO(dest string) error { + attr := &unix.MountAttr{ +diff --git a/daemon/containerfs_linux_test.go b/daemon/containerfs_linux_test.go +new file mode 100644 +index 0000000..08d797b +--- /dev/null ++++ b/daemon/containerfs_linux_test.go +@@ -0,0 +1,51 @@ ++package daemon // import "github.com/docker/docker/daemon" ++ ++import ( ++ "os" ++ "path/filepath" ++ "testing" ++ ++ "golang.org/x/sys/unix" ++ "gotest.tools/v3/assert" ++) ++ ++func TestCreateIfNotExists(t *testing.T) { ++ t.Run("directory", func(t *testing.T) { ++ dir := t.TempDir() ++ rootFd := openRootFd(t, dir) ++ defer unix.Close(rootFd) ++ ++ err := createIfNotExists(rootFd, "tocreate", true) ++ assert.NilError(t, err) ++ ++ fileinfo, err := os.Stat(filepath.Join(dir, "tocreate")) ++ assert.NilError(t, err, "Did not create destination") ++ assert.Assert(t, fileinfo.IsDir(), "Should have been a dir, seems it's not") ++ ++ err = createIfNotExists(rootFd, "tocreate", true) ++ assert.NilError(t, err, "Should not fail if already exists") ++ }) ++ t.Run("file", func(t *testing.T) { ++ dir := t.TempDir() ++ rootFd := openRootFd(t, dir) ++ defer unix.Close(rootFd) ++ ++ err := createIfNotExists(rootFd, "file/to/create", false) ++ assert.NilError(t, err) ++ ++ fileinfo, err := os.Stat(filepath.Join(dir, "file/to/create")) ++ assert.NilError(t, err, "Did not create destination") ++ ++ assert.Assert(t, !fileinfo.IsDir(), "Should have been a file, but created a directory") ++ ++ err = createIfNotExists(rootFd, "file/to/create", false) ++ assert.NilError(t, err, "Should not fail if already exists") ++ }) ++} ++ ++func openRootFd(t *testing.T, path string) int { ++ t.Helper() ++ fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) ++ assert.NilError(t, err) ++ return fd ++} -- 2.35.6