Linux filesystem development
 help / color / mirror / Atom feed
* [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b
@ 2026-06-19 14:58 Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 1/7] eventpoll: use hlist_is_singular_node() in __ep_remove() Quentin Schulz
                   ` (7 more replies)
  0 siblings, 8 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, stable, Quentin Schulz,
	Linus Torvalds, Jaeyoung Chung

Backport a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll /
struct file UAF") to 6.12.y. So the patch applies cleanly, commit
86e87059e6d1 ("eventpoll: move epi_fget() up"), commit 0bade234723e
("eventpoll: rename ep_remove_safe() back to ep_remove()"), commit
0feaf644f718 ("eventpoll: drop vestigial __ prefix from
ep_remove_{file,epi}()"), commit e9e5cd40d7c4 ("eventpoll: kill
__ep_remove()"), commit 0f7bdfd41300 ("eventpoll: split __ep_remove()")
and commit 3d9fd0abc94d ("eventpoll: use hlist_is_singular_node() in
__ep_remove()") are also backported.

Note that backport of commit 86e87059e6d1 ("eventpoll: move epi_fget()
up") conflicted due to missing commit 90ee6ed776c0 ("fs: port files to
file_ref") and its dependent commit 08ef26ea9ab3 ("fs: add file_ref").
The original commit is simply moving a function earlier in the file, so
we do the same even if the content of the function is actually slightly
different. I opted for this instead of backporting the other two commits
because they look a bit more involved than I would like to for stable.
They also do not apply cleanly so I drew the line before those two
"dependencies" and didn't add them to the list of backported patches in
this series.

Note that backport of 0bade234723e ("eventpoll: rename ep_remove_safe()
back to ep_remove()") is not necessary (e.g. 6.18.y doesn't have it), it
just makes git-range-diff even smaller so I thought it was nice to add
it. Maybe it'll make future backports easier too /me shrugs.

The changes between 3d9fd0abc94d^..a6dc643c6931 (commit log excluded)
and this series is (according to git-range-diff):

"""
      ## fs/eventpoll.c ##
     @@ fs/eventpoll.c: static void ep_free(struct eventpoll *ep)
    @@ fs/eventpoll.c: static void ep_free(struct eventpoll *ep)
     +  struct file *file;
     +
     +  file = epi->ffd.file;
    -+  if (!file_ref_get(&file->f_ref))
    ++  if (!atomic_long_inc_not_zero(&file->f_count))
     +          file = NULL;
     +  return file;
     +}
    @@ fs/eventpoll.c: static __poll_t __ep_eventpoll_poll(struct file *file, poll_tabl
     -  struct file *file;
     -
     -  file = epi->ffd.file;
    --  if (!file_ref_get(&file->f_ref))
    +-  if (!atomic_long_inc_not_zero(&file->f_count))
     -          file = NULL;
     -  return file;
     -}
"""

in patch 6.

Note that this series cleanly applies to v6.6.y as well but fails to
build with the following error:

/home/qschulz/work/upstream/linux/fs/eventpoll.c: In function ‘ep_remove’:
/home/qschulz/work/upstream/linux/fs/eventpoll.c:804:16: error: cleanup argument not a function
  804 |         struct file *file __free(fput) = NULL;
      |                ^~~~
make[4]: *** [/home/qschulz/work/upstream/linux/scripts/Makefile.build:243: fs/eventpoll.o] Error 1
make[4]: *** Waiting for unfinished jobs....

hence why I made this series 6.12.y-specific.

Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
---
Christian Brauner (7):
      eventpoll: use hlist_is_singular_node() in __ep_remove()
      eventpoll: split __ep_remove()
      eventpoll: kill __ep_remove()
      eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}()
      eventpoll: rename ep_remove_safe() back to ep_remove()
      eventpoll: move epi_fget() up
      eventpoll: fix ep_remove struct eventpoll / struct file UAF

 fs/eventpoll.c | 142 ++++++++++++++++++++++++++++++++-------------------------
 1 file changed, 79 insertions(+), 63 deletions(-)
---
base-commit: 0b8f247169e487eff2d4c2dd531bc43f7efda2cb
change-id: 20260619-6-12-cve-2026-46242-b3ceffc753a1

Best regards,
--  
Quentin Schulz <quentin.schulz@cherry.de>


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 1/7] eventpoll: use hlist_is_singular_node() in __ep_remove()
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 2/7] eventpoll: split __ep_remove() Quentin Schulz
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, stable, Quentin Schulz

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>
---
 fs/eventpoll.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index a860cb54658a3..8f9dc2f4891ff 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -827,7 +827,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.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 2/7] eventpoll: split __ep_remove()
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 1/7] eventpoll: use hlist_is_singular_node() in __ep_remove() Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 3/7] eventpoll: kill __ep_remove() Quentin Schulz
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, stable, Quentin Schulz,
	Linus Torvalds

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>
---
 fs/eventpoll.c | 27 +++++++++++++++++++++++----
 1 file changed, 23 insertions(+), 4 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 8f9dc2f4891ff..1cba4ae4a076b 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -797,6 +797,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.
@@ -808,8 +811,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();
 
@@ -825,8 +826,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);
@@ -840,6 +854,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.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 3/7] eventpoll: kill __ep_remove()
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 1/7] eventpoll: use hlist_is_singular_node() in __ep_remove() Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 2/7] eventpoll: split __ep_remove() Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 4/7] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}() Quentin Schulz
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, 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>
---
 fs/eventpoll.c | 67 ++++++++++++++++++++++++++--------------------------------
 1 file changed, 30 insertions(+), 37 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 1cba4ae4a076b..3ac8a26c3522f 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -797,49 +797,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. */
@@ -886,7 +855,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));
 }
 
@@ -1118,7 +1105,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);
 
 		/*
@@ -1127,7 +1114,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))

-- 
2.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 4/7] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}()
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
                   ` (2 preceding siblings ...)
  2026-06-19 14:58 ` [PATCH 6.12.y 3/7] eventpoll: kill __ep_remove() Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 5/7] eventpoll: rename ep_remove_safe() back to ep_remove() Quentin Schulz
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, 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>
---
 fs/eventpoll.c | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 3ac8a26c3522f..dc747f382dd95 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -801,7 +801,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;
@@ -825,7 +825,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);
 
@@ -871,9 +871,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));
 }
 
@@ -1118,8 +1118,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.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 5/7] eventpoll: rename ep_remove_safe() back to ep_remove()
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
                   ` (3 preceding siblings ...)
  2026-06-19 14:58 ` [PATCH 6.12.y 4/7] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}() Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 6/7] eventpoll: move epi_fget() up Quentin Schulz
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, stable, Quentin Schulz

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>
---
 fs/eventpoll.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index dc747f382dd95..27280ba4f3d5b 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -853,7 +853,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;
 
@@ -900,7 +900,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
@@ -909,7 +909,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();
 	}
 
@@ -1602,21 +1602,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;
 		}
 	}
@@ -1640,7 +1640,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;
 	}
 
@@ -2329,7 +2329,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.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 6/7] eventpoll: move epi_fget() up
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
                   ` (4 preceding siblings ...)
  2026-06-19 14:58 ` [PATCH 6.12.y 5/7] eventpoll: rename ep_remove_safe() back to ep_remove() Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-19 14:58 ` [PATCH 6.12.y 7/7] eventpoll: fix ep_remove struct eventpoll / struct file UAF Quentin Schulz
  2026-06-21 13:47 ` [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Sasha Levin
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, 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>
---
 fs/eventpoll.c | 56 ++++++++++++++++++++++++++++----------------------------
 1 file changed, 28 insertions(+), 28 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 27280ba4f3d5b..2993b76c21f68 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -797,6 +797,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
@@ -989,34 +1017,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.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH 6.12.y 7/7] eventpoll: fix ep_remove struct eventpoll / struct file UAF
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
                   ` (5 preceding siblings ...)
  2026-06-19 14:58 ` [PATCH 6.12.y 6/7] eventpoll: move epi_fget() up Quentin Schulz
@ 2026-06-19 14:58 ` Quentin Schulz
  2026-06-21 13:47 ` [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Sasha Levin
  7 siblings, 0 replies; 9+ messages in thread
From: Quentin Schulz @ 2026-06-19 14:58 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: linux-fsdevel, linux-kernel, stable, Quentin Schulz,
	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>
Signed-off-by: Quentin Schulz <quentin.schulz@cherry.de>
---
 fs/eventpoll.c | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index 2993b76c21f68..22605fbc12ded 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -883,22 +883,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.54.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b
  2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
                   ` (6 preceding siblings ...)
  2026-06-19 14:58 ` [PATCH 6.12.y 7/7] eventpoll: fix ep_remove struct eventpoll / struct file UAF Quentin Schulz
@ 2026-06-21 13:47 ` Sasha Levin
  7 siblings, 0 replies; 9+ messages in thread
