Skip to content

Add support for user systemd in systemd cgroup driver - #13853

Open
ThatsItForTheOtherOne wants to merge 2 commits into
google:masterfrom
ThatsItForTheOtherOne:rootless-systemd-cgroups
Open

Add support for user systemd in systemd cgroup driver#13853
ThatsItForTheOtherOne wants to merge 2 commits into
google:masterfrom
ThatsItForTheOtherOne:rootless-systemd-cgroups

Conversation

@ThatsItForTheOtherOne

@ThatsItForTheOtherOne ThatsItForTheOtherOne commented Jul 25, 2026

Copy link
Copy Markdown

Summary

runsc supports rootless deployments, but on a host using the systemd cgroup driver it fails for unprivileged users. #11543 has been open since March 2025. The only workaround is --ignore-cgroups, which disables resource limits.

The fix is two commits. One is the proper fix and the second is hardening. It can be dropped without affecting the overall fix.

1. runsc/cgroup: support rootless containers with the systemd cgroup driver

  • The driver always talked to the system service manager. This was diagnosed by DemiMarie in Error: OCI runtime error: runsc: creating container: systemd error: Interactive authentication required. #11543. Unprivileged users cannot create system-wide transient units, so polkit rejects the request as requiring interactive authorization. It now connects to the per-user instance when unprivileged, and mirrors runc's implementation.

  • Enabling controllers was fatal. The cgroupv2 path walk starts at the cgroup mount root, whose cgroup.subtree_control no unprivileged caller can write, so Join() aborted before the delegated scope could be used. This contrasted with runc's implementation. Controllers are now retried individually, and failures warn rather than abort.

Privilege is determined by translating the effective UID through /proc/self/uid_map, not geteuid(): rootless podman runs the OCI runtime inside a user namespace with the caller mapped to UID 0, so geteuid() returns 0 in exactly the case being fixed. The translated UID is also the credential the session bus requires for EXTERNAL authentication.

This commit also corrects g3doc/user_guide/systemd.md, which claimed the systemd driver is backed by the fs driver as runc's is. The text appears to have been reproduced from runc but was left unimplemented.

2. runsc/cgroup: fail when a requested limit's controller is unavailable

runsc generates unit properties from what the kernel supports, but not what is explicitly delegated to the user. runsc can request a limit for a controller that is not available in the container's cgroup. This can cause cases where limits are left silently unenforced.

Every controller that properties were generated for, that a spec actually requests a limit for, is recorded and verified against the leaf once the scope exists. Container creation fails, naming any that are missing. Controllers with no requested limit are not checked, so a delegation omitting something nobody is using still works.

Fixes #11543

Testing

Ubuntu 24.04, aarch64, kernel 6.14, cgroup v2, systemd 255, podman 4.9.3.
Requires CPU/cpuset delegation to user sessions, a standard rootless prerequisite (runc docs/cgroup-v2.md, "Configuring delegation"):

/etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids

Automated

//runsc/cgroup:cgroup_test   PASSED   (both commits, and the first alone)
//runsc/cgroup:cgroup_nogo   PASSED

//runsc/container:container_test fails identically on master and on this
branch in my environment, so it could not serve as a signal.

Rootless, end to end

Checks against three builds — master, first commit only (c1), both
(c1+2):

Check master c1 c1+2
Container starts (systemd driver) FAIL PASS PASS
Scope created at manager-relative path FAIL PASS PASS
memory.max / cpu.max applied SKIP PASS PASS
Unit owned by user manager, not system SKIP PASS PASS
Memory limit enforced (OOM) FAIL PASS PASS
Teardown removes the cgroup SKIP PASS PASS
Mandatory-controller warning emitted PASS PASS

Before, on master:

Error: OCI runtime error: creating container:
  systemd error: Interactive authentication required.

After:

$ podman run --rm --network=none --runtime=runsc alpine echo "Hello, gVisor rootless!"
Hello, gVisor rootless!

