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()
 

Reply via email to