From: Sasha Levin @ 2026-06-21 13:47 UTC (permalink / raw)
  To: Alexander Viro, Christian Brauner, Jan Kara, Davidlohr Bueso,
	Andrew Morton, Soheil Hassas Yeganeh, Eric Dumazet, Paolo Abeni
  Cc: Sasha Levin, linux-fsdevel, linux-kernel, stable, Quentin Schulz,
	Linus Torvalds, Jaeyoung Chung, Quentin Schulz

> Backport a6dc643c6931 ("eventpoll: fix ep_remove struct eventpoll /
> struct file UAF") to 6.12.y. So the patch applies cleanly, [6 prerequisite
> commits] ...

Queued the full 7-patch series for 6.12, thanks.

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-06-21 13:47 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-19 14:58 [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 1/7] eventpoll: use hlist_is_singular_node() in __ep_remove() Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 2/7] eventpoll: split __ep_remove() Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 3/7] eventpoll: kill __ep_remove() Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 4/7] eventpoll: drop vestigial __ prefix from ep_remove_{file,epi}() Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 5/7] eventpoll: rename ep_remove_safe() back to ep_remove() Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 6/7] eventpoll: move epi_fget() up Quentin Schulz
2026-06-19 14:58 ` [PATCH 6.12.y 7/7] eventpoll: fix ep_remove struct eventpoll / struct file UAF Quentin Schulz
2026-06-21 13:47 ` [PATCH 6.12.y 0/7] eventpoll: backport a6dc643c69311677c574a0f17a3f4d66a5f3744b Sasha Levin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox