Forwarded: [PATCH] eventpoll: restore EP_UNACTIVE_PTR sentinel for ctx->tfile_check_list

From: syzbot

Date: Thu May 28 2026 - 12:35:38 EST


For archival purposes, forwarding an incoming command email to
linux-kernel@xxxxxxxxxxxxxxx.

***

Subject: [PATCH] eventpoll: restore EP_UNACTIVE_PTR sentinel for ctx->tfile_check_list
Author: zhanwei919@xxxxxxxxx

#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git master

commit e09c77d94003 ("eventpoll: hoist CTL_ADD scratch state into
struct ep_ctl_ctx") moved tfile_check_list from file-scope global
to stack-allocated struct ep_ctl_ctx, replacing the EP_UNACTIVE_PTR
sentinel with NULL because "NULL is the obvious 'empty'
value and the zero-init handle it for free", and describe the
change "No functional change". but its not.

epitems_head->next is overload:

1: as a linked-list next pointer for heads on ctx->tfile_check_list,
2: as a membership flag: ep_remove_file() uses
!smp_load_acquire(&v->next) to mean "this head is not on any
pending ctx->tfile_check_list and is safe to free".

Before e09c77d94003, the EP_UNACTIVE_PTR sentinel made the two role
disjoint: a head on tfile_check_list always has a non-NULL next
(another head, or the sentinel), so v->next == NULL was equivalent
to never list. With the sentinel gone the list is NULL-terminated
and the tail head's ->next is NULL also. ep_remove_file()'s gate
no longer tell never list from list at the tail, and
misfires on the tail.

The reader hold epnested_mutex + rcu_read_lock; the freer hold
ep->mtx + file->f_lock. There is no sharing mutex between them; the
sentinel was the invariant the gate relied on to skip the read side.

The syzbot reproducer hit this within seconds on a multi-CPU VM.

Restore the sentinel: initialize ctx.tfile_check_list to
EP_UNACTIVE_PTR in do_epoll_ctl_file(), and walk it with
"!= EP_UNACTIVE_PTR" termination in reverse_path_check() and
clear_tfile_check_list(). The gate in ep_remove_file() regains its
never list exclusivity and stop misfiring on the tail.
ep_remove_file() itself does not change.

This restores the invariant the file-scope tfile_check_list relied
on before e09c77d94003, preserving the ctx packaging that commit
introduced.

Reported-by: syzbot+e70e1b6cba8714543f7c@xxxxxxxxxxxxxxxxxxxxxxxxx
Closes: https://syzkaller.appspot.com/bug?extid=e70e1b6cba8714543f7c
Fixes: e09c77d94003 ("eventpoll: hoist CTL_ADD scratch state into struct ep_ctl_ctx")
Suggested-by: Christian Brauner <brauner@xxxxxxxxxx>
Link: https://lore.kernel.org/all/20260528-rotwild-summt-kuhhandel-7276ef4c33b7@xxxxxxxxxx/

Signed-off-by: Zhan Wei <zhanwei919@xxxxxxxxx>
---
fs/eventpoll.c | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/fs/eventpoll.c b/fs/eventpoll.c
index a569e98d4a99..4973a5a5a3e1 100644
--- a/fs/eventpoll.c
+++ b/fs/eventpoll.c
@@ -1685,7 +1685,7 @@ static int reverse_path_check(struct ep_ctl_ctx *ctx)
{
struct epitems_head *p;

- for (p = ctx->tfile_check_list; p; p = p->next) {
+ for (p = ctx->tfile_check_list; p != EP_UNACTIVE_PTR; p = p->next) {
int error;
path_count_init(ctx);
rcu_read_lock();
@@ -2438,7 +2438,7 @@ static int ep_loop_check(struct ep_ctl_ctx *ctx, struct eventpoll *ep,
static void clear_tfile_check_list(struct ep_ctl_ctx *ctx)
{
rcu_read_lock();
- while (ctx->tfile_check_list) {
+ while (ctx->tfile_check_list != EP_UNACTIVE_PTR) {
struct epitems_head *head = ctx->tfile_check_list;
ctx->tfile_check_list = head->next;
unlist_file(head);
@@ -2601,7 +2601,9 @@ int do_epoll_ctl_file(struct file *f, int op, struct epoll_key *tf,
int full_check;
struct eventpoll *ep;
struct epitem *epi;
- struct ep_ctl_ctx ctx = { };
+ struct ep_ctl_ctx ctx = {
+ .tfile_check_list = EP_UNACTIVE_PTR,
+ };

/* The target file descriptor must support poll */
if (!file_can_poll(tf->file))
--
2.43.0