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)
PoC verification
Evidence pack
findings/poc/DF-4028 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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()initializestrack->cb_color_size_idx[i] = 0for 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 theCB_COLORn_SIZEregister (r600_cs.c:1176-1179).r600_cs_track_validate_cb()β reached fromr600_cs_track_check()at every draw packet (r600_cs.c:1721,1733,1745) whenever the target is enabled inCB_TARGET_MASK, has a valid color format inCB_COLORn_INFOand a relocatedCB_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):
- 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-2033range-checks only the register window), then the benign setup packets (DB_DEPTH_CONTROL=0to keepvalidate_dbquiet,CB_COLOR0_INFOwith a valid format +ARRAY_LINEAR_GENERAL,CB_TARGET_MASK=0xF,CB_COLOR0_BASE+ reloc NOP β noCB_COLOR0_SIZE), thenDRAW_INDEX_AUTO. - During the draw,
r600_cs_track_validate_cb(p, 0)writesib[0] = 0(r600_cs.c:468). - Parsing continues and succeeds; the patched IB goes to the GPU.
- The CP now reads
ib[0] == 0= type-0, count-0, reg-0 packet: it consumesib[0]andib[1]as one register write and resumes command execution atib[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 ofr600_cs_track_validate_cb(r600_cs.c:350-514) with default-initialized track values and prints the value stored toib[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 withoutCB_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/driabsent, 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_testablefix.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
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.
No comments yet.