On November 5, 2025, the Open Container Initiative disclosed three container-escape bugs in runc — CVE-2025-31133, CVE-2025-52565, and CVE-2025-52881 — all CVSS 7.3, all in the same code path, and all reachable from nothing more than a container a tenant is allowed to build. Eight months later, the question that actually matters for a self-hosted fleet isn't "was there a CVE." It's "did the fix actually reach the node my kubelet is running on, and would switching container runtimes have helped if it hadn't." The honest answer to the second question is no — and the reasoning why is the part most coverage of these CVEs skipped.
What actually broke
All three bugs live in the same place: the logic runc uses to protect sensitive host paths from a container that's allowed to define its own mount configuration.
| CVE | CVSS | Mechanism | Vulnerable runc | Patched runc |
|---|---|---|---|---|
| CVE-2025-31133 | 7.3 | maskedPaths bind-mounts /dev/null over sensitive host files (e.g. /proc/sysrq-trigger). runc never verified the container's /dev/null was a real /dev/null inode — replace it with a symlink during a mount race and runc bind-mounts the symlink's target read-write instead. | ≤ 1.2.7, 1.3.0–1.3.1, 1.4.0-rc.1/rc.2 (every release back through 1.0.0-rc3, per OCI's advisory) | 1.2.8, 1.3.3, 1.4.0-rc.3 |
| CVE-2025-52565 | 7.3 | runc bind-mounts /dev/pts/$n to /dev/console before masked/read-only paths are applied. Swap /dev/pts/$n for a symlink mid-race and that mount lands on /proc/sys/kernel/core_pattern instead — the file that controls what the kernel executes on any process crash. | Same range as above | 1.2.8, 1.3.3, 1.4.0-rc.3 |
| CVE-2025-52881 | 7.3 | Extends the same race to redirect arbitrary /proc writes, which can bypass AppArmor/SELinux relabeling in some configurations — turning an ordinary runc write into an attacker-chosen one. | Same range as above | 1.2.8, 1.3.3, 1.4.0-rc.3 |
Concretely, here's the CVE-2025-31133 race in the shape the CNCF and Sysdig writeups describe it: a container's OCI spec lists /proc/sysrq-trigger as a masked path, so runc's job is to bind-mount something inert — /dev/null — over it so the container can't read or write that file. runc locates /dev/null inside the container's own mount namespace, then bind-mounts whatever inode currently sits at that path. If an attacker-controlled process (the container's own init, or anything else running inside it before runc finishes setting up masked paths) deletes that /dev/null and replaces it with a symlink to, say, /proc/self/root/proc/sysrq-trigger, the race is won: runc's bind-mount now targets the symlink's destination instead of a harmless device node, and the "masked" path ends up read-write. The fix versions add isDevNull/verifyDevNull checks that confirm the source is genuinely device node 1:3 before trusting it, and resolve the target via /proc/self/fd/ rather than by re-walking a path an attacker could swap out from under the check.
Worth being precise about what "actively exploited" means here, because it's easy to overstate: as of this writing, none of the three is listed in CISA's Known Exploited Vulnerabilities catalog. There's no confirmed in-the-wild campaign. What does exist is a public, detailed root-cause writeup from CNCF and Sysdig, plus an experimental Falco detection rule for exactly this symlink-race pattern — which is the realistic threat model for a bug this well-documented: not a zero-day in active use, but a known recipe anyone can build a proof-of-concept from once they've read the advisory.
Who can actually pull the trigger
Sysdig's researchers were specific about the precondition: exploitation requires "the ability to start containers with custom mount configurations." That's not root on the host, and it's not even a privileged container — it's the ability to supply an OCI runtime config or a Dockerfile with a RUN --mount=... line that runc will honor. A malicious base image or a poisoned upstream layer gets you there just as well as a hand-crafted config.
That precondition should land differently depending on what your fleet actually runs. If every workload on a node is an internal image your own team built and pushed, the exploitable surface is "did a dependency get compromised" — real, but one step removed. If your platform's entire job is building and running containers from arbitrary tenant-submitted source — which is exactly what a self-hosted PaaS like Bex.co does with a git push — then "can a tenant supply a custom mount configuration" isn't a hypothetical, it's the product. A build pipeline that turns a stranger's repository into a running container is the attacker-controlled-mount-config precondition, by design. That's the concrete reason "which container runtime is under the hood, and is it patched" belongs in a self-hosted PaaS's own security documentation rather than in a CVE feed nobody on the team reads.
Does switching runtimes save you
It's a reasonable instinct: if runc has a masked-path bug, wouldn't crun — an independent, from-scratch C implementation of the same OCI runtime spec — just not have it? No. crun and youki have their own, structurally similar masked-path and mount-ordering logic, and both projects were looped into the coordinated disclosure alongside runc. crun's fix landed in its 1.21 release series, shipped within days of the November 5 advisory — which means a fleet that "solved" this by standardizing on crun instead of runc still had a patch to apply, just under a different package name. Same bug class, same urgency, different binary.
The one runtime that's genuinely unaffected by this specific bug class is gVisor's runsc, and the reason is architectural rather than a patch: gVisor's Sentry intercepts every syscall in userspace and never lets a sandboxed process open or receive a donated host file descriptor, so there's no exec-into-namespace handoff for a symlink race to exploit in the first place. That's a genuinely different security model — worth knowing if you're evaluating gVisor for isolating untrusted workloads (AI-agent sandboxes are the case that comes up most often) — but it's not a drop-in swap for a Cluster API-managed Kubernetes fleet built around standard OCI runtimes, and it comes with its own performance and compatibility tradeoffs. For everyone still running runc or crun as their kubelet's actual runtime, the only fix is the version bump, not a runtime swap.
Even the hyperscalers took months
If you're tempted to assume a managed Kubernetes offering closed this out on day one, the public record says otherwise. Google didn't ship the fix into GKE's Container-Optimized OS node images for the 1.35 and 1.36 release lines until June 20, 2026 — over seven months after the November 5, 2025 disclosure. AWS and Azure both tracked the same three CVEs through their own security-bulletin processes (AWS-2025-024, and an open Azure/AKS GitHub issue users had to watch for a resolution) rather than shipping an immediate hotfix. None of that is a knock on those teams — coordinating a runc/containerd bump across every supported node-image variant, region, and Kubernetes minor version is genuinely slow, careful work. It's the point: if the vendors with dedicated security-response teams and a financial incentive to move fast took months, a self-hosted fleet nobody's paging on this can't assume its node image quietly absorbed the fix on schedule. The only way to know is to check the running version yourself.
The patch checklist for a Cluster-API-managed fleet
- Check what's actually on the node, not what your provisioning script says it installed:
runc --versionandcontainerd --version(orcrictl infofor the containerd build in use) on a live node. Provisioning intent and running reality drift, especially on nodes that predate a given image revision. - Confirm against the safe versions: runc ≥ 1.2.8, 1.3.3, or 1.4.0-rc.3; containerd ≥ 1.6.39 or 1.7.28-2 (both pull in the fixed runc). crun fleets need ≥ 1.21.
- Bump the node image, not just the running binary. For a kubeadm/Cluster API Provider-bootstrapped fleet, that means rebuilding the node image (
image-builder's Packer/Ansible pipeline, or your distro's equivalent) with the patched package pinned, then rolling it out via yourMachineDeployment's rolling-update strategy — not hand-patching binaries on running nodes, which won't survive the next node replacement. - Don't assume your base OS caught it for you. Distro and container-OS vendors have a track record of lagging behind upstream runc security releases by months, not days — Talos Linux, for one, shipped a runc still vulnerable to an earlier container-escape CVE (2024-21626) for several release cycles before patching it in v1.5.6/1.6.4. Verify your specific node image's changelog names this CVE; don't infer it from a generic "security updates" bullet point.
- If you can't patch immediately, deploy the Falco rule Sysdig published for this bug class as a detection stopgap — it doesn't close the hole, but it turns a silent exploit attempt into an alert.
Put the answer in your security docs, not just your changelog
The underlying lesson generalizes past this specific CVE trio: "we use containers" is not a security answer, and neither is "we use Kubernetes." A tenant — or a security-conscious prospect — asking about isolation deserves a concrete answer: which OCI runtime is actually running their workload, what version, and how patch lag on that specific component is tracked and closed. For a platform whose core function is turning arbitrary pushed code into running containers, that answer needs to be a documented, current fact, not a plausible-sounding generality reconstructed under pressure during an incident review.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, orchestrated by Cluster API on hardware you actually control. Star the repo on GitHub or deploy your first app today.