Jaeyoung Chung's kernelCTF "Bad Epoll": a race-condition use-after-free in fs/eventpoll.c. ep_remove() clears file->f_ep under f_lock but keeps using the file (hlist_del_rcu + unlock) while a concurrent __fput() frees the still-referenced struct eventpoll. Reachable by any unprivileged user with no userns / CONFIG / capability; weaponised via cross-cache to a struct file, /proc/self/fdinfo arb-read, and ROP. Introduced 58c9b016e128 (6.4), fixed a6dc643c6931 (7.1-rc1), stable backport 7.0.13. CWE-416; not in KEV. The corpus's first epoll / VFS-teardown module. Shipped as a reconstructed, deliberately under-driven trigger on the stackrot / nft_catchall contract: detect() is a pure version gate (>= 6.4 and below the on-branch fix; no userns precondition -- epoll needs none); exploit() forks a CPU-pinned child that exercises the ep_remove-vs-__fput close window a hard-bounded 48 attempts / 2s and returns EXPLOIT_FAIL. It does not grind the race to a win, does not do the cross-cache reclaim, and does not bundle the fdinfo R/W + ROP (a won race frees a live struct file and rarely trips KASAN -> silent-corruption risk). Wired: registry, Makefile, safety rank (12 -- lowest in the corpus; a kernel race is the least predictable class), 5 detect() test rows (version gating), CVE metadata (sorted insert, CWE-416 / T1068 / not-KEV), README + CVES.md + website counts (44/39), RELEASE_NOTES v0.9.12, and a verify-vm target (sweep pending). Detection rules are intentionally weak/structural (epoll ubiquitous, rarely KASAN) -- post-exploitation euid-0 transition, no yara. Also corrects pre-existing README drift in the "not yet verified" count (8 -> 11). Not VM-verified, so the verified count stays 28 of 39. Version 0.9.12. Credit: Jaeyoung Chung (J-jaeyoung). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n
3.6 KiB
NOTICE — bad_epoll (CVE-2026-46242)
Vulnerability
CVE-2026-46242 — "Bad Epoll", a race-condition use-after-free in
the Linux kernel epoll subsystem (fs/eventpoll.c). On the file-teardown
path, ep_remove() clears file->f_ep under file->f_lock but continues
to use the file inside the critical section (hlist_del_rcu() over the
eventpoll refs list + spin_unlock()). A concurrent __fput() of a
linked epoll file observes the transient NULL f_ep, skips
eventpoll_release_file(), and proceeds to f_op->release, freeing a
struct eventpoll still in use → UAF.
The bug is reachable by any unprivileged local user — epoll_create1,
epoll_ctl, and close require no capability, no user namespace, and no
special kernel config. Exploitation converts the 8-byte UAF write into
control of a struct file via a cross-cache attack, gains arbitrary
kernel read through /proc/self/fdinfo, and ROPs to a root shell —
roughly 99% reliable despite a ~6-instruction race window. It also affects
Android. NVD class: CWE-416 (Use After Free), with a CWE-362 race
root cause. Not in CISA KEV (brand new).
Research credit
- Discovery, exploitation, and public PoC by Jaeyoung Chung
(GitHub
J-jaeyoung), submitted as a zero-day to Google's kernelCTF program. Repository: https://github.com/J-jaeyoung/bad-epoll and the kernelCTF submission underJ-jaeyoung/security-research(CVE-2026-46242_lts_cos, targetlts-6.12.67). SKELETONKEY's trigger reconstruction is informed by that public PoC (the epoll object graph and theep_remove-vs-__fputclose-race shape only — no offsets or ROP are reused). - Introduced by commit
58c9b016e128(Linux 6.4, 2023-04-08). - Fixed upstream by commit
a6dc643c69311677c574a0f17a3f4d66a5f3744b, merged for 7.1-rc1 (2026-04-24); stable backport 7.0.13. - Debian security tracker (authoritative backport versions):
https://security-tracker.debian.org/tracker/CVE-2026-46242 — forky
7.0.13-1/ sid7.0.14-1fixed; trixie 6.12.x still vulnerable at time of writing; bookworm 6.1 and bullseye 5.10 "not affected — vulnerable code not present".
All credit for finding, analysing, and exploiting this bug belongs to Jaeyoung Chung and to the upstream maintainers who fixed it. SKELETONKEY is the bundling and bookkeeping layer only.
SKELETONKEY role
🟡 Trigger (reconstructed) — primitive-only, not VM-verified. This is
the corpus's first epoll / VFS-file-teardown module and its cleanest
example of an SMP kernel race, shipped on the same "fire the bug class and
stop" contract as stackrot (CVE-2023-3269) and nft_catchall
(CVE-2026-23111).
detect() is a pure kernel-version gate (vulnerable iff >= 6.4 and below
the fix on-branch; stable backport 7.0.13, 7.1+ inherits; 6.1/5.10 not
affected) — no userns or CONFIG precondition, because none is required.
exploit() forks a CPU-pinned child that builds the epoll race pair and
exercises the ep_remove-vs-__fput concurrent-close window a
hard-bounded number of times (48 attempts / 2 s), widening it with
close(dup()) false-sharing storms, snapshots the eventpoll slab, and
returns EXPLOIT_FAIL.
It is deliberately under-driven: a won race frees a live
struct eventpoll (real corruption that rarely trips KASAN), so the module
does not grind the race to a win, does not perform the cross-cache reclaim,
and does not bundle the /proc/self/fdinfo arbitrary-read + ROP root-pop
(per-build offsets refused). The trigger is reconstructed from the public
kernelCTF PoC, not VM-verified — it never claims root it did not get. It
carries the lowest --auto safety rank in the corpus.