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

r100_packet3_load_vbpntr unsigned underflow on c==0 yields unbounded heap OOB writes via CS ioctl

Summary

r100_packet3_load_vbpntr() at r100.c:1315-1356 (PACKET3_3D_LOAD_VBPNTR opcode 0x2F CS validator). c=radeon_get_ib_value(p,idx++)&0x1F (0-31). Check at :1325 rejects only c>16, leaving c=0 unhandled. Loop at :1332 for(i=0;i<(c-1);i+=2,idx+=3): unsigned c-1 wraps to UINT_MAX when c=0 -> loop runs unboundedly. Each iteration writes track->arrays[i].esize/.robj, arrays[i+1].esize/.robj (:1343-1356) into fixed [16] array, and ib[idx+1]/ib[idx+2] (:1341/:1353) into bounded IB. After i>=16: heap overflow of kzalloc-d r100_cs_track into cb[]/zb[]/aa[]/textures[]/adjacent slab. Reachable by any DRM-authenticated local user via DRM_IOCTL_RADEON_CS (DRM_AUTH|DRM_RENDER_ALLOW) on r100-r500 + r300.c:1180. Fix: reject c==0 early, change loop guard to i+1<c (no unsigned underflow).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1209 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source userspace replica of r100_packet3_load_vbpntr loop with c==0 unsigned underflow -> arrays[16] OOB 6.0 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 132 B view raw
run.sh run-script ./harness 83 B view raw
build.log build-log final successful build, full output 78 B view raw
run.log run-log decisive run, full output incl OOB CONFIRMED 1.0 KB view raw
fix.diff suggested-fix reject c==0 + change loop guard to i+1<c (no underflow) 1.1 KB view raw
fix_module_proof.txt fix-build-proof radeon.ko built with fix under -Werror, 0 errors, r100.o produced 130 B view raw
fix_module_build.log fix-build-log radeon module build excerpt 5.2 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 3.6 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-1209 β€” Unsigned c-1 underflow in r100_packet3_load_vbpntr (r100.c)

Verdict: REPRODUCED (source-level + harness) β€” latent radeon-DRM bug, heap OOB write

The radeon DRM CS validator is not compiled into X86_64_GENERIC and no AMD GPU is present on the audit guest, so the bug cannot be triggered end-to-end here. It is a real latent bug in the loadable radeon module: confirmed by source trace and reproduced at the control-flow level with a userspace harness that mirrors the vulnerable function exactly.

The bug

sys/dev/drm/radeon/r100.c, function r100_packet3_load_vbpntr, lines 1311-1357. The PACKET3_3D_LOAD_VBPNTR CS validator:

unsigned c, i;                                        /* :1315 -- unsigned! */
...
c = radeon_get_ib_value(p, idx++) & 0x1F;             /* :1324 -- 0..31, from CS */
if (c > 16) { return -EINVAL; }                       /* :1325 -- rejects ONLY c>16 */
track->num_arrays = c;
for (i = 0; i < (c - 1); i+=2, idx+=3) {              /* :1332 -- BUG */
    ...
    track->arrays[i + 0].esize = ...;                 /* :1343 */
    track->arrays[i + 0].robj  = ...;                 /* :1344 */
    ...
    track->arrays[i + 1].esize = ...;                 /* :1355 */
    track->arrays[i + 1].robj  = ...;                 /* :1354 */
}

c and i are both unsigned (:1315). When c == 0 the guard at :1325 passes (0 is not > 16), then (c - 1) at :1332 wraps to UINT_MAX and the loop runs unboundedly. track->arrays[] is a fixed [16] array (r100_track.h:66); once i >= 16 the writes overflow arrays[] into cb[], zb[], aa[], textures[] (r100_track.h:67-70) and finally into the adjacent slab allocation. track is kzalloc'd.

Reachability / threat model

The validator runs on the PACKET3_3D_LOAD_VBPNTR opcode (0x2F) submitted via DRM_IOCTL_RADEON_CS (DRM_AUTH|DRM_RENDER_ALLOW) β€” reachable by any DRM-authenticated local user on r100-r500 hardware. The same path is exercised from r300.c:1180. A single crafted CS buffer with the count nibble == 0 triggers the unbounded write. Local memory-corruption / DoS (and the slab-adjacent overflow is a plausible privilege-escalation primitive on kernels without INVARIANTS).

Harness proof

harness.c replicates the loop verbatim with c=0 against a fixed arrays[16] replica followed by a canary. Output (decisive run):

c (from IB)           = 0
(c > 16) guard        = PASS  (guard PASSES, c==0 allowed)
(unsigned)(c - 1)     = 4294967295   <-- loop bound at r100.c:1332
arrays[16] = {esize=0x11111111 robj=0xaaaa}  <-- FIRST OOB WRITE (past [16])
arrays[17] = {esize=0x22222222 robj=0xbbbb}  <-- OOB
RESULT: heap OOB write CONFIRMED at r100.c:1343-1356 via c==0 underflow

