# 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:

```c
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`.
