rootless: keep the cgroup mount for containers with --net=host

For rootless + host netns (detach-netns mode), the daemon replaces the
/sys mounts, as sysfs cannot be mounted without owning the netns. This
removed the /sys/fs/cgroup mount as well, so such containers could not
see their own cgroup at all (e.g. /sys/fs/cgroup/pids.max was ENOENT
even when a pids limit was set and enforced).

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. runc mounts it with the container's cgroup namespace view,
so the container sees its own limits, e.g.:

  $ docker run --net=host --pids-limit 32 busybox cat /sys/fs/cgroup/pids.max
  32

This fixes TestUpdatePidsLimit in the rootless-systemd CI mode, which
was the last remaining failure there: it is the only test that reads
back cgroup limits from inside a container with --net=host (used for
speed), and it only runs when the daemon reports a cgroup driver other
than "none" (i.e. rootless + systemd + delegation). The cgroup limits
themselves were applied correctly all along (every container scope had
the pids controller and the correct pids.max on the host side); only
the container's view was missing.

Fixes the remaining part of the issue 44084.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
This commit is contained in:
Akihiro Suda
2026-07-09 17:19:39 +09:00
parent 15a2763030
commit 3c8d133837
2 changed files with 53 additions and 0 deletions

View File

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

View File

@@ -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))
}