* [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195
@ 2026-10-16 10:06 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (4 more replies)
0 siblings, 5 replies; 10+ messages in thread
From: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-10-16 10:06 UTC (permalink / raw)
To: meta-virtualization
From: Deepak Rathore <deeratho@cisco.com>
This patch applies the upstream release/2.2 backport for CVE-2026-50195.
The upstream fix commit is referenced in [1], and the public CVE record
is referenced in [2].
[1] https://github.com/containerd/containerd/commit/cff57884176a1e6ba0857a417753d799958e0f46
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-50195
Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
.../containerd/CVE-2026-50195.patch | 73 +++++++++++++++++++
.../containerd/containerd_git.bb | 1 +
2 files changed, 74 insertions(+)
create mode 100644 recipes-containers/containerd/containerd/CVE-2026-50195.patch
diff --git a/recipes-containers/containerd/containerd/CVE-2026-50195.patch b/recipes-containers/containerd/containerd/CVE-2026-50195.patch
new file mode 100644
index 00000000..2aad846f
--- /dev/null
+++ b/recipes-containers/containerd/containerd/CVE-2026-50195.patch
@@ -0,0 +1,73 @@
+From d996462fc527969992a87e0170992a8397222c82 Mon Sep 17 00:00:00 2001
+From: Samuel Karp <samuelkarp@google.com>
+Date: Tue, 26 May 2026 16:06:58 -0700
+Subject: [PATCH 1/4] cri: do not re-tag restored checkpoints
+
+Google-Bug-Id: 508657842
+
+CVE: CVE-2026-50195
+Upstream-Status: Backport [https://github.com/containerd/containerd/commit/cff57884176a1e6ba0857a417753d799958e0f46]
+
+Signed-off-by: Samuel Karp <samuelkarp@google.com>
+(cherry picked from commit 0c0918fa8fb4d997f889a3d811603995a3a2b68a)
+Signed-off-by: Samuel Karp <samuelkarp@google.com>
+(cherry picked from commit cff57884176a1e6ba0857a417753d799958e0f46)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ .../checkpoint/checkpoint-restore-cri-test.sh | 4 ++--
+ .../cri/server/container_checkpoint_linux.go | 17 -----------------
+ 2 files changed, 2 insertions(+), 19 deletions(-)
+
+diff --git a/contrib/checkpoint/checkpoint-restore-cri-test.sh b/contrib/checkpoint/checkpoint-restore-cri-test.sh
+index ff1f6e6c0..44e42426b 100755
+--- a/contrib/checkpoint/checkpoint-restore-cri-test.sh
++++ b/contrib/checkpoint/checkpoint-restore-cri-test.sh
+@@ -110,7 +110,7 @@ function test_from_archive() {
+ fi
+ # Cleanup
+ echo "--> Cleanup images: "
+- crictl rmi "${TEST_IMAGE}" | sed 's/^/----> \t/'
++ (crictl rmi "${TEST_IMAGE}" || true) | sed 's/^/----> \t/'
+ echo -n "--> Verifying container rootfs: "
+ crictl exec "$ctr_id" ls -la /root/testfile
+ if crictl exec "$ctr_id" ls -la /etc/motd >/dev/null 2>&1; then
+@@ -184,7 +184,7 @@ function test_from_oci() {
+ echo "--> Cleanup images: "
+ ../../bin/ctr -n k8s.io images rm localhost/checkpoint-image:latest | sed 's/^/----> \t/'
+ echo "--> Cleanup images: "
+- crictl rmi "${TEST_IMAGE}" | sed 's/^/----> \t/'
++ (crictl rmi "${TEST_IMAGE}" || true) | sed 's/^/----> \t/'
+ echo "--> Deleting all pods: "
+ crictl -t 5s rmp -fa | sed 's/^/----> \t/'
+ SUCCESS=1
+diff --git a/internal/cri/server/container_checkpoint_linux.go b/internal/cri/server/container_checkpoint_linux.go
+index b54963ae8..b122c392d 100644
+--- a/internal/cri/server/container_checkpoint_linux.go
++++ b/internal/cri/server/container_checkpoint_linux.go
+@@ -331,23 +331,6 @@ func (c *criService) CRImportCheckpoint(
+ if _, err := reference.ParseAnyReference(config.RootfsImageName); err != nil {
+ return "", fmt.Errorf("error parsing reference: %q is not a valid repository/tag %v", config.RootfsImageName, err)
+ }
+- tagImage, err := c.client.ImageService().Get(ctx, config.RootfsImageRef)
+- if err != nil {
+- return "", fmt.Errorf("failed to get checkpoint base image %s: %w", config.RootfsImageRef, err)
+- }
+- // Second step is to tag the image with the same tag it used to have
+- // during checkpointing. For the error that the image NAME:TAG already
+- // exists is ignored. It could happen that NAME:TAG now belongs to
+- // another NAME@DIGEST than during checkpointing and the restore will
+- // happen on another image.
+- // TODO: handle if NAME:TAG points to a different NAME@DIGEST
+- tagImage.Name = config.RootfsImageName
+- _, err = c.client.ImageService().Create(ctx, tagImage)
+- if err != nil {
+- if !errdefs.IsAlreadyExists(err) {
+- return "", fmt.Errorf("failed to tag checkpoint base image %s with %s: %w", config.RootfsImageRef, config.RootfsImageName, err)
+- }
+- }
+
+ var image imagestore.Image
+ for i := 1; i < 500; i++ {
+--
+2.35.6
+
diff --git a/recipes-containers/containerd/containerd_git.bb b/recipes-containers/containerd/containerd_git.bb
index 3cf088f0..4e8d95ee 100644
--- a/recipes-containers/containerd/containerd_git.bb
+++ b/recipes-containers/containerd/containerd_git.bb
@@ -13,6 +13,7 @@ SRC_URI = "git://github.com/containerd/containerd;branch=release/2.2;protocol=ht
file://CVE-2026-46680.patch \
file://CVE-2026-47262.patch \
file://CVE-2026-53488.patch \
+ file://CVE-2026-50195.patch \
"
# Apache-2.0 for containerd
--
2.35.6
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492
2026-10-16 10:06 [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-16 10:06 ` Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 3/5] containerd: Fix CVE-2026-53489 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (3 subsequent siblings)
4 siblings, 1 reply; 10+ messages in thread
From: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-10-16 10:06 UTC (permalink / raw)
To: meta-virtualization
From: Deepak Rathore <deeratho@cisco.com>
This patch applies the upstream release/2.2 backport for CVE-2026-53492.
The upstream fix commit is referenced in [1], and the public CVE record
is referenced in [2].
[1] https://github.com/containerd/containerd/commit/be8460656b84c4a1a4b244a03801e9fff1e914d3
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-53492
Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
.../containerd/CVE-2026-53492.patch | 274 ++++++++++++++++++
.../containerd/containerd_git.bb | 1 +
2 files changed, 275 insertions(+)
create mode 100644 recipes-containers/containerd/containerd/CVE-2026-53492.patch
diff --git a/recipes-containers/containerd/containerd/CVE-2026-53492.patch b/recipes-containers/containerd/containerd/CVE-2026-53492.patch
new file mode 100644
index 00000000..b291d94a
--- /dev/null
+++ b/recipes-containers/containerd/containerd/CVE-2026-53492.patch
@@ -0,0 +1,274 @@
+From 4e572a31ab40c4c04f72c133b47b87d0d79e38c2 Mon Sep 17 00:00:00 2001
+From: Samuel Karp <samuelkarp@google.com>
+Date: Mon, 8 Jun 2026 00:58:16 +0000
+Subject: [PATCH 2/4] cri: filter CDI annotations on checkpoint restore
+
+Filter out any annotations on the checkpointed container matching
+`cdi.k8s.io/` or exactly `cdi.k8s.io` during restore to prevent
+unauthorized device restoration. When an annotation is denied, a warning
+log is generated.
+
+Tested by:
+* Unit tests for exact matching, prefix boundaries, and metadata merging
+* Complete CRI integration and checkpoint restore suite
+
+Assisted-by: Antigravity
+
+CVE: CVE-2026-53492
+Upstream-Status: Backport [https://github.com/containerd/containerd/commit/be8460656b84c4a1a4b244a03801e9fff1e914d3]
+
+Signed-off-by: Samuel Karp <samuelkarp@google.com>
+(cherry picked from commit 861ffc1097685f9ecf13adaa381aca5fdf7ef0b4)
+Signed-off-by: Chris Henzie <chrishenzie@gmail.com>
+(cherry picked from commit be8460656b84c4a1a4b244a03801e9fff1e914d3)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ .../checkpoint/checkpoint-restore-cri-test.sh | 22 +++-
+ .../cri/server/container_checkpoint_linux.go | 56 ++++++---
+ .../server/container_checkpoint_linux_test.go | 107 ++++++++++++++++++
+ 3 files changed, 167 insertions(+), 18 deletions(-)
+ create mode 100644 internal/cri/server/container_checkpoint_linux_test.go
+
+diff --git a/contrib/checkpoint/checkpoint-restore-cri-test.sh b/contrib/checkpoint/checkpoint-restore-cri-test.sh
+index 44e42426b..e8c5c77c1 100755
+--- a/contrib/checkpoint/checkpoint-restore-cri-test.sh
++++ b/contrib/checkpoint/checkpoint-restore-cri-test.sh
+@@ -56,6 +56,15 @@ TESTDATA=testdata
+ # shellcheck disable=SC2034
+ export CONTAINERD_ADDRESS="$TESTDIR/c.sock"
+ export CONTAINER_RUNTIME_ENDPOINT="unix:///${CONTAINERD_ADDRESS}"
++
++# Generate crictl config file with 30s timeout
++export CRI_CONFIG_FILE="${TESTDIR}/crictl.yaml"
++cat <<EOF > "${CRI_CONFIG_FILE}"
++runtime-endpoint: unix://${CONTAINERD_ADDRESS}
++image-endpoint: unix://${CONTAINERD_ADDRESS}
++timeout: 30
++EOF
++
+ TEST_IMAGE=ghcr.io/containerd/alpine
+
+ function test_from_archive() {
+@@ -69,9 +78,12 @@ function test_from_archive() {
+ echo -n "--> Start pod: "
+ pod_id=$(crictl runp "$POD_JSON")
+ echo "$pod_id"
++ CTR_JSON=$(mktemp)
++ jq '.annotations = {"cdi.k8s.io/device":"gpu","safe.annotation":"true"}' "$TESTDATA"/container_sleep.json >"$CTR_JSON"
+ echo -n "--> Create container: "
+- ctr_id=$(crictl create "$pod_id" "$TESTDATA"/container_sleep.json "$POD_JSON")
++ ctr_id=$(crictl create "$pod_id" "$CTR_JSON" "$POD_JSON")
+ echo "$ctr_id"
++ rm -f "$CTR_JSON"
+ echo -n "--> Start container: "
+ crictl start "$ctr_id"
+ lines_before=$(crictl logs "$ctr_id" | wc -l)
+@@ -108,6 +120,12 @@ function test_from_archive() {
+ "should be larger than before checkpointing ($lines_before)"
+ false
+ fi
++ echo "--> Verifying CDI annotation filtering on restore: "
++ actual_annots=$(crictl inspect "$ctr_id" | jq -c '.status.annotations')
++ if jq -e 'has("cdi.k8s.io/device") or (has("safe.annotation") | not)' <<<"$actual_annots" >/dev/null; then
++ echo "error: CDI annotation was not filtered or safe annotation missing: $actual_annots"
++ exit 1
++ fi
+ # Cleanup
+ echo "--> Cleanup images: "
+ (crictl rmi "${TEST_IMAGE}" || true) | sed 's/^/----> \t/'
+@@ -192,6 +210,8 @@ function test_from_oci() {
+
+ cat >"${TESTDIR}/config.toml" <<EOF
+ version = 3
++[plugins."io.containerd.cri.v1.runtime"]
++ enable_cdi = false
+ [plugins."io.containerd.cri.v1.runtime".containerd]
+ default_runtime_name = "test-runtime"
+ [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.test-runtime]
+diff --git a/internal/cri/server/container_checkpoint_linux.go b/internal/cri/server/container_checkpoint_linux.go
+index b122c392d..13cd3e2e1 100644
+--- a/internal/cri/server/container_checkpoint_linux.go
++++ b/internal/cri/server/container_checkpoint_linux.go
+@@ -290,23 +290,11 @@ func (c *criService) CRImportCheckpoint(
+ }
+ }
+
+- if createAnnotations != nil {
+- // The hash also needs to be update or Kubernetes thinks the container needs to be restarted
+- _, ok1 := createAnnotations["io.kubernetes.container.hash"]
+- _, ok2 := originalAnnotations["io.kubernetes.container.hash"]
+-
+- if ok1 && ok2 {
+- originalAnnotations["io.kubernetes.container.hash"] = createAnnotations["io.kubernetes.container.hash"]
+- }
+-
+- // The restart count also needs to be correctly updated
+- _, ok1 = createAnnotations["io.kubernetes.container.restartCount"]
+- _, ok2 = originalAnnotations["io.kubernetes.container.restartCount"]
+-
+- if ok1 && ok2 {
+- originalAnnotations["io.kubernetes.container.restartCount"] = createAnnotations["io.kubernetes.container.restartCount"]
+- }
+- }
++ originalAnnotations = filterAndMergeAnnotations(
++ ctx,
++ originalAnnotations,
++ createAnnotations,
++ )
+
+ var containerdImage client.Image
+
+@@ -737,3 +725,37 @@ func writeSpecDumpFile(ctx context.Context, store content.Store, desc v1.Descrip
+
+ return nil
+ }
++
++func filterAndMergeAnnotations(
++ ctx context.Context,
++ checkpointAnnotations map[string]string,
++ createAnnotations map[string]string,
++) map[string]string {
++ result := make(map[string]string)
++
++ for k, v := range checkpointAnnotations {
++ if strings.HasPrefix(k, "cdi.k8s.io/") || k == "cdi.k8s.io" {
++ log.G(ctx).Warnf("Denying annotation %q in checkpoint restore", k)
++ continue
++ }
++ result[k] = v
++ }
++
++ // The hash also needs to be update or Kubernetes thinks the container needs to be restarted
++ _, ok1 := createAnnotations["io.kubernetes.container.hash"]
++ _, ok2 := result["io.kubernetes.container.hash"]
++
++ if ok1 && ok2 {
++ result["io.kubernetes.container.hash"] = createAnnotations["io.kubernetes.container.hash"]
++ }
++
++ // The restart count also needs to be correctly updated
++ _, ok1 = createAnnotations["io.kubernetes.container.restartCount"]
++ _, ok2 = result["io.kubernetes.container.restartCount"]
++
++ if ok1 && ok2 {
++ result["io.kubernetes.container.restartCount"] = createAnnotations["io.kubernetes.container.restartCount"]
++ }
++
++ return result
++}
+diff --git a/internal/cri/server/container_checkpoint_linux_test.go b/internal/cri/server/container_checkpoint_linux_test.go
+new file mode 100644
+index 000000000..3010e5233
+--- /dev/null
++++ b/internal/cri/server/container_checkpoint_linux_test.go
+@@ -0,0 +1,107 @@
++//go:build linux
++
++/*
++ 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 server
++
++import (
++ "context"
++ "sync"
++ "testing"
++
++ "github.com/containerd/log"
++ "github.com/sirupsen/logrus"
++ "github.com/stretchr/testify/assert"
++)
++
++type testLogHook struct {
++ mu sync.Mutex
++ entries []string
++}
++
++func (h *testLogHook) Levels() []logrus.Level {
++ return []logrus.Level{logrus.WarnLevel}
++}
++
++func (h *testLogHook) Fire(entry *logrus.Entry) error {
++ h.mu.Lock()
++ defer h.mu.Unlock()
++ h.entries = append(h.entries, entry.Message)
++ return nil
++}
++
++func TestFilterAndMergeAnnotations(t *testing.T) {
++ for desc, tc := range map[string]struct {
++ checkpointAnnotations map[string]string
++ createAnnotations map[string]string
++ expectedAnnotations map[string]string
++ expectedWarnings []string
++ }{
++ "cdi denied prefix boundaries": {
++ checkpointAnnotations: map[string]string{
++ "cdi.k8s.io/device": "gpu",
++ "cdi.k8s.io/": "true",
++ "cdi.k8s.io": "true",
++ "safe.org/cdi.k8s.io": "ignored",
++ "other": "val",
++ },
++ expectedAnnotations: map[string]string{
++ "safe.org/cdi.k8s.io": "ignored",
++ "other": "val",
++ },
++ expectedWarnings: []string{
++ `Denying annotation "cdi.k8s.io/device" in checkpoint restore`,
++ `Denying annotation "cdi.k8s.io/" in checkpoint restore`,
++ `Denying annotation "cdi.k8s.io" in checkpoint restore`,
++ },
++ },
++
++ "createAnnotations update kubernetes metadata if present in both": {
++ checkpointAnnotations: map[string]string{
++ "io.kubernetes.container.hash": "old-hash",
++ "io.kubernetes.container.restartCount": "1",
++ "safe.annotation": "2",
++ },
++ createAnnotations: map[string]string{
++ "io.kubernetes.container.hash": "new-hash",
++ "io.kubernetes.container.restartCount": "2",
++ },
++ expectedAnnotations: map[string]string{
++ "io.kubernetes.container.hash": "new-hash",
++ "io.kubernetes.container.restartCount": "2",
++ "safe.annotation": "2",
++ },
++ },
++ } {
++ t.Run(desc, func(t *testing.T) {
++ logger := logrus.New()
++ logger.SetLevel(logrus.WarnLevel)
++ hook := &testLogHook{}
++ logger.AddHook(hook)
++ ctx := log.WithLogger(context.Background(), logrus.NewEntry(logger))
++
++ res := filterAndMergeAnnotations(
++ ctx,
++ tc.checkpointAnnotations,
++ tc.createAnnotations,
++ )
++
++ assert.Equal(t, tc.expectedAnnotations, res)
++ assert.ElementsMatch(t, tc.expectedWarnings, hook.entries)
++ })
++ }
++}
+--
+2.35.6
+
diff --git a/recipes-containers/containerd/containerd_git.bb b/recipes-containers/containerd/containerd_git.bb
index 4e8d95ee..b2e9187a 100644
--- a/recipes-containers/containerd/containerd_git.bb
+++ b/recipes-containers/containerd/containerd_git.bb
@@ -14,6 +14,7 @@ SRC_URI = "git://github.com/containerd/containerd;branch=release/2.2;protocol=ht
file://CVE-2026-47262.patch \
file://CVE-2026-53488.patch \
file://CVE-2026-50195.patch \
+ file://CVE-2026-53492.patch \
"
# Apache-2.0 for containerd
--
2.35.6
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [meta-virtualization][wrynose][PATCH 3/5] containerd: Fix CVE-2026-53489
2026-10-16 10:06 [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-16 10:06 ` Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 4/5] containerd: Fix CVE-2026-53495 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (2 subsequent siblings)
4 siblings, 1 reply; 10+ messages in thread
From: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-10-16 10:06 UTC (permalink / raw)
To: meta-virtualization
From: Deepak Rathore <deeratho@cisco.com>
This patch applies the upstream release/2.2 backport for CVE-2026-53489.
The upstream fix commit is referenced in [1], and the public CVE record
is referenced in [2].
[1] https://github.com/containerd/containerd/commit/357652293053d0cd3ed565f718b0050aa662ae1a
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-53489
Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
.../containerd/CVE-2026-53489.patch | 529 ++++++++++++++++++
.../containerd/containerd_git.bb | 1 +
2 files changed, 530 insertions(+)
create mode 100644 recipes-containers/containerd/containerd/CVE-2026-53489.patch
diff --git a/recipes-containers/containerd/containerd/CVE-2026-53489.patch b/recipes-containers/containerd/containerd/CVE-2026-53489.patch
new file mode 100644
index 00000000..74915474
--- /dev/null
+++ b/recipes-containers/containerd/containerd/CVE-2026-53489.patch
@@ -0,0 +1,529 @@
+From 0e6f9d96caa978fd688a2d94140e95f348bfe74f Mon Sep 17 00:00:00 2001
+From: Brian Goff <cpuguy83@gmail.com>
+Date: Tue, 9 Jun 2026 11:37:18 -0700
+Subject: [PATCH 3/4] cri: make checkpoint restore robust to unexpected archive
+ content
+
+The CRI checkpoint restore path unpacked checkpoint archive/OCI image content
+directly into the container's persistent state directory and read files such as
+container.log back from it with a symlink-following copy. Checkpoint content is
+externally provided, so make restore more defensive about what it unpacks and
+how it reads those files back.
+
+Behavior changes:
+
+- Only unpack regular files and directories from the checkpoint archive.
+
+- Unpack checkpoint content into a dedicated <state>/ctrd-restore
+ subdirectory created fresh rather than into the state dir itself, so
+ checkpoint content cannot collide with containerd's own files (e.g.
+ the "status" blob). Restore and cleanup operate on that subdir;
+ cleanup is now a single RemoveAll of it.
+
+CVE: CVE-2026-53489
+Upstream-Status: Backport [https://github.com/containerd/containerd/commit/357652293053d0cd3ed565f718b0050aa662ae1a]
+
+Backport Changes:
+- CVE-2026-53492 and this upstream commit independently add
+ internal/cri/server/container_checkpoint_linux_test.go. Applying the
+ selected series produces an add/add conflict for that file.
+- Resolve the conflict by merging the upstream CVE-2026-53489 checkpoint
+ archive and symlink tests into the file created by CVE-2026-53492. This
+ retains both CVEs' upstream test coverage without changing security code.
+
+Signed-off-by: Brian Goff <cpuguy83@gmail.com>
+(cherry picked from commit 8196411f24065533093be4c7ad874c23b06178f3)
+Signed-off-by: Brian Goff <cpuguy83@gmail.com>
+(cherry picked from commit 357652293053d0cd3ed565f718b0050aa662ae1a)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ .../cri/server/container_checkpoint_linux.go | 186 ++++++++++++++----
+ .../server/container_checkpoint_linux_test.go | 130 ++++++++++++
+ internal/cri/server/container_start.go | 35 ++--
+ 3 files changed, 286 insertions(+), 65 deletions(-)
+
+diff --git a/internal/cri/server/container_checkpoint_linux.go b/internal/cri/server/container_checkpoint_linux.go
+index 13cd3e2e1..26f0d9b25 100644
+--- a/internal/cri/server/container_checkpoint_linux.go
++++ b/internal/cri/server/container_checkpoint_linux.go
+@@ -31,7 +31,6 @@ import (
+ "time"
+
+ crmetadata "github.com/checkpoint-restore/checkpointctl/lib"
+- "github.com/checkpoint-restore/go-criu/v7/stats"
+ "github.com/checkpoint-restore/go-criu/v7/utils"
+ "github.com/containerd/containerd/api/types/runc/options"
+ "github.com/containerd/containerd/v2/client"
+@@ -56,9 +55,86 @@ import (
+ "github.com/opencontainers/image-spec/identity"
+ v1 "github.com/opencontainers/image-spec/specs-go/v1"
+ spec "github.com/opencontainers/runtime-spec/specs-go"
++ "golang.org/x/sys/unix"
+ runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
++
++ // TODO: This package import is kept to prevent merge conflicts while integrating multiple
++ // branches, specifically because this changes vendoring.
++ _ "github.com/checkpoint-restore/go-criu/v7/stats"
+ )
+
++// copyNoFollow copies the regular file at src to dst without following a symlink
++// at the final path component of src.
++//
++// The checkpoint code reads files (container.log, status, stats-dump, dump.log)
++// out of the container state directory, which can contain entries unpacked from a
++// checkpoint archive or OCI image. Those entries are externally provided, so they
++// are read defensively.
++//
++// src is first lstat'd (which does not follow a final-component symlink) and must
++// be a regular file; non-regular entries are rejected before src is ever opened.
++// src is then opened with O_NOFOLLOW as a belt-and-suspenders guard in case the
++// entry changes type between the lstat and the open.
++func copyNoFollow(src, dst string, perm os.FileMode) error {
++ fi, err := os.Lstat(src)
++ if err != nil {
++ return err
++ }
++ if !fi.Mode().IsRegular() {
++ return fmt.Errorf("refusing to copy %s: not a regular file", src)
++ }
++
++ in, err := os.OpenFile(src, os.O_RDONLY|unix.O_NOFOLLOW, 0)
++ if err != nil {
++ return err
++ }
++ defer in.Close()
++
++ out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
++ if err != nil {
++ return err
++ }
++ defer out.Close()
++
++ _, err = io.Copy(out, in)
++ return err
++}
++
++// checkpointArchiveEntryAllowed reports whether a tar entry from a checkpoint
++// archive may be unpacked. Legitimate checkpoint archives contain only regular
++// files and directories; other entry types (symlinks, hardlinks, device and fifo
++// nodes) are not produced by the checkpoint code and are rejected as a hardening
++// measure.
++func checkpointArchiveEntryAllowed(hdr *tar.Header) bool {
++ switch hdr.Typeflag {
++ //nolint:staticcheck // TypeRegA is deprecated but we may still receive an external tar with TypeRegA
++ case tar.TypeReg, tar.TypeRegA, tar.TypeDir, tar.TypeXGlobalHeader:
++ return true
++ default:
++ return false
++ }
++}
++
++// assertCheckpointDirSafe verifies that the populated restore directory contains
++// only regular files and directories.
++//
++// The OCI-image restore path copies checkpoint content into the restore dir with
++// fs.CopyDir, which (unlike the tar unpack filter) faithfully recreates any
++// symlinks and special files present in the image. Restore-time consumers open
++// paths under this directory, so non-regular entries are rejected before they run.
++func assertCheckpointDirSafe(root string) error {
++ return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
++ if err != nil {
++ return err
++ }
++ // d.Type() reports the entry type without following symlinks.
++ if d.IsDir() || d.Type().IsRegular() {
++ return nil
++ }
++ return fmt.Errorf("refusing to restore checkpoint: %s is not a regular file or directory", path)
++ })
++}
++
+ // checkIfCheckpointOCIImage returns checks if the input refers to a checkpoint image.
+ // It returns the StorageImageID of the image the input resolves to, nil otherwise.
+ func (c *criService) checkIfCheckpointOCIImage(ctx context.Context, input string) (string, error) {
+@@ -187,6 +263,12 @@ func (c *criService) CRImportCheckpoint(
+ }(archiveFile)
+
+ filter := archive.WithFilter(func(hdr *tar.Header) (bool, error) {
++ // Reject entry types the checkpoint code never produces (symlinks,
++ // hardlinks, device/fifo nodes) so they are not recreated on disk.
++ if !checkpointArchiveEntryAllowed(hdr) {
++ log.G(ctx).Warnf("Skipping unexpected checkpoint archive entry %q (type %d)", hdr.Name, hdr.Typeflag)
++ return false, nil
++ }
+ // The checkpoint archive is unpacked twice if using a tar file directly.
+ // The first time only the metadata files are relevant to prepare the
+ // restore operation. This filter function ignores the large parts of
+@@ -381,16 +463,39 @@ func (c *criService) CRImportCheckpoint(
+ return "", err
+ }
+
++ // Confine all checkpoint content to a dedicated subdirectory of the container
++ // state dir instead of unpacking it directly into the state dir, so it cannot
++ // collide with containerd's own files there. Create it fresh; RemoveAll unlinks
++ // any pre-existing entry without following it.
++ restoreDir := filepath.Join(containerRootDir, checkpointRestoreDir)
++ if err := os.RemoveAll(restoreDir); err != nil {
++ return "", err
++ }
++ if err := os.Mkdir(restoreDir, 0o700); err != nil {
++ return "", err
++ }
++
+ if restoreStorageImageID != "" {
+- if err := fs.CopyDir(containerRootDir, mountPoint); err != nil {
++ if err := fs.CopyDir(restoreDir, mountPoint); err != nil {
+ return "", err
+ }
+ if err := mount.UnmountAll(mountPoint, 0); err != nil {
+ return "", err
+ }
++ // fs.CopyDir recreates any symlinks/special files from the image; reject
++ // them here so restore-time consumers only ever open regular files.
++ if err := assertCheckpointDirSafe(restoreDir); err != nil {
++ return "", err
++ }
+ } else {
+ // unpack the checkpoint archive
+ filter := archive.WithFilter(func(hdr *tar.Header) (bool, error) {
++ // Reject entry types the checkpoint code never produces (symlinks,
++ // hardlinks, device/fifo nodes) so they are not recreated on disk.
++ if !checkpointArchiveEntryAllowed(hdr) {
++ log.G(ctx).Warnf("Skipping unexpected checkpoint archive entry %q (type %d)", hdr.Name, hdr.Typeflag)
++ return false, nil
++ }
+ excludePatterns := []string{
+ crmetadata.ConfigDumpFile,
+ crmetadata.SpecDumpFile,
+@@ -408,19 +513,21 @@ func (c *criService) CRImportCheckpoint(
+
+ // Start from the beginning of the checkpoint archive
+ archiveFile.Seek(0, 0)
+- _, err = archive.Apply(ctx, containerRootDir, archiveFile, []archive.ApplyOpt{filter}...)
++ _, err = archive.Apply(ctx, restoreDir, archiveFile, []archive.ApplyOpt{filter}...)
+
+ if err != nil {
+- return "", fmt.Errorf("unpacking of checkpoint archive %s failed: %w", containerRootDir, err)
++ return "", fmt.Errorf("unpacking of checkpoint archive %s failed: %w", restoreDir, err)
+ }
+ }
+- log.G(ctx).Debugf("Unpacked checkpoint in %s", containerRootDir)
++ log.G(ctx).Debugf("Unpacked checkpoint in %s", restoreDir)
+
+- // Restore container log file (if it exists)
+- containerLog := filepath.Join(containerRootDir, "container.log")
+- _, err = c.os.Stat(containerLog)
+- if err == nil {
+- if err := c.os.CopyFile(containerLog, meta.LogPath, 0600); err != nil {
++ // Restore container log file (if it exists).
++ //
++ // container.log was unpacked from a checkpoint archive/OCI image, so it is
++ // copied without following a final-component symlink.
++ containerLog := filepath.Join(restoreDir, "container.log")
++ if err := copyNoFollow(containerLog, meta.LogPath, 0600); err != nil {
++ if !errors.Is(err, os.ErrNotExist) {
+ return "", fmt.Errorf("restoring container log file %s failed: %w", containerLog, err)
+ }
+ }
+@@ -488,7 +595,25 @@ func (c *criService) CheckpointContainer(ctx context.Context, r *runtime.Checkpo
+ if err != nil {
+ return nil, fmt.Errorf("failed to get task for container %q: %w", r.GetContainerId(), err)
+ }
+- img, err := task.Checkpoint(ctx, []client.CheckpointTaskOpts{withCheckpointOpts(i.Runtime.Name, c.getContainerRootDir(r.GetContainerId()))}...)
++
++ cpPath := filepath.Join(c.getContainerRootDir(container.ID), "ctrd-checkpoint")
++ // ctrd-checkpoint may already exist from a prior checkpoint operation. RemoveAll
++ // unlinks any existing entry (including a symlink) itself rather than its target,
++ // so creating the directory afterwards cannot write through a link.
++ if err := os.RemoveAll(cpPath); err != nil {
++ return nil, err
++ }
++ if err := os.Mkdir(cpPath, 0o700); err != nil {
++ return nil, err
++ }
++ defer os.RemoveAll(cpPath)
++
++ // Point CRIU's work directory (where it writes dump.log and stats-dump) at the
++ // dedicated, freshly-created checkpoint dir instead of the persistent container
++ // state dir. Otherwise checkpoint creation litters those files into the state
++ // dir where they are never cleaned up; here they land directly where they are
++ // archived from and are removed with cpPath.
++ img, err := task.Checkpoint(ctx, []client.CheckpointTaskOpts{withCheckpointOpts(i.Runtime.Name, cpPath)}...)
+ if err != nil {
+ return nil, fmt.Errorf("checkpointing container %q failed: %w", r.GetContainerId(), err)
+ }
+@@ -513,43 +638,20 @@ func (c *criService) CheckpointContainer(ctx context.Context, r *runtime.Checkpo
+ return nil, fmt.Errorf("failed to unmarshall blob into checkpoint data OCI index: %w", err)
+ }
+
+- cpPath := filepath.Join(c.getContainerRootDir(r.GetContainerId()), "ctrd-checkpoint")
+- if err := os.MkdirAll(cpPath, 0o700); err != nil {
+- return nil, err
+- }
+- defer os.RemoveAll(cpPath)
+-
+- // This internal containerd file is used by checkpointctl for
+- // checkpoint archive analysis.
+- if err := c.os.CopyFile(
+- filepath.Join(c.getContainerRootDir(r.GetContainerId()), crmetadata.StatusFile),
++ // This internal containerd file is used by checkpointctl for checkpoint archive
++ // analysis. It lives in the container state dir, which can hold files from a
++ // prior checkpoint operation, so it is read without following symlinks.
++ if err := copyNoFollow(
++ filepath.Join(c.getContainerRootDir(container.ID), crmetadata.StatusFile),
+ filepath.Join(cpPath, crmetadata.StatusFile),
+ 0o600,
+ ); err != nil {
+ return nil, err
+ }
+
+- // This file is created by CRIU and includes timing analysis.
+- // Also used by checkpointctl
+- if err := c.os.CopyFile(
+- filepath.Join(c.getContainerRootDir(r.GetContainerId()), stats.StatsDump),
+- filepath.Join(cpPath, stats.StatsDump),
+- 0o600,
+- ); err != nil {
+- return nil, err
+- }
+-
+- // The log file created by CRIU. This file could be missing.
+- // Let's ignore errors if the file is missing.
+- if err := c.os.CopyFile(
+- filepath.Join(c.getContainerRootDir(r.GetContainerId()), crmetadata.DumpLogFile),
+- filepath.Join(cpPath, crmetadata.DumpLogFile),
+- 0o600,
+- ); err != nil {
+- if !errors.Is(errors.Unwrap(err), os.ErrNotExist) {
+- return nil, err
+- }
+- }
++ // dump.log and stats-dump are written directly into cpPath by CRIU via its
++ // work directory (see withCheckpointOpts above), so they are already present
++ // for archiving and do not need to be copied out of the container state dir.
+
+ // Save the existing container log file
+ _, err = c.os.Stat(criContainerStatus.GetStatus().GetLogPath())
+diff --git a/internal/cri/server/container_checkpoint_linux_test.go b/internal/cri/server/container_checkpoint_linux_test.go
+index 3010e5233..740a52100 100644
+--- a/internal/cri/server/container_checkpoint_linux_test.go
++++ b/internal/cri/server/container_checkpoint_linux_test.go
+@@ -19,13 +19,19 @@
+ package server
+
+ import (
++ "archive/tar"
+ "context"
++ "errors"
++ "os"
++ "path/filepath"
+ "sync"
+ "testing"
+
+ "github.com/containerd/log"
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
++ "github.com/stretchr/testify/require"
++ "golang.org/x/sys/unix"
+ )
+
+ type testLogHook struct {
+@@ -105,3 +111,127 @@ func TestFilterAndMergeAnnotations(t *testing.T) {
+ })
+ }
+ }
++
++func TestCopyNoFollowRegularFile(t *testing.T) {
++ dir := t.TempDir()
++ src := filepath.Join(dir, "src")
++ dst := filepath.Join(dir, "dst")
++ require.NoError(t, os.WriteFile(src, []byte("hello"), 0o644))
++
++ require.NoError(t, copyNoFollow(src, dst, 0o600))
++
++ data, err := os.ReadFile(dst)
++ require.NoError(t, err)
++ assert.Equal(t, "hello", string(data))
++
++ info, err := os.Stat(dst)
++ require.NoError(t, err)
++ assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
++}
++
++func TestCopyNoFollowMissingSource(t *testing.T) {
++ dir := t.TempDir()
++ src := filepath.Join(dir, "does-not-exist")
++ dst := filepath.Join(dir, "dst")
++
++ err := copyNoFollow(src, dst, 0o600)
++ require.Error(t, err)
++ assert.True(t, errors.Is(err, os.ErrNotExist), "expected ErrNotExist, got %v", err)
++ assert.NoFileExists(t, dst)
++}
++
++func TestCopyNoFollowSymlinkSourceNotFollowed(t *testing.T) {
++ dir := t.TempDir()
++
++ // A stand-in for a file outside the copy that a symlink might point at.
++ secret := filepath.Join(dir, "outside-target")
++ require.NoError(t, os.WriteFile(secret, []byte("outside-content"), 0o600))
++
++ src := filepath.Join(dir, "container.log")
++ require.NoError(t, os.Symlink(secret, src))
++ dst := filepath.Join(dir, "dst")
++
++ err := copyNoFollow(src, dst, 0o600)
++ require.Error(t, err)
++ // A symlink is not a "missing file"; it must surface as an error, not be skipped.
++ assert.False(t, errors.Is(err, os.ErrNotExist))
++ // And the linked-to content must not have been copied into the destination.
++ assert.NoFileExists(t, dst)
++}
++
++func TestCopyNoFollowRejectsFIFO(t *testing.T) {
++ dir := t.TempDir()
++ src := filepath.Join(dir, "fifo")
++ require.NoError(t, unix.Mkfifo(src, 0o600))
++ dst := filepath.Join(dir, "dst")
++
++ // Must return promptly with an error rather than blocking on the FIFO open.
++ err := copyNoFollow(src, dst, 0o600)
++ require.Error(t, err)
++ assert.False(t, errors.Is(err, os.ErrNotExist))
++ assert.NoFileExists(t, dst)
++}
++
++func TestCopyNoFollowRejectsDirectory(t *testing.T) {
++ dir := t.TempDir()
++ src := filepath.Join(dir, "adir")
++ require.NoError(t, os.Mkdir(src, 0o700))
++ dst := filepath.Join(dir, "dst")
++
++ err := copyNoFollow(src, dst, 0o600)
++ require.Error(t, err)
++ assert.NoFileExists(t, dst)
++}
++
++func TestAssertCheckpointDirSafe(t *testing.T) {
++ t.Run("regular files and dirs allowed", func(t *testing.T) {
++ root := t.TempDir()
++ require.NoError(t, os.MkdirAll(filepath.Join(root, "checkpoint"), 0o700))
++ require.NoError(t, os.WriteFile(filepath.Join(root, "checkpoint", "img"), []byte("x"), 0o600))
++ require.NoError(t, os.WriteFile(filepath.Join(root, "rootfs-diff.tar"), []byte("x"), 0o600))
++ assert.NoError(t, assertCheckpointDirSafe(root))
++ })
++
++ t.Run("symlink rejected", func(t *testing.T) {
++ root := t.TempDir()
++ require.NoError(t, os.Symlink("/some/outside/path", filepath.Join(root, "rootfs-diff.tar")))
++ assert.Error(t, assertCheckpointDirSafe(root))
++ })
++
++ t.Run("symlink nested in subdir rejected", func(t *testing.T) {
++ root := t.TempDir()
++ require.NoError(t, os.MkdirAll(filepath.Join(root, "checkpoint"), 0o700))
++ require.NoError(t, os.Symlink("/some/outside/path", filepath.Join(root, "checkpoint", "pages-1.img")))
++ assert.Error(t, assertCheckpointDirSafe(root))
++ })
++
++ t.Run("fifo rejected", func(t *testing.T) {
++ root := t.TempDir()
++ require.NoError(t, unix.Mkfifo(filepath.Join(root, "fifo"), 0o600))
++ assert.Error(t, assertCheckpointDirSafe(root))
++ })
++}
++
++func TestCheckpointArchiveEntryAllowed(t *testing.T) {
++ for _, tc := range []struct {
++ name string
++ typ byte
++ allowed bool
++ }{
++ {"regular", tar.TypeReg, true},
++ //nolint:staticcheck // TypeRegA is deprecated but external tars may still use it
++ {"regular-A", tar.TypeRegA, true},
++ {"directory", tar.TypeDir, true},
++ {"global-header", tar.TypeXGlobalHeader, true},
++ {"symlink", tar.TypeSymlink, false},
++ {"hardlink", tar.TypeLink, false},
++ {"char-device", tar.TypeChar, false},
++ {"block-device", tar.TypeBlock, false},
++ {"fifo", tar.TypeFifo, false},
++ } {
++ t.Run(tc.name, func(t *testing.T) {
++ got := checkpointArchiveEntryAllowed(&tar.Header{Typeflag: tc.typ, Name: tc.name})
++ assert.Equal(t, tc.allowed, got)
++ })
++ }
++}
+diff --git a/internal/cri/server/container_start.go b/internal/cri/server/container_start.go
+index c0fb8c50f..0c7653cad 100644
+--- a/internal/cri/server/container_start.go
++++ b/internal/cri/server/container_start.go
+@@ -31,7 +31,6 @@ import (
+ runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
+
+ crmetadata "github.com/checkpoint-restore/checkpointctl/lib"
+- "github.com/checkpoint-restore/go-criu/v7/stats"
+ containerd "github.com/containerd/containerd/v2/client"
+ cio "github.com/containerd/containerd/v2/internal/cri/io"
+ containerstore "github.com/containerd/containerd/v2/internal/cri/store/container"
+@@ -41,6 +40,12 @@ import (
+ cioutil "github.com/containerd/containerd/v2/pkg/ioutil"
+ )
+
++// checkpointRestoreDir is the subdirectory under a container's persistent state
++// directory into which checkpoint content (CRIU images, container.log,
++// rootfs-diff.tar, ...) is unpacked during restore. Confining it here keeps
++// checkpoint content from colliding with containerd's own files in the state dir.
++const checkpointRestoreDir = "ctrd-restore"
++
+ // StartContainer starts the container.
+ func (c *criService) StartContainer(ctx context.Context, r *runtime.StartContainerRequest) (retRes *runtime.StartContainerResponse, retErr error) {
+ span := tracing.SpanFromContext(ctx)
+@@ -112,7 +117,7 @@ func (c *criService) StartContainer(ctx context.Context, r *runtime.StartContain
+ pid, err := container.Restore(
+ ctx,
+ ioCreation,
+- filepath.Join(c.getContainerRootDir(r.GetContainerId()), crmetadata.CheckpointDirectory),
++ filepath.Join(c.getContainerRootDir(r.GetContainerId()), checkpointRestoreDir, crmetadata.CheckpointDirectory),
+ )
+
+ if err != nil {
+@@ -155,28 +160,12 @@ func (c *criService) StartContainer(ctx context.Context, r *runtime.StartContain
+ // It handles the TaskExit event and update container state after this.
+ c.startContainerExitMonitor(context.Background(), id, task.Pid(), exitCh)
+
+- // cleanup checkpoint artifacts after restore
+- cleanup := [...]string{
+- crmetadata.RestoreLogFile,
+- crmetadata.DumpLogFile,
+- stats.StatsDump,
+- stats.StatsRestore,
+- crmetadata.NetworkStatusFile,
+- crmetadata.RootFsDiffTar,
+- crmetadata.DeletedFilesFile,
+- crmetadata.CheckpointDirectory,
+- crmetadata.StatusDumpFile,
+- crmetadata.ConfigDumpFile,
+- crmetadata.SpecDumpFile,
+- "container.log",
+- }
+- for _, del := range cleanup {
+- file := filepath.Join(c.getContainerRootDir(r.GetContainerId()), del)
+- err = os.RemoveAll(file)
+- if err != nil {
+- log.G(ctx).Infof("Non-fatal: removal of checkpoint file (%s) failed: %v", file, err)
+- }
++ // cleanup checkpoint artifacts after restore.
++ restoreDir := filepath.Join(c.getContainerRootDir(r.GetContainerId()), checkpointRestoreDir)
++ if err := os.RemoveAll(restoreDir); err != nil {
++ log.G(ctx).Warnf("Non-fatal: removal of checkpoint restore dir (%s) failed: %v", restoreDir, err)
+ }
++
+ log.G(ctx).Infof("Restored container %s successfully", r.GetContainerId())
+ return &runtime.StartContainerResponse{}, nil
+ }
+--
+2.35.6
+
diff --git a/recipes-containers/containerd/containerd_git.bb b/recipes-containers/containerd/containerd_git.bb
index b2e9187a..af756ba8 100644
--- a/recipes-containers/containerd/containerd_git.bb
+++ b/recipes-containers/containerd/containerd_git.bb
@@ -15,6 +15,7 @@ SRC_URI = "git://github.com/containerd/containerd;branch=release/2.2;protocol=ht
file://CVE-2026-53488.patch \
file://CVE-2026-50195.patch \
file://CVE-2026-53492.patch \
+ file://CVE-2026-53489.patch \
"
# Apache-2.0 for containerd
--
2.35.6
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [meta-virtualization][wrynose][PATCH 4/5] containerd: Fix CVE-2026-53495
2026-10-16 10:06 [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 3/5] containerd: Fix CVE-2026-53489 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-16 10:06 ` Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 5/5] containerd: Fix CVE-2026-53493 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Bruce Ashfield
4 siblings, 1 reply; 10+ messages in thread
From: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-10-16 10:06 UTC (permalink / raw)
To: meta-virtualization
From: Deepak Rathore <deeratho@cisco.com>
This patch applies the upstream release/2.2 backport for CVE-2026-53495.
The upstream fix commit is referenced in [1], and the public CVE record
is referenced in [2].
[1] https://github.com/containerd/containerd/commit/22ccf4314d1fe0834f8e28f10d37d5305ef9880c
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-53495
Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
.../containerd/CVE-2026-53495.patch | 108 ++++++++++++++++++
.../containerd/containerd_git.bb | 1 +
2 files changed, 109 insertions(+)
create mode 100644 recipes-containers/containerd/containerd/CVE-2026-53495.patch
diff --git a/recipes-containers/containerd/containerd/CVE-2026-53495.patch b/recipes-containers/containerd/containerd/CVE-2026-53495.patch
new file mode 100644
index 00000000..c22a39ca
--- /dev/null
+++ b/recipes-containers/containerd/containerd/CVE-2026-53495.patch
@@ -0,0 +1,108 @@
+From dd4821d9a9995c3a1fd17ae82b3484357f5424d2 Mon Sep 17 00:00:00 2001
+From: XlabAI <xlabai@tencent.com>
+Date: Tue, 9 Jun 2026 12:17:23 +0800
+Subject: [PATCH 4/4] cri: cancel ExecSync IO drain on context cancellation
+
+When drain_exec_sync_io_timeout is 0 and a child process holds the
+pipe, drainExecSyncIO blocks forever even after the client disconnects.
+Add ctx.Done() to the select to allow cancellation, and use a deferred
+context for Process.Delete cleanup when the original context is already
+canceled.
+
+CVE: CVE-2026-53495
+Upstream-Status: Backport [https://github.com/containerd/containerd/commit/22ccf4314d1fe0834f8e28f10d37d5305ef9880c]
+
+Signed-off-by: Guannan Wang <wgnbuaa@gmail.com>
+(cherry picked from commit ff39a972369e2f12fae561a58d658bbf8f2bc318)
+Signed-off-by: Samuel Karp <samuelkarp@google.com>
+(cherry picked from commit 22ccf4314d1fe0834f8e28f10d37d5305ef9880c)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ internal/cri/server/container_execsync.go | 13 ++++++-
+ .../cri/server/container_execsync_test.go | 34 +++++++++++++++++++
+ 2 files changed, 46 insertions(+), 1 deletion(-)
+
+diff --git a/internal/cri/server/container_execsync.go b/internal/cri/server/container_execsync.go
+index d86b4f677..190cd92c7 100644
+--- a/internal/cri/server/container_execsync.go
++++ b/internal/cri/server/container_execsync.go
+@@ -311,19 +311,30 @@ func drainExecSyncIO(ctx context.Context, execProcess containerd.Process, drainE
+
+ select {
+ case <-timerCh:
++ case <-ctx.Done():
+ case <-attachDone:
+ log.G(ctx).Tracef("Stream pipe for exec process %q done", execProcess.ID())
+ return nil
+ }
+
+ log.G(ctx).Debugf("Exec process %q exits but the io is still held by other processes. Trying to delete exec process to release io", execProcess.ID())
+- _, err := execProcess.Delete(ctx, containerd.WithProcessKill)
++ deleteCtx := ctx
++ if ctx.Err() != nil {
++ var cancel context.CancelFunc
++ deleteCtx, cancel = util.DeferContext()
++ defer cancel()
++ }
++ _, err := execProcess.Delete(deleteCtx, containerd.WithProcessKill)
+ if err != nil {
+ if !errdefs.IsNotFound(err) {
+ return fmt.Errorf("failed to release exec io by deleting exec process %q: %w",
+ execProcess.ID(), err)
+ }
+ }
++ if err := ctx.Err(); err != nil {
++ return fmt.Errorf("failed to drain exec process %q io before context cancellation: %w",
++ execProcess.ID(), err)
++ }
+ return fmt.Errorf("failed to drain exec process %q io in %s because io is still held by other processes",
+ execProcess.ID(), drainExecIOTimeout)
+ }
+diff --git a/internal/cri/server/container_execsync_test.go b/internal/cri/server/container_execsync_test.go
+index 0ef5f1c1b..51467638f 100644
+--- a/internal/cri/server/container_execsync_test.go
++++ b/internal/cri/server/container_execsync_test.go
+@@ -83,6 +83,40 @@ func TestDrainExecSyncIO(t *testing.T) {
+ assert.Error(t, drainExecSyncIO(ctx, ep, 3*time.Second, attachDoneCh))
+ assert.Equal(t, []string{"Delete"}, ep.actionEvents)
+ })
++
++ t.Run("CanceledNoTimeout", func(t *testing.T) {
++ ctx, cancel := context.WithCancel(ctx)
++ cancel()
++
++ ep := &fakeExecProcess{
++ id: t.Name(),
++ pid: uint32(os.Getpid()),
++ }
++
++ attachDoneCh := make(chan struct{})
++ assert.ErrorIs(t, drainExecSyncIO(ctx, ep, 0, attachDoneCh), context.Canceled)
++ assert.Equal(t, []string{"Delete"}, ep.actionEvents)
++ })
++
++ t.Run("CancelWhileDraining", func(t *testing.T) {
++ ctx, cancel := context.WithCancel(ctx)
++
++ ep := &fakeExecProcess{
++ id: t.Name(),
++ pid: uint32(os.Getpid()),
++ }
++
++ attachDoneCh := make(chan struct{})
++ errCh := make(chan error, 1)
++ go func() {
++ errCh <- drainExecSyncIO(ctx, ep, 0, attachDoneCh)
++ }()
++
++ time.AfterFunc(100*time.Millisecond, cancel)
++
++ assert.ErrorIs(t, <-errCh, context.Canceled)
++ assert.Equal(t, []string{"Delete"}, ep.actionEvents)
++ })
+ }
+
+ type fakeExecProcess struct {
+--
+2.35.6
+
diff --git a/recipes-containers/containerd/containerd_git.bb b/recipes-containers/containerd/containerd_git.bb
index af756ba8..2b037110 100644
--- a/recipes-containers/containerd/containerd_git.bb
+++ b/recipes-containers/containerd/containerd_git.bb
@@ -16,6 +16,7 @@ SRC_URI = "git://github.com/containerd/containerd;branch=release/2.2;protocol=ht
file://CVE-2026-50195.patch \
file://CVE-2026-53492.patch \
file://CVE-2026-53489.patch \
+ file://CVE-2026-53495.patch \
"
# Apache-2.0 for containerd
--
2.35.6
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [meta-virtualization][wrynose][PATCH 5/5] containerd: Fix CVE-2026-53493
2026-10-16 10:06 [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (2 preceding siblings ...)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 4/5] containerd: Fix CVE-2026-53495 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-16 10:06 ` Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-22 19:09 ` [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Bruce Ashfield
4 siblings, 1 reply; 10+ messages in thread
From: Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-10-16 10:06 UTC (permalink / raw)
To: meta-virtualization
From: Deepak Rathore <deeratho@cisco.com>
This patch applies the upstream release/2.2 backport for
CVE-2026-53493. It bounds Dispatch concurrency and references, then
bounds Walk references during image pull processing to prevent
uncontrolled descriptor-graph traversal and resource exhaustion.
The individual upstream backport commits are referenced in [1] and [2].
The public CVE advisory is referenced in [3].
[1] https://github.com/containerd/containerd/commit/efd11fc9c2935f1a7ce6626b348d1461bac34acf
[2] https://github.com/containerd/containerd/commit/053c0cc412f2a36c77a973d0b62ff4fa4815a9bb
[3] https://github.com/containerd/containerd/security/advisories/GHSA-pg57-6jwg-q645
Signed-off-by: Deepak Rathore <deeratho@cisco.com>
---
.../containerd/CVE-2026-53493_p1.patch | 646 ++++++++++++++++++
.../containerd/CVE-2026-53493_p2.patch | 329 +++++++++
.../containerd/containerd_git.bb | 2 +
3 files changed, 977 insertions(+)
create mode 100644 recipes-containers/containerd/containerd/CVE-2026-53493_p1.patch
create mode 100644 recipes-containers/containerd/containerd/CVE-2026-53493_p2.patch
diff --git a/recipes-containers/containerd/containerd/CVE-2026-53493_p1.patch b/recipes-containers/containerd/containerd/CVE-2026-53493_p1.patch
new file mode 100644
index 00000000..b4fd6ded
--- /dev/null
+++ b/recipes-containers/containerd/containerd/CVE-2026-53493_p1.patch
@@ -0,0 +1,646 @@
+From 95d5ef2d8204a639c8f452d8457b82406802a540 Mon Sep 17 00:00:00 2001
+From: Chris Henzie <chrishenzie@gmail.com>
+Date: Tue, 28 Jul 2026 14:09:57 -0700
+Subject: [PATCH 1/2] Bound Dispatch concurrency and references
+
+Traverse breadth-first, one level at a time. Acquire the limiter before
+starting each handler goroutine. Default to 32 concurrent handlers when
+no limiter is provided. Children wait until every handler in the current
+level finishes.
+
+Preserve duplicate visits and return an error wrapping
+ErrResourceExhausted when the traversal exceeds 10,000 references.
+Count children as handlers return them to bound the descriptors retained
+for the next level.
+
+CVE: CVE-2026-53493
+Upstream-Status: Backport [https://github.com/containerd/containerd/commit/efd11fc9c2935f1a7ce6626b348d1461bac34acf]
+
+Assisted-by: Claude Code
+Signed-off-by: Chris Henzie <chrishenzie@gmail.com>
+(cherry picked from commit 94e83c14c8aac963e24e28105dab9c9af812a2a8)
+Signed-off-by: Chris Henzie <chrishenzie@gmail.com>
+(cherry picked from commit efd11fc9c2935f1a7ce6626b348d1461bac34acf)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ core/images/handlers.go | 101 ++++++--
+ core/images/handlers_test.go | 469 +++++++++++++++++++++++++++++++++++
+ 2 files changed, 543 insertions(+), 27 deletions(-)
+ create mode 100644 core/images/handlers_test.go
+
+diff --git a/core/images/handlers.go b/core/images/handlers.go
+index 245b9ac43..a60835f97 100644
+--- a/core/images/handlers.go
++++ b/core/images/handlers.go
+@@ -23,6 +23,7 @@ import (
+ "slices"
+ "sort"
+ "strings"
++ "sync"
+
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/errdefs"
+@@ -32,6 +33,16 @@ import (
+ "golang.org/x/sync/semaphore"
+ )
+
++const (
++ // defaultMaxConcurrency bounds the number of concurrent handler
++ // goroutines when Dispatch is called with a nil limiter.
++ defaultMaxConcurrency = 32
++
++ // maxReferences caps the references a single Dispatch admits, duplicates
++ // included.
++ maxReferences = 10_000
++)
++
+ var (
+ // ErrSkipDesc is used to skip processing of a descriptor and
+ // its descendants.
+@@ -146,45 +157,81 @@ func WalkNotEmpty(ctx context.Context, handler Handler, descs ...ocispec.Descrip
+ // handler may return `ErrSkipDesc` to signal to the dispatcher to not traverse
+ // any children.
+ //
+-// A concurrency limiter can be passed in to limit the number of concurrent
+-// handlers running. When limiter is nil, there is no limit.
++// The limiter bounds concurrent handlers. When limiter is nil,
++// the limit is 32.
+ //
+ // Typically, this function will be used with `FetchHandler`, often composed
+ // with other handlers.
+ //
+ // If any handler returns an error, the dispatch session will be canceled.
++//
++// Each call is limited to 10,000 references, including duplicates.
++// Exceeding this limit returns an error wrapping
++// [errdefs.ErrResourceExhausted].
+ func Dispatch(ctx context.Context, handler Handler, limiter *semaphore.Weighted, descs ...ocispec.Descriptor) error {
+- eg, ctx2 := errgroup.WithContext(ctx)
+- for _, desc := range descs {
+- if limiter != nil {
+- if err := limiter.Acquire(ctx, 1); err != nil {
+- return err
+- }
++ if len(descs) == 0 {
++ return nil
++ }
++ if err := ctx.Err(); err != nil {
++ return err
++ }
++
++ if limiter == nil {
++ limiter = semaphore.NewWeighted(defaultMaxConcurrency)
++ }
++
++ var (
++ mu sync.Mutex
++ next []ocispec.Descriptor
++ referenceCount int
++ )
++
++ admit := func(descs []ocispec.Descriptor) error {
++ mu.Lock()
++ defer mu.Unlock()
++ if referenceCount+len(descs) > maxReferences {
++ return fmt.Errorf("too many descriptors (limit %d): %w", maxReferences, errdefs.ErrResourceExhausted)
+ }
++ referenceCount += len(descs)
++ next = append(next, descs...)
++ return nil
++ }
++
++ if err := admit(descs); err != nil {
++ return err
++ }
+
+- eg.Go(func() error {
+- desc := desc
++ for len(next) > 0 {
++ level := next
++ next = nil
+
+- children, err := handler.Handle(ctx2, desc)
+- if limiter != nil {
+- limiter.Release(1)
++ eg, egCtx := errgroup.WithContext(ctx)
++ for _, desc := range level {
++ // Acquire here to bound the number of handler goroutines.
++ if err := limiter.Acquire(egCtx, 1); err != nil {
++ break
+ }
+- if err != nil {
+- if errors.Is(err, ErrSkipDesc) {
+- return nil // don't traverse the children.
++ eg.Go(func() error {
++ defer limiter.Release(1)
++ children, err := handler.Handle(egCtx, desc)
++ if err != nil {
++ if errors.Is(err, ErrSkipDesc) {
++ return nil // don't traverse the children.
++ }
++ return err
+ }
+- return err
+- }
+-
+- if len(children) > 0 {
+- return Dispatch(ctx2, handler, limiter, children...)
+- }
+-
+- return nil
+- })
++ return admit(children)
++ })
++ }
++ if err := eg.Wait(); err != nil {
++ return err
++ }
++ // Acquire can fail on cancellation even if every handler returns nil.
++ if err := ctx.Err(); err != nil {
++ return err
++ }
+ }
+-
+- return eg.Wait()
++ return nil
+ }
+
+ // ChildrenHandler decodes well-known manifest types and returns their children.
+diff --git a/core/images/handlers_test.go b/core/images/handlers_test.go
+new file mode 100644
+index 000000000..5c4e54e86
+--- /dev/null
++++ b/core/images/handlers_test.go
+@@ -0,0 +1,469 @@
++/*
++ 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 images
++
++import (
++ "context"
++ "errors"
++ "fmt"
++ "sync"
++ "sync/atomic"
++ "testing"
++ "time"
++
++ "github.com/containerd/errdefs"
++ digest "github.com/opencontainers/go-digest"
++ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
++ "github.com/stretchr/testify/assert"
++ "github.com/stretchr/testify/require"
++ "golang.org/x/sync/errgroup"
++ "golang.org/x/sync/semaphore"
++)
++
++func newDesc(id string) ocispec.Descriptor {
++ return ocispec.Descriptor{
++ Digest: digest.FromString(id),
++ MediaType: ocispec.MediaTypeImageLayer,
++ Size: 1,
++ }
++}
++
++func digestSet(digests []digest.Digest) map[digest.Digest]struct{} {
++ set := make(map[digest.Digest]struct{}, len(digests))
++ for _, d := range digests {
++ set[d] = struct{}{}
++ }
++ return set
++}
++
++func TestDispatchTreeTraversal(t *testing.T) {
++ root := newDesc("root")
++ child1 := newDesc("child1")
++ child2 := newDesc("child2")
++ grandchild1 := newDesc("grandchild1")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {child1, child2},
++ child1.Digest: {grandchild1},
++ }
++
++ var visited []digest.Digest
++ var mu sync.Mutex
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ mu.Lock()
++ visited = append(visited, desc.Digest)
++ mu.Unlock()
++ return graph[desc.Digest], nil
++ })
++
++ err := Dispatch(context.Background(), handler, nil, root)
++ require.NoError(t, err)
++
++ set := digestSet(visited)
++ assert.Contains(t, set, root.Digest)
++ assert.Contains(t, set, child1.Digest)
++ assert.Contains(t, set, child2.Digest)
++ assert.Contains(t, set, grandchild1.Digest)
++ assert.Len(t, visited, 4)
++}
++
++func TestDispatchRepeatedReferences(t *testing.T) {
++ root := newDesc("root")
++ childA := newDesc("A")
++ childB := newDesc("B")
++ shared := newDesc("shared")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {childA, childB},
++ childA.Digest: {shared},
++ childB.Digest: {shared},
++ }
++
++ tests := []struct {
++ name string
++ dispatch func(context.Context, Handler, *semaphore.Weighted, ...ocispec.Descriptor) error
++ wantTotalCalls int32
++ wantSharedCalls int32
++ }{
++ {
++ name: "Dispatch visits every reference",
++ dispatch: Dispatch,
++ wantTotalCalls: 5,
++ wantSharedCalls: 2,
++ },
++ }
++
++ for _, tt := range tests {
++ t.Run(tt.name, func(t *testing.T) {
++ var totalCalls atomic.Int32
++ var sharedCalls atomic.Int32
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ totalCalls.Add(1)
++ if desc.Digest == shared.Digest {
++ sharedCalls.Add(1)
++ }
++ return graph[desc.Digest], nil
++ })
++
++ err := tt.dispatch(context.Background(), handler, nil, root)
++ require.NoError(t, err)
++ assert.Equal(t, tt.wantTotalCalls, totalCalls.Load())
++ assert.Equal(t, tt.wantSharedCalls, sharedCalls.Load())
++ })
++ }
++}
++
++func TestDispatchErrSkipDesc(t *testing.T) {
++ root := newDesc("root")
++ child1 := newDesc("child1")
++ child2 := newDesc("child2")
++ grandchild := newDesc("grandchild")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {child1, child2},
++ child1.Digest: {grandchild},
++ }
++
++ var visited []digest.Digest
++ var mu sync.Mutex
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ mu.Lock()
++ visited = append(visited, desc.Digest)
++ mu.Unlock()
++ if desc.Digest == child1.Digest {
++ return graph[desc.Digest], ErrSkipDesc
++ }
++ return graph[desc.Digest], nil
++ })
++
++ err := Dispatch(context.Background(), handler, nil, root)
++ require.NoError(t, err)
++
++ set := digestSet(visited)
++ assert.Contains(t, set, root.Digest)
++ assert.Contains(t, set, child1.Digest)
++ assert.Contains(t, set, child2.Digest)
++ assert.NotContains(t, set, grandchild.Digest, "grandchild must not be visited when its parent returns ErrSkipDesc")
++}
++
++func TestDispatchConcurrencyLimit(t *testing.T) {
++ tests := []struct {
++ name string
++ limiter *semaphore.Weighted
++ maxConcurrency int32
++ numChildren int
++ }{
++ {
++ name: "With explicit limiter",
++ limiter: semaphore.NewWeighted(3),
++ maxConcurrency: 3,
++ numChildren: 3 * 5,
++ },
++ {
++ name: "With default limiter",
++ limiter: nil,
++ maxConcurrency: defaultMaxConcurrency,
++ numChildren: defaultMaxConcurrency * 5,
++ },
++ }
++
++ for _, tt := range tests {
++ t.Run(tt.name, func(t *testing.T) {
++ root := newDesc("root")
++ children := make([]ocispec.Descriptor, tt.numChildren)
++ for i := range tt.numChildren {
++ children[i] = newDesc(fmt.Sprintf("child-%d", i))
++ }
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: children,
++ }
++
++ var concurrent, peak atomic.Int32
++ var exceeded atomic.Bool
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ n := concurrent.Add(1)
++ if n > tt.maxConcurrency {
++ exceeded.Store(true)
++ }
++ for {
++ old := peak.Load()
++ if n <= old || peak.CompareAndSwap(old, n) {
++ break
++ }
++ }
++ defer concurrent.Add(-1)
++
++ // Keep handlers running long enough to overlap.
++ time.Sleep(10 * time.Millisecond)
++ return graph[desc.Digest], nil
++ })
++
++ err := Dispatch(context.Background(), handler, tt.limiter, root)
++ require.NoError(t, err)
++
++ assert.False(t, exceeded.Load(), "concurrent handlers must not exceed capacity")
++ assert.Equal(t, tt.maxConcurrency, peak.Load(),
++ "concurrent handlers must reach the capacity the caller asked for")
++ })
++ }
++}
++
++func TestDispatchSharedLimiter(t *testing.T) {
++ const (
++ weight = 4
++ callers = 4
++ perTree = 12
++ )
++
++ limiter := semaphore.NewWeighted(weight)
++
++ var concurrent atomic.Int32
++ var exceeded atomic.Bool
++ visited := make([]atomic.Int32, callers)
++
++ var eg errgroup.Group
++ for c := range callers {
++ eg.Go(func() error {
++ root := newDesc(fmt.Sprintf("root-%d", c))
++ children := make([]ocispec.Descriptor, perTree)
++ for i := range perTree {
++ children[i] = newDesc(fmt.Sprintf("caller-%d-child-%d", c, i))
++ }
++ graph := map[digest.Digest][]ocispec.Descriptor{root.Digest: children}
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if concurrent.Add(1) > weight {
++ exceeded.Store(true)
++ }
++ defer concurrent.Add(-1)
++ visited[c].Add(1)
++
++ time.Sleep(2 * time.Millisecond)
++ return graph[desc.Digest], nil
++ })
++
++ return Dispatch(context.Background(), handler, limiter, root)
++ })
++ }
++
++ done := make(chan error, 1)
++ go func() { done <- eg.Wait() }()
++
++ select {
++ case err := <-done:
++ require.NoError(t, err)
++ case <-time.After(30 * time.Second):
++ t.Fatal("shared-limiter traversals did not complete; a caller is starved or deadlocked")
++ }
++
++ assert.False(t, exceeded.Load(), "combined concurrent handlers must not exceed the shared limiter weight")
++ for c := range callers {
++ assert.Equal(t, int32(perTree+1), visited[c].Load(),
++ "caller %d must visit every descriptor in its own graph", c)
++ }
++}
++
++func TestDispatchReferenceLimit(t *testing.T) {
++ // Each root is below the cap on its own; their combined references exceed it.
++ rootA := newDesc("root-a")
++ rootB := newDesc("root-b")
++
++ childrenA := make([]ocispec.Descriptor, maxReferences/2)
++ childrenB := make([]ocispec.Descriptor, maxReferences/2)
++ for i := range childrenA {
++ childrenA[i] = newDesc(fmt.Sprintf("a-%d", i))
++ childrenB[i] = newDesc(fmt.Sprintf("b-%d", i))
++ }
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ switch desc.Digest {
++ case rootA.Digest:
++ return childrenA, nil
++ case rootB.Digest:
++ return childrenB, nil
++ }
++ return nil, nil
++ })
++
++ require.ErrorIs(t, Dispatch(context.Background(), handler, nil, rootA, rootB), errdefs.ErrResourceExhausted)
++
++ acceptRoot := newDesc("accept-root")
++ exact := make([]ocispec.Descriptor, maxReferences-1)
++ for i := range exact {
++ exact[i] = newDesc(fmt.Sprintf("accept-%d", i))
++ }
++ acceptHandler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == acceptRoot.Digest {
++ return exact, nil
++ }
++ return nil, nil
++ })
++ require.NoError(t, Dispatch(context.Background(), acceptHandler, nil, acceptRoot))
++}
++
++func TestDispatchRejectsChildrenBeforeVisiting(t *testing.T) {
++ root := newDesc("root")
++ children := make([]ocispec.Descriptor, maxReferences)
++ for i := range children {
++ children[i] = newDesc(fmt.Sprintf("node-%d", i))
++ }
++ var childVisits atomic.Int32
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == root.Digest {
++ return children, nil
++ }
++ childVisits.Add(1)
++ return nil, nil
++ })
++
++ require.ErrorIs(t, Dispatch(context.Background(), handler, nil, root), errdefs.ErrResourceExhausted)
++ assert.Equal(t, int32(0), childVisits.Load(), "children of an over-limit slice must not be visited")
++}
++
++func TestDispatchDuplicateRoots(t *testing.T) {
++ root := newDesc("root")
++
++ tests := []struct {
++ name string
++ dispatch func(context.Context, Handler, *semaphore.Weighted, ...ocispec.Descriptor) error
++ wantCalls int32
++ }{
++ {"Dispatch visits each root", Dispatch, 2},
++ }
++
++ for _, tt := range tests {
++ t.Run(tt.name, func(t *testing.T) {
++ var calls atomic.Int32
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ calls.Add(1)
++ return nil, nil
++ })
++ require.NoError(t, tt.dispatch(context.Background(), handler, nil, root, root))
++ assert.Equal(t, tt.wantCalls, calls.Load())
++ })
++ }
++}
++
++func TestDispatchCountsDuplicateReferences(t *testing.T) {
++ root := newDesc("root")
++ dup := newDesc("dup")
++ children := make([]ocispec.Descriptor, maxReferences+1)
++ for i := range children {
++ children[i] = dup
++ }
++ fanout := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == root.Digest {
++ return children, nil
++ }
++ return nil, nil
++ })
++ require.ErrorIs(t, Dispatch(context.Background(), fanout, nil, root), errdefs.ErrResourceExhausted)
++
++ a, b := newDesc("a"), newDesc("b")
++ cycle := map[digest.Digest][]ocispec.Descriptor{a.Digest: {b}, b.Digest: {a}}
++ cyclic := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ return cycle[desc.Digest], nil
++ })
++ require.ErrorIs(t, Dispatch(context.Background(), cyclic, nil, a), errdefs.ErrResourceExhausted)
++}
++
++func TestDispatchErrorPropagation(t *testing.T) {
++ root := newDesc("root")
++ child1 := newDesc("child1")
++ child2 := newDesc("child2")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {child1, child2},
++ }
++
++ errTest := errors.New("handler error")
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == child1.Digest {
++ return nil, errTest
++ }
++ return graph[desc.Digest], nil
++ })
++
++ err := Dispatch(context.Background(), handler, nil, root)
++ require.ErrorIs(t, err, errTest)
++}
++
++func TestDispatchContextCancellation(t *testing.T) {
++ ctx, cancel := context.WithCancel(context.Background())
++ cancel()
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ return nil, nil
++ })
++
++ err := Dispatch(ctx, handler, nil, newDesc("root"))
++ require.ErrorIs(t, err, context.Canceled)
++}
++
++func TestDispatchContextCancellationMidFlight(t *testing.T) {
++ // Use more descriptors than the limiter admits so cancellation also covers
++ // handlers waiting to start.
++ const n = defaultMaxConcurrency * 4
++ descs := make([]ocispec.Descriptor, n)
++ for i := range n {
++ descs[i] = newDesc(fmt.Sprintf("cancel-%d", i))
++ }
++
++ ctx, cancel := context.WithCancel(context.Background())
++ defer cancel()
++
++ started := make(chan struct{}, n)
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ started <- struct{}{}
++ <-ctx.Done()
++ return nil, ctx.Err()
++ })
++
++ go func() {
++ for range defaultMaxConcurrency {
++ <-started
++ }
++ cancel()
++ }()
++
++ err := Dispatch(ctx, handler, nil, descs...)
++ require.ErrorIs(t, err, context.Canceled)
++}
++
++func TestDispatchCancellationWithNoHandlerError(t *testing.T) {
++ const n = defaultMaxConcurrency * 4
++ descs := make([]ocispec.Descriptor, n)
++ for i := range n {
++ descs[i] = newDesc(fmt.Sprintf("cancel-queued-%d", i))
++ }
++
++ ctx, cancel := context.WithCancel(context.Background())
++ defer cancel()
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == descs[0].Digest {
++ cancel()
++ }
++ return nil, nil
++ })
++
++ err := Dispatch(ctx, handler, nil, descs...)
++ require.ErrorIs(t, err, context.Canceled)
++}
+--
+2.53.0
+
diff --git a/recipes-containers/containerd/containerd/CVE-2026-53493_p2.patch b/recipes-containers/containerd/containerd/CVE-2026-53493_p2.patch
new file mode 100644
index 00000000..d76cfb2e
--- /dev/null
+++ b/recipes-containers/containerd/containerd/CVE-2026-53493_p2.patch
@@ -0,0 +1,329 @@
+From 77f0eab284656ca3b609ad347a83bb6adb9d6833 Mon Sep 17 00:00:00 2001
+From: Chris Henzie <chrishenzie@gmail.com>
+Date: Tue, 28 Jul 2026 14:18:32 -0700
+Subject: [PATCH 2/2] Bound Walk references
+
+Return an error wrapping ErrResourceExhausted when the walk exceeds
+10,000 references. Preserve duplicate visits and count every reference
+toward the limit across recursive calls and roots.
+
+CVE: CVE-2026-53493
+Upstream-Status: Backport [https://github.com/containerd/containerd/commit/053c0cc412f2a36c77a973d0b62ff4fa4815a9bb]
+
+Assisted-by: Claude Code
+Signed-off-by: Chris Henzie <chrishenzie@gmail.com>
+(cherry picked from commit 4f5f32636d47f051751065cf824a10da70c619fe)
+Signed-off-by: Chris Henzie <chrishenzie@gmail.com>
+(cherry picked from commit 053c0cc412f2a36c77a973d0b62ff4fa4815a9bb)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ core/images/handlers.go | 25 +++-
+ core/images/handlers_test.go | 240 +++++++++++++++++++++++++++++++++++
+ 2 files changed, 261 insertions(+), 4 deletions(-)
+
+diff --git a/core/images/handlers.go b/core/images/handlers.go
+index a60835f97..7d1ec35cb 100644
+--- a/core/images/handlers.go
++++ b/core/images/handlers.go
+@@ -38,8 +38,8 @@ const (
+ // goroutines when Dispatch is called with a nil limiter.
+ defaultMaxConcurrency = 32
+
+- // maxReferences caps the references a single Dispatch admits, duplicates
+- // included.
++ // maxReferences caps the references in a single Dispatch or Walk.
++ // Duplicate references count toward this limit.
+ maxReferences = 10_000
+ )
+
+@@ -99,9 +99,26 @@ func Handlers(handlers ...Handler) HandlerFunc {
+ //
+ // This differs from dispatch in that each sibling resource is considered
+ // synchronously.
++//
++// Each call is limited to 10,000 references, including duplicates.
++// Exceeding this limit returns an error wrapping
++// [errdefs.ErrResourceExhausted].
+ func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
+- for _, desc := range descs {
++ w := &walker{}
++ return w.walk(ctx, handler, descs...)
++}
+
++type walker struct {
++ referenceCount int
++}
++
++func (w *walker) walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
++ if w.referenceCount+len(descs) > maxReferences {
++ return fmt.Errorf("too many descriptors (limit %d): %w", maxReferences, errdefs.ErrResourceExhausted)
++ }
++ w.referenceCount += len(descs)
++
++ for _, desc := range descs {
+ children, err := handler.Handle(ctx, desc)
+ if err != nil {
+ if errors.Is(err, ErrSkipDesc) {
+@@ -111,7 +128,7 @@ func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) err
+ }
+
+ if len(children) > 0 {
+- if err := Walk(ctx, handler, children...); err != nil {
++ if err := w.walk(ctx, handler, children...); err != nil {
+ return err
+ }
+ }
+diff --git a/core/images/handlers_test.go b/core/images/handlers_test.go
+index 5c4e54e86..acad816d7 100644
+--- a/core/images/handlers_test.go
++++ b/core/images/handlers_test.go
+@@ -50,6 +50,246 @@ func digestSet(digests []digest.Digest) map[digest.Digest]struct{} {
+ return set
+ }
+
++func TestWalkTreeTraversal(t *testing.T) {
++ root := newDesc("root")
++ child1 := newDesc("child1")
++ child2 := newDesc("child2")
++ grandchild1 := newDesc("grandchild1")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {child1, child2},
++ child1.Digest: {grandchild1},
++ }
++
++ var visited []digest.Digest
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ visited = append(visited, desc.Digest)
++ return graph[desc.Digest], nil
++ })
++
++ err := Walk(context.Background(), handler, root)
++ require.NoError(t, err)
++
++ want := []digest.Digest{root.Digest, child1.Digest, grandchild1.Digest, child2.Digest}
++ assert.Equal(t, want, visited)
++}
++
++func TestWalkRepeatedReferences(t *testing.T) {
++ root := newDesc("root")
++ childA := newDesc("A")
++ childB := newDesc("B")
++ shared := newDesc("shared")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {childA, childB},
++ childA.Digest: {shared},
++ childB.Digest: {shared},
++ }
++
++ tests := []struct {
++ name string
++ walk func(context.Context, Handler, ...ocispec.Descriptor) error
++ wantTotalCalls int
++ wantSharedCalls int
++ }{
++ {
++ name: "Walk visits every reference",
++ walk: Walk,
++ wantTotalCalls: 5,
++ wantSharedCalls: 2,
++ },
++ }
++
++ for _, tt := range tests {
++ t.Run(tt.name, func(t *testing.T) {
++ var totalCalls, sharedCalls int
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ totalCalls++
++ if desc.Digest == shared.Digest {
++ sharedCalls++
++ }
++ return graph[desc.Digest], nil
++ })
++
++ err := tt.walk(context.Background(), handler, root)
++ require.NoError(t, err)
++ assert.Equal(t, tt.wantTotalCalls, totalCalls)
++ assert.Equal(t, tt.wantSharedCalls, sharedCalls)
++ })
++ }
++}
++
++func TestWalkErrSkipDesc(t *testing.T) {
++ root := newDesc("root")
++ child1 := newDesc("child1")
++ child2 := newDesc("child2")
++ grandchild := newDesc("grandchild")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {child1, child2},
++ child1.Digest: {grandchild},
++ }
++
++ var visited []digest.Digest
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ visited = append(visited, desc.Digest)
++ if desc.Digest == child1.Digest {
++ return graph[desc.Digest], ErrSkipDesc
++ }
++ return graph[desc.Digest], nil
++ })
++
++ err := Walk(context.Background(), handler, root)
++ require.NoError(t, err)
++
++ set := digestSet(visited)
++ assert.Contains(t, set, root.Digest)
++ assert.Contains(t, set, child1.Digest)
++ assert.Contains(t, set, child2.Digest)
++ assert.NotContains(t, set, grandchild.Digest, "grandchild must not be visited when its parent returns ErrSkipDesc")
++}
++
++func TestWalkReferenceLimit(t *testing.T) {
++ root := newDesc("root")
++ handlerReturning := func(childCount int) Handler {
++ children := make([]ocispec.Descriptor, childCount)
++ for i := range children {
++ children[i] = newDesc(fmt.Sprintf("node-%d", i))
++ }
++ return HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == root.Digest {
++ return children, nil
++ }
++ return nil, nil
++ })
++ }
++
++ require.NoError(t, Walk(context.Background(), handlerReturning(maxReferences-1), root))
++ require.ErrorIs(t, Walk(context.Background(), handlerReturning(maxReferences), root), errdefs.ErrResourceExhausted)
++}
++
++func TestWalkRejectsChildrenBeforeVisiting(t *testing.T) {
++ root := newDesc("root")
++ children := make([]ocispec.Descriptor, maxReferences)
++ for i := range children {
++ children[i] = newDesc(fmt.Sprintf("node-%d", i))
++ }
++ var childVisits int
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == root.Digest {
++ return children, nil
++ }
++ childVisits++
++ return nil, nil
++ })
++
++ require.ErrorIs(t, Walk(context.Background(), handler, root), errdefs.ErrResourceExhausted)
++ assert.Equal(t, 0, childVisits, "children of an over-limit slice must not be visited")
++}
++
++func TestWalkErrorPropagation(t *testing.T) {
++ root := newDesc("root")
++ child1 := newDesc("child1")
++ child2 := newDesc("child2")
++
++ graph := map[digest.Digest][]ocispec.Descriptor{
++ root.Digest: {child1, child2},
++ }
++
++ errTest := errors.New("handler error")
++
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == child1.Digest {
++ return nil, errTest
++ }
++ return graph[desc.Digest], nil
++ })
++
++ err := Walk(context.Background(), handler, root)
++ require.ErrorIs(t, err, errTest)
++}
++
++func TestWalkDuplicateRoots(t *testing.T) {
++ root := newDesc("root")
++
++ tests := []struct {
++ name string
++ walk func(context.Context, Handler, ...ocispec.Descriptor) error
++ wantCalls int
++ }{
++ {"Walk visits each root", Walk, 2},
++ }
++
++ for _, tt := range tests {
++ t.Run(tt.name, func(t *testing.T) {
++ var calls int
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ calls++
++ return nil, nil
++ })
++ require.NoError(t, tt.walk(context.Background(), handler, root, root))
++ assert.Equal(t, tt.wantCalls, calls)
++ })
++ }
++}
++
++func TestWalkCountsDuplicateReferences(t *testing.T) {
++ root := newDesc("root")
++ dup := newDesc("dup")
++ children := make([]ocispec.Descriptor, maxReferences+1)
++ for i := range children {
++ children[i] = dup
++ }
++ fanout := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == root.Digest {
++ return children, nil
++ }
++ return nil, nil
++ })
++ require.ErrorIs(t, Walk(context.Background(), fanout, root), errdefs.ErrResourceExhausted)
++
++ a, b := newDesc("a"), newDesc("b")
++ cycle := map[digest.Digest][]ocispec.Descriptor{a.Digest: {b}, b.Digest: {a}}
++ cyclic := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ return cycle[desc.Digest], nil
++ })
++ require.ErrorIs(t, Walk(context.Background(), cyclic, a), errdefs.ErrResourceExhausted)
++}
++
++func TestWalkNotEmpty(t *testing.T) {
++ root := newDesc("root")
++ child := newDesc("child")
++
++ t.Run("returns nil when a handler produces children", func(t *testing.T) {
++ graph := map[digest.Digest][]ocispec.Descriptor{root.Digest: {child}}
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ return graph[desc.Digest], nil
++ })
++ require.NoError(t, WalkNotEmpty(context.Background(), handler, root))
++ })
++
++ t.Run("returns ErrEmptyWalk when everything is filtered out", func(t *testing.T) {
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ return nil, nil
++ })
++ require.ErrorIs(t, WalkNotEmpty(context.Background(), handler, root), ErrEmptyWalk)
++ })
++
++ t.Run("propagates the reference-cap error", func(t *testing.T) {
++ children := make([]ocispec.Descriptor, maxReferences+1)
++ for i := range children {
++ children[i] = newDesc(fmt.Sprintf("node-%d", i))
++ }
++ handler := HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
++ if desc.Digest == root.Digest {
++ return children, nil
++ }
++ return nil, nil
++ })
++ require.ErrorIs(t, WalkNotEmpty(context.Background(), handler, root), errdefs.ErrResourceExhausted)
++ })
++}
++
+ func TestDispatchTreeTraversal(t *testing.T) {
+ root := newDesc("root")
+ child1 := newDesc("child1")
+--
+2.53.0
+
diff --git a/recipes-containers/containerd/containerd_git.bb b/recipes-containers/containerd/containerd_git.bb
index 2b037110..48d41dbc 100644
--- a/recipes-containers/containerd/containerd_git.bb
+++ b/recipes-containers/containerd/containerd_git.bb
@@ -17,6 +17,8 @@ SRC_URI = "git://github.com/containerd/containerd;branch=release/2.2;protocol=ht
file://CVE-2026-53492.patch \
file://CVE-2026-53489.patch \
file://CVE-2026-53495.patch \
+ file://CVE-2026-53493_p1.patch \
+ file://CVE-2026-53493_p2.patch \
"
# Apache-2.0 for containerd
--
2.35.6
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195
2026-10-16 10:06 [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (3 preceding siblings ...)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 5/5] containerd: Fix CVE-2026-53493 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-22 19:09 ` Bruce Ashfield
4 siblings, 0 replies; 10+ messages in thread
From: Bruce Ashfield @ 2026-10-22 19:09 UTC (permalink / raw)
To: meta-virtualization; +Cc: deeratho
merged
Bruce
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-22 19:09 ` Bruce Ashfield
0 siblings, 0 replies; 10+ messages in thread
From: Bruce Ashfield @ 2026-10-22 19:09 UTC (permalink / raw)
To: meta-virtualization; +Cc: deeratho
merged
Bruce
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [meta-virtualization][wrynose][PATCH 3/5] containerd: Fix CVE-2026-53489
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 3/5] containerd: Fix CVE-2026-53489 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-22 19:09 ` Bruce Ashfield
0 siblings, 0 replies; 10+ messages in thread
From: Bruce Ashfield @ 2026-10-22 19:09 UTC (permalink / raw)
To: meta-virtualization; +Cc: deeratho
merged
Bruce
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [meta-virtualization][wrynose][PATCH 4/5] containerd: Fix CVE-2026-53495
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 4/5] containerd: Fix CVE-2026-53495 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-22 19:09 ` Bruce Ashfield
0 siblings, 0 replies; 10+ messages in thread
From: Bruce Ashfield @ 2026-10-22 19:09 UTC (permalink / raw)
To: meta-virtualization; +Cc: deeratho
merged
Bruce
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [meta-virtualization][wrynose][PATCH 5/5] containerd: Fix CVE-2026-53493
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 5/5] containerd: Fix CVE-2026-53493 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-10-22 19:09 ` Bruce Ashfield
0 siblings, 0 replies; 10+ messages in thread
From: Bruce Ashfield @ 2026-10-22 19:09 UTC (permalink / raw)
To: meta-virtualization; +Cc: deeratho
merged
Bruce
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-10-22 19:09 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16 10:06 [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 2/5] containerd: Fix CVE-2026-53492 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 3/5] containerd: Fix CVE-2026-53489 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 4/5] containerd: Fix CVE-2026-53495 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-16 10:06 ` [meta-virtualization][wrynose][PATCH 5/5] containerd: Fix CVE-2026-53493 Deepak Rathore -X (deeratho - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-10-22 19:09 ` Bruce Ashfield
2026-10-22 19:09 ` [meta-virtualization][wrynose][PATCH 1/5] containerd: Fix CVE-2026-50195 Bruce Ashfield
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.