VYPR
Moderate severityNVD Advisory· Published Feb 16, 2023· Updated Mar 10, 2025

containerd supplementary groups are not set up properly

CVE-2023-25173

Description

containerd is an open source container runtime. A bug was found in containerd prior to versions 1.6.18 and 1.5.18 where supplementary groups are not set up properly inside a container. If an attacker has direct access to a container and manipulates their supplementary group access, they may be able to use supplementary group access to bypass primary group restrictions in some cases, potentially gaining access to sensitive information or gaining the ability to execute code in that container. Downstream applications that use the containerd client library may be affected as well.

This bug has been fixed in containerd v1.6.18 and v.1.5.18. Users should update to these versions and recreate containers to resolve this issue. Users who rely on a downstream application that uses containerd's client library should check that application for a separate advisory and instructions. As a workaround, ensure that the "USER $USERNAME" Dockerfile instruction is not used. Instead, set the container entrypoint to a value similar to ENTRYPOINT ["su", "-", "user"] to allow su to properly set up supplementary groups.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/containerd/containerdGo
< 1.5.181.5.18
github.com/containerd/containerdGo
>= 1.6.0, < 1.6.181.6.18

Affected products

1

Patches

1
133f6bb6cd82

Merge pull request from GHSA-hmfx-3pcx-653p

