Re: [bug] io_uring : NULL pointer deref in io_register_iowq_max_workers()
시리얼 <[email protected]>
| Newsgroups | org.kernel.vger.io-uring |
|---|---|
| Message-ID | <CACR30WgesGL5qR6QwMKBugVSPaH3TN_QEyUtdWKW-4VUme4GjQ@mail.gmail.com> |
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, 시리얼 <[email protected]>님이 작성: > > 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