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

R300_ZB_ZPASS_ADDR reloc patch records nothing and validates nothing: unvalidated u32 offset past the relocation BO becomes the GPU occlusion-counter write target (cross-BO GPU OOB increment-write)

Summary

r300_packet0_check handles every other GPU-address-bearing register by storing offset+robj into the track state so the final check enforces offset+size <= bo_size. The ZB_ZPASS_ADDR case only patches ib[idx] = idx_value + gpu_offset and records nothing, so the user-supplied u32 idx_value is never bounded against the target BO: the GPU SC unit increments a 4-byte counter at an attacker-chosen GPU VA up to +4GB past a GTT BO base - repeatable cross-BO corruption of other processes' buffers, the GTT-resident IB pool, or scanout; worst case post-validation corruption of a pool IB yielding an unvalidated GPU command stream. Unprivileged via render node on r300..rv570. The r100 twin shares the gap. Pure-ioctl PoC ships; hardware-gated. Bound-the-immediate fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4278 Β· 9 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
zpass_oob.c β€” 4.4 KB view raw
build.sh β€” 86 B view raw
run.sh β€” 104 B view raw
fix.diff β€” 791 B view raw
env.txt β€” 424 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.3 KB view raw

DF-4278 evidence pack β€” R300_ZB_ZPASS_ADDR unvalidated reloc offset β†’ GPU OOB write

  • Finding: sys/dev/drm/radeon/r300.c:1073-1082 (case R300_ZB_ZPASS_ADDR)
  • Class: CS-checker bypass β†’ GPU-side out-of-bounds write (cross-BO corruption)
  • Reach: unprivileged local user with DRM render access β†’ DRM_IOCTL_RADEON_CS
  • Verdict: untested β€” hardware-gated (no r300-class GPU on the audit guest; see env.txt)

What this pack contains

file what
zpass_oob.c pure-ioctl trigger: 4-dword CS setting ZB_ZPASS_ADDR = bo_gpu_offset + 0xF0000000
build.sh cc -O2 -o zpass_oob zpass_oob.c (needs a DragonFly system with radeon headers)
run.sh runs the trigger, expects /dev/dri/card0 backed by r300..rv570 hw
env.txt guest uname + proof no ATI/radeon device exists on the audit guest
fix.diff git-apply-able one-hunk fix adding the missing bo-size validation
manifest.json / verdict.json machine-readable verdict (status untested)

How to reproduce (on real hardware)

  1. Boot DragonFly with an r300/r350/rv350/rv370/rv380/rv515/rv530/rv570/rv580 GPU (PCI vendor 0x1002) and radeon KMS active (kldload radeonkms / kernel driver).
  2. ./build.sh && ./run.sh
  3. Vulnerable kernel: exit 0, "CS accepted" β€” the checker patched ib[1] = 0xF0000000 + gpu_offset with no bound check; any subsequent draw with the occlusion count enabled increments memory at that GPU VA.
  4. Patched kernel (fix.diff): exit 2, -EINVAL.

Why this is a real gap (static proof, path:line)

The checker's own discipline for every other GPU-address register:

register tracked? validated where
R300_RB3D_COLOROFFSET0..3 (r300.c:654) track->cb[i] r100.c:2264 pitch*cpp*maxy + offset <= bo_size
R300_ZB_DEPTHOFFSET (r300.c:671) track->zb r100.c:2283
R300_RB3D_AARESOLVE_OFFSET (r300.c:1115) track->aa r100.c:2303
R300_TX_OFFSET_0..15 (r300.c:684) track->textures[i] r100.c texture check (size > bo_size)
R300_ZB_ZPASS_ADDR (r300.c:1073) nothing nowhere

The r100 twin (RADEON_RB3D_ZPASS_ADDR, r100.c:1778) has the identical gap, which shows the omission is systemic, not an intentional exception.

VERDICT.md
↓ download raw

VERDICT β€” DF-4278: R300_ZB_ZPASS_ADDR reloc patch lacks bo-bounds validation β†’ GPU OOB write

status: untested | reproduced: 0 | impact: none (not demonstrated on guest) | confidence: likely

Bottom line

The code defect is certain and provable by reading the tree; runtime verification is impossible on the audit guest because it has no ATI/radeon GPU (QEMU virtual VGA only β€” env.txt). This is a hardware-gated finding (r300..rv570 class asics served by r300.c).

Root cause (path:line)

sys/dev/drm/radeon/r300.c:1073-1082, in r300_packet0_check():

    case R300_ZB_ZPASS_ADDR:
        r = radeon_cs_packet_next_reloc(p, &reloc, 0);
        if (r) { ... return r; }
        ib[idx] = idx_value + ((u32)reloc->gpu_offset);   /* <-- no bound check */
        break;

idx_value is the raw u32 the user placed in the IB after the PACKET0(0x4f5c, 0) header. The patch makes the GPU occlusion-query counter address = reloc_bo_gpu_offset + idx_value. Every sibling address register (COLOROFFSET/DEPTHOFFSET/AARESOLVE_OFFSET/TX_OFFSET) stores offset + robj into track and r100_cs_track_check() later enforces offset + pitch*cpp*maxy <= radeon_bo_size(robj) (r100.c:2264, 2283, 2303). ZPASS_ADDR stores nothing and validates nothing β†’ the user picks ANY u32 offset past the relocation BO's base, and the GPU's SC unit increments a 4-byte counter there on every passing pixel.

