public inbox for io-uring@vger.kernel.org
 help / color / mirror / Atom feed
* [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers()
@ 2026-05-23 12:00 시리얼
  2026-05-23 12:10 ` 시리얼
  2026-05-23 13:54 ` Jens Axboe
  0 siblings, 2 replies; 6+ messages in thread
From: 시리얼 @ 2026-05-23 12:00 UTC (permalink / raw)
  To: io-uring, axboe

Frist I'm not good at English, so my grammar might be weird.
and i use translator so It may not look natural.

I found NULL-pointer dereference (general protection fault under
KASAN) in io_register_iowq_max_workers() on 7.1.0-rc1. It is a race
between the IORING_REGISTER_IOWQ_MAX_WORKERS propagation loop and a
task installing its first io_uring task context (tctx) node on a
shared ring. A small multithreaded reproducer triggers it reliably.

syzkaller log:

Oops: general protection fault, probably for non-canonical address
0xdffffc0000000003: 0000 [#1] SMP KASAN NOPTI
KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
CPU: 1 UID: 0 PID: 230570 Comm: syz.1.42039 Not tainted 7.1.0-rc1 #1
PREEMPT(full)
Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996), BIOS
1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000000000 CR3: 000000005ea81000 CR4: 0000000000352ef0
Call Trace:
 <TASK>
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0xff/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7ff8543b85fd
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48
89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007ff8525edff8 EFLAGS: 00000246 ORIG_RAX: 00000000000001ab
RAX: ffffffffffffffda RBX: 00007ff854645fa0 RCX: 00007ff8543b85fd
RDX: 0000200000000040 RSI: 0000000000000013 RDI: 0000000000000003
RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000002 R11: 0000000000000246 R12: 0000000000000000
R13: 00007ffdb063f3d0 R14: 00007ff8525eece4 R15: 00007ffdb063f4c7
 </TASK>
Modules linked in:
---[ end trace 0000000000000000 ]---
RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f1280e130b0 CR3: 000000005ea81000 CR4: 0000000000352ef0
----------------
Code disassembly (best guess):
   0: bd 68 09 00 00       mov    $0x968,%ebp
   5: 48 89 fa             mov    %rdi,%rdx
   8: 48 c1 ea 03           shr    $0x3,%rdx
   c: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1)
  11: 74 05                 je     0x18
  13: e8 06 3a 40 01       call   0x1403a1e
  18: 48 8b ad 68 09 00 00 mov    0x968(%rbp),%rbp
  1f: 48 8d 7d 18           lea    0x18(%rbp),%rdi
  23: 48 89 fa             mov    %rdi,%rdx
  26: 48 c1 ea 03           shr    $0x3,%rdx
* 2a: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1) <-- trapping instruction
  2f: 74 05                 je     0x36
  31: e8 e8 39 40 01       call   0x1403a1e
  36: 48 8b 6d 18           mov    0x18(%rbp),%rbp
  3a: 48 85 ed             test   %rbp,%rbp
  3d: 0f                   .byte 0xf
  3e: 85 ec                 test   %ebp,%esp


<<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>

This bug is in io_register_iowq_max_workers()

mutex_lock(&ctx->tctx_lock);
list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
    tctx = node->task->io_uring;
    if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
        continue;
    // skip
}

propagates the limit to all registered users (non-SQPOLL path)

The node is published into ctx->tctx_list before node->task->io_uring
is set (io_uring/tctx.c):

io_tctx_install_node():
    node->task = current;
    mutex_lock(&ctx->tctx_lock);
    list_add(&node->ctx_node, &ctx->tctx_list);   // node visible
    mutex_unlock(&ctx->tctx_lock); // lock dropped

__io_uring_add_tctx_node():
    ret = io_tctx_install_node(ctx, tctx);
    if (!ret)
        current->io_uring = tctx;   // set AFTER, outside lock

There is a window where a node is on ctx->tctx_list while
node->task->io_uring is still NULL (the task is doing its first
io_uring op, tctx freshly allocated, not yet published). A concurrent
IORING_REGISTER_IOWQ_MAX_WORKERS on the same ring takes
ctx->tctx_lock, iterates, reads node->task->io_uring == NULL, and
dereferences tctx->io_wq → GPF.

The other two ctx->tctx_list consumers already guard this — cancel.c
io_async_cancel_one() and io_uring_try_cancel_iowq() both do if (!tctx
|| !tctx->io_wq). io_register_iowq_max_workers() is the only consumer
that omits the !tctx check, so this is simply a missing guard.

Reproducer

Plain (non-SQPOLL) ring shared across threads. A stream of fresh
threads each do their first io_uring_enter() (hits the window) while
two threads spam IORING_REGISTER_IOWQ_MAX_WORKERS. GPFs within
seconds-to-minutes on SMP+KASAN.

