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

ISA DMA channel validation is DIAGNOSTIC-only: OOB static-array writes forge another channel's bounce pointer (wild kernel write), and 1<<chan wraps to free/corrupt live channels

Summary

Every chan bound check in the 8237 glue is #ifdef DIAGNOSTIC (compiled out of X86_64_GENERIC) and dmastatus/dmastop never had any. Guest-proven with staged KLDs: acquire(34) wrapped 1<<34 onto bit 2 and release(34) freed a live channel 2; acquire(9) accepted chan>=8; init(8,0x1000) wrote bouncebufsize[8] onto bouncebuf[0]'s low dword and the heap pointer onto ram_devclass (bss adjacency verified via nm); the following dmastart bounce bcopy wrote kernel data to the forged pointer -> Fatal trap 12 supervisor write in memmove. chan values are driver-supplied KPI inputs (isa_set_resource never bounds the DRQ); PnP masks are byte-wide so no unpriv in-tree path - privileged/buggy-driver primitive. Unconditional-bounds fix validated on a patched nativekernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5519 Β· 15 files
FileTypeDescriptionSize
df5519.c β€” 1.8 KB view raw
df5519_panic.c β€” 2.6 KB view raw
Makefile.5519 β€” 51 B ↓ download
Makefile.5519p β€” 58 B ↓ download
build.sh β€” 114 B view raw
run.sh β€” 170 B view raw
run.log β€” 456 B view raw
panic.txt β€” 1.3 KB view raw
env.txt β€” 1.4 KB view raw
fix.diff β€” 3.1 KB view raw
fix_run.log β€” 2.7 KB view raw
VERDICT.md β€” 4.0 KB ↓ raw
README.md β€” 2.8 KB ↓ raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 5.0 KB view raw

DF-5519 β€” isa_dma.c: DMA channel validation is DIAGNOSTIC-only β†’ OOB static writes / forged bounce pointer / 1<<chan wrap

File: sys/bus/isa/x86_64/isa_dma.c (pass 2) Verdict: reproduced (see verdict.json, VERDICT.md) Kernel: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (no options DIAGNOSTIC β€” the panics at isa_dma.c:99/141/163/192/218/337 are compiled out)

What was proven on the running production kernel

  1. 1 << chan wraps for chan β‰₯ 32 β€” isa_dma_acquire(34) returns EBUSY while channel 2 is acquired (x86 32-bit shl masks the count to 5 bits); isa_dma_release(34) then clears channel 2's dma_inuse bit, i.e. a bogus channel number frees a live driver's channel.
  2. chan β‰₯ 8 silently accepted β€” isa_dma_acquire(9) returns 0.
  3. OOB write forges another channel's bounce pointer (.bss layout confirmed with nm /boot/kernel/kernel: dma_bouncebufsize[8] at 0xffffffff819fb260, dma_bouncebuf[8] at 0xffffffff819fb280, ram_devclass at 0xffffffff819fb2c0): isa_dma_init(8, 0x1000) writes dma_bouncebufsize[8] onto the low 32 bits of dma_bouncebuf[0], and dma_bouncebuf[8] (an 8-byte kernel heap pointer) onto ram_devclass (a devclass_t). The follow-up isa_dmastart(WRITE, hibuf, 256, chan=0) bounce-path bcopy then writes to the forged pointer β€” captured panic:

fault virtual address = 0xffffffff00001000 <- original ptr high32 | bouncebufsize param (0x1000) fault code = supervisor write data, page not present Stopped at memmove+0x2b: movq %rdx,(%rdi) <- bcopy in isa_dmastart

The fault address contains exactly the attacker-supplied bouncebufsize parameter in its low 32 bits β€” proof that the OOB write, not the legitimate allocation, chose the write destination.

Reachability (honest)

The chan argument is driver-supplied. In-tree callers take it from rman_get_start(res_drq) (fd.c:620, ppc.c:1891); isa_set_resource validates only the rid count, never the DRQ value (isa_common.c:890-902). PnP masks are byte-wide so PnP yields 0..7; kernel hints, PnP-BIOS tables and KLDs are root/loader-controlled. So the demonstrated corruption requires a misconfigured or buggy driver/module β€” the in-tree unprivileged paths on this guest (no fdc/ppc in X86_64_GENERIC) do not reach it. Severity Medium: full kernel-memory-corruption primitive once a chan>7 reaches the API.

