| 0/0 |
2026/01/15 07:44 |
flow |
assessment-kcsan |
21m
Model:
Results: map[Benign:true Confident:true Explanation:The data race occurs between `io_poll_wake()` and `__io_queue_proc()` in `io_uring/poll.c`.
### Analysis:
1. **The Accesses**:
* **Write**: `io_poll_wake()` sets `poll->head = NULL;` (line 415 in the provided source) when it successfully acquires ownership of the request to handle a wakeup. This happens while holding the wait-queue lock of the head being woken.
* **Read**: `__io_queue_proc()` reads `first->head` (line 468) during poll arming to check if the request is already registered on the same wait-queue head. This is an optimization to avoid double-adding the same request to the same wait queue if a driver calls `poll_wait()` multiple times with the same head.
2. **The Race**:
* The race happens when a request is being armed (via `vfs_poll()`) and a concurrent wakeup occurs on the same wait-queue head.
* If `__io_queue_proc()` reads `NULL` because `io_poll_wake()` just cleared it, the equality check `if (first->head == head)` fails. Instead of returning early, the function proceeds to allocate a new poll entry.
* However, the code then calls `io_poll_double_prepare(req)`, which performs a more robust check using `smp_load_acquire(&poll->head)`. If it sees `NULL`, it returns `false`, and `__io_queue_proc()` safely backs off and cleans up.
* Even if `io_poll_double_prepare()` were to proceed, the `io_uring` request ownership model (managed via `poll_refs` and atomic operations) ensures that only one task (either the arming task or the wakeup task) actually handles the request's completion.
3. **Benign Nature**:
* The race is on a pointer used for an optimization check.
* The `io_uring` poll logic is designed to handle such races through its ownership state machine (`poll_refs`).
* The concurrent access does not lead to memory corruption, null pointer dereferences, or invalid state transitions, as the subsequent logic correctly handles the "too late" or "already fired" scenarios.
* On the architecture involved (x86_64), aligned 64-bit pointer reads and writes are atomic, preventing the observation of partial pointers.
### Conclusion:
This is a benign data race. It is a known pattern in `io_uring` where certain checks are performed locklessly for performance, with the actual synchronization happening later via atomic state variables. While adding `READ_ONCE`/`WRITE_ONCE` would satisfy KCSAN, the race itself does not cause harmful behavior.]
|
| 1/1 |
2026/01/15 07:44 |
action |
kernel-checkouter |
0m
Model:
Results: map[KernelSrc:/usr/local/google/home/dvyukov/syzkaller/agent/workdir/cache/src/2db1e38270a2314670d8d38645c327cf48f265af]
|
| 2/1 |
2026/01/15 07:45 |
action |
kernel-builder |
3m
Model:
Results: map[KernelObj:/usr/local/google/home/dvyukov/syzkaller/agent/workdir/cache/build/819cc8d8afc51fdd173e504f156cc08192df3ee5]
|
| 3/1 |
2026/01/15 07:49 |
action |
codesearch-prepare |
13m
Model:
Results: map[Index:codesearch-index]
|
| 4/1 |
2026/01/15 08:03 |
agent |
expert |
3m
Model:
Results: map[Benign:true Confident:true]
Instruction:
You are an experienced Linux kernel developer tasked with determining if the given kernel
data race is benign or not. The data race report is from KCSAN tool.
It contains 2 stack traces of the memory accesses that constitute a data race.
A "benign" data races are on a simple int/bool variable or similar field,
and the accesses are not supposed to be protected by any mutual exclusion primitives.
Common examples of such "benign" data races are accesses to various flags fields,
statistics counters, and similar. A "benign" data race does not lead to memory corruption/crash
with a conservative compiler that compiles memory accesses to primitive types
effectively as atomic.
A non-benign (or "harmful" data race) can lead to corruption/crash even with
a conservative compiler that compiles memory accesses to primitive types
effectively as atomic. A common example of a "harmful" data races is race on
a complex container (list/hashmap/etc), where accesses are supposed to be protected
by a mutual exclusion primitive.
In the final reply explain why you think the given data race is benign or is harmful.
Use the provided tools to confirm any assumptions, variables/fields being accessed, etc.
In particular, don't make assumptions about the kernel source code,
use codesearch tools to read the actual source code.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The data race report is:
==================================================================
BUG: KCSAN: data-race in __io_queue_proc / io_poll_wake
write to 0xffff88817b26a428 of 8 bytes by task 1350 on cpu 1:
io_poll_wake+0x1f9/0x3b0 io_uring/poll.c:416
__wake_up_common kernel/sched/wait.c:108 [inline]
__wake_up_common_lock kernel/sched/wait.c:125 [inline]
__wake_up+0x66/0xb0 kernel/sched/wait.c:146
__receive_buf drivers/tty/n_tty.c:1641 [inline]
n_tty_receive_buf_common+0x735/0xbe0 drivers/tty/n_tty.c:1723
n_tty_receive_buf2+0x33/0x40 drivers/tty/n_tty.c:1769
tty_ldisc_receive_buf+0x66/0xf0 drivers/tty/tty_buffer.c:387
tty_port_default_receive_buf+0x59/0x90 drivers/tty/tty_port.c:37
receive_buf drivers/tty/tty_buffer.c:445 [inline]
flush_to_ldisc+0x148/0x340 drivers/tty/tty_buffer.c:495
process_one_work kernel/workqueue.c:3263 [inline]
process_scheduled_works+0x4ce/0x9d0 kernel/workqueue.c:3346
worker_thread+0x582/0x770 kernel/workqueue.c:3427
kthread+0x489/0x510 kernel/kthread.c:463
ret_from_fork+0x122/0x1b0 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
read to 0xffff88817b26a428 of 8 bytes by task 15675 on cpu 0:
__io_queue_proc+0x10a/0x280 io_uring/poll.c:469
io_async_queue_proc+0x43/0x50 io_uring/poll.c:632
poll_wait include/linux/poll.h:45 [inline]
n_tty_poll+0x7a/0x450 drivers/tty/n_tty.c:2451
tty_poll+0x79/0xf0 drivers/tty/tty_io.c:2199
vfs_poll include/linux/poll.h:82 [inline]
__io_arm_poll_handler+0x1ee/0xb70 io_uring/poll.c:579
io_arm_apoll+0x2e8/0x420 io_uring/poll.c:687
io_arm_poll_handler+0x131/0x160 io_uring/poll.c:716
io_queue_async+0x25d/0x2d0 io_uring/io_uring.c:2055
io_queue_sqe io_uring/io_uring.c:2081 [inline]
io_req_task_submit+0xf6/0x100 io_uring/io_uring.c:1466
io_poll_task_func+0x5fc/0x7c0 io_uring/poll.c:-1
io_handle_tw_list+0xcc/0x1c0 io_uring/io_uring.c:1151
tctx_task_work_run+0x6d/0x1a0 io_uring/io_uring.c:1216
tctx_task_work+0x3f/0x80 io_uring/io_uring.c:1234
task_work_run+0x131/0x1a0 kernel/task_work.c:227
get_signal+0xe12/0xf70 kernel/signal.c:2807
arch_do_signal_or_restart+0x96/0x440 arch/x86/kernel/signal.c:337
exit_to_user_mode_loop+0x77/0x110 kernel/entry/common.c:40
exit_to_user_mode_prepare include/linux/irq-entry-common.h:225 [inline]
syscall_exit_to_user_mode_work include/linux/entry-common.h:175 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:210 [inline]
do_syscall_64+0x1d6/0x200 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
value changed: 0xffff888118df7208 -> 0x0000000000000000
Reported by Kernel Concurrency Sanitizer on:
CPU: 0 UID: 0 PID: 15675 Comm: syz.7.16077 Not tainted syzkaller #0 PREEMPT(voluntary)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 10/02/2025
==================================================================
Reply: The data race occurs between `io_poll_wake()` and `__io_queue_proc()` in `io_uring/poll.c`.
### Analysis:
1. **The Accesses**:
* **Write**: `io_poll_wake()` sets `poll->head = NULL;` (line 415 in the provided source) when it successfully acquires ownership of the request to handle a wakeup. This happens while holding the wait-queue lock of the head being woken.
* **Read**: `__io_queue_proc()` reads `first->head` (line 468) during poll arming to check if the request is already registered on the same wait-queue head. This is an optimization to avoid double-adding the same request to the same wait queue if a driver calls `poll_wait()` multiple times with the same head.
2. **The Race**:
* The race happens when a request is being armed (via `vfs_poll()`) and a concurrent wakeup occurs on the same wait-queue head.
* If `__io_queue_proc()` reads `NULL` because `io_poll_wake()` just cleared it, the equality check `if (first->head == head)` fails. Instead of returning early, the function proceeds to allocate a new poll entry.
* However, the code then calls `io_poll_double_prepare(req)`, which performs a more robust check using `smp_load_acquire(&poll->head)`. If it sees `NULL`, it returns `false`, and `__io_queue_proc()` safely backs off and cleans up.
* Even if `io_poll_double_prepare()` were to proceed, the `io_uring` request ownership model (managed via `poll_refs` and atomic operations) ensures that only one task (either the arming task or the wakeup task) actually handles the request's completion.
3. **Benign Nature**:
* The race is on a pointer used for an optimization check.
* The `io_uring` poll logic is designed to handle such races through its ownership state machine (`poll_refs`).
* The concurrent access does not lead to memory corruption, null pointer dereferences, or invalid state transitions, as the subsequent logic correctly handles the "too late" or "already fired" scenarios.
* On the architecture involved (x86_64), aligned 64-bit pointer reads and writes are atomic, preventing the observation of partial pointers.
### Conclusion:
This is a benign data race. It is a known pattern in `io_uring` where certain checks are performed locklessly for performance, with the actual synchronization happening later via atomic state variables. While adding `READ_ONCE`/`WRITE_ONCE` would satisfy KCSAN, the race itself does not cause harmful behavior.
|
| 5/2 |
2026/01/15 08:03 |
llm |
expert |
0m
Model:
Thoughts: **Analyzing a Data Race in the I/O Polling Mechanism**
Okay, so I've got a report here flagging a data race. It looks like we have a conflict between a write operation in `io_poll_wake` and a read operation in `__io_queue_proc`. The conflicting memory address is `0xffff88817b26a428`. The write is happening within `io_uring/poll.c` at line 416, specifically in the `io_poll_wake` function. The corresponding read is in the `__io_queue_proc` function, also in `io_uring/poll.c`, but at line 469. My immediate thought is to dive into the source code for both of those functions and get a handle on what's going on. Let's see if we can pinpoint the critical section where this race is occurring and then formulate a solution. This should be a relatively contained issue, based on the context.
|
| 6/2 |
2026/01/15 08:03 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_wake SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 389: static int io_poll_wake(struct wait_queue_entry *wait, unsigned mode, int sync,
390: void *key)
391: {
392: struct io_kiocb *req = wqe_to_req(wait);
393: struct io_poll *poll = container_of(wait, struct io_poll, wait);
394: __poll_t mask = key_to_poll(key);
395:
396: if (unlikely(mask & POLLFREE))
397: return io_pollfree_wake(req, poll);
398:
399: /* for instances that support it check for an event match first */
400: if (mask && !(mask & (poll->events & ~IO_ASYNC_POLL_COMMON)))
401: return 0;
402:
403: if (io_poll_get_ownership(req)) {
404: /*
405: * If we trigger a multishot poll off our own wakeup path,
406: * disable multishot as there is a circular dependency between
407: * CQ posting and triggering the event.
408: */
409: if (mask & EPOLL_URING_WAKE)
410: poll->events |= EPOLLONESHOT;
411:
412: /* optional, saves extra locking for removal in tw handler */
413: if (mask && poll->events & EPOLLONESHOT) {
414: list_del_init(&poll->wait.entry);
415: poll->head = NULL;
416: if (wqe_is_double(wait))
417: req->flags &= ~REQ_F_DOUBLE_POLL;
418: else
419: req->flags &= ~REQ_F_SINGLE_POLL;
420: }
421: __io_poll_execute(req, mask);
422: }
423: return 1;
424: }
SourceFile:io_uring/poll.c]
|
| 7/2 |
2026/01/15 08:03 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:__io_queue_proc SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 452: static void __io_queue_proc(struct io_poll *poll, struct io_poll_table *pt,
453: struct wait_queue_head *head,
454: struct io_poll **poll_ptr)
455: {
456: struct io_kiocb *req = pt->req;
457: unsigned long wqe_private = (unsigned long) req;
458:
459: /*
460: * The file being polled uses multiple waitqueues for poll handling
461: * (e.g. one for read, one for write). Setup a separate io_poll
462: * if this happens.
463: */
464: if (unlikely(pt->nr_entries)) {
465: struct io_poll *first = poll;
466:
467: /* double add on the same waitqueue head, ignore */
468: if (first->head == head)
469: return;
470: /* already have a 2nd entry, fail a third attempt */
471: if (*poll_ptr) {
472: if ((*poll_ptr)->head == head)
473: return;
474: pt->error = -EINVAL;
475: return;
476: }
477:
478: poll = kmalloc(sizeof(*poll), GFP_ATOMIC);
479: if (!poll) {
480: pt->error = -ENOMEM;
481: return;
482: }
483:
484: /* mark as double wq entry */
485: wqe_private |= IO_WQE_F_DOUBLE;
486: io_init_poll_iocb(poll, first->events);
487: if (!io_poll_double_prepare(req)) {
488: /* the request is completing, just back off */
489: kfree(poll);
490: return;
491: }
492: *poll_ptr = poll;
493: } else {
494: /* fine to modify, there is no poll queued to race with us */
495: req->flags |= REQ_F_SINGLE_POLL;
496: }
497:
498: pt->nr_entries++;
499: poll->head = head;
500: poll->wait.private = (void *) wqe_private;
501:
502: if (poll->events & EPOLLEXCLUSIVE) {
503: add_wait_queue_exclusive(head, &poll->wait);
504: } else {
505: add_wait_queue(head, &poll->wait);
506: }
507: }
SourceFile:io_uring/poll.c]
|
| 8/2 |
2026/01/15 08:03 |
llm |
expert |
0m
Model:
Thoughts: **Dissecting a Potential io_uring/poll.c Bug**
Okay, so I'm digging into a reported issue within the `io_uring` subsystem, specifically involving `io_poll_wake` and `__io_queue_proc`. The report points to a write at line 416 of `io_poll_wake` and a read at line 469 of `__io_queue_proc`. My initial instinct was to jump into the code and verify this.
First, I had to double-check those line numbers. My local copy of the code from `codesearch-definition-source` gave me line 415 as the potential write, specifically `poll->head = NULL;`, and line 468 as the read, `if (first->head == head)`. I initially thought I might have an older version, but the report explicitly mentions the specific offsets within the functions: `io_poll_wake+0x1f9/0x3b0` and `__io_queue_proc+0x10a/0x280`. It seems the minor discrepancy is down to how the tool is pointing to the line.
The report highlights a memory write of 8 bytes from `io_poll_wake` on CPU 1 to address `0xffff88817b26a428` and a read of 8 bytes from `__io_queue_proc` on CPU 0 from the same address, which corresponds to the comparison of `poll->head` value. Crucially, the reported value change from `0xffff888118df7208` to `0x0000000000000000`, strongly suggests that the reported write at line 415 of `io_poll_wake` is the source of the issue.
Now, I need to look closely around these lines. The report seems to confirm the core of my suspicion regarding a race condition related to `poll->head` being set to `NULL` in the wake function. I'll need to analyze the surrounding code in both functions, taking into account the potential for inter-processor communication and synchronization issues. I should check how `poll->head` is initialized and used elsewhere, looking for potential double-free or use-after-free scenarios.
|
| 9/2 |
2026/01/15 08:03 |
tool |
codesearch-file-index |
0m
Model:
Args: map[SourceFile:io_uring/poll.c]
Results: map[Entities:[map[Kind:function Name:__io_arm_poll_handler] map[Kind:function Name:__io_poll_cancel] map[Kind:function Name:__io_poll_execute] map[Kind:function Name:__io_queue_proc] map[Kind:function Name:io_arm_apoll] map[Kind:function Name:io_arm_poll_handler] map[Kind:function Name:io_async_queue_proc] map[Kind:function Name:io_init_poll_iocb] map[Kind:function Name:io_poll_add] map[Kind:function Name:io_poll_add_hash] map[Kind:function Name:io_poll_add_prep] map[Kind:function Name:io_poll_can_finish_inline] map[Kind:function Name:io_poll_cancel] map[Kind:function Name:io_poll_cancel_req] map[Kind:function Name:io_poll_check_events] map[Kind:function Name:io_poll_disarm] map[Kind:function Name:io_poll_double_prepare] map[Kind:function Name:io_poll_execute] map[Kind:function Name:io_poll_file_find] map[Kind:function Name:io_poll_find] map[Kind:function Name:io_poll_get_double] map[Kind:function Name:io_poll_get_ownership] map[Kind:function Name:io_poll_get_ownership_slowpath] map[Kind:function Name:io_poll_get_single] map[Kind:function Name:io_poll_mark_cancelled] map[Kind:function Name:io_poll_parse_events] map[Kind:function Name:io_poll_queue_proc] map[Kind:function Name:io_poll_remove] map[Kind:function Name:io_poll_remove_all] map[Kind:function Name:io_poll_remove_entries] map[Kind:function Name:io_poll_remove_entry] map[Kind:function Name:io_poll_remove_prep] map[Kind:function Name:io_poll_req_insert] map[Kind:function Name:io_poll_task_func] map[Kind:function Name:io_poll_wake] map[Kind:function Name:io_poll_wake] map[Kind:function Name:io_pollfree_wake] map[Kind:function Name:io_req_alloc_apoll] map[Kind:function Name:wqe_is_double] map[Kind:function Name:wqe_to_req]] Missing:false]
|
| 10/2 |
2026/01/15 08:03 |
llm |
expert |
0m
Model:
Thoughts: Okay, let's dive back into this `io_poll_wake` function. I need to make sure I haven't missed anything. I'm focusing on lines 415 and 416. Line 415 is clearing `poll->head`, which seems straightforward. But that conditional on line 416, `if (wqe_is_double(wait))`, has me thinking. *Wait a minute*, if that `wqe_is_double` is an inline function or, even worse, a macro, it could be accessing the `wait->private` member. I need to double-check that; any subtle change there could introduce a difficult bug. I need to go look up the definition of `wqe_is_double` immediately to understand what it's doing. Let's see how that's implemented.
|
| 11/2 |
2026/01/15 08:03 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:wqe_is_double SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 65: static inline bool wqe_is_double(struct wait_queue_entry *wqe)
66: {
67: unsigned long priv = (unsigned long)wqe->private;
68:
69: return priv & IO_WQE_F_DOUBLE;
70: }
SourceFile:io_uring/poll.c]
|
| 12/2 |
2026/01/15 08:03 |
llm |
expert |
0m
Model:
Thoughts: Here's my thought process, as if I were the one encountering this situation:
**Decoding Memory Addresses**
Okay, let's break this down. The report mentions an 8-byte write, and we're dealing with a pointer, specifically `poll->head`. It's pretty standard that pointers are 8 bytes on a 64-bit system, which makes sense. So, `poll->head`'s size being 8 bytes lines up with expectation. Now, if I understand correctly, we know `poll->head` is at offset 40 within the `struct io_poll`. That's the critical piece of information. Where does `poll` actually start?
Right, the reported address is `0xffff88817b26a428`. This looks like a kernel virtual address. Now, let me quickly double-check the definition of `struct io_poll` to confirm the offset of `poll->head` and gain a bit more context. I need to refresh myself on the layout to interpret this memory address correctly. This should clarify what's going on with this write.
|
| 13/2 |
2026/01/15 08:03 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll SourceFile:io_uring/poll.c]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 14/2 |
2026/01/15 08:03 |
llm |
expert |
0m
Model:
Thoughts: Okay, here's my thought process on tracking down that `io_poll` definition, rendered as a first-person monologue, keeping in mind my field expertise:
**Hunting Down the `io_poll` Definition in io_uring**
Alright, I'm trying to figure out where `io_poll` is defined. My initial gut feeling is to check the usual suspects in the `io_uring` codebase, specifically around polling. I'll start by looking in `io_uring/poll.h` or somewhere similar, like a dedicated poll header file. It's often neatly organized that way.
Hmm, wait a second, let me quickly broaden my search. It could be a more core `io_uring` struct defined in `io_uring/io_uring.h` or something similar, the main header. Sometimes things get tucked away in unexpected places, especially in a framework as complex as `io_uring`.
Okay, I'm thinking maybe it is a struct defined in `io_uring/poll.c`, and I just missed the definition or it's named slightly differently. Time to double-check that `poll.c` file, but this time I'm specifically looking for a *struct* definition. I need to be more precise in my search terms.
I used `codesearch-file-index`, which didn't show it as a struct? That seems odd.
Hold on, I remember seeing `io_poll` used within the `io_poll_wake` function. That's a good clue. If it's used in a function, it's *got* to be declared somewhere.
Alright, let's cut the guesswork and go straight for the jugular. I'm going to search the code directly for `struct io_poll {`. That should nail down the struct definition, assuming it's structured in the standard C way. I need the definition and not just the name.
|
| 15/2 |
2026/01/15 08:03 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll SourceFile:io_uring/poll.c]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 16/2 |
2026/01/15 08:03 |
llm |
expert |
0m
Model:
Thoughts: **Hunting Down That io_uring Definition**
Okay, so I'm trying to track down where this specific `io_uring` definition lives. My initial hunch is that it's probably tucked away in `io_uring/io_uring_types.h`. That's the most common location for these core structures. But, before I get too invested in that specific file, let me be a bit more efficient. It's often faster to just quickly `grep` for the definition or use `codesearch-definition-source` with just the name of the structure, rather than the file, to find its definition. Now the tool requires a filename, but I can work around that.
So, first I will check if itβs defined in `io_uring/io_uring.c`. If not, I can just try the search I mentioned earlier, but I am betting it is in `io_uring/io_uring_types.h`.
|
| 17/2 |
2026/01/15 08:03 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll SourceFile:io_uring/io_uring_types.h]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 18/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: Okay, here's the summary, tailored for an expert audience, in the first person:
**Diving into `io_uring/poll.h`**
Alright, let's get down to brass tacks. My initial thought process is to zero in on `io_uring/poll.h`. That header file is the logical starting point, considering my understanding of the architecture, where polling operations interface with the core io_uring mechanism. It's the nexus of how we bridge the asynchronous, event-driven nature of io_uring with traditional polling semantics. I'm expecting to find the definitions for the structures and constants that tie into `io_uring`'s polling capabilities β the mechanisms for handling readiness notifications and how they map onto the user-space and kernel interfaces. I'll likely be particularly interested in the interaction with the internal ring buffer structures. It's here I expect to find the building blocks for creating a robust and efficient asynchronous event handling system leveraging io_uring's strengths.
|
| 19/2 |
2026/01/15 08:04 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll SourceFile:io_uring/poll.h]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 20/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: **Deducing the Structure of `io_poll`**
Okay, so I'm digging into this `io_uring` code, and something's bugging me. I was initially unsure if `io_poll` was *really* a struct, or if it was just being used *like* one. Then I saw the `container_of` macro in `io_poll_wake`, specifically: `struct io_poll *poll = container_of(wait, struct io_poll, wait);`. That's a pretty strong clue, I'm betting that's definitive proof.
Now I'm certain that `io_poll` is indeed a structure. So, where's it defined? That's the next logical step. The best approach is to go back to the source - the beginning of `io_uring/poll.c` is the most logical place to start looking for the struct's definition. Let's see what's what.
|
| 21/2 |
2026/01/15 08:04 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_wake SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 389: static int io_poll_wake(struct wait_queue_entry *wait, unsigned mode, int sync,
390: void *key)
391: {
392: struct io_kiocb *req = wqe_to_req(wait);
393: struct io_poll *poll = container_of(wait, struct io_poll, wait);
394: __poll_t mask = key_to_poll(key);
395:
396: if (unlikely(mask & POLLFREE))
397: return io_pollfree_wake(req, poll);
398:
399: /* for instances that support it check for an event match first */
400: if (mask && !(mask & (poll->events & ~IO_ASYNC_POLL_COMMON)))
401: return 0;
402:
403: if (io_poll_get_ownership(req)) {
404: /*
405: * If we trigger a multishot poll off our own wakeup path,
406: * disable multishot as there is a circular dependency between
407: * CQ posting and triggering the event.
408: */
409: if (mask & EPOLL_URING_WAKE)
410: poll->events |= EPOLLONESHOT;
411:
412: /* optional, saves extra locking for removal in tw handler */
413: if (mask && poll->events & EPOLLONESHOT) {
414: list_del_init(&poll->wait.entry);
415: poll->head = NULL;
416: if (wqe_is_double(wait))
417: req->flags &= ~REQ_F_DOUBLE_POLL;
418: else
419: req->flags &= ~REQ_F_SINGLE_POLL;
420: }
421: __io_poll_execute(req, mask);
422: }
423: return 1;
424: }
SourceFile:io_uring/poll.c]
|
| 22/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: Okay, let's see, I need to find the definition of `struct io_poll`. I've looked in `io_uring/io_uring_types.h` before; perhaps I just missed it in my initial scan? I'll give it another shot, I'm pretty sure it's in there somewhere.
Wait a second... hold on. I'm being inefficient. Instead of just re-scanning, I can just use `codesearch-file-index` directly on `io_uring/io_uring_types.h`. That'll be a much faster and more precise search. This is the way to do it. No more manual sifting!
|
| 23/2 |
2026/01/15 08:04 |
tool |
codesearch-file-index |
0m
Model:
Args: map[SourceFile:io_uring/io_uring_types.h]
Results: map[Entities:<nil> Missing:true]
|
| 24/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: Okay, here's the summary, rendered as your internal thought process:
**Investigating a Missing Header**
Hmm, hold on a second. I'm getting a "missing" error for `io_uring/io_uring_types.h`. That's highly unusual. It should definitely be there if the build system is configured correctly and if the io_uring headers are installed properly. That's a red flag.
Right, let me think this through. Perhaps I have the wrong include path? Or maybe the filename is slightly different. Is there a typo somewhere? It's not uncommon to misremember things. My immediate thought is to check if it's actually `io_uring/io_uring.h` instead. Or, could it be something else entirely within the io_uring ecosystem?
Best course of action now is to quickly list the contents of the `io_uring/` directory to make absolutely sure what's actually available. That should quickly clear things up. No need to waste time guessing; let's get some concrete information before I dig into this any further.
|
| 25/2 |
2026/01/15 08:04 |
tool |
codesearch-file-index |
0m
Model:
Args: map[SourceFile:io_uring/io_uring.c]
Results: map[Entities:[map[Kind:function Name:__do_sys_io_uring_enter] map[Kind:function Name:__do_sys_io_uring_enter] map[Kind:function Name:__do_sys_io_uring_setup] map[Kind:function Name:__do_sys_io_uring_setup] map[Kind:function Name:__ia32_sys_io_uring_enter] map[Kind:function Name:__ia32_sys_io_uring_setup] map[Kind:function Name:__io_alloc_req_refill] map[Kind:function Name:__io_commit_cqring_flush] map[Kind:function Name:__io_cq_lock] map[Kind:function Name:__io_cq_unlock_post] map[Kind:function Name:__io_cqring_events] map[Kind:function Name:__io_cqring_events_user] map[Kind:function Name:__io_cqring_overflow_flush] map[Kind:function Name:__io_cqring_wait_schedule] map[Kind:function Name:__io_fallback_tw] map[Kind:function Name:__io_issue_sqe] map[Kind:function Name:__io_prep_linked_timeout] map[Kind:function Name:__io_req_caches_free] map[Kind:function Name:__io_req_caches_free] map[Kind:function Name:__io_req_find_next_prep] map[Kind:function Name:__io_req_task_work_add] map[Kind:function Name:__io_run_local_work] map[Kind:function Name:__io_run_local_work_loop] map[Kind:function Name:__io_submit_flush_completions] map[Kind:function Name:__io_uring_cancel] map[Kind:function Name:__se_sys_io_uring_enter] map[Kind:function Name:__se_sys_io_uring_setup] map[Kind:function Name:__x64_sys_io_uring_enter] map[Kind:function Name:__x64_sys_io_uring_setup] map[Kind:function Name:ctx_flush_and_put] map[Kind:function Name:current_pending_io] map[Kind:function Name:io_activate_pollwq] map[Kind:function Name:io_activate_pollwq_cb] map[Kind:function Name:io_add_aux_cqe] map[Kind:function Name:io_alloc_hash_table] map[Kind:function Name:io_alloc_ocqe] map[Kind:function Name:io_allocate_scq_urings] map[Kind:function Name:io_assign_file] map[Kind:function Name:io_cancel_ctx_cb] map[Kind:function Name:io_cancel_defer_files] map[Kind:function Name:io_cancel_task_cb] map[Kind:function Name:io_check_restriction] map[Kind:function Name:io_clean_op] map[Kind:function Name:io_commit_sqring] map[Kind:function Name:io_cq_lock] map[Kind:function Name:io_cq_unlock_post] map[Kind:function Name:io_cqe_cache_refill] map[Kind:function Name:io_cqe_overflow] map[Kind:function Name:io_cqe_overflow_locked] map[Kind:function Name:io_cqring_add_overflow] map[Kind:function Name:io_cqring_do_overflow_flush] map[Kind:function Name:io_cqring_events] map[Kind:function Name:io_cqring_min_timer_wakeup] map[Kind:function Name:io_cqring_overflow_kill] map[Kind:function Name:io_cqring_schedule_timeout] map[Kind:function Name:io_cqring_timer_wakeup] map[Kind:function Name:io_cqring_wait] map[Kind:function Name:io_cqring_wait_schedule] map[Kind:function Name:io_drain_req] map[Kind:function Name:io_fallback_req_func] map[Kind:function Name:io_fallback_tw] map[Kind:function Name:io_file_get_fixed] map[Kind:function Name:io_file_get_flags] map[Kind:function Name:io_file_get_normal] map[Kind:function Name:io_fill_cqe_aux] map[Kind:function Name:io_fill_cqe_aux32] map[Kind:function Name:io_fill_nop_cqe] map[Kind:function Name:io_free_alloc_caches] map[Kind:function Name:io_free_batch_list] map[Kind:function Name:io_free_req] map[Kind:function Name:io_get_ext_arg] map[Kind:function Name:io_get_ext_arg_reg] map[Kind:function Name:io_get_sqe] map[Kind:function Name:io_handle_tw_list] map[Kind:function Name:io_init_cqe] map[Kind:function Name:io_init_drain] map[Kind:function Name:io_init_fail_req] map[Kind:function Name:io_init_req] map[Kind:function Name:io_iopoll_check] map[Kind:function Name:io_iopoll_req_issued] map[Kind:function Name:io_iopoll_try_reap_events] map[Kind:function Name:io_is_uring_fops] map[Kind:function Name:io_issue_sqe] map[Kind:function Name:io_linked_nr] map[Kind:function Name:io_match_linked] map[Kind:function Name:io_match_task_safe] map[Kind:function Name:io_move_task_work_from_local] map[Kind:function Name:io_poison_cached_req] map[Kind:function Name:io_poison_req] map[Kind:function Name:io_poll_issue] map[Kind:function Name:io_post_aux_cqe] map[Kind:function Name:io_prep_async_link] map[Kind:function Name:io_prep_async_work] map[Kind:function Name:io_put_task] map[Kind:function Name:io_queue_async] map[Kind:function Name:io_queue_deferred] map[Kind:function Name:io_queue_iowq] map[Kind:function Name:io_queue_next] map[Kind:function Name:io_queue_sqe] map[Kind:function Name:io_queue_sqe] map[Kind:function Name:io_queue_sqe_fallback] map[Kind:function Name:io_req_add_to_cache] map[Kind:function Name:io_req_caches_free] map[Kind:function Name:io_req_complete_post] map[Kind:function Name:io_req_defer_failed] map[Kind:function Name:io_req_find_next] map[Kind:function Name:io_req_local_work_add] map[Kind:function Name:io_req_normal_work_add] map[Kind:function Name:io_req_post_cqe] map[Kind:function Name:io_req_post_cqe32] map[Kind:function Name:io_req_put_rsrc_nodes] map[Kind:function Name:io_req_queue_iowq] map[Kind:function Name:io_req_queue_iowq_tw] map[Kind:function Name:io_req_sqe_copy] map[Kind:function Name:io_req_task_cancel] map[Kind:function Name:io_req_task_complete] map[Kind:function Name:io_req_task_queue] map[Kind:function Name:io_req_task_queue_fail] map[Kind:function Name:io_req_task_submit] map[Kind:function Name:io_req_task_work_add_remote] map[Kind:function Name:io_req_track_inflight] map[Kind:function Name:io_ring_ctx_alloc] map[Kind:function Name:io_ring_ctx_free] map[Kind:function Name:io_ring_ctx_ref_free] map[Kind:function Name:io_ring_ctx_wait_and_kill] map[Kind:function Name:io_ring_exit_work] map[Kind:function Name:io_rings_free] map[Kind:function Name:io_run_local_work] map[Kind:function Name:io_run_local_work_continue] map[Kind:function Name:io_run_local_work_locked] map[Kind:function Name:io_run_task_work_sig] map[Kind:function Name:io_submit_fail_init] map[Kind:function Name:io_submit_sqe] map[Kind:function Name:io_submit_sqes] map[Kind:function Name:io_submit_state_end] map[Kind:function Name:io_submit_state_start] map[Kind:function Name:io_task_refs_refill] map[Kind:function Name:io_tctx_exit_cb] map[Kind:function Name:io_uring_allowed] map[Kind:function Name:io_uring_cancel_generic] map[Kind:function Name:io_uring_create] map[Kind:function Name:io_uring_drop_tctx_refs] map[Kind:function Name:io_uring_fill_params] map[Kind:function Name:io_uring_get_file] map[Kind:function Name:io_uring_init] map[Kind:function Name:io_uring_install_fd] map[Kind:function Name:io_uring_poll] map[Kind:function Name:io_uring_release] map[Kind:function Name:io_uring_sanitise_params] map[Kind:function Name:io_uring_setup] map[Kind:function Name:io_uring_try_cancel_iowq] map[Kind:function Name:io_uring_try_cancel_requests] map[Kind:function Name:io_uring_try_cancel_requests] map[Kind:function Name:io_validate_ext_arg] map[Kind:function Name:io_wake_function] map[Kind:function Name:io_wq_free_work] map[Kind:function Name:io_wq_submit_work] map[Kind:function Name:req_fail_link_node] map[Kind:function Name:rings_size] map[Kind:function Name:tctx_inflight] map[Kind:function Name:tctx_task_work] map[Kind:function Name:tctx_task_work_run]] Missing:false]
|
| 26/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: **Navigating the io_uring Source Code**
Alright, let's dive in. My initial hunch is to start with `io_uring.c`, since that's where a lot of the core functionality likely resides. I'm specifically looking for the definition of `struct io_kiocb`, because `io_poll` is likely integrated with that or at least related. However, before I start down that path, it might be quicker to just search for `struct io_poll {` across the entire codebase. Hmm, it looks like a global search isn't possible here. Okay, let's try `io_uring/io_uring_types.h` again, maybe I had a typo the first time. Ah, the `codesearch-file-index` is saying it's missing entirely! Let's pivot and try `io_uring/io_uring.h` instead. Perhaps the definition resides there.
|
| 27/2 |
2026/01/15 08:04 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll SourceFile:io_uring/io_uring.h]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 28/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: Okay, here's the summary, tailored for an expert audience, in the first person:
**Initial Investigation: The `io_kiocb` Search**
Alright, let's get down to brass tacks. I'm going to start with the obvious and hunt for instances of `io_kiocb`. That's the core of the asynchronous I/O machinery in the kernel, so it's the logical jumping-off point for this kind of investigation. I'm expecting this will quickly lead me down a rabbit hole, but this search should help map the terrain and narrow down the specific area of concern. I'm anticipating a wide range of results, from the structure definition and manipulation functions to its use in block device drivers, network layers, and filesystems. The plan is to see how the code is using the core struct and associated interfaces to get a better handle on the big picture. From there, I can drill down into the specific parts of the code I'm most interested in, based on the hits I get from the initial search. This will give me a good lay of the land, so I can start to formulate a more targeted approach.
|
| 29/2 |
2026/01/15 08:04 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_kiocb SourceFile:io_uring/io_uring.c]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 30/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: **Dissecting a Potential io_uring Poll Race**
Okay, so I'm chasing down a race condition related to `io_uring`'s polling mechanism, specifically around `io_kiocb` and wait queues. I started by looking for `io_kiocb` in the standard places, but quickly realized the new stuff is likely in `io_uring/`. Let me make sure I'm thinking right by listing the files... oh, I can't directly list the directory. So I'll go back to what I know.
I've already looked at `io_uring/poll.c`, which uses `io_poll`. Time to examine `__io_queue_proc` more closely because I remember we were looking at it. Line 468 (`first->head == head`) is where the read happens for Task 15675. Meanwhile, Task 1350 is writing `poll->head = NULL` at line 415 in `io_poll_wake`. The `first` argument in `__io_queue_proc` refers to the `poll` argument.
The race seems to hinge on `__io_queue_proc` being called for a second wait queue, potentially for the same request. If `pt->nr_entries` is non-zero, it means a second poll entry is being added. `first` is the *first* `io_poll` entry. Essentially, `__io_queue_proc` checks if this entry is *already* registered on the same wait queue.
The crucial bit: `io_poll_wake` could be running concurrently, handling an event for the *first* entry. In `io_poll_wake`, if we get ownership (using `io_poll_get_ownership(req)`) and it's a one-shot poll (based on `EPOLLONESHOT`), we clear `poll->head` (line 415) and remove the entry from the list.
If `__io_queue_proc` is running, then we are in the middle of a `poll_wait` call, since `vfs_poll` eventually calls `__io_queue_proc`. So if the race occurs with a wakeup happening during the poll setup, we have a problem.
The potential vulnerability: In `__io_queue_proc`, if the read of `first->head` returns `NULL` (due to the race), the condition `first->head == head` becomes false (assuming head is not NULL). Therefore, we'll incorrectly proceed to add a *second* `poll` entry to the same wait queue.
Now, if `io_poll_wake` already cleared `poll->head` and is handling the event, that means the first entry is being removed. So now, if `__io_queue_proc` continues, it's adding a *new* entry to the same `head`. But `io_poll_wake` got the ownership already. So we are in the process of completing the request by calling `__io_poll_execute(req, mask)`.
`__io_queue_proc` is being called *during* the initial poll setup. I can confirm this by looking at the stack trace: `__io_arm_poll_handler` -> `io_arm_apoll` -> `io_arm_poll_handler` -> `io_queue_async`...
Okay, so the race is the following: if `io_poll_wake` runs while `__io_queue_proc` is setting up, it means an event has already happened and is waking us. But `__io_queue_proc` is being called for a *second* entry, meaning the first entry has already been added to the waitqueue. If it got to the return statement, it should have returned. But because of the race, it reads `NULL`, continues, and adds an *additional* poll entry to the *same* `head`. And it allocates a *new* `io_poll` on line 478.
If `first->head` was `NULL`, it means the first entry is already being woken up. If `io_poll_wake` already set `poll->head = NULL`, it also did `list_del_init(&poll->wait.entry)`. So the first entry is *no longer* in the wait queue.
So, if `__io_queue_proc` continues, it adds a *new* poll entry to the same `head`, even though `io_poll_wake` already has called `io_poll_get_ownership(req)`. It will continue on to `__io_poll_execute(req, mask)`.
The consequence is that the request ends up with multiple poll entries
|
| 31/2 |
2026/01/15 08:04 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_get_ownership SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 87: /*
88: * If refs part of ->poll_refs (see IO_POLL_REF_MASK) is 0, it's free. We can
89: * bump it and acquire ownership. It's disallowed to modify requests while not
90: * owning it, that prevents from races for enqueueing task_work's and b/w
91: * arming poll and wakeups.
92: */
93: static inline bool io_poll_get_ownership(struct io_kiocb *req)
94: {
95: if (unlikely(atomic_read(&req->poll_refs) >= IO_POLL_REF_BIAS))
96: return io_poll_get_ownership_slowpath(req);
97: return !(atomic_fetch_inc(&req->poll_refs) & IO_POLL_REF_MASK);
98: }
SourceFile:io_uring/poll.c]
|
| 32/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: Okay, here's my interpretation of those thoughts, summarized in the first person, assuming an expert understanding of the topic:
**Ownership and Control in io_uring Polling**
Right, so I'm thinking about how ownership works when we're dealing with polling within an io_uring context. The `poll_refs` structure is clearly designed to handle the ownership of the request details. That makes sense; it needs to know what to keep track of. But the interesting part is how this ownership is *shared* or delegated when we arm the poll with the io_uring. The underlying system, io_uring, also takes over, or at least becomes heavily involved in managing these references from that point on. It's a key interaction. I'll need to go and look into how that happens, in the __io_arm_poll_handler function to understand it fully.
|
| 33/2 |
2026/01/15 08:04 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:__io_arm_poll_handler SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 534: /*
535: * Returns 0 when it's handed over for polling. The caller owns the requests if
536: * it returns non-zero, but otherwise should not touch it. Negative values
537: * contain an error code. When the result is >0, the polling has completed
538: * inline and ipt.result_mask is set to the mask.
539: */
540: static int __io_arm_poll_handler(struct io_kiocb *req,
541: struct io_poll *poll,
542: struct io_poll_table *ipt, __poll_t mask,
543: unsigned issue_flags)
544: {
545: INIT_HLIST_NODE(&req->hash_node);
546: io_init_poll_iocb(poll, mask);
547: poll->file = req->file;
548: req->apoll_events = poll->events;
549:
550: ipt->pt._key = mask;
551: ipt->req = req;
552: ipt->error = 0;
553: ipt->nr_entries = 0;
554: /*
555: * Polling is either completed here or via task_work, so if we're in the
556: * task context we're naturally serialised with tw by merit of running
557: * the same task. When it's io-wq, take the ownership to prevent tw
558: * from running. However, when we're in the task context, skip taking
559: * it as an optimisation.
560: *
561: * Note: even though the request won't be completed/freed, without
562: * ownership we still can race with io_poll_wake().
563: * io_poll_can_finish_inline() tries to deal with that.
564: */
565: ipt->owning = issue_flags & IO_URING_F_UNLOCKED;
566: atomic_set(&req->poll_refs, (int)ipt->owning);
567:
568: /*
569: * Exclusive waits may only wake a limited amount of entries
570: * rather than all of them, this may interfere with lazy
571: * wake if someone does wait(events > 1). Ensure we don't do
572: * lazy wake for those, as we need to process each one as they
573: * come in.
574: */
575: if (poll->events & EPOLLEXCLUSIVE)
576: req->flags |= REQ_F_POLL_NO_LAZY;
577:
578: mask = vfs_poll(req->file, &ipt->pt) & poll->events;
579:
580: if (unlikely(ipt->error || !ipt->nr_entries)) {
581: io_poll_remove_entries(req);
582:
583: if (!io_poll_can_finish_inline(req, ipt)) {
584: io_poll_mark_cancelled(req);
585: return 0;
586: } else if (mask && (poll->events & EPOLLET)) {
587: ipt->result_mask = mask;
588: return 1;
589: }
590: return ipt->error ?: -EINVAL;
591: }
592:
593: if (mask &&
594: ((poll->events & (EPOLLET|EPOLLONESHOT)) == (EPOLLET|EPOLLONESHOT))) {
595: if (!io_poll_can_finish_inline(req, ipt)) {
596: io_poll_add_hash(req, issue_flags);
597: return 0;
598: }
599: io_poll_remove_entries(req);
600: ipt->result_mask = mask;
601: /* no one else has access to the req, forget about the ref */
602: return 1;
603: }
604:
605: io_poll_add_hash(req, issue_flags);
606:
607: if (mask && (poll->events & EPOLLET) &&
608: io_poll_can_finish_inline(req, ipt)) {
609: __io_poll_execute(req, mask);
610: return 0;
611: }
612: io_napi_add(req);
613:
614: if (ipt->owning) {
615: /*
616: * Try to release ownership. If we see a change of state, e.g.
617: * poll was waken up, queue up a tw, it'll deal with it.
618: */
619: if (atomic_cmpxchg(&req->poll_refs, 1, 0) != 1)
620: __io_poll_execute(req, 0);
621: }
622: return 0;
623: }
SourceFile:io_uring/poll.c]
|
| 34/2 |
2026/01/15 08:04 |
llm |
expert |
0m
Model:
Thoughts: **Understanding the io_uring Poll Race Condition**
Okay, so I'm looking at this potential race condition within the io_uring poll mechanism. It seems to center around the interaction between `__io_arm_poll_handler`, `vfs_poll`, `__io_queue_proc`, and `io_poll_wake`. The comment at line 561 in `__io_arm_poll_handler` is key β it acknowledges that races between `io_poll_wake` and the poll arming path are possible. I need to figure out the exact scenario where this happens and what the implications are.
`vfs_poll` is called at line 578, which calls `__io_queue_proc`. Inside `__io_queue_proc`, the check `first->head == head` is crucial. The goal is to see if the poll is already on a waitqueue. Here's where it gets interesting. `io_poll_wake` can clear `first->head` if it successfully takes ownership. That ownership is tied to incrementing `poll_refs`, which is initialized in `__io_arm_poll_handler`. `poll_refs` tells us if the poller owns the request.
If `poll_refs` is zero, `io_poll_wake` *can* easily obtain ownership. So if `__io_queue_proc` reads `first->head` as `NULL`, it adds a *second* entry for the same `head`. But it's this *second* entry that causes `poll->head` to get set, leaving the first entry's `head` as `NULL`. That can't be right! But then I remember: the reason for adding a second entry is because `vfs_poll` decides that it needs to wait on *another* wait queue. A TTY, for instance, may have separate waitqueues for reading and writing, though it usually is just one.
My key question is: what happens if the *same* `head` is used for a second entry after `io_poll_wake` already ran? `io_poll_wake` clears `first->head` and now `__io_queue_proc` might think it's a *new* `head`. But `io_poll_wake` was triggered from that same `head`. I think the race is that `io_poll_wake` runs and modifies the poll structure *concurrently* with `__io_queue_proc`.
Wait, `io_poll_wake` is the wake callback. It *is* called with the wait queue lock. So, the wake callback is holding the lock, but `__io_queue_proc` calls `add_wait_queue` which is also going to take the lock. I see that `vfs_poll` is calling `poll_wait` and `poll_wait` calls `_qproc` which is `__io_queue_proc`. So `__io_queue_proc` *doesn't* take the lock before checking `first->head`. The real problem is, if `io_poll_wake` clears `first->head` *just* before `__io_queue_proc` reads it, `__io_queue_proc` sees `NULL`. And then, `__io_queue_proc` sees the same `head`, the comment says it might add a second entry, but it should ignore it. It seems that double adds with the same `head` might happen in some drivers. If this is correct, then maybe `io_poll_wake` removes an entry at the same time and `__io_queue_proc` adds the entry back.
What is `io_poll_wake` is getting ownership? It's calling `__io_poll_execute` with a task work, that is then used to handle the poll.
Looking at `__io_arm_poll_handler`, if `io_poll_wake` has already run, `poll_refs` will have been modified. Specifically, this code block:
```c
if (ipt->owning) {
/*
* Try to release ownership. If we see a change of state, e.g.
* poll was waken up, queue up a tw, it'll deal with it.
*/
if (atomic_cmpxchg(&req->poll_refs, 1, 0) != 1)
__io_poll_execute(req, 0);
}
```
If it *was* the owner (owning = 1), it tries to drop ownership to 0. If that fails (meaning `io_poll_wake` changed it already), it calls `__
|
| 35/2 |
2026/01/15 08:05 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_remove_entries SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 152: static void io_poll_remove_entries(struct io_kiocb *req)
153: {
154: /*
155: * Nothing to do if neither of those flags are set. Avoid dipping
156: * into the poll/apoll/double cachelines if we can.
157: */
158: if (!(req->flags & (REQ_F_SINGLE_POLL | REQ_F_DOUBLE_POLL)))
159: return;
160:
161: /*
162: * While we hold the waitqueue lock and the waitqueue is nonempty,
163: * wake_up_pollfree() will wait for us. However, taking the waitqueue
164: * lock in the first place can race with the waitqueue being freed.
165: *
166: * We solve this as eventpoll does: by taking advantage of the fact that
167: * all users of wake_up_pollfree() will RCU-delay the actual free. If
168: * we enter rcu_read_lock() and see that the pointer to the queue is
169: * non-NULL, we can then lock it without the memory being freed out from
170: * under us.
171: *
172: * Keep holding rcu_read_lock() as long as we hold the queue lock, in
173: * case the caller deletes the entry from the queue, leaving it empty.
174: * In that case, only RCU prevents the queue memory from being freed.
175: */
176: rcu_read_lock();
177: if (req->flags & REQ_F_SINGLE_POLL)
178: io_poll_remove_entry(io_poll_get_single(req));
179: if (req->flags & REQ_F_DOUBLE_POLL)
180: io_poll_remove_entry(io_poll_get_double(req));
181: rcu_read_unlock();
182: }
SourceFile:io_uring/poll.c]
|
| 36/2 |
2026/01/15 08:05 |
llm |
expert |
0m
Model:
Thoughts: Here's my thought process, as I analyze this:
**Digging into Potential Double Polling Issues**
Okay, so I'm thinking about how a race condition could lead to a request being entered into the I/O queue twice. My initial thought is, "If `__io_queue_proc` somehow added a duplicate entry, would it automatically flag this with `REQ_F_DOUBLE_POLL`?" That seems like the logical place for it to be handled.
But wait, I need to be precise. Let me double-check the code. It looks like `__io_queue_proc` *doesn't* directly set the `REQ_F_DOUBLE_POLL` flag itself. Instead, it calls `io_poll_double_prepare(req)` on line 487. Okay, that's important. It's a layer of abstraction there. This implies that *`io_poll_double_prepare`* is where the double polling logic will be enacted. Next, I need to understand what `io_poll_double_prepare` is doing. Time to dive into *that* function...
|
| 37/2 |
2026/01/15 08:05 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_double_prepare SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 426: /* fails only when polling is already completing by the first entry */
427: static bool io_poll_double_prepare(struct io_kiocb *req)
428: {
429: struct wait_queue_head *head;
430: struct io_poll *poll = io_poll_get_single(req);
431:
432: /* head is RCU protected, see io_poll_remove_entries() comments */
433: rcu_read_lock();
434: head = smp_load_acquire(&poll->head);
435: /*
436: * poll arm might not hold ownership and so race for req->flags with
437: * io_poll_wake(). There is only one poll entry queued, serialise with
438: * it by taking its head lock. As we're still arming the tw hanlder
439: * is not going to be run, so there are no races with it.
440: */
441: if (head) {
442: spin_lock_irq(&head->lock);
443: req->flags |= REQ_F_DOUBLE_POLL;
444: if (req->opcode == IORING_OP_POLL_ADD)
445: req->flags |= REQ_F_ASYNC_DATA;
446: spin_unlock_irq(&head->lock);
447: }
448: rcu_read_unlock();
449: return !!head;
450: }
SourceFile:io_uring/poll.c]
|
| 38/2 |
2026/01/15 08:05 |
llm |
expert |
0m
Model:
Thoughts: **Summary of the io_poll Data Race Analysis**
Alright, let's break this down. KCSAN is reporting a data race between `io_poll_wake` (writing `poll->head = NULL;`) and `__io_queue_proc` (reading `first->head == head`). My initial thought is, "Wait, `io_poll_double_prepare` reads `poll->head` too!" But that uses `smp_load_acquire`. The reported race is on line 468, where we *don't* use acquire semantics, which is the immediate red flag.
The scenario is this: `io_poll_wake` likely clears `first->head` because the poll is firing. If `first->head` *is* `NULL`, then `io_poll_double_prepare` should also see `NULL` and return `false`, at which point `__io_queue_proc` cleans up safely and returns. This seems benign. But, what if the read *doesn't* see `NULL` immediately?
The key here is the purpose of the `if (first->head == head)` check, which is an optimization to avoid double-adding the same `head`. If `first->head` is `NULL`, the `if` is false, and we proceed to `io_poll_double_prepare`. Now, if `io_poll_wake` clears it *after* the `if` check but *before* `io_poll_double_prepare`, then `io_poll_double_prepare` will see `NULL` and return `false`. *That's* the likely scenario.
Let's assume the race is possible: `io_poll_wake` is clearing `poll->head`, but *after* `__io_queue_proc` read `first->head`, which isn't the same. In that case, `io_poll_wake` is taking `head->lock`, which means they are serialized when they are on the same head. If the heads are different, then they don't share a lock and the race is possible. The only harm here is `io_poll_double_prepare` returns `false`.
The real question is if that read can cause us to add a request when we shouldn't. But `io_poll_double_prepare` uses `smp_load_acquire` on *its* read, which should protect against seeing a partial value. But the write doesn't have a release.
Now, if we are in `vfs_poll` and call `poll_wait` with the *same* head, `first->head` should already be set within the same thread. `__io_queue_proc` is only called when we *arm* the poll. So if `pt->nr_entries > 0`, it means `__io_queue_proc` has been called before for this `vfs_poll` and has set `poll->head`. The wakeup can occur on *another* CPU as soon as `add_wait_queue` is called. So then the current thread reads that head but it has already been set to `NULL` on the other CPU. It checks `if (first->head == head)` again, and sees that `first->head` is `NULL`, but still proceed to `io_poll_double_prepare`, which returns `false`. Again, seems safe.
Is there any *risk*? This check is an optimization. So if we are wrong due to a race, we might just be doing extra work (and `io_poll_double_prepare` is more robust, but still safe). The fact that `io_poll_double_prepare` *uses* `smp_load_acquire` is important. This means the read should see that it's `NULL`.
The core of the problem: `poll->head = NULL;` is an indicator that the poll has *already fired*. This means that the read *should* be seeing `NULL`. We're optimizing, but KCSAN is pointing this out. However, plain pointer reads are atomic on x86, but KCSAN still detects this race.
Therefore, the fix is to use `READ_ONCE(first->head)` in `__io_queue_proc`, which is the correct thing to do to make this safe for the compiler. This could be a "benign" race, but it is better to be safe.
I need to make sure that there are no other places where `poll->head` is modified without a lock. In `io_poll_wake`, it's protected by the waitqueue lock.
In essence, if `first->head` is `NULL`, it just means the first entry is not associated with a wait queue.
|
| 39/2 |
2026/01/15 08:05 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:n_tty_poll SourceFile:drivers/tty/n_tty.c]
Results: map[Missing:false SourceCode:2430: /**
2431: * n_tty_poll - poll method for N_TTY
2432: * @tty: terminal device
2433: * @file: file accessing it
2434: * @wait: poll table
2435: *
2436: * Called when the line discipline is asked to poll() for data or for special
2437: * events. This code is not serialized with respect to other events save
2438: * open/close.
2439: *
2440: * This code must be sure never to sleep through a hangup.
2441: *
2442: * Locking: called without the kernel lock held -- fine.
2443: */
2444: static __poll_t n_tty_poll(struct tty_struct *tty, struct file *file,
2445: poll_table *wait)
2446: {
2447: __poll_t mask = 0;
2448:
2449: poll_wait(file, &tty->read_wait, wait);
2450: poll_wait(file, &tty->write_wait, wait);
2451: if (input_available_p(tty, 1))
2452: mask |= EPOLLIN | EPOLLRDNORM;
2453: else {
2454: tty_buffer_flush_work(tty->port);
2455: if (input_available_p(tty, 1))
2456: mask |= EPOLLIN | EPOLLRDNORM;
2457: }
2458: if (tty->ctrl.packet && tty->link->ctrl.pktstatus)
2459: mask |= EPOLLPRI | EPOLLIN | EPOLLRDNORM;
2460: if (test_bit(TTY_OTHER_CLOSED, &tty->flags))
2461: mask |= EPOLLHUP;
2462: if (tty_hung_up_p(file))
2463: mask |= EPOLLHUP;
2464: if (tty->ops->write && !tty_is_writelocked(tty) &&
2465: tty_chars_in_buffer(tty) < WAKEUP_CHARS &&
2466: tty_write_room(tty) > 0)
2467: mask |= EPOLLOUT | EPOLLWRNORM;
2468: return mask;
2469: }
SourceFile:drivers/tty/n_tty.c]
|
| 40/2 |
2026/01/15 08:05 |
llm |
expert |
0m
Model:
Thoughts: **Analyzing a TTY Poll Data Race**
Okay, so I'm looking at this `n_tty_poll` function, and it's calling `poll_wait` twice, once for read and once for write. Normally, these are distinct, but in certain configurations, or perhaps with specific TTYs, they can end up being the *same* wait queue. That's where things get interesting.
If the read and write wait queues are the same, `__io_queue_proc` gets called twice with the *same* `head`. The first call sets up the entry with `first->head = head`. The second call, which is where it checks if the entry number is 1, and enters an `unlikely` block, checks if `first->head == head`. The code's behavior here seems correct, but only if there isn't an intervening call to `io_poll_wake` between the two calls.
Here's the problem: if `io_poll_wake` gets in the middle, it can create a race condition. The first `poll_wait` adds an entry to, say, `read_wait`. `io_poll_wake` then runs, wakes up the entry, and crucially sets `first->head = NULL`. The *second* call to `poll_wait` happens for the write wait, which happens to be the *same* wait queue! So, when `__io_queue_proc` reads `first->head`, it sees `NULL` instead of the original `head` from the first call. This means it *doesn't* realize that there is already an entry for this wait queue and calls `io_poll_double_prepare`.
`io_poll_double_prepare` checks the `poll->head` again and, because of the race, likely *also* reads `NULL`. The function then returns `false` (meaning, "don't add the entry"). This behavior, surprisingly, is exactly what we want! The second entry *shouldn't* be added because `io_poll_wake` is already handling the wake-up.
So, the code *appears* correct from a functional standpoint, but it does have a data race, which KCSAN correctly identifies. The question is: is this data race *harmful*? Does it cause corruption? It looks like the check is an optimization and it seems okay for the race to occur here because `io_poll_double_prepare` does a final check for us.
Wait, looking closer at `io_poll_double_prepare`, it uses `smp_load_acquire` to read the head. The write in `io_poll_wake` isn't `smp_store_release`, but `poll->head = NULL` is a single write, which, because it's only writing `NULL`, should be eventually visible. If the first read saw a stale value, then the race would be missed.
Okay, let's explore more cases. If `first->head` was `HeadA` and `head` is `HeadB`, a first read might see `NULL` and continue. Then, if `io_poll_double_prepare` sees `NULL`, it will return `false`. If it sees `HeadA`, it would *then* try to add a double poll entry for `HeadB`. But, we shouldn't add more poll entries if `io_poll_wake` has already taken ownership. That's good!
In fact, `io_poll_double_prepare` handles this by returning `false` if the `head` is `NULL`.
Aha! `io_poll_get_ownership` has *already* been called by `io_poll_wake` if `io_poll_wake` is running. And if `io_poll_wake` got ownership, the `poll_refs` has been incremented! And `__io_arm_poll_handler` will check `poll_refs` later!
So, the race *could* potentially lead to `__io_queue_proc` thinking it successfully added an entry when it was *too late*, but `__io_arm_poll_handler` will decide the final outcome.
Is there any chance of memory corruption? The `poll` struct itself is allocated with `kmalloc`. If `io_poll_double_prepare` returns `false`, the memory is `kfree`'d. If it returns true, the pointer is stored in `*poll_ptr`.
Wait, the `poll->head = head;` assignment happens *after* the `kmalloc`. If we're in the `unlikely` block, `poll` refers to the *newly* allocated structure.
So, the race is specifically on `
|
| 41/2 |
2026/01/15 08:05 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_remove_entry SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 140: static inline void io_poll_remove_entry(struct io_poll *poll)
141: {
142: struct wait_queue_head *head = smp_load_acquire(&poll->head);
143:
144: if (head) {
145: spin_lock_irq(&head->lock);
146: list_del_init(&poll->wait.entry);
147: poll->head = NULL;
148: spin_unlock_irq(&head->lock);
149: }
150: }
SourceFile:io_uring/poll.c]
|
| 42/2 |
2026/01/15 08:05 |
llm |
expert |
0m
Model:
Thoughts: **Race Condition Analysis in io_uring Poll Handling**
Okay, so I'm diving into this io_uring poll code, specifically looking at a potential data race. My initial thought is that the `kfree` is not the problem here, the `apoll` struct is freed with the request, so memory safety isn't the primary concern.
The real race seems to be between `io_poll_wake` β which is setting `poll->head` to NULL β and `__io_queue_proc`, which is checking `poll->head` to avoid adding duplicate entries to the wait queue. The check `first->head == head` is meant to be an optimization. If the race occurs, one of two things could happen. Either we read the old `head` value and return early, which is fine, or we read `NULL` and end up in `io_poll_double_prepare`.
`io_poll_double_prepare` does a recheck of `poll->head` using `smp_load_acquire`. The logic there seems to dictate a return of `false` because it'll likely see NULL and `__io_queue_proc` will back off. If, by some chance, it *still* sees the old `head` value, it'll try to lock it. But here's the kicker: `io_poll_wake` already holds that lock if it's the *same* head. Therefore, serialization occurs.
Now, if `io_poll_wake` *finished* and released the lock, `io_poll_double_prepare` should see `poll->head` as `NULL` after taking the lock, but wait⦠I should look at line 434 and 442 again. `io_poll_double_prepare` reads `poll->head` BEFORE taking the lock, not after. It doesn't re-check `poll->head` after acquiring the spinlock!
So, if `head` was `HeadA` when read at line 434, and then `io_poll_wake` sets it to `NULL`, `io_poll_double_prepare` will still proceed to take `HeadA->lock` and set `REQ_F_DOUBLE_POLL`. But wait...that's still safe, because `io_poll_wake` has *already* taken ownership, and `__io_arm_poll_handler` will detect that via `poll_refs`.
Hold on...what if `io_poll_wake` is for the *first* entry and we're adding the *second*? It's better if we don't add more entries if `io_poll_wake` already ran.
Okay, now it seems more like a classic io_uring poll handling race. This has been discussed before. These races are usually benign because they only lead to a bit more work or handled by the `poll_refs` state machine.
Is it benign *for this analysis*? According to definitions, a "benign" data race is on a simple int/bool variable. A "non-benign" data race leads to crash/corruption. This case involves a *pointer*, used for a check, but it doesn't cause a crash. Even if we add a redundant entry, it will get cleaned up.
The `io_poll_double_prepare` is setting `REQ_F_DOUBLE_POLL`, and `io_poll_remove_entries` will clean it up. If we added an entry, but the request is completing...we might have an issue if the second entry isn't tracked. But `*poll_ptr = poll;` happens in `__io_queue_proc`, which is fine.
If the request is already being freed? No. `__io_arm_poll_handler` holds a reference, so `io_poll_wake` can't free the request. `io_poll_get_ownership` returns true if the ref is received, and schedules task work. As long as that task is pending, the request is alive.
So, this race is ultimately benign. It's about synchronization on a pointer and the io_uring poll logic is designed to handle these races.
Let me double-check `poll->head` can be anything else besides `NULL` in `io_poll_wake`. Line 415 is `poll->head = NULL;`. It is indeed setting it to null.
Wait, I need to check for other writes to `poll->head`. Line 499 in `__io_queue_proc` shows `poll->head = head;`, which occurs during initialization.
Is there a risk of seeing a partial pointer? Not on x86 for aligned 64-bit pointers.
|
| 43/2 |
2026/01/15 08:06 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_add_prep SourceFile:io_uring/poll.c]
Results: map[Missing:false SourceCode: 871: int io_poll_add_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
872: {
873: struct io_poll *poll = io_kiocb_to_cmd(req, struct io_poll);
874: u32 flags;
875:
876: if (sqe->buf_index || sqe->off || sqe->addr)
877: return -EINVAL;
878: flags = READ_ONCE(sqe->len);
879: if (flags & ~IORING_POLL_ADD_MULTI)
880: return -EINVAL;
881: if ((flags & IORING_POLL_ADD_MULTI) && (req->flags & REQ_F_CQE_SKIP))
882: return -EINVAL;
883:
884: poll->events = io_poll_parse_events(sqe, flags);
885: return 0;
886: }
SourceFile:io_uring/poll.c]
|
| 44/2 |
2026/01/15 08:06 |
llm |
expert |
0m
Model:
Thoughts: Here's a summary of my thought process:
**Diving Deeper into `io_poll`**
Okay, hold on a minute. I'm seeing `io_poll` being used not just as an event polling mechanism, but also as a command structure. That's interesting. The code snippet `struct io_poll *poll = io_kiocb_to_cmd(req, struct io_poll);` definitely suggests this dual purpose. I need to understand this better.
Right, I need to revisit `io_uring/poll.h` and see what's actually defined there. Before I do that, a quick search through the entire repository for instances of `io_poll {` should give me a better overview of how it's actually implemented and used in the broader context. That should help to clarify this command aspect.
|
| 45/2 |
2026/01/15 08:06 |
tool |
codesearch-definition-source |
0m
Model:
Args: map[IncludeLines:true Name:io_poll_table SourceFile:io_uring/poll.c]
Results: map[Missing:true SourceCode: SourceFile:]
|
| 46/2 |
2026/01/15 08:06 |
llm |
expert |
0m
Model:
Thoughts: **Analyzing a Potential Data Race in `io_uring`'s Polling Mechanism**
Okay, so I'm digging into this reported issue. My initial thought is to zero in on `io_poll_table`, since that's where polling seems to be managed within `io_uring`. I recall seeing `__io_queue_proc` and its use of `struct io_poll_table *pt`. I'll first check if I missed the struct definition within `io_uring/poll.c` itself.
Let me take a step back and examine the core `io_poll` structure. I need to make sure I'm not misremembering anything. I'll search for `struct io_poll` broadly, just in case. I did check `io_uring/io_uring_types.h`, but I don't think it's there. Let's also re-examine all files within the `io_uring/` directory, I may have the filename wrong. Ah, `io_uring/io_uring.h` looks promising.
Actually, I will just assume the common `io_poll` structure, which looks something like this:
```c
struct io_poll {
struct file *file;
struct wait_queue_entry wait;
struct wait_queue_head *head;
__poll_t events;
int error;
};
```
Right, if `wait` is indeed a `struct wait_queue_entry`, then it comprises of `flags`, `private`, `func`, and `entry` fields. I recall its size is around 40 bytes.
Now, let's nail down the offsets. `file` is 8 bytes. `wait` starts at offset 8. The `private` field of `wait` is at offset 16. The `head` pointer in `io_poll` is 48 bytes into the structure.
The reported address is `0xffff88817b26a428`. Let's assume `poll` starts at address `0xffff88817b26a3f8`. If so, `head` would be located at offset 48, which is offset 0x30 in hex. Adding 0x30 to the base, `0x3f8 + 0x30 = 0x428`. Bingo! The math checks out. That means the race is indeed on `poll->head`.
I see the value changed from an address to `0x0000000000000000`. This corresponds to `poll->head = NULL`, which is a key piece of information.
The read is inside `__io_queue_proc`:
```c
468: if (first->head == head)
```
This is an 8-byte read of `first->head`. The real question is: is this problematic?
Looking into the code, it seems like a lockless state check of `poll->head`, which is managed by `poll_refs`. It's a race, but a benign one, since this check is essentially a preliminary check. If the check passes, the request's ownership state (`poll_refs`) still ensures correctness.
Another detail to check: `n_tty_poll` might call `poll_wait` multiple times with potentially different wait queues. If they *are* different, the race is less impactful because `first->head == head` will likely be false anyway. If they are the same, it's an optimization to avoid adding the same head twice. Even if we hit this race and miss the optimization, it just attempts to add the duplicate head again, which is handled safely.
So, I'm leaning toward this being benign.
A final, crucial check: does `io_poll_wake` always hold the necessary lock when it sets `poll->head = NULL`? Yes, it's the wake callback, so it's always called with the waitqueue lock held. Does `__io_queue_proc` hold any locks during that read? No. So it's definitely a data race, but from my analysis, it's not harmful, as the request's ownership state (`poll_refs`) and the subsequent actions in `__io_queue_proc` guarantee correctness.
I'm confident in the analysis. I'll document my conclusion.
|
| 47/2 |
2026/01/15 08:06 |
tool |
set-results |
0m
Model:
Args: map[Benign:true Confident:true]
Results: map[Benign:true Confident:true]
|
| 48/2 |
2026/01/15 08:06 |
llm |
expert |
0m
Model:
Thoughts: Okay, here's my take on those thoughts, as if they were my own:
**Data Race Analysis Summary**
Alright, the debugging is done, and the culprit's been identified. Looks like we've got a data race on `poll->head`. It's happening between `io_poll_wake`, which is clearing it out (setting it to `NULL`), and `__io_queue_proc`, which is trying to read it to prevent the addition of duplicate entries. This is all part of `io_uring`'s poll arming optimization, which is essentially trying to avoid doing more work than needed. If the read in `__io_queue_proc` misses the `NULL` write from `io_poll_wake` β a race, naturally β the duplicate head check fails prematurely.
The good news? It's not a complete disaster. It *shouldn't* cause any nasty memory issues or crashes. The code, fortunately, has fallback mechanisms. If that initial check goes wrong, it'll fall through to the more robust checks in `io_poll_double_prepare`. And if *that* doesn't catch it, the request's ownership state machine (`poll_refs`) will eventually sort things out. So, while it's a race, and we want to eliminate it for performance and cleanliness, it's not a critical bug from a memory safety perspective. This kind of nuanced behavior is exactly what makes these low-level systems so complex, and so fascinating, to work with!
|