Linux filesystem development
 help / color / mirror / Atom feed
From: David Laight <david.laight.linux@gmail.com>
To: Guidong Han <2045gemini@gmail.com>
Cc: Alexander Viro <viro@zeniv.linux.org.uk>,
	Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>,
	Jann Horn <jannh@google.com>, Qi Tang <tpluszz77@gmail.com>,
	Junxi Qian <qjx1298677004@gmail.com>,
	linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH] eventpoll: pin files while checking reverse paths
Date: Thu, 23 Jul 2026 19:06:21 +0100	[thread overview]
Message-ID: <20260723190621.51e6e4ac@pumpkin> (raw)
In-Reply-To: <20260718104406.27897-1-2045gemini@gmail.com>

On Sat, 18 Jul 2026 18:44:06 +0800
Guidong Han <2045gemini@gmail.com> wrote:

> Commit 319c15174757 ("epoll: take epitem list out of struct file")
> intentionally removed temporary file references from the reverse path
> check list. At the time, both epitems and their files were freed after
> an RCU grace period, so unlist_file() could obtain file->f_lock through
> an epitem while clear_tfile_check_list() held rcu_read_lock().
> 
> Commit 0ede61d8589c ("file: convert to SLAB_TYPESAFE_BY_RCU") made
> struct file SLAB_TYPESAFE_BY_RCU and removed its RCU-delayed freeing.
> RCU still protects the epitem, but no longer keeps the referenced file
> from being freed and reused. A concurrent close can therefore make
> unlist_file() lock or unlock f_lock in a recycled file object.
> 
> This violates the documented SLAB_TYPESAFE_BY_RCU rule requiring a
> reference before acquiring an object's lock. The race was reproduced,
> causing a wild unlock of f_lock in a recycled file and breaking its
> mutual exclusion.
> 
> Add ->file to epitems_head to remember the pinned file independently of
> ->epitems. A concurrent EPOLL_CTL_DEL can empty ->epitems before the head  
> is unlisted, leaving no epi->ffd.file from which to drop the reference.
> 
> In list_file(), acquire the reference before adding the head to the
> check list. The caller either owns a reference or holds the ep->mtx for
> the epitem leading to the file. In the latter case, file_ref_get() can
> fail after the last reference is dropped, but eventpoll_release_file()
> must acquire the same mutex before the file can be freed. The dying leaf
> can be skipped because removing links cannot increase the reverse path
> count.
> 
> In unlist_file(), epnested_mutex excludes another list_file() or
> unlist_file(), while head->next prevents a concurrent EPOLL_CTL_DEL from
> freeing the head. Save head->file locally, clear it with head->next
> under f_lock, and drop the reference after the RCU-protected operation.

That seems to hold a reference to 'file' that persists after EPOLL_CTL_ADD
returns.
That breaks the requirement that the final close() will automagically
remove an fd from any epoll lists.

	David

