Files
moby/daemon/oci_linux_test.go
Evan Lezar 704210f9e6 daemon: preserve CDI additional GIDs
CDI injection runs from WithDevices and may append supplementary groups to process.user.additionalGids. Docker's WithUser option previously ran afterwards and replaced Process.User, dropping any CDI-provided groups.

Resolve the final container user before device injection so CDI edits append to that user, and add regression coverage for a CDI v0.7 spec that injects additionalGids.

Signed-off-by: Evan Lezar <elezar@nvidia.com>
2026-05-08 15:24:38 +02:00

296 lines
9.0 KiB
Go

package daemon
import (
"context"
"maps"
"os"
"path/filepath"
"slices"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/v2/daemon/config"
"github.com/moby/moby/v2/daemon/container"
"github.com/moby/moby/v2/daemon/libnetwork"
nwconfig "github.com/moby/moby/v2/daemon/libnetwork/config"
"github.com/moby/moby/v2/daemon/network"
"github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
func setupFakeDaemon(t *testing.T, c *container.Container) *Daemon {
t.Helper()
root := t.TempDir()
rootfs := filepath.Join(root, "rootfs")
err := os.MkdirAll(rootfs, 0o755)
assert.NilError(t, err)
netController, err := libnetwork.New(context.Background(), nwconfig.OptionDataDir(t.TempDir()))
assert.NilError(t, err)
d := &Daemon{
// some empty structs to avoid getting a panic
// caused by a null pointer dereference
linkIndex: newLinkIndex(),
netController: netController,
imageService: &fakeImageService{},
}
c.Root = root
c.BaseFS = rootfs
if c.Config == nil {
c.Config = new(containertypes.Config)
}
if c.HostConfig == nil {
c.HostConfig = new(containertypes.HostConfig)
}
if c.NetworkSettings == nil {
c.NetworkSettings = &network.Settings{Networks: make(map[string]*network.EndpointSettings)}
}
// HORRIBLE HACK: clean up shm mounts leaked by some tests. Otherwise the
// offending tests would fail due to the mounts blocking the temporary
// directory from being cleaned up.
t.Cleanup(func() {
if c.ShmPath != "" {
var err error
for err == nil { // Some tests over-mount over the same path multiple times.
err = unix.Unmount(c.ShmPath, unix.MNT_DETACH)
}
}
})
return d
}
type fakeImageService struct {
ImageService
}
func (i *fakeImageService) StorageDriver() string {
return "overlay"
}
func TestCreateSpecPreservesCDIAdditionalGIDs(t *testing.T) {
cdiDir := t.TempDir()
err := os.WriteFile(filepath.Join(cdiDir, "test-device.yaml"), []byte(`
cdiVersion: "0.7.0"
kind: "example.com/device"
devices:
- name: foo
containerEdits:
additionalGids:
- 1234
`), 0o644)
assert.NilError(t, err)
origDeviceDrivers := maps.Clone(deviceDrivers)
t.Cleanup(func() {
deviceDrivers = origDeviceDrivers
})
RegisterCDIDriver(cdiDir)
c := &container.Container{
Config: &containertypes.Config{},
HostConfig: &containertypes.HostConfig{
Resources: containertypes.Resources{
DeviceRequests: []containertypes.DeviceRequest{
{
Driver: "cdi",
DeviceIDs: []string{"example.com/device=foo"},
},
},
},
},
}
d := setupFakeDaemon(t, c)
s, err := d.createSpec(t.Context(), &configStore{}, c, nil)
assert.NilError(t, err)
assert.Assert(t, slices.Contains(s.Process.User.AdditionalGids, uint32(1234)), "CDI additional GID not present in OCI spec")
}
// TestTmpfsDevShmNoDupMount checks that a user-specified /dev/shm tmpfs
// mount (as in "docker run --tmpfs /dev/shm:rw,size=NNN") does not result
// in "Duplicate mount point" error from the engine.
// https://github.com/moby/moby/issues/35455
func TestTmpfsDevShmNoDupMount(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
c := &container.Container{
ShmPath: "foobar", // non-empty, for c.IpcMounts() to work
HostConfig: &containertypes.HostConfig{
IpcMode: containertypes.IPCModeShareable, // default mode
// --tmpfs /dev/shm:rw,exec,size=NNN
Tmpfs: map[string]string{
"/dev/shm": "rw,exec,size=1g",
},
},
}
d := setupFakeDaemon(t, c)
_, err := d.createSpec(t.Context(), &configStore{}, c, nil)
assert.Check(t, err)
}
// TestIpcPrivateVsReadonly checks that in case of IpcMode: private
// and ReadonlyRootfs: true (as in "docker run --ipc private --read-only")
// the resulting /dev/shm mount is NOT made read-only.
// https://github.com/moby/moby/issues/36503
func TestIpcPrivateVsReadonly(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
c := &container.Container{
HostConfig: &containertypes.HostConfig{
IpcMode: containertypes.IPCModePrivate,
ReadonlyRootfs: true,
},
}
d := setupFakeDaemon(t, c)
s, err := d.createSpec(t.Context(), &configStore{}, c, nil)
assert.Check(t, err)
// Find the /dev/shm mount in ms, check it does not have ro
for _, m := range s.Mounts {
if m.Destination != "/dev/shm" {
continue
}
assert.Check(t, is.Equal(false, slices.Contains(m.Options, "ro")))
}
}
// TestSysctlOverride ensures that any implicit sysctls (such as
// Config.Domainname) are overridden by an explicit sysctl in the HostConfig.
func TestSysctlOverride(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
ctx := t.Context()
c := &container.Container{
Config: &containertypes.Config{
Hostname: "foobar",
Domainname: "baz.cyphar.com",
},
HostConfig: &containertypes.HostConfig{
NetworkMode: "bridge",
Sysctls: map[string]string{},
},
}
d := setupFakeDaemon(t, c)
// Ensure that the implicit sysctl is set correctly.
s, err := d.createSpec(ctx, &configStore{}, c, nil)
assert.NilError(t, err)
assert.Equal(t, s.Hostname, "foobar")
assert.Equal(t, s.Linux.Sysctl["kernel.domainname"], c.Config.Domainname)
if sysctlExists("net.ipv4.ip_unprivileged_port_start") {
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ip_unprivileged_port_start"], "0")
}
if sysctlExists("net.ipv4.ping_group_range") {
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ping_group_range"], "0 2147483647")
}
// Set an explicit sysctl.
c.HostConfig.Sysctls["kernel.domainname"] = "foobar.net"
assert.Assert(t, c.HostConfig.Sysctls["kernel.domainname"] != c.Config.Domainname)
c.HostConfig.Sysctls["net.ipv4.ip_unprivileged_port_start"] = "1024"
s, err = d.createSpec(ctx, &configStore{}, c, nil)
assert.NilError(t, err)
assert.Equal(t, s.Hostname, "foobar")
assert.Equal(t, s.Linux.Sysctl["kernel.domainname"], c.HostConfig.Sysctls["kernel.domainname"])
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ip_unprivileged_port_start"], c.HostConfig.Sysctls["net.ipv4.ip_unprivileged_port_start"])
// Ensure the ping_group_range is not set on a daemon with user-namespaces enabled
s, err = d.createSpec(ctx, &configStore{Config: config.Config{RemappedRoot: "dummy:dummy"}}, c, nil)
assert.NilError(t, err)
_, ok := s.Linux.Sysctl["net.ipv4.ping_group_range"]
assert.Assert(t, !ok)
// Ensure the ping_group_range is set on a container in "host" userns mode
// on a daemon with user-namespaces enabled
c.HostConfig.UsernsMode = "host"
s, err = d.createSpec(ctx, &configStore{Config: config.Config{RemappedRoot: "dummy:dummy"}}, c, nil)
assert.NilError(t, err)
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ping_group_range"], "0 2147483647")
}
// TestSysctlOverrideHost ensures that any implicit network sysctls are not set
// with host networking
func TestSysctlOverrideHost(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
ctx := t.Context()
c := &container.Container{
Config: &containertypes.Config{},
HostConfig: &containertypes.HostConfig{
NetworkMode: "host",
Sysctls: map[string]string{},
},
}
d := setupFakeDaemon(t, c)
// Ensure that the implicit sysctl is not set
s, err := d.createSpec(ctx, &configStore{}, c, nil)
assert.NilError(t, err)
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ip_unprivileged_port_start"], "")
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ping_group_range"], "")
// Set an explicit sysctl.
c.HostConfig.Sysctls["net.ipv4.ip_unprivileged_port_start"] = "1024"
s, err = d.createSpec(ctx, &configStore{}, c, nil)
assert.NilError(t, err)
assert.Equal(t, s.Linux.Sysctl["net.ipv4.ip_unprivileged_port_start"], c.HostConfig.Sysctls["net.ipv4.ip_unprivileged_port_start"])
}
func TestGetSourceMount(t *testing.T) {
// must be able to find source mount for /
mnt, _, err := getSourceMount("/")
assert.NilError(t, err)
assert.Equal(t, mnt, "/")
// must be able to find source mount for current directory
cwd, err := os.Getwd()
assert.NilError(t, err)
_, _, err = getSourceMount(cwd)
assert.NilError(t, err)
}
func TestDefaultResources(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root") // TODO: is this actually true? I'm guilty of following the cargo cult here.
c := &container.Container{
HostConfig: &containertypes.HostConfig{
IpcMode: containertypes.IPCModeNone,
},
}
d := setupFakeDaemon(t, c)
s, err := d.createSpec(context.Background(), &configStore{}, c, nil)
assert.NilError(t, err)
checkResourcesAreUnset(t, s.Linux.Resources)
}
func checkResourcesAreUnset(t *testing.T, r *specs.LinuxResources) {
t.Helper()
if r != nil {
if r.Memory != nil {
assert.Check(t, is.DeepEqual(r.Memory, &specs.LinuxMemory{}))
}
if r.CPU != nil {
assert.Check(t, is.DeepEqual(r.CPU, &specs.LinuxCPU{}))
}
assert.Check(t, is.Nil(r.Pids))
if r.BlockIO != nil {
assert.Check(t, is.DeepEqual(r.BlockIO, &specs.LinuxBlockIO{}, cmpopts.EquateEmpty()))
}
if r.Network != nil {
assert.Check(t, is.DeepEqual(r.Network, &specs.LinuxNetwork{}, cmpopts.EquateEmpty()))
}
}
}