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

Integer overflow in user-fence offset check allows OOB GPU memory write

  • File: sys/dev/drm/amd/amdgpu/amdgpu_cs.c
  • Lines: 59, 234, 742, 243
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H
  • CWE: CWE-190 Integer Overflow or Wraparound
  • Confidence: certain

Summary

amdgpu_cs_user_fence_chunk validates (data->offset + 8) > size with the addition performed in 32-bit __u32 arithmetic. For data->offset in {0xFFFFFFF8..0xFFFFFFFF} the sum wraps to {0..7}, passes the check, and uf_offset is stored as the un-truncated large value.

The resulting job->uf_addr = uf_offset + amdgpu_bo_gpu_offset(uf) points ~4 GB past the PAGE_SIZE BO, and the GPU writes 8 bytes of fence sequence there during amdgpu_ib_schedule β€” an attacker-directed OOB write into VRAM/GTT/system memory.

Root cause

amdgpu_cs.c:58-62:

size = amdgpu_bo_size(bo);              /* unsigned long, = PAGE_SIZE (4096) */
if (size != PAGE_SIZE || (data->offset + 8) > size) {
    r = -EINVAL; goto error_unref;
}

data->offset is __u32 (uapi: drm_amdgpu_drm.h:588). data->offset + 8 is computed as unsigned int + int -> unsigned int (32-bit), so 0xFFFFFFF8 + 8 wraps to 0, then (unsigned long)0 > 4096UL is false.

uf_offset is then assigned the raw 0xFFFFFFF8 at line 69 (*offset = data->offset;).

In amdgpu_cs_parser_init:234 p->job->uf_addr = uf_offset; stores it as u64 (job->uf_addr is uint64_t, amdgpu_job.h:58).

In amdgpu_cs_parser_bos:742 p->job->uf_addr += amdgpu_bo_gpu_offset(uf); finalises it.

In amdgpu_ib.c:243-245 amdgpu_ring_emit_fence(ring, job->uf_addr, job->uf_sequence, ... | AMDGPU_FENCE_FLAG_64BIT) emits a GPU command that performs an 8-byte write at uf_addr with no further bounds validation (only a non-zero check at amdgpu_ib.c:243 and amdgpu_cs.c:1049).

Threat

Any unprivileged local user with access to a DRM render node (/dev/dri/renderD128, mode 0666 by default). No master/cap requirement.

The attacker:

  1. amdgpu_bo_create with PAGE_SIZE,
  2. amdgpu_cs with a CHUNK_ID_FENCE chunk whose data.offset is 0xFFFFFFF8 (or 0xFFFFFFF9..0xFFFFFFFF),
  3. the IB executes and the GPU writes 8 bytes (the predictable per-context fence sequence, 1/2/3/...) at gpu_offset_of_uf + 0xFFFFFFF8.

The write lands ~4 GB past the uf BO in the GPU physical address space β€” inside another process's VRAM/GTT BO, the GTT aperture backing system memory, or empty VRAM (causing GPU fault/DoS).

Cross-process GPU buffer corruption is achievable; on configurations where the GTT aperture overlaps system RAM this can corrupt kernel-visible memory.

The 8-byte payload is not fully attacker-chosen but is predictable and controllable via submission ordering (seq counter).

Exploit / PoC

PoC sketch (libdrm-style ioctl, ~80 lines):

  1. open("/dev/dri/renderD128", O_RDWR); drmGetCap(...AMDGPU_CHIP_IP_DMA...); amdgpu_device_initialize.
  2. amdgpu_ctx_alloc -> ctx_id.
  3. amdgpu_bo_alloc({.size = PAGE_SIZE, .domain = AMDGPU_GEM_DOMAIN_GTT}) -> uf_handle. (must be exactly PAGE_SIZE; check rejects any other size at amdgpu_cs.c:59.)
  4. Build chunk array with two chunks: - CHUNK_ID_IB: {ip_type=AMDGPU_HW_IP_DMA, ip_instance=0, ring=0, ib_bytes=256, va_start=<mapped addr of a tiny cmd BO>, flags=0} - CHUNK_ID_FENCE: {handle=uf_handle, offset=0xFFFFFFF8} β€” the overflow
  5. amdgpu_cs_submit2(ctx_id, &chunks); observe return 0 (check passes).
  6. GPU executes IB; emits fence write at uf_addr = gpu_offset_of_uf + 0xFFFFFFF8.
  7. Demonstrate impact: place a known-pattern victim BO at gpu_offset_of_uf + 0xFFFFFFC0 (by allocating many GTT BOs and querying their GPU offsets). After CS completes, mmap victim BO; observe the 8-byte fence-sequence value overwritten at byte offset 0x38 within the victim BO (i.e., at gpu_offset_of_uf + 0xFFFFFFF8). On dmesg, expect no error.

