# 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_REG`s, 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.
