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)
PoC verification
Evidence pack
findings/poc/DF-1209 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedVALIDATED 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
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- .
- c
- :
- 1
- 3
- 1
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- .
- c
- :
- 1
- 3
- 2
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- .
- c
- :
- 1
- 3
- 2
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- .
- c
- :
- 1
- 3
- 3
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- .
- c
- :
- 1
- 3
- 4
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- .
- c
- :
- 1
- 3
- 5
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 1
- 0
- 0
- _
- t
- r
- a
- c
- k
- .
- h
- :
- 6
- 6
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.
No comments yet.