diff --git a/SPECS/moby-containerd-cc/CVE-2026-53488.patch b/SPECS/moby-containerd-cc/CVE-2026-53488.patch new file mode 100644 index 00000000000..cf989b447dc --- /dev/null +++ b/SPECS/moby-containerd-cc/CVE-2026-53488.patch @@ -0,0 +1,467 @@ +From 4624acdb771a703d07bb74f9a4e0e4fbbd00782e Mon Sep 17 00:00:00 2001 +From: Ben Cressey +Date: Sat, 6 Jun 2026 01:31:40 +0900 +Subject: [PATCH] Do not propagate reserved labels from image configs + +Image config labels are copied onto the container by both the CRI +plugin (BuildLabels) and the client's WithImageConfigLabels option +used by `ctr run`. Labels in the containerd.io/* namespace are +interpreted by containerd itself and labels in the io.cri-containerd* +namespace are interpreted by the CRI plugin. An image config is not a +trusted source for labels in either namespace. + +Skip labels in both reserved namespaces when copying labels from an +image config to a container, and warn about each label skipped: an +image that tries to set them may be attempting to alter containerd +behavior. Oversized image labels are already skipped this way by +the CRI plugin. + +Labels set explicitly by clients, for example via `ctr run --label` +or in the CRI request, are unaffected. + +Verified with the CRI plugin and with `ctr run` against an image +whose config carries labels like these: the labels are no longer +present on the created container and a warning is logged for each. + +Assisted-by: Claude Code +Signed-off-by: Ben Cressey +Signed-off-by: Samuel Karp +(cherry picked from commit 0ec1af4cae1256d18719ca892bf66340499e8050) +Signed-off-by: Akihiro Suda +Signed-off-by: Samuel Karp +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/containerd/containerd/commit/4d8ba4d.patch + +--- + container_opts.go | 15 +++++ + container_opts_test.go | 75 +++++++++++++++++++++ + labels/labels.go | 12 ++++ + labels/validate.go | 9 +++ + labels/validate_test.go | 17 +++++ + pkg/cri/labels/labels.go | 6 +- + pkg/cri/sbserver/helpers.go | 12 +++- + pkg/cri/sbserver/helpers_test.go | 16 +++-- + pkg/cri/sbserver/podsandbox/helpers.go | 12 +++- + pkg/cri/sbserver/podsandbox/helpers_test.go | 16 +++-- + pkg/cri/server/helpers.go | 12 +++- + pkg/cri/server/helpers_test.go | 16 +++-- + 12 files changed, 202 insertions(+), 16 deletions(-) + create mode 100644 container_opts_test.go + +diff --git a/container_opts.go b/container_opts.go +index 4a93703..770acc1 100644 +--- a/container_opts.go ++++ b/container_opts.go +@@ -26,10 +26,12 @@ import ( + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" ++ "github.com/containerd/containerd/labels" + "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/protobuf" + "github.com/containerd/containerd/snapshots" ++ "github.com/containerd/log" + "github.com/containerd/typeurl/v2" + "github.com/opencontainers/image-spec/identity" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +@@ -114,6 +116,10 @@ func WithContainerLabels(labels map[string]string) NewContainerOpts { + // The existing labels are cleared as this is expected to be the first + // operation in setting up a container's labels. Use WithAdditionalContainerLabels + // to add/overwrite the existing image config labels. ++// ++// Image config labels in the namespaces reserved for containerd ++// (containerd.io/) and the CRI plugin (io.cri-containerd) are not copied ++// to the container. + func WithImageConfigLabels(image Image) NewContainerOpts { + return func(ctx context.Context, _ *Client, c *containers.Container) error { + ic, err := image.Config(ctx) +@@ -139,6 +145,15 @@ func WithImageConfigLabels(image Image) NewContainerOpts { + return fmt.Errorf("unknown image config media type %s", ic.MediaType) + } + c.Labels = config.Labels ++ // Labels in the containerd.io/* namespace are interpreted by containerd ++ // itself, and labels in the io.cri-containerd.* namespace are interpreted ++ // by the CRI plugin, so they are not copied from untrusted image configs. ++ for k := range c.Labels { ++ if labels.IsReserved(k) { ++ log.G(ctx).Warnf("skipping image label %q: the label namespace is reserved for containerd; possible malicious image attempting to alter containerd behavior", k) ++ delete(c.Labels, k) ++ } ++ } + return nil + } + } +diff --git a/container_opts_test.go b/container_opts_test.go +new file mode 100644 +index 0000000..8775ce5 +--- /dev/null ++++ b/container_opts_test.go +@@ -0,0 +1,75 @@ ++/* ++ 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 containerd ++ ++import ( ++ "context" ++ "encoding/json" ++ "testing" ++ ++ "github.com/containerd/containerd/containers" ++ "github.com/containerd/containerd/content" ++ "github.com/opencontainers/go-digest" ++ ocispec "github.com/opencontainers/image-spec/specs-go/v1" ++ "github.com/stretchr/testify/assert" ++ "github.com/stretchr/testify/require" ++) ++ ++// fakeImage implements the subset of Image used by WithImageConfigLabels: ++// Config returns a descriptor with the config blob inlined in Data, so the ++// content store is never consulted. ++type fakeImage struct { ++ Image ++ config ocispec.Descriptor ++} ++ ++func (i fakeImage) Config(context.Context) (ocispec.Descriptor, error) { ++ return i.config, nil ++} ++ ++func (i fakeImage) ContentStore() content.Store { ++ return nil ++} ++ ++func TestWithImageConfigLabels(t *testing.T) { ++ blob, err := json.Marshal(ocispec.Image{ ++ Config: ocispec.ImageConfig{ ++ Labels: map[string]string{ ++ "foo": "bar", ++ "containerd.io/restart.policy": "always", ++ "io.cri-containerd.kind": "sandbox", ++ }, ++ }, ++ }) ++ require.NoError(t, err) ++ ++ img := fakeImage{ ++ config: ocispec.Descriptor{ ++ MediaType: ocispec.MediaTypeImageConfig, ++ Digest: digest.FromBytes(blob), ++ Size: int64(len(blob)), ++ Data: blob, ++ }, ++ } ++ ++ var c containers.Container ++ require.NoError(t, WithImageConfigLabels(img)(context.Background(), nil, &c)) ++ ++ // labels in the namespaces reserved for containerd and the CRI plugin ++ // are not copied from the image config ++ assert.Equal(t, map[string]string{"foo": "bar"}, c.Labels) ++} +diff --git a/labels/labels.go b/labels/labels.go +index 0f9bab5..ba4c245 100644 +--- a/labels/labels.go ++++ b/labels/labels.go +@@ -16,6 +16,18 @@ + + package labels + ++// ReservedPrefix is the prefix of the label namespace reserved for labels ++// defined and consumed by containerd itself. Labels in this namespace must ++// not be copied from untrusted sources such as image config labels. Use ++// IsReserved to check for such labels. ++const ReservedPrefix = "containerd.io/" ++ ++// CRIContainerdPrefix is the prefix of the label namespace reserved for ++// labels defined and consumed by containerd's CRI plugin. Labels in this ++// namespace must not be copied from untrusted sources such as image config ++// labels. Use IsReserved to check for such labels. ++const CRIContainerdPrefix = "io.cri-containerd" ++ + // LabelUncompressed is added to compressed layer contents. + // The value is digest of the uncompressed content. + const LabelUncompressed = "containerd.io/uncompressed" +diff --git a/labels/validate.go b/labels/validate.go +index f83b5dd..18c2857 100644 +--- a/labels/validate.go ++++ b/labels/validate.go +@@ -18,6 +18,7 @@ package labels + + import ( + "fmt" ++ "strings" + + "github.com/containerd/containerd/errdefs" + ) +@@ -39,3 +40,11 @@ func Validate(k, v string) error { + } + return nil + } ++ ++// IsReserved returns true if the label key is in a namespace reserved for ++// containerd (ReservedPrefix) or its CRI plugin (CRIContainerdPrefix). ++// Reserved labels are interpreted by containerd and must not be copied from ++// untrusted sources such as image config labels. ++func IsReserved(k string) bool { ++ return strings.HasPrefix(k, ReservedPrefix) || strings.HasPrefix(k, CRIContainerdPrefix) ++} +diff --git a/labels/validate_test.go b/labels/validate_test.go +index 628f2fd..21fcd17 100644 +--- a/labels/validate_test.go ++++ b/labels/validate_test.go +@@ -53,6 +53,23 @@ func TestInvalidLabels(t *testing.T) { + } + } + ++func TestIsReserved(t *testing.T) { ++ for key, reserved := range map[string]bool{ ++ "containerd.io/": true, ++ "containerd.io/restart.status": true, ++ "containerd.io/gc.ref.content": true, ++ "io.cri-containerd": true, ++ "io.cri-containerd.kind": true, ++ "io.cri-containerd.image": true, ++ "io.cri-containerdfoo": true, ++ "containerd.io": false, ++ "io.containerd.something": false, ++ "com.example.app": false, ++ } { ++ assert.Equal(t, reserved, IsReserved(key), "IsReserved(%q)", key) ++ } ++} ++ + func TestLongKey(t *testing.T) { + key := strings.Repeat("s", keyMaxLen+1) + value := strings.Repeat("v", maxSize-len(key)) +diff --git a/pkg/cri/labels/labels.go b/pkg/cri/labels/labels.go +index 45cdb03..9e60db8 100644 +--- a/pkg/cri/labels/labels.go ++++ b/pkg/cri/labels/labels.go +@@ -16,9 +16,13 @@ + + package labels + ++import ( ++ clabels "github.com/containerd/containerd/labels" ++) ++ + const ( + // criContainerdPrefix is common prefix for cri-containerd +- criContainerdPrefix = "io.cri-containerd" ++ criContainerdPrefix = clabels.CRIContainerdPrefix + // ImageLabelKey is the label key indicating the image is managed by cri plugin. + ImageLabelKey = criContainerdPrefix + ".image" + // ImageLabelValue is the label value indicating the image is managed by cri plugin. +diff --git a/pkg/cri/sbserver/helpers.go b/pkg/cri/sbserver/helpers.go +index 7753a5a..d707245 100644 +--- a/pkg/cri/sbserver/helpers.go ++++ b/pkg/cri/sbserver/helpers.go +@@ -313,11 +313,21 @@ func filterLabel(k, v string) string { + return fmt.Sprintf("labels.%q==%q", k, v) + } + +-// buildLabel builds the labels from config to be passed to containerd ++// buildLabels builds the labels from config to be passed to containerd. ++// Image config labels in the namespaces reserved for containerd ++// (containerd.io/) and the CRI plugin (io.cri-containerd) are not copied ++// to the container. + func buildLabels(configLabels, imageConfigLabels map[string]string, containerType string) map[string]string { + labels := make(map[string]string) + + for k, v := range imageConfigLabels { ++ // Labels in the containerd.io/* namespace are interpreted by containerd ++ // itself, and labels in the io.cri-containerd.* namespace are interpreted ++ // by the CRI plugin, so they are not copied from untrusted image configs. ++ if clabels.IsReserved(k) { ++ logrus.Warnf("skipping image label %q: the label namespace is reserved for containerd; possible malicious image attempting to alter containerd behavior", k) ++ continue ++ } + if err := clabels.Validate(k, v); err == nil { + labels[k] = v + } else { +diff --git a/pkg/cri/sbserver/helpers_test.go b/pkg/cri/sbserver/helpers_test.go +index fdea610..7bc4bdd 100644 +--- a/pkg/cri/sbserver/helpers_test.go ++++ b/pkg/cri/sbserver/helpers_test.go +@@ -128,21 +128,29 @@ func TestGetRepoDigestAndTag(t *testing.T) { + + func TestBuildLabels(t *testing.T) { + imageConfigLabels := map[string]string{ +- "a": "z", +- "d": "y", +- "long-label": strings.Repeat("example", 10000), ++ "a": "z", ++ "d": "y", ++ "long-label": strings.Repeat("example", 10000), ++ "containerd.io/restart.policy": "always", ++ "io.cri-containerd.image": "managed", + } + configLabels := map[string]string{ + "a": "b", + "c": "d", ++ // reserved namespaces are only filtered for image config labels, not ++ // for labels from the CRI request ++ "containerd.io/restart.status": "stopped", + } + newLabels := buildLabels(configLabels, imageConfigLabels, containerKindSandbox) +- assert.Len(t, newLabels, 4) ++ assert.Len(t, newLabels, 5) + assert.Equal(t, "b", newLabels["a"]) + assert.Equal(t, "d", newLabels["c"]) + assert.Equal(t, "y", newLabels["d"]) ++ assert.Equal(t, "stopped", newLabels["containerd.io/restart.status"]) + assert.Equal(t, containerKindSandbox, newLabels[containerKindLabel]) + assert.NotContains(t, newLabels, "long-label") ++ assert.NotContains(t, newLabels, "containerd.io/restart.policy") ++ assert.NotContains(t, newLabels, "io.cri-containerd.image") + + newLabels["a"] = "e" + assert.Empty(t, configLabels[containerKindLabel], "should not add new labels into original label") +diff --git a/pkg/cri/sbserver/podsandbox/helpers.go b/pkg/cri/sbserver/podsandbox/helpers.go +index 1d13930..7ea56f7 100644 +--- a/pkg/cri/sbserver/podsandbox/helpers.go ++++ b/pkg/cri/sbserver/podsandbox/helpers.go +@@ -122,11 +122,21 @@ func getUserFromImage(user string) (*int64, string) { + return &uid, "" + } + +-// buildLabel builds the labels from config to be passed to containerd ++// buildLabels builds the labels from config to be passed to containerd. ++// Image config labels in the namespaces reserved for containerd ++// (containerd.io/) and the CRI plugin (io.cri-containerd) are not copied ++// to the container. + func buildLabels(configLabels, imageConfigLabels map[string]string, containerType string) map[string]string { + labels := make(map[string]string) + + for k, v := range imageConfigLabels { ++ // Labels in the containerd.io/* namespace are interpreted by containerd ++ // itself, and labels in the io.cri-containerd.* namespace are interpreted ++ // by the CRI plugin, so they are not copied from untrusted image configs. ++ if clabels.IsReserved(k) { ++ logrus.Warnf("skipping image label %q: the label namespace is reserved for containerd; possible malicious image attempting to alter containerd behavior", k) ++ continue ++ } + if err := clabels.Validate(k, v); err == nil { + labels[k] = v + } else { +diff --git a/pkg/cri/sbserver/podsandbox/helpers_test.go b/pkg/cri/sbserver/podsandbox/helpers_test.go +index 0e18d0c..1f655d4 100644 +--- a/pkg/cri/sbserver/podsandbox/helpers_test.go ++++ b/pkg/cri/sbserver/podsandbox/helpers_test.go +@@ -111,21 +111,29 @@ func TestGetRepoDigestAndTag(t *testing.T) { + + func TestBuildLabels(t *testing.T) { + imageConfigLabels := map[string]string{ +- "a": "z", +- "d": "y", +- "long-label": strings.Repeat("example", 10000), ++ "a": "z", ++ "d": "y", ++ "long-label": strings.Repeat("example", 10000), ++ "containerd.io/restart.policy": "always", ++ "io.cri-containerd.image": "managed", + } + configLabels := map[string]string{ + "a": "b", + "c": "d", ++ // reserved namespaces are only filtered for image config labels, not ++ // for labels from the CRI request ++ "containerd.io/restart.status": "stopped", + } + newLabels := buildLabels(configLabels, imageConfigLabels, containerKindSandbox) +- assert.Len(t, newLabels, 4) ++ assert.Len(t, newLabels, 5) + assert.Equal(t, "b", newLabels["a"]) + assert.Equal(t, "d", newLabels["c"]) + assert.Equal(t, "y", newLabels["d"]) ++ assert.Equal(t, "stopped", newLabels["containerd.io/restart.status"]) + assert.Equal(t, containerKindSandbox, newLabels[containerKindLabel]) + assert.NotContains(t, newLabels, "long-label") ++ assert.NotContains(t, newLabels, "containerd.io/restart.policy") ++ assert.NotContains(t, newLabels, "io.cri-containerd.image") + + newLabels["a"] = "e" + assert.Empty(t, configLabels[containerKindLabel], "should not add new labels into original label") +diff --git a/pkg/cri/server/helpers.go b/pkg/cri/server/helpers.go +index 58d674d..79ae55d 100644 +--- a/pkg/cri/server/helpers.go ++++ b/pkg/cri/server/helpers.go +@@ -285,11 +285,21 @@ func filterLabel(k, v string) string { + return fmt.Sprintf("labels.%q==%q", k, v) + } + +-// buildLabel builds the labels from config to be passed to containerd ++// buildLabels builds the labels from config to be passed to containerd. ++// Image config labels in the namespaces reserved for containerd ++// (containerd.io/) and the CRI plugin (io.cri-containerd) are not copied ++// to the container. + func buildLabels(configLabels, imageConfigLabels map[string]string, containerType string) map[string]string { + labels := make(map[string]string) + + for k, v := range imageConfigLabels { ++ // Labels in the containerd.io/* namespace are interpreted by containerd ++ // itself, and labels in the io.cri-containerd.* namespace are interpreted ++ // by the CRI plugin, so they are not copied from untrusted image configs. ++ if clabels.IsReserved(k) { ++ logrus.Warnf("skipping image label %q: the label namespace is reserved for containerd; possible malicious image attempting to alter containerd behavior", k) ++ continue ++ } + if err := clabels.Validate(k, v); err == nil { + labels[k] = v + } else { +diff --git a/pkg/cri/server/helpers_test.go b/pkg/cri/server/helpers_test.go +index 26119a1..2ebdb5e 100644 +--- a/pkg/cri/server/helpers_test.go ++++ b/pkg/cri/server/helpers_test.go +@@ -128,21 +128,29 @@ func TestGetRepoDigestAndTag(t *testing.T) { + + func TestBuildLabels(t *testing.T) { + imageConfigLabels := map[string]string{ +- "a": "z", +- "d": "y", +- "long-label": strings.Repeat("example", 10000), ++ "a": "z", ++ "d": "y", ++ "long-label": strings.Repeat("example", 10000), ++ "containerd.io/restart.policy": "always", ++ "io.cri-containerd.image": "managed", + } + configLabels := map[string]string{ + "a": "b", + "c": "d", ++ // reserved namespaces are only filtered for image config labels, not ++ // for labels from the CRI request ++ "containerd.io/restart.status": "stopped", + } + newLabels := buildLabels(configLabels, imageConfigLabels, containerKindSandbox) +- assert.Len(t, newLabels, 4) ++ assert.Len(t, newLabels, 5) + assert.Equal(t, "b", newLabels["a"]) + assert.Equal(t, "d", newLabels["c"]) + assert.Equal(t, "y", newLabels["d"]) ++ assert.Equal(t, "stopped", newLabels["containerd.io/restart.status"]) + assert.Equal(t, containerKindSandbox, newLabels[containerKindLabel]) + assert.NotContains(t, newLabels, "long-label") ++ assert.NotContains(t, newLabels, "containerd.io/restart.policy") ++ assert.NotContains(t, newLabels, "io.cri-containerd.image") + + newLabels["a"] = "e" + assert.Empty(t, configLabels[containerKindLabel], "should not add new labels into original label") +-- +2.45.4 + diff --git a/SPECS/moby-containerd-cc/moby-containerd-cc.spec b/SPECS/moby-containerd-cc/moby-containerd-cc.spec index b37170ad6c5..3f30b79fdd1 100644 --- a/SPECS/moby-containerd-cc/moby-containerd-cc.spec +++ b/SPECS/moby-containerd-cc/moby-containerd-cc.spec @@ -6,7 +6,7 @@ Summary: Industry-standard container runtime for confidential containers Name: moby-%{upstream_name} Version: 1.7.7 -Release: 13%{?dist} +Release: 14%{?dist} License: ASL 2.0 Group: Tools/Container URL: https://www.containerd.io @@ -30,6 +30,7 @@ Patch11:CVE-2025-64329.patch Patch12:CVE-2026-39882.patch Patch13:CVE-2026-35469.patch Patch14:CVE-2026-39821.patch +Patch15:CVE-2026-53488.patch %{?systemd_requires} @@ -87,6 +88,9 @@ fi %config(noreplace) %{_sysconfdir}/containerd/config.toml %changelog +* Mon Jul 06 2026 Azure Linux Security Servicing Account - 1.7.7-14 +- Patch for CVE-2026-53488 + * Thu May 28 2026 Akhila Guruju - 1.7.7-13 - Patch CVE-2026-39821