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

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:

  1. Data corruption β€” the destination contains stale data instead of source contents, corrupting GPU rendering or compute.
  2. 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.
  3. Potential GPU hang if corrupted buffer is used as a command stream.

PoC

findings/poc/DF-1672/:

  1. Open /dev/dri/card0 (requires video group membership).
  2. Allocate a 4 GB BO in VRAM: ioctl(fd, DRM_IOCTL_RADEON_GEM_CREATE, &args) where args.size = 0x100000000ULL, args.initial_domain = RADEON_GEM_DOMAIN_VRAM. This requires 4 GB+ free VRAM.
  3. Fill the BO with a known pattern (e.g. 0x41414141) by mmap-ing it and writing, or by submitting a CS that writes to it.
  4. Trigger a domain move from VRAM to GTT. On DragonFlyBSD, RADEON_GEM_SET_DOMAIN is 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 with RADEON_GEM_DOMAIN_GTT in the relocation list. This calls radeon_bo_list_validate β†’ ttm_bo_validate β†’ radeon_bo_move β†’ radeon_ttm_move_blit β†’ cik_copy_dma(rdev, old_start, new_start, 0x100000, bo->resv). Inside cik_copy_dma: size_in_bytes = 0x100000 << 12 = 0 (overflow), num_loops = 0, no copy emitted.
  5. 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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1672 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffcombined build rc=0

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.