Lab note: three runc flaws chain into a container escape to host root

This note maps three runc container-escape CVEs disclosed together on November 5, 2025: the maskedPaths symlink bypass, the /dev/console mount race, and the LSM label write redirect. Chained they reach host root; patched individually they leave gaps for homelab distros.

Share
This note maps three runc container-escape CVEs disclosed together on November 5, 2025: the maskedPaths symlink bypass (CVE-2025-31133), the /dev/console mount race (CVE-2025-52565), and the LSM label write redirect (CVE-2025-52881). All three share one prerequisite: a container with mount control. Chained, they reach host root; patched individually they still leave gaps for homelab distros that have not backported runc 1.3.3.

TL;DR

Three flaws in runc, disclosed together on November 5, 2025, let a container that can control its own mounts escape the container boundary and take root on the host. All three are fixed in runc 1.2.8, 1.3.3, and 1.4.0-rc.3, so the first action is a version check, which takes under a minute on any host. Nobody has confirmed active exploitation, and none of the trio is in the CISA KEV catalog as of this note. The urgency comes from patch age plus severity: the fix is nine months old, and several common homelab distros have not shipped a backport at all. If you run Debian 12 or 13, Proxmox VE 8.4 or 9.0, or TrueNAS SCALE 25.10.x, there is no patched package for you today. Run the interim mitigations below and calendar the backport.

What happened

The runc maintainers published three advisories on November 5, 2025. The individual reports came from Lei Wang (Huawei), Li Fubang (CIIC), and Tönis Tiigi (Docker); the underlying work on this class of bug goes back years to Aleksa Sarai at SUSE, who is the maintainer developing the fix. Each flaw, on its own, is a way to make runc write to a host procfs file it should not touch. Chained, they get to host root.

CVE-2025-31133 is the maskedPaths symlink bypass. runc hides sensitive host paths by bind-mounting the container's /dev/null on top of them, but it never verified the source was a real /dev/null inode. Swap it for a symlink during a mount race and runc bind-mounts the attacker's target read-write into the container. From there the path of interest is /proc/sys/kernel/core_pattern: reconfigure the coredump helper, trigger a crash, and the helper runs with full privileges on the host, because kernel upcalls are not namespaced. Host root. A second, lighter variant deletes /dev/null and makes runc skip the masking entirely, opening masked /proc files to read. CVSS v4 is 7.3 for the primary path and 5.6 for the read-only variant.

CVE-2025-52565 is the /dev/console mount race. For containers that allocate a console, runc bind-mounts /dev/pts/$n to /dev/console before maskedPaths and readonlyPaths are applied, with insufficient checks on the source. Same writable-procfs outcome: /proc/sysrq-trigger for a host DoS, or core_pattern for the escape. The maintainers rated the main attack High at CVSS v4 7.3. The advisory also describes two theoretical sub-issues (an os.Create truncation path and a /dev/pts inode swap) that their testing did not show to be exploitable, so those two carry no CVSS vector. The main race is still in the fix set.

CVE-2025-52881 is the LSM label write redirect, a more sophisticated rework of the 2019 CVE-2019-19921 pattern. The attacker tricks runc into writing the container's LSM label (AppArmor or SELinux) to the wrong procfs file, so the container runs unlabeled. On its own that is a label bypass, but the advisory goes further: the misdirected write can hit /proc/sysrq-trigger directly (writing the string "docker-default" there crashes the host) or /proc/sys/kernel/core_pattern through a redirected sysctl write, so it can deliver the escape without chaining anything at all. CVSS v4 7.3.

The chain that matters for severity: the default AppArmor profile is one of the only things standing between a normal container and the maskedPaths write, and CVE-2025-52881 removes exactly that. All three share the same prerequisite: the attacker has to be able to spawn containers with some control over their mounts. A standard malicious Dockerfile satisfies that, because docker buildx build runs build steps in parallel with custom shared mounts, and the maintainers verified the races against exactly that path.

Who is affected