#define _GNU_SOURCE
#include <pthread.h>
#include <string.h>
#include <sys/syscall.h>
#include <linux/io_uring.h>
static int ring_fd;
static long setup(unsigned e, struct io_uring_params *p){ return
syscall(__NR_io_uring_setup, e, p); }
static long enter(int fd, unsigned ts){ return
syscall(__NR_io_uring_enter, fd, ts, 0, 0, (void*)0, (size_t)0); }
static long reg(int fd, unsigned op, void *a, unsigned n){ return
syscall(__NR_io_uring_register, fd, op, a, n); }
static void *fresh(void *x){ enter(ring_fd, 1); return 0; }   // first
op -> window
static void *spam(void *x){ unsigned c[2]={1,1}; for(;;) reg(ring_fd,
IORING_REGISTER_IOWQ_MAX_WORKERS, c, 2); return 0; }
int main(void){
    struct io_uring_params p; memset(&p,0,sizeof(p));
    ring_fd = setup(8, &p);
    pthread_t s; pthread_create(&s,0,spam,0); pthread_create(&s,0,spam,0);
    for(;;){ pthread_t t[64];
        for(int i=0;i<64;i++) pthread_create(&t[i],0,fresh,0);
        for(int i=0;i<64;i++) pthread_join(t[i],0); }
}

Reproduced on 7.1.0-rc1 with KASAN; the racy ordering predates the
2024 shadow-variable cleanup that last touched register.c:422.

Suggested fix

Either make io_register_iowq_max_workers() match its siblings:

before:

mutex_lock(&ctx->tctx_lock);
list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
    tctx = node->task->io_uring;
    if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
        continue;
    // skip
}

to:

mutex_lock(&ctx->tctx_lock);
list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
    tctx = node->task->io_uring;
    if (!tctx || !tctx->io_wq)
        continue;
    // skip
}

or close the window in __io_uring_add_tctx_node() by publishing
current->io_uring = tctx before the node is added to ctx->tctx_list,
so a listed node always has a valid task->io_uring.


LimHyeonJun

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

