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

aac(4) dereferences adapter-controlled completion/AIF indices with no bounds validation at five sites β€” malicious or faulty controller gets wild kernel pointers and unlink writes

Summary

Indices taken from adapter MMIO/DMA are used unbounded: aac_new_intr cm = aac_commands + (index>>2); aac_complete cm = aac_commands + fib->Header.SenderData with a dead NULL check; aac_dequeue_fib AIF path &ac_fibs[aq_fib_addr/512] against the 8-slot array; the response path; aac_get_bus_info loops i<BusCount over 10-entry stack arrays. Compromised/faulty AAC controller firmware steers the completion machinery at a forged struct aac_command and gets TAILQ_REMOVE unlink writes, callout_stop, and an indirect cm_complete call at chosen kernel addresses - arbitrary-write ceiling; the BusCount site is an OOB stack read. Bounds-check fix at all five sites.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3820 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
fix.diff β€” 1.8 KB view raw
env.txt β€” 323 B view raw
verdict.json β€” 2.5 KB view raw

DF-3820 β€” aac(4) dereferences adapter-controlled FIB/queue indices with no bounds validation (malicious/errant controller β†’ wild kernel pointers)

  • File: sys/dev/raid/aac/aac.c
  • Lines: 853, 1118, 2221-2223, 2238-2240, 3771-3772
  • Class: CWE-129 (improper validation of array index) β†’ OOB pointer formation β†’ kernel memory read/write (device-trust)
  • Reach: PCI device DMA / MMIO only (compromised or faulty controller firmware; no unprivileged-software path).

What to look at

No runnable PoC exists without the hardware: every affected value is produced by the adapter (MMIO read or DMA into the common block). The defect is proven by inspection at each site:

  1. aac_new_intr() aac.c:853 β€” cm = sc->aac_commands + (index >> 2); index is read straight from AAC_GET_OUTB_QUEUE() (MMIO). No check against sc->total_fibs / aac_max_fibs. The subsequent aac_remove_busy(cm), aac_unmap_command(cm), cm->cm_complete(cm) operate on an arbitrary out-of-array struct aac_command *.
  2. aac_complete() aac.c:1118 β€” cm = sc->aac_commands + fib->Header.SenderData; β€” SenderData lives in the DMA'd FIB the adapter filled. The if (cm == NULL) guard at :1119 is dead (array offset can never be NULL); there is no range check.
  3. aac_dequeue_fib() aac.c:2221-2223 β€” AIF path: fib_index = aq_fib_addr / sizeof(struct aac_fib); *fib_addr = &sc->aac_common->ac_fibs[fib_index]; β€” aq_fib_addr is a 32-bit queue entry the adapter writes; ac_fibs has exactly AAC_ADAPTER_FIBS (8) slots (aacvar.h:71,217). A 32-bit index up to 0x1FFFFFF yields a pointer ~4 GB past the common block β†’ the AIF is then copied/processed from arbitrary kernel memory.
  4. aac_dequeue_fib() aac.c:2238-2240 β€” response path: cm = sc->aac_commands + (fib_index >> 2); *fib_addr = cm->cm_fib; β€” same: fib_index is the adapter-written aq_fib_addr, unbounded; cm->cm_fib dereferences the OOB command slot.
  5. aac_get_bus_info() aac.c:3771-3772 β€” for (i = 0; i < businfo.BusCount; i++) { ... businfo.BusValid[i] ... businfo.InitiatorBusId[i] } β€” BusCount is an adapter-supplied u32; both arrays have AAC_SCSI_MAX_PORTS (10) entries (aacreg.h:struct aac_getbusinf). OOB stack read of the local businfo copy, up to a wild read when BusCount is huge.

Impact ceiling

A compromised/faulty AAC controller (or anything able to DMA on its behalf) turns each site into a controlled kernel write: e.g. pick SenderData so that the bogus cm aliases chosen memory, then the completion path performs callout_stop, TAILQ_REMOVE (unlink writes) and an indirect call through cm->cm_complete. Severity capped at Low because the precondition is hardware/firmware compromise.

Verdict

inconclusive β€” no AAC HBA exists on the audit guest (see ../DF-3819/env.txt); the code paths are reachable only from adapter DMA/MMIO. Source-level: certain.

VERDICT.md
↓ download raw

VERDICT β€” DF-3820

Status: inconclusive (device-gated: no AAC controller on the audit guest; nothing can execute these paths without adapter DMA/MMIO). Source-level confidence: certain β€” every site is a plain, unguarded array-index-to-pointer computation on adapter-supplied data.