Anything that starts containers through a vulnerable runc: Docker Engine on Linux, containerd (which uses whatever runc the host runs), CRI-O, and the lightweight Kubernetes distributions that bundle their own, like k3s and k0s. crun and youki were told about the same class of bug and shipped coordinated releases; LXC is considered vulnerable in some aspects but takes a broader "non-user-namespaced containers are insecure by design" stance.

Docker Desktop on macOS and Windows is a different story: it is not part of this runc cluster. It had its own flaw, CVE-2025-9074 (an exposed Engine API), fixed in Docker Desktop 4.44.3. Upgrade it for that reason, but do not expect the runc version check below to mean anything on a laptop.

How to check your runc version

The fastest check on a Docker host:

$ docker version | grep -A1 "runc:"
 runc:
  Version:          1.3.6

That is the host running the check today: 1.3.6, patched. A command I have seen in older research notes, docker version --format with a Runtimes.runc.Version template, is wrong for current Docker: that template field does not exist in docker version (I tested it against Engine 29.6.2 and it errors). The grep above, or docker info on older engines, is the reliable one.

The authoritative check is the binary itself, because runc is a host package and the one in your PATH can differ from the one Docker or containerd actually invokes:

$ runc --version
runc version 1.3.6
commit: v1.3.6-0-g491b69ba
spec: 1.2.1
go: go1.25.12
libseccomp: 2.5.5

On a box that still ships a vulnerable runc, the same command reads:

$ runc --version
runc version 1.2.6
commit: v1.2.6-0-g6120df7
spec: 1.0.2

For containerd nodes, where runc is a separate package:

$ containerd --version
containerd containerd v2.2.6 11ce9d5f

The containerd version tells you nothing about runc; check runc directly on the same box.

The pass line: 1.2.8, 1.3.3, or 1.4.0-rc.3 or later. Everything before that on the 1.2.x, 1.3.x, and 1.4.0-rc lines is vulnerable. One subtlety worth knowing, because it has confused the coverage: upstream 1.3.2 is vulnerable, and the first fixed 1.3.x is 1.3.3. Vendor strings that read 1.3.2 (Amazon Linux ships runc-1.3.2-2) are backports into the 1.3.2 source tree, where the release suffix marks the fix. If you see 1.3.2 somewhere, check the vendor's advisory before trusting it. runc 1.1.x and earlier are EOL and were never patched.

How to patch

Upgrade to a distro or runtime that bundles a patched runc. What that means depends on what you run.

Docker Engine (server): 28.5.2 and later is the fix point; it bundles runc v1.3.3 and the release note names all three CVEs. If you are on 28.4.0 or an earlier 28.5.x, you are on runc v1.3.0 and vulnerable.

Ubuntu: the binary lives in a separate runc-app package, not the runc library package (which Canonical has marked Ignored as a backport that is too intrusive, so it stays unpatched). jammy and noble are fixed at runc-app 1.3.3-0ubuntu1~<rel>.3 after a regression fix (USN-7851-1, then USN-7851-2). focal was left Ignored with no fix. apt update && apt install runc-app, then restart the daemon.

Debian: this is the bad one. As of this note, the Debian Security Tracker shows no stable backport. bookworm ships 1.1.5+ds1-1+deb12u1 and trixie 1.1.15+ds1-2, both vulnerable, and the only fixed uploads are in experimental and unstable (1.3.3+ds1-x). forky and sid carry the fix. If you are on Debian 12 or 13, there is no apt fix today; either run the interim mitigations below or install a patched runc binary or Docker Engine over the distro package.

k3s: the 1.30 line was never fixed (the last 1.30 release, v1.30.14+k3s2, bundles runc v1.2.6). Every other line got its first patched release on November 20, 2025: v1.31.14+ and v1.32.10+ bundle runc v1.2.8, and v1.33.6+ and v1.34.2+ bundle v1.3.3. Anything on 1.30 or a pre-November-20 point release of 1.31-1.34 is still exposed.

