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

Attach error-path asymmetry: double bus_dmamap_destroy (double kfree of M_DEVBUF maps) and callout_drain/bus_dmamap_destroy walked through a NULL hcb array

Summary

Two attach-failure defects: (a) on partial per-hcb DMA alloc failure the fail path destroys the maps it created, then the attach error cleanup destroys them AGAIN via free_all - bus_dmamap_destroy kfree()s with no idempotence = double free into M_DEVBUF/kmalloc zone; (b) if the hcb-array kmalloc fails, free_all still runs dma_free_per_hcb with hcb_cnt set before the failure and hcbs==NULL - the helper computes hcb = NULL + i and callout_drains on a wild pointer: panic during OOM recovery. Boot/attach-time heap corruption on pvscsi-equipped systems under memory pressure (driver in GENERIC). hcbs_setup-bookkeeping fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4223 Β· 7 files
FileTypeDescriptionSize
README.md readme summary 3.1 KB ↓ raw
VERDICT.md verdict call-graph trace of both defects + fix validation 5.6 KB ↓ raw
fix.diff suggested-fix sc->hcbs_setup bookkeeping 1.1 KB view raw
patch_apply.log apply-log clean patch -p2 371 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.5 KB view raw
README.md readme summary
↓ download raw

DF-4223 β€” pvscsi attach/detach error-path asymmetry: double bus_dmamap_destroy (double kfree) and wild callout_drain through a NULL hcb array

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

What

pvscsi_free_all() (pvscsi.c:1566-1608) assumes sc->hcb_cnt per-hcb DMA resources exist and that sc->hcbs is valid. Both assumptions break on attach-failure paths, producing two distinct memory-safety defects:

(a) Double bus_dmamap_destroy β‡’ double kfree

On partial failure of pvscsi_dma_alloc_per_hcb() (pvscsi.c:558-621):

  1. the fail path at pvscsi.c:615-618 runs pvscsi_dma_free_per_hcb(sc, i) β€” destroying the bus_dmamap_ts it created for hcbs [0..i) (pvscsi.c:549-552);
  2. pvscsi_attach() then calls pvscsi_free_all() (pvscsi.c:1729), which runs pvscsi_dma_free_per_hcb(sc, sc->hcb_cnt) again (pvscsi.c:1586) β€” destroying the same [0..i) maps a second time.

DragonFly's bus_dmamap_destroy() (sys/platform/pc64/x86_64/busdma_machdep.c:439-449) tolerates a NULL map but performs kfree(map, M_DEVBUF) without clearing the caller's pointer and without any freed-flag β€” the second destroy is a double free into the M_DEVBUF/kmalloc zone (heap corruption). The [i..hcb_cnt) maps that were never created are NULL and harmless, which is exactly why this asymmetry survives casual review.

(b) sc->hcbs == NULL β‡’ wild callout_drain/bus_dmamap_destroy

If the hcb array allocation fails (pvscsi.c:1718-1724), attach still calls pvscsi_free_all(), which unconditionally runs pvscsi_dma_free_per_hcb(sc, sc->hcb_cnt) with sc->hcb_cnt = adapter_queue_size (set at pvscsi.c:1718 before the failing kmalloc) and sc->hcbs == NULL. The helper then computes hcb = sc->hcbs + i (pvscsi.c:542, 550) β€” a NULL/wild pointer for every i β€” and executes callout_drain(&hcb->callout) (pvscsi.c:543-544) on it β‡’ NULL/low-address kernel access β‡’ panic during OOM recovery (the exact moment the system is already fragile).

Both are classic error-path asymmetry bugs: the partial-cleanup function and the global-cleanup function both free the same resources, and the global cleanup trusts counts that were set before the failed allocation.

Reachability

