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

Admin submission queue lock-protocol violation: every admin-queue caller submits without queue->lk, racing subq_tail/unsubmitted/ksubq[] against the admin thread

Summary

nvme_submit_request documents 'Caller must hold the queue lock' and mutates shared ring state unlocked; the I/O path honors it but ALL admin submitters violate it (getlog ioctl, create/delete helpers, every admin-thread identify flow). unsubmitted is incremented under the lock but decremented unlocked. A user ioctl overlapping the attach/detach admin storm double-writes a ksubq slot: lost command -> unkillable non-PCATCH lksleep hang holding comq->lk, or corrupted accounting -> get_request NULL deref. Code-certain; race not dynamically isolated (DF-4529's teardown panic fires first on the same rig). Lock-taking-admin-wrapper fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4528 Β· 12 files
FileTypeDescriptionSize
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
build.sh β€” 146 B view raw
run.sh β€” 208 B view raw
run.log β€” 2.5 KB view raw
fix.diff β€” 4.3 KB view raw
README.md β€” 3.7 KB ↓ raw
verdict.json β€” 3.2 KB view raw
env.txt β€” 1.7 KB view raw

PoC DF-4528 β€” nvme(4) admin submission-queue lock-protocol violation

What this is

nvme_submit_request() documents its contract as "Caller must hold the queue lock" (nvme.c:509-513) and mutates shared submission-ring state without any internal lock:

cmd = &queue->ksubq[queue->subq_tail];       /* nvme.c:520 */
--queue->unsubmitted;                        /* nvme.c:521 */
if (++queue->subq_tail == queue->nqe) ...    /* nvme.c:522 */

The I/O path honors the contract (nvme_strategy_core() takes subq->lk around the submit, nvme_disk.c:336-338), but every admin-queue submitter violates it β€” nvme_getlog_ioctl() (nvme_ioctl.c:56), nvme_create_subqueue() (nvme.c:800), nvme_create_comqueue() (nvme.c:843), nvme_delete_subqueue() (nvme.c:877), nvme_delete_comqueue() (nvme.c:902) and all the admin thread's identify flows (nvme_admin.c:233, 319, 534, 598, 629) call nvme_submit_request() with no lock held. queue->unsubmitted is incremented under queue->lk in nvme_get_request() (nvme.c:480) but decremented unlocked here, so concurrent submitters corrupt the ring accounting, can double-write ksubq[subq_tail] (lost commands) and can drive nvme_get_request() to its NULL path (KKASSERT is a no-op on production, non-INVARIANTS kernels β€” sys/systm.h:116-118) or into the ring-full check with a corrupted counter.

Two concurrent submitters exist whenever the admin thread is issuing its attach/detach command sequence (identify controller, SET FEATURES number of queues, identify namespace Γ— N, create/delete queue Γ— N) while a user holds an open namespace fd and issues NVMEIOCGETLOG β€” e.g. hot-plug / rescan of a second... on the SAME controller: attach storm right after the namespace device appears, or the delete-queue storm during removal.

What was verified on the guest

  • The admin ioctl path itself is live and was stress-tested clean: 1 Γ— 60 s solo: 783,730 ioctls, 0 errors, max latency 6 ms; 6 Γ— 90 s concurrent: ~1.35 M ioctls, 0 errors, max latency 0.82 s (ioctl_lk serializes the getlog submitters among themselves β€” see run.log). The race needs the admin thread as the second submitter.
  • The deterministic trigger environment was built (nvmebang test module: on-demand driver attach/detach method invocation β†’ repeated admin command storms); during these runs the teardown-race panic of DF-4529 (same files, same rig) fires first β€” the detach-side defect masks the submit-side race before attach-storm data could be collected.
  • The identical locking violation also underlies the DF-4529 teardown race, which was reproduced to a panic on this rig.

Verdict: not_reproduced (dynamically), code-certain (statically) β€” the contract violation and its state mutations are unambiguous in the source; a clean dynamic demonstration requires the DF-4529 fix (or instrumented kernels) so the teardown panic does not preempt it.

Build / run

Same environment as DF-4529 (see ../DF-4529/README.md). The load tools:

cc -O2 -o /root/nvspam nvspam.c     # glob /dev/nvme*, loop NVMEIOCGETLOG
cc -O2 -o /root/nvslow nvslow.c     # per-call latency + errno stats
cc -O2 -o /root/nvgetlog nvgetlog.c # single-call sanity check
/root/nvgetlog once /dev/nvme0                    # sanity
/root/nvslow /dev/nvme0 60 0.1                    # solo load
for i in 1 2 3 4 5 6; do /root/nvslow /dev/nvme0 90 0.2 >/root/slow$i.log 2>&1 & done
# attach-storm window (needs DF-4529 fix in place to observe cleanly):
sysctl hw.nvmebang.action=2   # re-runs identify/make-queues admin storm

Fix

see fix.diff β€” admin submissions take the queue lock (new nvme_submit_admin_request() wrapper; all qid-0 submitters converted).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff (nvme_submit_admin_request() wrapper taking queue->lk; all qid-0 submitters converted) authored against the read-only tree; not kernel-tested because the race itself was not dynamically isolated (DF-4529 masking). Compiles against the same headers used by the validated DF-4529 fix build.

fix.diff
↓ fix.diffper-fix-DF-4528

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (contract citations), run.log (1.35M clean ioctls + rig), ../DF-4529 panic.txt (the sibling teardown race that masks it), fix.diff

PoC changes

Built nvgetlog/nvspam/nvslow load tools + nvmebang trigger module; QEMU relaunched with an emulated nvme controller (vm_nvme.sh).

Verified recommended fix

Route all admin-queue submissions through a wrapper that takes subqueues[0].lk around nvme_submit_request().

Verdict

Code-certain lock-protocol violation, not dynamically isolated on the guest. nvme_submit_request() (nvme.c:514-529) documents 'Caller must hold the queue lock' and mutates subq_tail/unsubmitted/ksubq[tail] unlocked; the I/O path honors it (nvme_disk.c:336-338) but every admin-queue submitter (nvme_ioctl.c:56 getlog; nvme.c:800/843/877/902 queue create/delete helpers; nvme_admin.c:233/319/534/598/629 identify flows) submits with no lock. Two concurrent admin submitters exist exactly when the admin thread's attach/detach command storm overlaps a user NVMEIOCGETLOG on the same controller. On the guest: the admin ioctl path was load-tested clean (1.35M ioctls, 0 errors, 6-way concurrent; getlog submitters serialize on ioctl_lk among themselves), and the deterministic trigger environment was built (nvmebang on-demand attach/detach storms) -- but the DF-4529 teardown panic (same rig, same files) fires before attach-storm race data could be collected, so the submit-side interleaving itself was not observed. Consequences if hit: double-written ksubq slot / lost command -> nvme_wait_request's unbounded, non-interruptible loop (unkillable thread holding comq lock), or corrupted unsubmitted accounting driving get_request into its NULL path (KKASSERT is a no-op on production kernels) -> NULL deref in nvme_get_admin_request (nvme.c:336).