Files

  • df5519.c / df5519_panic.c β€” KLD harnesses (clean / fatal)
  • build.sh, run.sh β€” exact build & run
  • run.log β€” clean-run serial output
  • panic.txt β€” forged-pointer wild-write panic
  • env.txt β€” uname, symbol map, config checks
  • fix.diff β€” unconditional chan validation (all 8 entry points)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-5519 VERDICT β€” reproduced

Bottom line: on the stock production kernel (X86_64_GENERIC, DIAGNOSTIC off), every channel-number check in sys/bus/isa/x86_64/isa_dma.c is compiled out. A KLD harness (root context, mirroring what any driver, hint-parsed config or third-party module supplies) demonstrated all three consequences: (a) 1<<34 aliases bit 2 β€” isa_dma_acquire(34) returned EBUSY and isa_dma_release(34) cleared a live channel's dma_inuse bit; (b) isa_dma_acquire(9)/isa_dma_init(9,…) accept chan β‰₯ 8 with no error; (c) the isa_dma_init(8, 0x1000) OOB static writes β€” dma_bouncebufsize[8] (at &dma_bouncebuf[0]-low32 per the .bss layout) and dma_bouncebuf[8] (at &ram_devclass) β€” forged channel 0's bounce pointer, and the subsequent isa_dmastart bounce bcopy wrote kernel data to the forged address 0xffffffff00001000 (panic captured in panic.txt). The fault VA's low 32 bits equal the bouncebufsize parameter β€” the write destination was chosen by the OOB write, not by any allocation.

Why it happens (path:line)

  • isa_dma.c:98-104, 140-143, 162-168, 191-194, 217-227, 336-342 β€” every chan & ~VALID_DMA_MASK check is #ifdef DIAGNOSTIC.
  • isa_dma.c:127-128 (dma_bouncebufsize[chan], dma_bouncebuf[chan]), 242-243, 288 (dmapageport[chan] OOB read β†’ wild outb) β€” unchecked static-array indexing.
  • isa_dma.c:145/149/170/180/225/239/258/344/348/351/435/441/485/488 β€” 1 << chan on u_int8_t masks / int shift: undefined/wrapping for chan β‰₯ 32.
  • isa_dmastatus (isa_dma.c:428) and isa_dmastop (isa_dma.c:483) had no validation at all, not even DIAGNOSTIC.
  • Channel provenance in-tree: fd.c:620 / ppc.c:1891 (rman_get_start of DRQ); isa_common.c:884-908 isa_set_resource never bounds the DRQ value; isavar.h:174-182 exports the KPI to any KLD.

Exploit chain (demonstrated to the wild-write step)

  1. isa_dma_init(0, 1024) β€” legitimate victim channel with real bounce buffer.
  2. isa_dma_init(8, V) β€” OOB write dma_bouncebufsize[8] = V lands on dma_bouncebuf[0]'s low 32 bits (array-adjacency verified via nm); OOB write dma_bouncebuf[8] = <heap ptr> clobbers ram_devclass (devclass_t β€” a type-confusion target for later devclass walks).
  3. isa_dmastart(ISADMA_WRITE, buf, n, 0) with a >16MB buffer β†’ bounce path trusts the forged pointer β†’ bcopy(buf, forged, n) β†’ kernel write to a semi-controlled address (low 32 bits = caller-chosen V; captured supervisor-write panic).
  4. 1<<chan wrap additionally allows freeing/clearing a live channel's state (release(34) cleared chan 2's inuse bit β€” captured).

Full uid0 escalation was not pursued: the trigger is gated on privileged configuration/buggy-driver input (no unprivileged in-tree path supplies chan>7 on this guest β€” no fdc/ppc devices in X86_64_GENERIC; PnP masks are byte-wide so PnP yields 0..7). The primitive (parameter-controlled pointer forgery + 8-byte heap-pointer write onto ram_devclass) is real memory corruption; the honest ceiling is "kernel memory corruption available to whatever context can name a DMA channel" β€” root/loader today, any future driver that forwards user-influenced DRQ values tomorrow.