Requires a pvscsi controller present (driver probes) and then (a) a bus_dmamap_create failure mid-loop or (b) an M_NOWAIT kmalloc failure (~26 KB for the default 256-hcb array) β€” i.e. severe memory pressure or resource exhaustion at attach time. Severity Low: attach-time, OOM-gated, DoS/heap-corruption only, but the driver ships in GENERIC (sys/config/X86_64_GENERIC:147).

Reproduce / validation

No pvscsi device on the audit guest β€” the faults cannot be scheduled. Source trace in VERDICT.md. fix.diff (track how many hcbs actually have per-hcb resources in sc->hcbs_setup; use it in pvscsi_free_all; zero it after the partial free) 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 call-graph trace of both defects + fix validation
↓ download raw

DF-4223 VERDICT β€” source-certain attach-error-path double-free and NULL-array wild access; HW(vmm)-gated guest

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

Defect (a): double bus_dmamap_destroy = double kfree

Call graph on a pvscsi_dma_alloc_per_hcb() partial failure:

pvscsi_attach (pvscsi.c:1727)
  -> pvscsi_dma_alloc_per_hcb (pvscsi.c:558)
       loop: bus_dmamap_create(...) for hcbs [0..k)   (pvscsi.c:583-592)
       FAILS at hcb k (map create error)
     fail:
       pvscsi_dma_free_per_hcb(sc, k)                  (pvscsi.c:615-618)
         -> bus_dmamap_destroy(buffer_dmat, hcb->dma_map) for [0..k)   (pvscsi.c:549-552)
            each destroy kfree()s the map               (busdma_machdep.c:445)
       return error
  -> device_printf + pvscsi_free_all(sc)               (pvscsi.c:1728-1730)
       -> pvscsi_dma_free_per_hcb(sc, sc->hcb_cnt)     (pvscsi.c:1586)
            -> bus_dmamap_destroy(...) for [0..hcb_cnt)  AGAIN
               [0..k): SECOND kfree of the same M_DEVBUF pointers  <-- double free
               [k..hcb_cnt): NULL maps (bus_dma_tag_create failure path
                             sets *mapp = NULL, busdma_machdep.c:429-430;
                             destroy tolerates NULL, busdma_machdep.c:442) β€” harmless

bus_dmamap_destroy (sys/platform/pc64/x86_64/busdma_machdep.c:439-449) has no idempotence: kfree(map, M_DEVBUF) with no pointer zeroing and no freed marker. A second destroy of the same pointer is a textbook double free (kmalloc-zone corruption β€” freelist poisoning class). Note the adjacent dmat->map_count-- also underflows for the never-created maps (cosmetic, map_count is size_t β€” wraps, no memory fault).

The partial-cleanup inside pvscsi_dma_alloc_per_hcb and the global cleanup in pvscsi_free_all both free the same objects with no shared bookkeeping β€” that is the root cause. (For contrast, pvscsi_dma_free IS idempotent: it bzero()s the descriptor after freeing, pvscsi.c:461 β€” the sg/sense DMA frees in the same paths are safe; only the map-destroy loop is not.)

Defect (b): hcbs == NULL walked as an array

pvscsi_attach (pvscsi.c:1718)   sc->hcb_cnt = adapter_queue_size;   (256 default)
pvscsi_attach (pvscsi.c:1719)   sc->hcbs = kmalloc(hcb_cnt * sizeof(*hcbs),
                                                   M_NOWAIT | M_ZERO);