> 
> Reported-by: Qi Tang <tpluszz77@gmail.com>
> Reported-by: Junxi Qian <qjx1298677004@gmail.com>
> Fixes: 0ede61d8589c ("file: convert to SLAB_TYPESAFE_BY_RCU")
> Cc: stable@vger.kernel.org
> Signed-off-by: Guidong Han <2045gemini@gmail.com>
> ---
> SLAB_TYPESAFE_BY_RCU allows a slab slot to be reused while an RCU reader
> still holds its old address. Once that address contains a new live
> struct file, KASAN sees valid, unpoisoned memory and cannot distinguish
> the stale object identity. CONFIG_DEBUG_SPINLOCK exposes the failure
> instead.
> 
> The failing interleaving is:
> 
> CPU0: nested EPOLL_CTL_ADD             CPU1: close/open churn
> ------------------------------------   ---------------------------------
> p = hlist_first_rcu(&head->epitems)
> epi = container_of(p, ...)
>                                        close(victim)
>                                          __fput()
>                                            eventpoll_release_file()
>                                            file_free(victim)
>                                        // the slot is free; f_lock remains
> spin_lock(&epi->ffd.file->f_lock)
>                                        open() reuses the slot as new_file
>                                          spin_lock_init(&new_file->f_lock)
> spin_unlock(&epi->ffd.file->f_lock)     // wild unlock of new_file's lock
> 
> CONFIG_DEBUG_SPINLOCK reports:
> 
> BUG: spinlock already unlocked on CPU#0, poc_unlist/150
>  lock: 0xffff8880067fb200, .magic: dead4ead, .owner: <none>/-1, .owner_cpu: -1
> CPU: 0 UID: 1000 PID: 150 Comm: poc_unlist Not tainted 7.2.0-rc3-dirty #22 PREEMPTLAZY 
> Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> Call Trace:
>  <TASK>
>  dump_stack_lvl+0x64/0x80
>  do_raw_spin_unlock+0x75/0xb0
>  _raw_spin_unlock+0xe/0x30
>  clear_tfile_check_list+0x88/0xe0
>  do_epoll_ctl_file+0x519/0xcf0
>  ? __pfx_ep_ptable_queue_proc+0x10/0x10
>  do_epoll_ctl+0x8f/0x100
>  __x64_sys_epoll_ctl+0x6f/0xa0
>  do_syscall_64+0xdc/0x520
>  ? srso_alias_return_thunk+0x5/0xfbef5
>  entry_SYSCALL_64_after_hwframe+0x76/0x7e
> RIP: 0033:0x42034e
> Code: 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 83 c8 ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 49 89 ca b8 e9 00 00 00 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48
> RSP: 002b:00007a657ff3c198 EFLAGS: 00000202 ORIG_RAX: 00000000000000e9
> RAX: ffffffffffffffda RBX: 00007a657ff3ccdc RCX: 000000000042034e
> RDX: 0000000000000003 RSI: 0000000000000001 RDI: 0000000000000004
> RBP: 00007a657ff3c2f0 R08: 0000000000000000 R09: 00007a657ff3c6c0
> R10: 00007a657ff3c1a4 R11: 0000000000000202 R12: 00007a657ff3c6c0
> R13: ffffffffffffffb8 R14: 000000000000000d R15: 00007fffb7de0210
>  </TASK>
> ------------[ cut here ]------------
> 
> unlist_file() does not appear as a separate frame because it was inlined
> into clear_tfile_check_list(). This report was obtained with mdelay()
> instrumentation immediately before spin_lock() and spin_unlock() in
> unlist_file() to widen the two race windows.
> 
> More importantly, this is a wild unlock. The stale unlock can target
> f_lock of a different live file and invalidate mutual exclusion for
> state protected by that lock. Turning this into a reliable exploit
> would require precise scheduling and same-slot reuse and is likely
> difficult, but the primitive is potentially exploitable.
> 
> Qi Tang and Junxi Qian helped identify this issue and are therefore
> credited by the Reported-by tags above.
> ---
>  fs/eventpoll.c | 18 +++++++++++++++++-
>  1 file changed, 17 insertions(+), 1 deletion(-)
> 
> diff --git a/fs/eventpoll.c b/fs/eventpoll.c
> index 0e65c7431dfc..eed8cecd94e3 100644
> --- a/fs/eventpoll.c
> +++ b/fs/eventpoll.c
> @@ -459,11 +459,14 @@ static struct kmem_cache *pwq_cache __ro_after_init;
>   * Wrapper anchor for file->f_ep when the watched file is not itself an
>   * eventpoll; for the epoll-watches-epoll case, file->f_ep points at
>   * &watched_ep->refs directly. The ->next field threads
> - * ctx->tfile_check_list during one EPOLL_CTL_ADD path check.
> + * ctx->tfile_check_list during one EPOLL_CTL_ADD path check. The ->file
> + * field holds a reference to the associated file while the head is on
> + * the list.
>   */
>  struct epitems_head {
>  	struct hlist_head epitems;
>  	struct epitems_head *next;
> +	struct file *file;
>  };
>  
>  static struct kmem_cache *ephead_cache __ro_after_init;
> @@ -480,6 +483,16 @@ static void list_file(struct file *file, struct ep_ctl_ctx *ctx)
>  
>  	head = container_of(file->f_ep, struct epitems_head, epitems);
>  	if (!head->next) {
> +		/*
> +		 * The caller owns a reference to @file or holds the ep->mtx for the
> +		 * epitem that led here. The latter blocks eventpoll_release_file()
> +		 * before the file allocation can be freed and reused. A dying leaf
> +		 * can be skipped since removing links cannot increase the reverse
> +		 * path count.
> +		 */
> +		if (!file_ref_get(&file->f_ref))
> +			return;
> +		head->file = file;
>  		head->next = ctx->tfile_check_list;
>  		ctx->tfile_check_list = head;
>  	}
> @@ -489,15 +502,18 @@ static void unlist_file(struct epitems_head *head)
>  {
>  	struct epitems_head *to_free = head;
>  	struct hlist_node *p = rcu_dereference(hlist_first_rcu(&head->epitems));
> +	struct file *file = head->file;
>  	if (p) {
>  		struct epitem *epi= container_of(p, struct epitem, fllink);
>  		spin_lock(&epi->ffd.file->f_lock);
>  		if (!hlist_empty(&head->epitems))
>  			to_free = NULL;
>  		head->next = NULL;
> +		head->file = NULL;
>  		spin_unlock(&epi->ffd.file->f_lock);
>  	}
>  	free_ephead(to_free);
> +	fput(file);
>  }
>  
>  #ifdef CONFIG_SYSCTL


  reply	other threads:[~2026-07-23 18:06 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-18 10:44 [PATCH] eventpoll: pin files while checking reverse paths Guidong Han
2026-07-23 18:06 ` David Laight [this message]
2026-07-24  3:23   ` Guidong Han
2026-07-23 19:50 ` Mateusz Guzik
2026-07-24  3:57   ` Guidong Han

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260723190621.51e6e4ac@pumpkin \
    --to=david.laight.linux@gmail.com \
    --cc=2045gemini@gmail.com \
    --cc=brauner@kernel.org \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=qjx1298677004@gmail.com \
    --cc=tpluszz77@gmail.com \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox