[BUG] WARNING in io_ring_exit_work (io_uring.c:2187) via IORING_REGISTER_BPF_FILTER — confirmed on 7.0-rc5 and rc6

From: antonius

Date: Tue Mar 31 2026 - 09:33:19 EST


Hello,

I am reporting a kernel WARNING discovered via Syzkaller fuzzing of Linux
7.0-rc5, targeting the new IORING_REGISTER_BPF_FILTER subsystem (new in 7.0).

The bug is confirmed on both 7.0-rc5 and 7.0-rc6. It is NOT fixed in rc6.
In rc6, the WARNING appears to have changed from WARN_ON to WARN_ON_ONCE
(fires only once per boot), which may explain why it was initially missed.

REPORTER
--------
Antonius / Blue Dragon Security
https://bluedragonsec.com
antonius@xxxxxxxxxxxxxxxxx

AFFECTED VERSIONS
-----------------
Confirmed: Linux 7.0.0-rc5 (QEMU, KASAN+KFENCE build, Syzkaller)
Confirmed: Linux 7.0.0-rc6 (QEMU, PREEMPT(lazy), PROVE_LOCKING build)
Not affected: kernels prior to 7.0 (IORING_REGISTER_BPF_FILTER is new in 7.0)
Status: NOT fixed in rc6

NOTE ON rc6 BEHAVIOR: The WARNING fires only once per boot in rc6
(WARN_ON_ONCE semantics), confirmed by:
  - trace hash "0000000000000000" in the dump
  - Silent on subsequent runs within same boot session
  - Fires again after reboot
Reset via: echo 0 > /sys/kernel/debug/clear_warn_once  (then retest)

CRASH OUTPUT — rc6 (7.0.0-rc6, PREEMPT(lazy))
----------------------------------------------
  [ 1021.589216] ------------[ cut here ]------------
  [ 1021.589240] WARNING: io_uring/io_uring.c:2187
                 at io_ring_exit_work+0xbea/0xd4b, CPU#0: kworker/u4:1/14
  [ 1021.589298] CPU: 0 UID: 0 PID: 14 Comm: kworker/u4:1
                 Not tainted 7.0.0-rc6 #1 PREEMPT(lazy)
  [ 1021.589326] Workqueue: iou_exit io_ring_exit_work
  [ 1021.589346] RIP: 0010:io_ring_exit_work+0xbea/0xd4b
  [ 1021.589393] RAX: 0000000000000000 RBX: ffff88810e659778
  [ 1021.589432] R13: 0000000000000000 R14: ffff888115c64000
                 R15: dffffc0000000000
  [ 1021.589474] Call Trace:
  [ 1021.589487]   ? check_prev_add+0x333/0xd30    ← lockdep active
  [ 1021.589533]   ? __pfx_io_tctx_exit_cb+0x10/0x10
  [ 1021.589577]   process_one_work+0xa16/0x1900
  [ 1021.590019]   worker_thread+0x5eb/0xe50
  [ 1021.590084]   kthread+0x366/0x450
  [ 1021.590122]   ret_from_fork+0x660/0xa80
  [ 1021.590200]  </TASK>
  [ 1021.590280] ---[ end trace 0000000000000000 ]---

CRASH OUTPUT — rc5 (7.0.0-rc5, PREEMPT(lazy), KASAN)
-----------------------------------------------------
  WARNING: io_uring/io_uring.c:2187
           at io_ring_exit_work+0xf84/0x1290
  Workqueue: iou_exit io_ring_exit_work
  R14: ffff88800c2e7000

COMPARISON rc5 vs rc6:
  - Bug location: IDENTICAL (io_uring.c:2187, same workqueue)
  - WARN type: rc5=WARN_ON (fires every time), rc6=WARN_ON_ONCE (once/boot)
  - Function size: rc5=0x1290, rc6=0xd4b (refactoring occurred)
  - R14 non-null in both (ctx->bpf_filters still set during teardown)

