# DF-5223 VERDICT — rv770_copy_dma DMA-ring under-reservation

## Bottom line

**not_reproduced at runtime (hardware-gated: no R700-class GPU in the audit guest);
deficit arithmetic proven by a guest-run simulator that models the kernel ring
accounting 1:1.** The bug is real in the code, user-reachable on real HD-4000
hardware, and its mechanics are fully deterministic.

## Root cause

`rv770_copy_dma()` reserves `num_loops * 5 + 8` dwords on the R600 DMA ring
(rv770_dma.c:59) but three things consume that ring space inside the reservation:

1. **Semaphore waits emitted by `radeon_sync_rings()`**: 3 dwords each
   (radeon_sync.c:187 → `r600_dma_semaphore_ring_emit`, r600_dma.c:319-321).
   `radeon_sync_rings()` explicitly documents *"we assume caller has already
   allocated space on waiters ring"* (radeon_sync.c:186) — the caller did not.
   On RV770 the sync sources are the GFX and UVD rings (same-ring fences are
   excluded, radeon_fence.c:768-770; `RADEON_NUM_SYNCS`=4, radeon.h:164), so up
   to **2** wait packets (6 dwords) can land on the DMA ring.
2. **Copy packets**: exactly `num_loops * 5` dwords (rv770_dma.c:74-78) — correctly budgeted.
3. **Fence emit**: 5 dwords (`r600_dma_fence_ring_emit`, r600_dma.c:292-297).

`+8` covers the 5-dword fence + exactly ONE 3-dword semaphore. With **two**
pending cross-ring fences the emission runs past the reserved window, and
`radeon_ring_write()` (radeon.h:2710-2716) only prints `DRM_ERROR` and writes
anyway (`wptr &= ptr_mask` — masked in the ring array, so **no kernel-host heap
OOB**; DF-1943 correctly noted that for the r600 twin). After commit padding
(radeon_ring.c:173, `align_mask` = 16-1 for the DMA ring, r600.c:2798) the total
overrun is **exactly 16 dwords** for every `num_loops ≡ 8, 11, 14 (mod 16)`
(i.e. copies of ~1.8–2.1 MB, ~2.5–2.9 MB, ~3.2–3.7 MB, …).

## Threat / reachability

- `rv770_copy_dma` is the **primary TTM copy callback** for RV770/RV730/RV710/
  RV740 (`.copy = &rv770_copy_dma`, `.copy_ring_index = R600_RING_TYPE_DMA_INDEX`,
  radeon_asic.c:1253-1256) — no boot tunable needed (unlike the r600 twin
  DF-1943, which is radeon_testing-gated because R600 uses `r600_copy_cpdda` as
  `.copy`).
- Constructing S=2 (two cross-ring fences on one bo resv) needs no privilege:
  read-only relocations get **shared** per-ring fences (`tv.shared = !write_domain`,
  radeon_cs.c:182; added via `ttm_eu_fence_buffer_objects`, ttm_execbuf_util.c:203-206),
  so a bo read by a GFX CS and a UVD decode job, then evicted under VRAM pressure
  by an unprivileged render-node client, carries unsignaled fences on two rings.
  `radeon_sync_resv()` picks up excl + all shared fences (radeon_sync.c:101-123).
- Impact when the ring is tight (free space at the guaranteed minimum,
  reserved+1): the 16 overrun dwords advance `wptr` past `rptr`, overwriting
  **un-executed, previously committed GPU commands** with fence-address/seq/nop
  dwords; the GPU then re-parses mid-packet garbage (attacker-influenced 32-bit
  GPU addresses as packet headers) → cross-context GPU memory corruption /
  lockup. When the ring has slack, the overrun is latent (accounting only).

## Verification performed

1. **Guest hardware proof** (`env.txt`): `pciconf -lv` shows only the QEMU
   std-VGA (`vgapci0@pci0:0:2:0 chip=0x11111234`) — no AMD/ATI device;
   `kldload radeondrm` → `No such file or directory` (module not even built on
   the guest kernel). The buggy path cannot execute on this guest — runtime
   reproduction impossible, honestly classified `not_reproduced`.
2. **Arithmetic proof on the guest** (`sim_accounting.c`, `run.log`): models
   `radeon_ring_alloc` align-up (:114), `count_dw` decrement + DRM_ERROR-then-
   write (:2710-2716), and commit nop padding (:173) with the exact rv770
   consumers. Output: 12 overflowing `(S=2, num_loops)` rows in 1..64, each
   exactly 16 dwords past the reserved window; S=0/S=1 never overflow.
3. **Fix applicability**: `git apply --check` passes against the pristine tree
   (`fix.diff`); fix mirrors the IB-path accounting (radeon_ib.c:135 reserves
   `64 + RADEON_NUM_SYNCS * 8`) and the family precedent (DF-1943 recommended
   `num_loops*4 + RADEON_NUM_SYNCS*3 + 8` for r600).

## Kill list (checked, not bugs)

- 32-bit `(num_gpu_pages << 12)` truncation — **DF-2214**, not re-reported.
- Ignored `radeon_sync_resv`/`radeon_sync_rings` returns (rv770_dma.c:66-67) —
  **DF-5131** explicitly enumerates rv770_dma.c among its 7 sites; not re-reported.
- `num_loops*5+8` int overflow: max `0x4000*5+8 = 81928` < INT_MAX; oversized
  requests rejected by `radeon_ring_alloc` (ndw > ring_size/4 → -ENOMEM, radeon_ring.c:112-113).
- Remainder math: sizes are 4096-byte multiples; per-loop 0xFFFF clamp + u32
  subtraction is exact; final `size_in_dw` reaches 0.
- `DMA_PACKET` count field: `n & 0xFFFF` (rv770d.h:663-666); clamp 0xFFFF fits;
  40-bit addressing via `& 0xfffffffc` + `upper_32_bits & 0xff` consistent.
- `radeon_fence_emit` cannot fail on DragonFly (`kmalloc(..., M_WAITOK)`,
  radeon_fence.c:135 ⇒ the `*fence == NULL` error path is dead code), so the
  `unlock_undo` + `radeon_sync_free(NULL)` error path (including the
  committed-signal-packet/semaphore-sa_bo-free adjunct) is unreachable here.
- Lock protocol balanced on both live paths (lock→commit / lock→undo).

## Family status

`r600_copy_dma` (+8, 4-dw packets, admin-gated on R600) = **DF-1943** (Low);
`evergreen_copy_dma` (+11 = 8-dw fence + 1 sem) and `si_copy_dma` (+11) share
the same latent shortfall for S≥2 (flagged in DF-5131 poc notes as unfiled);
this filing is the **user-reachable rv770 instance**. `cik_sdma` (+14, 2-dw
sem) has 2 dwords of slack beyond one semaphore — still short for S≥3.

## 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.
