32-bit integer overflow in DMA copy size (num_gpu_pages << 12) silently truncates copy
Summary
rv770_copy_dma computes copy length as (num_gpu_pages << RADEON_GPU_PAGE_SHIFT) where num_gpu_pages is 32-bit unsigned and shift performed in 32-bit arithmetic. For num_gpu_pages >= 2^20 (>=4 GiB buffer) shift wraps mod 2^32 producing small size_in_dw. DMA engine copies only tiny fraction of requested data while emitted fence signals success. TTM buffer-move layer (radeon_move_blit) completes move believing full copy happened leaving destination populated with stale/uninitialized GPU memory. Byte-for-byte same defect as evergreen_dma.c:121 (DF-2167) and same class as si_dma.c:246 (DF-2091). Hardware-constrained trigger: r7xx-class GPUs (Radeon HD 4000) ship with small VRAM (256MB-1GB) and GTT aperture usually well under 4GB so single BO >=4GB hard to construct on real rv770 hardware. Attacker: unprivileged local user with radeon DRM render/primary node. Impact: silent GPU-memory data corruption integrity. No kernel host-memory corruption no info leak. Ring buffer safe: radeon_ring_alloc rejects oversized radeon_ring_write wraps wptr &= ptr_mask. Lock protocol balanced. [VERIFIED pass2: reproduced via verbatim-code KLD rig - at 2^20 pages the shift wraps to zero, zero DMA packets scheduled yet fence signals success, silent truncation confirmed on 64KiB ring where the correct size is loudly refused; fix-validated - u64 widening restores full scheduling and bit-identical below boundary; NOT privesc - wrap only shrinks, host canaries intact]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2214 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.1 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| rig_head.c | β | 9.7 KB | view raw | |
| rig_tail.c | β | 6.6 KB | view raw | |
| verbatim_baseline.c | β | 1.6 KB | view raw | |
| verbatim_fixed.c | β | 1.6 KB | view raw | |
| df2214_rig.c | β | 19.5 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 116 B | view raw | |
| build.log | β | 1.0 KB | view raw | |
| run.log | β | 9.5 KB | view raw | |
| run_extract.log | β | 8.4 KB | view raw | |
| run2_extract.log | β | 8.4 KB | view raw | |
| run3_extract.log | β | 8.4 KB | view raw | |
| run_dmesg.log | β | 8.4 KB | view raw | |
| kldload_panic_iteration.txt | β | 796 B | view raw | |
| fix.diff | β | 608 B | view raw | |
| env.txt | β | 784 B | view raw | |
| manifest.json | β | 2.2 KB | view raw | |
| verdict.json | β | 8.1 KB | view raw |
DF-2214 β 32-bit wrap in rv770_copy_dma DMA copy size silently truncates TTM buffer moves
Status: REPRODUCED (verbatim-code KLD rig) β impact class: GPU-side memory
integrity (silent data corruption of video/GTT buffers). No host kernel memory
corruption, no privilege-escalation primitive (verified β see VERDICT.md).
Fix: A/B-validated in the same KLD (fix.diff, u64 widening identical in
shape to the verified evergreen/si fix).
What this pack contains
| file | role |
|---|---|
rig_head.c |
shims + minimal fabricated boundary structures (real constants, real ring semantics, every shim cites its production origin) |
rig_tail.c |
matrix driver + module glue |
verbatim_baseline.c |
unmodified production function, lines 42-94 of sys/dev/drm/radeon/rv770_dma.c, extracted by build.sh on the guest (sha256 pinned) |
verbatim_fixed.c |
same bytes + fix.diff applied + function renamed |
df2214_rig.c |
the exact assembled source that was compiled kernel-resident (head + baseline-verbatim + fixed-verbatim + tail) |
build.sh |
provenance chain + KLD build on the guest |
run.sh |
kldload once, capture dmesg matrix |
run_extract.log, run2_extract.log, run3_extract.log |
three identical decisive runs |
run_dmesg.log |
full message buffer of the decisive run |
build.log, run.log |
full untrimmed build + run transcripts |
kldload_panic_iteration.txt |
iteration note: first build panicked the loader (missing kern.mk flags); module never ran; guest reset |
fix.diff |
git-apply-able unified diff (u64 widening of the size computation) |
env.txt |
guest uname, cc, hashes, kldstat |
VERDICT.md, manifest.json, verdict.json |
verdict narrative + machine-readable results |
How to reproduce
On the DragonFly guest (root, /usr/src present):
./build.sh # rebuilds everything from the tree file; aborts on any hash mismatch
./run.sh # kldloads the rig; prints the 16-case matrix to dmesg
Provenance (checked at build time, build aborts otherwise)
- guest tree file
sha256 = b33101a3cce7eba62ac2d1dda4e1ec0f7225028942cdbb1cafd1c9a2670e33d9== auditedsys/tree file. - verbatim function = lines 42..94 == pattern extraction, pinned at
sha256 = c1b2d82ab166f9e04f7158a8d6fe0506d22ee1c6f05aca121ca0ac9b5a6b547c. - fixed variant =
fix.diffmechanically applied to those same bytes.
Expected output (baseline = unmodified production code)
DF2214 CASE variant=BASELINE pages=0x00100000 ... # num_gpu_pages = 2^20 DF2214 num_loops=0 copy_packets=0 ... scheduled_bytes=0 (0.0000% of request) fence_emitted=1 DF2214 VERDICT: SILENT_TRUNCATION (fence says success, 0 of 4294967296 bytes scheduled) DF2214 CASE variant=BASELINE pages=0x000fffff ... # 2^20-1: just below the wrap DF2214 num_loops=16385 copy_packets=16385 sum_chunk_dw=1073740800 ... DF2214 scheduled_bytes=4294963200 (100.0000% of request) fence_emitted=1 DF2214 VERDICT: FULL_COPY DF2214 CASE variant=BASELINE pages=0x000fffff ring=real64K ... # real 64 KiB ring (rv770.c:1968) DF2214 ERR_PTR(-12): ring reservation refused, ZERO packets, NO fence - loud failure DF2214 CASE variant=BASELINE pages=0x00100000 ring=real64K ... DF2214 ... scheduled_bytes=0 (0.0000%) fence_emitted=1 ... SILENT_TRUNCATION
With fix.diff (variant=FIXED): every case schedules exactly 100.0000% of the
request (or fails loudly ERR_PTR(-12) when the ring genuinely cannot hold
it), and every sub-2^20 case is bit-identical to baseline (16385 packets,
same first/last chunk sizes, same fence behavior) β no regression.
Attacker input
num_gpu_pages comes from the TTM layer: radeon_move_blit
(sys/dev/drm/radeon/radeon_ttm.c) computes
num_pages = new_mem->num_pages * (PAGE_SIZE / RADEON_GPU_PAGE_SIZE) and
passes it straight to the asic copy callback. A buffer-object move of >= 4 GiB
(num_gpu_pages >= 2^20) β plausible on large-GTT radeon systems β flips the
bug. The DMA engine then copies a fraction (or nothing) while
radeon_fence_emit still signals success, so ttm_bo_move_accel_cleanup
publishes a destination buffer that was never written: silent video-memory
integrity corruption.
DF-2214 β Verification Verdict
Status: reproduced (verbatim-code KLD rig on the QEMU guest) Impact: GPU-side memory integrity (silent truncation of large DMA copies); no host kernel memory corruption; no privilege-escalation primitive Confidence: certain
Verdict
REPRODUCED in kernel context by compiling the UNMODIFIED production function
rv770_copy_dma (sys/dev/drm/radeon/rv770_dma.c lines 42-94, sha256
c1b2d82ab166f9e04f7158a8d6fe0506d22ee1c6f05aca121ca0ac9b5a6b547c, extracted
from the guest tree file which is byte-identical to the audited tree,
b33101a3...) into a KLD that drives it at the exact boundary state with
fabricated-but-real-constant structures (radeon_ring with real ptr_mask
semantics per radeon.h:2708-2717 and real ring geometry 64 KiB per
rv770.c:1968, R600_RING_TYPE_DMA_INDEX=3 per radeon.h:149,
RADEON_GPU_PAGE_SHIFT=12 per radeon.h:645, DMA_PACKET macro per
rv770d.h:663-669, ring reservation semantics per radeon_ring.c:107-127).
Line 57 computes size_in_dw = (num_gpu_pages << RADEON_GPU_PAGE_SHIFT) / 4
in 32-bit unsigned arithmetic (u32 size_in_dw, unsigned num_gpu_pages). For
num_gpu_pages >= 2^20 the shift result exceeds 32 bits and wraps:
0x000FFFFF(2^20-1): size_in_dw=0x3FFFFC00, 16385 DMA_PACKET_COPY packets chunked at 0xFFFF, 4294963200 bytes scheduled = 100.0000% β correct, fence seq=1. FULL_COPY.0x00100000(2^20, a 4 GiB move): size_in_dw wraps to 0, num_loops=0, zero DMA packets, zero bytes scheduled β and the fence is still emitted and the function still returns a success fence (run_extract.log: "scheduled_bytes=0 (0.0000% of request) fence_emitted=1 ... VERDICT: SILENT_TRUNCATION").0x00100001: wraps to 4096 bytes β exactly one packet with n=1024 dwords (first_pkt=0x30000400) out of a 4 GiB+4K request.0x00100800: wraps to 8 MiB (0.1949% of request).0xC0000000(12 TiB request): wraps to 0 β silent total loss.
On the REAL ring geometry (64 KiB, rv770.c:1968) the contrast is sharpest:
a 4 GiB-4K copy (correct arithmetic) needs 81933 dwords, radeon_ring_lock
refuses with -ENOMEM and the production DRM_ERROR "radeon: moving bo (-12)"
fires β a loud failure the caller can react to. The wrapped 4 GiB copy asks
for only 8 dwords, locks trivially, emits nothing, and fences success:
ttm_bo_move_accel_cleanup then publishes a destination buffer that was
never written. Silent integrity corruption of GPU memory, undetectable by
the caller. Deterministic: three identical runs (run_extract.log,
run2_extract.log, run3_extract.log).
Negative claims checked (host memory / privesc)
- No host kernel memory corruption: the wrap can only SHRINK size_in_dw
(wrapped = true_size mod 2^32 <= true_size), hence num_loops and the ring
reservation
num_loops*5+8only ever shrink; ring writes are bounded by the reservation and bywptr &= ptr_maskinside the kmalloc'd ring buffer. Verified empirically: 4096-byte canary zones bracketing the ring buffer were INTACT after all 16 runs, count_dw never went negative (final=8, underruns=0). This is a GPU-data integrity bug, not a host memory-safety bug β there is no overwrite primitive and no route to uid=0 (the directive chain was assessed and closed: not a host memcorrupt/privesc class). - Not remotely reachable without local GPU access: the trigger requires a
= 4 GiB buffer-object move through the radeon TTM layer (radeon_ttm.c radeon_move_blit -> radeon_copy callback), i.e. an unprivileged local GPU client performing huge allocations/moves on rv770-class (r7xx/evergreen DMA) hardware with sufficient GTT. GPU-gated, Medium severity as filed.
Fix Status
fixed β A/B-validated in the same KLD. fix.diff (generated mechanically
with diff -u, applies cleanly with patch -p1) widens the computation to 64
bit, matching the shape of the verified evergreen fix
(findings/poc/batch_build/all_fixes.patch hunk at evergreen_dma.c):
u64 size_in_dw; u32 cur_size_in_dw;size_in_dw = ((u64)num_gpu_pages << RADEON_GPU_PAGE_SHIFT) / 4;
FIXED variant results: every sub-2^20 case bit-identical to baseline (e.g.
0xFFFFF: same 16385 packets, 100.0000%, same fence seq) β zero regression;
every >= 2^20 case schedules exactly 100.0000% of the request
(0x100000: sum_chunk_dw=1073741824 = the full 4 GiB) on the oversized test
ring, and on the real 64 KiB ring it now fails LOUDLY with ERR_PTR(-12) β
exactly like a genuine 4 GiB-4K copy β instead of silently fencing an empty
command stream. Residual nit (unchanged from evergreen fix): for absurd
inputs (num_gpu_pages >= 2^22) cur_size_in_dw = size_in_dw narrows before
the 0xFFFF cap on iterations after the first; harmless in practice because
ring_lock refuses such reservations long before (observed: attempted_ndw=
251662093 -> lock_rc=-12), but a follow-up could cap before assigning.
Source File
sys/dev/drm/radeon/rv770_dma.c (function lines 42-94; defect at line 57,
propagated through lines 58/69-81; success fence at line 83-93).
Iterations (poc_changes)
- First kldload panicked the guest BEFORE the module executed: DFly's link_elf_obj loader cannot base .eh_frame relocations emitted without the kernel flag set ("panic: lost base for relatab", see kldload_panic_iteration.txt). Fixed by compiling with sys/platform/pc64/conf/kern.mk flags (-fno-asynchronous-unwind-tables -mcmodel=kernel -mno-red-zone -msoft-float ...). Guest reset with-src; not related to the finding (module had not run).
- Kernel include environment replicated from sys/conf/kmod.mk semantics
(merged machine/ namespace, machine_base/cpu_base/dragonfly symlinks,
-D_KERNEL, -I/usr/src/include for
-class forwarders; then dropped because systm.h fortified memset collides). - Trigger values exactly as the finding specifies (2^20-1, 2^20, 2^20+1, plus 2^20+2048, 0xC0000000, and real-ring 128 MiB control).
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
fixedA/B validated in the same verbatim KLD: the FIXED variant (fix.diff applied to the same tree bytes, renamed) schedules exactly 100.0000 percent of the request for every num_gpu_pages >= 2^20 on the oversized test ring (e.g. 0x100000: sum_chunk_dw=1073741824 = full 4 GiB in 16385 packets) and fails LOUDLY with ERR_PTR(-12) on the real 64 KiB ring exactly like a genuine 4 GiB-4K copy, while every sub-2^20 case is bit-identical to the unpatched production code (same packet count, same first/last chunk sizes, same fence behavior, 128 MiB real-ring control FULL_COPY). Full kernel rebuild not meaningful for this target: the guest has no radeon hardware so the production path cannot execute there, and the rig compiles the identical production bytes kernel-resident (precedent DF-2290).
run_extract.log FIXED section: pages=0x00100000 -> scheduled_bytes=4294967296 (100.0000% of request) VERDICT FULL_COPY; real64K FIXED 0x00100000 -> ERR_PTR(-12) loud failure; sub-2^20 FIXED cells byte-for-byte equal to BASELINE cells. build.log: Hunk #1 succeeded at 48 (fix.diff applied to pristine tree bytes). fix_build.log/fix_run.log carry the full transcripts.
Confirmed kernel references
- sys/dev/drm/radeon/rv770_dma.c:57
- sys/dev/drm/radeon/rv770_dma.c:58
- sys/dev/drm/radeon/rv770_dma.c:69-81
- sys/dev/drm/radeon/rv770_dma.c:83-93
- sys/dev/drm/radeon/radeon.h:645
- sys/dev/drm/radeon/radeon.h:2708-2717
- sys/dev/drm/radeon/radeon_ring.c:107-127
- sys/dev/drm/radeon/rv770d.h:663-669
- sys/dev/drm/radeon/rv770.c:1968
- sys/dev/drm/radeon/radeon_ttm.c:radeon_move_blit
Detail
Exploit chain
unprivileged GPU client on rv770-class (r7xx/evergreen DMA engine) hardware with large GTT -> TTM buffer-object move of >= 4 GiB -> radeon_move_blit computes num_pages = new_mem->num_pages (PAGE_SIZE==RADEON_GPU_PAGE_SIZE) and calls the asic copy callback rv770_copy_dma with num_gpu_pages >= 2^20 (radeon_ttm.c) -> (num_gpu_pages << 12) wraps u32 at rv770_dma.c:57 -> size_in_dw tiny or zero -> zero/few DMA packets at rv770_dma.c:69-81 but success fence at rv770_dma.c:83-93 -> ttm_bo_move_accel_cleanup marks the move complete -> destination buffer object keeps stale/uninitialized contents while userspace believes the copy succeeded: silent video-memory integrity corruption, potential info exposure of prior GPU memory contents in the destination, no host memory effect.
Evidence (decisive lines)
run_extract.log (identical in run2/run3_extract.log): BASELINE pages=0x00100000 -> num_loops=0 copy_packets=0 scheduled_bytes=0 (0.0000% of request) fence_emitted=1 VERDICT SILENT_TRUNCATION; BASELINE pages=0x000fffff -> 16385 packets 100.0000% FULL_COPY; BASELINE real64K 0x000fffff -> ERR_PTR(-12) loud vs 0x00100000 -> silent no-op with fence; FIXED pages=0x00100000 -> sum_chunk_dw=1073741824 scheduled=4294967296 100.0000% FULL_COPY; FIXED real64K >=2^20 -> ERR_PTR(-12) loud; all 16 cases canaries=INTACT count_dw_final=8 underruns=0. build.log shows the pinned provenance hashes and the clean gcc 8.3 kernel-context build of both variants into one KLD. kldload_panic_iteration.txt documents the toolchain-only iteration (loader panic before the module ever ran, kern.mk flags).
PoC changes
No seed pack existed; rig authored from scratch in verify mode. Iterations: (1) guest kernel include environment replicated from sys/conf/kmod.mk semantics (merged machine namespace, machine_base/cpu_base/dragonfly symlinks, -D_KERNEL, -I/usr/src/include); (2)
Verified recommended fix
Widen the size computation at rv770_dma.c:57 - declare u64 size_in_dw (u32 cur_size_in_dw) and compute ((u64)num_gpu_pages << RADEON_GPU_PAGE_SHIFT) / 4 - see fix.diff; same shape as the verified evergreen_dma.c fix.
Verdict
REPRODUCED via verbatim-code KLD rig on the QEMU guest. The rig compiles the UNMODIFIED production function rv770_copy_dma (sys/dev/drm/radeon/rv770_dma.c lines 42-94, sha256 c1b2d82ab166f9e04f7158a8d6fe0506d22ee1c6f05aca121ca0ac9b5a6b547c, extracted from the guest tree file which is byte-identical to the audited tree b33101a3...) kernel-resident, walks it with fabricated boundary structures carrying the real constants (RADEON_GPU_PAGE_SHIFT 12, DMA_PACKET per rv770d.h:663-669, R600_RING_TYPE_DMA_INDEX 3, ptr_mask/ring_write semantics per radeon.h:2708-2717, ring reservation per radeon_ring.c:107-127, real 64 KiB DMA ring per rv770.c:1968) and drives the exact input domain. Line 57 computes size_in_dw in 32-bit unsigned arithmetic: at num_gpu_pages=0x100000 (a 4 GiB TTM move) it wraps to 0, so num_loops=0, ZERO DMA_PACKET_COPY packets are emitted, and radeon_fence_emit still fires - the function returns a success fence having scheduled 0 of 4294967296 bytes (run_extract.log: scheduled_bytes=0 fence_emitted=1, VERDICT SILENT_TRUNCATION). At 0x100001 exactly one packet with n=1024 dwords (first_pkt=0x30000400) is emitted of a 4 GiB+4K request. Below the wrap (0xFFFFF) the same verbatim code schedules 100.0000 percent via 16385 packets chunked at 0xFFFF. On the real 64 KiB ring geometry the correct 4 GiB-4K copy is refused loudly (radeon_ring_lock -ENOMEM, production DRM_ERROR radeon: moving bo (-12)) while the wrapped 4 GiB copy locks 8 dwords, emits nothing and fences success - silent integrity corruption that ttm_bo_move_accel_cleanup publishes as a completed move. Three identical deterministic runs. Impact ceiling is GPU-side data integrity (video/GTT buffer contents); host kernel memory is untouched - canary zones around the ring buffer stayed INTACT in all 16 runs and the wrap can only shrink the reservation, never grow it - so there is no overwrite primitive and no route to uid=0 (privesc directive assessed and closed: not host memcorrupt).
No comments yet.