[meta-virtualization][wrynose][PATCH 2/3] containerd: fix CVE-2026-47262
"Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)" <[email protected]> Fri, 10 Jul 2026 17:56:51 +0530
| Newsgroups | org.yoctoproject.lists.meta-virtualization |
|---|---|
| Message-ID | <[email protected]> |
From: Deepak Rathore <[email protected]> This patch applies the upstream release/2.2 backport for CVE-2026-47262. The upstream fix commit is referenced in [1], and the public CVE advisory is referenced in [2]. [1] https://github.com/containerd/containerd/commit/30708e8d1142287e9c6bb839f1b3f84c71ca4485 [2] https://github.com/containerd/containerd/security/advisories/GHSA-jpcc-p29g-p8mq Signed-off-by: Deepak Rathore <[email protected]> --- .../containerd/CVE-2026-47262.patch | 276 ++++++++++++++++++ .../containerd/containerd_git.bb | 1 + 2 files changed, 277 insertions(+) create mode 100644 recipes-containers/containerd/containerd/CVE-2026-47262.patch diff --git a/recipes-containers/containerd/containerd/CVE-2026-47262.patch b/recipes-containers/containerd/containerd/CVE-2026-47262.patch new file mode 100644 index 00000000..47f8a594 --- /dev/null +++ b/recipes-containers/containerd/containerd/CVE-2026-47262.patch @@ -0,0 +1,276 @@ +From 35162bac80f7801b1f99e0b86d0fa3f198470999 Mon Sep 17 00:00:00 2001 +From: Chris Henzie <[email protected]> +Date: Fri, 15 May 2026 22:19:37 +0000 +Subject: [PATCH 2/3] Bound user-database file reads in openUserFile + +openUserFile now stats the opened file, refuses anything that is not a +regular file, and wraps the returned fs.File so reads are capped at +maxUserFileBytes (10 MiB). All callers of openUserFile read either +etc/passwd or etc/group; both are regular files on real systems, well +under the cap. + +The cap and the regular-file check together bound parser memory use +when reading user-database files of unexpected shape or size. + +Adds tests for the cap and for the non-regular file rejection. The cap +test covers three boundary points: a small pad (trailing entry parsed), +a pad placing the entry's last byte exactly on the cap (still parsed), +and a pad past the cap (read returns an "exceeds" error). + +CVE: CVE-2026-47262 +Upstream-Status: Backport [https://github.com/containerd/containerd/commit/30708e8d1142287e9c6bb839f1b3f84c71ca4485] + +Assisted-by: Antigravity +Signed-off-by: Chris Henzie <[email protected]> +(cherry picked from commit 7b05ec421d0a07b33964c74145b6bf5dff58f476) +Signed-off-by: Chris Henzie <[email protected]> +(cherry picked from commit 30708e8d1142287e9c6bb839f1b3f84c71ca4485) +Signed-off-by: Deepak Rathore <[email protected]> +--- + pkg/oci/spec_opts.go | 56 +++++++++- + pkg/oci/spec_opts_user_bounds_test.go | 146 ++++++++++++++++++++++++++ + 2 files changed, 200 insertions(+), 2 deletions(-) + create mode 100644 pkg/oci/spec_opts_user_bounds_test.go + +diff --git a/pkg/oci/spec_opts.go b/pkg/oci/spec_opts.go +index 5cc7187d7..7e695fda1 100644 +--- a/pkg/oci/spec_opts.go ++++ b/pkg/oci/spec_opts.go +@@ -24,6 +24,7 @@ import ( + "encoding/json" + "errors" + "fmt" ++ "io" + "io/fs" + "math" + "os" +@@ -1821,10 +1822,13 @@ type readLinker interface { + // openUserFile attempts to open a file within the root fs. + // It handles cases where the file is an absolute symlink (e.g., NixOS /etc/passwd -> /nix/store/...), + // which triggers "path escapes from parent" errors in Go 1.24+ due to stricter os.DirFS validation. ++// ++// The returned file rejects non-regular sources and returns an error if more ++// than maxUserFileBytes are read from it. + func openUserFile(root fs.FS, name string) (fs.File, error) { + f, err := root.Open(name) + if err == nil { +- return f, nil ++ return wrapUserFile(f, name) + } + + // Check if the FS implements our local ReadLink interface. +@@ -1841,7 +1845,11 @@ func openUserFile(root fs.FS, name string) (fs.File, error) { + if rerr == nil { + // filepath.Rel might return OS-specific separators (backslashes on Windows). + // fs.Open strictly expects forward slashes, so we convert it. +- return root.Open(filepath.ToSlash(rel)) ++ f, oerr := root.Open(filepath.ToSlash(rel)) ++ if oerr != nil { ++ return nil, oerr ++ } ++ return wrapUserFile(f, name) + } + } + } +@@ -1850,3 +1858,47 @@ func openUserFile(root fs.FS, name string) (fs.File, error) { + // Return the original error if we couldn't resolve it + return nil, err + } ++ ++// maxUserFileBytes caps how much data is read from any user-database file ++// opened via openUserFile. Real systems keep these files well under 1 MiB; ++// 10 MiB is generous headroom while keeping peak memory during ++// user.ParsePasswd/ParseGroup bounded to single-digit MiB. ++const maxUserFileBytes = 10 << 20 ++ ++// wrapUserFile rejects non-regular sources and returns an fs.File that ++// errors out if more than maxUserFileBytes are read from it. ++func wrapUserFile(f fs.File, name string) (fs.File, error) { ++ info, err := f.Stat() ++ if err != nil { ++ f.Close() ++ return nil, fmt.Errorf("stat %s: %w", name, err) ++ } ++ if !info.Mode().IsRegular() { ++ f.Close() ++ return nil, fmt.Errorf("%s is not a regular file", name) ++ } ++ return &limitedFile{ ++ File: f, ++ // Allow one byte past the cap so an overflow surfaces as an ++ // error rather than a silent EOF that the parser would treat as ++ // a clean end-of-file (and miss any entries past the cap). ++ r: &io.LimitedReader{R: f, N: maxUserFileBytes + 1}, ++ name: name, ++ }, nil ++} ++ ++// limitedFile is an fs.File whose Read returns an error once more than ++// maxUserFileBytes have been read. ++type limitedFile struct { ++ fs.File ++ r *io.LimitedReader ++ name string ++} ++ ++func (l *limitedFile) Read(p []byte) (int, error) { ++ n, err := l.r.Read(p) ++ if l.r.N == 0 { ++ return n, fmt.Errorf("%q exceeds %d bytes", l.name, maxUserFileBytes) ++ } ++ return n, err ++} +diff --git a/pkg/oci/spec_opts_user_bounds_test.go b/pkg/oci/spec_opts_user_bounds_test.go +new file mode 100644 +index 000000000..54384f79a +--- /dev/null ++++ b/pkg/oci/spec_opts_user_bounds_test.go +@@ -0,0 +1,146 @@ ++/* ++ Copyright The containerd Authors. ++ ++ Licensed under the Apache License, Version 2.0 (the "License"); ++ you may not use this file except in compliance with the License. ++ You may obtain a copy of the License at ++ ++ http://www.apache.org/licenses/LICENSE-2.0 ++ ++ Unless required by applicable law or agreed to in writing, software ++ distributed under the License is distributed on an "AS IS" BASIS, ++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ See the License for the specific language governing permissions and ++ limitations under the License. ++*/ ++ ++package oci ++ ++import ( ++ "bytes" ++ "errors" ++ "io/fs" ++ "testing" ++ "testing/fstest" ++ "time" ++ ++ "github.com/moby/sys/user" ++ "github.com/stretchr/testify/assert" ++) ++ ++// TestOpenUserFileCapsReads asserts the boundary behavior of the read cap: ++// well below, ending exactly at, and past maxUserFileBytes. ++func TestOpenUserFileCapsReads(t *testing.T) { ++ t.Parallel() ++ ++ beyond := []byte("\nbeyond:x:42:\n") ++ ++ for _, tc := range []struct { ++ name string ++ padBytes int ++ wantGids []uint32 ++ wantErr bool ++ }{ ++ { ++ name: "pad below cap, beyond is parsed", ++ padBytes: 100, ++ wantGids: []uint32{42}, ++ }, ++ { ++ name: "beyond ends exactly at cap, is parsed", ++ padBytes: maxUserFileBytes - len(beyond), ++ wantGids: []uint32{42}, ++ }, ++ { ++ name: "pad past cap, read errors out", ++ padBytes: maxUserFileBytes, ++ wantErr: true, ++ }, ++ } { ++ t.Run(tc.name, func(t *testing.T) { ++ t.Parallel() ++ ++ data := append(bytes.Repeat([]byte{0}, tc.padBytes), beyond...) ++ fsys := fstest.MapFS{ ++ "etc/group": &fstest.MapFile{Data: data, Mode: 0o644}, ++ } ++ ++ gids, err := getSupplementalGroupsFromFS(fsys, func(g user.Group) bool { ++ return g.Name == "beyond" ++ }) ++ if tc.wantErr { ++ assert.ErrorContains(t, err, "exceeds") ++ return ++ } ++ assert.NoError(t, err) ++ assert.Equal(t, tc.wantGids, gids) ++ }) ++ } ++} ++ ++// TestOpenUserFileRejectsNonRegularFiles verifies that non-regular files ++// are refused before any byte is read from them. ++func TestOpenUserFileRejectsNonRegularFiles(t *testing.T) { ++ t.Parallel() ++ ++ for _, tc := range []struct { ++ name string ++ mode fs.FileMode ++ }{ ++ {name: "char device", mode: fs.ModeDevice | fs.ModeCharDevice | 0o666}, ++ {name: "socket", mode: fs.ModeSocket | 0o666}, ++ } { ++ t.Run(tc.name, func(t *testing.T) { ++ t.Parallel() ++ ++ f := &nonRegularFile{mode: tc.mode} ++ rootFS := singleFileFS{name: "etc/group", file: f} ++ ++ _, err := getSupplementalGroupsFromFS(rootFS, nil) ++ assert.Error(t, err) ++ assert.False(t, f.readCalled, "Read should not be called on non-regular file") ++ }) ++ } ++} ++ ++// nonRegularFile implements fs.File and reports a configurable non-regular ++// mode via Stat. ++type nonRegularFile struct { ++ mode fs.FileMode ++ readCalled bool ++} ++ ++func (f *nonRegularFile) Read([]byte) (int, error) { ++ f.readCalled = true ++ return 0, errors.New("read should not be called on non-regular file") ++} ++ ++func (f *nonRegularFile) Stat() (fs.FileInfo, error) { ++ return nonRegularFileInfo{mode: f.mode}, nil ++} ++func (f *nonRegularFile) Close() error { return nil } ++ ++type nonRegularFileInfo struct { ++ mode fs.FileMode ++} ++ ++func (nonRegularFileInfo) Name() string { return "group" } ++func (nonRegularFileInfo) Size() int64 { return 0 } ++func (i nonRegularFileInfo) Mode() fs.FileMode { return i.mode } ++func (nonRegularFileInfo) ModTime() time.Time { return time.Time{} } ++func (nonRegularFileInfo) IsDir() bool { return false } ++func (nonRegularFileInfo) Sys() any { return nil } ++ ++// singleFileFS routes a single name to a single fs.File and returns ++// fs.ErrNotExist for everything else. ++type singleFileFS struct { ++ name string ++ file fs.File ++} ++ ++func (s singleFileFS) Open(name string) (fs.File, error) { ++ if name == s.name { ++ return s.file, nil ++ } ++ return nil, fs.ErrNotExist ++} +-- +2.35.6 diff --git a/recipes-containers/containerd/containerd_git.bb b/recipes-containers/containerd/containerd_git.bb index ad41f248..43891c2f 100644 --- a/recipes-containers/containerd/containerd_git.bb +++ b/recipes-containers/containerd/containerd_git.bb @@ -11,6 +11,7 @@ SRC_URI = "git://github.com/containerd/containerd;branch=release/2.2;protocol=ht file://0001-build-don-t-use-gcflags-to-define-trimpath.patch \ file://cni-containerd-net.conflist \ file://CVE-2026-46680.patch \ + file://CVE-2026-47262.patch \ " # Apache-2.0 for containerd -- 2.35.6