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

dpt_init/dpt_attach: device-fed MAX_CHAN (up to 8 channels) never clamped to MAX_CHANNELS=3 β€” OOB writes of sims[3..7]/paths[3..7] over live softc members incl. bus_dma_tag_t/bus_dmamap_t (type confusion)

Summary

conf.MAX_CHAN is a 3-bit controller-fed field so dpt->channels ranges 1..8 while sims[]/paths[] are [MAX_CHANNELS=3] (the header even admits the 'FAT assumption'). The attach loop then performs up to 10 OOB pointer stores over paths[], commands_processed, parent_dmat, dccb_dmat, dccb_dmamap, sg_dmat; detach then bus_dma_tag_destroy()s the confused pointers - wild deref / kernel memory corruption. Layout proof compiled+run on the guest (proves &sims[3]==&paths[0] and enumerates all 10 OOB stores for MAX_CHAN=3..7). Clamp fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4240 Β· 15 files
FileTypeDescriptionSize
dfdpt_rig.c β€” 37.5 KB view raw
dfdpt_ctl.c β€” 7.5 KB view raw
Makefile β€” 108 B ↓ download
dpt.h β€” 38.3 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 206 B view raw
build.log β€” 1.6 KB view raw
run.log β€” 3.0 KB view raw
run_chan4_controlled.log β€” 3.4 KB view raw
run_channels8_fault.log β€” 2.3 KB view raw
fix_validation.log β€” 3.7 KB view raw
fix.diff β€” 776 B view raw
verdict.json β€” 5.2 KB view raw
VERDICT.md β€” 5.0 KB ↓ raw
env.txt β€” 789 B view raw
VERDICT.md
↓ download raw

DF-4240 β€” VERDICT

REPRODUCED (kernel memory corruption, crash; fix validated on a rebuilt kernel).

What was proven, and how

