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)
PoC verification
Evidence pack
findings/poc/DF-5519 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 << chanwraps for chan β₯ 32 βisa_dma_acquire(34)returnsEBUSYwhile channel 2 is acquired (x86 32-bitshlmasks the count to 5 bits);isa_dma_release(34)then clears channel 2'sdma_inusebit, i.e. a bogus channel number frees a live driver's channel.- chan β₯ 8 silently accepted β
isa_dma_acquire(9)returns 0. - OOB write forges another channel's bounce pointer (
.bsslayout confirmed withnm /boot/kernel/kernel:dma_bouncebufsize[8]at 0xffffffff819fb260,dma_bouncebuf[8]at 0xffffffff819fb280,ram_devclassat 0xffffffff819fb2c0):isa_dma_init(8, 0x1000)writesdma_bouncebufsize[8]onto the low 32 bits ofdma_bouncebuf[0], anddma_bouncebuf[8](an 8-byte kernel heap pointer) ontoram_devclass(adevclass_t). The follow-upisa_dmastart(WRITE, hibuf, 256, chan=0)bounce-pathbcopythen 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 & runrun.logβ clean-run serial outputpanic.txtβ forged-pointer wild-write panicenv.txtβ uname, symbol map, config checksfix.diffβ unconditional chan validation (all 8 entry points)VERDICT.md,manifest.json,verdict.json
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_MASKcheck is#ifdef DIAGNOSTIC. - isa_dma.c:127-128 (
dma_bouncebufsize[chan],dma_bouncebuf[chan]), 242-243, 288 (dmapageport[chan]OOB read β wildoutb) β unchecked static-array indexing. - isa_dma.c:145/149/170/180/225/239/258/344/348/351/435/441/485/488 β
1 << chanonu_int8_tmasks / 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_startof DRQ); isa_common.c:884-908isa_set_resourcenever bounds the DRQ value; isavar.h:174-182 exports the KPI to any KLD.
Exploit chain (demonstrated to the wild-write step)
isa_dma_init(0, 1024)β legitimate victim channel with real bounce buffer.isa_dma_init(8, V)β OOB writedma_bouncebufsize[8] = Vlands ondma_bouncebuf[0]'s low 32 bits (array-adjacency verified vianm); OOB writedma_bouncebuf[8] = <heap ptr>clobbersram_devclass(devclass_t β a type-confusion target for later devclass walks).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-chosenV; captured supervisor-write panic).1<<chanwrap 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
fixedPatched 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
Confirmed kernel references
- sys/bus/isa/x86_64/isa_dma.c:98-104
- sys/bus/isa/x86_64/isa_dma.c:127-128
- sys/bus/isa/x86_64/isa_dma.c:140-150
- sys/bus/isa/x86_64/isa_dma.c:162-181
- sys/bus/isa/x86_64/isa_dma.c:191-194
- sys/bus/isa/x86_64/isa_dma.c:217-227
- sys/bus/isa/x86_64/isa_dma.c:239-252
- sys/bus/isa/x86_64/isa_dma.c:288
- sys/bus/isa/x86_64/isa_dma.c:321
- sys/bus/isa/x86_64/isa_dma.c:336-342
- sys/bus/isa/x86_64/isa_dma.c:428-445
- sys/bus/isa/x86_64/isa_dma.c:483-492
- sys/bus/isa/isavar.h:174-182
- sys/dev/disk/fd/fd.c:620
- sys/dev/misc/ppc/ppc.c:1891
- sys/bus/isa/isa_common.c:884-908
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<
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.
No comments yet.