REPRODUCER (3 syscalls, minimized by Syzkaller)
-----------------------------------------------
Requires: root / CAP_SYS_ADMIN

  # Step 1: Register BPF filter at task level (fd=-1)
  io_uring_register(-1, IORING_REGISTER_BPF_FILTER=0x25, &filter, 1)

  # Step 2: Create ring with DEFER_TASKRUN + R_DISABLED
  r0 = io_uring_setup(0x1bcf, {flags=IORING_SETUP_R_DISABLED|
       IORING_SETUP_SUBMIT_ALL|IORING_SETUP_SINGLE_ISSUER|
       IORING_SETUP_DEFER_TASKRUN, ...})

  # Step 3: Register restrictions (NULL arg)
  io_uring_register(r0, IORING_REGISTER_RESTRICTIONS=0xb, NULL, 2)

  # closing r0 triggers io_ring_exit_work → WARNING at line 2187

BPF filter used: cmd_type=1, opcode=0x3d (BPF_JMP|BPF_JSET), flags=3,
  2 instructions: [{code=0x02,jt=0x26,jf=0x02,k=0}, {code=0x06,jt=0x5c,jf=0x06,k=5}]

Syzlang repro:
  io_uring_register$IORING_REGISTER_BPF_FILTER(0xffffffffffffffff, 0x25,
      &(0x7f0000002280)={0x1, 0x0, 0x0, {0x3d, 0x3, 0x2, 0x0, '\x00',
      &(0x7f0000002300)=[{0x2, 0x26, 0x2}, {0x6, 0x5c, 0x6, 0x5}]}}, 0x1)
  r0 = io_uring_setup(0x1bcf, &(0x7f0000000000)={0x8, 0x1, 0x30c0, 0x0,
      0x8000, 0x7, 0xffffffffffffffff, '\x00', ...})
  io_uring_register(r0, 0xb, 0x0, 0x2)

C reproducer attached.

REPRODUCE 

sudo ./repro_io_ring_exit_work_loop


ANALYSIS
--------
The WARNING at io_uring.c:2187 fires inside io_ring_ctx_free() during ring
teardown via the iou_exit workqueue. Register R14 is non-null (pointing to
a live kernel object) at the WARN site in both rc5 and rc6, indicating that
ctx->bpf_filters is unexpectedly non-NULL when io_ring_ctx_free() asserts
it should be NULL.

Root cause hypothesis: When IORING_REGISTER_BPF_FILTER is called with
fd=-1 (task-level filter registration, new in Linux 7.0), and a ring is
subsequently created with IORING_SETUP_DEFER_TASKRUN, the ring inherits
the task-level BPF filter via io_ctx_restriction_clone()/io_bpf_filter_clone().
During ring teardown in io_ring_ctx_free(), ctx->bpf_filters is not
properly nulled/freed, triggering the assertion.

The exact WARN_ON is in io_ring_ctx_free() at line 2187 — likely
WARN_ON(ctx->bpf_filters) or similar check on the bpf_filters pointer.
The specific cleanup path in io_bpf_filter_clone() / io_bpf_filters_free()
interaction needs review.

CLASSIFICATION
--------------
CWE: CWE-459 (Incomplete Cleanup) — ctx->bpf_filters not cleaned up
     properly on ring teardown when filter was inherited from task context
Impact: Memory leak (bpf_filters object leaked per ring close),
        assertion violation in io_ring_ctx_free()
Requires: root/CAP_SYS_ADMIN
No memory corruption (KASAN clean, no double-free detected)

DISCOVERY
---------
Found via Syzkaller fuzzing campaign targeting Linux 7.0-rc5 io_uring
BPF filter subsystem (Blue Dragon Security, March 2026).
No matching syzbot entry found for this specific call path
(IORING_REGISTER_BPF_FILTER with fd=-1 + DEFER_TASKRUN).

Reported-by: Antonius <antonius@xxxxxxxxxxxxxxxxx>
Blue Dragon Security — https://bluedragonsec.com

