pvscsi_process_completion() writes through a NULL ccb on duplicate/spurious device completion (missing guard its own timeout handler has)
Summary
The completion path resolves the hypervisor-fed context to an hcb and writes ccb->csio.scsi_status/resid without checking hcb->ccb for NULL. hcb->ccb is NULL for every hcb not in flight. A device (VMware host) emitting a duplicate completion for an already-completed context, or a spurious completion for a never-issued in-range context, produces a write to a fixed low address => panic. The driver's own timeout handler guards this exact condition; distinct from DF-1455 (out-of-range context): here the context is in-range and the hcb valid - only the ccb==NULL check is missing. Driver compiled into GENERIC. NULL-guard fix (validated: fix.diff applies and module rebuilds on the guest's pristine source).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4221 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | summary | 2.9 KB | β raw |
| VERDICT.md | verdict | line-by-line source trace + fix validation | 4.5 KB | β raw |
| fix.diff | suggested-fix | NULL-ccb guard in process_completion | 569 B | view raw |
| patch_apply.log | apply-log | clean patch -p2 on pristine guest source | 318 B | view raw |
| build.log | build-log | module rebuild with fix (guard string in pvscsi.ko) | 876 B | view raw |
| build_baseline.log | build-log | unpatched module build | 876 B | view raw |
| env.txt | environment | guest uname, source md5, no-VMware-PCI note | 478 B | view raw |
| verdict.json | verdict | machine verdict | 3.5 KB | view raw |
DF-4221 β pvscsi_process_completion dereferences a NULL ccb on duplicate/spurious device completion
File: sys/dev/virtual/vmware/pvscsi/pvscsi.c (pass 2; DF-1454/DF-1455 excluded, not re-reported)
What
pvscsi_process_completion() (pvscsi.c:888-1035) resolves the completion
entry's hypervisor-fed e->context to an hcb and then dereferences
hcb->ccb without any NULL check:
hcb = pvscsi_context_to_hcb(sc, e->context); /* :899 (range = DF-1455) */
callout_stop(&hcb->callout); /* :901 */
ccb = hcb->ccb; /* :903 */
btstat = e->host_status; /* :905 */
sdstat = e->scsi_status; /* :906 */
ccb->csio.scsi_status = sdstat; /* :908 <-- NULL write if ccb==NULL */
ccb->csio.resid = ccb->csio.dxfer_len - e->data_len; /* :909 */
hcb->ccb is NULL for every hcb that is not in flight: it is set NULL by
pvscsi_hcb_put() (pvscsi.c:382) on completion and on every error path.
The driver's own timeout handler does guard this exact case
(pvscsi_timeout, pvscsi.c:850-853: if (ccb == NULL) { /* Already completed */ return; }),
but the completion path does not.
Trigger (device/hypervisor-fed, same trust boundary as DF-1454/DF-1455): the completion ring is DMA shared with the virtual device; a malicious or buggy VMware host can emit
- a duplicate completion for a context that was already completed
(the hcb is back on the free list,
ccb == NULL), or - a spurious completion for a valid-but-never-issued context
(hcb never left the free list,
ccb == NULL).
Result: write of sdstat (device-controlled 16-bit expanded to u32) to the
fixed low address offsetof(union ccb, csio.scsi_status) (~0xB0) and
immediately further NULL-based writes/reads (:909, :911-918, :1018-1034) β
kernel page fault β panic (NULL page is unmapped). This is distinct from
DF-1455 (out-of-range context β wild hcbs + (context-1) pointer): here
the context is in-range and the hcb is valid β only the missing ccb == NULL
check matters.
Reachability
Requires a pvscsi controller (VMware hypervisor presenting 15ad:07c0) β the
attacker is the host/hypervisor side of the paravirtual device. The driver is
compiled into X86_64_GENERIC (sys/config/X86_64_GENERIC:147). Not
triggerable on the audit guest (QEMU exposes no pvscsi device β verified:
no 15ad PCI devices), hence source-level certainty + fix build validation
only.
Reproduce / validation
No live trigger on this guest. VERDICT.md carries the line-by-line trace.
fix.diff (NULL-ccb guard mirroring pvscsi_timeout's) applies cleanly to
the pristine guest kernel source and the pvscsi module rebuilds with the
guard compiled in (build.log; the guard's format string is present in the
resulting pvscsi.ko).
Files
VERDICT.md, fix.diff, patch_apply.log, build.log,
build_baseline.log (unpatched build), env.txt, manifest.json,
verdict.json
DF-4221 VERDICT β source-certain NULL-ccb deref on device-fed duplicate completion; HW(vmm)-gated, not live-reproducible on this guest
Status: inconclusive (hardware/vmm-gated β no pvscsi device on the audit guest); defect certain at source level
Root cause
pvscsi_process_completion():
sys/dev/virtual/vmware/pvscsi/pvscsi.c:899 hcb = pvscsi_context_to_hcb(sc, e->context); sys/dev/virtual/vmware/pvscsi/pvscsi.c:903 ccb = hcb->ccb; sys/dev/virtual/vmware/pvscsi/pvscsi.c:908 ccb->csio.scsi_status = sdstat; <-- NULL write sys/dev/virtual/vmware/pvscsi/pvscsi.c:909 ccb->csio.resid = ccb->csio.dxfer_len - e->data_len; sys/dev/virtual/vmware/pvscsi/pvscsi.c:911 if ((ccb->ccb_h.flags & CAM_DIR_MASK) != CAM_DIR_NONE) { ... }
e->context is read from the completion ring, which is shared DMA with the
hypervisor device (sc->cmp_ring, filled by the device;
pvscsi_process_cmp_ring pvscsi.c:1051-1058 walks it under only the
assumption that the device behaves). hcb->ccb is NULL whenever the hcb is
not in flight:
- set NULL in
pvscsi_hcb_put()(pvscsi.c:382) β called on every completion (pvscsi.c:1020) and on thebus_dmamap_load_ccberror path (pvscsi.c:1209); - hcbs start on the free list with
ccb == NULL(kmalloc M_ZERO + SLIST_INIT, pvscsi.c:1719-1720, 609-613).
The driver itself establishes that a not-in-flight hcb must be skipped β its timeout handler does exactly that:
sys/dev/virtual/vmware/pvscsi/pvscsi.c:850-853 if (ccb == NULL) { /* Already completed */ return; }
The completion path lacks the same guard. A device that emits a completion
for (a) an already-completed context (duplicate) or (b) a never-issued
in-range context drives ccb == NULL into ccb->csio.scsi_status = sdstat
β a write to a fixed low address (first page unmapped on amd64) β page fault
β panic. DoS, not controllable corruption: the write target is fixed by the
structure layout, not by the attacker.
Why not DF-1455 (not a duplicate)
DF-1455 covers the unvalidated context (out-of-range context β wild
sc->hcbs + (context - 1) β OOB hcb) and the log2-derived ring masks. This
finding covers the in-range context, idle hcb case: a distinct missing
check (ccb == NULL), reached with a perfectly valid context value, on a
valid hcb pointer. Both defects live in the same function; the fixes are
independent and complementary (an in-range check alone does not fix this,
and my ccb guard alone does not fix DF-1455).
Reachability analysis
- Attacker: the hypervisor/VMware host (device side of the paravirt ring). Same trust boundary already accepted by this audit for DF-1454 (device-fed sense length) and DF-1455 (device-fed context/log2).
- The driver ships in GENERIC (
sys/config/X86_64_GENERIC:147,sys/conf/files:3087), so any DragonFly system on VMware ESXi/Fusion with a PVSCSI controller runs this code. - Guest check performed:
pciconf -lon the audit guest shows no 15ad (VMware) PCI devices β QEMU does not emulate pvscsi β so no live trigger is possible here (device-absent, not a false positive).
Fix validation (on the audit guest's kernel source)
- Guest
/usr/src/sys/.../pvscsi/pvscsi.cmd5d3cfdba6fe7b03d200afcfaec291f544== pristine host copy (fidelity record inenv.txt). - Baseline module build: OK (
build_baseline.log). fix.diffapplies withpatch -p2(patch_apply.log) and the module rebuilds cleanly (build.log); the guard's new format string"completion for idle context %llu ignored"is present in the resultingpvscsi.kobinary β the patched path is compiled in.- Runtime validation HW-gated (no device) β
fix_status: inconclusive(compiles + applies; not exercised against hardware).
Fix correctness notes
The guard returns without consuming/freeing anything: for an idle hcb there
is no ccb to complete, no DMA map to unload (any map was unloaded when the
real completion ran), and the callout is stopped harmlessly (idle hcbs have
no pending callout). The consumer index still advances in
pvscsi_process_cmp_ring (pvscsi.c:1057), so the ring makes progress β no
livelock. Combined with DF-1455's (recommended) in-range context check, the
completion path then handles every device-fed entry safely.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
inconclusivefix.diff applies cleanly to the pristine guest kernel source (patch_apply.log) and the pvscsi module compiles with the guard (build.log; guard string verified inside the built pvscsi.ko). Runtime behaviour is hardware(vmm)-gated on this guest β no pvscsi device β so the fix is compile-validated only.
patch_apply.log (clean apply), build.log (module rebuild OK), strings pvscsi.ko | grep 'idle context' => 'completion for idle context %llu ignored'
Confirmed kernel references
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:888-909
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:903
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:908
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:850-853
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:377-386
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:1018-1020
- sys/dev/virtual/vmware/pvscsi/pvscsi.c:1051-1058
Detail
Evidence (decisive lines)
VERDICT.md trace: pvscsi.c:903 (ccb = hcb->ccb) -> pvscsi.c:908 (ccb->csio.scsi_status = sdstat with ccb==NULL), contrast with the guarded timeout path pvscsi.c:850-853. fix.diff applies to pristine guest source (patch_apply.log), module rebuilds (build.log) and the guard's string 'completion for idle context %llu ignored' is present in the built pvscsi.ko.
PoC changes
no PoC source β device(hypervisor)-gated; evidence is the source trace plus on-guest fix build validation
Verified recommended fix
In pvscsi_process_completion, after ccb = hcb->ccb, return early (with a device_printf) when ccb == NULL, mirroring the guard already present in pvscsi_timeout
Verdict
Source-certain NULL-dereference: pvscsi_process_completion (pvscsi.c:899-909) resolves the device-fed completion context to an hcb and writes ccb->csio.scsi_status without checking hcb->ccb for NULL, although hcb->ccb is NULL for every not-in-flight hcb (pvscsi_hcb_put pvscsi.c:382) and the driver's own timeout handler guards this exact case (pvscsi.c:850-853). A duplicate completion for an already-completed context, or a spurious completion for a never-issued in-range context, both device(hypervisor)-fed via the shared completion ring, drive a write to a fixed low address => kernel panic. Distinct from DF-1455 (out-of-range context => wild hcb pointer): here the context is in-range and the hcb valid; only the ccb==NULL check is missing. Not live-reproducible on the audit guest (QEMU exposes no pvscsi device; no 15ad PCI ids), so impact recorded as none; realistic impact is a hypervisor/host-triggered denial of service on VMware guests.
No comments yet.