containerd has an integer overflow in User ID handling
Description
containerd is an open-source container runtime. A bug was found in containerd prior to versions 1.6.38, 1.7.27, and 2.0.4 where containers launched with a User set as a UID:GID larger than the maximum 32-bit signed integer can cause an overflow condition where the container ultimately runs as root (UID 0). This could cause unexpected behavior for environments that require containers to run as a non-root user. This bug has been fixed in containerd 1.6.38, 1.7.27, and 2.04. As a workaround, ensure that only trusted images are used and that only trusted users have permissions to import images.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/containerd/containerd/v2Go | < 2.0.4 | 2.0.4 |
github.com/containerd/containerdGo | >= 1.7.0-beta.0, < 1.7.27 | 1.7.27 |
github.com/containerd/containerdGo | < 1.6.38 | 1.6.38 |
Affected products
1- Range: < 1.6.38
Patches
3cf158e884cfeMerge commit from fork
2 files changed · +112 −4
oci/spec_opts.go+20 −4 modified@@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "os" "path/filepath" "runtime" @@ -576,6 +577,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 @@ -592,8 +607,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) @@ -604,12 +619,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)
oci/spec_opts_linux_test.go+92 −0 modified@@ -32,6 +32,98 @@ import ( "golang.org/x/sys/unix" ) +//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) + }) + } +} + //nolint:gosec func TestWithUserID(t *testing.T) { t.Parallel()
1a43cb6a1035Merge commit from fork
2 files changed · +112 −4
pkg/oci/spec_opts.go+20 −4 modified@@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "os" "path/filepath" "runtime" @@ -593,6 +594,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 @@ -611,8 +626,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) @@ -623,12 +638,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)
pkg/oci/spec_opts_linux_test.go+92 −0 modified@@ -33,6 +33,98 @@ import ( "golang.org/x/sys/unix" ) +//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) + }) + } +} + //nolint:gosec func TestWithUserID(t *testing.T) { t.Parallel()
05044ec0a9a7Merge commit from fork
2 files changed · +112 −4
oci/spec_opts.go+20 −4 modified@@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "os" "path/filepath" "runtime" @@ -594,6 +595,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 @@ -612,8 +627,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) @@ -624,12 +639,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)
oci/spec_opts_linux_test.go+92 −0 modified@@ -33,6 +33,98 @@ import ( "golang.org/x/sys/unix" ) +//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) + }) + } +} + //nolint:gosec func TestWithUserID(t *testing.T) { t.Parallel()
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-265r-hfxg-fhmgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-40635ghsaADVISORY
- github.com/containerd/containerd/commit/05044ec0a9a75232cad458027ca83437aae3f4daghsax_refsource_MISCWEB
- github.com/containerd/containerd/commit/1a43cb6a1035441f9aca8f5666a9b3ef9e70ab20ghsax_refsource_MISCWEB
- github.com/containerd/containerd/commit/cf158e884cfe4812a6c371b59e4ea9bc4c46e51aghsax_refsource_MISCWEB
- github.com/containerd/containerd/security/advisories/GHSA-265r-hfxg-fhmgghsax_refsource_CONFIRMWEB
- lists.debian.org/debian-lts-announce/2025/05/msg00005.htmlghsaWEB
News mentions
0No linked articles in our index yet.