JSTGTECH
← Back to blog

CVE-2024-21626 and runc's shared-kernel blast radius

4 min read

Every container on a multi-tenant host is a guest sharing one kernel with every other tenant. Namespaces and cgroups make that kernel look partitioned, but they don’t partition the kernel’s code — a bug reachable from inside a container is reachable from the same privilege level the kernel runtime itself trusts. CVE-2024-21626, the “Leaky Vessels” flaw in runc, is the clearest recent proof: a leaked file descriptor let a malicious image escape its container and land as root on the host, no exploit chain required beyond a crafted Dockerfile (Snyk, The Hacker News). And it wasn’t a one-off — a fresh batch of runc container-escape CVEs landed in late 2025, hitting the exact same trust boundary from a different angle.

Root cause

runc is the low-level OCI runtime underneath Docker, containerd, and most of Kubernetes’ container execution path — it’s the thing that actually calls pivot_root, sets up namespaces, and execs the container process. CVE-2024-21626 came from an order-of-operations bug: runc could leak an internal file descriptor referencing the host’s working directory before it finished walling the process off with pivot_root. A malicious image that set its WORKDIR to something like /proc/self/fd/7 could ride that leaked fd straight into a directory on the host filesystem, escaping the container rootfs entirely (GitHub PoC, Red Hat).

The 2025 trio — CVE-2025-31133, CVE-2025-52565, and CVE-2025-52881, disclosed by a SUSE researcher in November — is the same story with a different mechanism: race conditions in how runc mounts /dev and enforces “masked paths” under /proc. An attacker who controls container startup config can swap /dev/null or /dev/pts/$n for a symlink pointing at a sensitive procfs file milliseconds before runc bind-mounts it, tricking the runtime into mounting host /proc paths read-write inside the container, or redirecting writes meant for a scoped procfs entry to arbitrary host paths like /proc/sysrq-trigger (Sysdig, OCI advisory). Different bug class, same underlying truth: the isolation boundary is enforced entirely by kernel code that every container process can call into, and a single logic slip anywhere in that code path breaks the boundary for everyone sharing the kernel.

Blast radius

An attacker who lands one of these isn’t stealing data from a container — they’re getting arbitrary code execution on the host, at whatever privilege the container runtime holds, which is typically root. On a shared build farm, CI runner, or multi-tenant Kubernetes node, that one escape gives an attacker every other container’s filesystem, secrets mounted into other pods, the kubelet’s credentials, and a jumping-off point to the rest of the cluster. Leaky Vessels was especially nasty for CI/CD because it triggers just by building a malicious image — you don’t need to run untrusted code in production, you just need your pipeline to pull an attacker-controlled base image or Dockerfile and build it (Palo Alto Networks).

Working proof-of-concept exploit code for CVE-2024-21626 has been public on GitHub since shortly after disclosure, which is exactly the scenario that turns a CVSS score into an operational problem — no 0-day skill required, just an unpatched runtime and a way to get a container built or started. The 2025 trio requires a bit more precision (winning a mount race), but the outcome is the same: root on the node, and from there, lateral movement across whatever else that node was trusted to isolate.

Remediation

Patch the runtime, not just the orchestrator sitting on top of it — Docker and Kubernetes ship runc bundled, so “update Kubernetes” doesn’t automatically mean “update runc” if you’re on a vendored or older build:

  • CVE-2024-21626: fixed in runc 1.1.12, containerd 1.6.28 / 1.7.13, and Docker Engine 25.0.2 (Docker Scout)
  • CVE-2025-31133 / CVE-2025-52565 / CVE-2025-52881: fixed in runc ** 1.2.8, 1.3.3, and 1.4.0-rc.3** (SecurityWeek)

Beyond patching, this bug class is exactly why “container” and “security boundary” shouldn’t be treated as synonyms for anything you don’t trust. Defense in depth that actually helps here: run rootless containers so a kernel-level escape lands as an unprivileged host user instead of root; apply seccomp and drop capabilities aggressively so even a successful escape has less to work with; and for genuinely untrusted workloads — public CI runners, multi-tenant SaaS sandboxes, anything running code you didn’t write — put a real boundary under the shared kernel with gVisor (a userspace syscall shim the exploit has to get through first) or Kata Containers (a real VM boundary per workload, so a kernel exploit only pops that one micro-VM).

The bigger lesson

runc, containerd, and every OCI-compliant runtime built on the shared-kernel model give you process isolation, not a security boundary against a sufficiently novel kernel bug — and this class of vulnerability proves that gap isn’t hypothetical, it’s recurred across at least two unrelated bug classes in two years. That’s fine for internal services running your own trusted images. It’s a real risk for anything that runs code you don’t control: public CI, contributor-submitted builds, multi-tenant platforms. For those, the honest question isn’t “did we patch runc” — it’s “why are we relying on a shared kernel to isolate an adversary in the first place,” and whether a VM-backed runtime like Kata or a syscall-filtering sandbox like gVisor should be the default instead of the exception.

Related posts