Package: containerd Version: 1.6.20~ds1-1+deb12u1 Severity: important Tags: security patch User: t...@security.debian.org Usertags: CVE-2024-40635
Dear Maintainer, I'm submitting a patch for CVE-2024-40635 in the containerd package. Vulnerability details: - CVE ID: CVE-2024-40635 - Description: Integer overflow in UID/GID handling allows containers to run as root - Affected versions: All versions prior to 1.6.38, 1.7.27, and 2.0.4 - Fixed upstream in: https://github.com/containerd/containerd/commit/11504c3fc5f45634f2d93d57743a998194430b82 The vulnerability allows containers launched with a User set as a UID:GID larger than the maximum 32-bit signed integer to cause an overflow condition where the container ultimately runs as root (UID 0) . My patch adds validation for UID/GID values to prevent integer overflow, backported from the upstream fix. I've tested the patch and confirmed it correctly rejects values larger than MaxInt32. The patch has been tested on Debian bookworm and works correctly. Thank you for considering this contribution. Best regards, Mostafa Amin
Description: Fix integer overflow in UID/GID validation This patch adds validation to prevent integer overflow when parsing user IDs larger than MaxInt32, which could cause containers to run as root. . Without the fix, values larger than MaxInt32 are accepted and incorrectly converted to uint32, potentially allowing containers to run as root (UID 0). . CVE-2024-40635 Author: Mostafa Amin <mostafa.a...@windriver.com> Origin: upstream, https://github.com/containerd/containerd/commit/11504c3fc5f45634f2d93d57743a998194430b82 Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1100806 Last-Update: 2025-04-14 --- This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ Index: containerd-1.6.20~ds1/oci/spec_opts.go =================================================================== --- containerd-1.6.20~ds1.orig/oci/spec_opts.go +++ containerd-1.6.20~ds1/oci/spec_opts.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "os" "path/filepath" "runtime" @@ -582,6 +583,20 @@ func WithUser(userstr string) SpecOpts { defer ensureAdditionalGids(s) setProcess(s) s.Process.User.AdditionalGids = nil + // While the Linux kernel allows the max UID to be MaxUint32 - 2, + // and the OCI Runtime Spec has no definition about the max UID, + // the runc implementation is known to require the UID to be <= MaxInt32. + // + // containerd follows runc's limitation here. + // + // In future we may relax this limitation to allow MaxUint32 - 2, + // or, amend the OCI Runtime Spec to codify the implementation limitation. + const ( + minUserID = 0 + maxUserID = math.MaxInt32 + minGroupID = 0 + maxGroupID = math.MaxInt32 + ) // For LCOW it's a bit harder to confirm that the user actually exists on the host as a rootfs isn't // mounted on the host and shared into the guest, but rather the rootfs is constructed entirely in the @@ -598,8 +613,8 @@ func WithUser(userstr string) SpecOpts { switch len(parts) { case 1: v, err := strconv.Atoi(parts[0]) - if err != nil { - // if we cannot parse as a uint they try to see if it is a username + if err != nil || v < minUserID || v > maxUserID { + // if we cannot parse as an int32 then try to see if it is a username return WithUsername(userstr)(ctx, client, c, s) } return WithUserID(uint32(v))(ctx, client, c, s) @@ -610,12 +625,13 @@ func WithUser(userstr string) SpecOpts { ) var uid, gid uint32 v, err := strconv.Atoi(parts[0]) - if err != nil { + if err != nil || v < minUserID || v > maxUserID { username = parts[0] } else { uid = uint32(v) } - if v, err = strconv.Atoi(parts[1]); err != nil { + v, err = strconv.Atoi(parts[1]) + if err != nil || v < minGroupID || v > maxGroupID { groupname = parts[1] } else { gid = uint32(v) Index: containerd-1.6.20~ds1/oci/spec_opts_linux_test.go =================================================================== --- containerd-1.6.20~ds1.orig/oci/spec_opts_linux_test.go +++ containerd-1.6.20~ds1/oci/spec_opts_linux_test.go @@ -32,6 +32,97 @@ import ( ) //nolint:gosec +func TestWithUser(t *testing.T) { + t.Parallel() + + expectedPasswd := `root:x:0:0:root:/root:/bin/ash + guest:x:405:100:guest:/dev/null:/sbin/nologin + ` + expectedGroup := `root:x:0:root + bin:x:1:root,bin,daemon + daemon:x:2:root,bin,daemon + sys:x:3:root,bin,adm + guest:x:100:guest + ` + td := t.TempDir() + apply := fstest.Apply( + fstest.CreateDir("/etc", 0777), + fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777), + fstest.CreateFile("/etc/group", []byte(expectedGroup), 0777), + ) + if err := apply.Apply(td); err != nil { + t.Fatalf("failed to apply: %v", err) + } + c := containers.Container{ID: t.Name()} + testCases := []struct { + user string + expectedUID uint32 + expectedGID uint32 + err string + }{ + { + user: "0", + expectedUID: 0, + expectedGID: 0, + }, + { + user: "root:root", + expectedUID: 0, + expectedGID: 0, + }, + { + user: "guest", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "guest:guest", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "guest:nobody", + err: "no groups found", + }, + { + user: "405:100", + expectedUID: 405, + expectedGID: 100, + }, + { + user: "405:2147483648", + err: "no groups found", + }, + { + user: "-1000", + err: "no users found", + }, + { + user: "2147483648", + err: "no users found", + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.user, func(t *testing.T) { + t.Parallel() + s := Spec{ + Version: specs.Version, + Root: &specs.Root{ + Path: td, + }, + Linux: &specs.Linux{}, + } + err := WithUser(testCase.user)(context.Background(), nil, &c, &s) + if err != nil { + assert.EqualError(t, err, testCase.err) + } + assert.Equal(t, testCase.expectedUID, s.Process.User.UID) + assert.Equal(t, testCase.expectedGID, s.Process.User.GID) + }) + } + } + func TestWithUserID(t *testing.T) { t.Parallel()