runsc runs inside a user namespace here, confirming the uid_map translation is
load-bearing rather than merely unit-tested:

$ cat /proc/self/uid_map     # as seen by runsc
         0       1000          1
         1     100000      65536

Limits reach the kernel:

$ cat $SCOPE/memory.max ; cat $SCOPE/cpu.max
268435456
50000 100000

And they are applied by the user manager, not the system one. LoadState is
the property that distinguishes a unit a manager has from one it does not;
systemctl show exits 0 and reports defaults for unknown units:

$ systemctl --user show libpod-$CID.scope -p LoadState -p MemoryMax
LoadState=loaded
MemoryMax=268435456

$ systemctl show libpod-$CID.scope -p LoadState -p MemoryMax
LoadState=not-found
MemoryMax=infinity

Enforced, and cleaned up — the delete path runs without
DBUS_SESSION_BUS_ADDRESS, exercising the $XDG_RUNTIME_DIR/bus fallback and
the persisted state:

$ podman run --rm --memory=256m alpine sh -c 'tail /dev/zero'; echo "exit=$?"
exit=137
$ podman rm -f lim && find /sys/fs/cgroup -path '*libpod*'
(no output)

podman update exercises the second D-Bus connection, made in a separate
process using Rootless restored from the state file:

$ podman update --memory=512m upd
$ cat $SCOPE/memory.max
536870912
$ systemctl --user show libpod-$CID.scope -p MemoryMax
MemoryMax=536870912

A container created by a master build and torn down after swapping in the
patched binary exits cleanly, confirming old state files still decode:

$ sudo podman rm -f compat; echo "exit=$?"
exit=0

Rootful — unchanged

Both builds, both cgroup drivers, all exit 0:

master systemd exit=0      c1+2 systemd exit=0
master cgroupfs exit=0     c1+2 cgroupfs exit=0

Limits still land on the system manager, with the cgroup under
machine.slice rather than user@1000.service, confirming Rootless=false
takes the original path with an empty ManagerCG:

$ sudo systemctl show libpod-$CID.scope -p MemoryMax -p CPUQuotaPerSecUSec
CPUQuotaPerSecUSec=500ms
MemoryMax=268435456
/sys/fs/cgroup/machine.slice/libpod-$CID.scope

Second commit: a requested limit that cannot be enforced is now fatal

The check applies to any controller the spec requests a limit for. io is used
to demonstrate it.

With io removed from Delegate= and an io limit requested, the first commit
alone warns and runs anyway:

W ... Unable to enable mandatory cgroup controllers io in
  ".../user@1000.service": ...; resource limits for them will NOT be applied

$ podman run --rm --runtime=runsc --blkio-weight=100 alpine true; echo "exit=$?"
exit=0
$ systemctl --user show $SCOPE -p IOWeight
IOWeight=910                       # systemd recorded it
$ cat $SCOPE/cgroup.controllers
cpuset cpu memory pids             # io absent; no io.weight file exists

With both commits the same command fails and names the controller. Containers requesting no io limit are unaffected, every other check still passes with io undelegated. No systemd unit or cgroup is left behind by the failure, since the scope is transient.

Not covered

  • The fs cgroup driver remains unusable for rootless containers. Out of scope for this change.
  • The second commit checks controllers at container creation. A limit changed later via runsc update for an undelegated controller is not re-checked.
  • Only io was tested un-delegated; the other controllers are covered by the same code path but were not exercised. On Ubuntu, I could not easily un-delegate the other controllers besides cpuset, which caused a crash anyway without commit 2.
  • aarch64, Ubuntu 24.04, systemd 255, cgroup v2 only. No checkpoint/restore or concurrent-creation testing.
  • hugeTLB2.generateProperties returns nothing, so hugepage limits are never sent to systemd and the new check does not cover them. Pre-existing and unchanged.