Attack path

Unprivileged user β†’ open /dev/dri/card0 (render access) β†’ DRM_IOCTL_RADEON_CS (radeon_cs_ioctl, radeon_cs.c) β†’ chunk IB parsed by r300_cs_parse (r300.c:1274) β†’ r100_cs_parse_packet0 (r100.c:1375, 0x4f5c is in r300_reg_safe_bm) β†’ r300_packet0_check case R300_ZB_ZPASS_ADDR (r300.c:1073) β†’ patched IB executes on the CP β†’ Z-pass counting writes at gpu_offset + attacker_u32.

Impact analysis (why "likely" not "certain")

  • The missing check is certain (direct code reading; systemic across r100.c:1778 and r300.c:1073).
  • The impact ceiling is a repeatable GPU-side increment-write (add N pixels per draw) at an attacker-chosen 32-bit GPU VA. GTT BOs are packed contiguously in the aperture, so a large offset lands in neighboring allocations: other users' BOs, the kernel-managed radeon IB pool (radeon_sa_manager, GTT-resident), or scanout buffers. Corrupting a to-be-executed IB after validation is the theoretical worst case (unvalidated GPU commands), which is why this matters beyond "my own buffer".
  • It does not directly write host kernel memory (GPU VA space only) and requires r300..rv570 hardware β€” hence Medium, not High.
  • Identical code exists in upstream Linux (carried for 15+ years); this does not make it less of a DragonFly defect, it makes coordinated disclosure easier.

Why untested (per the honest-status table)

Guest is DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC) under QEMU; pciconf -l shows no vendor-0x1002 device and no radeon DRM device (env.txt). The cited path is unreachable at runtime on this guest β€” hardware-gated. No amount of guest work can change that, so the trigger is shipped as a faithful pure-ioctl recipe (zpass_oob.c) with exact expected output for vulnerable vs patched kernels.

Fix

fix.diff adds the missing validation consistent with the sibling cases:

    case R300_ZB_ZPASS_ADDR:
        r = radeon_cs_packet_next_reloc(p, &reloc, 0);
        if (r) { ... }
        if ((u64)idx_value + 8 > radeon_bo_size(reloc->robj)) {
            DRM_ERROR("ZB_ZPASS_ADDR offset %u past BO\n", idx_value);
            return -EINVAL;
        }
        ib[idx] = idx_value + ((u32)reloc->gpu_offset);
        break;

(8 bytes covers r500's 64-bit occlusion counter; r300 uses 4.)

Fix validation on hw-gated paths cannot be run on this guest β†’ fix_status: not_testable.

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_testable
baseline no→ patch + rebuild →patched clean

fix.diff adds the missing idx_value+8 <= radeon_bo_size(reloc->robj) check; cannot be kernel-built and exercised on the audit guest because the radeon device (and thus the CS path) does not exist there β€” hardware-gated validation

findings/poc/DF-4278/fix.diff (sha256 19516d00787c2a2eaafe1d8d902ce382f694fa6e99461cac68d1036ae63d9f67)
↓ fix.diffper-fix-DF-4278

Confirmed kernel references

Detail

Exploit chain

open /dev/dri/card0 -> DRM_IOCTL_RADEON_CS with IB [PACKET0(0x4f5c,0), 0xF0000000, NOP+reloc] -> checker accepts (no bound) -> CP programs ZB_ZPASS_ADDR = bo_gpu_offset + 0xF0000000 -> draw with occlusion counting enabled -> GPU increments 4 bytes at attacker-chosen GPU VA -> cross-BO corruption; theoretical ceiling: corrupt an IB in the GTT pool after validation => unvalidated GPU command stream.

Evidence (decisive lines)

VERDICT.md (sibling-case comparison table); zpass_oob.c (exact 4-dword CS recipe); env.txt (no 0x1002 device on guest); fix.diff (validated-bound check mirroring sibling cases).

PoC changes

no seed present; authored from scratch as a pure-ioctl trigger (no libdrm dependency)

Verified recommended fix

Validate the ZB_ZPASS_ADDR immediate against the relocation BO size before patching (idx_value + 8 <= radeon_bo_size(reloc->robj)) as in fix.diff.

Verdict

Certain code defect, hardware-gated runtime: r300_packet0_check's R300_ZB_ZPASS_ADDR case (r300.c:1073-1082) patches the occlusion-counter GPU address as ib[idx] = idx_value + reloc->gpu_offset without storing offset/robj in the track state and without any bo-size validation β€” unlike every sibling address register (COLOROFFSET/DEPTHOFFSET/AARESOLVE_OFFSET/TX_OFFSET), which are all later enforced against radeon_bo_size() in r100_cs_track_check (r100.c:2264/2283/2303). An unprivileged DRM user therefore selects an arbitrary u32 offset past a GTT BO base where the GPU SC unit increments a 4-byte counter per passing pixel β€” a repeatable GPU-side OOB write into neighboring GTT/VRAM allocations (other users' BOs, the GTT-resident radeon IB pool, scanout). The QEMU audit guest has no ATI/radeon device (env.txt), so the trigger could not be executed: status untested, impact not demonstrated locally. The r100 twin (r100.c:1778) shares the gap.