* Re: [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers()
  2026-05-23 12:00 [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers() 시리얼
@ 2026-05-23 12:10 ` 시리얼
  2026-05-23 12:23   ` 시리얼
  2026-05-23 13:54 ` Jens Axboe
  1 sibling, 1 reply; 6+ messages in thread
From: 시리얼 @ 2026-05-23 12:10 UTC (permalink / raw)
  To: io-uring, axboe

I checked again: the missing `!tctx` check in
io_register_iowq_max_workers() is older, but the specific race window
I described appears to come from the recent tctx refactor.

2026년 5월 23일 (토) 오후 9:00, 시리얼 <shja0831@gmail.com>님이 작성:
>
> Frist I'm not good at English, so my grammar might be weird.
> and i use translator so It may not look natural.
>
> I found NULL-pointer dereference (general protection fault under
> KASAN) in io_register_iowq_max_workers() on 7.1.0-rc1. It is a race
> between the IORING_REGISTER_IOWQ_MAX_WORKERS propagation loop and a
> task installing its first io_uring task context (tctx) node on a
> shared ring. A small multithreaded reproducer triggers it reliably.
>
> syzkaller log:
>
> Oops: general protection fault, probably for non-canonical address
> 0xdffffc0000000003: 0000 [#1] SMP KASAN NOPTI
> KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
> CPU: 1 UID: 0 PID: 230570 Comm: syz.1.42039 Not tainted 7.1.0-rc1 #1
> PREEMPT(full)
> Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996), BIOS
> 1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
> RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
> RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
> RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
> Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
> 3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
> 3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
> RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
> RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
> RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
> RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
> R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
> R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
> FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
> CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> CR2: 0000000000000000 CR3: 000000005ea81000 CR4: 0000000000352ef0
> Call Trace:
>  <TASK>
>  do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
>  do_syscall_64+0xff/0xf80 arch/x86/entry/syscall_64.c:94
>  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> RIP: 0033:0x7ff8543b85fd
> Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48
> 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
> 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
> RSP: 002b:00007ff8525edff8 EFLAGS: 00000246 ORIG_RAX: 00000000000001ab
> RAX: ffffffffffffffda RBX: 00007ff854645fa0 RCX: 00007ff8543b85fd
> RDX: 0000200000000040 RSI: 0000000000000013 RDI: 0000000000000003
> RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
> R10: 0000000000000002 R11: 0000000000000246 R12: 0000000000000000
> R13: 00007ffdb063f3d0 R14: 00007ff8525eece4 R15: 00007ffdb063f4c7
>  </TASK>
> Modules linked in:
> ---[ end trace 0000000000000000 ]---
> RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
> RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
> RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
> Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
> 3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
> 3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
> RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
> RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
> RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
> RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
> R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
> R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
> FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
> CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> CR2: 00007f1280e130b0 CR3: 000000005ea81000 CR4: 0000000000352ef0
> ----------------
> Code disassembly (best guess):
>    0: bd 68 09 00 00       mov    $0x968,%ebp
>    5: 48 89 fa             mov    %rdi,%rdx
>    8: 48 c1 ea 03           shr    $0x3,%rdx
>    c: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1)
>   11: 74 05                 je     0x18
>   13: e8 06 3a 40 01       call   0x1403a1e
>   18: 48 8b ad 68 09 00 00 mov    0x968(%rbp),%rbp
>   1f: 48 8d 7d 18           lea    0x18(%rbp),%rdi
>   23: 48 89 fa             mov    %rdi,%rdx
>   26: 48 c1 ea 03           shr    $0x3,%rdx
> * 2a: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1) <-- trapping instruction
>   2f: 74 05                 je     0x36
>   31: e8 e8 39 40 01       call   0x1403a1e
>   36: 48 8b 6d 18           mov    0x18(%rbp),%rbp
>   3a: 48 85 ed             test   %rbp,%rbp
>   3d: 0f                   .byte 0xf
>   3e: 85 ec                 test   %ebp,%esp
>
>
> <<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>
>
> This bug is in io_register_iowq_max_workers()
>
> mutex_lock(&ctx->tctx_lock);
> list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
>     tctx = node->task->io_uring;
>     if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
>         continue;
>     // skip
> }
>
> propagates the limit to all registered users (non-SQPOLL path)
>
> The node is published into ctx->tctx_list before node->task->io_uring
> is set (io_uring/tctx.c):
>
> io_tctx_install_node():
>     node->task = current;
>     mutex_lock(&ctx->tctx_lock);
>     list_add(&node->ctx_node, &ctx->tctx_list);   // node visible
>     mutex_unlock(&ctx->tctx_lock); // lock dropped
>
> __io_uring_add_tctx_node():
>     ret = io_tctx_install_node(ctx, tctx);
>     if (!ret)
>         current->io_uring = tctx;   // set AFTER, outside lock
>
> There is a window where a node is on ctx->tctx_list while
> node->task->io_uring is still NULL (the task is doing its first
> io_uring op, tctx freshly allocated, not yet published). A concurrent
> IORING_REGISTER_IOWQ_MAX_WORKERS on the same ring takes
> ctx->tctx_lock, iterates, reads node->task->io_uring == NULL, and
> dereferences tctx->io_wq → GPF.
>
> The other two ctx->tctx_list consumers already guard this — cancel.c
> io_async_cancel_one() and io_uring_try_cancel_iowq() both do if (!tctx
> || !tctx->io_wq). io_register_iowq_max_workers() is the only consumer
> that omits the !tctx check, so this is simply a missing guard.
>
> Reproducer
>
> Plain (non-SQPOLL) ring shared across threads. A stream of fresh
> threads each do their first io_uring_enter() (hits the window) while
> two threads spam IORING_REGISTER_IOWQ_MAX_WORKERS. GPFs within
> seconds-to-minutes on SMP+KASAN.
>
> #define _GNU_SOURCE
> #include <pthread.h>
> #include <string.h>
> #include <sys/syscall.h>
> #include <linux/io_uring.h>
> static int ring_fd;
> static long setup(unsigned e, struct io_uring_params *p){ return
> syscall(__NR_io_uring_setup, e, p); }
> static long enter(int fd, unsigned ts){ return
> syscall(__NR_io_uring_enter, fd, ts, 0, 0, (void*)0, (size_t)0); }
> static long reg(int fd, unsigned op, void *a, unsigned n){ return
> syscall(__NR_io_uring_register, fd, op, a, n); }
> static void *fresh(void *x){ enter(ring_fd, 1); return 0; }   // first
> op -> window
> static void *spam(void *x){ unsigned c[2]={1,1}; for(;;) reg(ring_fd,
> IORING_REGISTER_IOWQ_MAX_WORKERS, c, 2); return 0; }
> int main(void){
>     struct io_uring_params p; memset(&p,0,sizeof(p));
>     ring_fd = setup(8, &p);
>     pthread_t s; pthread_create(&s,0,spam,0); pthread_create(&s,0,spam,0);
>     for(;;){ pthread_t t[64];
>         for(int i=0;i<64;i++) pthread_create(&t[i],0,fresh,0);
>         for(int i=0;i<64;i++) pthread_join(t[i],0); }
> }
>
> Reproduced on 7.1.0-rc1 with KASAN; the racy ordering predates the
> 2024 shadow-variable cleanup that last touched register.c:422.
>
> Suggested fix
>
> Either make io_register_iowq_max_workers() match its siblings:
>
> before:
>
> mutex_lock(&ctx->tctx_lock);
> list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
>     tctx = node->task->io_uring;
>     if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
>         continue;
>     // skip
> }
>
> to:
>
> mutex_lock(&ctx->tctx_lock);
> list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
>     tctx = node->task->io_uring;
>     if (!tctx || !tctx->io_wq)
>         continue;
>     // skip
> }
>
> or close the window in __io_uring_add_tctx_node() by publishing
> current->io_uring = tctx before the node is added to ctx->tctx_list,
> so a listed node always has a valid task->io_uring.
>
>
> LimHyeonJun

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

* Re: [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers()
  2026-05-23 12:10 ` 시리얼
@ 2026-05-23 12:23   ` 시리얼
  0 siblings, 0 replies; 6+ messages in thread
From: 시리얼 @ 2026-05-23 12:23 UTC (permalink / raw)
  To: io-uring, axboe

Sorry, I made a mistake in my follow-up.

Before that reply, I had checked the current Linus tree and confirmed
that this code path was still present there. Later, I checked again in
my local directory with git, did not see it there, and sent the
correction too quickly.

I checked again, and the same ordering is in the current Linus tree as
well: the node is added to ctx->tctx_list before current->io_uring =
tctx, and io_register_iowq_max_workers() still dereferences
tctx->io_wq without a !tctx check.

Sorry for the confusion.


2026년 5월 23일 (토) 오후 9:10, 시리얼 <shja0831@gmail.com>님이 작성:
>
> I checked again: the missing `!tctx` check in
> io_register_iowq_max_workers() is older, but the specific race window
> I described appears to come from the recent tctx refactor.
>
> 2026년 5월 23일 (토) 오후 9:00, 시리얼 <shja0831@gmail.com>님이 작성:
> >
> > Frist I'm not good at English, so my grammar might be weird.
> > and i use translator so It may not look natural.
> >
> > I found NULL-pointer dereference (general protection fault under
> > KASAN) in io_register_iowq_max_workers() on 7.1.0-rc1. It is a race
> > between the IORING_REGISTER_IOWQ_MAX_WORKERS propagation loop and a
> > task installing its first io_uring task context (tctx) node on a
> > shared ring. A small multithreaded reproducer triggers it reliably.
> >
> > syzkaller log:
> >
> > Oops: general protection fault, probably for non-canonical address
> > 0xdffffc0000000003: 0000 [#1] SMP KASAN NOPTI
> > KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
> > CPU: 1 UID: 0 PID: 230570 Comm: syz.1.42039 Not tainted 7.1.0-rc1 #1
> > PREEMPT(full)
> > Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996), BIOS
> > 1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
> > RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
> > RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
> > RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
> > Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
> > 3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
> > 3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
> > RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
> > RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
> > RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
> > RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
> > R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
> > R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
> > FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
> > CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> > CR2: 0000000000000000 CR3: 000000005ea81000 CR4: 0000000000352ef0
> > Call Trace:
> >  <TASK>
> >  do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
> >  do_syscall_64+0xff/0xf80 arch/x86/entry/syscall_64.c:94
> >  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> > RIP: 0033:0x7ff8543b85fd
> > Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48
> > 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
> > 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
> > RSP: 002b:00007ff8525edff8 EFLAGS: 00000246 ORIG_RAX: 00000000000001ab
> > RAX: ffffffffffffffda RBX: 00007ff854645fa0 RCX: 00007ff8543b85fd
> > RDX: 0000200000000040 RSI: 0000000000000013 RDI: 0000000000000003
> > RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
> > R10: 0000000000000002 R11: 0000000000000246 R12: 0000000000000000
> > R13: 00007ffdb063f3d0 R14: 00007ff8525eece4 R15: 00007ffdb063f4c7
> >  </TASK>
> > Modules linked in:
> > ---[ end trace 0000000000000000 ]---
> > RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
> > RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
> > RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
> > Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
> > 3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
> > 3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
> > RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
> > RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
> > RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
> > RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
> > R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
> > R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
> > FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
> > CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> > CR2: 00007f1280e130b0 CR3: 000000005ea81000 CR4: 0000000000352ef0
> > ----------------
> > Code disassembly (best guess):
> >    0: bd 68 09 00 00       mov    $0x968,%ebp
> >    5: 48 89 fa             mov    %rdi,%rdx
> >    8: 48 c1 ea 03           shr    $0x3,%rdx
> >    c: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1)
> >   11: 74 05                 je     0x18
> >   13: e8 06 3a 40 01       call   0x1403a1e
> >   18: 48 8b ad 68 09 00 00 mov    0x968(%rbp),%rbp
> >   1f: 48 8d 7d 18           lea    0x18(%rbp),%rdi
> >   23: 48 89 fa             mov    %rdi,%rdx
> >   26: 48 c1 ea 03           shr    $0x3,%rdx
> > * 2a: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1) <-- trapping instruction
> >   2f: 74 05                 je     0x36
> >   31: e8 e8 39 40 01       call   0x1403a1e
> >   36: 48 8b 6d 18           mov    0x18(%rbp),%rbp
> >   3a: 48 85 ed             test   %rbp,%rbp
> >   3d: 0f                   .byte 0xf
> >   3e: 85 ec                 test   %ebp,%esp
> >
> >
> > <<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>
> >
> > This bug is in io_register_iowq_max_workers()
> >
> > mutex_lock(&ctx->tctx_lock);
> > list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
> >     tctx = node->task->io_uring;
> >     if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
> >         continue;
> >     // skip
> > }
> >
> > propagates the limit to all registered users (non-SQPOLL path)
> >
> > The node is published into ctx->tctx_list before node->task->io_uring
> > is set (io_uring/tctx.c):
> >
> > io_tctx_install_node():
> >     node->task = current;
> >     mutex_lock(&ctx->tctx_lock);
> >     list_add(&node->ctx_node, &ctx->tctx_list);   // node visible
> >     mutex_unlock(&ctx->tctx_lock); // lock dropped
> >
> > __io_uring_add_tctx_node():
> >     ret = io_tctx_install_node(ctx, tctx);
> >     if (!ret)
> >         current->io_uring = tctx;   // set AFTER, outside lock
> >
> > There is a window where a node is on ctx->tctx_list while
> > node->task->io_uring is still NULL (the task is doing its first
> > io_uring op, tctx freshly allocated, not yet published). A concurrent
> > IORING_REGISTER_IOWQ_MAX_WORKERS on the same ring takes
> > ctx->tctx_lock, iterates, reads node->task->io_uring == NULL, and
> > dereferences tctx->io_wq → GPF.
> >
> > The other two ctx->tctx_list consumers already guard this — cancel.c
> > io_async_cancel_one() and io_uring_try_cancel_iowq() both do if (!tctx
> > || !tctx->io_wq). io_register_iowq_max_workers() is the only consumer
> > that omits the !tctx check, so this is simply a missing guard.
> >
> > Reproducer
> >
> > Plain (non-SQPOLL) ring shared across threads. A stream of fresh
> > threads each do their first io_uring_enter() (hits the window) while
> > two threads spam IORING_REGISTER_IOWQ_MAX_WORKERS. GPFs within
> > seconds-to-minutes on SMP+KASAN.
> >
> > #define _GNU_SOURCE
> > #include <pthread.h>
> > #include <string.h>
> > #include <sys/syscall.h>
> > #include <linux/io_uring.h>
> > static int ring_fd;
> > static long setup(unsigned e, struct io_uring_params *p){ return
> > syscall(__NR_io_uring_setup, e, p); }
> > static long enter(int fd, unsigned ts){ return
> > syscall(__NR_io_uring_enter, fd, ts, 0, 0, (void*)0, (size_t)0); }
> > static long reg(int fd, unsigned op, void *a, unsigned n){ return
> > syscall(__NR_io_uring_register, fd, op, a, n); }
> > static void *fresh(void *x){ enter(ring_fd, 1); return 0; }   // first
> > op -> window
> > static void *spam(void *x){ unsigned c[2]={1,1}; for(;;) reg(ring_fd,
> > IORING_REGISTER_IOWQ_MAX_WORKERS, c, 2); return 0; }
> > int main(void){
> >     struct io_uring_params p; memset(&p,0,sizeof(p));
> >     ring_fd = setup(8, &p);
> >     pthread_t s; pthread_create(&s,0,spam,0); pthread_create(&s,0,spam,0);
> >     for(;;){ pthread_t t[64];
> >         for(int i=0;i<64;i++) pthread_create(&t[i],0,fresh,0);
> >         for(int i=0;i<64;i++) pthread_join(t[i],0); }
> > }
> >
> > Reproduced on 7.1.0-rc1 with KASAN; the racy ordering predates the
> > 2024 shadow-variable cleanup that last touched register.c:422.
> >
> > Suggested fix
> >
> > Either make io_register_iowq_max_workers() match its siblings:
> >
> > before:
> >
> > mutex_lock(&ctx->tctx_lock);
> > list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
> >     tctx = node->task->io_uring;
> >     if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
> >         continue;
> >     // skip
> > }
> >
> > to:
> >
> > mutex_lock(&ctx->tctx_lock);
> > list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
> >     tctx = node->task->io_uring;
> >     if (!tctx || !tctx->io_wq)
> >         continue;
> >     // skip
> > }
> >
> > or close the window in __io_uring_add_tctx_node() by publishing
> > current->io_uring = tctx before the node is added to ctx->tctx_list,
> > so a listed node always has a valid task->io_uring.
> >
> >
> > LimHyeonJun

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

* Re: [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers()
  2026-05-23 12:00 [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers() 시리얼
  2026-05-23 12:10 ` 시리얼
@ 2026-05-23 13:54 ` Jens Axboe
  2026-05-24 11:08   ` [PATCH] io_uring/tctx: set ->io_uring before publishing the tctx node Lim HyeonJun
  1 sibling, 1 reply; 6+ messages in thread
From: Jens Axboe @ 2026-05-23 13:54 UTC (permalink / raw)
  To: 시리얼, io-uring

On 5/23/26 6:00 AM, ??? wrote:
> Frist I'm not good at English, so my grammar might be weird.
> and i use translator so It may not look natural.
> 
> I found NULL-pointer dereference (general protection fault under
> KASAN) in io_register_iowq_max_workers() on 7.1.0-rc1. It is a race
> between the IORING_REGISTER_IOWQ_MAX_WORKERS propagation loop and a
> task installing its first io_uring task context (tctx) node on a
> shared ring. A small multithreaded reproducer triggers it reliably.
> 
> syzkaller log:
> 
> Oops: general protection fault, probably for non-canonical address
> 0xdffffc0000000003: 0000 [#1] SMP KASAN NOPTI
> KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
> CPU: 1 UID: 0 PID: 230570 Comm: syz.1.42039 Not tainted 7.1.0-rc1 #1
> PREEMPT(full)
> Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996), BIOS
> 1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
> RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
> RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
> RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
> Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
> 3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
> 3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
> RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
> RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
> RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
> RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
> R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
> R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
> FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
> CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> CR2: 0000000000000000 CR3: 000000005ea81000 CR4: 0000000000352ef0
> Call Trace:
>  <TASK>
>  do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
>  do_syscall_64+0xff/0xf80 arch/x86/entry/syscall_64.c:94
>  entry_SYSCALL_64_after_hwframe+0x77/0x7f
> RIP: 0033:0x7ff8543b85fd
> Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48
> 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
> 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
> RSP: 002b:00007ff8525edff8 EFLAGS: 00000246 ORIG_RAX: 00000000000001ab
> RAX: ffffffffffffffda RBX: 00007ff854645fa0 RCX: 00007ff8543b85fd
> RDX: 0000200000000040 RSI: 0000000000000013 RDI: 0000000000000003
> RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
> R10: 0000000000000002 R11: 0000000000000246 R12: 0000000000000000
> R13: 00007ffdb063f3d0 R14: 00007ff8525eece4 R15: 00007ffdb063f4c7
>  </TASK>
> Modules linked in:
> ---[ end trace 0000000000000000 ]---
> RIP: 0010:io_register_iowq_max_workers io_uring/register.c:423 [inline]
> RIP: 0010:__io_uring_register io_uring/register.c:865 [inline]
> RIP: 0010:__do_sys_io_uring_register.cold+0xcae/0xe32 io_uring/register.c:1029
> Code: bd 68 09 00 00 48 89 fa 48 c1 ea 03 42 80 3c 2a 00 74 05 e8 06
> 3a 40 01 48 8b ad 68 09 00 00 48 8d 7d 18 48 89 fa 48 c1 ea 03 <42> 80
> 3c 2a 00 74 05 e8 e8 39 40 01 48 8b 6d 18 48 85 ed 0f 85 ec
> RSP: 0018:ffffc90002a9fd90 EFLAGS: 00010206
> RAX: 1ffff11004c9f63a RBX: ffff888054ee4000 RCX: 0000000000000001
> RDX: 0000000000000003 RSI: ffffffff81364a23 RDI: 0000000000000018
> RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000001
> R10: ffffc90002a9fd90 R11: 0000000080000000 R12: ffff8880264fb1c0
> R13: dffffc0000000000 R14: 0000000000000013 R15: 0000000000000013
> FS:  00007ff8525ee6c0(0000) GS:ffff8880d687a000(0000) knlGS:0000000000000000
> CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> CR2: 00007f1280e130b0 CR3: 000000005ea81000 CR4: 0000000000352ef0
> ----------------
> Code disassembly (best guess):
>    0: bd 68 09 00 00       mov    $0x968,%ebp
>    5: 48 89 fa             mov    %rdi,%rdx
>    8: 48 c1 ea 03           shr    $0x3,%rdx
>    c: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1)
>   11: 74 05                 je     0x18
>   13: e8 06 3a 40 01       call   0x1403a1e
>   18: 48 8b ad 68 09 00 00 mov    0x968(%rbp),%rbp
>   1f: 48 8d 7d 18           lea    0x18(%rbp),%rdi
>   23: 48 89 fa             mov    %rdi,%rdx
>   26: 48 c1 ea 03           shr    $0x3,%rdx
> * 2a: 42 80 3c 2a 00       cmpb   $0x0,(%rdx,%r13,1) <-- trapping instruction
>   2f: 74 05                 je     0x36
>   31: e8 e8 39 40 01       call   0x1403a1e
>   36: 48 8b 6d 18           mov    0x18(%rbp),%rbp
>   3a: 48 85 ed             test   %rbp,%rbp
>   3d: 0f                   .byte 0xf
>   3e: 85 ec                 test   %ebp,%esp
> 
> 
> <<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>
> 
> This bug is in io_register_iowq_max_workers()
> 
> mutex_lock(&ctx->tctx_lock);
> list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
>     tctx = node->task->io_uring;
>     if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
>         continue;
>     // skip
> }
> 
> propagates the limit to all registered users (non-SQPOLL path)
> 
> The node is published into ctx->tctx_list before node->task->io_uring
> is set (io_uring/tctx.c):
> 
> io_tctx_install_node():
>     node->task = current;
>     mutex_lock(&ctx->tctx_lock);
>     list_add(&node->ctx_node, &ctx->tctx_list);   // node visible
>     mutex_unlock(&ctx->tctx_lock); // lock dropped
> 
> __io_uring_add_tctx_node():
>     ret = io_tctx_install_node(ctx, tctx);
>     if (!ret)
>         current->io_uring = tctx;   // set AFTER, outside lock
> 
> There is a window where a node is on ctx->tctx_list while
> node->task->io_uring is still NULL (the task is doing its first
> io_uring op, tctx freshly allocated, not yet published). A concurrent
> IORING_REGISTER_IOWQ_MAX_WORKERS on the same ring takes
> ctx->tctx_lock, iterates, reads node->task->io_uring == NULL, and
> dereferences tctx->io_wq ? GPF.
> 
> The other two ctx->tctx_list consumers already guard this ? cancel.c
> io_async_cancel_one() and io_uring_try_cancel_iowq() both do if (!tctx
> || !tctx->io_wq). io_register_iowq_max_workers() is the only consumer
> that omits the !tctx check, so this is simply a missing guard.
> 
> Reproducer
> 
> Plain (non-SQPOLL) ring shared across threads. A stream of fresh
> threads each do their first io_uring_enter() (hits the window) while
> two threads spam IORING_REGISTER_IOWQ_MAX_WORKERS. GPFs within
> seconds-to-minutes on SMP+KASAN.
> 
> #define _GNU_SOURCE
> #include <pthread.h>
> #include <string.h>
> #include <sys/syscall.h>
> #include <linux/io_uring.h>
> static int ring_fd;
> static long setup(unsigned e, struct io_uring_params *p){ return
> syscall(__NR_io_uring_setup, e, p); }
> static long enter(int fd, unsigned ts){ return
> syscall(__NR_io_uring_enter, fd, ts, 0, 0, (void*)0, (size_t)0); }
> static long reg(int fd, unsigned op, void *a, unsigned n){ return
> syscall(__NR_io_uring_register, fd, op, a, n); }
> static void *fresh(void *x){ enter(ring_fd, 1); return 0; }   // first
> op -> window
> static void *spam(void *x){ unsigned c[2]={1,1}; for(;;) reg(ring_fd,
> IORING_REGISTER_IOWQ_MAX_WORKERS, c, 2); return 0; }
> int main(void){
>     struct io_uring_params p; memset(&p,0,sizeof(p));
>     ring_fd = setup(8, &p);
>     pthread_t s; pthread_create(&s,0,spam,0); pthread_create(&s,0,spam,0);
>     for(;;){ pthread_t t[64];
>         for(int i=0;i<64;i++) pthread_create(&t[i],0,fresh,0);
>         for(int i=0;i<64;i++) pthread_join(t[i],0); }
> }
> 
> Reproduced on 7.1.0-rc1 with KASAN; the racy ordering predates the
> 2024 shadow-variable cleanup that last touched register.c:422.
> 
> Suggested fix
> 
> Either make io_register_iowq_max_workers() match its siblings:
> 
> before:
> 
> mutex_lock(&ctx->tctx_lock);
> list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
>     tctx = node->task->io_uring;
>     if (WARN_ON_ONCE(!tctx->io_wq)) // derefs tctx without NULL check
>         continue;
>     // skip
> }
> 
> to:
> 
> mutex_lock(&ctx->tctx_lock);
> list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
>     tctx = node->task->io_uring;
>     if (!tctx || !tctx->io_wq)
>         continue;
>     // skip
> }
> 
> or close the window in __io_uring_add_tctx_node() by publishing
> current->io_uring = tctx before the node is added to ctx->tctx_list,
> so a listed node always has a valid task->io_uring.