runsc always connected to the system service manager to create the
transient scope backing a container's cgroup. Unprivileged users cannot
create system-wide units, so polkit denies StartTransientUnit and
container creation fails with "systemd error: Interactive authentication
required". Rootless podman passes --systemd-cgroup, so runsc could only
be used with --ignore-cgroups, which discards all resource limits.

Connect to the per-user service manager when running unprivileged, and
resolve unit cgroup paths relative to that manager's own cgroup. Note
that the Slice= property remains relative to the managing instance even
though the path is not. This mirrors runc:
https://github.com/opencontainers/cgroups/blob/79cbc7c/systemd/user.go
https://github.com/opencontainers/cgroups/blob/79cbc7c/systemd/v2.go

Rootless podman runs the OCI runtime inside a user namespace with the
caller mapped to UID 0, so privilege is determined by translating the
effective UID through /proc/self/uid_map rather than by geteuid() alone.
The translated UID is also the credential the bus requires for EXTERNAL
authentication.

Enabling controllers through a parent's cgroup.subtree_control is also
made non-fatal, which the above depends on: the path walk starts at the
cgroup mount root, which no unprivileged caller can write, so Join()
aborted before the delegated scope could be used. Controllers are retried
individually; a warning names any mandatory controller that remains
unenabled, while optional and unmanaged ones are only logged at debug
level. Under delegation these writes are largely redundant, since systemd
has already enabled the delegated controllers in the subtree it created.
runc does the same, see
https://github.com/opencontainers/cgroups/blob/79cbc7c/fs2/create.go

This does not make the fs cgroup driver usable for rootless containers.
That path fails earlier, when creating the cgroup directory, and is left
unchanged.

Rootless and ManagerCG are exported so that they survive the CgroupJSON
round-trip: podman passes --systemd-cgroup on create but not on delete,
so teardown relies on the persisted state. Sandboxes created by an older
runsc decode both as zero values and retain the previous behaviour.

Finally, correct the systemd cgroup driver documentation. It claimed the
driver is backed by the fs driver, as runc's is, but no such fallback has
ever existed in runsc: cgroupSystemd overrides Install and Update and
never reaches controllerv2.set(). Resources with no systemd equivalent
are simply not applied.

Fixes google#11543

Assisted-by: Claude Code
Signed-off-by: Clair Kirsch <44384688+ThatsItForTheOtherOne@users.noreply.github.com>
runsc generates systemd unit properties from cgroupV2.Controllers, which
is read from cgroup.controllers at the mount root. That reports what the
kernel supports, not what was delegated to the systemd instance the unit
will live under, so runsc can request a limit for a controller that is
not available where the container's cgroup is created. systemd records
the property and applies what it can; the limit is simply never enforced.

For memory, cpu and cpuset this eventually surfaces, since the sandbox
reads memory.max, cpu.max and cpuset.cpus.effective at startup and fails
if they are absent. For io and pids nothing reads the value back and the
limit is dropped silently.

The existing controller check cannot catch either case. It also tests
against cgroupV2.Controllers, so under delegation every controller
appears present and neither the mandatory branch nor controller.skip()
is ever reached.

Record the controllers that properties were generated for, i.e. those the
spec actually requests limits for, and verify after the scope is created
that each is present in the leaf cgroup. Container creation fails naming
any that are missing. Controllers with no requested limit are not checked,
so a delegation that happens to omit a controller nobody is using is still
fine.

The scope is a transient unit, so systemd releases it when this process
exits and failing here does not leak it.

Assisted-by: Claude Code
Signed-off-by: Clair Kirsch <44384688+ThatsItForTheOtherOne@users.noreply.github.com>
@google-cla

google-cla Bot commented Jul 25, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@github-actions
github-actions Bot requested a review from rexren-gif July 25, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error: OCI runtime error: runsc: creating container: systemd error: Interactive authentication required.

1 participant