# 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)`:

1. `map->callback_arg` = `&cba` → `ata_dmasetprd()` (`ata-dma.c:199-216`)
   reads `args->dmatab` from the **dead stack frame** and writes up to
   8×nsegs bytes of PRD entries wherever that stale word points, plus
   4-byte `args->error`/`args->nsegs` writes into the dead frame. Whoever
   owns that thread-stack memory at fire time governs the write target.
2. `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_setprd` fires **in SWI context (`current process = Idle`)** reading
  `args->dmatab` from 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 exact `prd[i].addr = htole32(...)` write of `ata-dma.c:210`.
- **DEMO B (freed buffer)** — reproduced: victim buffer `kfree`d 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/cd0` completed 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_iosize` to 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_load` enforces
  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->dma` guarded by callers
  (ata-pci.c:796-800, 817).
- alignment/zero-length/oversize checks in dmaload — present; `count`
  signedness 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.
