Skip to content

libct: require cgroups for rootless containers without private pidns - #5594

Open
xz-dev wants to merge 2 commits into
opencontainers:mainfrom
xz-dev:host-pidns-needs-cgroup
Open

xz-dev wants to merge 2 commits into
opencontainers:mainfrom
xz-dev:host-pidns-needs-cgroup

Conversation

@xz-dev

@xz-dev xz-dev commented Oct 31, 2026 •

Copy link
Copy Markdown

Summary

A rootless container without an effective cgroup needs its own private PID namespace to ensure all of its processes can be killed. In a host or joined PID namespace, killing the container's init does not kill its other processes. Waiting for the namespace owner's eventual exit is not independent cleanup of the joining container.

Since #4398, runc has warned about this configuration and said it would become an error. This PR turns that existing warning into an error when Manager.Apply returns cgroups.ErrRootless and Namespaces.IsPrivate(NEWPID) is false. The host-only exception from the earlier revision is removed.

  • Private PID namespace: the existing eligible rootless cgroup-error fallback remains allowed.
  • Host or joined PID namespace: the same failure is rejected with an explanation and the original cgroup error.
  • Successful cgroup setup: namespace sharing remains supported.
  • Other cgroup errors retain their existing behavior.

No new option and no pidfd changes are included.

Tests

  • create.bats checks rejection of the existing rootless host/shared case without cgroups.
  • start_hello.bats requires cgroup access for its rootless host-PID case.
  • kill.bats' shared-PID test now uses distinct A/B cgroups when available, starts extra detached exec work in B, verifies B's processes are gone after SIGKILL, and verifies A remains running. Without rootless cgroup access, it instead expects B admission to fail and A to survive.

Local validation of this revision:

  • Controlled rootless read-only-cgroup fixture: the earlier PR binary admits joined B and fails the new rejection expectation; the revised binary rejects joined and host cases for the intended cgroup reason, leaves no rejected state, and preserves private A.
  • Focused delegated-rootless BATS: 6/6 pass, including host PIDns, host PIDns after init exits, shared PIDns, create and run cases.
  • go test -tags seccomp ./libcontainer ./libcontainer/configs: pass.
  • Build, gofmt, shellcheck and diff-check: pass. Local build used the existing pure-Go libpathrs fallback.

Not rerun for this revision: full rootful, systemd, cgroup-v1/hybrid matrices or remote CI. Earlier revision results are not presented as coverage of the new joined restriction.

Compatibility

This is a behavior change for rootless host/joined containers relying on the no-cgroup fallback, including the historical configuration in #4394. The compatibility path previously allowed command completion while admitting leaked processes; the warning's planned error is now enforced. Configurations with working cgroups are unchanged.

