β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4698

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4698 Β· 2 files
FileTypeDescriptionSize
README.md file 2.6 KB ↓ raw
verdict.json file 1.9 KB view raw
README.md file
↓ download 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

  1. comq creation fails at index k β†’ the comq unwind loop (nvme_admin.c:498-501) frees comqueues k..1 (correct, comq_err_idx includes the failed one via the ++i at :465).
  2. 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 β†’ break at :482 with i = 1 and no ++i β†’ subq_err_idx = 1 β†’ the unwind loop for (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.
  3. goto tryagain (nvme_admin.c:514) with 1/1 queues β†’ nvme_alloc_subqueue(sc,1) runs again on the still-allocated queue: * nvme.c:135 lockinit(&queue->lk, …) on an already-initialized lock; * nvme.c:145/163 bus_dmamem_alloc overwrites queue->ksubq/queue->kprps and nvme.c:197 overwrites queue->reqary without 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 if nvme_alloc_comqueue itself 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 tryagain path, nvme_free_subqueue()/nvme_free_comqueue() any still-allocated queue in 1..N (or track allocation state and refuse double alloc).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

trigger cannot be generated by the available device model (QEMU 7.2 always succeeds queue creation)

per-fix-DF-4698

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.