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

isa_dmadone() bounce-buffer readback has no size clamp (asymmetric with isa_dmastart) and release keeps stale dma_bounced: unbounded OOB kernel read β€” deterministic panic, info leak into driver buffers

Summary

dmastart validates nbytes against the bounce size before copying IN, but dmadone bcopy's the caller-supplied nbytes OUT with no check - and release deliberately leaves stale dma_bounced armed, so a release/re-acquire without dmadone poisons the next readback. Guest-proven: nbytes=0x800 on a 64B bounce silently copied 0x800 bytes OOB (in-page leak); 0x11000/0x20000 walked past the mapped arena -> Fatal trap 12 supervisor read in memmove from isa_dmadone, 2/2 boots. In-tree callers pass paired sizes (latent contract slip). Clamp-and-clear fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5520 Β· 15 files
FileTypeDescriptionSize
df5520.c β€” 3.7 KB view raw
Makefile.5520 β€” 51 B ↓ download
build.sh β€” 131 B view raw
run.sh β€” 170 B view raw
run.log β€” 1.5 KB view raw
run.2.log β€” 854 B view raw
panic.txt β€” 918 B view raw
leak_sample.txt β€” 1.3 KB view raw
env.txt β€” 1.4 KB view raw
fix.diff β€” 1.2 KB view raw
fix_run.log β€” 2.7 KB view raw
VERDICT.md β€” 2.5 KB ↓ raw
README.md β€” 1.9 KB ↓ raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 4.4 KB view raw

DF-5520 β€” isa_dmadone() copies nbytes out of the bounce buffer with no clamp β†’ unbounded OOB kernel read (panic / info leak into driver buffers)

File: sys/bus/isa/x86_64/isa_dma.c (pass 2) Verdict: reproduced (see verdict.json, VERDICT.md)

isa_dmastart() validates the transfer size against the channel's bounce buffer before copying into it (isa_dma.c:241-244), but isa_dmadone() (isa_dma.c:351-357) bcopys the caller-supplied nbytes out of the bounce buffer with no check at all. A driver that passes a done-size larger than the started size β€” or that reaches dmadone with the stale dma_bounced bit that isa_dma_release() deliberately does not clear (the XXX comment at isa_dma.c:170-178) β€” gets an out-of-bounds read of the low-memory DMA arena delivered into its buffer.

Demonstrated on the stock kernel

  • Silent in-page read (df5520.ko, nbytes=0x800 out of a 64-byte bounce): 0x800 bytes copied from beyond the chunk into the module's buffer (run.log, leak_sample.txt); guest survives. On this boot the neighboring page was fresh zeros β€” with any prior use of that low-memory page the same call returns those contents.
  • Fatal walk-off (DONE_NBYTES=0x11000 and 0x20000): the same bcopy runs past the mapped low-arena window β€” deterministic Fatal trap 12, supervisor read data in memmove called from isa_dmadone, fault VA 0xffffffff82604000, 2/2 boots (panic.txt, run.2.log).

In-tree callers (fd.c, ppc.c) currently pass matching sizes, so this is an API-contract/hardening defect (any future or third-party driver mistake turns it into a panic or leak), not a directly unprivileged-reachable bug.

Fix

fix.diff clamps the readback to dma_bouncebufsize[chan] (with a console warning) and clears the stale dma_bounced bit in isa_dma_release(). Patched-kernel rerun: clamp message printed, no data beyond 64 bytes copied, no panic (fix_run.log).

VERDICT.md
↓ download raw

DF-5520 VERDICT β€” reproduced

Bottom line: isa_dmadone() trusts its nbytes argument when copying DMA results back out of the bounce buffer. KLD harness (root context, free channel 1, 64-byte bounce installed via isa_dma_init) called isa_dmastart(READ, 64, 1) then isa_dmadone(READ, out, N, 1):

  • N=0x800: 0x800 bytes copied from beyond the 64-byte chunk (in-page, silent) β€” run.log + leak_sample.txt.
  • N=0x11000 and N=0x20000: bcopy walked past the mapped low-arena window β†’ Fatal trap 12 (supervisor read) in memmove from isa_dmadone, fault VA 0xffffffff82604000, reproducible on two independent boots β€” panic.txt, run.2.log.

