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)
PoC verification
Evidence pack
findings/poc/DF-5520 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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=0x11000and0x20000): the same bcopy runs past the mapped low-arena window β deterministicFatal trap 12, supervisor read datainmemmovecalled fromisa_dmadone, fault VA0xffffffff82604000, 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).
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 againstdma_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()clearsdma_busybut deliberately leavesdma_bouncedset (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
fixedPatched 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
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.
No comments yet.