k0s: I could not verify a patched k0s release from primary sources this pass. k0s bundles its own runc under /var/lib/k0s/bin/runc, so the honest move is to check that binary directly and cross-reference the k0s release notes for a runc bump.

Proxmox VE: Proxmox ships no PVE-owned runc package (I checked the bookworm pve pool: zero runc entries), so a PVE node runs the underlying Debian runc. That means PVE 8.4 (Debian 12 base) and 9.0 (Debian 13 base) inherit Debian's unpatched runc, and the PVE security-advisory feed has no entry for this trio. You are as patched as Debian is.

TrueNAS SCALE: iXsystems acknowledged all three CVEs for 25.10.0 (runc 1.1.5+ds1-1+deb12u1) and named the fix for 26.04.0, but as of this note 26.04.0 is not GA and the recommended stable is still 25.10.6. Per-point-release runc versions are not published, so treat every 25.10.x point release as exposed.

If you can't patch today

The runc advisories name "enable user namespaces" as the main non-patch mitigation. It is real but partial, and on a running homelab it is expensive, so do not treat it as a fix.

User namespace remapping (docker --userns-remap) re-maps container root to an unprivileged host UID, so a breakout lands on a user with no host privileges. It applies to every container the daemon starts. The maintainer's own note says it "block[s] most of the most serious aspects of these attacks," because host procfs is no longer reachable through the container's DAC permissions, which breaks the /dev/null-symlink, bind-mount, write-to-proc chain. But it is not total: the same advisory warns the AppArmor backstop is "not that helpful" against CVE-2025-52881, which bypasses LSM labels. Read remap as "blocks the worst vectors," not "closes the cluster."

The operational cost is what operators actually hit. On an existing install, enabling remap does not migrate your data; it makes it disappear. Docker's docs are blunt: it "effectively masks existing image and container layers" and recommends enabling the feature on a new installation, not an existing one. After the flip, docker image ls comes back empty, you re-pull everything, and named volumes plus bind-mounted paths need ownership re-arranged to the remapped UID or they go read-only. That re-pull-plus-manual-chown is the real cost, and it is why a live Proxmox/Nextcloud/LLM stack is a bad time to flip it.

Rootless mode is stronger (both the daemon and the containers run non-root) but loses more: ports below 1024, --net=host, AppArmor and overlay networks, --privileged containers, and it moves you to user-space slirp4netns/pasta networking, which is slower. It also needs cgroup v2 and systemd for --cpus/--memory limits to apply.

Cheaper and worth doing everywhere: cut the spawn surface. The chain requires spawning containers with control over mounts, so audit who can run docker and who is in the docker group (the single most common homelab mistake). Pin images by digest from registries you trust and stop pulling one-off inference or utility images from random orgs onto the node that matters. Drop --privileged unless workloads require it, and keep untrusted images off the node entirely.

What not to do

Do not run rootless or remap without checking that your workloads survive it. Pi-hole on port 53, Nextcloud on 443, and a GPU/LLM container with a privileged passthrough are the three that most often break.

Do not assume AppArmor or SELinux protects you here. CVE-2025-52881 exists specifically to bypass LSM labels, so the profile that is your usual backstop is one of the things the attack removes.

Do not trust a "1.3.2" version string as patched. Upstream 1.3.2 is vulnerable; only vendor backports into the 1.3.2 tree (with a release suffix) are fixed.

Do not count CISA KEV as your risk signal. The trio is not in the catalog, which is consistent with no confirmed exploitation, but it also means the absence of a KEV listing is not the absence of risk. The fix is old and the chain is real; patch on schedule, not on a watchlist.

The Ollama 300k exposed-servers write-up (https://blog.malwlab.se/ollama-300000-exposed-servers-attack/) covers the same operator problem one layer up: an inference server that is reachable and unauthenticated. That one is published and worth reading alongside this. My local LLM hardening note covers the intake discipline this note leans on (pin by digest, verify before you run), and the adjacent CVE-2026-34040 Docker AuthZ bypass advisory is in the same neighborhood; both are still drafts and will be linked here when they ship.

Topics: