Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical and moderate cleanup correctness issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Adds fallback cgroup cleanup after runc delete --force to reap leaked container processes and remove stuck cgroups.
Changes:
- Supports cgroup v1/v2 and cgroupfs/systemd paths.
- Kills, waits for, and removes leftover cgroups.
- Adds shutdown integration and Linux cleanup tests.
| File | Summary |
|---|---|
cmd/containerd-shim-runc-v2/manager/manager_linux.go |
Invokes cgroup reaping during shim stop. |
cmd/containerd-shim-runc-v2/manager/cgroup_linux.go |
Implements cleanup. Findings: critical v2 root-slice path handling issue (2 votes); moderate v1 systemd cleanup issue (1 vote); nit for missing v1 test coverage (1 vote). |
cmd/containerd-shim-runc-v2/manager/cgroup_linux_test.go |
Tests path parsing and cgroup reaping. Findings: two moderate assertions can allow tests to hang (1 vote each). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical cgroup cleanup correctness and safety issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 4
Open (4)
Resolved since last review (1)
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Root cgroup handling poses a critical safety risk, and test coverage has blocking reliability gaps.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Relative cgroup paths, v1 deletion timeouts, and pidfd-dependent test behavior remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
Resolved since last review (2)
The runc shim's Stop, which containerd runs to clean up after a shim that is gone or a task whose creation failed, relies entirely on "runc delete --force" to tear the container down. That is not enough: - runc can fail to tear the container down, which is only logged; - runc reports success without doing anything when the container state.json was never written, which is what a "runc create" killed part way through leaves behind (opencontainers/runc#4757, reverted in containerd#5153). The "runc init" left over from such a create can even be frozen. Either way, processes are left running in the container cgroup. They keep the cgroup from being removed and the rootfs from being unmounted, so the bundle can never be deleted: containerd reloads the same dead shim and repeats the same failing cleanup, unmount retries included, on every start, and the leftover processes are never reaped. Reap the cgroup directly after runc, whatever runc reported: kill what is left in it, wait for it to empty, and remove it. The cgroup path comes from the bundle config.json, which containerd writes before runc is ever invoked, so this does not depend on any runc state. Both cgroup v1 and v2 are handled, with either the systemd or the cgroupfs path notation as selected by the SystemdCgroup option. The reap is bounded, as it runs while containerd starts up, and it never signals a process outside of the cgroup: processes are listed while the cgroup is frozen and are signalled through pidfds. When runc did its job the cgroup is already gone and this is a no-op. This kills nothing that "runc delete --force" is not already meant to kill: runc treats the container cgroup as owned by the container. Signed-off-by: Yongxiu Cui <cuiyongxiu@gmail.com>


Reap the container cgroup when the runc shim is stopped
Background
We run containerd on bare metal Kubernetes nodes and found nodes where containerd took 30+ seconds to start, every time it was restarted. Each start logged the same cleanup failures for the same set of old containers:
The bundles of these containers could never be deleted. Every containerd start loaded them again, ran the same failing cleanup (with its unmount retries, until the 5s dead shim cleanup timeout), and gave up. On containerd 1.6/1.7, which loads shims serially, this adds about 5s of boot time per leaked bundle. On 2.x it happens in parallel, but the bundles, cgroups and processes still leak forever.
On such a node, the container cgroup still held processes (in some cases a frozen
runc:[2:INIT]). They pinned the rootfs mount, so the unmount failed withEBUSY.Bundle.Delete()then failed, and the dead shim stayed around for the next start.Root cause
manager.Stop()in the runc shim is what containerd runs to clean up after a shim that has died, and after a task whose creation failed. For tearing the container down, it relies entirely onrunc delete --force. That is not enough:runc delete --forcecan fail. The failure is only logged, and cleanup continues with the rootfs unmount, which then fails because the processes are still there.runc delete --forcereturns success without doing anything when the container'sstate.jsonwas never written. That is exactly the state arunc createkilled partway through leaves behind, for example when containerd restarts (which kills its in-flightruncchildren) duringNewTask. At that point runc has already created the cgroup and startedrunc init, and it may have left the cgroup frozen. This is Preventing containers from being unable to be deleted opencontainers/runc#4757 (merged, then reverted in Zombies with exec probes : Kubernetes 1.20-containerd 1.5.0-beta3 #5153 because of a conmon regression; Prepare v1.5.0-rc.0 #5257 is a new attempt that is still under discussion).In both cases the processes left in the container cgroup keep the cgroup populated and the rootfs busy, and nothing ever reaps them.
Fix
After
runc delete --force, whatever it reported,Stop()now reaps the container cgroup directly: it kills whatever is left in it, waits for it to empty, and removes it.linux.cgroupsPathin the bundle'sconfig.json. containerd writes that file before runc is ever invoked, so this does not depend on any runc state.SystemdCgroupoption decides whether the path uses systemdslice:prefix:namenotation or is a literal cgroupfs path, the same way runc decides.cgroup.kill(which also kills frozen processes). On kernels older than 5.14 it falls back to the same bounded loop as v1.runc createleft frozen. Signals go through pidfds, re-checked against the cgroup membership, so a reused PID outside the cgroup is never signalled.This kills nothing that
runc delete --forceis not already meant to kill: runc treats the container cgroup as owned by the container.The fix lives in the shim because
Stop()is the one place that runs in both cleanup paths: the create failure and the dead shim cleanup at boot. It also does not depend on how runc eventually addresses #4757. It covers both thestate.jsongap and a plainrunc deletefailure.How to reproduce
I wrote a small, deterministic reproducer: https://github.com/yongxiu/leakrepro
It creates a container through the containerd client with an OCI
createRuntimehook. runc runs that hook after it has created the cgroup and mounted the rootfs, but before it writesstate.json. The hook freezes the container cgroup and SIGKILLsrunc create. That leaves exactly the production end state:runc:[2:INIT], reparented to PID 1, still in the container cgroup and pinning the rootfs;state.json, sorunc delete --forceexits 0 and does nothing.No image or snapshotter is needed. It reproduces 100% of the time.
On a throwaway node (it deliberately leaks a process):
The repo also has
run-repro.sh. It runs each containerd build in a privileged, throwaway Docker container with its own containerd, so the host is not touched.Results
runc 1.3.6, cgroup v2, 3 runs each, identical every run:
NewTaskfails afterrunc:[2:INIT]+ cgroupI also verified it on a real cgroup v2 node running containerd v2.2.1, with only this change cherry-picked into the shim. With the stock shim the container leaks. With the patched shim,
leakrepro inspectreports CLEAN right after the failed create, and containerd restarts stay fast. After the fix there is no leak at all.Tests
cgroup_linux_test.goadds unit tests for the path resolution (systemd notation including the root slice-.slice, cgroupfs paths with colons) and the choice of cgroup v1 subsystem. It also runs real-kernel tests against a delegated child cgroup: a live process, a frozen process, empty and already-removed cgroups, the bounded freeze-and-kill fallback, and a check that a listed PID that is no longer in the cgroup is not signalled. Those tests skip themselves when cgroup v2 delegation is not available.Related: opencontainers/runc#4757, opencontainers/runc#5153, opencontainers/runc#5257