Root cause (path:line)

  • sys/bus/isa/x86_64/isa_dma.c:351-357 β€” bcopy(dma_bouncebuf[chan], addr, nbytes) with no clamp against dma_bouncebufsize[chan]; asymmetric with the strict start-side check at isa_dma.c:241-244.
  • sys/bus/isa/x86_64/isa_dma.c:170-178 β€” isa_dma_release() clears dma_busy but deliberately leaves dma_bounced set (in-code XXX admits it); a release/re-acquire without dmadone arms the stale-bounce copy for the next transfer's dmadone.
  • In-tree callers keep sizes paired (fd.c:1844-1864, 1918-1921, 1955-1958, 1995-1998, 2011-2013; ppc.c:1594-1608, 1697-1727), so the bug is latent β€” an API contract violation away from a panic/leak, including via the stale dma_bounced bit.

Impact

Demonstrated: kernel panic (unbounded read, 2/2 deterministic) and a silent OOB read of the ISA low-memory DMA arena into a driver buffer (contents on this guest: fresh-page zeros; the primitive returns whatever previously occupied that low memory β€” other drivers' DMA buffers / bus_dma low-address pools). Gated on a driver contract slip; no unprivileged in-tree trigger. Low severity / certain confidence.

Fix validation

fix.diff (applies alone or stacked with DF-5519's diff β€” same file): - dmadone clamps to the installed bounce size and warns; - release clears the stale dma_bounced bit. Patched nativekernel rerun: isa_dmadone: count 2048 exceeds bounce buffer size 64, clamping printed, outbuf stays zero past byte 64, guest alive with the fatal-size variant too β€” no panic (fix_run.log).

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 (fix.diff applied in guest /usr/src, stacked with DF-5519's validation in the same file): the 0x800 run printed 'isa_dmadone: count 2048 exceeds bounce buffer size 64, clamping' with nothing copied past the bounce buffer, and the previously-fatal 0x11000 run printed the clamp (69632 -> 64) with NO panic and the guest stayed up.

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

Confirmed kernel references

Detail

Exploit chain

isa_dma_init(1,64) -> isa_dmastart(READ,64,1) arms dma_bounced -> isa_dmadone(READ,out,N,1) with N>>64 performs the unclamped bcopy: within the mapped page it silently discloses neighboring low-memory DMA-arena contents into the driver buffer; across the mapped window edge it faults the kernel (captured). Gated on a driver contract slip (size mismatch or missing dmadone before release); no unprivileged in-tree trigger.

Evidence (decisive lines)

run.log (0x800 silent run with leak dump + primed 0xE7 residue); panic.txt (0x11000: Fatal trap 12, supervisor read, VA 0xffffffff82604000, Stopped at memmove+0x10a from isa_dmadone); run.2.log (0x20000: identical fault address, second boot); leak_sample.txt; fix_run.log ('isa_dmadone: count 69632 exceeds bounce buffer size 64, clamping' -- no panic, guest alive).

PoC changes

Seeded concept only; wrote the harness, discovered the bounce chunk sits at the mapped low-arena edge (0x11000/0x20000 both fault at 0xffffffff82604000) so the fatal size doubles as determinism evidence, added an in-page 0x800 silent variant with a freed-chunk residue primer, added sys/bus.h include + generated isa_if/bus_if/device_if headers.

Verified recommended fix

Clamp isa_dmadone's bounce readback to dma_bouncebufsize[chan] (warn on clamp) and clear the stale dma_bounced bit in isa_dma_release().

Verdict

isa_dmadone() (isa_dma.c:351-357) bcopy()s the caller-supplied nbytes out of the channel's bounce buffer without any clamp against dma_bouncebufsize[chan] -- the mirror of the strict start-side check at isa_dma.c:241-244. KLD proof on the stock kernel with a 64-byte bounce on channel 1: nbytes=0x800 copied 0x800 bytes from beyond the chunk into the module's buffer (silent in-page OOB read; guest alive); nbytes=0x11000 and 0x20000 walked past the mapped low-arena window -> Fatal trap 12 supervisor READ in memmove called from isa_dmadone at 0xffffffff82604000, deterministic across independent boots (2/2). isa_dma_release() also deliberately leaves the stale dma_bounced bit set (XXX comment isa_dma.c:170-178), which arms the same unclamped copy for a later transfer's dmadone after a release/re-acquire without dmadone. In-tree callers (fd.c, ppc.c) pass paired sizes today, so this is a latent API-contract defect: any driver size mismatch or lifecycle slip becomes a kernel panic or an info leak of low-memory DMA-arena contents into a driver buffer. Low severity, certain confidence.