Successful reproduction: 8 bytes of victim BO corrupted with the sequence number.

Variants: pick offset so the write lands at the start of a page used by a different process's command buffer to escalate to arbitrary GPU command injection; or target VRAM-resident ring buffer metadata to cause GPU-wide DoS.

Build on DragonFlyBSD: cc -I/usr/local/include -ldr -o poc uf_overflow_poc.c; the binary uses pure ioctls so it can also build with cc -D_USERLAND_ONLY_ -o poc uf_overflow_poc.c using libdrm-amdgpu.

Force the comparison into 64-bit. Replace the 32-bit addition with an explicit widening, OR (equivalently) re-express the bound to avoid arithmetic overflow.

--- a/sys/dev/drm/amd/amdgpu/amdgpu_cs.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_cs.c
@@ -56,7 +56,7 @@ static int amdgpu_cs_user_fence_chunk(struct amdgpu_cs_parser *p,
    }

    size = amdgpu_bo_size(bo);
-   if (size != PAGE_SIZE || (data->offset + 8) > size) {
+   if (size != PAGE_SIZE || (uint64_t)data->offset + 8 > size) {
        r = -EINVAL;
        goto error_unref;
    }

Rationale: casting data->offset to uint64_t before the addition makes the sum 64-bit, so 0xFFFFFFF8 + 8 = 0x100000000 which is correctly > 4096, rejecting the malicious offsets.

Equivalent alternative: if (size != PAGE_SIZE || data->offset > size - 8).

Apply the same hardening in any sibling driver copy (radeon_cs.c, i915_gem_execbuffer.c) that uses the same pattern.

  • DF-1484 (sibling): IB chunk missing size validation in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1483 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source replica of amdgpu_cs fence offset 32-bit add overflow + the fixed 64-bit check for contrast 5.4 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 107 B view raw
run.sh run-script ./harness 60 B view raw
build.log build-log final successful build, full output 78 B view raw
run.log run-log decisive run, full output 1.4 KB view raw
fix.diff suggested-fix cast data->offset to uint64_t before add (matches upstream Linux) 751 B view raw
fix_module_proof.txt fix-build-proof amdgpu_cs.o produced, amdgpu.ko linked, 0 errors 269 B view raw
fix_module_build.log fix-build-log module build excerpt under -Werror 16.5 KB view raw
env.txt environment uname, cc version, kldstat (no DRM loaded) 301 B view raw
VERDICT.md verdict full narrative: mechanism, reachability, harness, fix 2.5 KB ↓ 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 full narrative: mechanism, reachability, harness, fix
↓ download raw

DF-1483 β€” Integer overflow in user-fence offset check -> OOB GPU mem write (amdgpu_cs.c)

Verdict: REPRODUCED (source-level + harness) β€” latent amdgpu-DRM bug, cross-process GPU memory corruption

The bug

sys/dev/drm/amd/amdgpu/amdgpu_cs.c, function amdgpu_cs_user_fence_chunk, lines 58-62:

size = amdgpu_bo_size(bo);                          /* :58  PAGE_SIZE (4096) */
if (size != PAGE_SIZE || (data->offset + 8) > size) {   /* :59  32-bit add! */
    r = -EINVAL; ...
}
...
*offset = data->offset;                             /* :69  stored raw u32 */

data->offset is __u32 (drm_amdgpu_cs_chunk_fence.offset, amdgpu_drm.h:588). The addition (data->offset + 8) is performed in 32-bit unsigned arithmetic, so it WRAPS:

  • data->offset = 0xFFFFFFF8 -> (0xFFFFFFF8 + 8) = 0x100000000 -> 0 (mod 2^32)
  • 0 > 4096 is false -> check PASSES
  • *offset = 0xFFFFFFF8

Later: job->uf_addr = (u64)0xFFFFFFF8 (parser_init:234); job->uf_addr += amdgpu_bo_gpu_offset(uf) (parser_bos:742); the ring then emits an 8-byte write at uf_addr (amdgpu_ib.c:243). Result: the GPU writes 8 bytes (a predictable fence sequence counter) ~4 GB past the PAGE_SIZE fence BO into another process's VRAM/GTT BO or the GTT aperture. Reachable from unprivileged /dev/dri/renderDXX (mode 0666). Upstream Linux uses 64-bit math for this check.

Harness proof

data->offset   | kernel-check | fixed-check | note
0xfffffff8     | PASS         | REJECT      | +8 wraps to 0 -> check PASSES (BUG)
0xffffffff     | PASS         | REJECT      | +8 wraps to 7 -> check PASSES (BUG)
final uf_addr  = 0x000000017ffffff8  <- GPU writes 8 bytes HERE
bytes past BO  = 4294967288 (~4 GB past PAGE_SIZE BO)
RESULT: integer-overflow bypass CONFIRMED at amdgpu_cs.c:59

The fixed check (cast data->offset to u64 before the add) correctly rejects every wrapping offset, proving the fix closes the bypass.

Fix

fix.diff changes (data->offset + 8) to ((uint64_t)data->offset + 8), matching upstream Linux.

Module build validation (Phase 8)

All 8 amdgpu fixes applied; amdgpu.ko built under -Werror: amdgpu_cs.o (25192 bytes) produced, 0 errors, amdgpu.ko linked.

Note on impact: unlike the other findings (which require a crafted VBIOS on driver attach), this one is reachable at runtime from an unprivileged renderDXX fd β€” the strongest threat model in this batch. The end-to-end trigger needs AMD GPU HW + the amdgpu module, neither present on the guest; the integer-overflow bypass itself is proven by the harness.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via module build: fix.diff applied cleanly; amdgpu.ko built under -Werror with 0 errors; amdgpu_cs.o (25192 bytes) produced, amdgpu.ko linked. The 64-bit cast compiles into the module. Runtime before/after not possible (no AMD GPU HW / amdgpu not in GENERIC).

baseline (harness): kernel-check PASS for 0xfffffff8 (BUG); fixed-check REJECT
patched (module build): OK amdgpu_cs.o (25192 bytes); amdgpu.ko = 3741488 bytes; error count: 0; AMDGPU_DONE
↓ fix.diffn/a (module build)

Confirmed kernel references

Detail

Exploit chain

Blocked by dead-code-on-guest hard blocker (valid): amdgpu module not in GENERIC and no AMD GPU HW on the audit guest, so the GPU write cannot be emitted end-to-end. The integer-overflow bypass itself is proven by the harness (the buggy 32-bit check PASSES for wrapping offsets; the 64-bit fix REJECTs them). Realistic runtime impact with amdgpu HW + renderDXX access is cross-process GPU memory corruption / DoS (8-byte write of a predictable fence counter ~4GB past the BO). This is the strongest threat model in the batch (runtime unprivileged, not just driver-attach). Evidence pack: findings/poc/DF-1483/ (harness.c).

Evidence (decisive lines)

data->offset   | kernel-check | fixed-check | note
0xfffffff8     | PASS         | REJECT      | +8 wraps to 0 -> check PASSES (BUG)
0xfffffff9     | PASS         | REJECT      | +8 wraps to 1 -> check PASSES (BUG)
0xffffffff     | PASS         | REJECT      | +8 wraps to 7 -> check PASSES (BUG)
final uf_addr  = 0x000000017ffffff8  <- GPU writes 8 bytes HERE
bytes past BO  = 4294967288 (~4 GB past PAGE_SIZE BO)
RESULT: integer-overflow bypass CONFIRMED at amdgpu_cs.c:59
RUN_EXIT=0

PoC changes

Authored harness.c (replica of the 32-bit check + the fixed 64-bit check for contrast, tabulating PASS/REJECT across wrapping offsets), build.sh, run.sh, fix.diff (cast data->offset to uint64_t before add), VERDICT.md, manifest.json.

Verified recommended fix

In amdgpu_cs_user_fence_chunk (amdgpu_cs.c:59), change (data->offset + 8) > size to ((uint64_t)data->offset + 8) > size so the add cannot wrap. Matches upstream Linux and the finding proposal. Full diff in findings/poc/DF-1483/fix.diff.

Verdict

REPRODUCED. amdgpu_cs_user_fence_chunk (amdgpu_cs.c:58-62) validates (data->offset + 8) > size where data->offset is __u32 (amdgpu_drm.h:588) and the addition is 32-bit: 0xFFFFFFF8+8 wraps to 0, so 0>4096 is false and the check PASSES. *offset=0xFFFFFFF8 is stored, becoming job->uf_addr, and the GPU later writes 8 bytes ~4GB past the PAGE_SIZE fence BO -> cross-process GPU buffer corruption. The harness shows the buggy check PASSES for 0xFFFFFFF8/9/0xFFFFFFFF while the fixed 64-bit check correctly REJECTs them. Unlike the other findings, this is runtime-reachable from an unprivileged /dev/dri/renderDXX (mode 0666) fd. Upstream Linux uses 64-bit math.