The five sites

# site source of index target object bound that should hold
1 aac.c:853 (aac_new_intr) AAC_GET_OUTB_QUEUE() MMIO sc->aac_commands[] (total_fibs ≀ 512/256) index >> 2 < sc->total_fibs
2 aac.c:1118 (aac_complete) fib->Header.SenderData (DMA) sc->aac_commands[] SenderData < sc->total_fibs
3 aac.c:2221-2223 (aac_dequeue_fib AIF) aq_fib_addr (adapter-written queue entry) sc->aac_common->ac_fibs[8] fib_index < AAC_ADAPTER_FIBS
4 aac.c:2238-2240 (aac_dequeue_fib resp) same sc->aac_commands[] fib_index >> 2 < total_fibs
5 aac.c:3771-3772 (aac_get_bus_info) BusCount (LCT/DMA-derived reply) stack businfo.BusValid[10], InitiatorBusId[10] BusCount ≀ AAC_SCSI_MAX_PORTS

Not one of them validates. The driver comment at aac.c:2232-2237 even acknowledges the value "was originally provided by the driver" β€” but a malicious or buggy adapter is free to return anything.

Why these matter even though they are device-gated

Modern threat models treat PCIe devices as untrusted once firmware compromise or a Thunderbolt-style attach is in play; DragonFly's own hardening push (IOMMU-less boards included) treats device-driven OOB as a finding class. Sites 1-4 hand the attacker a fully controlled fake struct aac_command location (via the 32-bit index scaling) which the completion machinery then writes through (aac_remove_busy β†’ TAILQ_REMOVE unlink writes; callout_stop; indirect call cm->cm_complete(cm)), i.e. arbitrary-write primitives, not just panics.

The upstream FreeBSD ancestor has the same pattern; this finding is filed as shared-hardening.

Guest evidence

The audit guest has no AAC hardware whatsoever:

  • ls /dev/aacd0 β†’ ENOENT; pciconf -l | grep -c "vendor=0x(1044|9005|9004|1028)" β†’ 0 (copied as ../DF-3819/env.txt, identical run).
  • aac is compiled into X86_64_GENERIC but never attaches.

No kernel path can be driven without the card β‡’ inconclusive, honest per the audit's status set.

Fix

fix.diff adds the five missing bounds checks (print + drop/break), each against the correct limit: sc->total_fibs (Γ—3), AAC_ADAPTER_FIBS, AAC_SCSI_MAX_PORTS.

Fix validation

not_testable β€” same hardware precondition as the baseline.

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

not_testable
baseline no→ patch + rebuild →patched clean

Baseline and patched runs both require an attached AAC controller; absent on the audit guest, so the fix cannot be exercised. Diff compiles-by-inspection against the current tree.

fix.diff
↓ fix.diffper-fix-DF-3820

Confirmed kernel references

Detail

Exploit chain

device-side: craft completion index -> fake struct aac_command at chosen kernel offset -> aac_remove_busy()/aac_unmap_command()/cm->cm_complete(cm) write through it -> kernel code execution. Precondition: compromised/faulty PCIe controller firmware.

Evidence (decisive lines)

VERDICT.md table of the five sites with exact bounds that are missing; ../DF-3819/env.txt for guest hardware absence; fix.diff adding all five checks

PoC changes

No trigger exists for device-DMA-only paths; pack is analysis + fix only.

Verified recommended fix

Bounds-check every adapter-supplied index before pointer formation (see fix.diff: total_fibs, AAC_ADAPTER_FIBS, AAC_SCSI_MAX_PORTS).

Verdict

Five adapter-controlled indices are turned into kernel pointers with no bounds validation: aac.c:853 (index>>2 into aac_commands from MMIO outbound queue), aac.c:1118 (SenderData from DMA'd FIB into aac_commands, dead NULL check), aac.c:2221-2223 (aq_fib_addr/512 into the 8-slot ac_fibs[]), aac.c:2238-2240 (aq_fib_addr>>2 into aac_commands), aac.c:3771-3772 (BusCount over the 10-entry stack BusValid/InitiatorBusId arrays). A malicious or faulty AAC controller gets controlled wild-pointer derefs and unlink-style writes through the fake aac_command (aac_remove_busy TAILQ_REMOVE, callout_stop, cm_complete indirect call). Cannot be exercised: guest has no AAC HBA (no /dev/aacd0, zero matching PCI vendor IDs, driver compiled into kernel but never attaches).