Setting ->io_uring = tctx before adding to the list is, by far, the
better fix. Rather than just report it, do you want to submit an actual
patch for that? I can surely patch it up myself, but you could also just
send a patch for it.

-- 
Jens Axboe

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

* [PATCH] io_uring/tctx: set ->io_uring before publishing the tctx node
  2026-05-23 13:54 ` Jens Axboe
@ 2026-05-24 11:08   ` Lim HyeonJun
  2026-05-24 18:08     ` Jens Axboe
  0 siblings, 1 reply; 6+ messages in thread
From: Lim HyeonJun @ 2026-05-24 11:08 UTC (permalink / raw)
  To: Jens Axboe, io-uring; +Cc: Lim HyeonJun

io_register_iowq_max_workers() walks ctx->tctx_list under ctx->tctx_lock
and dereferences each node's task->io_uring without a NULL check:

	list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
		tctx = node->task->io_uring;
		if (WARN_ON_ONCE(!tctx->io_wq))
			continue;
		...
	}

__io_uring_add_tctx_node() installs the node into ctx->tctx_list (via
io_tctx_install_node(), which does the list_add() under tctx_lock) and
only assigns current->io_uring = tctx afterwards. A task doing its first
io_uring operation on a shared ring therefore has a window in which its
node is already visible on ctx->tctx_list while node->task->io_uring is
still NULL. A concurrent IORING_REGISTER_IOWQ_MAX_WORKERS on the same
ring reads that NULL and dereferences tctx->io_wq:

  KASAN: null-ptr-deref in range [0x0000000000000018-0x000000000000001f]
  RIP: io_register_iowq_max_workers io_uring/register.c:423

Publish current->io_uring = tctx before installing the node, so any node
visible on ctx->tctx_list always has a valid task->io_uring. The
tctx_lock taken in io_tctx_install_node() orders this store before the
node becomes visible to other iterators. On the install/limits failure
paths the freshly allocated tctx is freed, so clear current->io_uring
there as well to avoid leaving a dangling pointer.

The bug reproduces on an SMP+KASAN build with a plain (non-SQPOLL) ring
shared across threads: a stream of fresh threads each do their first
io_uring_enter() while two threads spam IORING_REGISTER_IOWQ_MAX_WORKERS;
it GPFs within seconds.

Fixes: 7880174e1e5e ("io_uring/tctx: clean up __io_uring_add_tctx_node() error handling")
Signed-off-by: Lim HyeonJun <shja0831@gmail.com>
---
 io_uring/tctx.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/io_uring/tctx.c b/io_uring/tctx.c
index 6af62ca9baba..42b219b34aa8 100644
--- a/io_uring/tctx.c
+++ b/io_uring/tctx.c
@@ -139,12 +139,14 @@ static int io_tctx_install_node(struct io_ring_ctx *ctx,
 int __io_uring_add_tctx_node(struct io_ring_ctx *ctx)
 {
 	struct io_uring_task *tctx = current->io_uring;
+	bool new_tctx = false;
 	int ret;
 
 	if (unlikely(!tctx)) {
 		tctx = io_uring_alloc_task_context(current, ctx);
 		if (IS_ERR(tctx))
 			return PTR_ERR(tctx);
+		new_tctx = true;
 
 		if (data_race(ctx->int_flags) & IO_RING_F_IOWQ_LIMITS_SET) {
 			unsigned int limits[2];
@@ -168,13 +170,15 @@ int __io_uring_add_tctx_node(struct io_ring_ctx *ctx)
 	if (tctx->io_wq)
 		io_wq_set_exit_on_idle(tctx->io_wq, false);
 
-	ret = io_tctx_install_node(ctx, tctx);
-	if (!ret) {
+	if (new_tctx)
 		current->io_uring = tctx;
+
+	ret = io_tctx_install_node(ctx, tctx);
+	if (!ret)
 		return 0;
-	}
-	if (!current->io_uring) {
 err_free:
+	if (new_tctx) {
+		current->io_uring = NULL;
 		if (tctx->io_wq) {
 			io_wq_exit_start(tctx->io_wq);
 			io_wq_put_and_exit(tctx->io_wq);
-- 
2.53.0


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

* Re: [PATCH] io_uring/tctx: set ->io_uring before publishing the tctx node
  2026-05-24 11:08   ` [PATCH] io_uring/tctx: set ->io_uring before publishing the tctx node Lim HyeonJun
