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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4221 Β· 8 files
FileTypeDescriptionSize
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
README.md readme summary
↓ download 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

VERDICT.md verdict line-by-line source trace + fix validation
↓ download raw

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 the bus_dmamap_load_ccb error 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 -l on 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.c md5 d3cfdba6fe7b03d200afcfaec291f544 == pristine host copy (fidelity record in env.txt).
  • Baseline module build: OK (build_baseline.log).
  • fix.diff applies with patch -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 resulting pvscsi.ko binary β€” 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

inconclusive
baseline no→ patch + rebuild →patched clean

fix.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'
↓ fix.diffper-fix-DF-4221

Confirmed kernel references

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.