sys/dev/raid/dpt/dpt_scsi.c is compiled into the guest's X86_64_GENERIC kernel (nm /boot/kernel/kernel β†’ dpt_attach @ 0xffffffff80439500, dpt_init @ …89f0+, dptallocccbs @ …75e0, dpt_action @ …8010). There is no DPT HBA on the guest, so the rig (dfdpt_rig.c) stands in for the hardware only:

  1. Baseline, controlled (chan8 4, run.log + run_chan4_controlled.log): fabricate a post-dpt_init softc (real bus_dma, real DMA-mapped dccb array, free list built by production dptallocccbs), set channels = 4 β€” exactly what production dpt_init computes from a device-fed MAX_CHAN = 3 (dpt_scsi.c:1313, no clamp) β€” and call the production dpt_attach. Observed: - attach_ret=4: the loop ran iteration i=3 out of bounds; - &sims[3] == &paths[0] (rig prints 0 bytes apart): iteration 3 stored a cam_sim* into paths[0], clobbering the path pointer installed by iteration 0 β€” paths[0] == sims[3] value confirmed ("type confusion: YES"); - canary commands_processed 0x41414141 β†’ 0x8d481e20 and lost_interrupts 0x42424242 β†’ 0xfffff800: together the kernel pointer 0xfffff8008d481e20 that xpt_create_path(&dpt->paths[3]) wrote across both counters (8-byte store at &paths[3] == &commands_processed).
  2. Baseline, catastrophic (chan8 8, run_channels8_fault.log): with channels=8 all ten OOB stores execute; the store at &sims[7] (== &channels+max_id) corrupts the loop-bound field channels with the low 32 bits of a kernel pointer, the loop runs away, and production xpt_create_path faults writing a cam_path* to an unmapped page 77,824 bytes past the softc: Fatal trap 12: page fault … supervisor write data, page not present … xpt_create_path+0x6b: movq %rbx,(%r12) β€” system down in DDB.
  3. Fix validation (fix_validation.log): all three dpt fixes applied to the guest's /usr/src, kernel rebuilt (nativekernel, gcc 8.3) and booted. The rig's fullinit path emulates the EATA config handshake (virtual HBA kthread: watches HA_WCOMMAND, DMA-feeds the 0xc1 config page incl. MAX_CHAN=7, raises HA_AIRQ; the handshake emulation is race-free β€” see rig comments) so the production dpt_init parses the device-fed config itself: - dpt7: controller reported 8 channels; clamping to 3 (the fix) - dpt7: FW Rev. , 3 channels, 16 CCBs (production init banner β€” full success) - production dpt_attach returned 3; canaries 0x41414141β†’0x41414141, 0x42424242β†’0x42424242 intact; slot at +176 still reads 0x4242424241414141 (the canary pair) and +184 reads 0xf00000003 (channels=3, max_id=15 β€” the softc's own fields, no pointer store); runaway=0.

Baseline behavior gone on the patched kernel; normal 3-channel attach unaffected.

Threat model

All inputs are device-fed: conf.MAX_CHAN arrives over the EATA PIO/DMA config protocol from the PCI card (a hostile or glitching controller β€” firmware bug, power blip, PCI reset β€” or a passed-through/virtualized device). Attach runs once per boot/hotplug, so the corruption is single-shot but destructive: with MAX_CHAN=3..4 it type-confuses paths[0] (CAM later treats a cam_sim* as a cam_path*) and turns two counters into a pointer; with MAX_CHANβ‰₯5 it corrupts channels/max_id, the TAILQ linkage of dpt_softcs, and β€” via the loop-bound feedback β€” writes an unbounded stream of kernel pointers past the softc (demonstrated page fault). Local unprivβ†’root is not the threat model here; this is a hostile/failed-device kernel-corruption bug.

Notes / caveats

  • The rig's path0_holds_sim detector compares the value at &sims[3] with the value at &paths[0] β€” the same address β€” so it prints "YES" even on the patched kernel where no OOB store happened (&paths[0] legitimately holds its own path). The canaries and attach_ret/channels are the real discriminators; VERDICT relies on those.
  • The chan8 baseline bypasses dpt_init (fabricates its output), because the full handshake emulation was built for the fix-validation run; the fullinit path closes that gap end-to-end on the patched kernel (device β†’ MAX_CHAN β†’ clamp).
  • Earlier seed evidence (dpt_df4240_layout.c, old run.log) was a userland ABI layout proof only; superseded by this run.

Bottom line: the unclamped, controller-fed channel count drives production dpt_attach into demonstrated out-of-bounds pointer stores (type confusion over live softc members and an uncontrolled runaway write), and the one-line clamp in fix.diff removes it.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Validated on a rebuilt+rebooted guest kernel carrying all three dpt fixes: fullinit (production dpt_init parsing a device-fed MAX_CHAN=7 via emulated EATA handshake) prints the clamp warning, emits channels=3, production dpt_attach returns 3 with canaries intact and zero OOB stores, while the same rig on the unpatched kernel corrupted the canaries/paths[0] (channels=4) and page-faulted (channels=8).

fix_validation.log
↓ fix.diffper-fix-DF-4240

Confirmed kernel references

Detail

Exploit chain

device-fed MAX_CHAN=7 -> dpt_init channels=8 (unclamped) -> dpt_attach stores sims[3..7]/paths[3..7] OOB -> paths[0] type-confused with cam_sim, commands_processed/lost_interrupts become a cam_path, channels clobbered by sims[7] -> loop-bound feedback -> unbounded kernel pointer stores past the softc -> page fault (write) in production xpt_create_path -> kernel down. Impact ceiling on a real machine: attach-time kernel heap corruption with partially device-chosen pointer values.

Evidence (decisive lines)

run.log (both baseline variants, verbatim); run_channels8_fault.log (Fatal trap 12 at xpt_create_path+0x6b, fault VA 0xfffff801184f3000 = softc+77824); run_chan4_controlled.log (canaries 0x41414141->0x8d481e20 / 0x42424242->0xfffff800, paths[0]==sims[3]); fix_validation.log (clamping message, channels=3, canaries intact, attach_ret=3)

PoC changes

No runnable kernel-level seed existed (the prior seed was a userland ABI layout prover). Built the dfdpt production-handler dispatch rig from scratch: resolves production dpt_attach/dpt_init/dpt_intr from the running kernel (globals link, statics primed via sysctl with host-side prologue byte verification), fabricates post-init softc state over real busdma, populates the free dccb list via production dptallocccbs, and adds a virtual-HBA kthread emulating the EATA config handshake so the fix could be validated through production dpt_init end-to-end. Two rig bugs fixed along the way (sim-lock assert on registration; lock leak across setup) plus a stale-binary reload trap.

Verified recommended fix

Clamp dpt->channels to MAX_CHANNELS after conf.MAX_CHAN+1 (dpt_scsi.c:1313).

Verdict

REPRODUCED via the production-handler KLD dispatch rig: with a device-fed channels=4 (MAX_CHAN=3, exactly what production dpt_init computes unclamped at dpt_scsi.c:1313), the production dpt_attach (0xffffffff80439500 in the shipped X86_64_GENERIC) executed the i=3 iteration out of bounds -- &sims[3]==&paths[0] stored a cam_sim over paths[0] (type confusion confirmed: paths[0]==sims[3] value) and xpt_create_path(&paths[3]) wrote the kernel pointer 0xfffff8008d481e20 across commands_processed (0x41414141->0x8d481e20) and lost_interrupts (0x42424242->0xfffff800). With channels=8 all 10 OOB stores executed and the store at &sims[7]==&channels corrupted the loop bound itself: production dpt_attach ran away writing cam_sim/cam_path* pointers until xpt_create_path faulted on an unmapped page 77,824 bytes past the softc (Fatal trap 12, supervisor write, page not present) -- system down in DDB. FIX VALIDATED on a rebuilt kernel via the fullinit path, which emulates the EATA config handshake (virtual HBA DMA-feeds the 0xc1 config page with MAX_CHAN=7) so the PRODUCTION dpt_init parses the device-fed data itself: 'dpt7: controller reported 8 channels; clamping to 3', init completes ('3 channels, 16 CCBs'), production dpt_attach returns 3, canaries intact (0x41414141/0x42424242 preserved), no pointer store past paths[2], runaway=0. Threat model: hostile/glitching PCI device (no unprivileged-local vector).