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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2214 Β· 20 files
FileTypeDescriptionSize
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)

  1. guest tree file sha256 = b33101a3cce7eba62ac2d1dda4e1ec0f7225028942cdbb1cafd1c9a2670e33d9 == audited sys/ tree file.
  2. verbatim function = lines 42..94 == pattern extraction, pinned at sha256 = c1b2d82ab166f9e04f7158a8d6fe0506d22ee1c6f05aca121ca0ac9b5a6b547c.
  3. fixed variant = fix.diff mechanically 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.

VERDICT.md
↓ download raw

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+8 only ever shrink; ring writes are bounded by the reservation and by wptr &= ptr_mask inside 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)

  1. 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).
  2. 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).
  3. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/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.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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) include dropped because the fortified memset macro from systm.h collides with its prototype; (3) first kldload panicked the DFly link_elf_obj loader (lost base for relatab) because the object lacked sys/platform/pc64/conf/kern.mk flags - .eh_frame relocations the loader cannot base; fixed with -fno-asynchronous-unwind-tables -mcmodel=kernel -mno-red-zone -msoft-float -mno-mmx -mno-sse -fno-stack-protector; guest reset with-src (module had never executed; unrelated to the finding); (4) trigger values match the finding exactly (2^20-1 vs 2^20 plus 2^20+1, 2^20+2048, 0xC0000000 and a 128 MiB real-ring control).

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