From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:09 +0200 Subject: [PATCH] eventpoll: fix ep_remove struct eventpoll / struct file UAF CVE: CVE-2026-46242 Upstream commit a6dc643c69311677c574a0f17a3f4d66a5f3744b, adapted to the pre-e9e5cd40d7c4 ("eventpoll: kill __ep_remove()") code shape still used by this kernel, where the removal path is the monolithic __ep_remove(ep, epi, force) rather than upstream's ep_remove()/ep_remove_epi()/ ep_remove_file() split. ep_remove() used epi->ffd.file across the file->f_lock critical section without holding a reference. A concurrent __fput() taking the eventpoll_release() fastpath could free the file (and, for the epoll-watches-epoll case, the watched struct eventpoll) mid-section, leading to a use-after-free / attacker-controllable kmem_cache_free(). Pin the file with epi_fget() before entering the critical section and release it afterwards. A failed pin means __fput() is already in flight and will remove the epi via eventpoll_release_file() (which blocks on ep->mtx), so it is safe to bail. The pin is taken only on the !force path: the force path is reached from eventpoll_release_file() itself, i.e. from within __fput() where f_count has already dropped to zero and epi_fget() would necessarily fail; there @file is still valid and no concurrent __fput() can race, so it keeps using epi->ffd.file directly. A successful pin also makes the in-lock epi->dying recheck redundant; the cheap lockless READ_ONCE(epi->dying) fast-path bailout is kept. Fixes: 58c9b016e128 ("epoll: use refcount to reduce ep_mutex contention") Reported-by: Jaeyoung Chung Signed-off-by: Christian Brauner --- diff --git a/fs/eventpoll.c b/fs/eventpoll.c index f4efb5b..68e58d0 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -821,6 +821,8 @@ static void ep_free(struct eventpoll *ep) kfree(ep); } +static struct file *epi_fget(const struct epitem *epi); + /* * Removes a "struct epitem" from the eventpoll RB tree and deallocates * all the associated resources. Must be called with "mtx" held. @@ -832,6 +834,7 @@ static void ep_free(struct eventpoll *ep) static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) { struct file *file = epi->ffd.file; + struct file *to_put = NULL; struct epitems_head *to_free; struct hlist_head *head; @@ -842,13 +845,27 @@ static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) */ ep_unregister_pollwait(ep, epi); - /* Remove the current item from the list of epoll hooks */ - spin_lock(&file->f_lock); - if (epi->dying && !force) { - spin_unlock(&file->f_lock); - return false; + if (!force) { + /* cheap sync with eventpoll_release_file() */ + if (unlikely(READ_ONCE(epi->dying))) + return false; + /* + * Grabbing a reference proves we are not racing + * eventpoll_release_file() and are not going to: a concurrent + * __fput() cannot free @file (nor, for the epoll-on-epoll + * case, the watched eventpoll) across the f_lock section + * below. A failed pin means @file already reached refcount + * zero and __fput() is in flight; it will remove this epi via + * eventpoll_release_file(), so bail out here. + */ + file = epi_fget(epi); + if (!file) + return false; + to_put = file; } + /* Remove the current item from the list of epoll hooks */ + spin_lock(&file->f_lock); to_free = NULL; head = file->f_ep; if (head->first == &epi->fllink && !epi->fllink.next) { @@ -882,6 +899,8 @@ static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) kfree_rcu(epi, rcu); percpu_counter_dec(&ep->user->epoll_watches); + if (to_put) + fput(to_put); return true; } -- 2.43.0