32-bit multiply overflow in r100_cs_track_check() PW=1 vertex-array bound (esize * max_indx * 4) -> CS validator bypass -> GPU OOB index/vertex fetch
Summary
The PW=1 bound is computed as track->arrays[i].esize * track->max_indx * 4 entirely in 32-bit unsigned, while max_indx is set verbatim from the IB by the 0x23 handler (full u32, no mask). esize=2 / max_indx=0x80000000 gives size==0 and the validator accepts a draw instructing the GPU to fetch 0x80000000 indices from a 64KB BO. Local DRI user on pre-R300 asics obtains cross-process GPU-memory disclosure (indices/vertices read from adjacent BOs become render output) and/or GPU lockup. Upstream Linux has '* 4UL' but the esize*max_indx product still wraps in u32 - carried upstream variant. Fix: compute both PW cases in 64-bit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3765 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df3765_max_indx.c | β | 4.4 KB | view raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 118 B | view raw | |
| build.log | β | 221 B | view raw | |
| run.log | β | 266 B | view raw | |
| env.txt | β | 595 B | view raw | |
| fix.diff | β | 737 B | view raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| manifest.json | β | 870 B | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-3765 - VERDICT
status: inconclusive (runtime) | reproduced: 0 | impact (expected): leak | confidence: certain (source)
What was verified
In r100_cs_track_check() the PW=1 vertex-array bound is computed as track->arrays[i].esize * track->max_indx * 4 with all factors 'unsigned int'; the product wraps mod 2^32. max_indx is set verbatim from the IB by the 3D_RNDR_GEN_INDX_PRIM (0x23) handler (r100.c:1966) with no mask, so esize=2 / max_indx=0x80000000 yields size==0 and the validator accepts a draw that instructs the GPU to fetch 0x80000000 indices from a 64KB BO -> GPU DMA reads far outside the object (cross-process GPU-memory disclosure via rendering readback, and/or GPU hang/lockup). Upstream Linux has ' 4UL' which still wraps in the u32 esizemax_indx product, so this is a carried upstream defect (variant). PoC builds and exits 1 on this guest: no ATI ASIC / radeonkms module -> inconclusive at runtime, certain at source level (integer identities).
Kernel references
- sys/dev/drm/radeon/r100.c:2323-2341 (PW=1 check: size = esize * max_indx * 4 in 32-bit unsigned)
- sys/dev/drm/radeon/r100.c:1966 (max_indx = radeon_get_ib_value(p, idx+1): full attacker u32)
- sys/dev/drm/radeon/r100.c:2424 (default max_indx 0x00FFFFFF)
Why not reproduced on the guest
Guest = DragonFly 6.5-DEVELOPMENT x86_64 under QEMU (virtio-only): - kldstat: only kernel + ehci.ko + xhci.ko (drm.ko exists in /boot/kernel but is not loaded and there is no radeonkms.ko anywhere) - /dev/dri does not exist - pciconf shows no vendor-1002 (ATI) device => The radeon CS ioctl path cannot execute. The finding is proven by line-exact source analysis (integer identities / NULL flow), and the PoC is proven to build (build.log, cc 8.3 -Wall -O2, zero warnings).
Upstream comparison
Checked against Linux torvalds master r100.c (fetched 2026-09-16): upstream uses ' 4UL' but the esizemax_indx product still wraps in u32 (variant).
Fix
fix.diff in this directory (not applied to the read-only sys/ tree; kernel rebuild validation not possible - no target ASIC).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored from line-exact source analysis; kernel-rebuild validation impossible on this guest (no target ASIC to baseline)
fix.diff in pack
Confirmed kernel references
- sys/dev/drm/radeon/r100.c:2323-2341 (PW=1 check: size = esize * max_indx * 4 in 32-bit unsigned)
- sys/dev/drm/radeon/r100.c:1966 (max_indx = radeon_get_ib_value(p, idx+1): full attacker u32)
- sys/dev/drm/radeon/r100.c:2424 (default max_indx 0x00FFFFFF)
Detail
Evidence (decisive lines)
VERDICT.md (full narrative); df3765_max_indx.c (trigger); build.log (clean compile, cc 8.3 -Wall -O2); run.log (device absent); env.txt (guest has no radeon hw/module); fix.diff
PoC changes
Re-seeded PoC from scratch: DF-style _IOWR('d', nr, type) ioctl encoding (uapi drm.h:800), self-contained uapi structs, packet builders verified against r100d.h/radeon_reg.h constants, register reachability verified against r100_reg_safe.h bitmap (102 entries).
Verified recommended fix
Compute the PW=1 (and PW=2) vertex-array bound in 64-bit: (u64)esize * max_indx * 4
Verdict
In r100_cs_track_check() the PW=1 vertex-array bound is computed as track->arrays[i].esize * track->max_indx * 4 with all factors 'unsigned int'; the product wraps mod 2^32. max_indx is set verbatim from the IB by the 3D_RNDR_GEN_INDX_PRIM (0x23) handler (r100.c:1966) with no mask, so esize=2 / max_indx=0x80000000 yields size==0 and the validator accepts a draw that instructs the GPU to fetch 0x80000000 indices from a 64KB BO -> GPU DMA reads far outside the object (cross-process GPU-memory disclosure via rendering readback, and/or GPU hang/lockup). Upstream Linux has ' 4UL' which still wraps in the u32 esizemax_indx product, so this is a carried upstream defect (variant). PoC builds and exits 1 on this guest: no ATI ASIC / radeonkms module -> inconclusive at runtime, certain at source level (integer identities).
No comments yet.