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)
PoC verification
Evidence pack
findings/poc/DF-4528 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_testablefix.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
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).
No comments yet.