modules: add bad_epoll (CVE-2026-46242, "Bad Epoll" epoll teardown race UAF)
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

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
This commit is contained in:
KaraZajac
2026-07-04 19:35:33 -04:00
parent 4d0a0e2443
commit 95589e26cb
17 changed files with 787 additions and 22 deletions
+44
View File
@@ -72,6 +72,7 @@ extern const struct skeletonkey_module ptrace_pidfd_module;
extern const struct skeletonkey_module sudo_host_module;
extern const struct skeletonkey_module cifswitch_module;
extern const struct skeletonkey_module nft_catchall_module;
extern const struct skeletonkey_module bad_epoll_module;
static int g_pass = 0;
static int g_fail = 0;
@@ -891,6 +892,49 @@ static void run_all(void)
&nft_catchall_module, &h_nca_nouserns,
SKELETONKEY_PRECOND_FAIL);
/* ── bad_epoll (CVE-2026-46242) ──────────────────────────────
* Pure version gate: vulnerable iff >= 6.4 (bug introduced
* 58c9b016e128) AND below the fix on-branch (stable backport
* 7.0.13; 7.1+ inherits via mainline). NO userns/CONFIG
* precondition — epoll is reachable by every unprivileged user, so
* there is deliberately no PRECOND_FAIL path to test. userns state
* of the base host is irrelevant here. */
/* 6.1.100 predates the vulnerable epoll path (introduced 6.4) → OK */
struct skeletonkey_host h_bep_61 =
mk_host(h_kernel_6_12, 6, 1, 100, "6.1.100-test");
run_one("bad_epoll: 6.1.100 predates the bug (introduced 6.4) → OK",
&bad_epoll_module, &h_bep_61,
SKELETONKEY_OK);
/* 6.12.70 in range [6.4, 7.0.13) → VULNERABLE (no userns needed) */
struct skeletonkey_host h_bep_61270 =
mk_host(h_kernel_6_12, 6, 12, 70, "6.12.70-test");
run_one("bad_epoll: 6.12.70 in range → VULNERABLE (no userns gate)",
&bad_epoll_module, &h_bep_61270,
SKELETONKEY_VULNERABLE);
/* 7.0.5 on the 7.0 branch, below the 7.0.13 backport → VULNERABLE */
struct skeletonkey_host h_bep_705 =
mk_host(h_kernel_6_12, 7, 0, 5, "7.0.5-test");
run_one("bad_epoll: 7.0.5 below the 7.0.13 backport → VULNERABLE",
&bad_epoll_module, &h_bep_705,
SKELETONKEY_VULNERABLE);
/* 7.0.13 exact backport → OK via patch table */
struct skeletonkey_host h_bep_70130 =
mk_host(h_kernel_6_12, 7, 0, 13, "7.0.13-test");
run_one("bad_epoll: 7.0.13 (exact backport) → OK via patch table",
&bad_epoll_module, &h_bep_70130,
SKELETONKEY_OK);
/* 7.1.0 newer than every entry → mainline-inherited fix → OK */
struct skeletonkey_host h_bep_710 =
mk_host(h_kernel_6_12, 7, 1, 0, "7.1.0-test");
run_one("bad_epoll: 7.1.0 above the backport → OK (mainline inherit)",
&bad_epoll_module, &h_bep_710,
SKELETONKEY_OK);
/* ── coverage report ─────────────────────────────────────────
* Iterate the runtime registry (populated by skeletonkey_register_*
* calls in main()) and warn for any module that was not touched