A six-instruction crack in the wall that lets any tenant grab the landlord's master key
CVE-2026-46242 — dubbed Bad Epoll — is a use-after-free race condition in the Linux kernel's epoll subsystem, specifically in ep_remove(). When two chained epoll file descriptors are closed concurrently, ep_remove_file() clears file->f_ep under file->f_lock but continues using the same struct file inside the critical section. A concurrent __fput() observes the transient NULL, runs ep_eventpoll_release() → ep_free(), and kfree()s the watched struct eventpoll. The subsequent hlist_del_rcu() then writes into freed kmalloc-192 slab memory. Affected kernels span 6.4 through 6.18.32, 6.19 through 7.0.9, and certain stable-backport branches (5.15.209–5.16, 6.1.175–6.2). The fix — pinning @file via epi_fget() at the top of ep_remove() — landed upstream on April 24, 2026 (commit a6dc643c6931), five weeks before public disclosure on May 30.
The vendor's HIGH / 7.8 score accurately captures the base-score math — local access, low privileges, full C/I/A — but dramatically undersells the real-world risk. Researcher Jaeyoung Chung (Seoul National University) published a weaponized exploit through Google's kernelCTF program that achieves 99% reliability on LTS 6.12 and 98% on Container-Optimized OS, despite a race window only six instructions wide. The exploit chains four linked epoll FDs, leaks kernel addresses through /proc/self/fdinfo, and pivots into a ROP chain that drops a root shell. Critically, it works from inside Chrome's renderer sandbox and on Android 6.4+ devices. There is no workaround — epoll is compiled into the kernel and cannot be disabled. Every Linux 6.4+ host running without the patch is one ./bad_epoll invocation from root.
5 steps from start to impact.
Obtain unprivileged local code execution
epoll_create, epoll_ctl, and close, which are available to all userspace processes including sandboxed ones.- Local or sandboxed code execution on a Linux 6.4+ host
- Requires initial foothold — SSH, container tenant, web shell, or browser RCE chain
Chain four epoll file descriptors
close() calls on the linked FDs to race ep_remove() against __fput(). The six-instruction race window is widened via scheduling manipulation and a retry loop that never crashes the kernel, yielding ~99% hit rate.- Ability to call epoll_create/epoll_ctl/close (unprivileged)
- Kernel 6.4+ with vulnerable ep_remove() code path
- None — epoll cannot be disabled or restricted; seccomp profiles rarely block epoll syscalls
Leak kernel addresses via /proc/self/fdinfo
struct eventpoll slab is reclaimed with attacker-controlled data. The exploit reads back kernel pointers through /proc/self/fdinfo on the surviving epoll FD, defeating KASLR without any separate info-leak primitive. This gives the attacker the kernel text base and slab addresses needed for the ROP chain.- Successful UAF trigger from step 2
- Access to /proc/self/fdinfo (available to all processes)
- Kernel hardening (e.g.,
kptr_restrict=2) limits some /proc leaks, but the exploit reads through the epoll fdinfo handler which exposes internal struct fields regardless
Hijack control flow via ROP chain
commit_creds(prepare_kernel_cred(0)) or equivalent, granting the attacker's task uid=0. The process then spawns a root shell.- Kernel base address from step 3
- Controlled data in freed slab from step 2
- SMEP/SMAP are not bypassed by ret2user — the ROP chain stays in kernel text, so these mitigations do not block the exploit
Pivot from root to lateral movement
- Root shell from step 4
- Post-exploitation detection by EDR/HIDS is the last line of defense
kernel.unprivileged_userns_clone=0 (Debian/Ubuntu) or user.max_user_namespaces=0 (RHEL). While this does not block the core epoll race, it limits some exploit stabilization techniques that rely on namespace isolation for heap shaping. This is a partial friction increase, not a fix. Deploy within 3 days alongside patch scheduling.epoll_create1 and epoll_create. Warning: This will break most real applications — nginx, Node.js, Go, Java, Python asyncio all require epoll. Only viable for extremely constrained single-purpose containers that use poll/select instead. Test thoroughly before deploying.- Network segmentation / firewalls: This is a local privilege escalation — network controls are irrelevant to the attack path. The attacker is already on the host.
- SELinux / AppArmor in default profiles: Default MAC policies do not restrict epoll syscalls or /proc/self/fdinfo access. A targeted custom policy could theoretically block fdinfo reads, but this would break application monitoring and health checks.
- KASLR alone: The exploit defeats KASLR by reading kernel pointers through the epoll fdinfo handler on the corrupted FD. KASLR provides zero additional friction.
- Container isolation (namespaces/cgroups): The exploit escapes container boundaries by escalating to root on the host kernel. Container isolation is the thing being bypassed, not a defense against this bug.
The supporting signals.
| In-the-wild status | No confirmed in-the-wild exploitation as of 2026-10-03. Not listed in CISA KEV. No GreyNoise tags or scanning activity observed (local-only attack vector limits network-visible telemetry). |
|---|---|
| PoC availability | Weaponized public exploit by Jaeyoung Chung (Seoul National University), submitted to Google kernelCTF ($71,337 bounty). Published July 14, 2026. Achieves 99% reliability on LTS 6.12.67, 98% on COS-121. GitHub repos: 0xBlackash/CVE-2026-46242, Baba01hacker666/CVE-2026-46242. Additionally listed in SecureWithUmer/CVE-2026-PoCs collection. |
| EPSS score | 0.00176 (user-reported). Other sources report 3.15% / 87th percentile — discrepancy may reflect model revision date. Either way, EPSS significantly underweights local kernel LPE with public weaponized exploit. |
| CVSS vector | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — 7.8 HIGH. The Scope:Unchanged is debatable given container/sandbox escape capability, which would push to Scope:Changed and ~8.8. |
| Affected versions | Linux kernel 6.4 – 6.18.32, 6.19 – 7.0.9. Backport-affected: 5.15.209 – 5.16, 6.1.175 – 6.2. Bug introduced April 2023. Android kernels 6.4+ affected; Pixel 8 (6.1) not affected. |
| Fixed versions | Upstream: 6.18.33, 7.0.10. Stable backports: 6.12.95, 6.6.144, 6.1.178. Debian: trixie 6.12.111-1, forky/sid 7.2.8-1 (bullseye/bookworm not affected). Ubuntu: 26.04 7.0.0-28.28, 24.04 6.8.0-137.137, 22.04 various HWE kernels. |
| Sandbox/container escape | Confirmed exploitable from Chrome renderer sandbox (seccomp + namespaces). Container escape on Kubernetes nodes gives attacker root on the node OS, access to kubelet creds and all co-located pods. |
| Scanning/exposure data | Not applicable for network scanning (AV:L). However, Shodan/Censys Linux kernel fingerprinting shows tens of millions of hosts running affected 6.4+ kernels. Any multi-tenant Linux environment is a target. |
| Disclosure timeline | Reported privately: February 2026. Fix merged: April 24, 2026 (commit a6dc643c6931). CVE published: May 30, 2026. Public exploit released: July 14, 2026. |
| Researcher | Jaeyoung Chung, Seoul National University Computer Security Lab. Submitted as 0-day to Google kernelCTF, awarded $71,337 bounty. |
Sources.
- NVD — CVE-2026-46242 Detail
- SecurityOnline — Public Exploit and Details Released for Bad Epoll Root Flaw
- Zero Hunt — Bad Epoll: A 6-instruction race that hands any Linux user root
- GitHub Advisory Database — GHSA-h4r8-4j67-99mx
- Ubuntu Security — CVE-2026-46242
- Debian Security Tracker — CVE-2026-46242
- SecurityWeek — Proof-of-Concept Exploit Released for Linux Bad Epoll Root Access Vulnerability
- TuxCare — Bad Epoll CVE Analysis
Why this verdict
- Public weaponized exploit with 99% reliability: The kernelCTF submission by Jaeyoung Chung is not a crash PoC — it is a fully weaponized root exploit with a retry loop that never panics the kernel. Weaponization cost for any attacker is zero; they download and compile. This alone elevates risk well above a theoretical 7.8.
- No workaround exists:
epollis compiled into the kernel and used by virtually every userspace application (systemd, nginx, Node.js, containerd, Chrome). It cannot be disabled, module-blocked, or restricted via seccomp without breaking the host. The only fix is the patch. - Sandbox and container escape: The exploit is confirmed to work from inside Chrome's renderer sandbox and from unprivileged container processes. On a Kubernetes node, any compromised pod tenant can escalate to node root, dump kubelet credentials, and pivot across the cluster. This transforms a LOW-value container compromise into fleet-scale impact.
- Role multiplier: The Linux kernel is the canonical component for every high-value role — KVM/QEMU hypervisor hosts (VM escape to hypervisor root), Kubernetes nodes (container escape to node root → cluster pivot), CI/CD runners (build job → root → supply-chain poisoning), production database servers (app compromise → root → data exfiltration), and backup infrastructure. ≥10% of Linux 6.4+ installs serve these roles by definition. The blast radius from a single compromised container host is fleet-scale (all co-located workloads, cluster credentials, lateral network access). This floors the verdict at CRITICAL per the high-value-role rule.
- Universal attack surface: Every Linux 6.4+ host — server, desktop, Android device, cloud VM, container node — is vulnerable. The affected population numbers in the tens of millions. The prerequisite is merely 'be a local user,' which includes every container tenant, every CI job, every PHP/Node/Python application with an RCE, and every Chrome tab if chained with a renderer bug.
Why not higher?
A 9.0 already reflects the maximum practical severity for a local privilege escalation with no remote attack vector. A score above 9.0 would require unauthenticated remote exploitation (AV:N). While the exploit is devastating once local access is obtained, the initial foothold is still a prerequisite the attacker must solve independently. The absence of confirmed in-the-wild exploitation and KEV listing also argue against pushing to 9.5+.
Why not lower?
Downgrading below CRITICAL is not defensible. The Linux kernel is the canonical high-value-role component — it IS the hypervisor, the container runtime host, the CI runner OS. The chain succeeds in every one of these roles with fleet-scale blast radius. A 99% reliable public exploit with no kernel crash, no workaround, and sandbox escape capability means the exploitation barrier is effectively zero for any attacker with local access. The 'requires local access' friction does not break the CRITICAL floor because local access is the *default state* for container tenants, CI jobs, and multi-user servers — the very environments where this bug is most dangerous.
Crowdsourced verification payload.
Run this script on each target Linux host as any user (no root needed). Invoke with bash check_bad_epoll.sh. It compares the running kernel version against known-fixed versions and reports VULNERABLE, PATCHED, or UNKNOWN.
#!/bin/bash
# check_bad_epoll.sh — CVE-2026-46242 (Bad Epoll) version checker
# Run on target Linux host. No root required.
# Exit codes: 1=VULNERABLE, 0=PATCHED, 2=UNKNOWN
set -euo pipefail
KERNEL=$(uname -r)
echo "[*] Running kernel: $KERNEL"
# Extract major.minor.patch (strip distro suffixes)
VERSION=$(echo "$KERNEL" | grep -oP '^[0-9]+\.[0-9]+\.[0-9]+')
if [[ -z "$VERSION" ]]; then
echo "UNKNOWN — could not parse kernel version from: $KERNEL"
exit 2
fi
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3)
ver_gte() {
# Returns 0 if $MAJOR.$MINOR.$PATCH >= $1.$2.$3
local m1=$1 m2=$2 m3=$3
if (( MAJOR > m1 )); then return 0; fi
if (( MAJOR == m1 && MINOR > m2 )); then return 0; fi
if (( MAJOR == m1 && MINOR == m2 && PATCH >= m3 )); then return 0; fi
return 1
}
ver_lt() {
! ver_gte "$1" "$2" "$3"
}
# Kernels < 5.15.209: not affected (vulnerable code not present)
if ver_lt 5 15 209; then
echo "PATCHED — kernel $VERSION predates vulnerable code (introduced ~5.15.209/6.4)"
exit 0
fi
# 5.15.209 – 5.15.x: check if >= 5.15.x fixed (not affected per Debian, code not present in 5.10/6.1 base)
# 6.1.175 – 6.1.x: fixed in 6.1.178
if (( MAJOR == 6 && MINOR == 1 )); then
if (( PATCH >= 175 && PATCH < 178 )); then
echo "VULNERABLE — kernel $VERSION is in affected 6.1.175-6.1.177 range (fixed in 6.1.178)"
exit 1
elif (( PATCH >= 178 )); then
echo "PATCHED — kernel $VERSION includes fix (6.1.178+)"
exit 0
else
echo "PATCHED — kernel $VERSION predates vulnerable backport in 6.1 branch"
exit 0
fi
fi
# 6.2 – 6.3.x: affected, no stable fix (upgrade to 6.6.144+)
if (( MAJOR == 6 && (MINOR == 2 || MINOR == 3) )); then
echo "VULNERABLE — kernel $VERSION is in affected 6.2-6.3 range (upgrade to 6.6.144+)"
exit 1
fi
# 6.4 – 6.6.x: fixed in 6.6.144
if (( MAJOR == 6 && MINOR >= 4 && MINOR <= 6 )); then
if (( MINOR == 6 && PATCH >= 144 )); then
echo "PATCHED — kernel $VERSION includes fix (6.6.144+)"
exit 0
else
echo "VULNERABLE — kernel $VERSION is in affected range (fixed in 6.6.144)"
exit 1
fi
fi
# 6.7 – 6.12.x: fixed in 6.12.95
if (( MAJOR == 6 && MINOR >= 7 && MINOR <= 12 )); then
if (( MINOR == 12 && PATCH >= 95 )); then
echo "PATCHED — kernel $VERSION includes fix (6.12.95+)"
exit 0
else
echo "VULNERABLE — kernel $VERSION is in affected range (fixed in 6.12.95)"
exit 1
fi
fi
# 6.13 – 6.18.x: fixed in 6.18.33
if (( MAJOR == 6 && MINOR >= 13 )); then
if (( MINOR == 18 && PATCH >= 33 )); then
echo "PATCHED — kernel $VERSION includes fix (6.18.33+)"
exit 0
elif (( MINOR > 18 )); then
echo "PATCHED — kernel $VERSION is past affected range"
exit 0
else
echo "VULNERABLE — kernel $VERSION is in affected range (fixed in 6.18.33)"
exit 1
fi
fi
# 7.0.x: fixed in 7.0.10
if (( MAJOR == 7 && MINOR == 0 )); then
if (( PATCH >= 10 )); then
echo "PATCHED — kernel $VERSION includes fix (7.0.10+)"
exit 0
else
echo "VULNERABLE — kernel $VERSION is in affected 7.0.0-7.0.9 range"
exit 1
fi
fi
# 7.1+: fixed
if ver_gte 7 1 0; then
echo "PATCHED — kernel $VERSION is past all affected ranges"
exit 0
fi
# 5.15.209 – 5.15.x / 5.16.x edge cases
if (( MAJOR == 5 && MINOR == 15 && PATCH >= 209 )); then
echo "VULNERABLE — kernel $VERSION may contain backported vulnerable code (verify with distro advisory)"
exit 1
fi
if (( MAJOR == 5 && MINOR == 16 )); then
echo "VULNERABLE — kernel $VERSION is in affected 5.16 range"
exit 1
fi
if (( MAJOR == 6 && MINOR == 0 )); then
echo "PATCHED — kernel $VERSION predates vulnerable code in 6.x branch"
exit 0
fi
echo "UNKNOWN — kernel $VERSION not in known affected/fixed ranges; verify manually"
exit 2