Re: [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers()

시리얼 <[email protected]>
Newsgroups org.kernel.vger.io-uring
Message-ID <CACR30WjNLCzzFTvdtM-kHC5Ei+W6H6Jeq3WpVh-asm0Ju1zv_Q@mail.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, 시리얼 <[email protected]>님이 작성:
>
> 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.