/*
* Reproducer v2 (loop mode): WARNING in io_ring_exit_work
*
* Untuk memudahkan triggering bug di lab:
* Jalankan loop untuk meningkatkan probabilitas trigger.
*
* Discovered by: Antonius <antonius@xxxxxxxxxxxxxxxxx>
* Blue Dragon Security - bluedragonsec.com
*
* Compile: gcc -o repro2 repro_io_ring_exit_work_loop.c
* Run: ./repro2
*/

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <errno.h>

#ifndef __NR_io_uring_setup
#define __NR_io_uring_setup 425
#endif
#ifndef __NR_io_uring_register
#define __NR_io_uring_register 427
#endif

#define IORING_SETUP_R_DISABLED (1U << 6)
#define IORING_SETUP_SUBMIT_ALL (1U << 7)
#define IORING_SETUP_SINGLE_ISSUER (1U << 12)
#define IORING_SETUP_DEFER_TASKRUN (1U << 13)
#define IORING_REGISTER_RESTRICTIONS 11
#define IORING_REGISTER_BPF_FILTER 37

struct io_uring_params {
uint32_t sq_entries;
uint32_t cq_entries;
uint32_t flags;
uint32_t sq_thread_cpu;
uint32_t sq_thread_idle;
uint32_t features;
uint32_t wq_fd;
uint8_t pad[84];
};

struct sock_filter_insn { uint16_t code; uint8_t jt, jf; uint32_t k; };

struct io_uring_bpf_reg {
uint16_t cmd_type;
uint16_t cmd_flags;
uint32_t resv;
uint32_t opcode;
uint32_t flags;
uint32_t filter_len;
uint8_t pdu_size;
uint8_t resv2[3];
uint64_t filter_ptr;
uint8_t resv3[40];
};

int main(void)
{
struct sock_filter_insn insns[2] = {
{0x02, 0x26, 0x02, 0x00000000},
{0x06, 0x5c, 0x06, 0x00000005},
};

struct io_uring_bpf_reg bpf_reg = {
.cmd_type = 1,
.cmd_flags = 0,
.resv = 0,
.opcode = 0x3d,
.flags = 0x3,
.filter_len = 2,
.pdu_size = 0,
.filter_ptr = (uint64_t)(uintptr_t)insns,
};
memset(bpf_reg.resv3, 0, sizeof(bpf_reg.resv3));

printf("[*] io_ring_exit_work reproducer (loop mode)\n");
printf("[*] Iterating to trigger async WARNING in workqueue...\n\n");

for (int i = 0; i < 10; i++) {
/* Step 1: BPF_FILTER with fd=-1 */
syscall(__NR_io_uring_register, (long)-1, IORING_REGISTER_BPF_FILTER,
&bpf_reg, 1);

/* Step 2: io_uring_setup with DEFER_TASKRUN | R_DISABLED */
struct io_uring_params p = {
.sq_entries = 8,
.cq_entries = 1,
.flags = IORING_SETUP_R_DISABLED | IORING_SETUP_SUBMIT_ALL |
IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN,
.sq_thread_idle = 0x8000,
.features = 7,
.wq_fd = (uint32_t)-1,
};
int fd = (int)syscall(__NR_io_uring_setup, 0x1bcf, &p);
printf("[iter %2d] io_uring_setup fd=%d\n", i, fd);

if (fd >= 0) {
/* Step 3: RESTRICTIONS with NULL - triggers inconsistency */
syscall(__NR_io_uring_register, fd, IORING_REGISTER_RESTRICTIONS, 0, 2);
/* Close triggers io_ring_exit_work workqueue */
close(fd);
}

usleep(100000); /* 100ms - let workqueue process */
}

printf("\n[*] Check dmesg for: WARNING at io_uring/io_uring.c:2187\n");
printf("[*] Workqueue: iou_exit io_ring_exit_work\n");
return 0;
}