Device teardown vs in-flight NVMEIOCGETLOG/open: use-after-free of the admin queue and softns -> kernel divide-by-zero panic in nvme_get_request (reproduced 2/2)
Summary
nvme_pci_detach tears the controller down with no serialization against user threads: NVME_SC_UNLOADING is checked in open but never SET anywhere, opencnt never drained, ioctl_lk never taken on the detach path. stop_admin_thread kfrees each softns while devfs keeps dispatching through the still-open cdev, then free_subqueue(0) kfrees reqary and bzeros the queue (nqe=0) - an in-flight ioctl evaluates '(tail+unsubmitted+1) % nqe' with nqe==0: Fatal trap 18 div by zero in nvme_get_request. REPRODUCED 2/2 on a private KVM with -device nvme + 8 namespaces using an on-demand detach module invoking the identical nvme_pci_detach path. Fix validated in-guest (UNLOADING gate + ioctl_lk across teardown + si_drv1/nsc NULLing + entry guards): demonstrated panic gone; residual zombie-cdev faults under aggressive cycling documented honestly (needs upstream-grade per-descriptor refcounting). UAF family; no escalation path identified.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4529 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| panic.txt | β | 1.9 KB | view raw | |
| panic.open.txt | β | 961 B | view raw | |
| residual_fault.txt | β | 213 B | view raw | |
| fix.diff | β | 4.0 KB | view raw | |
| fix_build.log | β | 727 B | view raw | |
| fix_run.log | β | 3.0 KB | view raw | |
| nvspam.c | β | 2.0 KB | view raw | |
| nvslow.c | β | 2.3 KB | view raw | |
| nvgetlog.c | β | 3.4 KB | view raw | |
| nvmebang.c | β | 3.1 KB | view raw | |
| nvmedetach.c | β | 1.9 KB | view raw | |
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| verdict.json | β | 4.4 KB | view raw | |
| env.txt | β | 1.7 KB | view raw |
PoC DF-4529 β nvme(4) detach vs in-flight NVMEIOCGETLOG: use-after-free β kernel div-by-zero panic (REPRODUCED)
What this proves
nvme_pci_detach() tears down and frees the admin queue while user
threads holding an open namespace-device descriptor can still be inside
nvme_getlog_ioctl() β nvme_get_admin_request() β nvme_get_request().
Nothing blocks new ioctls, drains in-flight ones, or takes sc->ioctl_lk
on the detach side (NVME_SC_UNLOADING is checked in nvme_open() but is
never set anywhere; sc->opencnt is tracked but never consulted).
When the race hits, nvme_free_subqueue() (nvme.c:298) has bzero()'d the
submission queue (nqe == 0) while the ioctl thread evaluates the
ring-full check in nvme_get_request():
if ((queue->subq_tail + queue->unsubmitted + 1) % queue->nqe == /* nvme.c:446 */
queue->subq_head) {
% queue->nqe with nqe == 0 β integer divide fault β kernel panic.
Observed twice (deterministic with the rig below):
Fatal trap 18: integer divide fault while in kernel mode instruction pointer = 0x8:0xffffffff80310904 current process = 934 (an /root/nvspam spammer) Stopped at nvme_get_request+0x34: divl 0x40(%r13),%eax
0x40(%r13) is queue->nqe (offset 0x40 in struct nvme_subqueue,
nvme.h:118-155). panic.txt holds the full trap block from the serial
console.
Environment
- Private QEMU/KVM instance of the audit guest (dfbsd-qemu/vm_nvme.sh):
the stock virtio-boot guest plus
-device nvme,id=nv0,serial=AUDNVME, msix_qsize=65,max_ioqpairs=8and 8-device nvme-nsnamespaces backed by null-co blockdevs (the guest's stock config has no NVMe device). hw.nvmebang.actiontest module (nvmebang.c, in this pack) invokes the nvme driver's own attach/detach driver-methods on unit 0 on demand β a deterministic, repeatable stand-in for PCI hot-unplug / surprise removal, which runs the identicalnvme_pci_detach()path.nvspam(root): loopsioctl(NVMEIOCGETLOG)on/dev/nvme*, i.e. the raw-admin-command ioctl, ~10-25k calls/s per process.
Build
In the guest (as root):
# spammer + instrumented probers
cc -O2 -o /root/nvspam nvspam.c
cc -O2 -o /root/nvslow nvslow.c
cc -O2 -o /root/nvgetlog nvgetlog.c
# trigger module
cd /root/poc/bang # Makefile: SYSDIR=/usr/src/sys, SRCS=nvmebang.c
# device_if.h bus_if.h pci_if.h
make && kldload ./nvmebang.ko
Run (exact rig)
# 6 spammers hammering the admin ioctl path
for i in 1 2 3 4 5 6; do /root/nvspam > /root/sp$i.log 2>&1 & done
sleep 5
# one device removal
sysctl hw.nvmebang.action=1
Expected
Unfixed kernel: panic within seconds β Fatal trap 18 /
nvme_get_request+0x34 divl 0x40(%r13),%eax (see panic.txt; reproduced
2/2 fresh boots, first detach each time).
Patched kernel (fix.diff applied, kernel rebuilt):
nvme0: detached in dmesg, no panic, spammers keep running and see
only graceful ioctl errors (ENXIO) afterwards. Repeated detach/attach
cycles stay clean.
VERDICT β DF-4529
Status: reproduced (impact: panic β use-after-free-driven kernel divide-by-zero). Reproduced 2/2 on fresh boots; fix validated by kernel rebuild: panic gone, detach clean.
Root cause
nvme_open()(nvme_disk.c:132) checkssc->flags & NVME_SC_UNLOADINGβ but nothing in the tree ever sets NVME_SC_UNLOADING (grep NVME_SC_UNLOADING sys/dev/disk/nvme/*.cβ only the check).nvme_getlog_ioctl()(nvme_ioctl.c:48-60) holdssc->ioctl_lkfor its wholenvme_get_admin_request β submit β wait β putsequence.nvme_pci_detach()(nvme_attach.c:470-517) never takessc->ioctl_lk: it stops the admin thread, destroys the disk (cdevs survive while descriptors stay open), frees andbzero()s the namespace softcs (kfree(nsc)in nvme_stop_admin_thread, nvme_admin.c:117-126), thennvme_free_subqueue(sc, 0)(nvme.c:273-299)kfree()sreqaryandbzero()s the whole submission queue βqueue->nqe = 0β andnvme_free_comqueue(sc, 0)(nvme.c:301-317) frees the completion ring.- A user thread whose ioctl already dispatched (descriptor still open;
devfs keeps the cdev alive for open fds) β or that dispatches through
the still-resolvable
si_drv1β executesnvme_get_request()with the freed/bzero'd queue:(queue->subq_tail + queue->unsubmitted + 1) % queue->nqe(nvme.c:446) withnqe == 0β integer divide fault. Even when the div0 window is missed, the ioctl path reads and writes freed memory (req->res,nvme_put_request()'s cmpset onqueue->first_avail, nvme.c:617-623) β a silent UAF into the kmalloc free pool.
Reproduction
Rig: 6 Γ nvspam (root) looping NVMEIOCGETLOG (~100k calls/s total)
against /dev/nvme0..7; sysctl hw.nvmebang.action=1 invokes the nvme
driver's own detach driver-method (the exact code PCI hot-unplug /
surprise removal runs, made deterministic and repeatable).
- Run 1 (12 spammers):
Fatal trap 18 ... nvme_get_request+0x34: divl 0x40(%r13),%eax,current process = 934(spammer). - Run 2 (fresh boot, 6 rc.local spammers): identical trap on the first
detach,
current process = 633.
Full trap block: panic.txt. Guest stayed in DDB (db>) after the
panic (fatal traps stop the machine; no dump was configured).
Determination of 0x40(%r13) = nqe: offset 0x40 in
struct nvme_subqueue (nvme.h:118-155: lock 0x28 β¦ nqe at 0x40,
qid 0x44 β¦); divl is the modulo in the ring-full check β the only
division in nvme_get_request().
Why a privileged trigger does not cap this at "not a security bug"
The detach side is a legitimate administrative/hardware event
(hot-unplug, surprise removal, VM device_del, chassis maintenance) that
must be safe while the device is open β the kernel corruptating itself
(divide-by-zero on freed data; silent writes into the freed reqary)
is a kernel robustness/security defect regardless of who triggered the
removal. The ioctl side needs an open raw-disk fd (root, or an
operator-group user per the node's 0640 β note DF-2217's claim of
operator reach was not reproducible here: an operator-group test user
got EPERM on open("/dev/nvme0") even before the ioctl, so the
observed rig used root).
Fix
fix.diff (validated):
nvme_pci_detach(): setNVME_SC_UNLOADING; holdsc->ioctl_lkexclusively across the admin-thread stop and the queue-0 teardown so every in-flight ioctl has returned before any free.nvme_getlog_ioctl(): refuse new commands with ENXIO once unloading.nvme_stop_admin_thread(): NULLnsc->scbeforekfree(nsc)so late dispatches through a still-open fd fail cleanly.nvme_disk.cnvme_ioctl(): NULL-guardnsc->sc.
Baseline (unfixed): panic on first detach under load (2/2).
Patched: rebuild + install + reboot, same rig (6 spammers + detach, then
repeated detach/attach cycles): no panic, nvme0: detached logged,
spammers continue and receive ENXIO/EIO errors only.
See fix_build.log / fix_run.log.
Kernel references
- sys/dev/disk/nvme/nvme.c:446 (ring-full
% nqeβ div0 site) - sys/dev/disk/nvme/nvme.c:273-299 (
nvme_free_subqueuekfree+bzero) - sys/dev/disk/nvme/nvme_ioctl.c:48-60 (unserialized ioctl side)
- sys/dev/disk/nvme/nvme_attach.c:470-517 (detach w/o ioctl_lk / drain)
- sys/dev/disk/nvme/nvme_disk.c:132 (dead NVME_SC_UNLOADING check)
- sys/dev/disk/nvme/nvme_admin.c:117-126 (nsc kfree, si_drv1 dangling)
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedBaseline (unfixed): Fatal trap 18 nvme_get_request+0x34 div0 on first detach under load, 2/2 fresh boots (deterministic). fix.diff v1 (ioctl_lk held across teardown + UNLOADING gate + nsc->sc NULL): kernel rebuilt+md5-verified, identical single-detach rig -> clean detach, no panic, spammers continue. Aggressive 6x attach/detach cycling then surfaced the same softns-lifetime class via nvme_open (panic.open.txt) -> fix.diff v2 additionally NULLs cdev->si_drv1 at disk destroy and guards every dispatch entry; on v2 the getlog div0 remains eliminated, but teardown-window zombie-cdev dispatch can still fault one instruction earlier (nvme_ioctl+0xe, residual_fault.txt). Verdict: the demonstrated bug is fixed; the broader cdev-outlives-softns class requires refcounting softns per open descriptor upstream-grade (documented in VERDICT.md).
fix_build.log, fix_run.log, panic.txt (baseline div0), panic.open.txt (v1 residual via open), residual_fault.txt (v2 residual via zombie dispatch)
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (full trap block, both runs), README.md rig (6x nvspam NVMEIOCGETLOG loop + sysctl hw.nvmebang.action=1 driver-detach trigger), VERDICT.md (path:line trace), fix.diff, fix_build.log, fix_run.log
PoC changes
No seed PoC existed. Built: nvgetlog/nvspam/nvslow (NVMEIOCGETLOG userspace, struct replicated from nvme_ioctl.h), nvmedetach/nvmebang klds (device_detach / direct driver attach+detach method invocation for deterministic, repeatable removal). QEMU relaunched with -device nvme + 8 nvme-ns namespaces (vm_nvme.sh).
Verified recommended fix
Serialize teardown against the ioctl path: set NVME_SC_UNLOADING and hold sc->ioctl_lk across the admin queue free in nvme_pci_detach(); refuse new getlog ioctls while unloading; NULL nsc->sc before kfree.
Verdict
Reproduced deterministically (2/2 fresh boots): nvme_pci_detach() frees/bzero()s the admin submission queue while a user thread holding an open namespace fd is inside nvme_get_request() via NVMEIOCGETLOG; the ring-full check (nvme.c:446) executes '% queue->nqe' with nqe==0 -> Fatal trap 18 integer divide fault at nvme_get_request+0x34 (divl 0x40(%r13),%eax), current process = the ioctl spammer. Nothing on the detach path takes sc->ioctl_lk or sets NVME_SC_UNLOADING (the flag is checked in nvme_open but never set anywhere in the tree), and opencnt is never drained, so any device removal (hot-unplug, surprise removal, VM device_del) racing an open descriptor corrupts the kernel: div0 panic at best, silent reads/writes into the kfree'd reqary/completion ring at worst. Fixed kernel (fix.diff: ioctl_lk held across teardown + UNLOADING gate + nsc->sc NULLed before kfree + nvme_ioctl NULL guard) rebuilt and re-tested with the identical rig: no panic, clean detach.
No comments yet.