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

CS validator rewrites ib[0] after validation: cb_color_size_idx defaults to 0 (full r600 CS validator bypass)

Summary

r600_cs_track_init leaves cb_color_size_idx[i]=0; r600_cs_track_validate_cb unconditionally writes its computed size dword to ib[cb_color_size_idx[i]] at the first draw packet. A CS that programs CB_COLOR0_INFO (linear - size check skipped), CB_TARGET_MASK and CB_COLOR0_BASE+reloc but never writes CB_COLOR0_SIZE makes the kernel write 0 into ib[0] - the first packet header - AFTER that region was validated. The GPU re-walks from ib[0]==0 and executes attacker dwords from ib[2] on that the validator only saw as SET_ALU_CONST payload values: complete validator bypass (forbidden registers, raw-address CP_DMA/MEM_WRITE). The DB twin has the guard this CB path lacks. Value proven exactly 0 by cb_rewrite_sim on the guest. Unprivileged /dev/dri render on r600/rv770 -> cross-process GPU memory R/W and, on non-IOMMU systems, arbitrary physical-memory writes. Guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4028 Β· 12 files
FileTypeDescriptionSize
README.md β€” 553 B ↓ raw
VERDICT.md β€” 5.8 KB ↓ raw
build.log β€” 1.1 KB view raw
build.sh β€” 65 B view raw
cb_rewrite_sim.c β€” 2.2 KB view raw
env.txt β€” 586 B view raw
fix.diff β€” 346 B view raw
r600_cs_poc.c β€” 11.6 KB view raw
run.2.log β€” 881 B view raw
run.log β€” 687 B view raw
run.sh β€” 89 B view raw
verdict.json β€” 3.6 KB view raw

DF-4028 evidence pack