pvscsi_attach (pvscsi.c:1721)   if (sc->hcbs == NULL) { ... pvscsi_free_all(sc); ... }
pvscsi_free_all (pvscsi.c:1586)     pvscsi_dma_free_per_hcb(sc, sc->hcb_cnt);
pvscsi_dma_free_per_hcb (pvscsi.c:541-544)
    for (i = 0; i < hcbs_allocated; ++i) {
        hcb = sc->hcbs + i;             /* NULL + i*sizeof(pvscsi_hcb) */
        callout_drain(&hcb->callout);   /* wild read/write at NULL+104*i β€” PANIC */

hcb_cnt was set before the failing allocation, so the loop runs 256 times over a NULL base; the very first callout_drain dereferences NULL (kern_timeout.c:1047 _callout_cancel_or_stop touches cc->flags etc.). A system that just failed a 26 KB allocation now takes a kernel page fault in its error path.

Reachability

Driver must be present (pvscsi PCI id 15ad:07c0 β€” VMware). Then: (a) needs bus_dmamap_create failure partway through the hcb loop (busdma tag/map resource exhaustion); (b) needs the hcb-array kmalloc to fail under memory pressure. Both are attach-time-only, OOM-gated β€” severity Low (DoS / heap corruption during boot-time stress; not attacker-schedulable beyond memory-pressure induction). Ships in GENERIC: sys/config/X86_64_GENERIC:147, sys/conf/files:3087.

Not triggerable on the audit guest: no pvscsi device (no 15ad PCI ids in pciconf -l). Verified by code trace only.

Fix

fix.diff adds sc->hcbs_setup ("number of hcbs whose per-hcb DMA resources exist"):

  • set to hcb_cnt only after the full alloc loop succeeds (pvscsi.c:612β†’ new line);
  • zeroed in the fail path after the partial free, so pvscsi_free_all never re-destroys the already-destroyed maps;
  • pvscsi_free_all uses sc->hcbs_setup instead of sc->hcb_cnt (pvscsi.c:1586) β€” with hcbs == NULL, hcbs_setup == 0 and the loop body never runs (softc is zero-allocated by newbus).

Detach behaviour is unchanged (on a fully attached device hcbs_setup == hcb_cnt).

Fix validation

  • pristine guest source md5 d3cfdba6fe7b03d200afcfaec291f544 == host sys/ copy (env.txt);
  • patch -p2 < fix.diff applies cleanly (patch_apply.log);
  • pvscsi module rebuilds with the fix (build.log).
  • Runtime validation HW-gated (no device) β‡’ fix_status: inconclusive.

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

fix.diff (sc->hcbs_setup bookkeeping) 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-4223

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md call graphs: double destroy pvscsi.c:615-618 + pvscsi.c:1586 vs busdma_machdep.c:442-446 (kfree, NULL-tolerant but not double-tolerant); NULL-array walk pvscsi.c:1718-1724 -> 1586 -> 541-544. patch_apply.log + build.log: fix.diff (sc->hcbs_setup bookkeeping) applies and compiles on pristine guest source.

PoC changes

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

Verified recommended fix

Track hcbs whose per-hcb DMA resources exist (sc->hcbs_setup): set it only on full success, zero it after the partial free, and make pvscsi_free_all use it instead of hcb_cnt so freed maps are never destroyed twice and a NULL hcb array is never walked

Verdict

Source-certain attach-error-path asymmetry in pvscsi: (a) after a partial pvscsi_dma_alloc_per_hcb failure, the fail path destroys the per-hcb bus_dmamaps (pvscsi.c:615-618 -> 549-552) and pvscsi_attach's error cleanup destroys them AGAIN via pvscsi_free_all (pvscsi.c:1586) β€” DragonFly bus_dmamap_destroy (busdma_machdep.c:439-449) kfree()s without idempotence, so this is a double free into M_DEVBUF (heap corruption); (b) when the hcb-array kmalloc fails, pvscsi_free_all still walks pvscsi_dma_free_per_hcb(sc, sc->hcb_cnt) with sc->hcbs == NULL and hcb_cnt set before the failure (pvscsi.c:1718-1724, 541-544), calling callout_drain on NULL+i*sizeof(hcb) => panic during OOM recovery. Attach-time, OOM/resource-exhaustion-gated; driver ships in GENERIC. Not triggerable on the audit guest (no pvscsi device), so impact recorded none; realistic ceiling is boot-time DoS/heap corruption on VMware systems under memory pressure.