diff --git a/daemon/internal/rootless/specconv/specconv_linux.go b/daemon/internal/rootless/specconv/specconv_linux.go index 8b601cd2e0..ccff1c5db0 100644 --- a/daemon/internal/rootless/specconv/specconv_linux.go +++ b/daemon/internal/rootless/specconv/specconv_linux.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/containerd/cgroups/v3" "github.com/containerd/log" "github.com/moby/moby/v2/daemon/internal/rootless" "github.com/opencontainers/runtime-spec/specs-go" @@ -244,6 +245,15 @@ func removeSysfs(s *specs.Spec) error { var mounts []specs.Mount // nolint: prealloc for _, mount := range s.Mounts { if strings.HasPrefix(mount.Destination, "/sys") { + // Unlike sysfs, mounting cgroup2 does not require the netns to + // be owned by the userns, so the cgroup mount can be kept when + // running with cgroup v2. Without it, the container cannot see + // its own limits (e.g. /sys/fs/cgroup/pids.max), although they + // are still enforced. + // https://github.com/moby/moby/issues/44084 + if cgroups.Mode() == cgroups.Unified && path.Clean(mount.Destination) == "/sys/fs/cgroup" { + mounts = append(mounts, mount) + } continue } mounts = append(mounts, mount) diff --git a/daemon/internal/rootless/specconv/specconv_linux_test.go b/daemon/internal/rootless/specconv/specconv_linux_test.go new file mode 100644 index 0000000000..0264cd935f --- /dev/null +++ b/daemon/internal/rootless/specconv/specconv_linux_test.go @@ -0,0 +1,43 @@ +package specconv + +import ( + "testing" + + "github.com/containerd/cgroups/v3" + "github.com/opencontainers/runtime-spec/specs-go" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" +) + +// TestRemoveSysfs checks that the cgroup mount is retained when replacing +// the /sys mounts for rootless + host netns, so that the container can +// still see its own resource limits (e.g. /sys/fs/cgroup/pids.max). +// https://github.com/moby/moby/issues/44084 +func TestRemoveSysfs(t *testing.T) { + skip.If(t, cgroups.Mode() != cgroups.Unified, "test requires cgroup v2") + + spec := &specs.Spec{ + Mounts: []specs.Mount{ + {Destination: "/proc", Type: "proc", Source: "proc"}, + {Destination: "/sys", Type: "sysfs", Source: "sysfs", Options: []string{"nosuid", "noexec", "nodev", "ro"}}, + {Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: []string{"ro", "nosuid", "noexec", "nodev"}}, + }, + } + assert.NilError(t, removeSysfs(spec)) + assert.Check(t, is.DeepEqual([]specs.Mount{ + {Destination: "/proc", Type: "proc", Source: "proc"}, + {Destination: "/sys/fs/cgroup", Type: "cgroup", Source: "cgroup", Options: []string{"ro", "nosuid", "noexec", "nodev"}}, + }, spec.Mounts)) +} + +// TestRemoveSysfsCustomSysMount checks that a user-specified /sys mount is +// left alone. +func TestRemoveSysfsCustomSysMount(t *testing.T) { + mounts := []specs.Mount{ + {Destination: "/sys", Type: "bind", Source: "/sys", Options: []string{"rbind", "ro"}}, + } + spec := &specs.Spec{Mounts: mounts} + assert.NilError(t, removeSysfs(spec)) + assert.Check(t, is.DeepEqual(mounts, spec.Mounts)) +}