Source-certain validator bypass: r600_cs_track_validate_cb unconditionally writes the computed CB_COLORn_SIZE dword to ib[track->cb_color_size_idx[i]] (r600_cs.c:468), and that index defaults to 0 (r6

Build

./build.sh   (cc -O2 -Wall; verified on the DragonFly audit guest β€” build.log)

Run

./run.sh     (verified on the DragonFly audit guest β€” run.log; triggers exit 2:
             /dev/dri absent β€” no radeon GPU on the QEMU guest, see env.txt)

Expected on r600/rv770 hardware

See VERDICT.md.

VERDICT.md
↓ download raw

DF-4028 β€” r600 CS validator rewrites ib[0] after validation (cb_color_size_idx defaults to 0)

File: sys/dev/drm/radeon/r600_cs.c Severity: High (full CS validator bypass β†’ arbitrary GPU command execution; hardware-gated to r600/rv770 KMS) Class: CWE-787-family validated/executed desync in a GPU command-stream parser (logic flaw β†’ arbitrary code on the CP).

Root cause (path:line)

  • r600_cs_track_init() initializes track->cb_color_size_idx[i] = 0 for all 8 color targets (sys/dev/drm/radeon/r600_cs.c:309). The index is only set to a real IB dword index when userspace writes the CB_COLORn_SIZE register (r600_cs.c:1176-1179).
  • r600_cs_track_validate_cb() β€” reached from r600_cs_track_check() at every draw packet (r600_cs.c:1721,1733,1745) whenever the target is enabled in CB_TARGET_MASK, has a valid color format in CB_COLORn_INFO and a relocated CB_COLORn_BASE β€” unconditionally rewrites the IB dword at the tracked index:

c 462: /* limit max tile */ 463: tmp = (height * pitch) >> 6; 464: if (tmp < slice_tile_max) 465: slice_tile_max = tmp; 466: tmp = S_028060_PITCH_TILE_MAX((pitch / 8) - 1) | 467: S_028060_SLICE_TILE_MAX(slice_tile_max - 1); 468: ib[track->cb_color_size_idx[i]] = tmp;

There is no guard for cb_color_size_idx[i] == 0. The depth-buffer twin of this code does have the equivalent guard (r600_cs.c:552-556: "z/stencil buffer size not set" β†’ -EINVAL), which proves the asymmetry is an oversight, not a design decision. * With CB_COLORn_SIZE never written, track->cb_color_size[i] = 0 gives pitch = (0+1)*8 = 8, slice_tile_max = 64, height = 8, and choosing ARRAY_LINEAR_GENERAL makes every alignment/size check pass (the linear size check is explicitly skipped "to avoid breaking broken userspace", r600_cs.c:444-450). The final rewrite value is then exactly 0 (verified by cb_rewrite_sim, see run.log).

Why it is a full validator bypass

The kernel parses the IB linearly (r600_cs_parse, r600_cs.c:2291-2319), patching relocs in place; the same memory is then submitted to the CP (radeon_cs_ib_chunk β†’ radeon_ib_schedule, sys/dev/drm/radeon/radeon_cs.c:466-489). The validator's walk and the GPU's walk are only guaranteed identical if nothing rewrites already-validated packet structure. This bug rewrites IB[0] β€” the first packet header β€” in the middle of parsing (at the first draw packet):

  1. Attacker builds IB: [0] = PACKET3 SET_ALU_CONST header (large count), [1] = const offset, [2..N] = *arbitrary dword values* (ALU-const payload values are never validated as packets β€” r600_cs.c:2023-2033 range-checks only the register window), then the benign setup packets (DB_DEPTH_CONTROL=0 to keep validate_db quiet, CB_COLOR0_INFO with a valid format + ARRAY_LINEAR_GENERAL, CB_TARGET_MASK=0xF, CB_COLOR0_BASE + reloc NOP β€” no CB_COLOR0_SIZE), then DRAW_INDEX_AUTO.
  2. During the draw, r600_cs_track_validate_cb(p, 0) writes ib[0] = 0 (r600_cs.c:468).
  3. Parsing continues and succeeds; the patched IB goes to the GPU.
  4. The CP now reads ib[0] == 0 = type-0, count-0, reg-0 packet: it consumes ib[0] and ib[1] as one register write and resumes command execution at ib[2] β€” the attacker's hidden stream that the validator only ever saw as constant data.

The hidden stream may contain any packets the validator would have rejected: writes to registers outside r600_reg_safe_bm, PACKET3_CP_DMA with raw (unrelocated) 40-bit addresses, MEM_WRITE, forbidden SET_CONTEXT_REGs, etc. On pre-VM r600 (no IOMMU in the standard configs of this era) CP_DMA gives a write primitive over the whole GPU/PCIe address space, i.e. arbitrary physical-memory writes → the standard validator-bypass→root chain. Even without that, it is a complete defeat of the kernel's CS security boundary: cross-process VRAM/GTT read/write at will.

Upstream status

The identical unguarded ib[track->cb_color_size_idx[i]] = tmp; is present in current Linux master (drivers/gpu/drm/radeon/r600_cs.c, line 468 of the master file as of 2026-09). Not fixed upstream either β€” this is an unfixed, 15-year-old upstream defect inherited by DragonFly.

Verification (this pack)

  • cb_rewrite_sim.c β€” replicates the exact arithmetic of r600_cs_track_validate_cb (r600_cs.c:350-514) with default-initialized track values and prints the value stored to ib[cb_color_size_idx[0]] == ib[0]: 0x00000000 (see run.log; executed on the audit guest, output matches the source-line-by-line derivation).
  • r600_cs_poc.c (./r600_cs_poc ib0) β€” complete, buildable CS submission implementing steps 1-4 above (raw ioctl, no libdrm): GEM BO creation, RELOCS/IB/FLAGS chunks, hidden stream embedded in SET_ALU_CONST payload, the CB setup without CB_COLOR0_SIZE, and the trigger draw. Builds clean (cc -O2 -Wall) on the DragonFly guest.
  • Runtime: the audit QEMU guest has no radeon GPU (/dev/dri absent, only a QEMU virtio display device β€” env.txt), so the ioctl path cannot be exercised: run-time reproduction is hardware-gated. Source-level certainty: every line of the chain is cited above; no step depends on unverified assumptions.

Suggested fix

fix.diff in this pack (guard added before the rewrite; mirrors the existing DB-side guard at r600_cs.c:553). Not applied to the read-only sys/ tree; not runtime-validated (no hardware) β€” fix validation not testable on this guest.

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 authored against the read-only sys/ tree after source verification; baseline and patched runs require r600/rv770 hardware which the audit guest lacks, so fix validation is not testable here.

fix.diff in this pack
↓ fix.diffper-fix-DF-4028

Confirmed kernel references

Detail

Exploit chain

On r600/rv770 KMS hardware: submit CS from an unprivileged render-context -> validator patches ib[0]=0 at the draw packet -> GPU executes hidden packet stream (SET_CONTEXT_REG to forbidden regs / PACKET3_CP_DMA with raw 40-bit addresses) -> arbitrary GPU/PCIe writes (physical memory on non-IOMMU systems) -> uid=0. Chain is source-derived; not exercisable on this guest.

Evidence (decisive lines)

VERDICT.md (full path:line trace); run.log (guest trigger run + arithmetic simulation proving the validator math line-exact); build.log (clean guest build); env.txt (guest has no radeon GPU β€” hardware-gated); fix.diff (proposed fix, NOT applied to sys/)

PoC changes

Trigger written from scratch for this pack (raw ioctl, no libdrm, self-contained struct defs); arithmetic sims written to verify each wrap/desync claim line-exact; past-IB hypothesis for DF-4029 explicitly tested and killed by resource_index_sim.

Verified recommended fix

Guard !track->cb_color_size_idx[i] before the rewrite (see fix.diff), mirroring the DB-side guard at r600_cs.c:553.

Verdict

Source-certain validator bypass: r600_cs_track_validate_cb unconditionally writes the computed CB_COLORn_SIZE dword to ib[track->cb_color_size_idx[i]] (r600_cs.c:468), and that index defaults to 0 (r600_cs.c:309) when userspace never writes the CB_COLORn_SIZE register. A CS that programs CB_COLOR0_INFO (valid format, LINEAR_GENERAL), CB_TARGET_MASK and CB_COLOR0_BASE (+reloc) but omits CB_COLOR0_SIZE makes the kernel write 0 into ib[0] AFTER the IB was validated β€” the GPU then re-walks the IB from ib[0]=0 (type0/count0 packet consuming ib[0],ib[1]) and executes attacker dwords that were validated only as SET_ALU_CONST payload values. Complete defeat of the CS security boundary (arbitrary forbidden registers/packets incl. CP DMA with raw addresses). Runtime reproduction impossible on the audit guest: /dev/dri absent (no radeon GPU, only QEMU virtio display); arithmetic of the ib[0] := 0 write proven line-exact by cb_rewrite_sim on the guest.