| 0/0 |
2026/08/09 03:49 |
flow |
assessment-security |
56m
Results: map[DenialOfService:true Explanation:**Bug Analysis:**
The bug is a refcount underflow on a `net_device` structure, which occurs in the
error handling path of `io_register_zcrx` in the io_uring subsystem.
When `io_register_zcrx` is called to register a zero-copy Rx interface queue
(`IORING_REGISTER_ZCRX_IFQ`), it eventually calls `zcrx_register_netdev`. Inside
`zcrx_register_netdev`, the code fetches the network device using
`netdev_get_by_index_lock()`:
```c
ifq->netdev = netdev_get_by_index_lock(current->nsproxy->net_ns,
reg->if_idx);
if (!ifq->netdev)
return -ENODEV;
if (!ifq->netdev->queue_mgmt_ops) {
ret = -EOPNOTSUPP;
goto netdev_put_unlock;
}
netdev_hold(ifq->netdev, &ifq->netdev_tracker, GFP_KERNEL);
```
The function `netdev_get_by_index_lock()` returns the `net_device` with its
instance lock held, but it **does not** hold a reference to the device (the
reference taken during the lookup is released before returning). The reference
is supposed to be explicitly taken later by calling `netdev_hold()`.
However, if the network device does not support queue management operations
(`!ifq->netdev->queue_mgmt_ops`), the function jumps to the error path
(`netdev_put_unlock`) and returns `-EOPNOTSUPP`. At this point, `ifq->netdev` is
already set, but `netdev_hold()` was never called.
The error propagates back to `io_register_zcrx`, which then calls
`zcrx_unregister()` -> `zcrx_unregister_user()` -> `io_close_queue()` to clean
up. `io_close_queue()` checks if `ifq->netdev` is not NULL, and if so, it
unconditionally calls `netdev_put()`:
```c
if (netdev) {
if (ifq->if_rxq != -1) {
// ...
}
netdev_put(netdev, &netdev_tracker);
}
```
Because a reference was never actually taken, this `netdev_put()` call drops a
non-existent reference, causing a refcount underflow (as seen in the
`refcount_t: decrement hit 0` warning).
**Security Impact:**
A refcount underflow on a `net_device` will eventually lead to the premature
freeing of the device, resulting in a Use-After-Free (UAF). UAF on a
`net_device` structure is a powerful primitive that is typically exploitable for
privilege escalation and can easily cause a Denial of Service (kernel crash).
However, the entry point to this vulnerable code path (`io_register_zcrx`) has a
strict capability check at the very beginning:
```c
if (!capable(CAP_NET_ADMIN))
return -EPERM;
```
The `capable()` function checks for the capability in the **initial user
namespace** (`init_user_ns`). This means that the bug cannot be triggered by
unprivileged users, nor can it be triggered from within a user namespace (where
a user might have `CAP_NET_ADMIN` but only relative to that specific namespace).
An attacker must already possess `CAP_NET_ADMIN` in the host's initial namespace
(effectively root) to reach the vulnerable code. Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/08/09 03:49 |
action |
syz-repro-to-c-repro |
0m
Results: map[SimplifiedCRepro:// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef __NR_io_uring_register
#define __NR_io_uring_register 427
#endif
#ifndef __NR_io_uring_setup
#define __NR_io_uring_setup 425
#endif
uint64_t r[1] = {0xffffffffffffffff};
int main(void)
{
syscall(__NR_mmap, /*addr=*/0x1ffffffff000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200000000000ul, /*len=*/0x1000000, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200001000000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
const char* reason;
(void)reason;
intptr_t res = 0;
if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
// io_uring_setup arguments: [
// entries: int32 = 0x1 (4 bytes)
// params: ptr[inout, io_uring_params] {
// io_uring_params {
// sq_entries: int32 = 0x0 (4 bytes)
// cq_entries: int32 = 0x0 (4 bytes)
// flags: io_uring_setup_flags = 0x3800 (4 bytes)
// sq_thread_cpu: int32 = 0x0 (4 bytes)
// sq_thread_idle: int32 = 0x0 (4 bytes)
// features: int32 = 0x0 (4 bytes)
// wq_fd: fd_io_uring (resource)
// resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00} (length 0xc)
// sq_off: io_sqring_offsets {
// head: int32 = 0x0 (4 bytes)
// tail: int32 = 0x0 (4 bytes)
// ring_mask: int32 = 0x0 (4 bytes)
// ring_entries: int32 = 0x0 (4 bytes)
// flags: int32 = 0x0 (4 bytes)
// dropped: int32 = 0x0 (4 bytes)
// array: int32 = 0x0 (4 bytes)
// resv1: int32 = 0x0 (4 bytes)
// user_addr: int64 = 0x0 (8 bytes)
// }
// cq_off: io_cqring_offsets {
// head: int32 = 0x0 (4 bytes)
// tail: int32 = 0x0 (4 bytes)
// ring_mask: int32 = 0x0 (4 bytes)
// ring_entries: int32 = 0x0 (4 bytes)
// overflow: int32 = 0x0 (4 bytes)
// cqes: int32 = 0x0 (4 bytes)
// flags: int32 = 0x0 (4 bytes)
// resv1: int32 = 0x0 (4 bytes)
// user_addr: int64 = 0x0 (8 bytes)
// }
// }
// }
// ]
// returns fd_io_uring
*(uint32_t*)0x200000000084 = 0;
*(uint32_t*)0x200000000088 = 0x3800;
*(uint32_t*)0x20000000008c = 0;
*(uint32_t*)0x200000000090 = 0;
*(uint32_t*)0x200000000098 = 0;
memset((void*)0x20000000009c, 0, 12);
res = syscall(__NR_io_uring_setup, /*entries=*/1, /*params=*/0x200000000080ul);
if (res != -1)
r[0] = res;
// io_uring_register$IORING_REGISTER_ZCRX_IFQ arguments: [
// fd: fd_io_uring (resource)
// opcode: const = 0x20 (8 bytes)
// arg: ptr[in, io_uring_zcrx_ifq_reg] {
// io_uring_zcrx_ifq_reg {
// if_idx: int32 = 0x1 (4 bytes)
// if_rxq: int32 = 0x0 (4 bytes)
// rq_entries: int32 = 0x8 (4 bytes)
// flags: zcrx_reg_flags = 0x0 (4 bytes)
// area_ptr: ptr[inout, io_uring_zcrx_area_reg] {
// io_uring_zcrx_area_reg {
// addr: nil
// len: len = 0x0 (8 bytes)
// rq_area_token: int64 = 0x0 (8 bytes)
// flags: io_uring_zcrx_area_flags = 0x0 (4 bytes)
// dmabuf_fd: int32 = 0x0 (4 bytes)
// __resv2: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x10)
// }
// }
// region_ptr: ptr[inout, io_uring_region_desc] {
// io_uring_region_desc {
// user_addr: ptr[inout, buffer] {
// buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x1000)
// }
// size: len = 0x1000 (8 bytes)
// flags: io_uring_region_desc_flags = 0x1 (4 bytes)
// id: int32 = 0x0 (4 bytes)
// mmap_offset: int64 = 0x0 (8 bytes)
// resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x20)
// }
// }
// offsets: io_uring_zcrx_offsets {
// head: int32 = 0x0 (4 bytes)
// tail: int32 = 0x0 (4 bytes)
// rqes: int32 = 0x0 (4 bytes)
// __resv2: const = 0x0 (4 bytes)
// __resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x10)
// }
// zcrx_id: int32 = 0x0 (4 bytes)
// rx_buf_len: int32 = 0x1000 (4 bytes)
// __resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x18)
// }
// }
// nr_args: const = 0x1 (8 bytes)
// ]
*(uint32_t*)0x200000000100 = 1;
*(uint32_t*)0x200000000104 = 0;
*(uint32_t*)0x200000000108 = 8;
*(uint32_t*)0x20000000010c = 0;
*(uint64_t*)0x200000000110 = 0x200000000200;
*(uint64_t*)0x200000000200 = 0;
*(uint64_t*)0x200000000208 = 0;
*(uint64_t*)0x200000000210 = 0;
*(uint32_t*)0x200000000218 = 0;
*(uint32_t*)0x20000000021c = 0;
memset((void*)0x200000000220, 0, 16);
*(uint64_t*)0x200000000118 = 0x200000000300;
*(uint64_t*)0x200000000300 = 0x200000002000;
memset((void*)0x200000002000, 0, 4096);
*(uint64_t*)0x200000000308 = 0x1000;
*(uint32_t*)0x200000000310 = 1;
*(uint32_t*)0x200000000314 = 0;
*(uint64_t*)0x200000000318 = 0;
memset((void*)0x200000000320, 0, 32);
*(uint32_t*)0x200000000120 = 0;
*(uint32_t*)0x200000000124 = 0;
*(uint32_t*)0x200000000128 = 0;
*(uint32_t*)0x20000000012c = 0;
memset((void*)0x200000000130, 0, 16);
*(uint32_t*)0x200000000140 = 0;
*(uint32_t*)0x200000000144 = 0x1000;
memset((void*)0x200000000148, 0, 24);
syscall(__NR_io_uring_register, /*fd=*/r[0], /*opcode=*/0x20ul, /*arg=*/0x200000000100ul, /*nr_args=*/1ul);
return 0;
}
]
|
| 2/1 |
2026/08/09 03:49 |
action |
kernel-checkouter |
8m
Results: map[KernelSrc:/app/workdir/cache/src/2947c5c1a11678cf78ec8c13d4f6fb56588f3968]
|
| 3/1 |
2026/08/09 03:58 |
action |
kernel-builder |
31m
Results: map[KernelObj:/app/workdir/cache/build/0731667083cd58b93aa2076e75e4a0c1970a8b95]
|
| 4/1 |
2026/08/09 04:30 |
action |
codesearch-prepare |
13m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/08/09 04:43 |
agent |
expert |
2m
Model:
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption, elevated privileges, or an information leak.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a completely wild pointer access
(e.g. user-space address, or non-canonical address, but not on NULL or address corresponding to KASAN shadow
for NULL address), including both data accesses and control transfers, that also usually implies possibility
of exploitation. Such reports usually say "unable to handle kernel paging request".
Uses of uninitialized values detected by KMSAN may be exploitable b/c attacker frequently can affect uninit
values with spraying techniques. However, for these exploitability depends on how exactly the uninit value
is used in the code, and what it affects.
Information leaks are exploitable on their own and should be classified as such. A bug that copies kernel
memory contents to userspace (e.g. an out-of-bounds read whose result is returned to the caller, or
uninitialized stack/heap bytes written to a user buffer) is exploitable: it can reveal kernel pointer
values and defeat KASLR, expose sensitive data such as cryptographic keys or other processes' memory, and
serves as a necessary building block in most modern kernel privilege-escalation exploit chains. Do not classify
an information leak as non-exploitable solely because it does not directly cause a memory write or control-flow
hijack; the leak itself is the exploit primitive.
Think of what happens after the bug is triggered. Some bugs cause kernel panic and halt execution,
they are harder to exploit. For example, BUG reports halts the kernel. However, WARNING reports don't halt
execution in production builds. Debug bug detection tools (like KASAN, KMSAN, KCSAN, UBSAN) are also not enabled
in production builds, so attacker can freely exploit these bugs w/o being detected by these tools.
If you see an integer overflow, think how the overflowed value used later (if it's used as allocation size,
or an array index). If you see an out-of-bounds read, think if it's followed by an out-of-bounds write as well.
Some KCSAN data-races may be exploitable by skilled attackers as well. Think what data structures got corrupted
as the result of data races and how. However, note that kernel has lots of "benign" data races that don't lead
to any runtime misbehavior at all.
* Denial Of Service:
Determine if the bug can result in denial-of-service. Most bugs can, since they cause system crash,
hangs, deadlocks, or resource leaks. This is mostly applicable to WARNING bugs that won't cause system crash
in production. For these think what will be consequences of the violation of the kernel assumptions flagged
by the WARNING. In some cases the unexpected condition is also properly handled by the normal control flow
(e.g. with "if (WARN_ON(...))"), these won't cause denial-of-service. If the condition is not handled,
then it may or may not cause denial-of-service.
* Accessible From Unprivileged Processes:
Determine if the bug can be reached from a typical (non-root) user process that does NOT have any special capabilities
(like CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON) or access to device nodes restricted to root.
Assume that unprivileged_bpf_disabled=1, that is eBPF loading is not accessible. However, cBPF (classical BPF)
is still accessible to non-root processes.
Assume that user namespaces are not accessible, that is, the process cannot get the mentioned capabilities even
within a new user namespace (checked by ns_capable() function in the kernel sources).
* Accessible From User Namespaces:
Determine if the bug can be reached within a user-namespace where the process has all capabilities
(including CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON). Such capabilities are checked with ns_capable()
function in the kernel sources.
* VM Guest Trigger:
Determine if the bug can be triggered from the context of a typical KVM guest (e.g., set up by a QEMU VMM).
Consider accesses to standard Linux host paravirtualized features (virtio-blk, virtio-net, etc.),
and handling of VM exits in the KVM code.
* VM Host Trigger in The Confidential Computing Context:
Determine if the bug can be triggered in a confidential computing guest kernel from the context of a KVM host.
Consider access to standard Linux guest paravirtualized features (virtio-blk, virtio-net, etc.).
* Ethernet Network Trigger:
Determine if the bug can be triggered by processing ingress network Ethernet traffic, either directly (network stack)
or via drivers exposed to network data.
* Other Remote Trigger:
Determine if the bug can be triggered by processing remote traffic other than Ethernet (Wifi, Bluetooth, NFC, etc).
* Peripheral Trigger:
Determine if the bug can be triggered via an untrusted peripheral device that can be physically plugged
into a system, such as a USB device or a niche hardware driver handling external hardware inputs.
This is particularly important for mobile and desktop environments where users can plug in unknown devices.
* Malicious Filesystem Trigger:
Determine if the bug can be triggered by the kernel mounting and parsing a malicious filesystem image.
This is highly critical for Desktop and Mobile environments where external media or downloaded images
might be auto-mounted.
Don't make assumptions about the kernel source code (it may be different from what you assume it is).
Extensively use the provided code access tools (codesearch-*, git-*, grepper, etc)
to examine the actual source code, and confirm any assumptions.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The kernel bug report is:
------------[ cut here ]------------
refcount_t: decrement hit 0; leaking memory.
WARNING: lib/refcount.c:31 at refcount_warn_saturate+0x70/0x110 lib/refcount.c:31, CPU#0: syz.0.17/5808
Modules linked in:
CPU: 0 UID: 0 PID: 5808 Comm: syz.0.17 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 07/24/2026
RIP: 0010:refcount_warn_saturate+0x70/0x110 lib/refcount.c:31
Code: c6 c0 fe 36 8f e8 90 e5 ee fc 83 fb 01 7e 22 83 fb 02 74 39 83 fb 03 74 47 83 fb 04 75 68 e8 f7 df ee fc 48 8d 3d b0 16 8b 0b <67> 48 0f b9 3a eb 66 85 db 74 3e 83 fb 01 75 4c e8 db df ee fc 48
RSP: 0018:ffffc900032d78c8 EFLAGS: 00010293
RAX: ffffffff84d8a429 RBX: 0000000000000004 RCX: ffff888032abddc0
RDX: 0000000000000000 RSI: ffffffff8f36fec0 RDI: ffffffff9063bae0
RBP: ffffc900032d7a10 R08: ffff888032abddc0 R09: 0000000000000005
R10: 0000000000000004 R11: 0000000000000000 R12: ffff8880353d2700
R13: dffffc0000000000 R14: ffff8880353d2744 R15: 0000000000000001
FS: 00005555674e4500(0000) GS:ffff888124f12000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000002020 CR3: 0000000079e90000 CR4: 00000000003526f0
Call Trace:
<TASK>
__refcount_dec include/linux/refcount.h:461 [inline]
refcount_dec include/linux/refcount.h:476 [inline]
ref_tracker_free+0x706/0x830 lib/ref_tracker.c:303
netdev_tracker_free include/linux/netdevice.h:4545 [inline]
netdev_put include/linux/netdevice.h:4562 [inline]
io_close_queue io_uring/zcrx.c:582 [inline]
zcrx_unregister_user+0x2d8/0x430 io_uring/zcrx.c:681
zcrx_unregister io_uring/zcrx.c:688 [inline]
io_register_zcrx+0x875/0xc00 io_uring/zcrx.c:1021
__io_uring_register io_uring/register.c:942 [inline]
__do_sys_io_uring_register io_uring/register.c:1040 [inline]
__se_sys_io_uring_register+0xc23/0x1ab0 io_uring/register.c:1017
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x510 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7fa6a959e0d9
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 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:00007ffe56af7168 EFLAGS: 00000246 ORIG_RAX: 00000000000001ab
RAX: ffffffffffffffda RBX: 00007fa6a9825fa0 RCX: 00007fa6a959e0d9
RDX: 0000200000000100 RSI: 0000000000000020 RDI: 0000000000000003
RBP: 00007fa6a9635024 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000001 R11: 0000000000000246 R12: 0000000000000000
R13: 00007fa6a9825fac R14: 00007fa6a9825fa0 R15: 00007fa6a9825fa0
</TASK>
----------------
Code disassembly (best guess), 5 bytes skipped:
0: e8 90 e5 ee fc call 0xfceee595
5: 83 fb 01 cmp $0x1,%ebx
8: 7e 22 jle 0x2c
a: 83 fb 02 cmp $0x2,%ebx
d: 74 39 je 0x48
f: 83 fb 03 cmp $0x3,%ebx
12: 74 47 je 0x5b
14: 83 fb 04 cmp $0x4,%ebx
17: 75 68 jne 0x81
19: e8 f7 df ee fc call 0xfceee015
1e: 48 8d 3d b0 16 8b 0b lea 0xb8b16b0(%rip),%rdi # 0xb8b16d5
* 25: 67 48 0f b9 3a ud1 (%edx),%rdi <-- trapping instruction
2a: eb 66 jmp 0x92
2c: 85 db test %ebx,%ebx
2e: 74 3e je 0x6e
30: 83 fb 01 cmp $0x1,%ebx
33: 75 4c jne 0x81
35: e8 db df ee fc call 0xfceee015
3a: 48 rex.W
It is reproducible with the followint program.
Keep in mind that it may lack the precise threading, sandboxing, and some arguments of a working reproducer.
But it should give an idea of the involved syscalls.
// autogenerated by syzkaller (https://github.com/google/syzkaller)
#define _GNU_SOURCE
#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef __NR_io_uring_register
#define __NR_io_uring_register 427
#endif
#ifndef __NR_io_uring_setup
#define __NR_io_uring_setup 425
#endif
uint64_t r[1] = {0xffffffffffffffff};
int main(void)
{
syscall(__NR_mmap, /*addr=*/0x1ffffffff000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200000000000ul, /*len=*/0x1000000, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
syscall(__NR_mmap, /*addr=*/0x200001000000ul, /*len=*/0x1000, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/(intptr_t)-1, /*offset=*/0ul);
const char* reason;
(void)reason;
intptr_t res = 0;
if (write(1, "executing program\n", sizeof("executing program\n") - 1)) {}
// io_uring_setup arguments: [
// entries: int32 = 0x1 (4 bytes)
// params: ptr[inout, io_uring_params] {
// io_uring_params {
// sq_entries: int32 = 0x0 (4 bytes)
// cq_entries: int32 = 0x0 (4 bytes)
// flags: io_uring_setup_flags = 0x3800 (4 bytes)
// sq_thread_cpu: int32 = 0x0 (4 bytes)
// sq_thread_idle: int32 = 0x0 (4 bytes)
// features: int32 = 0x0 (4 bytes)
// wq_fd: fd_io_uring (resource)
// resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00} (length 0xc)
// sq_off: io_sqring_offsets {
// head: int32 = 0x0 (4 bytes)
// tail: int32 = 0x0 (4 bytes)
// ring_mask: int32 = 0x0 (4 bytes)
// ring_entries: int32 = 0x0 (4 bytes)
// flags: int32 = 0x0 (4 bytes)
// dropped: int32 = 0x0 (4 bytes)
// array: int32 = 0x0 (4 bytes)
// resv1: int32 = 0x0 (4 bytes)
// user_addr: int64 = 0x0 (8 bytes)
// }
// cq_off: io_cqring_offsets {
// head: int32 = 0x0 (4 bytes)
// tail: int32 = 0x0 (4 bytes)
// ring_mask: int32 = 0x0 (4 bytes)
// ring_entries: int32 = 0x0 (4 bytes)
// overflow: int32 = 0x0 (4 bytes)
// cqes: int32 = 0x0 (4 bytes)
// flags: int32 = 0x0 (4 bytes)
// resv1: int32 = 0x0 (4 bytes)
// user_addr: int64 = 0x0 (8 bytes)
// }
// }
// }
// ]
// returns fd_io_uring
*(uint32_t*)0x200000000084 = 0;
*(uint32_t*)0x200000000088 = 0x3800;
*(uint32_t*)0x20000000008c = 0;
*(uint32_t*)0x200000000090 = 0;
*(uint32_t*)0x200000000098 = 0;
memset((void*)0x20000000009c, 0, 12);
res = syscall(__NR_io_uring_setup, /*entries=*/1, /*params=*/0x200000000080ul);
if (res != -1)
r[0] = res;
// io_uring_register$IORING_REGISTER_ZCRX_IFQ arguments: [
// fd: fd_io_uring (resource)
// opcode: const = 0x20 (8 bytes)
// arg: ptr[in, io_uring_zcrx_ifq_reg] {
// io_uring_zcrx_ifq_reg {
// if_idx: int32 = 0x1 (4 bytes)
// if_rxq: int32 = 0x0 (4 bytes)
// rq_entries: int32 = 0x8 (4 bytes)
// flags: zcrx_reg_flags = 0x0 (4 bytes)
// area_ptr: ptr[inout, io_uring_zcrx_area_reg] {
// io_uring_zcrx_area_reg {
// addr: nil
// len: len = 0x0 (8 bytes)
// rq_area_token: int64 = 0x0 (8 bytes)
// flags: io_uring_zcrx_area_flags = 0x0 (4 bytes)
// dmabuf_fd: int32 = 0x0 (4 bytes)
// __resv2: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x10)
// }
// }
// region_ptr: ptr[inout, io_uring_region_desc] {
// io_uring_region_desc {
// user_addr: ptr[inout, buffer] {
// buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x1000)
// }
// size: len = 0x1000 (8 bytes)
// flags: io_uring_region_desc_flags = 0x1 (4 bytes)
// id: int32 = 0x0 (4 bytes)
// mmap_offset: int64 = 0x0 (8 bytes)
// resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x20)
// }
// }
// offsets: io_uring_zcrx_offsets {
// head: int32 = 0x0 (4 bytes)
// tail: int32 = 0x0 (4 bytes)
// rqes: int32 = 0x0 (4 bytes)
// __resv2: const = 0x0 (4 bytes)
// __resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x10)
// }
// zcrx_id: int32 = 0x0 (4 bytes)
// rx_buf_len: int32 = 0x1000 (4 bytes)
// __resv: buffer: {00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00} (length 0x18)
// }
// }
// nr_args: const = 0x1 (8 bytes)
// ]
*(uint32_t*)0x200000000100 = 1;
*(uint32_t*)0x200000000104 = 0;
*(uint32_t*)0x200000000108 = 8;
*(uint32_t*)0x20000000010c = 0;
*(uint64_t*)0x200000000110 = 0x200000000200;
*(uint64_t*)0x200000000200 = 0;
*(uint64_t*)0x200000000208 = 0;
*(uint64_t*)0x200000000210 = 0;
*(uint32_t*)0x200000000218 = 0;
*(uint32_t*)0x20000000021c = 0;
memset((void*)0x200000000220, 0, 16);
*(uint64_t*)0x200000000118 = 0x200000000300;
*(uint64_t*)0x200000000300 = 0x200000002000;
memset((void*)0x200000002000, 0, 4096);
*(uint64_t*)0x200000000308 = 0x1000;
*(uint32_t*)0x200000000310 = 1;
*(uint32_t*)0x200000000314 = 0;
*(uint64_t*)0x200000000318 = 0;
memset((void*)0x200000000320, 0, 32);
*(uint32_t*)0x200000000120 = 0;
*(uint32_t*)0x200000000124 = 0;
*(uint32_t*)0x200000000128 = 0;
*(uint32_t*)0x20000000012c = 0;
memset((void*)0x200000000130, 0, 16);
*(uint32_t*)0x200000000140 = 0;
*(uint32_t*)0x200000000144 = 0x1000;
memset((void*)0x200000000148, 0, 24);
syscall(__NR_io_uring_register, /*fd=*/r[0], /*opcode=*/0x20ul, /*arg=*/0x200000000100ul, /*nr_args=*/1ul);
return 0;
}
Reply: **Bug Analysis:**
The bug is a refcount underflow on a `net_device` structure, which occurs in the error handling path of `io_register_zcrx` in the io_uring subsystem.
When `io_register_zcrx` is called to register a zero-copy Rx interface queue (`IORING_REGISTER_ZCRX_IFQ`), it eventually calls `zcrx_register_netdev`. Inside `zcrx_register_netdev`, the code fetches the network device using `netdev_get_by_index_lock()`:
```c
ifq->netdev = netdev_get_by_index_lock(current->nsproxy->net_ns,
reg->if_idx);
if (!ifq->netdev)
return -ENODEV;
if (!ifq->netdev->queue_mgmt_ops) {
ret = -EOPNOTSUPP;
goto netdev_put_unlock;
}
netdev_hold(ifq->netdev, &ifq->netdev_tracker, GFP_KERNEL);
```
The function `netdev_get_by_index_lock()` returns the `net_device` with its instance lock held, but it **does not** hold a reference to the device (the reference taken during the lookup is released before returning). The reference is supposed to be explicitly taken later by calling `netdev_hold()`.
However, if the network device does not support queue management operations (`!ifq->netdev->queue_mgmt_ops`), the function jumps to the error path (`netdev_put_unlock`) and returns `-EOPNOTSUPP`. At this point, `ifq->netdev` is already set, but `netdev_hold()` was never called.
The error propagates back to `io_register_zcrx`, which then calls `zcrx_unregister()` -> `zcrx_unregister_user()` -> `io_close_queue()` to clean up. `io_close_queue()` checks if `ifq->netdev` is not NULL, and if so, it unconditionally calls `netdev_put()`:
```c
if (netdev) {
if (ifq->if_rxq != -1) {
// ...
}
netdev_put(netdev, &netdev_tracker);
}
```
Because a reference was never actually taken, this `netdev_put()` call drops a non-existent reference, causing a refcount underflow (as seen in the `refcount_t: decrement hit 0` warning).
**Security Impact:**
A refcount underflow on a `net_device` will eventually lead to the premature freeing of the device, resulting in a Use-After-Free (UAF). UAF on a `net_device` structure is a powerful primitive that is typically exploitable for privilege escalation and can easily cause a Denial of Service (kernel crash).
However, the entry point to this vulnerable code path (`io_register_zcrx`) has a strict capability check at the very beginning:
```c
if (!capable(CAP_NET_ADMIN))
return -EPERM;
```
The `capable()` function checks for the capability in the **initial user namespace** (`init_user_ns`). This means that the bug cannot be triggered by unprivileged users, nor can it be triggered from within a user namespace (where a user might have `CAP_NET_ADMIN` but only relative to that specific namespace). An attacker must already possess `CAP_NET_ADMIN` in the host's initial namespace (effectively root) to reach the vulnerable code.
|
| 6/2 |
2026/08/09 04:43 |
llm |
expert |
0m
Model:
Tokens:
input: 23417
output: 40
thoughts: 520
Thoughts: **Analyzing a Refcount Leak in io_uring/zcrx.c**
Okay, so I've got this syzkaller bug report. The core issue is a `refcount_t` decrement hitting zero, indicating a memory leak. Specifically, it's flagging a `refcount_warn_saturate` in `lib/refcount.c` with the CPU and syzkaller process ID. The call trace points to the `zcrx` functionality within `io_uring`.
The stack trace tells a story: the `ref_tracker_free` function in `lib/ref_tracker.c` is where things are going sideways, which is called by the `zcrx` functions. I see it starts with `refcount_dec` from `include/linux/refcount.h`, then `ref_tracker_free` which in turn calls `netdev_tracker_free` and `netdev_put`, ultimately leading into `io_close_queue` and then finally, the `zcrx` unregister functions, specifically `zcrx_unregister_user` and `io_register_zcrx` functions. This points directly to the `zcrx_unregister_user` routine.
The reproducer utilizes `io_uring_setup` and `io_uring_register` with the `IORING_REGISTER_ZCRX_IFQ` opcode (0x20). My focus needs to be on the interplay between `io_register_zcrx` and `zcrx_unregister_user`. I need to dig into how these functions are managing the refcounts and where that refcount is getting decremented too many times. There's a clear connection to the `io_uring` subsystem and the `zcrx` (zero-copy receive) functionality, so it seems like there might be a flaw in its resource management, likely in its handling of refcounts or the overall lifecycle of these `zcrx` related objects.
|
| 7/2 |
2026/08/09 04:43 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:io_register_zcrx]
Results: map[SourceCode: 888: int io_register_zcrx(struct io_ring_ctx *ctx,
889: struct io_uring_zcrx_ifq_reg __user *arg)
890: {
891: struct zcrx_event_desc notif;
892: struct io_uring_zcrx_area_reg area;
893: struct io_uring_zcrx_ifq_reg reg;
894: struct io_uring_region_desc rd;
895: struct io_zcrx_ifq *ifq;
896: int ret;
897: u32 id;
898:
899: /*
900: * 1. Interface queue allocation.
901: * 2. It can observe data destined for sockets of other tasks.
902: */
903: if (!capable(CAP_NET_ADMIN))
904: return -EPERM;
905:
906: /* mandatory io_uring features for zc rx */
907: if (!(ctx->flags & IORING_SETUP_DEFER_TASKRUN))
908: return -EINVAL;
909: if (!(ctx->flags & (IORING_SETUP_CQE32|IORING_SETUP_CQE_MIXED)))
910: return -EINVAL;
911: if (copy_from_user(®, arg, sizeof(reg)))
912: return -EFAULT;
913: if (!mem_is_zero(®.__resv, sizeof(reg.__resv)) || reg.zcrx_id)
914: return -EINVAL;
915: if (reg.flags & ~ZCRX_SUPPORTED_REG_FLAGS)
916: return -EINVAL;
917: if (reg.flags & ZCRX_REG_IMPORT)
918: return import_zcrx(ctx, arg, ®);
919: if (copy_from_user(&rd, u64_to_user_ptr(reg.region_ptr), sizeof(rd)))
920: return -EFAULT;
921: if (reg.if_rxq == -1 || !reg.rq_entries)
922: return -EINVAL;
923: if ((reg.if_rxq || reg.if_idx) && (reg.flags & ZCRX_REG_NODEV))
924: return -EINVAL;
925: if (reg.rq_entries > IO_RQ_MAX_ENTRIES) {
926: if (!(ctx->flags & IORING_SETUP_CLAMP))
927: return -EINVAL;
928: reg.rq_entries = IO_RQ_MAX_ENTRIES;
929: }
930: reg.rq_entries = roundup_pow_of_two(reg.rq_entries);
931:
932: if (copy_from_user(&area, u64_to_user_ptr(reg.area_ptr), sizeof(area)))
933: return -EFAULT;
934:
935: memset(¬if, 0, sizeof(notif));
936: if (reg.event_desc && copy_from_user(¬if, u64_to_user_ptr(reg.event_desc),
937: sizeof(notif)))
938: return -EFAULT;
939: if (notif.type_mask & ~ZCRX_EVENT_TYPE_MASK)
940: return -EINVAL;
941: if (notif.flags & ~ZCRX_EVENT_DESC_FLAG_STATS)
942: return -EINVAL;
943: if (!(notif.flags & ZCRX_EVENT_DESC_FLAG_STATS)) {
944: if (notif.stats_offset)
945: return -EINVAL;
946: }
947: if (!mem_is_zero(¬if.__resv2, sizeof(notif.__resv2)))
948: return -EINVAL;
949:
950: ifq = io_zcrx_ifq_alloc(ctx);
951: if (!ifq)
952: return -ENOMEM;
953:
954: ifq->notif_data = notif.user_data;
955: ifq->allowed_notif_mask = notif.type_mask;
956:
957: if (ctx->user) {
958: get_uid(ctx->user);
959: ifq->user = ctx->user;
960: }
961: if (ctx->mm_account) {
962: mmgrab(ctx->mm_account);
963: ifq->mm_account = ctx->mm_account;
964: }
965: ifq->rq.nr_entries = reg.rq_entries;
966:
967: scoped_guard(mutex, &ctx->mmap_lock) {
968: /* preallocate id */
969: ret = xa_alloc(&ctx->zcrx_ctxs, &id, NULL, xa_limit_31b, GFP_KERNEL);
970: if (ret)
971: goto ifq_free;
972: }
973:
974: ret = io_allocate_rbuf_ring(ctx, ifq, ®, &rd, id);
975: if (ret)
976: goto err;
977:
978: if (notif.flags & ZCRX_EVENT_DESC_FLAG_STATS) {
979: ret = zcrx_validate_notif_stats(ifq, ®, ¬if);
980: if (ret)
981: goto err;
982: }
983:
984: ifq->kern_readable = !(area.flags & IORING_ZCRX_AREA_DMABUF);
985:
986: if (!(reg.flags & ZCRX_REG_NODEV)) {
987: ret = zcrx_register_netdev(ifq, ®, &area);
988: if (ret)
989: goto err;
990: } else {
991: ret = io_zcrx_create_area(ifq, &area, ®);
992: if (ret)
993: goto err;
994: }
995:
996: reg.zcrx_id = id;
997:
998: scoped_guard(mutex, &ctx->mmap_lock) {
999: /* publish ifq */
1000: ret = -ENOMEM;
1001: if (xa_store(&ctx->zcrx_ctxs, id, ifq, GFP_KERNEL))
1002: goto err;
1003: }
1004:
1005: reg.rx_buf_len = 1U << ifq->niov_shift;
1006:
1007: if (copy_to_user(arg, ®, sizeof(reg)) ||
1008: copy_to_user(u64_to_user_ptr(reg.region_ptr), &rd, sizeof(rd)) ||
1009: copy_to_user(u64_to_user_ptr(reg.area_ptr), &area, sizeof(area))) {
1010: ret = -EFAULT;
1011: goto err;
1012: }
1013:
1014: if (notif.type_mask)
1015: zcrx_set_ring_ctx(ifq, ctx);
1016: return 0;
1017: err:
1018: scoped_guard(mutex, &ctx->mmap_lock)
1019: xa_erase(&ctx->zcrx_ctxs, id);
1020: ifq_free:
1021: zcrx_unregister(ifq, ctx);
1022: return ret;
1023: }
SourceFile:io_uring/zcrx.c]
|
| 8/2 |
2026/08/09 04:43 |
llm |
expert |
0m
Model:
Tokens:
input: 26711
output: 38
thoughts: 162
Thoughts: **Analyzing the Permissions in `io_register_zcrx`**
Okay, so I'm looking at this `io_register_zcrx` function, and the first thing I see is this `if (!capable(CAP_NET_ADMIN)) return -EPERM;` line. Right, so it's checking for `CAP_NET_ADMIN` β which, I immediately think, means we're looking at permissions at the *initial* user namespace level. I have to make sure that is the case.
And, yeah, my brain says, if `capable(CAP_NET_ADMIN)` is required, that inherently means this function isn't accessible to unprivileged processes. And definitely *not* accessible from within user namespaces, right? Because it's using the standard `capable()` call and not the namespaced version `ns_capable()`. I need to double-check that `capable()` always checks `init_user_ns`. Yes, I'm right, it checks the initial user namespace.
Next, I should really look at `zcrx_unregister_user` and `zcrx_unregister`, just to be certain. I should see if the permission controls are similar.
|
| 9/2 |
2026/08/09 04:43 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:zcrx_unregister]
Results: map[SourceCode: 686: static void zcrx_unregister(struct io_zcrx_ifq *ifq, struct io_ring_ctx *ctx)
687: {
688: zcrx_unregister_user(ifq, ctx);
689: io_put_zcrx_ifq(ifq);
690: }
SourceFile:io_uring/zcrx.c]
|
| 10/2 |
2026/08/09 04:43 |
llm |
expert |
0m
Model:
Tokens:
input: 27295
output: 40
thoughts: 13
Thoughts: Here's a first-person summary, assuming I'm an expert and this is my thought process:
**Diving into `zcrx_unregister_user`**
Okay, time to dissect this. `zcrx_unregister_user`... the name itself screams "user management." Given the naming convention, this likely resides within the ZCRX framework, suggesting it's central to handling user accounts, registrations, or authentication. I'm immediately thinking about its role in lifecycle management β are we talking about account deletion, deactivation, or a more subtle unbinding from resources? What are the implications of this action? Any cleanup routines triggered? I need to understand the side effects. Is this a critical function? Does it involve data sanitization, resource release, or perhaps even logging events related to the unregistration? I'm going to look at its implementation and see what the dependencies are, and whether any concurrency concerns need to be addressed.
|
| 11/2 |
2026/08/09 04:43 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:zcrx_unregister_user]
Results: map[SourceCode: 671: static void zcrx_unregister_user(struct io_zcrx_ifq *ifq, struct io_ring_ctx *ctx)
672: {
673: scoped_guard(spinlock_bh, &ifq->ctx_lock) {
674: if (ctx && ifq->master_ctx == ctx) {
675: ifq->master_ctx = NULL;
676: percpu_ref_put(&ctx->refs);
677: }
678: }
679:
680: if (refcount_dec_and_test(&ifq->user_refs)) {
681: io_close_queue(ifq);
682: io_zcrx_scrub(ifq);
683: }
684: }
SourceFile:io_uring/zcrx.c]
|
| 12/2 |
2026/08/09 04:43 |
llm |
expert |
0m
Model:
Tokens:
input: 27805
output: 38
thoughts: 16
Thoughts: Here's my understanding of the thought process, from the perspective of an expert:
**Understanding the Relationship Between `io_close_queue` and `netdev_put`**
Okay, so I'm focusing on the interaction between `io_close_queue` and `netdev_put`. My immediate thought is, "Wait, `io_close_queue` *calls* `netdev_put`?" That's a crucial connection. It signifies a very specific relationship and probably indicates how resources are managed within this context. The significance here is likely a deep-dive into resource management. It means closing the I/O queue will trigger a "put" operation on a network device. This strongly suggests that `netdev_put` is responsible for freeing, or at least decreasing the reference count of, a network device in some way, which is tightly coupled with whatever `io_close_queue` is doing. This definitely merits further investigation to understand *why* this call is happening and, most importantly, *when*. This could be where resource leaks are occurring.
|
| 13/2 |
2026/08/09 04:43 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:io_close_queue]
Results: map[SourceCode: 561: static void io_close_queue(struct io_zcrx_ifq *ifq)
562: {
563: struct net_device *netdev;
564: netdevice_tracker netdev_tracker;
565: struct pp_memory_provider_params p = {
566: .mp_ops = &io_uring_pp_zc_ops,
567: .mp_priv = ifq,
568: };
569:
570: scoped_guard(mutex, &ifq->pp_lock) {
571: netdev = ifq->netdev;
572: netdev_tracker = ifq->netdev_tracker;
573: ifq->netdev = NULL;
574: }
575:
576: if (netdev) {
577: if (ifq->if_rxq != -1) {
578: netdev_lock(netdev);
579: netif_mp_close_rxq(netdev, ifq->if_rxq, &p);
580: netdev_unlock(netdev);
581: }
582: netdev_put(netdev, &netdev_tracker);
583: }
584: ifq->if_rxq = -1;
585: }
SourceFile:io_uring/zcrx.c]
|
| 14/2 |
2026/08/09 04:43 |
llm |
expert |
0m
Model:
Tokens:
input: 28500
output: 43
thoughts: 119
Thoughts: **Analyzing the I/O Registration and Unregistration Flow**
Okay, let's break this down. It seems the core of this I/O registration/unregistration process hinges around `io_close_queue`, triggered by `zcrx_unregister_user`. That's a key linkage to keep in mind.
Now, looking at the potential failure paths in `io_register_zcrx`, I see a clear sequence. If either `zcrx_register_netdev` or `io_zcrx_create_area` throws an error, we immediately jump to the `err` label. And from there, it's straight to `ifq_free`. That's the expected cleanup procedure, which I'd expect given the failure.
The interesting part is the `io_register_zcrx` allocation. The first line of `io_register_zcrx` is to call the `io_zcrx_ifq_alloc` function. If that returns NULL, it's an immediate `-ENOMEM`, signifying a memory allocation issue. I'll need to double check the allocation logic and the context being passed for that to be certain of the nature of the issue.
|
| 15/2 |
2026/08/09 04:43 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:io_zcrx_ifq_alloc]
Results: map[SourceCode: 534: static struct io_zcrx_ifq *io_zcrx_ifq_alloc(struct io_ring_ctx *ctx)
535: {
536: struct io_zcrx_ifq *ifq;
537:
538: ifq = kzalloc_obj(*ifq);
539: if (!ifq)
540: return NULL;
541:
542: ifq->if_rxq = -1;
543: spin_lock_init(&ifq->ctx_lock);
544: spin_lock_init(&ifq->rq.lock);
545: mutex_init(&ifq->pp_lock);
546: refcount_set(&ifq->refs, 1);
547: refcount_set(&ifq->user_refs, 1);
548: return ifq;
549: }
SourceFile:io_uring/zcrx.c]
|
| 16/2 |
2026/08/09 04:43 |
llm |
expert |
0m
Model:
Tokens:
input: 29210
output: 41
thoughts: 384
Thoughts: **Analyzing the ZCRX Network Device Registration and Unregistration Flow**
Okay, so I'm tracing this `ifq->netdev` pointer through the registration and unregistration process. I see that it starts as `NULL`, which is the expected initial state. Then, when a device registers in `io_register_zcrx`, the code branches. If `ZCRX_REG_NODEV` is *not* set, the code calls `zcrx_register_netdev`. If that call fails, we hit the `err` label, and `ifq_free` gets called, which unregisters via `zcrx_unregister`. That unregistration path ultimately leads to `io_close_queue`.
Now, within `io_close_queue`, there's a critical section protected by `ifq->pp_lock`. Inside, I observe the assignment: `netdev = ifq->netdev`. Then, and *only if* `netdev` is actually not NULL, the code proceeds to shut down the RX queue and call `netdev_put`.
The part that worries me, is if `zcrx_register_netdev` fails, and subsequently `io_close_queue` is called. It implies the `netdev` is indeed NULL. Which leads to `netdev_put` *not* being called! That's a potential resource leak.
Let me take a step back and examine `zcrx_register_netdev` more closely...
|
| 17/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:zcrx_register_netdev]
Results: map[SourceCode: 816: static int zcrx_register_netdev(struct io_zcrx_ifq *ifq,
817: struct io_uring_zcrx_ifq_reg *reg,
818: struct io_uring_zcrx_area_reg *area)
819: {
820: struct pp_memory_provider_params mp_param = {};
821: unsigned if_rxq = reg->if_rxq;
822: int ret;
823:
824: ifq->netdev = netdev_get_by_index_lock(current->nsproxy->net_ns,
825: reg->if_idx);
826: if (!ifq->netdev)
827: return -ENODEV;
828: if (!ifq->netdev->queue_mgmt_ops) {
829: ret = -EOPNOTSUPP;
830: goto netdev_put_unlock;
831: }
832:
833: netdev_hold(ifq->netdev, &ifq->netdev_tracker, GFP_KERNEL);
834:
835: ifq->dev = netdev_queue_get_dma_dev(ifq->netdev, if_rxq, NETDEV_QUEUE_TYPE_RX);
836: if (!ifq->dev) {
837: ret = -EOPNOTSUPP;
838: goto netdev_put_unlock;
839: }
840: get_device(ifq->dev);
841:
842: ret = io_zcrx_create_area(ifq, area, reg);
843: if (ret)
844: goto netdev_put_unlock;
845:
846: if (reg->rx_buf_len)
847: mp_param.rx_page_size = 1U << ifq->niov_shift;
848: mp_param.mp_ops = &io_uring_pp_zc_ops;
849: mp_param.mp_priv = ifq;
850: ret = netif_mp_open_rxq(ifq->netdev, if_rxq, &mp_param, NULL);
851: if (ret)
852: goto netdev_put_unlock;
853:
854: ifq->if_rxq = if_rxq;
855: ret = 0;
856: netdev_put_unlock:
857: netdev_unlock(ifq->netdev);
858: return ret;
859: }
SourceFile:io_uring/zcrx.c]
|
| 18/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 30731
output: 41
thoughts: 187
Thoughts: **Analyzing `zcrx_register_netdev` in Detail**
Okay, so I'm looking at this `zcrx_register_netdev` function, and my first thought is: "Aha! This is where the magic happens." I see it's getting a network device using `netdev_get_by_index_lock`. Wait a minute, what is that doing? Does it return a netdev with a reference count bumped? I need to clarify that. I need to make sure I understand the reference counting implications here, because that's crucial. I need to know if I'm supposed to `put` it later. Let me check the definition of `netdev_get_by_index_lock` to be absolutely certain how the reference count is handled. This is where things can get dicey if you're not careful.
|
| 19/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:net/core/dev.c Name:netdev_get_by_index_lock]
Results: map[SourceCode:1118: /**
1119: * netdev_get_by_index_lock() - find a device by its ifindex
1120: * @net: the applicable net namespace
1121: * @ifindex: index of device
1122: *
1123: * Search for an interface by index. If a valid device
1124: * with @ifindex is found it will be returned with netdev->lock held.
1125: * netdev_unlock() must be called to release it.
1126: *
1127: * Return: pointer to a device with lock held, NULL if not found.
1128: */
1129: struct net_device *netdev_get_by_index_lock(struct net *net, int ifindex)
1130: {
1131: struct net_device *dev;
1132:
1133: dev = dev_get_by_index(net, ifindex);
1134: if (!dev)
1135: return NULL;
1136:
1137: return __netdev_put_lock(dev, net);
1138: }
SourceFile:net/core/dev.c]
|
| 20/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 31494
output: 38
thoughts: 52
Thoughts: Here's my summary, as the expert:
**Navigating Network Device Reference Counting**
Okay, so `dev_get_by_index`... that hands back a *reference*, right? And then, the question is, how does this all work with the locking and reference management? Is the expectation that `__netdev_put_lock` is where that reference is ultimately placed and the netdev returned with its lock?
Let's dive into the guts of `__netdev_put_lock` and confirm my assumptions. Need to see how the references are manipulated within that function.
|
| 21/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:net/core/dev.c Name:__netdev_put_lock]
Results: map[SourceCode: 37: static inline struct net_device *
38: __netdev_put_lock(struct net_device *dev, struct net *net)
39: {
40: return netdev_put_lock(dev, net, NULL);
41: }
SourceFile:net/core/dev.h]
|
| 22/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 31813
output: 37
thoughts: 12
Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience:
**Diving into `netdev_put_lock`**
Okay, so we're looking at `netdev_put_lock`. My immediate thought is, what's the context here? Are we talking about the global lock on the network device list, or is this something more specific, like a per-device spinlock related to reference counting or some other critical section? I need to quickly establish the scope. I'm assuming we're dealing with a kernel context here, but just to be sure, I should check the surrounding code for clues.
Knowing the function name, I'm already anticipating a release operation, likely associated with a previously acquired lock, probably called via `netdev_get_lock` or some similar function. The naming convention strongly suggests a "put" or release action. I need to understand the synchronization semantics involved. Is it a mutex, a spinlock, read-write lock, or something more specialized? Does it involve any ordering constraints with other locks or memory barriers?
My next step is to quickly review the data structures protected by this lock. What exactly is `netdev_put_lock` supposed to protect? Is it protecting device state, shared data structures, or something else? I need to be sure this function is implemented in a way that provides the necessary synchronization guarantees. I should check if there are any potential race conditions or deadlocks introduced if the lock isn't handled correctly.
Given my past experience with similar kernel locking primitives, I anticipate the core of the implementation will involve atomic operations or some kind of conditional release. I'll need to confirm that this is correctly done to ensure proper synchronization, especially considering multi-processor environments and potential preemption. I should also be mindful of potential lock contention issues. If this is a frequently used lock, I might want to see how the system handles lock contention.
|
| 23/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:net/core/dev.h Name:netdev_put_lock]
Results: map[SourceCode:1081: /* Release the held reference on the net_device, and if the net_device
1082: * is still registered try to lock the instance lock. If device is being
1083: * unregistered NULL will be returned (but the reference has been released,
1084: * either way!)
1085: *
1086: * This helper is intended for locking net_device after it has been looked up
1087: * using a lockless lookup helper. Lock prevents the instance from going away.
1088: */
1089: struct net_device *
1090: netdev_put_lock(struct net_device *dev, struct net *net,
1091: netdevice_tracker *tracker)
1092: {
1093: netdev_lock(dev);
1094: if (dev->reg_state > NETREG_REGISTERED ||
1095: dev->moving_ns || !net_eq(dev_net(dev), net)) {
1096: netdev_unlock(dev);
1097: netdev_put(dev, tracker);
1098: return NULL;
1099: }
1100: netdev_put(dev, tracker);
1101: return dev;
1102: }
SourceFile:net/core/dev.c]
|
| 24/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 32639
output: 30
thoughts: 1730
Thoughts: **Refcount Underflow in zcrx_register_netdev Leads to Use-After-Free Vulnerability**
Okay, so `netdev_put_lock` releases a reference, good. `netdev_get_by_index_lock` returns a net device with the lock held but *without* an extra reference. That's key. Now, `zcrx_register_netdev` in its use of `netdev_get_by_index_lock` is where things get interesting. It grabs the device:
```c
ifq->netdev = netdev_get_by_index_lock(current->nsproxy->net_ns,
reg->if_idx);
if (!ifq->netdev)
return -ENODEV;
if (!ifq->netdev->queue_mgmt_ops) {
ret = -EOPNOTSUPP;
goto netdev_put_unlock;
}
netdev_hold(ifq->netdev, &ifq->netdev_tracker, GFP_KERNEL);
```
If `!ifq->netdev->queue_mgmt_ops` fails, it goes to `netdev_put_unlock`, then returns `-EOPNOTSUPP`. The critical part is that `ifq->netdev` is already assigned the result of `netdev_get_by_index_lock` *before* the check and potential early return.
Then, during unregistration, `io_close_queue` is called which in turn has a call to `netdev_put`:
```c
scoped_guard(mutex, &ifq->pp_lock) {
netdev = ifq->netdev;
netdev_tracker = ifq->netdev_tracker;
ifq->netdev = NULL;
}
if (netdev) {
if (ifq->if_rxq != -1) {
netdev_lock(netdev);
netif_mp_close_rxq(netdev, ifq->if_rxq, &p);
netdev_unlock(netdev);
}
netdev_put(netdev, &netdev_tracker);
}
```
Here's the problem: if `!ifq->netdev->queue_mgmt_ops` is true, then `netdev_hold` *isn't* called, so the initial reference from `netdev_get_by_index_lock` is still the *only* reference. However, `io_close_queue` is still going to call `netdev_put`, causing an underflow in the reference count.
Let's look at the flow again. The assignment happens first, then the check, then the `goto` if it fails. So `ifq->netdev` is set even when we don't have an extra ref. This leads to a double-put scenario, or rather, putting a reference we didn't take. This will cause a refcount decrement hit 0 warning.
Okay, other failure paths in `zcrx_register_netdev` don't have the same problem: if the failure is later on after `netdev_hold`, then `netdev_put` in `io_close_queue` is correct.
The specific path:
```c
if (!ifq->netdev->queue_mgmt_ops) {
ret = -EOPNOTSUPP;
goto netdev_put_unlock;
}
```
This is where the bug lives! `ifq->netdev` is assigned *before* `netdev_hold` is called, and if the check fails, `io_close_queue` will still call `netdev_put` leading to the underflow.
Is this exploitable? A refcount underflow on `net_device` will lead to it being freed prematurely! That's a use-after-free vulnerability! This is exploitable... but, wait. `io_register_zcrx` requires `CAP_NET_ADMIN`. And that capability is checked against the *initial* user namespace! Okay, time to verify that assumption.
|
| 25/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/capability.c Name:capable]
Results: map[SourceCode: 404: /**
405: * capable - Determine if the current task has a superior capability in effect
406: * @cap: The capability to be tested for
407: *
408: * Return true if the current task has the given superior capability currently
409: * available for use, false if not.
410: *
411: * This sets PF_SUPERPRIV on the task if the capability is available on the
412: * assumption that it's about to be used.
413: */
414: bool capable(int cap)
415: {
416: return ns_capable(&init_user_ns, cap);
417: }
SourceFile:kernel/capability.c]
|
| 26/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 35556
output: 37
thoughts: 233
Thoughts: **Analyzing the CAP_NET_ADMIN Vulnerability**
Okay, so `capable(CAP_NET_ADMIN)` is definitely checking the `init_user_ns`. That's the starting point. This means this specific bug is *only* exploitable by a process that has `CAP_NET_ADMIN` within the initial user namespace. But hold on, am I missing something? Let's verify that there is *absolutely* no other way to reach `io_register_zcrx` without that capability.
Right, the code confirms it. The very first check in `io_register_zcrx` is the `capable(CAP_NET_ADMIN)` call. So, no, it's not directly accessible by unprivileged processes, and it's also not reachable from user namespaces because of that `init_user_ns` check. It's a solid guardrail.
Now, the bug report mentions "refcount_t: decrement hit 0; leaking memory." That's a refcount underflow. Is there any alternate path to trigger this outside of the standard flow? Let me check where `io_register_zcrx` is called from. Aha, it's called from `__io_uring_register`.
|
| 27/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/register.c Name:__io_uring_register]
Results: map[SourceCode: 750: static int __io_uring_register(struct io_ring_ctx *ctx, unsigned opcode,
751: void __user *arg, unsigned nr_args)
752: __releases(ctx->uring_lock)
753: __acquires(ctx->uring_lock)
754: {
755: int ret;
756:
757: /*
758: * We don't quiesce the refs for register anymore and so it can't be
759: * dying as we're holding a file ref here.
760: */
761: if (WARN_ON_ONCE(percpu_ref_is_dying(&ctx->refs)))
762: return -ENXIO;
763:
764: if (ctx->submitter_task && ctx->submitter_task != current)
765: return -EEXIST;
766:
767: if ((ctx->int_flags & IO_RING_F_REG_RESTRICTED) && !(ctx->flags & IORING_SETUP_R_DISABLED)) {
768: opcode = array_index_nospec(opcode, IORING_REGISTER_LAST);
769: if (!test_bit(opcode, ctx->restrictions.register_op))
770: return -EACCES;
771: }
772:
773: switch (opcode) {
774: case IORING_REGISTER_BUFFERS:
775: ret = -EFAULT;
776: if (!arg)
777: break;
778: ret = io_sqe_buffers_register(ctx, arg, nr_args, NULL);
779: break;
780: case IORING_UNREGISTER_BUFFERS:
781: ret = -EINVAL;
782: if (arg || nr_args)
783: break;
784: ret = io_sqe_buffers_unregister(ctx);
785: break;
786: case IORING_REGISTER_FILES:
787: ret = -EFAULT;
788: if (!arg)
789: break;
790: ret = io_sqe_files_register(ctx, arg, nr_args, NULL);
791: break;
792: case IORING_UNREGISTER_FILES:
793: ret = -EINVAL;
794: if (arg || nr_args)
795: break;
796: ret = io_sqe_files_unregister(ctx);
797: break;
798: case IORING_REGISTER_FILES_UPDATE:
799: ret = io_register_files_update(ctx, arg, nr_args);
800: break;
801: case IORING_REGISTER_EVENTFD:
802: ret = -EINVAL;
803: if (nr_args != 1)
804: break;
805: ret = io_eventfd_register(ctx, arg, 0);
806: break;
807: case IORING_REGISTER_EVENTFD_ASYNC:
808: ret = -EINVAL;
809: if (nr_args != 1)
810: break;
811: ret = io_eventfd_register(ctx, arg, 1);
812: break;
813: case IORING_UNREGISTER_EVENTFD:
814: ret = -EINVAL;
815: if (arg || nr_args)
816: break;
817: ret = io_eventfd_unregister(ctx);
818: break;
819: case IORING_REGISTER_PROBE:
820: ret = -EINVAL;
821: if (!arg || nr_args > 256)
822: break;
823: ret = io_probe(ctx, arg, nr_args);
824: break;
825: case IORING_REGISTER_PERSONALITY:
826: ret = -EINVAL;
827: if (arg || nr_args)
828: break;
829: ret = io_register_personality(ctx);
830: break;
831: case IORING_UNREGISTER_PERSONALITY:
832: ret = -EINVAL;
833: if (arg)
834: break;
835: ret = io_unregister_personality(ctx, nr_args);
836: break;
837: case IORING_REGISTER_ENABLE_RINGS:
838: ret = -EINVAL;
839: if (arg || nr_args)
840: break;
841: ret = io_register_enable_rings(ctx);
842: break;
843: case IORING_REGISTER_RESTRICTIONS:
844: ret = io_register_restrictions(ctx, arg, nr_args);
845: break;
846: case IORING_REGISTER_FILES2:
847: ret = io_register_rsrc(ctx, arg, nr_args, IORING_RSRC_FILE);
848: break;
849: case IORING_REGISTER_FILES_UPDATE2:
850: ret = io_register_rsrc_update(ctx, arg, nr_args,
851: IORING_RSRC_FILE);
852: break;
853: case IORING_REGISTER_BUFFERS2:
854: ret = io_register_rsrc(ctx, arg, nr_args, IORING_RSRC_BUFFER);
855: break;
856: case IORING_REGISTER_BUFFERS_UPDATE:
857: ret = io_register_rsrc_update(ctx, arg, nr_args,
858: IORING_RSRC_BUFFER);
859: break;
860: case IORING_REGISTER_IOWQ_AFF:
861: ret = -EINVAL;
862: if (!arg || !nr_args)
863: break;
864: ret = io_register_iowq_aff(ctx, arg, nr_args);
865: break;
866: case IORING_UNREGISTER_IOWQ_AFF:
867: ret = -EINVAL;
868: if (arg || nr_args)
869: break;
870: ret = io_unregister_iowq_aff(ctx);
871: break;
872: case IORING_REGISTER_IOWQ_MAX_WORKERS:
873: ret = -EINVAL;
874: if (!arg || nr_args != 2)
875: break;
876: ret = io_register_iowq_max_workers(ctx, arg);
877: break;
878: case IORING_REGISTER_RING_FDS:
879: ret = io_ringfd_register(ctx, arg, nr_args);
880: break;
881: case IORING_UNREGISTER_RING_FDS:
882: ret = io_ringfd_unregister(ctx, arg, nr_args);
883: break;
884: case IORING_REGISTER_PBUF_RING:
885: ret = -EINVAL;
886: if (!arg || nr_args != 1)
887: break;
888: ret = io_register_pbuf_ring(ctx, arg);
889: break;
890: case IORING_UNREGISTER_PBUF_RING:
891: ret = -EINVAL;
892: if (!arg || nr_args != 1)
893: break;
894: ret = io_unregister_pbuf_ring(ctx, arg);
895: break;
896: case IORING_REGISTER_SYNC_CANCEL:
897: ret = -EINVAL;
898: if (!arg || nr_args != 1)
899: break;
900: ret = io_sync_cancel(ctx, arg);
901: break;
902: case IORING_REGISTER_FILE_ALLOC_RANGE:
903: ret = -EINVAL;
904: if (!arg || nr_args)
905: break;
906: ret = io_register_file_alloc_range(ctx, arg);
907: break;
908: case IORING_REGISTER_PBUF_STATUS:
909: ret = -EINVAL;
910: if (!arg || nr_args != 1)
911: break;
912: ret = io_register_pbuf_status(ctx, arg);
913: break;
914: case IORING_REGISTER_NAPI:
915: ret = -EINVAL;
916: if (!arg || nr_args != 1)
917: break;
918: ret = io_register_napi(ctx, arg);
919: break;
920: case IORING_UNREGISTER_NAPI:
921: ret = -EINVAL;
922: if (nr_args != 1)
923: break;
924: ret = io_unregister_napi(ctx, arg);
925: break;
926: case IORING_REGISTER_CLOCK:
927: ret = -EINVAL;
928: if (!arg || nr_args)
929: break;
930: ret = io_register_clock(ctx, arg);
931: break;
932: case IORING_REGISTER_CLONE_BUFFERS:
933: ret = -EINVAL;
934: if (!arg || nr_args != 1)
935: break;
936: ret = io_register_clone_buffers(ctx, arg);
937: break;
938: case IORING_REGISTER_ZCRX_IFQ:
939: ret = -EINVAL;
940: if (!arg || nr_args != 1)
941: break;
942: ret = io_register_zcrx(ctx, arg);
943: break;
944: case IORING_REGISTER_RESIZE_RINGS:
945: ret = -EINVAL;
946: if (!arg || nr_args != 1)
947: break;
948: ret = io_register_resize_rings(ctx, arg);
949: break;
950: case IORING_REGISTER_MEM_REGION:
951: ret = -EINVAL;
952: if (!arg || nr_args != 1)
953: break;
954: ret = io_register_mem_region(ctx, arg);
955: break;
956: case IORING_REGISTER_QUERY:
957: ret = io_query(arg, nr_args);
958: break;
959: case IORING_REGISTER_ZCRX_CTRL:
960: ret = io_zcrx_ctrl(ctx, arg, nr_args);
961: break;
962: case IORING_REGISTER_BPF_FILTER:
963: ret = -EINVAL;
964:
965: if (nr_args != 1)
966: break;
967: ret = io_register_bpf_filter(&ctx->restrictions, arg);
968: if (!ret)
969: WRITE_ONCE(ctx->bpf_filters,
970: ctx->restrictions.bpf_filters->filters);
971: break;
972: default:
973: ret = -EINVAL;
974: break;
975: }
976:
977: return ret;
978: }
SourceFile:io_uring/register.c]
|
| 28/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 39557
output: 43
thoughts: 152
Thoughts: **My Analysis of the `io_register_zcrx` Function**
Okay, so I'm looking at `io_register_zcrx` and it seems pretty straightforward. I see that it's only called by `__io_uring_register` when the `IORING_REGISTER_ZCRX_IFQ` command is issued. That makes sense. And right off the bat, I spot the `capable(CAP_NET_ADMIN)` check. So, that clearly establishes that this operation *should* require `CAP_NET_ADMIN` privileges, specifically within the initial user namespace.
But, hold on a second... I need to be thorough. I should consider all possible call paths. Is this the *only* way to get here? What if `io_close_queue` is triggered from elsewhere? I know that this function is called from `zcrx_unregister_user`. Okay, let's trace that. `zcrx_unregister_user` is called from `zcrx_unregister`, and also from `io_zcrx_ifq_release`. This could introduce an unexpected path through the code. I need to examine these other function calls now.
|
| 29/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:io_uring/zcrx.c Name:io_zcrx_ifq_release]
Results: map[SourceCode: SourceFile:]
|
| 30/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 40028
output: 53
thoughts: 16
Thoughts: Okay, here's my summary, presented as a direct, internal monologue, aimed at a fellow expert:
**Investigating `zcrx_unregister_user` Callers**
Alright, time to dig in. Let's see what's calling `zcrx_unregister_user`. It's a key function, obviously, likely involved in user de-registration or cleanup, which means potential resource management implications. I need to understand the control flow here, see where this unregistration process is initiated and under what conditions. Understanding the calling contexts will illuminate its purpose, any potential race conditions, or performance bottlenecks. I need to be sure this process is done safely and efficiently. I'll need to know where I can start looking and what to expect. Let's see what the code tells us!
|
| 31/2 |
2026/08/09 04:44 |
tool |
codesearch-find-references |
0m
Args: map[ContextFile:io_uring/zcrx.c IncludeSnippetLines:0 Name:zcrx_unregister_user SourceTreePrefix:]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:io_terminate_zcrx SourceFile:io_uring/zcrx.c SourceLine:1051] map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:zcrx_unregister SourceFile:io_uring/zcrx.c SourceLine:688]] TruncatedOutput:false]
|
| 32/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 40391
output: 40
thoughts: 13
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience, presented in the first person:
**Deep Dive Check: `io_terminate_zcrx`**
Alright, time to circle back to `io_terminate_zcrx`. Given my understanding of the system architecture, I need to pinpoint the critical aspects of this function. I'm immediately thinking about its role in the I/O pipeline, especially its interaction with the ZCRX component. Is it a finalization routine, a cleanup function, or something else entirely? I need to analyze how it handles resources. Does it free buffers, close file descriptors, or signal completion to upstream processes?
I'll be looking for potential race conditions or synchronization issues. Does it utilize any locks or mutexes? Are there any obvious areas where a thread might hang or a deadlock could occur, specifically with regard to how it interacts with the ZCRX subsystem? My past experience with similar systems makes me wary of improper error handling, so I'll also check how it deals with unexpected situations, particularly within a high-throughput environment. What are the logging mechanisms in place here?
I'm also curious about its performance impact. If it's a bottleneck, that needs to be addressed ASAP. What's the typical execution time, and how does it scale under load? What kind of profiling data is available? Time to see what the debugger reveals.
|
| 33/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:io_terminate_zcrx]
Results: map[SourceCode:1035: void io_terminate_zcrx(struct io_ring_ctx *ctx)
1036: {
1037: struct io_zcrx_ifq *ifq;
1038: unsigned long id = 0;
1039:
1040: lockdep_assert_held(&ctx->uring_lock);
1041:
1042: while (1) {
1043: scoped_guard(mutex, &ctx->mmap_lock)
1044: ifq = xa_find(&ctx->zcrx_ctxs, &id, ULONG_MAX, XA_PRESENT);
1045: if (!ifq)
1046: break;
1047: if (WARN_ON_ONCE(is_zcrx_entry_marked(ctx, id)))
1048: break;
1049: set_zcrx_entry_mark(ctx, id);
1050: id++;
1051: zcrx_unregister_user(ifq, ctx);
1052: }
1053: }
SourceFile:io_uring/zcrx.c]
|
| 34/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 41068
output: 40
thoughts: 127
Thoughts: **Analyzing the Security Implications of `io_terminate_zcrx` and `ifq` Handling**
Okay, so `io_terminate_zcrx` is the function that gets called when the ring bites the dust, or something along those lines. I need to understand the connection between that and the `ifq` (Interrupt Field Queue). The critical point is that to find an `ifq` within the `ctx->zcrx_ctxs` structure, the `ifq` must have been successfully registered using `io_register_zcrx`. This looks like the main point of concern.
Now, `io_register_zcrx` has a significant hurdle β it demands `CAP_NET_ADMIN` permissions. This is great, in a way. It essentially means that without those elevated privileges, there's no way to even get an `ifq` associated with the context. That's a reasonable level of protection, limiting the potential attack surface. It's a key security measure.
However, I'm not entirely satisfied. I need to be absolutely sure. This raises a question in my mind: Are there any other ways to acquire an `ifq` that I'm overlooking? This is a point of potential vulnerability. What about `IORING_REGISTER_ZCRX_CTRL`? Is there some loophole or unexpected path there? The documentation must be checked thoroughly. I have to make sure there are no other backdoors. I need to investigate every possible way an `ifq` might be obtained.
|
| 35/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:io_zcrx_ctrl]
Results: map[SourceCode:1430: int io_zcrx_ctrl(struct io_ring_ctx *ctx, void __user *arg, unsigned nr_args)
1431: {
1432: struct zcrx_ctrl ctrl;
1433: struct io_zcrx_ifq *zcrx;
1434:
1435: BUILD_BUG_ON(sizeof(ctrl.zc_export) != sizeof(ctrl.zc_flush));
1436: BUILD_BUG_ON(sizeof(ctrl.zc_export) != sizeof(ctrl.zc_arm_event));
1437:
1438: if (nr_args)
1439: return -EINVAL;
1440: if (copy_from_user(&ctrl, arg, sizeof(ctrl)))
1441: return -EFAULT;
1442: if (!mem_is_zero(&ctrl.__resv, sizeof(ctrl.__resv)))
1443: return -EFAULT;
1444:
1445: zcrx = xa_load(&ctx->zcrx_ctxs, ctrl.zcrx_id);
1446: if (!zcrx)
1447: return -ENXIO;
1448:
1449: switch (ctrl.op) {
1450: case ZCRX_CTRL_FLUSH_RQ:
1451: return zcrx_flush_rq(ctx, zcrx, &ctrl);
1452: case ZCRX_CTRL_EXPORT:
1453: return zcrx_export(ctx, zcrx, &ctrl, arg);
1454: case ZCRX_CTRL_ARM_EVENT:
1455: return zcrx_arm_notif(ctx, zcrx, &ctrl);
1456: }
1457:
1458: return -EOPNOTSUPP;
1459: }
SourceFile:io_uring/zcrx.c]
|
| 36/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 42100
output: 38
thoughts: 121
Thoughts: **Analyzing the ZCRX Registration and Import Process**
Okay, so looking at `io_zcrx_ctrl`, it's clear that it's just doing a lookup. It's expecting a pre-existing `ifq` to be registered within `ctx->zcrx_ctxs`. That makes sense. But this means we *have* to register it first via `io_register_zcrx`, right? It's the only way it would know.
Now, wait a minute... what about `import_zcrx`? It feels like I'm missing something, since I can't find how the `ifq` is registered.
Let's look at `io_register_zcrx`. Aha! There's a check in there:
```c
if (reg.flags & ZCRX_REG_IMPORT)
return import_zcrx(ctx, arg, ®);
```
Okay, so if the `ZCRX_REG_IMPORT` flag is set in the registration, then we branch to `import_zcrx`. This seems like the key. I need to understand what `import_zcrx` does to truly complete the registration. Let's delve into that function.
|
| 37/2 |
2026/08/09 04:44 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:io_uring/zcrx.c Name:import_zcrx]
Results: map[SourceCode: 754: static int import_zcrx(struct io_ring_ctx *ctx,
755: struct io_uring_zcrx_ifq_reg __user *arg,
756: struct io_uring_zcrx_ifq_reg *reg)
757: {
758: struct io_zcrx_ifq *ifq;
759: struct file *file;
760: int fd, ret;
761: u32 id;
762:
763: if (!(ctx->flags & IORING_SETUP_DEFER_TASKRUN))
764: return -EINVAL;
765: if (!(ctx->flags & (IORING_SETUP_CQE32|IORING_SETUP_CQE_MIXED)))
766: return -EINVAL;
767: if (reg->if_rxq || reg->rq_entries || reg->area_ptr || reg->region_ptr)
768: return -EINVAL;
769: if (reg->event_desc)
770: return -EINVAL;
771: if (reg->flags & ~ZCRX_REG_IMPORT)
772: return -EINVAL;
773:
774: fd = reg->if_idx;
775: CLASS(fd, f)(fd);
776: if (fd_empty(f))
777: return -EBADF;
778:
779: file = fd_file(f);
780: if (file->f_op != &zcrx_box_fops || !file->private_data)
781: return -EBADF;
782:
783: ifq = file->private_data;
784: refcount_inc(&ifq->refs);
785: refcount_inc(&ifq->user_refs);
786:
787: scoped_guard(mutex, &ctx->mmap_lock) {
788: ret = xa_alloc(&ctx->zcrx_ctxs, &id, NULL, xa_limit_31b, GFP_KERNEL);
789: if (ret)
790: goto err;
791: }
792:
793: reg->zcrx_id = id;
794: io_fill_zcrx_offsets(®->offsets);
795: if (copy_to_user(arg, reg, sizeof(*reg))) {
796: ret = -EFAULT;
797: goto err_xa_erase;
798: }
799:
800: scoped_guard(mutex, &ctx->mmap_lock) {
801: ret = -ENOMEM;
802: if (xa_store(&ctx->zcrx_ctxs, id, ifq, GFP_KERNEL))
803: goto err_xa_erase;
804: }
805:
806: return 0;
807: err_xa_erase:
808: scoped_guard(mutex, &ctx->mmap_lock)
809: xa_erase(&ctx->zcrx_ctxs, id);
810: err:
811: /* the import path never set the ->master_ctx ref, don't drop it */
812: zcrx_unregister(ifq, NULL);
813: return ret;
814: }
SourceFile:io_uring/zcrx.c]
|
| 38/2 |
2026/08/09 04:44 |
llm |
expert |
0m
Model:
Tokens:
input: 43532
output: 54
thoughts: 389
Thoughts: **Analyzing the Vulnerability: A Deep Dive**
Okay, so I'm looking at this code and immediately I see that `import_zcrx` gets called *after* a `capable(CAP_NET_ADMIN)` check in `io_register_zcrx`. That's important. It means even importing this thing requires full-on network admin capabilities, which is a pretty high bar.
I'm thinking, "Are there any other ways to hit `zcrx_register_netdev` or `io_close_queue`?" Gotta check all the angles. Nope, looks like `zcrx_register_netdev` is solely called from `io_register_zcrx`, and `io_close_queue` is solely called from `zcrx_unregister_user`. Okay, that severely limits the scope of this. It's essentially confined to processes that *already* have `CAP_NET_ADMIN` privileges in the *initial* user namespace.
Now, my mind is racing. "Could a process with that level of privilege trigger something that leads to a Use-After-Free?" Bingo. It's a UAF of `net_device`. I see it clearly. The vulnerability is a UAF of `net_device`. But then the reality sinks in, if it requires root in the initial namespace, we're talking about a privileged escalation, not a sandbox escape. This isn't reachable from any unprivileged process, and certainly not from a user namespace.
Just to be absolutely sure, let's look at that `capable()` check again. Yep, `io_register_zcrx` is the only place it's done.
I need to confirm if I'm missing something, could a normal user somehow trigger this? So let's double check if `io_register_zcrx` is the only one calling `zcrx_register_netdev`.
|
| 39/2 |
2026/08/09 04:45 |
tool |
codesearch-find-references |
0m
Args: map[ContextFile:io_uring/zcrx.c IncludeSnippetLines:0 Name:zcrx_register_netdev SourceTreePrefix:]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:io_register_zcrx SourceFile:io_uring/zcrx.c SourceLine:987]] TruncatedOutput:false]
|
| 40/2 |
2026/08/09 04:45 |
llm |
expert |
0m
Model:
Tokens:
input: 44458
output: 51
thoughts: 26
Thoughts: Okay, here's my summarized thought process, presented as if I were having these thoughts:
**Analyzing Register Access and Queue Closure**
Right, so I'm focusing on this `io_register_zcrx` function. I need to be absolutely certain of its dependencies, its interactions. And, as far as I can tell from a quick scan, it's the *only* place this function is being called. That's a good starting point, simplifies things... but I need to be certain.
Okay, wait, before I finalize anything, what about that `io_close_queue` function? Is there any potential for interaction, any sharing of resources or context that might impact the register setup or de-registration in `io_register_zcrx`? I need to check if there's any overlap. Maybe the queue closure impacts the register setup, or vice versa. I need to fully understand if these are independent. Time to dig deeper and see.
|
| 41/2 |
2026/08/09 04:45 |
tool |
codesearch-find-references |
0m
Args: map[ContextFile:io_uring/zcrx.c IncludeSnippetLines:0 Name:io_close_queue SourceTreePrefix:]
Results: map[References:[map[ReferenceKind:calls ReferencingEntityKind:function ReferencingEntityName:zcrx_unregister_user SourceFile:io_uring/zcrx.c SourceLine:681]] TruncatedOutput:false]
|
| 42/2 |
2026/08/09 04:45 |
llm |
expert |
0m
Model:
Tokens:
input: 44813
output: 41
thoughts: 1110
Thoughts: **Analysis of the IORING_REGISTER_ZCRX_IFQ Vulnerability**
Okay, let's break this down. My initial assessment is that the `zcrx_unregister_user` function is the only one that calls this, right? That means the entire feature `IORING_REGISTER_ZCRX_IFQ` is locked behind `CAP_NET_ADMIN`. That's a significant restriction. The implications are clear: this bug is *not* accessible to unprivileged processes, and it's also blocked in user namespaces because `capable()` checks the `init_user_ns`.
Now, the obvious question: is there *any* way a standard user could trigger this? No. Given that constraint, is this a Denial of Service? Yes, definitely. The underflow in the reference counter will lead to a Use-After-Free (UAF) situation that'll crash the kernel. The user already has root privileges due to the need for `CAP_NET_ADMIN`. I'll classify it as Denial of Service and Exploitable, but I'm marking it Unprivileged and UserNamespace as false.
So, let's explore if there are any other possible attack vectors. Could a privileged process call `io_uring_register` on behalf of an unprivileged one? Could an unprivileged process create an io_uring fd and pass it off? Yes, but a privileged process would have to call `io_uring_register` with `IORING_REGISTER_ZCRX_IFQ`. Why would a privileged process do this for an unprivileged one? Unless there's a setuid binary or some specific service, I don't see it. As far as I can tell, no such services are known.
What if the unprivileged process tries to use `io_uring_setup` and then tries `IORING_REGISTER_ZCRX_IFQ` itself? That would fail with `-EPERM` because the user lacks `CAP_NET_ADMIN`.
Let me look at the reproducer:
```c
res = syscall(__NR_io_uring_setup, /*entries=*/1, /*params=*/0x200000000080ul);
if (res != -1)
r[0] = res;
syscall(__NR_io_uring_register, /*fd=*/r[0], /*opcode=*/0x20ul, /*arg=*/0x200000000100ul, /*nr_args=*/1ul);
```
The reproducer runs as root in the syzkaller VM. Because Syzkaller runs as root, it has `CAP_NET_ADMIN`.
Now, let's confirm the `io_register_zcrx` is the only place `capable(CAP_NET_ADMIN)` is checked. Yes.
Let me review the bug in detail to be absolutely certain. In `zcrx_register_netdev`:
```c
ifq->netdev = netdev_get_by_index_lock(current->nsproxy->net_ns,
reg->if_idx);
if (!ifq->netdev)
return -ENODEV;
if (!ifq->netdev->queue_mgmt_ops) {
ret = -EOPNOTSUPP;
goto netdev_put_unlock;
}
netdev_hold(ifq->netdev, &ifq->netdev_tracker, GFP_KERNEL);
```
If `!ifq->netdev->queue_mgmt_ops` is true, then it returns `-EOPNOTSUPP`. The control then goes to `err` and calls `zcrx_unregister`, which, in turn, calls `io_close_queue`.
`io_close_queue` does this:
```c
scoped_guard(mutex, &ifq->pp_lock) {
netdev = ifq->netdev;
netdev_tracker = ifq->netdev_tracker;
ifq->netdev = NULL;
}
if (netdev) {
if (ifq->if_rxq != -1) {
netdev_lock(netdev);
netif_mp_close_rxq(netdev, ifq->if_rxq, &p);
netdev_unlock(netdev);
}
netdev_put(netdev, &netdev_tracker);
}
```
Since `ifq->netdev` was set, `netdev` is not NULL. `ifq->if_rxq` is
|
| 43/2 |
2026/08/09 04:45 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:net/core/dev.c Name:netdev_get_by_index_lock]
Results: map[SourceCode:1118: /**
1119: * netdev_get_by_index_lock() - find a device by its ifindex
1120: * @net: the applicable net namespace
1121: * @ifindex: index of device
1122: *
1123: * Search for an interface by index. If a valid device
1124: * with @ifindex is found it will be returned with netdev->lock held.
1125: * netdev_unlock() must be called to release it.
1126: *
1127: * Return: pointer to a device with lock held, NULL if not found.
1128: */
1129: struct net_device *netdev_get_by_index_lock(struct net *net, int ifindex)
1130: {
1131: struct net_device *dev;
1132:
1133: dev = dev_get_by_index(net, ifindex);
1134: if (!dev)
1135: return NULL;
1136:
1137: return __netdev_put_lock(dev, net);
1138: }
SourceFile:net/core/dev.c]
|
| 44/2 |
2026/08/09 04:45 |
llm |
expert |
0m
Model:
Tokens:
input: 47341
output: 38
thoughts: 10
Thoughts: Okay, here's my interpretation of that request, formatted as requested:
**Navigating the `__netdev_put_lock` Function**
Alright, let's break this down. My mind immediately jumps to `__netdev_put_lock`. The name itself suggests a function related to networking devices and, importantly, the release of some kind of locking mechanism. This triggers a cascade of associated thoughts:
* **Locking Context:** I'm guessing this function is deeply intertwined with the networking stack, likely managing the lifecycle of network devices and ensuring thread safety. It's probably called when a network device is being de-initialized, disabled, or released.
* **Purpose:** The 'put' in the name indicates the function likely *releases* a lock. This release could be a mutex, spinlock, read/write lock, or even a more specialized concurrency primitive that protects the internal data structures of a network device. I'm already formulating hypotheses about the critical sections this lock would protect β probably data relating to device status, packet queues, transmit/receive buffers, configuration parameters, and link states. The lock protects against concurrent access to shared resources.
* **Implications:** When this lock is released, I'm thinking about the potential race conditions that need careful handling. The function *must* be correctly synchronized. Any failure could lead to deadlocks, data corruption, or crashes. Iβm picturing the classic scenarios: one thread accessing device data, another shutting down the device, and the need to serialize the access to avoid undefined behavior.
* **Implementation Depth:** Now I'm mentally parsing the likely structure of the function. It's likely involved in updating a reference count, deallocating resources, or signaling other subsystems of the kernel. Iβm expecting a series of conditional checks, lock release operations, and potentially some device-specific cleanup routines. I would start trying to form a mental model of the control flow through the function in different scenarios, especially around error conditions and potential lock contention. Iβm considering how it would interact with other related functions such as `__netdev_get_lock` or other netdevice operations that use locks.
* **Broader Picture:** I'm zooming out a bit and considering the larger context within the network stack. I'm placing this function within the framework of device driver operations, network interface management, and the overall network resource management of the Linux kernel. I'm considering how this might relate to network namespaces, virtual devices, and other kernel networking modules.
Essentially, my mental process is about tracing the control flow, identifying potential race conditions, and understanding how it safeguards network device data integrity!
|
| 45/2 |
2026/08/09 04:45 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:net/core/dev.h Name:__netdev_put_lock]
Results: map[SourceCode: 37: static inline struct net_device *
38: __netdev_put_lock(struct net_device *dev, struct net *net)
39: {
40: return netdev_put_lock(dev, net, NULL);
41: }
SourceFile:net/core/dev.h]
|
| 46/2 |
2026/08/09 04:45 |
llm |
expert |
0m
Model:
Tokens:
input: 48050
output: 37
thoughts: 9
Thoughts: Okay, here's the summary, expressed as your thoughts, as an expert in the field:
**Reflecting on `netdev_put_lock`**
Right, so I'm thinking about `netdev_put_lock`. It's a key primitive, and it immediately makes me consider the implications for concurrency and resource management within the network device driver model. What's the context here? We're releasing a lock associated with a network device. This suggests a critical section has completed β likely related to device initialization, de-initialization, or perhaps a more granular operation on the device itself, like a per-queue operation. The name itself, `put`, implies releasing or relinquishing ownership; it's the counterpart to the acquisition, the *getting* of the lock, which is likely a separate function.
Now, I start to dissect what this means at a deeper level. First, what kind of lock are we talking about? Is it a spinlock, a mutex, a read-write lock? The choice of lock type has huge performance ramifications, influencing latency, contention, and the overall system throughput, all things I spend my time thinking about. Spinlocks are appropriate for short critical sections where the lock is expected to be held for a very brief period, avoiding the overhead of going to sleep/waking up. Mutexes are better for longer critical sections, preventing the possibility of the CPU spinning unnecessarily, but at the cost of the context switch involved. Read-write locks are a third option that is incredibly useful when you have a lot of read operations vs a few write operations.
Then there's the question of *where* this function is called. Is it called from interrupt context? If so, this immediately limits the types of locks that can be safely used. Spinlocks become the most likely choice because of the limitations of the kernel. If it's a softirq context, same considerations apply, but perhaps with slightly more leeway depending on the specific implementation. If it's in process context, more options open up, but then we start worrying about potential deadlocks if this function interacts with other locks or functions. The specific device driver structure also factors in. Are we dealing with a single global device lock, or per-device, or even per-queue locks? Per-queue locks offer more fine-grained concurrency, but increase complexity. A global device lock is simpler to reason about but can become a bottleneck.
And finally, of course, the implications for debugging and reliability. The correct use of these primitives is *essential* for a stable system. A subtle race condition, introduced due to improper lock usage, can lead to data corruption, kernel panics, or performance degradation. A well-written kernel module needs to be thoroughly audited and debugged to check that all locking primitives are handled correctly, that the locks are used consistently, and that the locking order is always maintained to prevent deadlocks. That's what I am thinking about now, as I reflect on this function.
|
| 47/2 |
2026/08/09 04:45 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:net/core/dev.c Name:netdev_put_lock]
Results: map[SourceCode:1081: /* Release the held reference on the net_device, and if the net_device
1082: * is still registered try to lock the instance lock. If device is being
1083: * unregistered NULL will be returned (but the reference has been released,
1084: * either way!)
1085: *
1086: * This helper is intended for locking net_device after it has been looked up
1087: * using a lockless lookup helper. Lock prevents the instance from going away.
1088: */
1089: struct net_device *
1090: netdev_put_lock(struct net_device *dev, struct net *net,
1091: netdevice_tracker *tracker)
1092: {
1093: netdev_lock(dev);
1094: if (dev->reg_state > NETREG_REGISTERED ||
1095: dev->moving_ns || !net_eq(dev_net(dev), net)) {
1096: netdev_unlock(dev);
1097: netdev_put(dev, tracker);
1098: return NULL;
1099: }
1100: netdev_put(dev, tracker);
1101: return dev;
1102: }
SourceFile:net/core/dev.c]
|
| 48/2 |
2026/08/09 04:45 |
llm |
expert |
0m
Model:
Tokens:
input: 49099
output: 66
thoughts: 957
Thoughts: **Analysis of a Refcount Underflow Bug in io_close_queue**
Okay, let's break this down. It seems there's a reference counting issue lurking in the interaction between `io_close_queue`, `zcrx_register_netdev`, and the `net_device` structure. My initial thought is, `netdev_put_lock` *does* call `netdev_put` which releases the reference taken by `dev_get_by_index`, right before returning the device. So, `netdev_get_by_index_lock` hands off a `net_device` with the lock held, but crucially, no reference is *held* by the caller.
The `zcrx_register_netdev` function, knowing this, tries to remedy the situation using `netdev_hold`. It's essential it does, given the reference passed over, and the assumption a reference is required. However, if that `netdev_hold` fails *before* it's even called, and the check for `ifq->netdev->queue_mgmt_ops` fails, `ifq->netdev` is already set, and the subsequent `io_close_queue` call (through the error path) will call `netdev_put` on it. And that's where the problem arises: `netdev_put` is being called on a `net_device` where a corresponding `netdev_hold` never happened, hence the underflow.
This is a clear refcount underflow on the `net_device` β a textbook UAF. Exploitable, for sure. The UAF of `net_device` can lead to privilege escalation and system crashes; there's no doubt about that.
Now, the critical question: Is this thing reachable? `io_register_zcrx`, the function where the vulnerability lies, requires `CAP_NET_ADMIN` in the initial user namespace. This makes it unavailable to unprivileged processes and user namespaces, via the `capable(CAP_NET_ADMIN)` check. Only privileged users within the initial namespace, aka root, can trigger it.
Let's do a quick scan to make sure there are no alternate paths to `zcrx_register_netdev`. I'm pretty sure `io_register_zcrx` is the *only* caller of `zcrx_register_netdev`, so there's no other way to reach it. And we know that `io_close_queue` is called by `zcrx_unregister_user`, which in turn is called by `zcrx_unregister` and `io_terminate_zcrx` β both of which require `ifq` created by `io_register_zcrx`. This confirms the lack of any way around the permission check.
So, to be clear: the bug *is* a UAF due to the refcount underflow, and it *is* exploitable if triggered. However, due to the `CAP_NET_ADMIN` check, unprivileged users and those in user namespaces can't trigger it.
Here's the detailed logic: In `zcrx_register_netdev`, `ifq->netdev` is set to the result of `netdev_get_by_index_lock`. The reference count isn't held! A subsequent check of `!ifq->netdev->queue_mgmt_ops` (if it fails) triggers the error path *before* `netdev_hold` can happen. When an error is handled the code calls `io_close_queue`, which calls `netdev_put(ifq->netdev)`. This releases a reference that was never taken, leading to the underflow, UAF, and potential kernel compromise. This, however, requires `CAP_NET_ADMIN`, ruling out any unprivileged triggers.
|
| 49/2 |
2026/08/09 04:45 |
tool |
set-results |
0m
Args: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:true Exploitable:true FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:false UserNamespace:false VMGuestTrigger:false VMHostTrigger:false]
|
| 50/2 |
2026/08/09 04:45 |
llm |
expert |
0m
Model:
Tokens:
input: 51014
output: 810
thoughts: 13
Thoughts: Here's your summary:
**Finalizing the Response**
Okay, the analysis is done. All the data, the nuances, the potential pitfalls β I've processed it all. I've considered the implications and the likely counter-arguments. Now, it's time to craft the final response. Itβs time to synthesize everything and present a coherent, informed conclusion. I need to ensure it's accurate, concise, and effectively communicates the core findings. Let's get this right.
|
| 51/1 |
2026/08/09 04:45 |
action |
format-explanation |
0m
Results: map[Explanation:**Bug Analysis:**
The bug is a refcount underflow on a `net_device` structure, which occurs in the
error handling path of `io_register_zcrx` in the io_uring subsystem.
When `io_register_zcrx` is called to register a zero-copy Rx interface queue
(`IORING_REGISTER_ZCRX_IFQ`), it eventually calls `zcrx_register_netdev`. Inside
`zcrx_register_netdev`, the code fetches the network device using
`netdev_get_by_index_lock()`:
```c
ifq->netdev = netdev_get_by_index_lock(current->nsproxy->net_ns,
reg->if_idx);
if (!ifq->netdev)
return -ENODEV;
if (!ifq->netdev->queue_mgmt_ops) {
ret = -EOPNOTSUPP;
goto netdev_put_unlock;
}
netdev_hold(ifq->netdev, &ifq->netdev_tracker, GFP_KERNEL);
```
The function `netdev_get_by_index_lock()` returns the `net_device` with its
instance lock held, but it **does not** hold a reference to the device (the
reference taken during the lookup is released before returning). The reference
is supposed to be explicitly taken later by calling `netdev_hold()`.
However, if the network device does not support queue management operations
(`!ifq->netdev->queue_mgmt_ops`), the function jumps to the error path
(`netdev_put_unlock`) and returns `-EOPNOTSUPP`. At this point, `ifq->netdev` is
already set, but `netdev_hold()` was never called.
The error propagates back to `io_register_zcrx`, which then calls
`zcrx_unregister()` -> `zcrx_unregister_user()` -> `io_close_queue()` to clean
up. `io_close_queue()` checks if `ifq->netdev` is not NULL, and if so, it
unconditionally calls `netdev_put()`:
```c
if (netdev) {
if (ifq->if_rxq != -1) {
// ...
}
netdev_put(netdev, &netdev_tracker);
}
```
Because a reference was never actually taken, this `netdev_put()` call drops a
non-existent reference, causing a refcount underflow (as seen in the
`refcount_t: decrement hit 0` warning).
**Security Impact:**
A refcount underflow on a `net_device` will eventually lead to the premature
freeing of the device, resulting in a Use-After-Free (UAF). UAF on a
`net_device` structure is a powerful primitive that is typically exploitable for
privilege escalation and can easily cause a Denial of Service (kernel crash).
However, the entry point to this vulnerable code path (`io_register_zcrx`) has a
strict capability check at the very beginning:
```c
if (!capable(CAP_NET_ADMIN))
return -EPERM;
```
The `capable()` function checks for the capability in the **initial user
namespace** (`init_user_ns`). This means that the bug cannot be triggered by
unprivileged users, nor can it be triggered from within a user namespace (where
a user might have `CAP_NET_ADMIN` but only relative to that specific namespace).
An attacker must already possess `CAP_NET_ADMIN` in the host's initial namespace
(effectively root) to reach the vulnerable code.]
|