nvme(4): duplicate nsid in the device-supplied ACTIVE NAMESPACE LIST re-attaches a live namespace β second nvme_disk_attach() bzeroes the live struct disk inside the global disklist (self-loop), double-inserts devstat, creates duplicate /dev/nvmeN cdevs β deterministic kernel panic on any later disklist walk
Summary
The nsid scan trusts the device list without dedup; a repeated nsid re-runs the whole attach on the live object: bioq/lock re-init on live objects, devstat double-insert into the global device_statq, disk_create bzero()ing the live struct disk while linked in the global disklist then double-LIST_INSERT_HEAD (self-loop), plus a duplicate same-name cdev. REPRODUCED on the QEMU-nvme guest via a KLD injecting exactly the duplicate decision and calling the REAL nvme_disk_attach on the REAL live nsc: panic 'Bad link elm ... next->prev != elm' in disk_enumerate, 2/2 fresh boots, triggerable by unpriv `sysctl kern.disks`. Fix validated on a rebuilt kernel: refusal message, clean walks, clean teardown. Crafted/firmware-compromised controller model (trivially emulated in a virt stack). Dedupe + idempotent-attach + cdev-clear fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4697 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df4697.c | β | 3.6 KB | view raw | |
| Makefile | β | 144 B | β download | |
| build.sh | β | 120 B | view raw | |
| run.sh | β | 147 B | view raw | |
| build.log | β | 3.7 KB | view raw | |
| run.log | β | 339 B | view raw | |
| run.2.log | β | 45 B | view raw | |
| run.patched.log | β | 250 B | view raw | |
| panic.txt | β | 907 B | view raw | |
| panic.2.txt | β | 819 B | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 1.8 KB | view raw |
Fix verification
fixedfix.diff applied to guest /usr/src, kernel rebuilt (nativekernel KERNCONF=X86_64_GENERIC, #1 22:14) and booted; identical PoC run: guard printed 'nvme0: namespace 1 already attached', cdev unchanged, sysctl kern.disks/kern.devstat.all rc=0, list identical to baseline, normal single detach still works. Baseline panic 2/2 vs patched 0/1.
findings/poc/DF-4697/run.patched.log; serial log lines 'nvme0: namespace 1 already attached' + cdev(after)=cdev(before); patched kernel uname '#1: Tue Sep 22 22:14:04 UTC 2026'
Confirmed kernel references
Detail
Exploit chain
crafted/misbehaving NVMe controller returns active-ns list [1,1] (or any duplicate) -> admin thread double-attaches nsid 1 (nvme_admin.c:680) -> global disklist self-loop + duplicate cdev + devstat queue corruption -> any unprivileged 'sysctl kern.disks' panics the kernel (deterministic); teardown leaves a stale /dev/nvmeN cdev pointing at the destroyed struct disk (UAF surface).
Evidence (decisive lines)
panic.txt + panic.2.txt (identical 'Bad link elm' signatures, disk_enumerate<-sysctl_disks trace, 2 fresh boots); run.log/run.2.log (second attach returned with cdev(after)!=cdev(before) = duplicate cdev); run.patched.log + 'namespace 1 already attached' in serial log (fix works, walks rc=0); fix.diff
PoC changes
seed sketch replaced entirely: wrote KLD (df4697.c) that locates the live controller via the driver's own globals (nvme_sc_list/nvme_master_lock, nvme_attach.c:50-51) and performs the second nvme_disk_attach() exactly as the buggy scan does; Makefile fixed to bsd.kmod.mk + generated bus_if/device_if/pci_if/pcidevs headers; module rebuilt after every panic (fs sync) loss.
Verified recommended fix
Dedupe the device-supplied active-ns scan in nvme_admin_state_identify_ns() and make nvme_disk_attach() idempotent (see fix.diff).
Verdict
Duplicate nsid in the device-controlled ACTIVE NAMESPACE LIST makes nvme_admin_state_identify_ns() call nvme_disk_attach() twice on the same live nvme_softns_t; the second disk_create() bzero()s the live struct disk while it is linked in the global disklist and re-inserts the same node (self-loop), devstat_add_entry() double-inserts into the global device_statq, and a duplicate /dev/nvmeN cdev is created. Any later disklist walk (sysctl kern.disks, unprivileged) panics with 'Bad link elm next->prev != elm'. Reproduced 2/2 on fresh boots of the QEMU-nvme guest: because stock QEMU cannot emit a duplicated nsid list, the PoC module injects only the duplicate-decision and then calls the REAL nvme_disk_attach() on the REAL live namespace (nvme_admin.c:680 frame-for-frame); the panic trace is pure unmodified driver/kernel code (disk_enumerate <- sysctl_disks <- sys___sysctl). fix.diff (scan dedupe + attach idempotence) validated on-guest with a rebuilt kernel: refusal message, identical disk list, clean walks and clean teardown, no panic.
No comments yet.