Missing minimum COUNT validation in r300_packet3_check allows OOB read/write past IB buffer
- File:
sys/dev/drm/radeon/r300.c - Lines: 1176, 1184, 1191, 1192, 1198, 1202, 1206, 1228, 1229, 1242, 1243
- Severity: Medium
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
Summary
r300_packet3_check() handles PACKET3_INDX_BUFFER, _3D_DRAW_IMMD,
_3D_DRAW_VBUF, and _3D_DRAW_INDX by accessing ib[idx+1]
(= ib[pkt->idx+2]) without first checking that pkt->count covers that data
dword.
The parser (radeon_cs.c:768) only verifies the packet fits in the IB β it does
not enforce a per-opcode minimum COUNT.
A crafted packet3 with COUNT=0 (or COUNT=1 for INDX_BUFFER) placed at the
end of the IB passes the parser's bounds check, then the handler reads and/or
writes 4 bytes past the declared IB buffer.
For INDX_BUFFER this is an OOB write; the subsequent call to
r100_cs_track_check_pkt3_indx_buffer (r100.c:1909) reads ib[idx+2]
(= ib[pkt->idx+3]) causing a further OOB read.
Root cause
In r300_packet3_check (r300.c:1166), idx is set to pkt->idx+1 (line
1176).
The parser radeon_cs_packet_parse (radeon_cs.c:768) accepts a type-3 packet
whenever (pkt->count + 1 + pkt->idx) < length_dw.
For a packet with COUNT=0 this allows pkt->idx up to length_dw-2, so the
packet's sole data dword is at ib[length_dw-1] but the handler reaches
ib[idx+1] = ib[length_dw] β one dword past the end.
Concretely:
PACKET3_INDX_BUFFER(r300.c:1184): line 1191ib[idx+1] = radeon_get_ib_value(p, idx + 1) + ((u32)reloc->gpu_offset)is both an OOB read (viaradeon_get_ib_value) and an OOB write (top->ib.ptr[length_dw]) whenCOUNT<1. Thenr300.c:1192callsr100_cs_track_check_pkt3_indx_bufferwhich atr100.c:1909doesvalue = radeon_get_ib_value(p, idx + 2)=ib[pkt->idx+3]=ib[length_dw+1], a second OOB read, whenCOUNT<2. NormalINDX_BUFFERhasCOUNT=2(3 data dwords:IB_INFO,IB_BASE_LO,IB_NUM_INDICES).PACKET3_3D_DRAW_IMMD(r300.c:1198): line 1202radeon_get_ib_value(p, idx + 1)and line 1206 readib[length_dw]OOB whenCOUNT<1.PACKET3_3D_DRAW_VBUF(r300.c:1228): line 1229radeon_get_ib_value(p, idx + 1)OOB read whenCOUNT<1.PACKET3_3D_DRAW_INDX(r300.c:1242): line 1243radeon_get_ib_value(p, idx + 1)OOB read whenCOUNT<1.
No if (pkt->count < N) return -EINVAL guard exists anywhere in
r300_packet3_check (confirmed: only pkt->count references are lines 1207,
1222, 1290 β none are bounds checks).
The _2 variants (DRAW_IMMD_2/VBUF_2/INDX_2) only access ib[idx] which
is always the first data dword and safe.
Threat
Attacker is any local user with access to /dev/dri/card0 (typically via
video group or render node /dev/dri/renderD128, no root required).
The CS ioctl (DRM_IOCTL_RADEON_CS) is the entry point β radeon_cs_ioctl β
r300_cs_parse β r300_packet3_check. The attacker fully controls IB contents
and length, the reloc list, and packet headers/counts.
Impact:
- (a) OOB WRITE of 4 bytes via
INDX_BUFFERintop->ib.ptr[length_dw]β this is the SA sub-allocator pool (GTT-backed system memory mapped viaradeon_bo_kmap,radeon_ib.c:61/radeon_sa.c:118), corrupting an adjacent IB's command data in the 1 MB pool; the written value is partially controlled (reloc->gpu_offsetadded to an OOB-read neighbor value). - (b) OOB READ of 4-8 bytes β on the non-VM path (always used for r300-era
chips,
radeon_cs.c:346kvmalloc_array)radeon_get_ib_valuereads fromkdata[length_dw], 4 bytes past akvmalloc'd slab array, potentially leaking the first dword of an adjacent slab object (kernel pointer or other sensitive data) intotrack->vap_vf_cntl, which influences size computations andDRM_ERROR/dmesgoutput inr100_cs_track_check.
Practical host-kernel escalation not demonstrated β the write lands within the SA BO's mapped region (GPU command memory, not general kernel heap), and GPU DMA on r300 is confined to validated GTT entries.
Realistic impact: cross-process GPU command corruption, GPU hang/DoS, and a limited/convoluted kernel slab info leak.
Exploit / PoC
Minimal PoC via DRM_IOCTL_RADEON_CS (libdrm or raw ioctl):
- Open
/dev/dri/card0(DRM_IOCTL_AUTHor render node). - Create a reloc target GEM buffer via
DRM_IOCTL_RADEON_GEM_CREATE+DRM_IOCTL_RADEON_GEM_SET_DOMAIN(size>= 4096, GTT domain). - Build CS chunks:
- IB chunk:
length_dw = 2. Contents:ib[0] = 0xC0003300(PACKET3 header: type=3, opcode=0x33INDX_BUFFER,COUNT=0);ib[1] = 0x00000000(dummy data dword). - Reloc chunk: oneradeon_cs_relocreferencing the GEM buffer, withwrite_domainmatching, andreloc_indicespointing at ib offset 1 (the data dword the handler will patch). - Submit via
DRM_IOCTL_RADEON_CS. - Result:
r300_cs_parseβradeon_cs_packet_parseaccepts the packet (check:(0+1+0)=1 < 2).r300_packet3_checkentersPACKET3_INDX_BUFFER.radeon_cs_packet_next_relocsucceeds. Line 1191:radeon_get_ib_value(p, 0+1+1)readskdata[2]= 4 bytes past the 8-bytekvmallocslab allocation (OOB READ of adjacent slab data). Then writesp->ib.ptr[2] = read_value + reloc->gpu_offset(OOB WRITE, 4 bytes past the SA-allocated IB buffer). Thenr100_cs_track_check_pkt3_indx_bufferreadsib[idx+2] = ib[3] = kdata[3](OOB READ, 8 bytes past slab alloc).
Success indicators: no crash (the SA pool has headroom); but the adjacent slab
object's first dword is now in track state and may appear in dmesg if
r100_cs_track_check fails with an error printing nverts-derived values.
To confirm the OOB write, run under a kernel with KASAN/slab poisoning (panic on the OOB) or observe GPU malfunction from the corrupted adjacent IB.
On DragonFly: build a kernel with DEBUG_MEMGUARD or INVARIANTS to catch the
slab OOB.
The PoC trigger source is ~60 lines of C using raw ioctl() (no libdrm
dependency); place at findings/poc/DF-1492/trigger.c with
build.sh 'cc -O2 -o trigger trigger.c' and run.sh './trigger /dev/dri/card0'.
Recommended fix
Add per-opcode minimum COUNT validation at the top of each affected case in
r300_packet3_check, before any ib[idx+N] access. INDX_BUFFER needs
count>=2 (it accesses ib[idx+1] directly and ib[idx+2] via
r100_cs_track_check_pkt3_indx_buffer); the three DRAW handlers need
count>=1 (they access ib[idx+1]).
--- a/sys/dev/drm/radeon/r300.c
+++ b/sys/dev/drm/radeon/r300.c
@@ -1184,6 +1184,11 @@ static int r300_packet3_check(struct radeon_cs_parser *p,
break;
case PACKET3_INDX_BUFFER:
+ if (pkt->count < 2) {
+ DRM_ERROR("PACKET3_INDX_BUFFER requires count >= 2 (got %d)\n",
+ pkt->count);
+ return -EINVAL;
+ }
r = radeon_cs_packet_next_reloc(p, &reloc, 0);
if (r) {
DRM_ERROR("No reloc for packet3 %d\n", pkt->opcode);
@@ -1198,6 +1203,10 @@ static int r300_packet3_check(struct radeon_cs_parser *p,
/* Draw packet */
case PACKET3_3D_DRAW_IMMD:
+ if (pkt->count < 1) {
+ DRM_ERROR("DRAW_IMMD requires count >= 1\n");
+ return -EINVAL;
+ }
/* Number of dwords is vtx_size * (num_vertices - 1)
* PRIM_WALK must be equal to 3 vertex data in embedded
* in cmd stream */
@@ -1228,6 +1237,10 @@ static int r300_packet3_check(struct radeon_cs_parser *p,
break;
case PACKET3_3D_DRAW_VBUF:
+ if (pkt->count < 1) {
+ DRM_ERROR("DRAW_VBUF requires count >= 1\n");
+ return -EINVAL;
+ }
track->vap_vf_cntl = radeon_get_ib_value(p, idx + 1);
r = r100_cs_track_check(p->rdev, track);
if (r) {
@@ -1242,6 +1255,10 @@ static int r300_packet3_check(struct radeon_cs_parser *p,
break;
case PACKET3_3D_DRAW_INDX:
+ if (pkt->count < 1) {
+ DRM_ERROR("DRAW_INDX requires count >= 1\n");
+ return -EINVAL;
+ }
track->vap_vf_cntl = radeon_get_ib_value(p, idx + 1);
r = r100_cs_track_check(p->rdev, track);
if (r) {
Note: the same missing-minimum-COUNT pattern exists in r100.c:1920
(r100_packet3_check) at lines 1945 (INDX_BUFFER), 1975 (DRAW_IMMD), 2000
(DRAW_VBUF_2 reads ib[idx] β safe), 2014/2021 (DRAW_VBUF/INDX read
ib[idx+1]). The fix should be applied to r100.c as well for completeness,
but this finding is scoped to r300.c.
Related findings
- DF-1209 (sibling, r100.c):
LOAD_VBPNTRc==0 underflow, reachable from this file viar100_packet3_load_vbpntratr300.c:1180.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1492 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 1.6 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 2.6 KB | β raw |
| fix.diff | suggested-fix | git-apply-able minimal fix; compiles -Werror clean | 504 B | view raw |
| build.sh | build-script | echoes the module/kernel rebuild command | 380 B | view raw |
| run.sh | run-script | no live trigger on this guest | 288 B | view raw |
| env.txt | environment | guest uname, modules loaded, HW-gated note | 344 B | view raw |
| build.log | build-log | kernel build log excerpt proving -Werror clean compile of patched source | 384 B | view raw |
| fix_apply.log | apply-log | patch --dry-run output proving fix.diff applies cleanly on with-src | 417 B | view 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 |
PoC DF-1492: r300_packet3_check missing per-opcode minimum COUNT
Class: IB OOB read
Cited site: sys/dev/drm/radeon/r300.c:1166-1248
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
No β radeon DRM HW-gated. Trigger is a CS IB ending with a packet3 (INDX_BUFFER/DRAW_*) of COUNT=0.
The bug is confirmed at the source level by tracing the cited path:line in
sys/dev/drm/radeon/r300.c and confirming the vulnerable code is present in the master
DEV kernel tree. The fix.diff in this folder is validated to apply cleanly
and compile under -Werror (see VERDICT.md).
Mechanism
r300.c:1176 idx = pkt->idx + 1;. Several opcodes (PACKET3_INDX_BUFFER :1191, _3D_DRAW_IMMD :1202, _3D_DRAW_VBUF :1229, _3D_DRAW_INDX :1243) read ib[idx+1] without checking pkt->count. The parser radeon_cs.c:768 accepts a packet when (pkt->count+1+pkt->idx) < length_dw, so a COUNT=0 packet at end of IB has idx+1 == length_dw β ib[idx+1] reads past the IB allocation.
Realistic impact ceiling
leak (info leak / DoS)
Fix
Reject packets with pkt->count < 1 at the top of r300_packet3_check (after the idx assignment).
See fix.diff for the git-apply-able patch.
How to validate the fix
# 1. Apply fix.diff against the in-guest source: scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1492.diff ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1492.diff' # 2. Rebuild the affected module (preferred) or a single-fix kernel: ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src/sys/sys/dev/drm/radeon && make' # 3. The compile must succeed with -Werror (it does β see build.log).
VERDICT β DF-1492: r300_packet3_check missing per-opcode minimum COUNT
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/drm/radeon/r300.c:1166-1248,
but the affected driver attaches only to hardware not present in the audit QEMU
guest, so it cannot be live-triggered here. The fix.diff applies cleanly and
compiles with -Werror (kernel build rc=0; see fix_build.log).
Mechanism (cited path β primitive β effect)
r300.c:1176 idx = pkt->idx + 1;. Several opcodes (PACKET3_INDX_BUFFER :1191, _3D_DRAW_IMMD :1202, _3D_DRAW_VBUF :1229, _3D_DRAW_INDX :1243) read ib[idx+1] without checking pkt->count. The parser radeon_cs.c:768 accepts a packet when (pkt->count+1+pkt->idx) < length_dw, so a COUNT=0 packet at end of IB has idx+1 == length_dw β ib[idx+1] reads past the IB allocation.
Reachability on this guest
No β radeon DRM HW-gated. Trigger is a CS IB ending with a packet3 (INDX_BUFFER/DRAW_*) of COUNT=0.
Phase 6 β escalation potential
This is a IB OOB read primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
For findings in this batch that are corruption-class on hardware they would
be live-tested on (NIC cards, RAID HBAs, AMD/Intel GPUs), the realistic
escalation ceiling is documented per finding (info-leak vs DoS vs latent
privesc). No uid=0 claim is made β none is reachable on this guest.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part ofmake -j6 nativekernel KERNCONF=X86_64_GENERIC(kernel build rc=0; affected module builds radeon.ko/amdgpu.ko/sound.ko/i915.ko/vga_switcheroo.ko all produced). - For musycc.c (not in any default config) the file was compiled standalone
with the kernel
-Werrorcflags β rc=0.
Reject packets with pkt->count < 1 at the top of r300_packet3_check (after the idx assignment).
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 3
- 0
- 0
- .
- c
- :
- 1
- 1
- 7
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 3
- 0
- 0
- .
- c
- :
- 1
- 1
- 9
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 3
- 0
- 0
- .
- c
- :
- 1
- 2
- 0
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- 3
- 0
- 0
- .
- c
- :
- 1
- 2
- 4
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- a
- d
- e
- o
- n
- _
- c
- s
- .
- c
- :
- 7
- 6
- 8
Detail
Exploit chain
none β radeon DRM HW-gated (no Radeon GPU in guest). Primitive is info-leak/DoS on real HW; no live escalation possible on this guest.
Evidence (decisive lines)
Source-level confirmation at sys/dev/drm/radeon/r300.c:1176, sys/dev/drm/radeon/r300.c:1191, sys/dev/drm/radeon/r300.c:1202. fix.diff applies cleanly (patch -p1 --forward: APPLIES_OK) and compiles -Werror clean as part of `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0; affected .o/.ko produced). No live trigger on this guest (HW/module gated).
PoC changes
Wrote VERDICT.md, fix.diff (one hunk: reject pkt->count < 1 at top of r300_packet3_check), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.
Verified recommended fix
At the top of r300_packet3_check after idx = pkt->idx + 1, reject if (pkt->count < 1) return -EINVAL. Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1492/fix.diff.
Verdict
r300_packet3_check idx = pkt->idx + 1 at 1176. Several opcodes (PACKET3_INDX_BUFFER :1191, _3D_DRAW_IMMD :1202, _3D_DRAW_VBUF :1229, _3D_DRAW_INDX :1243) read ib[idx+1] without checking pkt->count. Parser radeon_cs.c:768 accepts when (pkt->count+1+pkt->idx) < length_dw, so a COUNT=0 packet at end of IB has idx+1 == length_dw β ib[idx+1] reads past the IB allocation. radeon DRM HW-gated as DF-1453. Source-level confirmed.
No comments yet.