β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5223

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5223 Β· 11 files
FileTypeDescriptionSize
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 run
  • env.txt β€” guest environment + hardware proof
  • fix.diff β€” git-apply-verified reservation fix (never applied to sys/)
  • VERDICT.md β€” full narrative
VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5223

Confirmed kernel references

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