Files
KaraZajac 95589e26cb
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
modules: add bad_epoll (CVE-2026-46242, "Bad Epoll" epoll teardown race UAF)
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
2026-07-04 19:35:33 -04:00

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 userepoll_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 under J-jaeyoung/security-research (CVE-2026-46242_lts_cos, target lts-6.12.67). SKELETONKEY's trigger reconstruction is informed by that public PoC (the epoll object graph and the ep_remove-vs-__fput close-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 / sid 7.0.14-1 fixed; 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.