Fix validation

fix.diff makes the bounds check unconditional in all 8 entry points (EINVAL / early return / -1 as fits each signature; keeps the double-init "impossible request" panic, matching FreeBSD's unconditional checks). Patched-kernel rerun: acquire(9)/acquire(34) β†’ EINVAL with "out of range" on the console, no aliasing, init(8,…) β†’ EINVAL, no forged-pointer panic (fix_run.log). Validated on a nativekernel build of X86_64_GENERIC with fix applied (combined with DF-5520's clamp β€” same file).

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

Patched nativekernel of X86_64_GENERIC (fix.diff applied in guest /usr/src, combined with DF-5520's clamp in the same file): acquire(9)/acquire(34)/init(8) all return EINVAL with 'out of range' on console; release(34) no longer clears chan 2; the forged-pointer bcopy panic is gone (dmastart used the real bounce buffer); guest survives every module. Baseline panics documented in panic.txt.

findings/poc/DF-5519/fix_run.log (boot A + boot B sections); kernel #1 Tue Oct 6 20:57:57 UTC 2026
↓ fix.diffper-fix-DF-5519

Confirmed kernel references

Detail

Exploit chain

isa_dma_init(0,1024) arms victim channel 0 -> isa_dma_init(8,V) OOB write forges dma_bouncebuf[0]=orig_high32:V and clobbers ram_devclass with a heap pointer -> isa_dmastart(WRITE,buf,n,0) bounce-path bcopy writes n controlled bytes to the forged VA (demonstrated: panic at 0xffffffff00001000 with V=0x1000); 1<7 on this guest.

Evidence (decisive lines)

run.log (wrap-alias + chan>=8 acceptance, guest alive); panic.txt (Fatal trap 12, supervisor WRITE, VA 0xffffffff00001000 = original ptr high32 | bouncebufsize param 0x1000, Stopped at memmove+0x2b inside isa_dmastart's bcopy); env.txt (nm layout: &dma_bouncebufsize[8]==&dma_bouncebuf[0], &dma_bouncebuf[8]==&ram_devclass); fix_run.log (patched kernel: EINVAL everywhere, no panic).

PoC changes

Lead was analysis-only; built staged KLD harnesses (clean + fatal), confirmed .bss aliasing with nm before choosing chan=8, added sys/bus.h include for isa_if.h device_t, generated isa_if/bus_if/device_if headers and the dragonfly symlink for out-of-tree bsd.kmod.mk builds, split fatal/non-fatal runs to keep clean evidence.

Verified recommended fix

Make the chan bounds check unconditional in all eight entry points (EINVAL/early-return/-1; keep double-init panic), matching FreeBSD's unconditional checks.

Verdict

On the stock production kernel (X86_64_GENERIC, no DIAGNOSTIC) every DMA channel-number check in isa_dma.c is compiled out. KLD proof: isa_dma_acquire(34) returned EBUSY via 1<<34 wrapping onto bit 2 and isa_dma_release(34) then cleared the live channel-2 inuse bit; isa_dma_acquire(9) accepted chan>=8; isa_dma_init(8,0x1000) performed the OOB static writes (dma_bouncebufsize[8] onto dma_bouncebuf[0]'s low 32 bits, dma_bouncebuf[8] heap pointer onto ram_devclass) forging channel 0's bounce pointer, and the following isa_dmastart bounce bcopy wrote kernel data to the forged address 0xffffffff00001000 (fault VA low half == the bouncebufsize parameter) -- Fatal trap 12 supervisor write, captured. Reachability is driver/misconfig-gated (chan from rman DRQ start; isa_set_resource never bounds it; PnP masks are byte-wide so in-tree PnP yields 0..7; hints/KLD are privileged), so no unprivileged in-tree trigger exists on this guest (no fdc/ppc devices) -- but the primitive is full parameter-steered kernel memory corruption for any context that can name a DMA channel.