From 4f686864a5a05788068e5b9f2db41a3d06bc5f28 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Wed, 25 Sep 2024 11:05:16 +0200 Subject: [PATCH 01/11] epoll: annotate racy check cve-pre CVE-2026-46242 commit-author Christian Brauner commit 6474353a5e3d0b2cf610153cea0c61f576a36d0a Epoll relies on a racy fastpath check during __fput() in eventpoll_release() to avoid the hit of pointlessly acquiring a semaphore. Annotate that race by using WRITE_ONCE() and READ_ONCE(). Link: https://lore.kernel.org/r/66edfb3c.050a0220.3195df.001a.GAE@google.com Link: https://lore.kernel.org/r/20240925-fungieren-anbauen-79b334b00542@brauner Reviewed-by: Jan Kara Reported-by: syzbot+3b6b32dc50537a49bb4a@syzkaller.appspotmail.com Signed-off-by: Christian Brauner (cherry picked from commit 6474353a5e3d0b2cf610153cea0c61f576a36d0a) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 6 ++++-- include/linux/eventpoll.h | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index 14e7663f5053f..c2b9630e98832 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -742,7 +742,8 @@ static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) to_free = NULL; head = file->f_ep; if (head->first == &epi->fllink && !epi->fllink.next) { - file->f_ep = NULL; + /* See eventpoll_release() for details. */ + WRITE_ONCE(file->f_ep, NULL); if (!is_file_epoll(file)) { struct epitems_head *v; v = container_of(head, struct epitems_head, epitems); @@ -1505,7 +1506,8 @@ static int attach_epitem(struct file *file, struct epitem *epi) spin_unlock(&file->f_lock); goto allocate; } - file->f_ep = head; + /* See eventpoll_release() for details. */ + WRITE_ONCE(file->f_ep, head); to_free = NULL; } hlist_add_head_rcu(&epi->fllink, file->f_ep); diff --git a/include/linux/eventpoll.h b/include/linux/eventpoll.h index 593322c946e63..1f6b98a7b403e 100644 --- a/include/linux/eventpoll.h +++ b/include/linux/eventpoll.h @@ -42,7 +42,7 @@ static inline void eventpoll_release(struct file *file) * because the file in on the way to be removed and nobody ( but * eventpoll ) has still a reference to this file. */ - if (likely(!file->f_ep)) + if (likely(!READ_ONCE(file->f_ep))) return; /* From be1515d84bc0c3c4b45b174ca3306698be0d38ff Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:04 +0200 Subject: [PATCH 02/11] eventpoll: use hlist_is_singular_node() in __ep_remove() cve-pre CVE-2026-46242 commit-author Christian Brauner commit 3d9fd0abc94d8cd430cc7cd7d37ce5e5aae2cd2b Replace the open-coded "epi is the only entry in file->f_ep" check with hlist_is_singular_node(). Same semantics, and the helper avoids the head-cacheline access in the common false case. Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-1-2470f9eec0f5@kernel.org Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit 3d9fd0abc94d8cd430cc7cd7d37ce5e5aae2cd2b) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index c2b9630e98832..fbf4345581c03 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -741,7 +741,7 @@ static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) to_free = NULL; head = file->f_ep; - if (head->first == &epi->fllink && !epi->fllink.next) { + if (hlist_is_singular_node(&epi->fllink, head)) { /* See eventpoll_release() for details. */ WRITE_ONCE(file->f_ep, NULL); if (!is_file_epoll(file)) { From 0529288ffdfb5f2876362ba63d40f09e44c81d72 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:05 +0200 Subject: [PATCH 03/11] eventpoll: split __ep_remove() cve-pre CVE-2026-46242 commit-author Christian Brauner commit 0f7bdfd413000985de09fc39eb9efa1e091a3ce0 Split __ep_remove() to delineate file removal from epoll item removal. Suggested-by: Linus Torvalds Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-2-2470f9eec0f5@kernel.org Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit 0f7bdfd413000985de09fc39eb9efa1e091a3ce0) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index fbf4345581c03..fa3b79ba12ae7 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -711,6 +711,9 @@ static void ep_free(struct eventpoll *ep) kfree(ep); } +static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, struct file *file); +static bool __ep_remove_epi(struct eventpoll *ep, struct epitem *epi); + /* * Removes a "struct epitem" from the eventpoll RB tree and deallocates * all the associated resources. Must be called with "mtx" held. @@ -722,8 +725,6 @@ 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 epitems_head *to_free; - struct hlist_head *head; lockdep_assert_irqs_enabled(); @@ -739,8 +740,21 @@ static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) return false; } - to_free = NULL; - head = file->f_ep; + __ep_remove_file(ep, epi, file); + return __ep_remove_epi(ep, epi); +} + +/* + * Called with &file->f_lock held, + * returns with it released + */ +static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, struct file *file) +{ + struct epitems_head *to_free = NULL; + struct hlist_head *head = file->f_ep; + + lockdep_assert_held(&ep->mtx); + if (hlist_is_singular_node(&epi->fllink, head)) { /* See eventpoll_release() for details. */ WRITE_ONCE(file->f_ep, NULL); @@ -754,6 +768,11 @@ static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) hlist_del_rcu(&epi->fllink); spin_unlock(&file->f_lock); free_ephead(to_free); +} + +static bool __ep_remove_epi(struct eventpoll *ep, struct epitem *epi) +{ + lockdep_assert_held(&ep->mtx); rb_erase_cached(&epi->rbn, &ep->rbr); From d9055748e9d872accd79de2ea122699936aba628 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:06 +0200 Subject: [PATCH 04/11] eventpoll: kill __ep_remove() cve-pre CVE-2026-46242 commit-author Christian Brauner commit e9e5cd40d7c403e19f21d0f7b8b8ba3a76b58330 Remove the boolean conditional in __ep_remove() and restructure the code so the check for racing with eventpoll_release_file() are only done in the ep_remove_safe() path where they belong. Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-3-2470f9eec0f5@kernel.org Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit e9e5cd40d7c403e19f21d0f7b8b8ba3a76b58330) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 67 ++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index fa3b79ba12ae7..1ab68c841ca5c 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -711,49 +711,18 @@ static void ep_free(struct eventpoll *ep) kfree(ep); } -static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, struct file *file); -static bool __ep_remove_epi(struct eventpoll *ep, struct epitem *epi); - -/* - * Removes a "struct epitem" from the eventpoll RB tree and deallocates - * all the associated resources. Must be called with "mtx" held. - * If the dying flag is set, do the removal only if force is true. - * This prevents ep_clear_and_put() from dropping all the ep references - * while running concurrently with eventpoll_release_file(). - * Returns true if the eventpoll can be disposed. - */ -static bool __ep_remove(struct eventpoll *ep, struct epitem *epi, bool force) -{ - struct file *file = epi->ffd.file; - - lockdep_assert_irqs_enabled(); - - /* - * Removes poll wait queue hooks. - */ - 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; - } - - __ep_remove_file(ep, epi, file); - return __ep_remove_epi(ep, epi); -} - /* * Called with &file->f_lock held, * returns with it released */ -static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, struct file *file) +static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, + struct file *file) { struct epitems_head *to_free = NULL; struct hlist_head *head = file->f_ep; lockdep_assert_held(&ep->mtx); + lockdep_assert_held(&file->f_lock); if (hlist_is_singular_node(&epi->fllink, head)) { /* See eventpoll_release() for details. */ @@ -800,7 +769,25 @@ static bool __ep_remove_epi(struct eventpoll *ep, struct epitem *epi) */ static void ep_remove_safe(struct eventpoll *ep, struct epitem *epi) { - if (__ep_remove(ep, epi, false)) + struct file *file = epi->ffd.file; + + lockdep_assert_irqs_enabled(); + lockdep_assert_held(&ep->mtx); + + ep_unregister_pollwait(ep, epi); + + /* sync with eventpoll_release_file() */ + if (unlikely(READ_ONCE(epi->dying))) + return; + + spin_lock(&file->f_lock); + if (epi->dying) { + spin_unlock(&file->f_lock); + return; + } + __ep_remove_file(ep, epi, file); + + if (__ep_remove_epi(ep, epi)) WARN_ON_ONCE(ep_refcount_dec_and_test(ep)); } @@ -1009,7 +996,7 @@ void eventpoll_release_file(struct file *file) spin_lock(&file->f_lock); if (file->f_ep && file->f_ep->first) { epi = hlist_entry(file->f_ep->first, struct epitem, fllink); - epi->dying = true; + WRITE_ONCE(epi->dying, true); spin_unlock(&file->f_lock); /* @@ -1018,7 +1005,13 @@ void eventpoll_release_file(struct file *file) */ ep = epi->ep; mutex_lock(&ep->mtx); - dispose = __ep_remove(ep, epi, true); + + ep_unregister_pollwait(ep, epi); + + spin_lock(&file->f_lock); + __ep_remove_file(ep, epi, file); + dispose = __ep_remove_epi(ep, epi); + mutex_unlock(&ep->mtx); if (dispose && ep_refcount_dec_and_test(ep)) From c5f0f5ed4c23fb129d1bf9e4eb2f9bb757c9dd87 Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Fri, 24 Apr 2026 00:23:18 +0200 Subject: [PATCH 05/11] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}() cve-pre CVE-2026-46242 commit-author Christian Brauner commit 0feaf644f7180c4a91b6b405a881afbfd958f1cf With __ep_remove() gone, the double-underscore on __ep_remove_file() and __ep_remove_epi() no longer contrasts with a __-less parent and just reads as noise. Rename both to ep_remove_file() and ep_remove_epi(). No functional change. Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit 0feaf644f7180c4a91b6b405a881afbfd958f1cf) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index 1ab68c841ca5c..f824ad7d6e0e5 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -715,7 +715,7 @@ static void ep_free(struct eventpoll *ep) * Called with &file->f_lock held, * returns with it released */ -static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, +static void ep_remove_file(struct eventpoll *ep, struct epitem *epi, struct file *file) { struct epitems_head *to_free = NULL; @@ -739,7 +739,7 @@ static void __ep_remove_file(struct eventpoll *ep, struct epitem *epi, free_ephead(to_free); } -static bool __ep_remove_epi(struct eventpoll *ep, struct epitem *epi) +static bool ep_remove_epi(struct eventpoll *ep, struct epitem *epi) { lockdep_assert_held(&ep->mtx); @@ -785,9 +785,9 @@ static void ep_remove_safe(struct eventpoll *ep, struct epitem *epi) spin_unlock(&file->f_lock); return; } - __ep_remove_file(ep, epi, file); + ep_remove_file(ep, epi, file); - if (__ep_remove_epi(ep, epi)) + if (ep_remove_epi(ep, epi)) WARN_ON_ONCE(ep_refcount_dec_and_test(ep)); } @@ -1009,8 +1009,8 @@ void eventpoll_release_file(struct file *file) ep_unregister_pollwait(ep, epi); spin_lock(&file->f_lock); - __ep_remove_file(ep, epi, file); - dispose = __ep_remove_epi(ep, epi); + ep_remove_file(ep, epi, file); + dispose = ep_remove_epi(ep, epi); mutex_unlock(&ep->mtx); From c32df3e64267390e7ee50554b0d1b3fc74cb5c3f Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:07 +0200 Subject: [PATCH 06/11] eventpoll: rename ep_remove_safe() back to ep_remove() cve-pre CVE-2026-46242 commit-author Christian Brauner commit 0bade234723e40e4937be912e105785d6a51464e The current name is just confusing and doesn't clarify anything. Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-4-2470f9eec0f5@kernel.org Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit 0bade234723e40e4937be912e105785d6a51464e) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index f824ad7d6e0e5..1047cd19b4d7b 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -767,7 +767,7 @@ static bool ep_remove_epi(struct eventpoll *ep, struct epitem *epi) /* * ep_remove variant for callers owing an additional reference to the ep */ -static void ep_remove_safe(struct eventpoll *ep, struct epitem *epi) +static void ep_remove(struct eventpoll *ep, struct epitem *epi) { struct file *file = epi->ffd.file; @@ -814,7 +814,7 @@ static void ep_clear_and_put(struct eventpoll *ep) /* * Walks through the whole tree and try to free each "struct epitem". - * Note that ep_remove_safe() will not remove the epitem in case of a + * Note that ep_remove() will not remove the epitem in case of a * racing eventpoll_release_file(); the latter will do the removal. * At this point we are sure no poll callbacks will be lingering around. * Since we still own a reference to the eventpoll struct, the loop can't @@ -823,7 +823,7 @@ static void ep_clear_and_put(struct eventpoll *ep) for (rbp = rb_first_cached(&ep->rbr); rbp; rbp = next) { next = rb_next(rbp); epi = rb_entry(rbp, struct epitem, rbn); - ep_remove_safe(ep, epi); + ep_remove(ep, epi); cond_resched(); } @@ -1585,21 +1585,21 @@ static int ep_insert(struct eventpoll *ep, const struct epoll_event *event, mutex_unlock(&tep->mtx); /* - * ep_remove_safe() calls in the later error paths can't lead to + * ep_remove() calls in the later error paths can't lead to * ep_free() as the ep file itself still holds an ep reference. */ ep_get(ep); /* now check if we've created too many backpaths */ if (unlikely(full_check && reverse_path_check())) { - ep_remove_safe(ep, epi); + ep_remove(ep, epi); return -EINVAL; } if (epi->event.events & EPOLLWAKEUP) { error = ep_create_wakeup_source(epi); if (error) { - ep_remove_safe(ep, epi); + ep_remove(ep, epi); return error; } } @@ -1623,7 +1623,7 @@ static int ep_insert(struct eventpoll *ep, const struct epoll_event *event, * high memory pressure. */ if (unlikely(!epq.epi)) { - ep_remove_safe(ep, epi); + ep_remove(ep, epi); return -ENOMEM; } @@ -2276,7 +2276,7 @@ int do_epoll_ctl(int epfd, int op, int fd, struct epoll_event *epds, * The eventpoll itself is still alive: the refcount * can't go to zero here. */ - ep_remove_safe(ep, epi); + ep_remove(ep, epi); error = 0; } else { error = -ENOENT; From 01f695a2ea27d1066df93dd0b0a0e147f188c76b Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:08 +0200 Subject: [PATCH 07/11] eventpoll: move epi_fget() up cve-pre CVE-2026-46242 commit-author Christian Brauner commit 86e87059e6d1fd5115a31949726450ed03c1073b upstream-diff | Pure code motion, no functional change intended (matches upstream's own commit message). Kept this tree's existing epi_fget() body using atomic_long_inc_not_zero(&file->f_count) instead of upstream's file_ref_get(&file->f_ref); the file_ref_t/file_ref_get() API comes from a separate, unrelated whole-VFS struct file refcounting rework (commit 90ee6ed776c06 "fs: port files to file_ref", 2024-10-07) that is not present on this branch and is out of scope for this CVE fix. Both mechanisms provide the identical "pin the file, fail if it's already at zero refs" semantics the upcoming ep_remove() UAF fix depends on, so this substitution preserves the security property unchanged. We'll need it when removing files so move it up. No functional change. Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-5-2470f9eec0f5@kernel.org Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit 86e87059e6d1fd5115a31949726450ed03c1073b) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 56 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index 1047cd19b4d7b..b389fd78fab97 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -711,6 +711,34 @@ static void ep_free(struct eventpoll *ep) kfree(ep); } +/* + * The ffd.file pointer may be in the process of being torn down due to + * being closed, but we may not have finished eventpoll_release() yet. + * + * Normally, even with the atomic_long_inc_not_zero, the file may have + * been free'd and then gotten re-allocated to something else (since + * files are not RCU-delayed, they are SLAB_TYPESAFE_BY_RCU). + * + * But for epoll, users hold the ep->mtx mutex, and as such any file in + * the process of being free'd will block in eventpoll_release_file() + * and thus the underlying file allocation will not be free'd, and the + * file re-use cannot happen. + * + * For the same reason we can avoid a rcu_read_lock() around the + * operation - 'ffd.file' cannot go away even if the refcount has + * reached zero (but we must still not call out to ->poll() functions + * etc). + */ +static struct file *epi_fget(const struct epitem *epi) +{ + struct file *file; + + file = epi->ffd.file; + if (!atomic_long_inc_not_zero(&file->f_count)) + file = NULL; + return file; +} + /* * Called with &file->f_lock held, * returns with it released @@ -882,34 +910,6 @@ static __poll_t __ep_eventpoll_poll(struct file *file, poll_table *wait, int dep return res; } -/* - * The ffd.file pointer may be in the process of being torn down due to - * being closed, but we may not have finished eventpoll_release() yet. - * - * Normally, even with the atomic_long_inc_not_zero, the file may have - * been free'd and then gotten re-allocated to something else (since - * files are not RCU-delayed, they are SLAB_TYPESAFE_BY_RCU). - * - * But for epoll, users hold the ep->mtx mutex, and as such any file in - * the process of being free'd will block in eventpoll_release_file() - * and thus the underlying file allocation will not be free'd, and the - * file re-use cannot happen. - * - * For the same reason we can avoid a rcu_read_lock() around the - * operation - 'ffd.file' cannot go away even if the refcount has - * reached zero (but we must still not call out to ->poll() functions - * etc). - */ -static struct file *epi_fget(const struct epitem *epi) -{ - struct file *file; - - file = epi->ffd.file; - if (!atomic_long_inc_not_zero(&file->f_count)) - file = NULL; - return file; -} - /* * Differs from ep_eventpoll_poll() in that internal callers already have * the ep->mtx so we need to start from depth=1, such that mutex_lock_nested() From e0980e4290192349d23e82c01eb5c37274ddccbf Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Thu, 23 Apr 2026 11:56:09 +0200 Subject: [PATCH 08/11] eventpoll: fix ep_remove struct eventpoll / struct file UAF cve CVE-2026-46242 commit-author Christian Brauner commit a6dc643c69311677c574a0f17a3f4d66a5f3744b ep_remove() (via ep_remove_file()) cleared file->f_ep under file->f_lock but then kept using @file inside the critical section (is_file_epoll(), hlist_del_rcu() through the head, spin_unlock). A concurrent __fput() taking the eventpoll_release() fastpath in that window observed the transient NULL, skipped eventpoll_release_file() and ran to f_op->release / file_free(). For the epoll-watches-epoll case, f_op->release is ep_eventpoll_release() -> ep_clear_and_put() -> ep_free(), which kfree()s the watched struct eventpoll. Its embedded ->refs hlist_head is exactly where epi->fllink.pprev points, so the subsequent hlist_del_rcu()'s "*pprev = next" scribbles into freed kmalloc-192 memory. In addition, struct file is SLAB_TYPESAFE_BY_RCU, so the slot backing @file could be recycled by alloc_empty_file() -- reinitializing f_lock and f_ep -- while ep_remove() is still nominally inside that lock. The upshot is an attacker-controllable kmem_cache_free() against the wrong slab cache. Pin @file via epi_fget() at the top of ep_remove() and gate the critical section on the pin succeeding. With the pin held @file cannot reach refcount zero, which holds __fput() off and transitively keeps the watched struct eventpoll alive across the hlist_del_rcu() and the f_lock use, closing both UAFs. If the pin fails @file has already reached refcount zero and its __fput() is in flight. Because we bailed before clearing f_ep, that path takes the eventpoll_release() slow path into eventpoll_release_file() and blocks on ep->mtx until the waiter side's ep_clear_and_put() drops it. The bailed epi's share of ep->refcount stays intact, so the trailing ep_refcount_dec_and_test() in ep_clear_and_put() cannot free the eventpoll out from under eventpoll_release_file(); the orphaned epi is then cleaned up there. A successful pin also proves we are not racing eventpoll_release_file() on this epi, so drop the now-redundant re-check of epi->dying under f_lock. The cheap lockless READ_ONCE(epi->dying) fast-path bailout stays. Fixes: 58c9b016e128 ("epoll: use refcount to reduce ep_mutex contention") Reported-by: Jaeyoung Chung Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-6-2470f9eec0f5@kernel.org Signed-off-by: Christian Brauner (Amutable) (cherry picked from commit a6dc643c69311677c574a0f17a3f4d66a5f3744b) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index b389fd78fab97..bdd25340ef9fd 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -797,22 +797,26 @@ static bool ep_remove_epi(struct eventpoll *ep, struct epitem *epi) */ static void ep_remove(struct eventpoll *ep, struct epitem *epi) { - struct file *file = epi->ffd.file; + struct file *file __free(fput) = NULL; lockdep_assert_irqs_enabled(); lockdep_assert_held(&ep->mtx); ep_unregister_pollwait(ep, epi); - /* sync with eventpoll_release_file() */ + /* cheap sync with eventpoll_release_file() */ if (unlikely(READ_ONCE(epi->dying))) return; - spin_lock(&file->f_lock); - if (epi->dying) { - spin_unlock(&file->f_lock); + /* + * If we manage to grab a reference it means we're not in + * eventpoll_release_file() and aren't going to be. + */ + file = epi_fget(epi); + if (!file) return; - } + + spin_lock(&file->f_lock); ep_remove_file(ep, epi, file); if (ep_remove_epi(ep, epi)) From 8d22118519410ff8600d11e9bdbbb848f31a5380 Mon Sep 17 00:00:00 2001 From: Nicholas Carlini Date: Tue, 31 Mar 2026 15:25:32 +0200 Subject: [PATCH 09/11] eventpoll: defer struct eventpoll free to RCU grace period cve CVE-2026-43074 commit-author Nicholas Carlini commit 07712db80857d5d09ae08f3df85a708ecfc3b61f In certain situations, ep_free() in eventpoll.c will kfree the epi->ep eventpoll struct while it still being used by another concurrent thread. Defer the kfree() to an RCU callback to prevent UAF. Fixes: f2e467a48287 ("eventpoll: Fix semi-unbounded recursion") Signed-off-by: Nicholas Carlini Signed-off-by: Christian Brauner (cherry picked from commit 07712db80857d5d09ae08f3df85a708ecfc3b61f) Signed-off-by: Shreeya Patel --- fs/eventpoll.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/eventpoll.c b/fs/eventpoll.c index bdd25340ef9fd..720771e0cc547 100644 --- a/fs/eventpoll.c +++ b/fs/eventpoll.c @@ -225,6 +225,9 @@ struct eventpoll { */ refcount_t refcount; + /* used to defer freeing past ep_get_upwards_depth_proc() RCU walk */ + struct rcu_head rcu; + #ifdef CONFIG_NET_RX_BUSY_POLL /* used to track busy poll napi_id */ unsigned int napi_id; @@ -708,7 +711,8 @@ static void ep_free(struct eventpoll *ep) mutex_destroy(&ep->mtx); free_uid(ep->user); wakeup_source_unregister(ep->ws); - kfree(ep); + /* ep_get_upwards_depth_proc() may still hold epi->ep under RCU */ + kfree_rcu(ep, rcu); } /* From 62a8e2ce66d2ab9592c507c719004b34538b0ab5 Mon Sep 17 00:00:00 2001 From: Keenan Dong Date: Wed, 8 Apr 2026 16:46:00 +0800 Subject: [PATCH 10/11] rtmutex: Use waiter::task instead of current in remove_waiter() cve CVE-2026-43499 commit-author Keenan Dong commit 3bfdc63936dd4773109b7b8c280c0f3b5ae7d349 remove_waiter() is used by the slowlock paths, but it is also used for proxy-lock rollback in rt_mutex_start_proxy_lock() when invoked from futex_requeue(). In the latter case waiter::task is not current, but remove_waiter() operates on current for the dequeue operation. That results in several problems: 1) the rbtree dequeue happens without waiter::task::pi_lock being held 2) the waiter task's pi_blocked_on state is not cleared, which leaves a dangling pointer primed for UAF around. 3) rt_mutex_adjust_prio_chain() operates on the wrong top priority waiter task Use waiter::task instead of current in all related operations in remove_waiter() to cure those problems. [ tglx: Fixup rt_mutex_adjust_prio_chain(), add a comment and amend the changelog ] Fixes: 8161239a8bcc ("rtmutex: Simplify PI algorithm and make highest prio task get lock") Reported-by: Yuan Tan Reported-by: Yifan Wu Reported-by: Juefei Pu Reported-by: Xin Liu Signed-off-by: Keenan Dong Signed-off-by: Thomas Gleixner Cc: stable@vger.kernel.org (cherry picked from commit 3bfdc63936dd4773109b7b8c280c0f3b5ae7d349) Signed-off-by: Shreeya Patel --- kernel/locking/rtmutex.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/kernel/locking/rtmutex.c b/kernel/locking/rtmutex.c index 1eb4f949909c7..e881d95480309 100644 --- a/kernel/locking/rtmutex.c +++ b/kernel/locking/rtmutex.c @@ -1534,20 +1534,23 @@ static bool rtmutex_spin_on_owner(struct rt_mutex_base *lock, * * Must be called with lock->wait_lock held and interrupts disabled. It must * have just failed to try_to_take_rt_mutex(). + * + * When invoked from rt_mutex_start_proxy_lock() waiter::task != current ! */ static void __sched remove_waiter(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter) { bool is_top_waiter = (waiter == rt_mutex_top_waiter(lock)); struct task_struct *owner = rt_mutex_owner(lock); + struct task_struct *waiter_task = waiter->task; struct rt_mutex_base *next_lock; lockdep_assert_held(&lock->wait_lock); - raw_spin_lock(¤t->pi_lock); - rt_mutex_dequeue(lock, waiter); - current->pi_blocked_on = NULL; - raw_spin_unlock(¤t->pi_lock); + scoped_guard(raw_spinlock, &waiter_task->pi_lock) { + rt_mutex_dequeue(lock, waiter); + waiter_task->pi_blocked_on = NULL; + } /* * Only update priority if the waiter was the highest priority @@ -1583,7 +1586,7 @@ static void __sched remove_waiter(struct rt_mutex_base *lock, raw_spin_unlock_irq(&lock->wait_lock); rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock, - next_lock, NULL, current); + next_lock, NULL, waiter_task); raw_spin_lock_irq(&lock->wait_lock); } From 1da06165f5723b60ec7f0588817978537c223897 Mon Sep 17 00:00:00 2001 From: Davidlohr Bueso Date: Thu, 7 May 2026 04:29:13 -0700 Subject: [PATCH 11/11] locking/rtmutex: Skip remove_waiter() when waiter is not enqueued cve CVE-2026-53163 commit-author Davidlohr Bueso commit 40a25d59e85b3c8709ac2424d44f65610467871e syzbot triggered the following splat in remove_waiter() via FUTEX_CMP_REQUEUE_PI: KASAN: null-ptr-deref in range [0x0000000000000a88-0x0000000000000a8f] class_raw_spinlock_constructor remove_waiter+0x159/0x1200 kernel/locking/rtmutex.c:1561 rt_mutex_start_proxy_lock+0x103/0x120 futex_requeue+0x10e4/0x20d0 __x64_sys_futex+0x34f/0x4d0 task_blocks_on_rt_mutex() does not arm the waiter upon deadlock detection, leaving waiter->task nil, where 3bfdc63936dd ("rtmutex: Use waiter::task instead of current in remove_waiter()") made this fatal. Furthermore, rt_mutex_start_proxy_lock() should not be calling into remove_waiter() upon a successfully grabbing the rtmutex. 1a1fb985f2e2 ("futex: Handle early deadlock return correctly"), moved the remove_waiter() out of __rt_mutex_start_proxy_lock() (where 'ret' was only ever 0 or < 0) into the wrapper. Tighten this check to account for try_to_take_rt_mutex(). Fixes: 3bfdc63936dd ("rtmutex: Use waiter::task instead of current in remove_waiter()") Reported-by: syzbot+78147abe6c524f183ee9@syzkaller.appspotmail.com Signed-off-by: Davidlohr Bueso Signed-off-by: Thomas Gleixner Cc: stable@vger.kernel.org Closes: https://lore.kernel.org/all/69f114ac.050a0220.ac8b.0003.GAE@google.com/ Link: https://patch.msgid.link/20260507112913.1019537-1-dave@stgolabs.net (cherry picked from commit 40a25d59e85b3c8709ac2424d44f65610467871e) Signed-off-by: Shreeya Patel --- kernel/locking/rtmutex.c | 3 +++ kernel/locking/rtmutex_api.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/kernel/locking/rtmutex.c b/kernel/locking/rtmutex.c index e881d95480309..1cecc10925795 100644 --- a/kernel/locking/rtmutex.c +++ b/kernel/locking/rtmutex.c @@ -1547,6 +1547,9 @@ static void __sched remove_waiter(struct rt_mutex_base *lock, lockdep_assert_held(&lock->wait_lock); + if (!waiter_task) /* never enqueued */ + return; + scoped_guard(raw_spinlock, &waiter_task->pi_lock) { rt_mutex_dequeue(lock, waiter); waiter_task->pi_blocked_on = NULL; diff --git a/kernel/locking/rtmutex_api.c b/kernel/locking/rtmutex_api.c index 6614ccdc118cc..341a540f62a5b 100644 --- a/kernel/locking/rtmutex_api.c +++ b/kernel/locking/rtmutex_api.c @@ -347,7 +347,7 @@ int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock, raw_spin_lock_irq(&lock->wait_lock); ret = __rt_mutex_start_proxy_lock(lock, waiter, task, &wake_q); - if (unlikely(ret)) + if (unlikely(ret < 0)) remove_waiter(lock, waiter); preempt_disable(); raw_spin_unlock_irq(&lock->wait_lock);