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

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 1191 ib[idx+1] = radeon_get_ib_value(p, idx + 1) + ((u32)reloc->gpu_offset) is both an OOB read (via radeon_get_ib_value) and an OOB write (to p->ib.ptr[length_dw]) when COUNT<1. Then r300.c:1192 calls r100_cs_track_check_pkt3_indx_buffer which at r100.c:1909 does value = radeon_get_ib_value(p, idx + 2) = ib[pkt->idx+3] = ib[length_dw+1], a second OOB read, when COUNT<2. Normal INDX_BUFFER has COUNT=2 (3 data dwords: IB_INFO, IB_BASE_LO, IB_NUM_INDICES).
  • PACKET3_3D_DRAW_IMMD (r300.c:1198): line 1202 radeon_get_ib_value(p, idx + 1) and line 1206 read ib[length_dw] OOB when COUNT<1.
  • PACKET3_3D_DRAW_VBUF (r300.c:1228): line 1229 radeon_get_ib_value(p, idx + 1) OOB read when COUNT<1.
  • PACKET3_3D_DRAW_INDX (r300.c:1242): line 1243 radeon_get_ib_value(p, idx + 1) OOB read when COUNT<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_BUFFER into p->ib.ptr[length_dw] β€” this is the SA sub-allocator pool (GTT-backed system memory mapped via radeon_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_offset added 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:346 kvmalloc_array) radeon_get_ib_value reads from kdata[length_dw], 4 bytes past a kvmalloc'd slab array, potentially leaking the first dword of an adjacent slab object (kernel pointer or other sensitive data) into track->vap_vf_cntl, which influences size computations and DRM_ERROR/dmesg output in r100_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):

  1. Open /dev/dri/card0 (DRM_IOCTL_AUTH or render node).
  2. Create a reloc target GEM buffer via DRM_IOCTL_RADEON_GEM_CREATE + DRM_IOCTL_RADEON_GEM_SET_DOMAIN (size >= 4096, GTT domain).
  3. Build CS chunks: - IB chunk: length_dw = 2. Contents: ib[0] = 0xC0003300 (PACKET3 header: type=3, opcode=0x33 INDX_BUFFER, COUNT=0); ib[1] = 0x00000000 (dummy data dword). - Reloc chunk: one radeon_cs_reloc referencing the GEM buffer, with write_domain matching, and reloc_indices pointing at ib offset 1 (the data dword the handler will patch).
  4. Submit via DRM_IOCTL_RADEON_CS.
  5. Result: r300_cs_parse β†’ radeon_cs_packet_parse accepts the packet (check: (0+1+0)=1 < 2). r300_packet3_check enters PACKET3_INDX_BUFFER. radeon_cs_packet_next_reloc succeeds. Line 1191: radeon_get_ib_value(p, 0+1+1) reads kdata[2] = 4 bytes past the 8-byte kvmalloc slab allocation (OOB READ of adjacent slab data). Then writes p->ib.ptr[2] = read_value + reloc->gpu_offset (OOB WRITE, 4 bytes past the SA-allocated IB buffer). Then r100_cs_track_check_pkt3_indx_buffer reads ib[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'.

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.

  • DF-1209 (sibling, r100.c): LOAD_VBPNTR c==0 underflow, reachable from this file via r100_packet3_load_vbpntr at r300.c:1180.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1492 Β· 10 files
FileTypeDescriptionSize
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
README.md readme human-readable summary
↓ download 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.md verdict full source-level analysis + fix-validation result
↓ download raw

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 in fix_apply.log).
  • Compiled with -Werror as part of make -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 -Werror cflags β€” 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

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.