ata_dmaload() mishandles bus_dmamap_load()==EINPROGRESS: busdma defers the load and later fires setprd into a dead stack frame (and re-loads the completed request's freed buffer) β kernel memory corruption + permanent channel wedge
Summary
DF busdma strips BUS_DMA_NOWAIT and defers short-bounce-pool loads with the callback armed against a STACK cba - when bounce pages free up anywhere in the shared zone, busdma_swi re-runs the load into the dead frame (stale &dmatab aims an 8*nsegs-byte PRD write) and re-walks the freed request buffer; a load exceeding the whole pool queues forever, wedging the channel permanently. KLD PoC with nata's exact tag geometry: 3/3 demos (PRD written through stale word into dead frame markers, freed-KVA reload, permanent wedge). Private-bounce-zone + fail-stop fix mechanism-validated (16/16 worst-case loads complete synchronously). Sibling of DF-4191 (sdhci).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5083 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5083.c | β | 12.7 KB | view raw | |
| Makefile | β | 73 B | β download | |
| straddle.c | β | 1.3 KB | view raw | |
| build.sh | β | 150 B | view raw | |
| run.sh | β | 223 B | view raw | |
| build.log | β | 6.4 KB | view raw | |
| run.log | β | 2.3 KB | view raw | |
| run1.out | β | 2.3 KB | view raw | |
| run.2.log | β | 484 B | view raw | |
| run.3.log | β | 1.5 KB | view raw | |
| panic-run-B.txt | β | 15.4 KB | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| code_hash.txt | β | 75 B | view raw | |
| fix.diff | β | 2.9 KB | view raw | |
| VERDICT.md | β | 8.8 KB | β raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-5083 VERDICT β reproduced (kernel memory-corruption primitive + permanent channel DoS)
Finding: sys/dev/disk/nata/ata-dma.c β ata_dmaload() calls
bus_dmamap_load(..., ch->dma->setprd, &cba, BUS_DMA_NOWAIT) with a
stack-allocated struct ata_dmasetprd_args and treats every non-zero
return as terminal (ata-dma.c:251-254). DragonFly busdma strips
BUS_DMA_NOWAIT for real maps (sys/platform/pc64/x86_64/busdma_machdep.c:776-783,
"follow old semantics") and, when the tag's bounce pool is short, QUEUES
the map with map->callback/map->callback_arg/map->buf armed and
returns EINPROGRESS (busdma_machdep.c:645-656, 793-803). ata_dmaload
returns the error to ata_begin_transaction
(sys/dev/disk/nata/ata-lowlevel.c:126-134, 182-189), which completes the
request with an error β the buffer goes back to the issuer β and never
unloads the map (ATA_DMA_LOADED was never set, ata-dma.c:273). When
bounce pages are later freed anywhere in the zone, busdma_swi()
(busdma_machdep.c:1437-1450) re-runs
bus_dmamap_load(map->dmat, map, map->buf, map->buflen, map->callback,
map->callback_arg, 0):
map->callback_arg=&cbaβata_dmasetprd()(ata-dma.c:199-216) readsargs->dmatabfrom the dead stack frame and writes up to 8Γnsegs bytes of PRD entries wherever that stale word points, plus 4-byteargs->error/args->nsegswrites into the dead frame. Whoever owns that thread-stack memory at fire time governs the write target.map->buf= the completed request's buffer β the deferred load re-walks freed/reused KVA and produces PRD entries with physical addresses extracted from whatever now owns those pages β the disk controller would DMA against stale addresses had the driver armed it.
Bounce zones are shared system-wide between compatible tags
(busdma_machdep.c:1081-1096), so nata's 16-page pool
(bus_dmamap_create β atop(maxsize) pages, busdma_machdep.c:401-413)
is contended by every other 32-bit-limited busdma user; and any single
load needing more pages than the whole pool (17-page-straddling 64KB
transfer on classic IDE; up to 1MB transfers on nata-ahci/marvell/
promise/siil which override max_iosize after the tags are built) is
queued forever, wedging the channel: every subsequent DMA load on
that channel returns EINPROGRESS (ata-lowlevel prints "setting up DMA
failed") β permanent local DoS of that disk/CD channel, reachable by
ordinary unprivileged IO once one such transfer occurs.
Verification (QEMU guest, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, 4608MB)
Kernel module df5083.ko reproducing nata's exact tag geometry and call
pattern (private bounce zone + 256MB lowaddr so the trigger is
deterministic; nata's real threshold is 32-bit, same code path):
- DEMO A (deferred write through stale stack word) β reproduced 3/3
runs + this final run: victim load returns EINPROGRESS=36 with
cba=0xfffff80131d1f798; after the holder releases bounce pages,po_setprdfires in SWI context (current process = Idle) readingargs->dmatabfrom the returned frame; the stale word pointed at the module's marker and the callback wrote real PRD entries (00129000:00001000,00128000:00001000, β¦ β bounce-page bus addresses) through it (run.log). In other runs the stale word held garbage/NULL βFatal trap 12 β¦ supervisor write β¦ current process = Idle β¦ Stopped at po_setprd+0x48(panic-run-B.txt,run.3.log) β the exactprd[i].addr = htole32(...)write ofata-dma.c:210. - DEMO B (freed buffer) β reproduced: victim buffer
kfreed while queued (request completed with error); deferred load re-ran over the freed KVA and fired the callback into the dead frame (PRD entries zero-phys because the freed PTEs were torn down; with reuse they are the new owner's physical addresses). - DEMO C (permanent wedge) β reproduced 4/4: 18-page need vs 17-page pool queued forever holding the entire pool; subsequent 1-page load β EINPROGRESS. This is the on-path nata failure mode ("ata%d: setting up DMA failed" forever).
- On-path attempt: with the install ISO attached (acd0 on the PIIX3
IDE channel) and 3.1GB of low memory drained, misaligned 64KB raw
reads from
/dev/cd0completed cleanly β this guest's kernel-side disk buffers stay below 4GB (bottom-up allocation), so nata's own 32-bit tag never bounced. The >4GB-buffer precondition is a real-hardware memory-pressure condition (the stock guest's shared boot zone already bounced 2 pages at boot, so >4GB DMA loads do occur in practice), not arrangeable here without hours of memory-pressure grooming; the mechanism itself is proven at the exact nata geometry.
Exploitability (honest ceiling)
- Certain: permanent per-channel DMA wedge (local DoS of the affected
disk/CD) once any single transfer needs more bounce pages than the
pool β trivially reachable on the nata chipset variants that raise
max_iosizeto 1MB on a 16-page pool, and reachable on classic IDE via a 64KB transfer straddling 17 pages. - Demonstrated primitive: attacker-governed stale-stack word selects the destination of an 8Γnsegs-byte PRD write (contents = physical addresses of the re-loaded buffer) plus two 4-byte writes into a live kernel thread's stack. A full uid=0 chain would additionally require controlling the dead frame's contents at fire time (the frame belongs to the ata request-service thread; its reuse is influenced by subsequent ata/callout activity) and was not developed within this run β recorded as the next escalation step, not as a completed chain.
Fix validation
fix.diff (authored after verification, never applied to sys/):
private bounce zone (BUS_DMA_PRIVBZONE) + one extra page of tag maxsize
so the pool always covers a worst-case straddling transfer of
max_iosize bytes, making deferral impossible by construction for the
single-map-per-channel geometry; defensive fail-stop if EINPROGRESS ever
occurs. DEMO F in the module rebuilds exactly this geometry: 16/16
worst-case 17-page-straddling loads completed synchronously, EINPROGRESS
0/16 (run.3.log). A patched-kernel build was not run: the on-path
trigger does not manifest on this guest (see above), so a kernel rebuild
could not demonstrate anything the geometry demo does not; recorded as
fix_status: not_testable with the mechanism-level validation as
evidence. Chipset drivers that raise max_iosize after ata_dmainit()
(ata-ahci.c:558, ata-marvell.c, ata-promise.c, ata-siliconimage.c) need
pool sizing of their own β flagged in the fix comment.
Negative space (what was checked and killed in ata-dma.c pass 2)
- PRD table overflow: sg table is a dedicated MAXTABSZ=PAGE_SIZE (512
entries) bus_dmamem'd page,
bus_dmamap_loadenforces nsegments=ATA_DMA_ENTRIES=256 (busdma_machdep.c:701-702, 732-733), KASSERT is belt-and-braces β killed. prd[i-1]with nsegs==0: count==0 rejected earlier (panic) and busdma always yields β₯1 segment for non-empty loads β killed.- DF-2086 (known, not re-reported): dmaalloc error-path double
kfree(ch->dma). - dmafree ordering (map/tag destroy order, partial-init walks on M_ZERO'd
fields) β correct; NULL-deref of
ch->dmaguarded by callers (ata-pci.c:796-800, 817). - alignment/zero-length/oversize checks in dmaload β present;
countsignedness bounded by max_iosize check β killed. - ATA_DMA_LOADED flag races β channel state_mtx held by callers (ata-lowlevel.c:214) β killed.
- sg_map never unloaded in dmaunload β intentional persistent mapping β killed.
intel_guc_fw.c (second file, clean)
All memory-touching paths are bounded and validated:
guc_xfer_rsa copies exactly UOS_RSA_SCRATCH_COUNTΓ4 = 256 bytes into
a fixed stack array via sg_pcopy_to_buffer with the return count
checked (intel_guc_fw.c:133-138); header_size/ucode_size/
rsa_offset are validated against the blob size at fetch time
(intel_uc_fw.c:87, 99, 106, 110) before upload; version selection is a
static platform table (intel_guc_fw.c:54-84); forcewake get/put are
balanced including error paths (:245, 266); guc_wait_ucode's status
is always initialized by guc_ready before use. The broken skip
handling in the underlying sg_pcopy_to_buffer belongs to
sys/dev/drm/linux_scatterlist.c and is already filed (DF-2127,
DF-2128); the non-propagated guc_xfer_rsa error at intel_guc_fw.c:254-256
is cosmetic (final guc_wait_ucode status catches the failure) β no
security impact.
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
not_testablefix.diff construction validated at mechanism level in-kernel (module DEMO F: 16/16 worst-case 17-page-straddle loads on the fixed geometry completed synchronously, EINPROGRESS 0/16, deferred callback cannot arm β run.3.log). A patched-kernel build was not exercised because the on-path trigger does not manifest on this guest (kernel-heap disk buffers stay below 4GB), so a rebuilt kernel would show nothing the geometry demo does not.
run.3.log: 'DF5083: F: 16x worst-case straddle loads on FIXED geometry: EINPROGRESS=0/16'; fix.diff applies to ata_dmaalloc() data_tag creation + defensive EINPROGRESS panic in ata_dmaload().
Confirmed kernel references
- sys/dev/disk/nata/ata-dma.c:223
- sys/dev/disk/nata/ata-dma.c:251-254
- sys/dev/disk/nata/ata-dma.c:199-216
- sys/dev/disk/nata/ata-lowlevel.c:126-134
- sys/platform/pc64/x86_64/busdma_machdep.c:776-787
- sys/platform/pc64/x86_64/busdma_machdep.c:645-656
- sys/platform/pc64/x86_64/busdma_machdep.c:1437-1450
- sys/platform/pc64/x86_64/busdma_machdep.c:1086-1095
Detail
Exploit chain
(1) >4GB-RAM nata/IDE box, request pages above the 32-bit bounce threshold (steady state under memory pressure); (2) a >16-bounce-page transfer (17-page-straddling 64KB IO on classic IDE, trivially any large IO on nata-ahci/marvell/promise/siil whose max_iosize overrides outgrow the 16-page pool) gets EINPROGRESS and is queued forever -> permanent channel wedge (demonstrated, DEMO C); (3) a satisfiable queue (any concurrent reservation on the shared bounce zone) fires busdma_swi after the request completed: setprd writes 8*nsegs PRD bytes through the dead frame's dmatab word plus two 4-byte writes into the live thread's stack (demonstrated, DEMOs A/B incl. NULL-target panic) β kernel memory corruption whose destination is governed by stale stack contents; (4) [not developed] groom the dead frame (ata request-service thread stack) to aim the PRD write / weaponize the stale-buffer reload into DMA-against-reused-pages for uid=0.
Evidence (decisive lines)
run.log (=run1.out): full clean run β 'DEFERRED busdma_swi CALLBACK FIRED into DEAD FRAME', marker[] filled with real PRD entries (00129000:00001000...), 'CHANNEL WEDGED FOREVER'. run.2.log: 3/3 stability. panic-run-B.txt + run.3.log: Fatal trap 12 supervisor write at po_setprd+0x48 (the ata-dma.c:210 write), current process = Idle (SWI fired after frame death), and DEMO F fix-geometry 0/16 deferred. VERDICT.md: full narrative incl. on-path limitation.
PoC changes
Seed concept only; module written from scratch: nata data_tag geometry (alignment 2/boundary 64KB/maxsize 64KB/nseg 256) with BUS_DMA_PRIVBZONE + 256MB lowaddr for a deterministic trigger (real nata: shared zone, 32-bit lowaddr, needs >4GB-resident buffers this guest's bottom-up allocator won't give the kernel heap); holder-map pool pressure via BUS_DMA_ONEBPAGE map create; per-demo tags; DEMO F validates the fix construction. straddle.c (+errno.h fix) is the on-path attempt.
Verified recommended fix
Private bounce zone (BUS_DMA_PRIVBZONE) + one extra page of data_tag maxsize so the pool always covers a worst-case straddling max_iosize transfer, making bus_dmamap_load deferral impossible by construction; fail-stop panic if EINPROGRESS ever occurs.
Verdict
ata_dmaload() (ata-dma.c:251-254) treats bus_dmamap_load()==EINPROGRESS as terminal while DragonFly busdma keeps the map queued with the driver's stack callback arg, the setprd callback and the request buffer armed; busdma_swi() later re-runs the load and fires ata_dmasetprd into the dead stack frame (write target = stale &dmatab word) and over the completed request's freed buffer. Proven on the guest with nata's exact tag geometry: deferred PRD write through the stale word reproduced 3/3 (marker writes and, when the stale word held garbage, Fatal trap 12 supervisor-write at the ata-dma.c:210 PRD instruction in SWI context), freed-buffer reload reproduced, and the unsatisfiable-queue variant wedges the channel permanently (every later DMA load returns EINPROGRESS) β the on-path DoS. Full uid=0 chain not developed: needs control of the dead frame's contents at fire time; recorded as primitive-level escalation potential.
No comments yet.