rv770_copy_dma under-reserves the DMA ring for radeon_sync_rings() semaphore waits: with two pending cross-ring fences, 16 dwords are written past the reserved window, overwriting un-executed GPU commands (user-reachable rv770 twin of DF-1943)
Summary
Reservation num_loops*5+8 omits the up-to-two 3-dword DMA_PACKET_SEMAPHORE waits radeon_sync_rings() emits on the SAME ring (radeon_ib.c:135 reserves 64+RADEON_NUM_SYNCS*8 for the identical contract); fence+nop-align then overrun exactly 16 dwords for num_loops mod 16 in {8,11,14} with S=2 (~1.8-3.7MB copies). radeon_ring_write DRM_ERRORs and writes anyway -> wptr overtakes rptr, GPU re-parses attacker-influenced garbage as packets -> cross-context VRAM corruption/hang. Primary TTM .copy callback (no tunable needed, unlike DF-1943 r600 twin); S=2 constructible unpriv via shared read-only relocs (GFX+UVD). Guest simulator (1:1, guest-run cc 8.3) prints all 12 overflowing combos; hardware-gated (no HD4000 in QEMU, radeondrm not built). Ring-reservation accounting fix mirrors radeon_ib.c:135.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5223 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sim_accounting.c | β | 4.5 KB | view raw | |
| build.sh | β | 123 B | view raw | |
| run.sh | β | 153 B | view raw | |
| build.log | β | 9 B | view raw | |
| run.log | β | 2.0 KB | view raw | |
| env.txt | β | 387 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| README.md | β | 1.7 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-5223 β rv770_copy_dma under-reserves DMA-ring space for multi-ring semaphore waits (GPU command-stream corruption)
- File:
sys/dev/drm/radeon/rv770_dma.c:59(Radeon RV770/RV730/RV710/RV740 β HD 4000 series,radeon_asic.c:2412-2416) - Class: incorrect buffer-size calculation (CWE-131) β GPU-visible ring-buffer overwrite (un-executed command corruption)
- Hardware-gated: requires an R700-class Radeon GPU (DMA copy engine + GFX/UVD rings). NOT reproducible on the QEMU std-VGA audit guest.
Build & run (inside the DragonFly guest)
sh build.sh # cc -O2 -Wall -o /tmp/sim_accounting sim_accounting.c sh run.sh # prints every (S, num_loops) combination that overflows the reservation
Expected output (decisive lines)
DEFICIT: S=2 num_loops=8 reserved=48 consumed=64 overflow=16 dwords past reserved window ... DEFICIT: S=2 num_loops=11 reserved=64 consumed=80 overflow=16 dwords past reserved window ... ... (every num_loops β‘ 8, 11, 14 mod 16; S=0 and S=1 never overflow) DEFICIT CONFIRMED (static arithmetic; hardware-gated runtime) exit=1
The simulator is a 1:1 arithmetic model of radeon_ring_alloc/radeon_ring_write/
radeon_ring_commit (paths cited in the source header) β it is not the kernel
path itself; the kernel path cannot execute without R700 hardware (see VERDICT.md
for the guest proof: no AMD/ATI PCI device, kldload radeondrm β module absent).
Artifacts
sim_accounting.cβ the simulator (all kernel semantics cited path:line)build.sh,run.sh,build.log,run.logβ guest build + decisive runenv.txtβ guest environment + hardware prooffix.diffβ git-apply-verified reservation fix (never applied tosys/)VERDICT.mdβ full narrative
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:
- 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. - Copy packets: exactly
num_loops * 5dwords (rv770_dma.c:74-78) β correctly budgeted. - 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_dmais 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 usesr600_copy_cpddaas.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 viattm_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
wptrpastrptr, 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
- Guest hardware proof (
env.txt):pciconf -lvshows 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 classifiednot_reproduced. - Arithmetic proof on the guest (
sim_accounting.c,run.log): modelsradeon_ring_allocalign-up (:114),count_dwdecrement + 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. - Fix applicability:
git apply --checkpasses against the pristine tree (fix.diff); fix mirrors the IB-path accounting (radeon_ib.c:135 reserves64 + RADEON_NUM_SYNCS * 8) and the family precedent (DF-1943 recommendednum_loops*4 + RADEON_NUM_SYNCS*3 + 8for r600).
Kill list (checked, not bugs)
- 32-bit
(num_gpu_pages << 12)truncation β DF-2214, not re-reported. - Ignored
radeon_sync_resv/radeon_sync_ringsreturns (rv770_dma.c:66-67) β DF-5131 explicitly enumerates rv770_dma.c among its 7 sites; not re-reported. num_loops*5+8int overflow: max0x4000*5+8 = 81928< INT_MAX; oversized requests rejected byradeon_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_dwreaches 0. DMA_PACKETcount field:n & 0xFFFF(rv770d.h:663-666); clamp 0xFFFF fits; 40-bit addressing via& 0xfffffffc+upper_32_bits & 0xffconsistent.radeon_fence_emitcannot fail on DragonFly (kmalloc(..., M_WAITOK), radeon_fence.c:135 β the*fence == NULLerror path is dead code), so theunlock_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.
Fix verification
not_testablefix.diff (git apply --check clean) mirrors the IB-path reservation accounting (radeon_ib.c:135 reserves 64 + RADEON_NUM_SYNCS8 for the same documented contract) and the DF-1943 family fix. Baseline/patched kernel behavior cannot be validated on this guest: the radeon driver is absent from the guest kernel and no R700 GPU exists (env.txt), so no observable behavior exists to compare. The simulator run.log doubles as the arithmetic baseline: under the fix's reservation (num_loops5+20 >= worst-case consumers num_loops*5+11) no (S,L) combination can exceed the reserved window.
fix.diff; git apply --check output (GIT_APPLY_CHECK_OK) in session log; run.log arithmetic (max consumers = num_loops*5+6+5 = num_loops*5+11 <= num_loops*5+8+12 under the fix).
Confirmed kernel references
- sys/dev/drm/radeon/rv770_dma.c:59
- sys/dev/drm/radeon/rv770_dma.c:66-67
- sys/dev/drm/radeon/radeon_sync.c:186-194
- sys/dev/drm/radeon/r600_dma.c:319-321
- sys/dev/drm/radeon/r600_dma.c:292-297
- sys/dev/drm/radeon/radeon.h:2708-2716
- sys/dev/drm/radeon/radeon_ring.c:106-131
- sys/dev/drm/radeon/radeon_ring.c:173
- sys/dev/drm/radeon/r600.c:2798
- sys/dev/drm/radeon/radeon_ib.c:135
- sys/dev/drm/radeon/radeon_asic.c:1253-1256
- sys/dev/drm/radeon/radeon_cs.c:182
- sys/dev/drm/ttm/ttm_execbuf_util.c:203-206
Detail
Exploit chain
On real RV770 hardware: (1) unprivileged client makes a bo read by a GFX CS and a UVD decode job (two unsignaled per-ring fences on the resv); (2) forces VRAM eviction so TTM moves the bo via rv770_copy_dma with num_loops==8/11/14 mod 16 while keeping the DMA ring nearly full; (3) radeon_sync_rings emits the 2nd semaphore wait past the reservation, wptr overtakes rptr by up to 15 dwords, overwriting un-executed committed commands with fence/nop dwords; (4) GPU re-parses mid-packet garbage (attacker-influenced GPU-address dwords as DMA packet headers) -> cross-context GPU memory corruption or GPU lockup (DoS). No host-memory corruption; no kernel privilege escalation path - impact ceiling is GPU-visible memory corruption/DoS.
Evidence (decisive lines)
run.log: 12 'DEFICIT: S=2 ... overflow=16 dwords' rows from the guest-run simulator; env.txt: pciconf std-VGA-only + 'kldload: can't load radeondrm'; fix.diff passes git apply --check; VERDICT.md kill list separates this from DF-2214/DF-5131/DF-1943.
PoC changes
No seed existed; authored sim_accounting.c from scratch as a 1:1 arithmetic model of the kernel ring-accounting code (every mirrored line cited in the header).
Verified recommended fix
Reserve the semaphore-wait space radeon_sync_rings may emit on the waiter ring: radeon_ring_lock(rdev, ring, num_loops * 5 + 8 + RADEON_NUM_SYNCS * 3);
Verdict
rv770_copy_dma reserves num_loops*5+8 dwords on the R600 DMA ring (rv770_dma.c:59) but radeon_sync_rings() emits up to two 3-dword semaphore waits on that same ring (GFX+UVD cross-ring fences; radeon_sync.c:186-194 documents the caller-side allocation contract) plus a 5-dword fence emit (r600_dma.c:292-297) - with two pending cross-ring fences every num_loops==8,11,14 (mod 16) overruns the reserved window by exactly 16 dwords after commit padding, and radeon_ring_write() (radeon.h:2710-2716) writes past it anyway (masked in-ring, no host heap OOB), overwriting un-executed GPU commands when the ring is tight. Runtime reproduction is impossible on the audit guest: pciconf shows only QEMU std-VGA (chip 0x11111234) and kldload radeondrm fails (module absent), so the bug is hardware-gated to real HD-4000 GPUs; the deficit arithmetic was proven instead by a guest-run 1:1 simulator of radeon_ring_alloc/ring_write/ring_commit semantics (12 overflowing combinations, each exactly 16 dwords). User-reachable on real hardware: rv770_copy_dma is the primary TTM .copy callback (radeon_asic.c:1253-1256), and S=2 is constructible by an unprivileged render-node client because read-only relocations add shared per-ring fences (radeon_cs.c:182, ttm_execbuf_util.c:203-206).
No comments yet.