flawopen.com/Teardowns/cve-2024-21626-runc-container-breakout-leaky-vessels
CVE-2024-21626: How runc fixed the Leaky Vessels escape
CVE-2024-21626 combined an internal runc file-descriptor leak with a missing post-chdir boundary check. Depending on the launch path, a container could start with a host-namespace working directory or use a related exec path to reach host files.[1], [2], [3]
A container launch is like a building handoff: runc carries temporary keys while preparing the rooms. The bug let a requested room (working directory) resolve through one of those internal keys into the host building; the container process did not simply inherit the keys. The fix checks the final room is inside the container and closes internal handles before handing over control.[1], [2]
Core Concepts & Subsystem Terms
runc- The OCI runtime that creates and starts containers.[1]
Internal file descriptor- An operating-system handle for an open file or directory. A leaked descriptor can carry access across a process boundary if it remains open when a child is executed.[1], [3]
cwd- The process working directory. Linux can report ENOENT from Getwd when cwd is outside the process mount namespace root.[1], [2]
Container breakout- A path from a container process to host resources that should remain outside its isolation boundary.[1]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-403
Root Cause Analysis
Before runc 1.1.12, internal file descriptors could leak into runc init. The descriptors were internal to runc init, not simply inherited by the container process. In attacks 1 and 2, a configured or selected working directory could resolve through a leaked host-mount-namespace handle; runc did not verify after chdir that the final cwd was inside the container root. Separate variants used execve path handling to overwrite a host binary. The 1.1.12 release added four layers: verify cwd with Linux Getwd/ENOENT behavior, close internal descriptors immediately before execve, fix identified descriptor leaks (including O_CLOEXEC for the cgroup handle), and mark non-stdio descriptors close-on-exec before starting runc init.[1], [2], [3], [4]
CVE-2024-21626: attack variants and the runc 1.1.12 protection that closes each path
| Advisory variant | Required conditions and boundary | Relevant fix or workaround |
|---|---|---|
Attack 1 — malicious image via runc run | A privileged user must be induced to start an untrusted image. Its configured working directory can resolve outside the container mount namespace. | The post-chdir unix.Getwd() check fails when cwd is outside the namespace root. As a temporary workaround, permit only / as cwd for untrusted images. |
Attack 2 — container process plus runc exec | A process already inside the container must anticipate an administrator’s non-root runc exec --cwd choice and arrange the relevant path before exec. | The same post-chdir cwd check blocks execution outside the container. Restrict --cwd to / until patched. |
| Attack 3a — host-binary overwrite through image launch | This adapts attack 1 and retains its requirement that a privileged user start the malicious image; the variant crosses the execve boundary to a host binary. | Close internal runc descriptors immediately before execve, fix identified leaks, and set O_CLOEXEC. Upgrade to runc 1.1.12 or a vendor-fixed release. |
Attack 3b — host-binary overwrite through runc exec | This adapts attack 2 and starts from a malicious process in a running container the attacker can affect; a privileged administrator must still perform an ordinary runc exec on that container. The advisory says this needs less user interaction than the image path. | The close-before-execve and close-on-exec protections address this distinct path. The maintainers report no practical workaround for this variant; upgrade. |
Step-by-Step Attack Flow
Impact and scope
The advisory documents host-filesystem access and host control across four variants. Image-based variants require a privileged user to run an untrusted image. Attack 3b still requires an administrator to perform an ordinary runc exec on a container the attacker can affect.[1]
Source Code: Flaw vs. Secure Implementation
// Simplified pre-fix behavior; not verbatim runc source
if err := os.Chdir(config.Cwd); err != nil {
return err
}
// Missing: verify the final cwd is inside the container namespace root
// Upstream runc 1.1.12 boundary check (excerpt)
if wd, err := unix.Getwd(); errors.Is(err, unix.ENOENT) {
return errors.New("cwd is outside the container mount namespace root")
} else if err != nil {
return fmt.Errorf("cannot verify cwd safety: %w", err)
} else if !filepath.IsAbs(wd) {
return fmt.Errorf("cwd is not absolute: %q", wd)
}
Engineering & System Hardening Checklist
- ✓Upgrade runc to 1.1.12 or a later vendor-supported release; verify the effective runtime version in each node image.[1], [4]
- ✓Treat untrusted container images as code execution requests; restrict who can launch them through Docker, Kubernetes, and build pipelines.[1]
- ✓For runtime implementations, check cwd after chdir with the platform-supported namespace-root check, and fail closed on errors.[1], [2]
- ✓Set O_CLOEXEC on internal descriptors and close non-stdio runtime descriptors at the last safe point before exec; account for runtime-owned descriptors.[1], [3]
- ✓Do not treat user namespaces as a substitute for the runc patch; use them as an additional isolation layer where supported.[1]