https://github.com/containerd/containerdDerek McGowanFeb 15, 2023via ghsa
5 files changed · +504 79
  • integration/addition_gids_test.go+91 41 modified
    @@ -20,6 +20,7 @@
     package integration
     
     import (
    +	"fmt"
     	"os"
     	"path/filepath"
     	"testing"
    @@ -31,49 +32,98 @@ import (
     )
     
     func TestAdditionalGids(t *testing.T) {
    -	testPodLogDir, err := os.MkdirTemp("/tmp", "additional-gids")
    -	require.NoError(t, err)
    -	defer os.RemoveAll(testPodLogDir)
    +	testImage := GetImage(BusyBox)
    +	EnsureImageExists(t, testImage)
    +	type testCase struct {
    +		description string
    +		opts        []ContainerOpts
    +		expected    string
    +	}
     
    -	t.Log("Create a sandbox with log directory")
    -	sb, sbConfig := PodSandboxConfigWithCleanup(t, "sandbox", "additional-gids",
    -		WithPodLogDirectory(testPodLogDir))
    +	testCases := []testCase{
    +		{
    +			description: "Equivalent of `docker run` (no option)",
    +			opts:        nil,
    +			expected:    "groups=0(root),10(wheel)",
    +		},
    +		{
    +			description: "Equivalent of `docker run --group-add 1 --group-add 1234`",
    +			opts:        []ContainerOpts{WithSupplementalGroups([]int64{1 /*daemon*/, 1234 /*new group*/})},
    +			expected:    "groups=0(root),1(daemon),10(wheel),1234",
    +		},
    +		{
    +			description: "Equivalent of `docker run --user 1234`",
    +			opts:        []ContainerOpts{WithRunAsUser(1234)},
    +			expected:    "groups=0(root)",
    +		},
    +		{
    +			description: "Equivalent of `docker run --user 1234:1234`",
    +			opts:        []ContainerOpts{WithRunAsUser(1234), WithRunAsGroup(1234)},
    +			expected:    "groups=1234",
    +		},
    +		{
    +			description: "Equivalent of `docker run --user 1234 --group-add 1234`",
    +			opts:        []ContainerOpts{WithRunAsUser(1234), WithSupplementalGroups([]int64{1234})},
    +			expected:    "groups=0(root),1234",
    +		},
    +		{
    +			description: "Equivalent of `docker run --user daemon` (Supported by CRI, although unsupported by kube-apiserver)",
    +			opts:        []ContainerOpts{WithRunAsUsername("daemon")},
    +			expected:    "groups=1(daemon)",
    +		},
    +		{
    +			description: "Equivalent of `docker run --user daemon --group-add 1234` (Supported by CRI, although unsupported by kube-apiserver)",
    +			opts:        []ContainerOpts{WithRunAsUsername("daemon"), WithSupplementalGroups([]int64{1234})},
    +			expected:    "groups=1(daemon),1234",
    +		},
    +	}
     
    -	var (
    -		testImage     = GetImage(BusyBox)
    -		containerName = "test-container"
    -	)
    +	for i, tc := range testCases {
    +		i, tc := i, tc
    +		tBasename := fmt.Sprintf("case-%d", i)
    +		t.Run(tBasename, func(t *testing.T) {
    +			t.Log(tc.description)
    +			t.Logf("Expected=%q", tc.expected)
     
    -	EnsureImageExists(t, testImage)
    +			testPodLogDir := t.TempDir()
    +
    +			t.Log("Create a sandbox with log directory")
    +			sb, sbConfig := PodSandboxConfigWithCleanup(t, "sandbox", tBasename,
    +				WithPodLogDirectory(testPodLogDir))
    +
    +			t.Log("Create a container to print id")
    +			containerName := tBasename
    +			cnConfig := ContainerConfig(
    +				containerName,
    +				testImage,
    +				append(
    +					[]ContainerOpts{
    +						WithCommand("id"),
    +						WithLogPath(containerName),
    +					}, tc.opts...)...,
    +			)
    +			cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig)
    +			require.NoError(t, err)
    +
    +			t.Log("Start the container")
    +			require.NoError(t, runtimeService.StartContainer(cn))
    +
    +			t.Log("Wait for container to finish running")
    +			require.NoError(t, Eventually(func() (bool, error) {
    +				s, err := runtimeService.ContainerStatus(cn)
    +				if err != nil {
    +					return false, err
    +				}
    +				if s.GetState() == runtime.ContainerState_CONTAINER_EXITED {
    +					return true, nil
    +				}
    +				return false, nil
    +			}, time.Second, 30*time.Second))
     
    -	t.Log("Create a container to print id")
    -	cnConfig := ContainerConfig(
    -		containerName,
    -		testImage,
    -		WithCommand("id"),
    -		WithLogPath(containerName),
    -		WithSupplementalGroups([]int64{1 /*daemon*/, 1234 /*new group*/}),
    -	)
    -	cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig)
    -	require.NoError(t, err)
    -
    -	t.Log("Start the container")
    -	require.NoError(t, runtimeService.StartContainer(cn))
    -
    -	t.Log("Wait for container to finish running")
    -	require.NoError(t, Eventually(func() (bool, error) {
    -		s, err := runtimeService.ContainerStatus(cn)
    -		if err != nil {
    -			return false, err
    -		}
    -		if s.GetState() == runtime.ContainerState_CONTAINER_EXITED {
    -			return true, nil
    -		}
    -		return false, nil
    -	}, time.Second, 30*time.Second))
    -
    -	t.Log("Search additional groups in container log")
    -	content, err := os.ReadFile(filepath.Join(testPodLogDir, containerName))
    -	assert.NoError(t, err)
    -	assert.Contains(t, string(content), "groups=1(daemon),10(wheel),1234")
    +			t.Log("Search additional groups in container log")
    +			content, err := os.ReadFile(filepath.Join(testPodLogDir, containerName))
    +			assert.NoError(t, err)
    +			assert.Contains(t, string(content), tc.expected+"\n")
    +		})
    +	}
     }
    
  • integration/main_test.go+39 0 modified
    @@ -311,6 +311,45 @@ func WithLogPath(path string) ContainerOpts {
     	}
     }
     
    +// WithRunAsUser sets the uid.
    +func WithRunAsUser(uid int64) ContainerOpts {
    +	return func(c *runtime.ContainerConfig) {
    +		if c.Linux == nil {
    +			c.Linux = &runtime.LinuxContainerConfig{}
    +		}
    +		if c.Linux.SecurityContext == nil {
    +			c.Linux.SecurityContext = &runtime.LinuxContainerSecurityContext{}
    +		}
    +		c.Linux.SecurityContext.RunAsUser = &runtime.Int64Value{Value: uid}
    +	}
    +}
    +
    +// WithRunAsUsername sets the username.
    +func WithRunAsUsername(username string) ContainerOpts {
    +	return func(c *runtime.ContainerConfig) {
    +		if c.Linux == nil {
    +			c.Linux = &runtime.LinuxContainerConfig{}
    +		}
    +		if c.Linux.SecurityContext == nil {
    +			c.Linux.SecurityContext = &runtime.LinuxContainerSecurityContext{}
    +		}
    +		c.Linux.SecurityContext.RunAsUsername = username
    +	}
    +}
    +
    +// WithRunAsGroup sets the gid.
    +func WithRunAsGroup(gid int64) ContainerOpts {
    +	return func(c *runtime.ContainerConfig) {
    +		if c.Linux == nil {
    +			c.Linux = &runtime.LinuxContainerConfig{}
    +		}
    +		if c.Linux.SecurityContext == nil {
    +			c.Linux.SecurityContext = &runtime.LinuxContainerSecurityContext{}
    +		}
    +		c.Linux.SecurityContext.RunAsGroup = &runtime.Int64Value{Value: gid}
    +	}
    +}
    +
     // WithSupplementalGroups adds supplemental groups.
     func WithSupplementalGroups(gids []int64) ContainerOpts { //nolint:unused
     	return func(c *runtime.ContainerConfig) {
    
  • oci/spec_opts.go+103 37 modified
    @@ -113,6 +113,17 @@ func setCapabilities(s *Spec) {
     	}
     }
     
    +// ensureAdditionalGids ensures that the primary GID is also included in the additional GID list.
    +func ensureAdditionalGids(s *Spec) {
    +	setProcess(s)
    +	for _, f := range s.Process.User.AdditionalGids {
    +		if f == s.Process.User.GID {
    +			return
    +		}
    +	}
    +	s.Process.User.AdditionalGids = append([]uint32{s.Process.User.GID}, s.Process.User.AdditionalGids...)
    +}
    +
     // WithDefaultSpec returns a SpecOpts that will populate the spec with default
     // values.
     //
    @@ -522,7 +533,9 @@ func WithNamespacedCgroup() SpecOpts {
     //	user, uid, user:group, uid:gid, uid:group, user:gid
     func WithUser(userstr string) SpecOpts {
     	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
    +		defer ensureAdditionalGids(s)
     		setProcess(s)
    +		s.Process.User.AdditionalGids = nil
     
     		// 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
    @@ -615,7 +628,9 @@ func WithUser(userstr string) SpecOpts {
     // WithUIDGID allows the UID and GID for the Process to be set
     func WithUIDGID(uid, gid uint32) SpecOpts {
     	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
    +		defer ensureAdditionalGids(s)
     		setProcess(s)
    +		s.Process.User.AdditionalGids = nil
     		s.Process.User.UID = uid
     		s.Process.User.GID = gid
     		return nil
    @@ -628,12 +643,11 @@ func WithUIDGID(uid, gid uint32) SpecOpts {
     // additionally sets the gid to 0, and does not return an error.
     func WithUserID(uid uint32) SpecOpts {
     	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
    +		defer ensureAdditionalGids(s)
     		setProcess(s)
    -		if c.Snapshotter == "" && c.SnapshotKey == "" {
    -			if !isRootfsAbs(s.Root.Path) {
    -				return errors.New("rootfs absolute path is required")
    -			}
    -			user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
    +		s.Process.User.AdditionalGids = nil
    +		setUser := func(root string) error {
    +			user, err := UserFromPath(root, func(u user.User) bool {
     				return u.Uid == int(uid)
     			})
     			if err != nil {
    @@ -645,7 +659,12 @@ func WithUserID(uid uint32) SpecOpts {
     			}
     			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
     			return nil
    -
    +		}
    +		if c.Snapshotter == "" && c.SnapshotKey == "" {
    +			if !isRootfsAbs(s.Root.Path) {
    +				return errors.New("rootfs absolute path is required")
    +			}
    +			return setUser(s.Root.Path)
     		}
     		if c.Snapshotter == "" {
     			return errors.New("no snapshotter set for container")
    @@ -660,20 +679,7 @@ func WithUserID(uid uint32) SpecOpts {
     		}
     
     		mounts = tryReadonlyMounts(mounts)
    -		return mount.WithTempMount(ctx, mounts, func(root string) error {
    -			user, err := UserFromPath(root, func(u user.User) bool {
    -				return u.Uid == int(uid)
    -			})
    -			if err != nil {
    -				if os.IsNotExist(err) || err == ErrNoUsersFound {
    -					s.Process.User.UID, s.Process.User.GID = uid, 0
    -					return nil
    -				}
    -				return err
    -			}
    -			s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
    -			return nil
    -		})
    +		return mount.WithTempMount(ctx, mounts, setUser)
     	}
     }
     
    @@ -685,13 +691,12 @@ func WithUserID(uid uint32) SpecOpts {
     // the container.
     func WithUsername(username string) SpecOpts {
     	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
    +		defer ensureAdditionalGids(s)
     		setProcess(s)
    +		s.Process.User.AdditionalGids = nil
     		if s.Linux != nil {
    -			if c.Snapshotter == "" && c.SnapshotKey == "" {
    -				if !isRootfsAbs(s.Root.Path) {
    -					return errors.New("rootfs absolute path is required")
    -				}
    -				user, err := UserFromPath(s.Root.Path, func(u user.User) bool {
    +			setUser := func(root string) error {
    +				user, err := UserFromPath(root, func(u user.User) bool {
     					return u.Name == username
     				})
     				if err != nil {
    @@ -700,6 +705,12 @@ func WithUsername(username string) SpecOpts {
     				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
     				return nil
     			}
    +			if c.Snapshotter == "" && c.SnapshotKey == "" {
    +				if !isRootfsAbs(s.Root.Path) {
    +					return errors.New("rootfs absolute path is required")
    +				}
    +				return setUser(s.Root.Path)
    +			}
     			if c.Snapshotter == "" {
     				return errors.New("no snapshotter set for container")
     			}
    @@ -713,16 +724,7 @@ func WithUsername(username string) SpecOpts {
     			}
     
     			mounts = tryReadonlyMounts(mounts)
    -			return mount.WithTempMount(ctx, mounts, func(root string) error {
    -				user, err := UserFromPath(root, func(u user.User) bool {
    -					return u.Name == username
    -				})
    -				if err != nil {
    -					return err
    -				}
    -				s.Process.User.UID, s.Process.User.GID = uint32(user.Uid), uint32(user.Gid)
    -				return nil
    -			})
    +			return mount.WithTempMount(ctx, mounts, setUser)
     		} else if s.Windows != nil {
     			s.Process.User.Username = username
     		} else {
    @@ -733,7 +735,7 @@ func WithUsername(username string) SpecOpts {
     }
     
     // WithAdditionalGIDs sets the OCI spec's additionalGids array to any additional groups listed
    -// for a particular user in the /etc/groups file of the image's root filesystem
    +// for a particular user in the /etc/group file of the image's root filesystem
     // The passed in user can be either a uid or a username.
     func WithAdditionalGIDs(userstr string) SpecOpts {
     	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
    @@ -742,7 +744,9 @@ func WithAdditionalGIDs(userstr string) SpecOpts {
     			return nil
     		}
     		setProcess(s)
    +		s.Process.User.AdditionalGids = nil
     		setAdditionalGids := func(root string) error {
    +			defer ensureAdditionalGids(s)
     			var username string
     			uid, err := strconv.Atoi(userstr)
     			if err == nil {
    @@ -803,6 +807,68 @@ func WithAdditionalGIDs(userstr string) SpecOpts {
     	}
     }
     
    +// WithAppendAdditionalGroups append additional groups within the container.
    +// The passed in groups can be either a gid or a groupname.
    +func WithAppendAdditionalGroups(groups ...string) SpecOpts {
    +	return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
    +		// For LCOW or on Darwin additional GID's are not supported
    +		if s.Windows != nil || runtime.GOOS == "darwin" {
    +			return nil
    +		}
    +		setProcess(s)
    +		setAdditionalGids := func(root string) error {
    +			defer ensureAdditionalGids(s)
    +			gpath, err := fs.RootPath(root, "/etc/group")
    +			if err != nil {
    +				return err
    +			}
    +			ugroups, err := user.ParseGroupFile(gpath)
    +			if err != nil {
    +				return err
    +			}
    +			groupMap := make(map[string]user.Group)
    +			for _, group := range ugroups {
    +				groupMap[group.Name] = group
    +			}
    +			var gids []uint32
    +			for _, group := range groups {
    +				gid, err := strconv.ParseUint(group, 10, 32)
    +				if err == nil {
    +					gids = append(gids, uint32(gid))
    +				} else {
    +					g, ok := groupMap[group]
    +					if !ok {
    +						return fmt.Errorf("unable to find group %s", group)
    +					}
    +					gids = append(gids, uint32(g.Gid))
    +				}
    +			}
    +			s.Process.User.AdditionalGids = append(s.Process.User.AdditionalGids, gids...)
    +			return nil
    +		}
    +		if c.Snapshotter == "" && c.SnapshotKey == "" {
    +			if !filepath.IsAbs(s.Root.Path) {
    +				return errors.New("rootfs absolute path is required")
    +			}
    +			return setAdditionalGids(s.Root.Path)
    +		}
    +		if c.Snapshotter == "" {
    +			return errors.New("no snapshotter set for container")
    +		}
    +		if c.SnapshotKey == "" {
    +			return errors.New("rootfs snapshot not created for container")
    +		}
    +		snapshotter := client.SnapshotService(c.Snapshotter)
    +		mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
    +		if err != nil {
    +			return err
    +		}
    +
    +		mounts = tryReadonlyMounts(mounts)
    +		return mount.WithTempMount(ctx, mounts, setAdditionalGids)
    +	}
    +}
    +
     // WithCapabilities sets Linux capabilities on the process
     func WithCapabilities(caps []string) SpecOpts {
     	return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
    @@ -907,7 +973,7 @@ func UserFromPath(root string, filter func(user.User) bool) (user.User, error) {
     // ErrNoGroupsFound can be returned from GIDFromPath
     var ErrNoGroupsFound = errors.New("no groups found")
     
    -// GIDFromPath inspects the GID using /etc/passwd in the specified rootfs.
    +// GIDFromPath inspects the GID using /etc/group in the specified rootfs.
     // filter can be nil.
     func GIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) {
     	gpath, err := fs.RootPath(root, "/etc/group")
    
  • oci/spec_opts_linux_test.go+269 0 modified
    @@ -18,15 +18,203 @@ package oci
     
     import (
     	"context"
    +	"fmt"
     	"os"
     	"path/filepath"
     	"testing"
     
    +	"github.com/containerd/containerd/containers"
     	"github.com/containerd/containerd/pkg/testutil"
    +	"github.com/containerd/continuity/fs/fstest"
     	specs "github.com/opencontainers/runtime-spec/specs-go"
    +	"github.com/stretchr/testify/assert"
     	"golang.org/x/sys/unix"
     )
     
    +// nolint:gosec
    +func TestWithUserID(t *testing.T) {
    +	t.Parallel()
    +
    +	expectedPasswd := `root:x:0:0:root:/root:/bin/ash
    +guest:x:405:100:guest:/dev/null:/sbin/nologin
    +`
    +	td := t.TempDir()
    +	apply := fstest.Apply(
    +		fstest.CreateDir("/etc", 0777),
    +		fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 0777),
    +	)
    +	if err := apply.Apply(td); err != nil {
    +		t.Fatalf("failed to apply: %v", err)
    +	}
    +	c := containers.Container{ID: t.Name()}
    +	testCases := []struct {
    +		userID      uint32
    +		expectedUID uint32
    +		expectedGID uint32
    +	}{
    +		{
    +			userID:      0,
    +			expectedUID: 0,
    +			expectedGID: 0,
    +		},
    +		{
    +			userID:      405,
    +			expectedUID: 405,
    +			expectedGID: 100,
    +		},
    +		{
    +			userID:      1000,
    +			expectedUID: 1000,
    +			expectedGID: 0,
    +		},
    +	}
    +	for _, testCase := range testCases {
    +		testCase := testCase
    +		t.Run(fmt.Sprintf("user %d", testCase.userID), func(t *testing.T) {
    +			t.Parallel()
    +			s := Spec{
    +				Version: specs.Version,
    +				Root: &specs.Root{
    +					Path: td,
    +				},
    +				Linux: &specs.Linux{},
    +			}
    +			err := WithUserID(testCase.userID)(context.Background(), nil, &c, &s)
    +			assert.NoError(t, err)
    +			assert.Equal(t, testCase.expectedUID, s.Process.User.UID)
    +			assert.Equal(t, testCase.expectedGID, s.Process.User.GID)
    +		})
    +	}
    +}
    +
    +// nolint:gosec
    +func TestWithUsername(t *testing.T) {
    +	t.Parallel()
    +
    +	expectedPasswd := `root:x:0:0:root:/root:/bin/ash
    +guest:x:405:100:guest:/dev/null:/sbin/nologin
    +`
    +	td := t.TempDir()
    +	apply := fstest.Apply(
    +		fstest.CreateDir("/etc", 0777),
    +		fstest.CreateFile("/etc/passwd", []byte(expectedPasswd), 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:        "root",
    +			expectedUID: 0,
    +			expectedGID: 0,
    +		},
    +		{
    +			user:        "guest",
    +			expectedUID: 405,
    +			expectedGID: 100,
    +		},
    +		{
    +			user: "1000",
    +			err:  "no users found",
    +		},
    +		{
    +			user: "unknown",
    +			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 := WithUsername(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 TestWithAdditionalGIDs(t *testing.T) {
    +	t.Parallel()
    +	expectedPasswd := `root:x:0:0:root:/root:/bin/ash
    +bin:x:1:1:bin:/bin:/sbin/nologin
    +daemon:x:2:2:daemon:/sbin:/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
    +`
    +	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
    +		expected []uint32
    +	}{
    +		{
    +			user:     "root",
    +			expected: []uint32{0, 1, 2, 3},
    +		},
    +		{
    +			user:     "1000",
    +			expected: []uint32{0},
    +		},
    +		{
    +			user:     "bin",
    +			expected: []uint32{0, 2, 3},
    +		},
    +		{
    +			user:     "bin:root",
    +			expected: []uint32{0},
    +		},
    +		{
    +			user:     "daemon",
    +			expected: []uint32{0, 1},
    +		},
    +	}
    +	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,
    +				},
    +			}
    +			err := WithAdditionalGIDs(testCase.user)(context.Background(), nil, &c, &s)
    +			assert.NoError(t, err)
    +			assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids)
    +		})
    +	}
    +}
    +
     func TestAddCaps(t *testing.T) {
     	t.Parallel()
     
    @@ -247,3 +435,84 @@ func TestGetDevices(t *testing.T) {
     		})
     	})
     }
    +
    +func TestWithAppendAdditionalGroups(t *testing.T) {
    +	t.Parallel()
    +	expectedContent := `root:x:0:root
    +bin:x:1:root,bin,daemon
    +daemon:x:2:root,bin,daemon
    +`
    +	td := t.TempDir()
    +	apply := fstest.Apply(
    +		fstest.CreateDir("/etc", 0777),
    +		fstest.CreateFile("/etc/group", []byte(expectedContent), 0777),
    +	)
    +	if err := apply.Apply(td); err != nil {
    +		t.Fatalf("failed to apply: %v", err)
    +	}
    +	c := containers.Container{ID: t.Name()}
    +
    +	testCases := []struct {
    +		name           string
    +		additionalGIDs []uint32
    +		groups         []string
    +		expected       []uint32
    +		err            string
    +	}{
    +		{
    +			name:     "no additional gids",
    +			groups:   []string{},
    +			expected: []uint32{0},
    +		},
    +		{
    +			name:     "no additional gids, append root gid",
    +			groups:   []string{"root"},
    +			expected: []uint32{0},
    +		},
    +		{
    +			name:     "no additional gids, append bin and daemon gids",
    +			groups:   []string{"bin", "daemon"},
    +			expected: []uint32{0, 1, 2},
    +		},
    +		{
    +			name:           "has root additional gids, append bin and daemon gids",
    +			additionalGIDs: []uint32{0},
    +			groups:         []string{"bin", "daemon"},
    +			expected:       []uint32{0, 1, 2},
    +		},
    +		{
    +			name:     "append group id",
    +			groups:   []string{"999"},
    +			expected: []uint32{0, 999},
    +		},
    +		{
    +			name:     "unknown group",
    +			groups:   []string{"unknown"},
    +			err:      "unable to find group unknown",
    +			expected: []uint32{0},
    +		},
    +	}
    +
    +	for _, testCase := range testCases {
    +		testCase := testCase
    +		t.Run(testCase.name, func(t *testing.T) {
    +			t.Parallel()
    +			s := Spec{
    +				Version: specs.Version,
    +				Root: &specs.Root{
    +					Path: td,
    +				},
    +				Process: &specs.Process{
    +					User: specs.User{
    +						AdditionalGids: testCase.additionalGIDs,
    +					},
    +				},
    +			}
    +			err := WithAppendAdditionalGroups(testCase.groups...)(context.Background(), nil, &c, &s)
    +			if err != nil {
    +				assert.EqualError(t, err, testCase.err)
    +			}
    +			assert.Equal(t, testCase.expected, s.Process.User.AdditionalGids)
    +		})
    +	}
    +}
    
  • pkg/cri/server/container_create_linux.go+2 1 modified
    @@ -348,7 +348,8 @@ func (c *criService) containerSpecOpts(config *runtime.ContainerConfig, imageCon
     		// Because it is still useful to get additional gids for uid 0.
     		userstr = strconv.FormatInt(securityContext.GetRunAsUser().GetValue(), 10)
     	}
    -	specOpts = append(specOpts, customopts.WithAdditionalGIDs(userstr))
    +	specOpts = append(specOpts, customopts.WithAdditionalGIDs(userstr),
    +		customopts.WithSupplementalGroups(securityContext.GetSupplementalGroups()))
     
     	asp := securityContext.GetApparmor()
     	if asp == nil {
    

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

19

News mentions

0

No linked articles in our index yet.