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

XPT_ABORT handler dereferences the ccb hcb back-pointer without a NULL check β€” aborting a completed/never-issued CCB panics

Summary

XPT_ABORT reads abort_hcb->ccb where the back-pointer is NULLed on every completion and NULL for CCBs never issued. Any abort of a not-in-flight CCB faults at a small fixed offset from NULL. Reachable via targ(4) abort paths racing a concurrent completion (abort_all_pending under crit only while completions clear the pointer under sc->lock), and targwrite's user abort translation. The sibling virtio-scsi never dereferences a back-pointer for abort. NULL-check + back-pointer-hygiene fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4222 Β· 7 files
FileTypeDescriptionSize
README.md readme summary 2.8 KB ↓ raw
VERDICT.md verdict line-by-line trace + reachability + fix validation 4.7 KB ↓ raw
fix.diff suggested-fix one-line NULL check 459 B view raw
patch_apply.log apply-log clean patch -p2 319 B view raw
build.log build-log module rebuild with fix 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.3 KB view raw
README.md readme summary
↓ download raw

DF-4222 β€” pvscsi XPT_ABORT handler dereferences a NULL hcb back-pointer (panic on abort of a completed CCB)

File: sys/dev/virtual/vmware/pvscsi/pvscsi.c (pass 2; DF-1454/DF-1455 excluded)

What