@ 2026-05-24 18:08     ` Jens Axboe
  0 siblings, 0 replies; 6+ messages in thread
From: Jens Axboe @ 2026-05-24 18:08 UTC (permalink / raw)
  To: io-uring, Lim HyeonJun


On Sun, 24 May 2026 20:08:53 +0900, Lim HyeonJun wrote:
> io_register_iowq_max_workers() walks ctx->tctx_list under ctx->tctx_lock
> and dereferences each node's task->io_uring without a NULL check:
> 
> 	list_for_each_entry(node, &ctx->tctx_list, ctx_node) {
> 		tctx = node->task->io_uring;
> 		if (WARN_ON_ONCE(!tctx->io_wq))
> 			continue;
> 		...
> 	}
> 
> [...]

Applied, thanks!

[1/1] io_uring/tctx: set ->io_uring before publishing the tctx node
      commit: a88c02915d9c6160cfc7ab1b26ed64b2993e2b94

Best regards,
-- 
Jens Axboe




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

end of thread, other threads:[~2026-05-24 18:08 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-23 12:00 [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers() 시리얼
2026-05-23 12:10 ` 시리얼
2026-05-23 12:23   ` 시리얼
2026-05-23 13:54 ` Jens Axboe
2026-05-24 11:08   ` [PATCH] io_uring/tctx: set ->io_uring before publishing the tctx node Lim HyeonJun
2026-05-24 18:08     ` Jens Axboe

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