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

시리얼 <[email protected]>
Newsgroups org.kernel.vger.io-uring
Message-ID <CACR30Wj7yEweYqJg4Ovrbr4s9a8EZRYD8FMAWhjWUv3XunrMFQ@mail.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
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.