# DF-4447 — VCE two-pass parse desync via reloc dword aliasing → infinite kernel loop

**File:** `sys/dev/drm/amd/amdgpu/amdgpu_vce.c`
**Status:** untested (no AMD GPU hardware in the audit guest — mechanism proven by line-level trace below)
**Severity:** Medium · **CWE:** CWE-835 (infinite loop) / CWE-1264 (parser desync)

## Root cause

`amdgpu_vce_ring_parse_cs()` parses the same IB twice:

* **Loop 1** (`amdgpu_vce.c:724-788`) validates: only `(len < 8) || (len & 3)`
  is checked (`:728`); there is **no per-command minimum length** and **no
  default case** (unknown commands are accepted silently).
* **Loop 2** (`amdgpu_vce.c:790-915`) re-reads `len`/`cmd` from the IB
  (`:791-792`) **and patches dwords in the same IB** via
  `amdgpu_vce_cs_reloc()`.

`amdgpu_vce_cs_reloc()` writes the patched address as
`set(lo, lower_32_bits(addr)); set(hi, upper_32_bits(addr))`
(`amdgpu_vce.c:656-657`). For the bitstream command the call is
`amdgpu_vce_cs_reloc(p, ib_idx, idx+3, idx+2, tmp, bs_idx)` (`:877`), i.e. it
writes **dword[idx+2] = upper32(final)** and **dword[idx+3] = lower32(final)**.

If the bitstream command carries `len = 8` (2 dwords — accepted by loop 1,
since the only constraint is `len >= 8`), the **next command begins at dword
idx+2**: its `len` field *is* dword[idx+2] and its `cmd` field *is*
dword[idx+3]. The relocation therefore overwrites the next command's header
with the patched address. Loop 2 then advances `idx += len/4` (`:914`) and
re-parses the **patched** dwords as `len`/`cmd` — a command stream that loop 1
never validated.

## Exploit arithmetic

`final = amdgpu_bo_gpu_offset(bo) + (base − mapping->start·PAGE_SIZE) (mod 2⁶⁴)`
where `base` is the raw 64-bit value the user placed in the two address
dwords (`amdgpu_vce.c:634-654`). Loop 1's walk over the *original* stream
must survive, which constrains only `upper32(base)` (seen as a "len") to be
`>= 8` and 4-aligned — satisfied by mapping the BO at VA 0x800000000 (32 GiB,
`upper32 = 8`).

The attacker needs `final = (len′ << 32) | cmd′` with:

* `cmd′` ∈ no-op cases of loop 2 (`0x04000001`–`0x04000009`, `0x05000002`,
  `0x05000009` — cases `:828-836` do nothing),
* `len′ ∈ {0,1,2,3}` → `idx += len/4 == 0` → **infinite loop** (loop 2 has no
  length check of its own; the len check exists only in loop 1 `:728` and in
  parse_cs_vm `:958`).

`upper32(final) ≤ 3` holds automatically: loop-1's `amdgpu_vce_validate_bo`
for the bitstream pins the BO below 4 GiB (`lpfn = 0x100000000>>PAGE_SHIFT`,
`:591`), and the delta within the mapping is < 64 MiB. `lower32(final)` is
steerable: `delta = base − mapping->start` is arbitrary (not page aligned —
`base` is raw IB dwords), so sweeping delta over a 4 KiB window in 4-byte
steps (residue adjusted per no-op cmd) hits a no-op value within ≤1024
submissions; no kernel information leak is needed — the hang itself is the
oracle.

Loop-1 walk of the original stream (must error-free):
`[12,0x00000001,handle] [8,0x05000004,HI32(base),LO32(base)] [8,tmp=8? …]`
then padding pairs `(8, 0x04000001)`: the "command" at dword idx+2 has
len = `upper32(base)` = 8 and cmd = `lower32(base)` (unknown cmd → no-op in
loop 1), then `[8, tmp]`, then benign pairs to the end of the IB.

Loop-2: session → bitstream(reloc patches dword[idx+2]=0, dword[idx+3]=
0x04000001) → next iteration reads `len=0, cmd=0x04000001` → no-op case →
`idx += 0` → re-reads the same pair forever.

## Impact

1. **Unprivileged local DoS** (render node, mode 0666): the CS ioctl never
   returns; the submitting thread spins in kernel mode without sleeping,
   yielding, or checking signals. One spinning submission per CPU wedges all
   CPUs. HW: CIK (Bonaire/Hawaii/Kaveri/Kabini/Mullins — `cik.c:2024..2083`)
   and VI-era parts wired to `vce_v2_0_ip_block` whose `.parse_cs` is exactly
   this function (`vce_v2_0.c:615`).
2. **Validation bypass**: every command parsed after the desync point is
   attacker-synthesized from patched address dwords and never passed the
   loop-1 validation (e.g. the 4-GiB-boundary placement check in
   `amdgpu_vce_validate_bo` is skipped for re-injected buffer commands).
   `amdgpu_vce_cs_reloc`'s own mapping-bounds check still applies, so this is
   HW-scope (GPU fault / firmware misbehavior), not kernel OOB by itself.

Not covered by the known findings: DF-1626 is about dword offsets beyond the
*end of the IB* (needs `idx+N ≥ length_dw`); this bug's writes are *inside*
the IB and the desync is against the *command length*. DF-1625 is u32 size
overflow; DF-1627 is the `1<<session_idx` UB.

Upstream status: **still present in Linux v6.6** (same two-loop structure,
same unbounded reloc patch offsets, no len re-check in loop 2) — this is a
novel finding also affecting upstream (not part of the 2018-2019 fix series;
all of those fixes are present in this DF copy, verified by diff against
v4.17/v6.6).

## Verification attempt

* Guest: `DragonFly dfbsd 6.5-DEVELOPMENT #0 … x86_64` (env.txt). No AMD PCI
  device (`pciconf -l | grep 1002` empty), no `/dev/dri` → amdgpu paths
  cannot execute on the audit guest. Trigger source (dword-exact) is provided
  in `vce_cs_desync.c`; the mechanism is proven by the line trace above.

## Recommended fix

Tie the reloc patch/read offsets to the current command's declared length:
per-command minimum lengths in the loop-2 switch (equivalently loop 1), so no
patched dword can alias the next command's header. See `fix.diff`.

## 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.