sys/dev/virtual/vmware/pvscsi/pvscsi.c:1394-1397
    abort_ccb = ccb->cab.abort_ccb;
    abort_hcb = abort_ccb->ccb_h.ccb_pvscsi_hcb;

    if (abort_hcb->ccb != NULL && abort_hcb->ccb == abort_ccb) {   /* <-- deref */

ccb_pvscsi_hcb is the driver's per-CCB back-pointer. It is cleared to NULL on every normal completion:

sys/dev/virtual/vmware/pvscsi/pvscsi.c:1018-1019
    ccb->ccb_h.ccb_pvscsi_hcb = NULL;
    ccb->ccb_h.ccb_pvscsi_sc = NULL;

and is NULL for any CCB that was never issued through this SIM. The XPT_ABORT handler dereferences it unconditionally (abort_hcb->ccb on the first line of the condition) β€” NULL->ccb read at a small fixed offset β‡’ kernel page fault / panic whenever CAM asks pvscsi to abort a CCB that is not currently in flight on this SIM.

The correct pattern is the standard one used by every other checked SIM in this tree β€” NULL-check the back-pointer before dereferencing:

    if (abort_hcb != NULL && abort_hcb->ccb == abort_ccb) {

(the original first clause abort_hcb->ccb != NULL short-circuits the same way once the pointer itself is checked; the semantics for a live hcb are identical).

Reachability

XPT_ABORT reaches pvscsi_action() from:

  • targ(4) kernel paths β€” abort_all_pending() (sys/bus/cam/scsi/scsi_target.c:1074-1116) walks the pending queue under crit_enter() only (scsi_target.c:498-500) and issues xpt_action(XPT_ABORT) per CCB; a completion landing on another CPU between queue inspection and the abort running clears ccb_pvscsi_hcb (pvscsi.c:1018) β‡’ NULL deref. Same for scsi_targ_bh.c:351,359.
  • targ(4) user paths β€” targwrite accepts user XPT_ABORT CCBs (scsi_target.c:719-745 translates the user abort_ccb pointer to a kernel CCB found on the pending queue; the same completion race applies).
  • Any kernel or userland (/dev/xpt0 CAMIOCOMMAND, root) issuer that aborts a CCB after it completed.

targ device nodes are privileged (root/operator), and the completion race window is narrow, so this is a privileged/racy local DoS β€” but it is a plain missing NULL check, trivially fixed. Contrast: the sibling virtio-scsi driver never dereferences a back-pointer for abort (vtscsi_cam_abort, virtio_scsi.c:941-974 uses its own request queue).

Reproduce / validation

No pvscsi device on the audit guest (QEMU; no 15ad PCI ids) β€” no live trigger. Source trace in VERDICT.md; fix.diff (one-line NULL check) applies cleanly to the pristine guest kernel source and the module rebuilds (build.log).

Files

VERDICT.md, fix.diff, patch_apply.log, build.log, env.txt, manifest.json, verdict.json

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

DF-4222 VERDICT β€” source-certain NULL back-pointer deref in XPT_ABORT; HW(vmm)-gated guest, not live-reproducible

Status: inconclusive (no pvscsi device on audit guest); defect certain at source level

Root cause

pvscsi_action() case XPT_ABORT:

sys/dev/virtual/vmware/pvscsi/pvscsi.c:1394   abort_ccb = ccb->cab.abort_ccb;
sys/dev/virtual/vmware/pvscsi/pvscsi.c:1395   abort_hcb = abort_ccb->ccb_h.ccb_pvscsi_hcb;
sys/dev/virtual/vmware/pvscsi/pvscsi.c:1397   if (abort_hcb->ccb != NULL && abort_hcb->ccb == abort_ccb) {
                                                    ^^^^^^^^^^^^^^^ NULL deref when abort_hcb == NULL

ccb_pvscsi_hcb lifecycle in this driver:

  • set when a SCSI I/O starts: pvscsi.c:1326 (ccb_h->ccb_pvscsi_hcb = hcb);
  • cleared on every completion: pvscsi.c:1018 (ccb->ccb_h.ccb_pvscsi_hcb = NULL;) inside pvscsi_process_completion, which runs under sc->lock from the interrupt / cmp-ring processing;
  • never set for CCBs that never passed through pvscsi_action(XPT_SCSI_IO).

Therefore any XPT_ABORT whose abort_ccb (a) already completed normally or (b) was never issued through this SIM presents abort_hcb == NULL and the first operand read abort_hcb->ccb faults at a small fixed offset from NULL (union ccb's ccb_h is at offset 0; spriv_ptr0 sits a few hundred bytes in) β‡’ page fault β‡’ panic.

The driver's own error paths make the dangling-pointer situation worse in a benign direction but underline the inconsistency: the bus_dmamap_load_ccb failure path (pvscsi.c:1200-1212) and the two finish_ccb paths (pvscsi.c:1329-1341) xpt_done() the CCB without clearing ccb_pvscsi_hcb (unlike pvscsi.c:1018) β€” leaving a stale non-NULL pointer that happens not to crash the abort handler only because the hcb memory is still valid.

Race that reaches it in-tree

targ(4):

sys/bus/cam/scsi/scsi_target.c:498-500   crit_enter(); abort_all_pending(softc); crit_exit();
sys/bus/cam/scsi/scsi_target.c:1099-1103 TAILQ_FOREACH(ccb_h, &softc->pending_ccb_queue, ...) {
                                             cab->abort_ccb = (union ccb *)ccb_h;
                                             xpt_action((union ccb *)cab);

crit_enter() blocks interrupts only on the current CPU. A pvscsi completion interrupt on another CPU completes the very CCB being iterated (pvscsi_process_completion clears ccb_pvscsi_hcb at pvscsi.c:1018 and targdone removes it from the pending queue); the subsequently dispatched XPT_ABORT then dereferences the now-NULL back-pointer β‡’ panic. The user-triggered abort path (targwrite β†’ targgetccb translation at scsi_target.c:719-745) has the same window against the pending queue.

Impact ceiling: local denial of service (panic). Requires pvscsi hardware (VMware host) plus a privileged or racy CAM-level abort β€” hence severity Low.

Why not a duplicate of DF-1454/DF-1455

Those cover the completion ring's device-fed sense_len bound and context/log2 validation. This is the CAM action path (XPT_ABORT) and a host-side NULL check β€” different function, different input, different fix. (A comparison against the FreeBSD original was attempted but the FreeBSD pvscsi driver never shipped in a release branch and is no longer fetchable; the defect is established from DragonFly's own source, which NULL-checks the identical pointer in pvscsi_timeout at pvscsi.c:850-853 but not here.)

Fix validation

  • fix.diff = one-line change to the standard abort_hcb != NULL && abort_hcb->ccb == abort_ccb pattern.
  • Applied with patch -p2 to the pristine guest kernel source (md5-identical to host sys/, see env.txt): patch_apply.log β€” clean.
  • Module rebuild: build.log β€” OK.
  • Runtime HW-gated on this guest (no pvscsi device; pciconf -l shows no 15ad vendor) β‡’ fix_status: inconclusive (compile-validated).

Kernel references

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

One-line NULL-check fix applies cleanly to the pristine guest kernel source (patch_apply.log) and the pvscsi module compiles (build.log). Runtime validation HW-gated on this guest.

patch_apply.log (clean apply), build.log (module rebuild OK)
↓ fix.diffper-fix-DF-4222

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md: pvscsi.c:1397 first-operand deref vs pvscsi.c:1018 NULL-clear vs pvscsi.c:850-853 guarded timeout path; scsi_target.c:498-500 crit_enter-only abort loop racing pvscsi.c:1018. patch_apply.log + build.log: one-line fix applies and compiles on the pristine guest kernel source.

PoC changes

no PoC source β€” device-gated; evidence is the source trace plus on-guest fix build validation

Verified recommended fix

Change the XPT_ABORT guard to 'if (abort_hcb != NULL && abort_hcb->ccb == abort_ccb)' and clear ccb_pvscsi_hcb in the execute_ccb/finish_ccb error paths

Verdict

Source-certain NULL dereference: pvscsi_action(XPT_ABORT) at pvscsi.c:1395-1397 reads abort_hcb->ccb where abort_hcb = abort_ccb->ccb_h.ccb_pvscsi_hcb, a back-pointer the driver itself sets to NULL on every completion (pvscsi.c:1018) and that is NULL for CCBs never issued through this SIM. Aborting a completed/never-issued CCB β€” reachable in-tree through targ(4) abort paths that race a concurrent completion (scsi_target.c:498-500,1099-1103 run under crit_enter() only, completion clears the pointer on another CPU under sc->lock) β€” faults at a small fixed offset from NULL => panic. The driver already NULL-checks the identical pointer in its timeout handler (pvscsi.c:850-853), proving the intended protocol; every other checked SIM in the tree validates before dereference. Not live-reproducible on the audit guest (QEMU exposes no pvscsi device) so impact recorded as none; realistic ceiling is a privileged/racy local DoS on VMware guests.