* [STABLE v5.15.y 2/8] eventpoll: use hlist_is_singular_node() in __ep_remove()
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
@ 2026-09-08 8:50 ` Lee Jones
2026-09-08 8:50 ` [STABLE v5.15.y 3/8] eventpoll: split __ep_remove() Lee Jones
` (5 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:50 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Jeff Layton,
Peter Zijlstra, Josef Bacik, Christian Brauner, Eric Dumazet,
Andrew Morton, Davidlohr Bueso, Soheil Hassas Yeganeh,
Paolo Abeni, linux-fsdevel, linux-kernel
Cc: stable, Quentin Schulz, Wentao Guan
From: Christian Brauner <brauner@kernel.org>
[ Upstream 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) <brauner@kernel.org>
Stable-dep-of: a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll / struct file UAF")
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 605963b245b2c436803cbbefddf5ee5cb326ceaa)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 8762d0908637..5945390e352c 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -738,7 +738,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)) {
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [STABLE v5.15.y 3/8] eventpoll: split __ep_remove()
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
2026-09-08 8:50 ` [STABLE v5.15.y 2/8] eventpoll: use hlist_is_singular_node() in __ep_remove() Lee Jones
@ 2026-09-08 8:50 ` Lee Jones
2026-09-08 8:51 ` [STABLE v5.15.y 4/8] eventpoll: kill __ep_remove() Lee Jones
` (4 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:50 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Sasha Levin, Josef Bacik,
Jeff Layton, Christian Brauner, Peter Zijlstra, Eric Dumazet,
Andrew Morton, Paolo Abeni, Davidlohr Bueso,
Soheil Hassas Yeganeh, linux-fsdevel, linux-kernel
Cc: stable, Linus Torvalds, Quentin Schulz, Wentao Guan
From: Christian Brauner <brauner@kernel.org>
[ Upstream commit 0f7bdfd413000985de09fc39eb9efa1e091a3ce0 ]
Split __ep_remove() to delineate file removal from epoll item removal.
Suggested-by: Linus Torvalds <torvalds@linux-foundation.org>
Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-2-2470f9eec0f5@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
Stable-dep-of: a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll / struct file UAF")
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 66bc7c8a33df3618e43ce0d0d4a985595ce57861)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 27 +++++++++++++++++++++++----
1 file changed, 23 insertions(+), 4 deletions(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 5945390e352c..693ad9826e32 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -708,6 +708,9 @@ static void ep_free(struct eventpoll *ep)
kfree_rcu(ep, rcu);
}
+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.
@@ -719,8 +722,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();
@@ -736,8 +737,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);
@@ -751,6 +765,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);
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [STABLE v5.15.y 4/8] eventpoll: kill __ep_remove()
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
2026-09-08 8:50 ` [STABLE v5.15.y 2/8] eventpoll: use hlist_is_singular_node() in __ep_remove() Lee Jones
2026-09-08 8:50 ` [STABLE v5.15.y 3/8] eventpoll: split __ep_remove() Lee Jones
@ 2026-09-08 8:51 ` Lee Jones
2026-09-08 22:39 ` Sasha Levin
2026-09-08 8:51 ` [STABLE v5.15.y 5/8] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}() Lee Jones
` (3 subsequent siblings)
6 siblings, 1 reply; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:51 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Sasha Levin, Jeff Layton,
Wentao Guan, Josef Bacik, Christian Brauner, Peter Zijlstra,
Eric Dumazet, Andrew Morton, Paolo Abeni, Soheil Hassas Yeganeh,
Davidlohr Bueso, linux-fsdevel, linux-kernel
Cc: stable, Quentin Schulz
From: Christian Brauner <brauner@kernel.org>
[ Upstream 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) <brauner@kernel.org>
Stable-dep-of: a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll / struct file UAF")
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 13e8b6ab5fd34129b2a4a1b2c00b4921dde462cd)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 68 +++++++++++++++++++++++---------------------------
1 file changed, 31 insertions(+), 37 deletions(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 693ad9826e32..3d0cad2592a1 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -708,49 +708,18 @@ static void ep_free(struct eventpoll *ep)
kfree_rcu(ep, rcu);
}
-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. */
@@ -797,7 +766,26 @@ static bool __ep_remove_epi(struct eventpoll *ep, struct epitem *epi)
*/
static void ep_remove_safe(struct eventpoll *ep, struct epitem *epi)
{
- WARN_ON_ONCE(__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));
}
static void ep_clear_and_put(struct eventpoll *ep)
@@ -1008,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);
/*
@@ -1017,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)
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [STABLE v5.15.y 4/8] eventpoll: kill __ep_remove()
2026-09-08 8:51 ` [STABLE v5.15.y 4/8] eventpoll: kill __ep_remove() Lee Jones
@ 2026-09-08 22:39 ` Sasha Levin
0 siblings, 0 replies; 9+ messages in thread
From: Sasha Levin @ 2026-09-08 22:39 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Jeff Layton, Wentao Guan,
Josef Bacik, Christian Brauner, Peter Zijlstra, Eric Dumazet,
Andrew Morton, Paolo Abeni, Soheil Hassas Yeganeh,
Davidlohr Bueso, linux-fsdevel, linux-kernel
Cc: Sasha Levin, stable, Quentin Schulz
> 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.
This leaves ep_remove() with a redundant second refcount decrement on 5.15.y.
On this tree the decrement already happens inside the helper (ep_remove_epi()
ends with return ep_refcount_dec_and_test(ep), carried over correctly by patch
3/8), but this patch imports upstream's caller body verbatim:
if (ep_remove_epi(...))
WARN_ON_ONCE(ep_refcount_dec_and_test(ep));
Upstream can do that because its own helper returns true without decrementing;
5.15.y's doesn't, so on the WARN_ON_ONCE path this decrements an already-zero
refcount.
Could you respin with WARN_ON_ONCE(ep_remove_epi(ep, epi)); here instead,
keeping the decrement inside the helper?
--
Thanks,
Sasha
^ permalink raw reply [flat|nested] 9+ messages in thread
* [STABLE v5.15.y 5/8] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}()
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
` (2 preceding siblings ...)
2026-09-08 8:51 ` [STABLE v5.15.y 4/8] eventpoll: kill __ep_remove() Lee Jones
@ 2026-09-08 8:51 ` Lee Jones
2026-09-08 8:51 ` [STABLE v5.15.y 6/8] eventpoll: rename ep_remove_safe() back to ep_remove() Lee Jones
` (2 subsequent siblings)
6 siblings, 0 replies; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:51 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Jeff Layton,
Peter Zijlstra, Josef Bacik, Wentao Guan, Christian Brauner,
Andrew Morton, Paolo Abeni, Soheil Hassas Yeganeh,
Davidlohr Bueso, Eric Dumazet, linux-fsdevel, linux-kernel
Cc: stable, Quentin Schulz
From: Christian Brauner <brauner@kernel.org>
[ Upstream 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) <brauner@kernel.org>
Stable-dep-of: a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll / struct file UAF")
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit e40f6d02a75baa0f145e0394a2d9b31b969da247)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 3d0cad2592a1..cf00714afb4a 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -712,7 +712,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;
@@ -736,7 +736,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);
@@ -782,9 +782,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);
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [STABLE v5.15.y 6/8] eventpoll: rename ep_remove_safe() back to ep_remove()
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
` (3 preceding siblings ...)
2026-09-08 8:51 ` [STABLE v5.15.y 5/8] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}() Lee Jones
@ 2026-09-08 8:51 ` Lee Jones
2026-09-08 8:51 ` [STABLE v5.15.y 7/8] eventpoll: move epi_fget() up Lee Jones
2026-09-08 8:51 ` [STABLE v5.15.y 8/8] eventpoll: fix ep_remove struct eventpoll / struct file UAF Lee Jones
6 siblings, 0 replies; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:51 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Josef Bacik,
Christian Brauner, Jeff Layton, Peter Zijlstra, Davidlohr Bueso,
Paolo Abeni, Eric Dumazet, Andrew Morton, Soheil Hassas Yeganeh,
linux-fsdevel, linux-kernel
Cc: stable, Quentin Schulz, Wentao Guan
From: Christian Brauner <brauner@kernel.org>
[ Upstream 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) <brauner@kernel.org>
Stable-dep-of: a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll / struct file UAF")
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 136e5900ea830dee549e3485c8931ec1c8f6c1b1)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index cf00714afb4a..f6db6621bbee 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -764,7 +764,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;
@@ -812,7 +812,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
@@ -821,7 +821,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();
}
@@ -1588,21 +1588,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;
}
}
@@ -1626,7 +1626,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;
}
@@ -2305,7 +2305,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;
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [STABLE v5.15.y 7/8] eventpoll: move epi_fget() up
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
` (4 preceding siblings ...)
2026-09-08 8:51 ` [STABLE v5.15.y 6/8] eventpoll: rename ep_remove_safe() back to ep_remove() Lee Jones
@ 2026-09-08 8:51 ` Lee Jones
2026-09-08 8:51 ` [STABLE v5.15.y 8/8] eventpoll: fix ep_remove struct eventpoll / struct file UAF Lee Jones
6 siblings, 0 replies; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:51 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Peter Zijlstra,
Josef Bacik, Wentao Guan, Christian Brauner, Eric Dumazet,
Soheil Hassas Yeganeh, Davidlohr Bueso, Andrew Morton,
Paolo Abeni, linux-fsdevel, linux-kernel
Cc: stable, Quentin Schulz
From: Christian Brauner <brauner@kernel.org>
[ Upstream commit 86e87059e6d1fd5115a31949726450ed03c1073b ]
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) <brauner@kernel.org>
Stable-dep-of: a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll / struct file UAF")
[file_ref_get(&file->f_ref) from original commit left as
atomic_long_inc_not_zero(&file->f_count) due to v6.12.y missing commit
90ee6ed776c0 ("fs: port files to file_ref") and its dependent commit
08ef26ea9ab3 ("fs: add file_ref")]
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 7859566c519c75965b57ab2caccf8d6268717c9a)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 56 +++++++++++++++++++++++++-------------------------
1 file changed, 28 insertions(+), 28 deletions(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index f6db6621bbee..88a7a8ead9cd 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -708,6 +708,34 @@ static void ep_free(struct eventpoll *ep)
kfree_rcu(ep, rcu);
}
+/*
+ * 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()
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [STABLE v5.15.y 8/8] eventpoll: fix ep_remove struct eventpoll / struct file UAF
2026-09-08 8:50 [STABLE v5.15.y 1/8] file: add fput() cleanup helper Lee Jones
` (5 preceding siblings ...)
2026-09-08 8:51 ` [STABLE v5.15.y 7/8] eventpoll: move epi_fget() up Lee Jones
@ 2026-09-08 8:51 ` Lee Jones
6 siblings, 0 replies; 9+ messages in thread
From: Lee Jones @ 2026-09-08 8:51 UTC (permalink / raw)
To: lee, Alexander Viro, Greg Kroah-Hartman, Peter Zijlstra,
Sasha Levin, Wentao Guan, Christian Brauner,
Soheil Hassas Yeganeh, Eric Dumazet, Davidlohr Bueso, Paolo Abeni,
Andrew Morton, linux-fsdevel, linux-kernel
Cc: stable, Jaeyoung Chung
From: Christian Brauner <brauner@kernel.org>
[ Upstream 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 <jjy600901@snu.ac.kr>
Link: https://patch.msgid.link/20260423-work-epoll-uaf-v1-6-2470f9eec0f5@kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
(cherry picked from commit a6dc643c69311677c574a0f17a3f4d66a5f3744b)
Signed-off-by: Wentao Guan <guanwentao@uniontech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 3e1144d2515d28e4312e663ea05eac203101491d)
Signed-off-by: Lee Jones <lee@kernel.org>
---
fs/eventpoll.c | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 88a7a8ead9cd..864959c045d1 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -794,22 +794,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))
--
2.55.0.979.g7e5102b832-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread