Safeguard
Container Security

Container escape techniques and defense in depth

CVE-2024-21626 let a leaked file descriptor turn runc exec into host root. Here's how container escapes actually work — and the layers that stop them.

Safeguard Research Team
Research
6 min read

On January 31, 2024, researchers disclosed CVE-2024-21626, nicknamed "Leaky Vessels": a flaw in runc, the low-level runtime underneath Docker, containerd, and Kubernetes, that let a container process escape to the host with nothing more exotic than a crafted working directory. It was the second time in five years runc itself — not a misconfiguration, the actual code that creates containers — had a critical breakout bug; CVE-2019-5736 hit the same component in February 2019. Between those two disclosures sits a much larger, much older category of escape that needs no CVE at all: the --privileged flag, an over-granted Linux capability, or a bind-mounted Docker socket, any of which hands a container the keys to its host by design rather than by bug. This post walks through both categories — the runtime exploits and the configuration mistakes — and the specific, verifiable defenses that close each one, from capability dropping and seccomp profiles to Kubernetes Pod Security Admission and runtime detection mapped to MITRE ATT&CK. Understanding the mechanism is what separates a security team that can write an enforceable policy from one repeating "don't run privileged containers" without knowing why it matters.

What was CVE-2019-5736 and why did it matter?

CVE-2019-5736, disclosed February 11, 2019, let a malicious container overwrite the host's runc binary itself, achieving root code execution on the host the next time any container was started or entered. The technique abused how runc handled its own executable's file descriptor: a container process could open /proc/self/exe as O_PATH, then reopen it for writing through /proc/self/fd/, racing to overwrite the host runc binary during the brief window a subsequent runc exec or runc run had it memory-mapped. Any container that ran attacker-influenced code as root — including many that didn't look "privileged" at all — could trigger it. AWS, Google, and Palo Alto Unit 42 all published advisories, and the fix (runc 1.0-rc7, shipped in Docker 18.09.2) added protections around how the runtime opens and executes its own binary. It was the first widely publicized proof that the container runtime layer itself, not just container configuration, was an attack surface.

What is Leaky Vessels and how did it differ?

CVE-2024-21626, disclosed January 31, 2024 and affecting runc versions up to 1.1.11, leaked an internal file descriptor — in the worked example, one pointing at host cgroupfs — into the container's init process. A container (or a malicious image whose build steps run under runc) could set its working directory to /proc/self/fd/N, referencing that leaked descriptor, and inherit a working directory rooted in the host's filesystem namespace rather than its own. From there, a spawned process operating with that working directory could read or write host files outside any container boundary. Researchers at Snyk and WithSecure, working with the runc maintainers, coordinated disclosure across the ecosystem; fixes landed in runc 1.1.12, containerd 1.6.28/1.7.13, and Docker 25.0.2 within days. Unlike CVE-2019-5736, which required an active runc exec, this one could be triggered purely by building or running a malicious image — a supply-chain-adjacent path, not just an insider-threat one.

How do privileged flags and capabilities enable escapes without any bug at all?

The --privileged flag in Docker (or a Kubernetes pod running securityContext.privileged: true) disables nearly every isolation mechanism the kernel offers a container: it grants all Linux capabilities, disables seccomp filtering, and gives the container access to all host devices. A process root inside a privileged container can mount the host's raw block device and read or write any file on it — no exploit required, because the isolation was never applied. Individual capabilities carry similar risk on their own: CAP_SYS_ADMIN permits mounting filesystems and manipulating namespaces; CAP_SYS_MODULE allows loading arbitrary kernel modules, which is equivalent to arbitrary kernel code execution; CAP_SYS_PTRACE lets a container attach to and manipulate processes outside its own namespace when process namespaces are shared. A classic technique against older, unpatched cgroup v1 hosts abuses CAP_SYS_ADMIN to write to a cgroup's release_agent file, which the kernel then executes on the host when the cgroup empties — turning a filesystem write into arbitrary host command execution. None of this depends on a CVE; it depends on a container being granted more than it needs.

What mount-based misconfigurations create escape paths?

Bind-mounting the wrong host path into a container is one of the most common ways teams grant host root without ever intending to. Mounting the Docker socket (/var/run/docker.sock) into a container gives that container full control over the Docker daemon — it can launch a new, privileged sibling container that mounts the host's root filesystem, which is functionally equivalent to host root access. Mounting the host's / directly, or leaving /proc and /sys writable inside the container rather than the default restricted views, similarly exposes host-level controls (kernel tunables, process information for every host PID) to container-scoped code. These patterns show up regularly in CI/CD runners and monitoring agents that "need" broad host access for legitimate reasons, which is exactly why they deserve the tightest possible scoping rather than a blanket mount.

What layered defenses actually stop these techniques?

No single control stops every escape path, which is why the standard guidance is defense in depth across several independent layers. Drop --privileged entirely and run with --cap-drop=ALL, adding back only the specific capabilities a workload genuinely needs. Enforce read-only root filesystems so even a compromised process can't persist changes. Apply seccomp, AppArmor, or SELinux profiles to restrict which syscalls a container can make — Docker's default seccomp profile alone blocks dozens of syscalls, including mount and kernel-module operations, that most escape techniques depend on. Run containers rootless or with user-namespace remapping so a "root" process inside the container maps to an unprivileged UID on the host. And keep runc, containerd, and your container engine patched — both CVE-2019-5736 and CVE-2024-21626 were fixed within days of disclosure, but only for the fleets that actually applied the update. In Kubernetes, Pod Security Admission's "restricted" profile and OPA/Gatekeeper policies can block privileged pods, host-path mounts, and dangerous capabilities cluster-wide before a pod is ever scheduled.

How Safeguard helps

Safeguard's runtime protection includes a managed "Container escape" detection rule pack, mapped to MITRE ATT&CK technique T1611, that watches for namespace breakout attempts, privileged mount activity, and host-root access as workloads actually execute — catching the exploitation attempt itself, not just the misconfiguration that made it possible. Detections carry gated response actions, from alert through block, kill, quarantine, and full workload isolation, so a confirmed escape attempt can be contained without waiting on a human to notice a log line. On the patching side, self-healing containers close the other half of the gap: when a new CVE like a future runc or containerd flaw lands on a base image or dependency inside your containers, Safeguard's Griffin AI plans a patch, rebuilds the image, runs your test suite, and — depending on the mode you choose — promotes the fix automatically, with a median time-to-heal in the 20-to-45-minute range across customer tenants. Together, that's runtime detection for the exploit and automated patching for the vulnerability that enabled it.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.