Build & run

cc -O2 -Wall -o harness harness.c
./harness

or ./build.sh && ./run.sh.

Fix

fix.diff makes two changes in r100_packet3_load_vbpntr: 1. Reject c == 0 alongside the existing c > 16 check (a LOAD_VBPNTR with zero buffers is nonsensical and is the only value that underflows). 2. Change the loop guard from i < (c - 1) to i + 1 < c so there is no unsigned underflow even if a future caller bypasses the c == 0 check.

Module build validation (Phase 8)

Both radeon fixes (DF-1209 in r100.c, DF-1437 in sumo_dpm.c) were applied to /usr/src and radeon.ko was built with make KERNCONF=X86_64_GENERIC under -Werror:

OK r100.o (64504 bytes)
OK sumo_dpm.o (20056 bytes)
radeon.ko = 2029128 bytes
error count: 0

The fix compiles cleanly and links into radeon.ko. See fix_module_proof.txt and fix_module_build.log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via module build (the defined validation surface for this non-GENERIC latent radeon bug): the fix.diff applied cleanly to /usr/src and radeon.ko built under -Werror with 0 errors; r100.o (64504 bytes) produced and radeon.ko (2029128 bytes) linked. The bounds check now compiles into the module. Runtime before/after is not possible on this guest (no AMD GPU HW / radeon not in GENERIC).

baseline (harness on unpatched source): arrays[16] = {esize=0x11111111 robj=0xaaaa} <-- FIRST OOB WRITE; (unsigned)(c-1)=4294967295
patched (module build): OK r100.o (64504 bytes); radeon.ko = 2029128 bytes; error count: 0; RADEON_DONE
↓ fix.diffn/a (module build)

Confirmed kernel references

Detail

Exploit chain

Blocked by dead-code-on-guest hard blocker (valid): the radeon DRM module is NOT compiled into X86_64_GENERIC and no AMD GPU HW is present on the audit guest, so the kernel path cannot be triggered end-to-end here. Per the latent-bug resolution (DF-0594/0616/0281 pattern), the primitive is proven at the harness/object level: the unsigned underflow + unbounded arrays[16] overflow is demonstrated in userspace. The realistic runtime impact on a system WITH radeon HW is kernel memory corruption / panic (DoS), and the slab-adjacent overflow is a plausible escalation primitive. Next move on HW: groom r100_cs_track's slab neighbors and trigger via a crafted RADEON_CS LOAD_VBPNTR with the count nibble==0. Evidence pack: findings/poc/DF-1209/ (harness.c).

Evidence (decisive lines)

c (from IB)           = 0
(c > 16) guard        = PASS  (guard PASSES, c==0 allowed)
(unsigned)(c - 1)     = 4294967295   <-- loop bound at r100.c:1332
arrays[15] = ... (in-bounds, last legal slot)
arrays[16] = {esize=0x11111111 robj=0xaaaa}  <-- FIRST OOB WRITE (past [16])
arrays[17] = {esize=0x22222222 robj=0xbbbb}  <-- OOB
first OOB write at arrays[16] (in-kernel: cb[]/zb[]/aa[]/textures[]/slab)
RESULT: heap OOB write CONFIRMED at r100.c:1343-1356 via c==0 underflow
RUN_EXIT=0

PoC changes

Authored harness.c (userspace replica of the c==0 underflow loop with arrays[16]+canary), build.sh, run.sh, fix.diff (reject c==0 + change loop guard to i+1<c), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff for correct hunk headers. Seeded poc.c left in place (harness.c is authoritative).

Verified recommended fix

In r100_packet3_load_vbpntr: extend the guard to if (c > 16 || c == 0) and change the loop bound from i < (c - 1) to i + 1 < c (no unsigned underflow). Supersedes the finding proposal by adding the defensive loop-guard change on top of the c==0 reject. Full diff in findings/poc/DF-1209/fix.diff.

Verdict

REPRODUCED. r100_packet3_load_vbpntr (r100.c:1311-1357) reads a 5-bit count c from the CS buffer, guards only c>16 (:1325), then loops for(i=0;i<(c-1);i+=2) (:1332) with both c,i unsigned. When c==0 the guard passes and (c-1) wraps to UINT_MAX, so the loop writes unboundedly past the fixed track->arrays[16] (r100_track.h:66) into cb/zb/aa/textures and the adjacent slab. Confirmed by harness: arrays[16] clobbered, (unsigned)(c-1)==4294967295. Reachable by any DRM_AUTH user via RADEON_CS ioctl on r100-r500.