A booby-trapped shipping container that can only scratch the paint on the loading dock — not steal the cargo
CVE-2026-41579 is a symlink-following bug in runc's rootfs setup. When runc processes a container image where /dev is a symlink to somewhere else, setupPtmx and setupDevSymlinks naively call os.Remove and os.Symlink against filepath.Join(rootfs, '/dev/ptmx') — which, because filepath.Join resolves symlinks poorly, ends up pointing at a host directory. The attacker gets to delete a file named ptmx in any pre-existing host directory or drop a fixed set of symlinks (fd, stdin, stdout, stderr, core) with hardcoded targets. Affected: runc < 1.3.6, < 1.4.3, < 1.5.0-rc.3.
The vendor's LOW / 3.3 (AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N) is *broadly* correct: no confidentiality loss, no availability loss, no container escape, no code execution — just a narrow integrity primitive exercised by a chosen image at container-start time. But 3.3 undersells the practical picture slightly. Docker masks the flaw with its top-level ro overlay, but Podman and containerd do not, and rootless-Podman users are pulling untrusted images all day. A well-chosen symlink target (e.g. clobbering a systemd unit's ptmx sentinel, or dropping symlinks into a config-scan directory) is a plausible local persistence / tampering primitive. We nudge to MEDIUM (4.5) to reflect the Podman/containerd exposure gap — still nowhere near HIGH.
4 steps from start to impact.
Attacker publishes malicious OCI image
/dev is a symbolic link to an absolute host path such as /etc or /var/lib/kubelet. Standard tools (buildah, crane, hand-rolled tar) can produce this — the OCI spec does not forbid it. The image is pushed to a public registry or seeded into an internal mirror.- Attacker can publish or influence an image the victim will pull
- Victim uses Podman, containerd, CRI-O, or another runc-based runtime that does NOT overlay-mask /dev
- Docker's top-level ro layer masks the malicious /dev symlink — Docker installs are not exploitable
- Image-signing/
cosignpolicies block unsigned pulls in mature environments - Registry allowlists in Kubernetes admission (Kyverno/OPA) can block unknown registries
/dev symlinks.Victim pulls and runs the image on vulnerable runc
runc create invokes rootfs setup.- User interaction (
UI:R) — someone or some automation mustrunthe image - Runtime is Podman/containerd/CRI-O with vulnerable runc
- Kubernetes clusters on managed platforms (EKS/GKE/AKS) get runc patched via node AMI refresh within weeks
- Rootless Podman still runs as the user — host damage limited to what that UID can touch
runc follows /dev symlink during setupPtmx / setupDevSymlinks
setupPtmx calls os.Remove(filepath.Join(rootfs, '/dev/ptmx')). Because /dev inside rootfs is a symlink to e.g. /etc, the resolved path is /etc/ptmx on the host. Any pre-existing host file at that path is deleted. setupDevSymlinks then creates the fixed set of fd/stdin/stdout/stderr/core symlinks in that same host directory.- runc runs with sufficient privilege (root for system Podman/containerd; user UID for rootless)
- Symlink targets and names are hardcoded — attacker can pick the directory but not arbitrary content
- SELinux/AppArmor container profiles may deny the write when the resolved path crosses the container label
-w /etc -p wa catches the host write. Falco rules on open_by_handle_at outside container mount namespace fire.Attacker leverages the primitive
ptmx sentinel file used by a host service, (b) drop symlinks in a directory scanned by a systemd generator, cron, or config-reload watcher to influence host behavior, (c) tamper with monitoring/EDR state files. This is not RCE and not container escape — it is a targeted host-filesystem tamper primitive.- A host process must later read/act on the tampered directory contents
- Fixed symlink names (
fd,stdin,stdout,stderr,core) drastically limit which host processes are influenced - Most sensitive directories (
/etc/shadow,/root/.ssh) do not use those filenames
/etc, /usr/local/etc, /var/lib catches the drop-in.The supporting signals.
| In-the-wild exploitation | None observed as of 2026-07-01. Disclosed same day; no active campaigns. |
|---|---|
| Proof-of-concept | GHSA-xjvp-4fhw-gc47 advisory includes the technical write-up; no public weaponized PoC repo yet. Trivial to reproduce from the advisory description. |
| EPSS | Not yet scored (disclosure day). Expected <1% — image-borne, requires user pull. |
| KEV status | Not listed. Unlikely to be added — impact scope too narrow. |
| CVSS | CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N — 3.3 LOW. Local, user interaction required, integrity-only. Vector is accurate. |
| Affected versions | runc < 1.3.6, < 1.4.3, and < 1.5.0-rc.3 |
| Fixed versions | runc 1.3.6, 1.4.3, 1.5.0-rc.3. Debian/Ubuntu/RHEL backports rolling out via runc, containerd.io, podman package channels. |
| Runtime exposure carve-out | Docker: not exploitable (top-level ro layer masks malicious /dev symlink). Podman, containerd, CRI-O: exploitable when using unpatched runc. |
| Disclosure | 2026-07-01 via opencontainers/runc GitHub Security Advisory GHSA-xjvp-4fhw-gc47 |
| Reporter | opencontainers runc maintainers (Aleksa Sarai and team) — coordinated disclosure via GHSA |
noisgate verdict.
The decisive factor is impact ceiling: this is a narrow host-filesystem integrity primitive (deleting files named ptmx and dropping a fixed set of symlinks) — not RCE, not container escape, not confidentiality loss. We nudge above the vendor's LOW because Podman and containerd users lack Docker's masking overlay, and container runtimes are canonically high-value-role components — but the primitive itself is too constrained to justify HIGH.
Why this verdict
- Impact is integrity-only and shape-constrained. The primitive is delete-a-file-named-ptmx and drop-a-fixed-set-of-symlinks — attacker picks the directory, not the payload. That is not a general-purpose write.
- Docker is out of scope. Docker's top-level ro layer masks the malicious /dev symlink. In a fleet where Docker dominates the desktop and CI footprint, exposure is materially smaller than the runc install base suggests.
- User interaction required.
UI:Rmeans someone or something must pull and run the malicious image. Registry allowlists,cosignpolicy, and Kyverno/OPA admission cut the exposed population further. - Role multiplier — container runtime on Kubernetes nodes: the affected component IS a canonical high-value-role component (runs under kubelet/containerd on every worker). Chain succeeds under containerd/CRI-O, but blast radius is capped at *host filesystem tamper*, not node takeover or cluster pivot. That prevents downgrade to LOW but does not lift us to HIGH.
- Role multiplier — developer workstation with Podman: chain succeeds, blast radius is single-user files. Adds a MEDIUM-amplifier signal, not a floor.
Why not higher?
There is no confidentiality impact, no availability impact, no code execution, and no namespace/cgroup escape. The primitive cannot be turned into RCE without a second, unrelated vuln in a host process that consumes the tampered files. HIGH is reserved for chains that end in host or cluster compromise on their own.
Why not lower?
Vendor LOW (3.3) underweights the Podman/containerd exposure gap and the role-multiplier fact that runc runs on every Kubernetes node. In multi-tenant Kubernetes and shared build environments, a malicious image is a realistic threat model — LOW invites deprioritization past the 1-year mark, which is too long for a runtime component this central.
What to do — in priority order.
- Enforce signed-image admission (cosign + Kyverno/OPA Gatekeeper) — Blocks the delivery vector at the door — malicious untrusted images cannot reach a vulnerable runc. Deploy within the 365-day remediation window; no mitigation SLA applies at MEDIUM, but this control has payoff far beyond this CVE.
- Pin container runtime to Docker where feasible on shared multi-tenant hosts — Docker's overlay masks the /dev symlink and makes the flaw unexploitable. Only a valid interim on hosts where Podman/containerd isn't functionally required.
- Enable SELinux
container_tor AppArmorrunc-defaultprofiles in enforcing mode — MAC policy denies the cross-namespace host write even when runc mis-resolves the path. Verify withsestatus/aa-statusand audit denials viaausearch -m AVC. - Add auditd + FIM watches on
/etc,/usr/local/etc, and container-config directories —auditd -w /etc -p wa -k runc-symlinkplus AIDE/Wazuh FIM catches the primitive if it fires. Detection floor while you roll patched runc. - Roll patched runc (1.3.6 / 1.4.3 / 1.5.0-rc.3) via container runtime package upgrade — Update
containerd.io,podman, or vendor node image. Kubernetes node fleets: rebuild AMIs / node images and cordon-drain-replace. Complete within the 365-day noisgate remediation window; prioritize multi-tenant clusters.
- Rootless Podman alone — reduces blast radius to the user UID but the primitive still fires; user-writable directories can still be tampered.
- Network segmentation / WAF / EDR alerting on network traffic — the attack is entirely local to container-start; no network component to inspect.
- Read-only root filesystem inside the container — the vulnerable operation is runc's setup of the container, before the container's mount is finalized; the container's own ro flag doesn't apply.
- Seccomp default profiles — the syscalls used (
symlink,unlink) are permitted by default profiles because they're required for normal rootfs setup.
Crowdsourced verification payload.
Run on each Linux host that executes containers (Kubernetes nodes, build agents, developer workstations). Requires read access to the runc binary — no root needed. Invoke as ./check-cve-2026-41579.sh (optionally ./check-cve-2026-41579.sh /usr/local/sbin/runc to override the binary path).
#!/usr/bin/env bash
# check-cve-2026-41579.sh — detect vulnerable runc versions
# Fixed in: 1.3.6, 1.4.3, 1.5.0-rc.3
# Exit: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
RUNC_BIN="${1:-$(command -v runc 2>/dev/null)}"
if [ -z "${RUNC_BIN}" ] || [ ! -x "${RUNC_BIN}" ]; then
# Try common containerd-bundled paths
for p in /usr/bin/runc /usr/sbin/runc /usr/local/bin/runc /usr/local/sbin/runc /run/torcx/bin/runc; do
[ -x "$p" ] && RUNC_BIN="$p" && break
done
fi
if [ -z "${RUNC_BIN}" ] || [ ! -x "${RUNC_BIN}" ]; then
echo "UNKNOWN: runc binary not found"
exit 2
fi
RAW=$("${RUNC_BIN}" --version 2>/dev/null | head -n1)
# Expect: 'runc version 1.2.5' or 'runc version 1.5.0-rc.3'
VER=$(echo "${RAW}" | awk '{print $3}')
if [ -z "${VER}" ]; then
echo "UNKNOWN: could not parse version from: ${RAW}"
exit 2
fi
# Split into major.minor.patch and optional -rc.N
CORE=${VER%%-*}
SUFFIX=${VER#${CORE}}
IFS='.' read -r MAJ MIN PAT <<< "${CORE}"
MAJ=${MAJ:-0}; MIN=${MIN:-0}; PAT=${PAT:-0}
vuln=1 # assume vulnerable
case "${MAJ}.${MIN}" in
1.3)
[ "${PAT}" -ge 6 ] && vuln=0
;;
1.4)
[ "${PAT}" -ge 3 ] && vuln=0
;;
1.5)
if [ "${PAT}" -ge 0 ]; then
# 1.5.0-rc.3 or later is fixed; 1.5.0-rc.1/rc.2 are vulnerable
if [ -z "${SUFFIX}" ]; then
vuln=0 # 1.5.0 GA
else
RC=$(echo "${SUFFIX}" | sed -n 's/^-rc\.\([0-9]\+\).*/\1/p')
[ -n "${RC}" ] && [ "${RC}" -ge 3 ] && vuln=0
fi
fi
;;
*)
# 1.6+ or 2.x — assume fixed
if [ "${MAJ}" -gt 1 ] || { [ "${MAJ}" -eq 1 ] && [ "${MIN}" -gt 5 ]; }; then
vuln=0
fi
;;
esac
if [ ${vuln} -eq 0 ]; then
echo "PATCHED: ${RUNC_BIN} version ${VER}"
exit 0
else
echo "VULNERABLE: ${RUNC_BIN} version ${VER} — upgrade to 1.3.6 / 1.4.3 / 1.5.0-rc.3+"
exit 1
fi
If you remember one thing.
Sources
- GHSA-xjvp-4fhw-gc47 (runc security advisory)
- Miggo vulnerability database — CVE-2026-41579
- Debian security tracker — CVE-2026-41579
- Amazon Linux ALAS — CVE-2026-41579
- GitLab advisory database — CVE-2026-41579
- OffSeq Threat Radar — CVE-2026-41579
- opencontainers/runc releases
- Vulnerability-Lookup (CIRCL) — GHSA-XJVP-4FHW-GC47
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.