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)
PoC verification
Evidence pack
findings/poc/DF-4278 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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; seeenv.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)
- Boot DragonFly with an r300/r350/rv350/rv370/rv380/rv515/rv530/rv570/rv580 GPU
(PCI vendor 0x1002) and
radeonKMS active (kldload radeonkms/ kernel driver). ./build.sh && ./run.sh- Vulnerable kernel: exit 0, "CS accepted" β the checker patched
ib[1] = 0xF0000000 + gpu_offsetwith no bound check; any subsequent draw with the occlusion count enabled increments memory at that GPU VA. - 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 β 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_testablefix.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)
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.
No comments yet.