In January 2024 Snyk's research team disclosed a cluster of container escape vulnerabilities they branded Leaky Vessels: CVE-2024-21626 in runc, CVE-2024-23651, CVE-2024-23652, and CVE-2024-23653 in BuildKit. The headline CVE was a process.cwd leak through an unclosed file descriptor, which let a malicious image trigger a working-directory move into the host's filesystem and then execute from there. Two years on, the techniques have not aged out of the threat model. Fleets that patched runc once and moved on often still have ancient BuildKit images in their CI pipelines, and the class keeps generating incidents that look new but are not.
What actually broke in runc?
A file descriptor in runc pointed into the host filesystem and was not closed before the container process was handed control. CVE-2024-21626 exploited this by using WORKDIR /proc/self/fd/<N> in a Dockerfile, or a crafted runtime spec, so the new process's current working directory landed on a path that resolved into the host. From there the process could read and write host files with the privileges of the container runtime, which on most setups is root.
The underlying bug is banal. Forgetting to set close-on-exec on a long-lived file descriptor is a mistake that appears in CVE databases every year. What made this one dangerous is that runc is the default OCI runtime behind Docker, containerd, CRI-O, and most Kubernetes clusters. One unpatched runc binary covered a very large blast radius. Snyk's disclosure timeline and Aleksa Sarai's patch commit show a coordinated fix, but "one patched runc" was never the full mitigation story.
Why is the class still alive in 2026?
Because the patch is runc-scoped and the supply chain is much larger. An organization can upgrade its Kubernetes node runc to 1.1.12 or later and still ship developer laptops and CI runners with trojanable Docker Desktop versions. BuildKit ships inside Docker, inside GitHub Actions runners, inside GitLab CI shared runners, and inside third-party build services that few teams inventory. Leaky Vessels' BuildKit CVEs (23651/23652/23653) hit mount cache, frontend, and the --mount=type=cache path, and all three require the BuildKit daemon to be on a recent version. The long tail of pinned, cached, or vendored BuildKit binaries inside build images is the reason red teams still find exploitable instances in 2026 engagements.
A second reason is that the mitigations are subtle. Docker's guidance was to upgrade runc and BuildKit. Kubernetes guidance was to upgrade the node runtime. Neither spoke directly to the CI runner maintainers, the build-image authors, or the vendors bundling Docker-in-Docker for test pipelines. Work by Rory McCune and the NCC Group container research team in 2024 and 2025 documented how container-escape classes survive specifically in these supply-chain-adjacent places, and Leaky Vessels is the canonical example.
How does exploitation look on a real system?
The simplest proof is a Dockerfile with WORKDIR /proc/self/fd/7 followed by a RUN step. On an unpatched runc, the RUN executes with cwd already in the host's /. An attacker who can push a malicious image and convince a target to pull and run it gets host file access for the duration of that container step. In BuildKit the analogous primitive is a crafted mount that escapes the expected mount path. Snyk's public PoCs cover both families.
In production, the delivery vector is almost never a directly pushed malicious image. It is a transitive dependency: a base image pulled from a registry, a GitHub Actions docker-based action that builds a child image, or a vendor-shipped build helper that wraps BuildKit. The attacker does not need shell on the host; they need a commit into a build configuration that causes a vulnerable runtime to process a crafted input. This matches the broader shift toward supply-chain-delivered exploits documented at Black Hat USA 2024 and Def Con 32.
What detection actually catches it?
Signature-based detection on the WORKDIR /proc/self/fd/... pattern works for the CVE-2024-21626 variant and is cheap to add to image scanners. It will not catch re-derived variants that use a different file-descriptor path. Behavioral detection on runtime is stronger: a container process whose cwd resolves outside its rootfs is anomalous under any reasonable policy, and Falco, Tracee, and Tetragon all have rules that trigger on this. Tetragon's 2024 detection rules for Leaky Vessels specifically are a good starting point for teams running eBPF-based runtime security.
Image provenance scanning is the other layer. Builds that produce images with unusual WORKDIR values, or that embed old BuildKit binaries, are worth flagging at build time regardless of whether you can prove an exploit. This is the cheaper-and-earlier version of the runtime rule.
What about Kubernetes admission-time controls?
Admission-time policy has a role but it is narrower than vendors suggest. Kyverno or OPA Gatekeeper can block images that reference known-vulnerable base images, require a minimum runc version on the node, and block privileged: true pods. None of that prevents a supply chain where a trusted-looking image contains a trojaned build step. The admission layer is one of four or five controls you need, not the single one.
The more valuable admission control is actually on securityContext. A pod running with allowPrivilegeEscalation: false, a dropped capability set, a read-only root filesystem, and a non-root user is measurably harder to exploit via a container escape even on an unpatched runc. Leaky Vessels' CVE-2024-21626 still works in principle with these controls, but the attacker's post-escape options shrink dramatically.
How should build pipelines be hardened specifically?
Two changes do most of the work. First, pin BuildKit and runc versions in every build environment you control, not just in Kubernetes nodes. This includes CI runners, developer laptop Docker installations, and any build service or runner image you build in-house. Second, treat the build environment as a tenancy boundary: a compromised image should not be able to reach the host or the other builds on the same runner. Ephemeral runners (fresh VM per job) are the cleanest way to enforce this, and GitHub-hosted runners, GitLab SaaS runners, and CircleCI machine executors all default to this model. Self-hosted runners are the common place where this principle fails.
The related practice is to stop using Docker-in-Docker (DinD) for build steps when you have a choice. DinD was always a workaround for the absence of a first-class rootless build runtime, and it magnifies the blast radius of any container-escape class. Kaniko, Buildah with --isolation=chroot, and BuildKit's rootless mode are viable substitutes for most image builds, and all of them reduce the surface that Leaky Vessels-style bugs can reach.
What does the research agenda look like now?
The runc maintainers merged a set of hardening changes in 2024 and 2025 that went beyond the specific Leaky Vessels CVEs: tighter fd handling, stricter mount validation, and more aggressive seccomp defaults in the reference OCI spec. The USENIX Security 2025 program included work on systematic discovery of container-escape primitives by differential testing of OCI runtimes, and the takeaway was that runc is not uniquely bad — most OCI runtimes have undiscovered primitives in similar code paths.
For defenders the practical implication is that the class will keep producing CVEs, and the patch cadence has to become a normal part of CI runner and node maintenance rather than a one-time fire drill. The 2024 disclosure was newsworthy; the 2026 version of it will be routine, and treating it as routine is the goal.
How Safeguard.sh Helps
Safeguard.sh's continuous image scanning inventories every runc and BuildKit binary in your build supply chain, not just the ones on Kubernetes nodes, and flags the exact CVE-2024-21626, 23651, 23652, and 23653 versions that keep surviving in CI runner images. Reachability analysis ties the finding to the specific build steps and services that consume a vulnerable runtime, so you fix the ones that matter first instead of triaging every Dockerfile in the org. Griffin AI reviews pull requests for the WORKDIR /proc/self/fd/... pattern and other known exploitation primitives, and Eagle watches runtime for cwd-escape behavior across the fleet. Guardrails in the build pipeline block images whose base layers embed vulnerable BuildKit, and 100-level dependency depth catches the transitive cases where a trusted base image pulls a trojaned helper.