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:
amdgpu_bo_createwithPAGE_SIZE,amdgpu_cswith aCHUNK_ID_FENCEchunk whosedata.offsetis0xFFFFFFF8(or0xFFFFFFF9..0xFFFFFFFF),- 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):
open("/dev/dri/renderD128", O_RDWR);drmGetCap(...AMDGPU_CHIP_IP_DMA...);amdgpu_device_initialize.amdgpu_ctx_alloc->ctx_id.amdgpu_bo_alloc({.size = PAGE_SIZE, .domain = AMDGPU_GEM_DOMAIN_GTT})->uf_handle. (must be exactlyPAGE_SIZE; check rejects any other size atamdgpu_cs.c:59.)- 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 amdgpu_cs_submit2(ctx_id, &chunks); observe return 0 (check passes).- GPU executes IB; emits fence write at
uf_addr = gpu_offset_of_uf + 0xFFFFFFF8. - 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,mmapvictim BO; observe the 8-byte fence-sequence value overwritten at byte offset 0x38 within the victim BO (i.e., atgpu_offset_of_uf + 0xFFFFFFF8). Ondmesg, 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.
Recommended fix
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.
Related findings
- DF-1484 (sibling): IB chunk missing size validation in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1483 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 > 4096is 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
fixedVALIDATED 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
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- c
- s
- .
- c
- :
- 5
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- c
- s
- .
- c
- :
- 5
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- c
- s
- .
- c
- :
- 6
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- u
- a
- p
- i
- /
- d
- r
- m
- /
- a
- m
- d
- g
- p
- u
- _
- d
- r
- m
- .
- h
- :
- 5
- 8
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- u
- a
- p
- i
- /
- d
- r
- m
- /
- a
- m
- d
- g
- p
- u
- _
- d
- r
- m
- .
- h
- :
- 5
- 8
- 8
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.
No comments yet.