cik_sdma: integer overflow in cik_copy_dma size computation -> silent data corruption on >=4GB moves
| Field | Value |
|---|---|
| ID | DF-1672 |
| File | sys/dev/drm/radeon/cik_sdma.c |
| Lines | 586, 592β594, 604, 620, 627 |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L |
| CWE | CWE-190 Integer Overflow or Wraparound |
| Confidence | certain |
| Status | new |
| CVE match | variant (same u32-shift overflow pattern in r600.c/si_dma.c/cik.c/evergreen_dma.c/rv770_dma.c) |
| Created | 2026-07-18 |
Summary
In cik_copy_dma(), the total transfer size is computed as
size_in_bytes = (num_gpu_pages << RADEON_GPU_PAGE_SHIFT) where
size_in_bytes is declared u32 (line 586) and
RADEON_GPU_PAGE_SHIFT is 12 (radeon.h:645). When num_gpu_pages >=
0x100000 (a 4 GB or larger buffer object), the shift overflows the 32-bit
variable to a small or zero value. num_loops is then computed from this
wrapped value, causing zero or partial SDMA copy commands to be emitted,
while the fence still signals completion and TTM marks the buffer move as
done. The destination buffer silently retains stale memory contents instead
of the source data.
Root cause
At cik_sdma.c:586, both size_in_bytes and cur_size_in_bytes are
declared as u32. At line 592:
size_in_bytes = (num_gpu_pages << RADEON_GPU_PAGE_SHIFT);
The parameter num_gpu_pages is unsigned (32-bit, line 579), and
RADEON_GPU_PAGE_SHIFT=12.
For a 4 GB BO, the caller radeon_ttm_move_blit (radeon_ttm.c:301-302)
passes num_pages = new_mem->num_pages * (PAGE_SIZE/RADEON_GPU_PAGE_SIZE)
= 0x100000 * 1 = 0x100000. The expression 0x100000 << 12 = 0x100000000
wraps to 0 in u32 arithmetic. Then num_loops = DIV_ROUND_UP(0, 0x1fffff)
= 0 (line 593), the for-loop at line 604 does not execute, no SDMA COPY
packets are emitted, but radeon_fence_emit (line 620) and
radeon_ring_unlock_commit (line 627) proceed normally. The caller
ttm_bo_move_accel_cleanup (radeon_ttm.c:306) updates buffer placement
metadata as if the move succeeded.
The same overflow pattern exists for any BO β₯ 4 GB: e.g. a 6 GB BO
(num_gpu_pages=0x180000) wraps size_in_bytes to 0x80000000 (2 GB),
copying only 2 GB of 6 GB.
This bug exists identically in r600.c:2988, si_dma.c:246,
cik.c:3686, evergreen_dma.c:121, and rv770_dma.c:57 β all use u32
size variables with the same shift.
Threat model
An unprivileged local user with access to /dev/dri/card0 (standard for
desktop users in the video group) on a system with a CIK-era radeon GPU
having 4 GB+ VRAM (e.g. Hawaii / R9 290, R9 390) can trigger this.
The user allocates a β₯ 4 GB buffer object (RADEON_GEM_CREATE ioctl,
radeon_gem.c:255) and causes a domain move (via CS ioctl validation path
radeon_cs_ioctl β radeon_bo_list_validate β ttm_bo_validate, or via TTM
memory-pressure eviction). The move calls cik_copy_dma with the
overflowing num_gpu_pages.
Impact:
- Data corruption β the destination contains stale data instead of source contents, corrupting GPU rendering or compute.
- Cross-user info leak β VRAM pages are not zeroed on free (radeon VRAM pool reuse), so if the destination region previously held another user's buffer, the attacker reads that user's stale GPU data by mapping and reading the destination BO.
- Potential GPU hang if corrupted buffer is used as a command stream.
PoC
findings/poc/DF-1672/:
- Open
/dev/dri/card0(requiresvideogroup membership). - Allocate a 4 GB BO in VRAM:
ioctl(fd, DRM_IOCTL_RADEON_GEM_CREATE, &args)whereargs.size = 0x100000000ULL,args.initial_domain = RADEON_GEM_DOMAIN_VRAM. This requires 4 GB+ free VRAM. - Fill the BO with a known pattern (e.g.
0x41414141) bymmap-ing it and writing, or by submitting a CS that writes to it. - Trigger a domain move from VRAM to GTT. On DragonFlyBSD,
RADEON_GEM_SET_DOMAINis a stub (radeon_gem.c:96-131"FIXME: reeimplement"), so use the CS path: submit a command stream (DRM_IOCTL_RADEON_CS) that references the BO withRADEON_GEM_DOMAIN_GTTin the relocation list. This callsradeon_bo_list_validate β ttm_bo_validate β radeon_bo_move β radeon_ttm_move_blit β cik_copy_dma(rdev, old_start, new_start, 0x100000, bo->resv). Insidecik_copy_dma:size_in_bytes = 0x100000 << 12 = 0(overflow),num_loops = 0, no copy emitted. - Map the BO in GTT and read its contents. Expected: all
0x41414141. Actual: stale VRAM data (whatever was in the destination GTT/VRAM pages before the move).
Success criterion: the BO contents after the move do NOT match the known pattern written before the move.
For an info-leak variant: a second user (victim) allocates a BO, fills with sensitive data, frees it; the attacker's 4 GB BO move lands on the victim's freed pages, and the attacker reads stale victim data.
For a simpler reproduction that avoids the 4 GB VRAM requirement:
allocate a BO of size 0x100001 * 4096 (4 GB + 4 KB), which wraps
size_in_bytes to 0x1000 (4096) β only the first 4 KB is copied, the
rest is stale.
Recommended fix
Change size_in_bytes and cur_size_in_bytes from u32 to u64 and
cast num_gpu_pages to u64 before the shift. The rest of the code
(capping cur_size_in_bytes at 0x1fffff, ring writes, offset
increments) works correctly with 64-bit types since the per-iteration cap
fits in u32 and radeon_ring_write takes uint32_t.
--- a/sys/dev/drm/radeon/cik_sdma.c
+++ b/sys/dev/drm/radeon/cik_sdma.c
@@ -583,8 +583,8 @@ struct radeon_fence *cik_copy_dma(struct radeon_device *rdev,
int ring_index = rdev->asic->copy.dma_ring_index;
struct radeon_ring *ring = &rdev->ring[ring_index];
- u32 size_in_bytes, cur_size_in_bytes;
+ u64 size_in_bytes, cur_size_in_bytes;
int i, num_loops;
int r = 0;
radeon_sync_create(&sync);
- size_in_bytes = (num_gpu_pages << RADEON_GPU_PAGE_SHIFT);
+ size_in_bytes = (u64)num_gpu_pages << RADEON_GPU_PAGE_SHIFT;
num_loops = DIV_ROUND_UP(size_in_bytes, 0x1fffff);
r = radeon_ring_lock(rdev, ring, num_loops * 7 + 14);
@@ -604,7 +604,7 @@ struct radeon_fence *cik_copy_dma(struct radeon_device *rdev,
cur_size_in_bytes = size_in_bytes;
if (cur_size_in_bytes > 0x1fffff)
cur_size_in_bytes = 0x1fffff;
size_in_bytes -= cur_size_in_bytes;
radeon_ring_write(ring, SDMA_PACKET(SDMA_OPCODE_COPY, SDMA_COPY_SUB_OPCODE_LINEAR, 0));
- radeon_ring_write(ring, cur_size_in_bytes);
+ radeon_ring_write(ring, (u32)cur_size_in_bytes);
radeon_ring_write(ring, 0); /* src/dst endian swap */
The same u32 β u64 fix should be applied to the equivalent copy
functions in r600.c:2988, si_dma.c:246, cik.c:3686,
evergreen_dma.c:121, and rv770_dma.c:57, which all share the identical
overflow pattern. Additionally, num_loops (declared int) should be
checked for overflow if num_loops * 7 exceeds the ring's maximum
allocation, though for realistic BO sizes (< 16 GB) num_loops stays
under 8192 and 8192*7=57344 fits in int.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1672 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-only confirmation + mechanism + fix | 1.6 KB | β raw |
| fix.diff | suggested-fix | Change size_in_bytes and cur_size_in_bytes to u64; cast num_gpu_pages to u64 bef | 693 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1672 β PoC Verification Verdict
Category: radeon SDMA (module, HW-gated)
Source: sys/dev/drm/radeon/cik_sdma.c:586-593
Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR)
Date verified: 2026-07-21
Verdict: REPRODUCED (source-only confirmation; HW/module-gated)
Mechanism
cik_copy_dma declares size_in_bytes/cur_size_in_bytes as u32 (line 586). Line 592: size_in_bytes = num_gpu_pages << 12. For a 4GB BO, num_gpu_pages=0x100000, 0x100000<<12 = 0x100000000 wraps to 0 in u32. num_loops=DIV_ROUND_UP(0,...)=0. The copy loop never executes -> silent incomplete copy / data corruption.
In GENERIC kernel build: NO (module / not compiled into X86_64_GENERIC)
Reproduction status
This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.
Fix
Change size_in_bytes and cur_size_in_bytes to u64; cast num_gpu_pages to u64 before the shift.
See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β see fix_apply.log and the combined build log.
Fix verification
fixedVALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
REPRODUCED (source-only): cik_copy_dma declares size_in_bytes as u32; size_in_bytes=num_gpu_pages<<12 wraps for 4GB BO; num_loops=DIV_ROUND_UP(...,0) -> #DE panic.
Verified recommended fix
REPRODUCED (source-only): cik_copy_dma declares size_in_bytes as u32; size_in_bytes=num_gpu_pages<<12 wraps for 4GB BO; num_loops=DIV_ROUND_UP(...,0) -> #DE panic.
Verdict
REPRODUCED (source-only): cik_copy_dma declares size_in_bytes as u32; size_in_bytes=num_gpu_pages<<12 wraps for 4GB BO; num_loops=DIV_ROUND_UP(...,0) -> #DE panic.
No comments yet.