BuildKit also has an intentional case: a containerized, daemonless buildkitd can rely on its outer PID namespace for eventual cleanup of all build processes (moby/buildkit#5512 (comment)). That is different from independent cleanup of each inner container. An explicit compatibility annotation has been proposed in review; no such option is included in this revision, and its interface remains under discussion.

Context: podman-container-tools/podman#30078

crun counterpart: containers/crun#2427

A rootless container which can not have a cgroup, and has no PID
namespace of its own, can not be killed properly: "runc kill" and
"runc delete --force" kill its init, and leave all its other processes
running.  Since runc 1.2.0 (opencontainers#4398), runc warns about this, and says it
will be an error in a future version.

Make it an error for a container in the host PID namespace (no PID
namespace in its config).  A container which joins the PID namespace
of another container still gets the warning, as its processes are
killed with the init of that namespace (see opencontainers#4394).

Assisted-by: Claude Opus 5.5
Signed-off-by: Xiangzhe Zeng <xiangzhedev@gmail.com>
@xz-dev
xz-dev force-pushed the host-pidns-needs-cgroup branch from cae43b9 to 4b91a4c Compare October 31, 2026 03:18
xz-dev added a commit to xz-dev/youki that referenced this pull request Oct 31, 2026
In the host PID namespace, the cgroup is the only way to find all the
container processes. Without it, "youki kill --all" and "youki delete
--force" can only kill init, and the other processes keep running.
Podman uses "kill --all" to stop such a container
(podman-container-tools/podman#30078).

Keep ignoring the rootless cgroup errors only when the container has a
PID namespace, new or joined. crun (containers/crun#2427) and runc
(opencontainers/runc#5594) are changed the same way.

Only the cgroup v2 manager changes: the systemd manager uses the
rootless flag only to apply device rules on a best effort basis, and
the v1 manager ignores it.

Signed-off-by: Xiangzhe Zeng <xiangzhedev@gmail.com>
Comment thread libcontainer/process_linux.go Resolved
Comment thread libcontainer/process_linux.go Outdated
}
if !p.config.Config.Namespaces.IsPrivate(configs.NEWPID) {
// TODO: make this an error in runc 1.3.
logrus.Warn("Creating a rootless container with no cgroup and no private pid namespace. " +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep the current behavior with an annotation?


BTW, having unkillable processes is not a serious concern for BuildKit, as long as the following conditions are satisfied:

Because all the RUN processes are killed on the death of the buildkitd process.

Originally posted by @AkihiroSuda in #5512

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now also have this PR to mitigate the zombie issue

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that is useful context. I see why the outer PID namespace of a containerized, daemonless buildkitd can provide eventual cleanup of all the RUN processes. That makes the compatibility concern concrete. Thanks also for linking moby/buildkit#7087; I have read its cancellation-cleanup approach, but have not tested it.

I was just going through the original discussion:

I think both concerns are valid: runc should not silently promise independent container cleanup that it cannot provide, but there are real callers deliberately relying on a larger enclosing lifecycle.

For now, I have updated this PR in 8cf811d and the corresponding implicit-fallback policy in containers/crun#2427 (containers/crun@910dc07): when eligible rootless cgroup setup fails, only a private PID namespace may continue. Sharing with a working cgroup is unchanged.

The kernel distinction is important here. Linux already provides cgroups and private PID namespaces for whole-container cleanup. Without either, POSIX process groups are not a reliable replacement for container membership: a process can leave the group with setsid or setpgid. FreeBSD's PROC_REAP_KILL is an example of kernel-managed signalling of a reaper's descendants. Linux's subreaper API provides orphan adoption, not that operation. This is not a drop-in solution for runc, but it explains why signalling one PID or scanning its current children does not provide the missing guarantee.

I also made refusal explicit in my youki PR, youki-dev/youki#3862, for new host/joined-PIDns containers that hit the eligible rootless cgroup failure. Private/no-cgroup containers, including exec into them, remain allowed. That is a proposed policy in the PR, not an already-merged upstream decision.

I had also been considering an explicit compatibility opt-in, such as a flag or environment variable, but had not settled on an interface that fits runc's conventions. An annotation may be a better fit. My hesitation is keeping known incomplete per-container cleanup as the implicit default solely for compatibility, rather than making that trade-off explicit. Would a narrowly scoped annotation retaining the existing best-effort behavior and warning be the preferred approach? I have not added such an option in this revision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AkihiroSuda I agree on the need for annotation. Something like this maybe?

      "annotations": {
          "run.oci.allow_unkillable_processes": "1"
      }

so we can use the same name across various runtimes.

The name chosen so it says what the caller agrees to, rather than describing the conditions in which it applies.

Ah, and it also needs to be added to PotentiallyUnsafeConfigAnnotations in features.go.

Replace the non-private PID namespace warning with an error when
cgroup setup returns ErrRootless. A joined namespace does not provide
independent cleanup of the joining container, so it needs the same
cgroup protection as a host PID namespace.

Keep private-namespace fallback and cgroup-backed sharing unchanged.
Extend the shared-namespace test to check refusal without a cgroup,
and cleanup of init plus exec work without stopping the namespace owner
when separate cgroups are available.

Assisted-by: GPT-6.1-sol
Signed-off-by: Xiangzhe Zeng <xiangzhedev@gmail.com>
@xz-dev xz-dev changed the title libct: refuse rootless container in host pidns without cgroup libct: require cgroups for rootless containers without private pidns Nov 1, 2026
@xz-dev

xz-dev commented Nov 1, 2026

Copy link
Copy Markdown
Author

I've updated this PR and containers/crun#2427 to apply the same rule to the existing implicit rootless cgroup-error fallback:

Cgroup setup PID namespace runc crun
Succeeds Private, host or joined Allowed, unchanged Allowed, unchanged
Eligible rootless setup failure New/private Existing fallback allowed Existing fallback allowed
Eligible rootless setup failure Host Rejected Rejected
Eligible rootless setup failure Joined Rejected Rejected

The rejection explains that a container without a private PID namespace needs a cgroup to reliably kill all its processes. No new compatibility option is included in these revisions. crun's explicit --cgroup-manager=disabled mode is outside this change and remains unchanged.

Revisions: runc 8cf811d8, crun 910dc072.

@xz-dev
xz-dev requested a review from kolyshkin November 1, 2026 11:08
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.

3 participants