nvme(4): queue-creation retry (tryagain) re-allocates still-allocated subqueues β lockinit on a live lock and permanent leak of the previous queue's DMA maps + request array
Summary
Asymmetric unwind between the comq and subq loops leaves subqueues[1] allocated-but-uncreated; the 1/1 tryagain then re-allocates over it: lockinit on the live lock and overwrites of ksubq/kprps/reqary without freeing - a full queue's DMA buffers + request array leak per occurrence across reset cycles. Device-that-fails-then-succeeds path. Track-allocation-state fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4698 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 2.6 KB | β raw | |
| verdict.json | file | 1.9 KB | view raw |
DF-4698 β nvme(4): queue-creation retry path re-allocates live subqueues/comqueues β double lockinit + permanent DMA/request-pool leak
Where
sys/dev/disk/nvme/nvme_admin.c nvme_admin_state_make_queues() β error unwind asymmetry
between the completion-queue and submission-queue loops (lines 453β515) interacting with
nvme_alloc_subqueue/nvme_free_subqueue (sys/dev/disk/nvme/nvme.c:126β299).
Trace
- comq creation fails at index k β the comq unwind loop (nvme_admin.c:498-501) frees
comqueues k..1 (correct,
comq_err_idxincludes the failed one via the++iat :465). - The subq loop still runs (nvme_admin.c:471-485):
nvme_alloc_subqueue(sc,1)succeeds (pure memory op),nvme_create_subqueue(sc,1)fails because its completion queue no longer exists βbreakat :482 withi = 1and no++iβsubq_err_idx = 1β the unwind loopfor (i = subq_err_idx - 1; i >= 1; --i)(nvme_admin.c:494-497) runs zero iterations β subqueues[1] stays allocated (reqary kmalloc + sque/prps/adm DMA maps) but uncreated. goto tryagain(nvme_admin.c:514) with 1/1 queues βnvme_alloc_subqueue(sc,1)runs again on the still-allocated queue: *nvme.c:135lockinit(&queue->lk, β¦)on an already-initialized lock; *nvme.c:145/163bus_dmamem_allocoverwritesqueue->ksubq/queue->kprpsandnvme.c:197overwritesqueue->reqarywithout freeing the previous allocations β one full set of queue DMA buffers + request array leaks permanently (the old pointers are gone; teardown frees only the current ones). The identical asymmetry exists for the comq loop ifnvme_alloc_comqueueitself fails after a prior successful retry-less attempt.
Threat
Requires a controller that fails an I/O-queue creation and then succeeds on the 1/1 retry ("device lied to us" path, nvme_admin.c:503-514) β firmware malfunction or a malicious device. Impact: permanent kernel/DMA memory leak (repeatable on every controller reset cycle) and re-initialization of a live lock. No direct memory-safety violation on the leaked objects (they are simply lost).
PoC
Not runnable on the audit guest: stock QEMU 7.2 always succeeds queue creation, and no
mechanism exists to make it fail selectively. untested (analysis-certain).
Fix sketch
- unwind the subq loop with the same
++i-inclusion convention as the comq loop, and - before re-allocating a queue in the
tryagainpath,nvme_free_subqueue()/nvme_free_comqueue()any still-allocated queue in 1..N (or track allocation state and refuse double alloc).
Fix verification
not_testabletrigger cannot be generated by the available device model (QEMU 7.2 always succeeds queue creation)
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4698/README.md (line-accurate trace); nvme_admin.c:453-515, nvme.c:126-145, nvme.c:197-228
PoC changes
no seed; analysis-only pack
Verified recommended fix
Free any still-allocated queues before re-allocating in the tryagain path and make the subq unwind loop include the failed queue index.
Verdict
Analysis-certain error-path defect, not drivable on the available emulator: nvme_admin_state_make_queues() frees failed completion queues including the partially-created one (subq loop uses no matching ++i), so after a create failure + 1/1 retry the still-allocated subqueues[1] is re-alloced: lockinit on a live lock and the previous ksubq/kprps/reqary DMA allocations are overwritten without free -> permanent leak per occurrence. Trigger requires a controller that fails a queue-create then succeeds on retry; QEMU 7.2 never fails queue creation, so PoC left untested by design.
No comments yet.