VCE two-pass parse desynchronization: amdgpu_vce_cs_reloc patches the NEXT command's len/cmd header via in-IB dword aliasing β attacker-synthesized command stream skips loop-1 validation and len=0 causes an infinite in-kernel loop in the CS ioctl
Summary
Loop 1 validates the IB (only len>=8 && !(len&3), no per-command minimums, unknown cmds are silent no-ops); loop 2 re-walks and calls cs_reloc which WRITES patched address dwords at idx+2/idx+3. A len=8 buffer command's next-command header IS dwords idx+2/idx+3 - the relocation overwrites it with an attacker-steerable value; loop 2 then re-parses the synthesized header: len/4==0 at :914 spins the ioctl forever in-kernel (unkillable, one CPU per submission), and everything after the desync point was never validated by loop 1. Unpriv render-node CS ioctl on vce_v2_0 ASICs; novel - still unfixed in Linux v6.6. All 2018-2019 CVE-series parser fixes verified already present. Per-command-length-floors fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4447 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| vce_cs_desync.c | β | 9.7 KB | view raw | |
| VERDICT.md | β | 5.7 KB | β raw | |
| README.md | β | 1.3 KB | β raw | |
| fix.diff | β | 3.1 KB | view raw | |
| env.txt | β | 444 B | view raw | |
| manifest.json | β | 944 B | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-4447 β VCE reloc dword aliasing desync / infinite loop
- Target:
sys/dev/drm/amd/amdgpu/amdgpu_vce.cβamdgpu_vce_ring_parse_cssecond loop +amdgpu_vce_cs_reloc - Class: parser desync β infinite kernel loop (CWE-835) + skipped validation pass
- Reach: unprivileged local user,
/dev/dri/renderD128, AMDGPU CS ioctl (AMDGPU_HW_IP_VCE) on CIK/Tonga/Fiji-class GPUs (vce_v2_0_ip_block)
Contents
vce_cs_desync.cβ dword-exact trigger (render node β GEM VRAM bo β GEM_VA map at 32 GiB β CS submissions sweeping the mapping delta; the hit submission never returns)VERDICT.mdβ full line-trace narrative, exploit arithmetic, kill/verify notesfix.diffβ per-command minimum length checks (git-apply-able, authored against the read-only sys/ tree; never applied there)env.txtβ guest environment (no AMD GPU β untestable)
Reproduce (on hardware with a VCE 2.0-era AMD GPU)
cc -O2 -o vce_cs_desync vce_cs_desync.c ./vce_cs_desync /dev/dri/renderD128
Expected: within β€1024 submissions the process hangs inside the CS ioctl
(unkillable, D-state; procstat -kk shows the thread in
amdgpu_vce_ring_parse_cs); each hung submission pins one CPU at 100%.
On the audit QEMU guest this cannot run (no AMD GPU, no /dev/dri) β see
VERDICT.md for the source-level proof.
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-readslen/cmdfrom the IB (:791-792) and patches dwords in the same IB viaamdgpu_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-836do 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:728and 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
- 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 tovce_v2_0_ip_blockwhose.parse_csis exactly this function (vce_v2_0.c:615). - 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_bois 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 1002empty), no/dev/driβ amdgpu paths cannot execute on the audit guest. Trigger source (dword-exact) is provided invce_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.
Fix verification
not_testablefix.diff authored against the read-only sys/ tree (per-command length floors + len sanity re-check in loop 2). Cannot be build- or run-validated on the audit guest: no AMD GPU, amdgpu does not attach, so neither baseline nor patched kernel can execute the VCE parse path.
findings/poc/DF-4447/fix.diff (git-apply-able unified diff vs sys/dev/drm/amd/amdgpu/amdgpu_vce.c); env.txt documents guest hardware absence
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4447/VERDICT.md (line trace); vce_cs_desync.c (dword-exact trigger: session cmd, len=8 bitstream cmd whose idx+2/idx+3 are the next header, delta sweep); amdgpu_vce.c:656-657 (patch writes), :791-792+914 (loop-2 re-read + idx advance with no len check), :728 (len check only in loop 1)
PoC changes
n/a (first evidence pack for this finding; trigger authored from scratch against the DF amdgpu source, raw ioctls, no libdrm dependency)
Verified recommended fix
Per-command minimum length checks covering every dword loop 2 reads or patches (see fix.diff): session 12, task info 32, create 44, encode 52, context/bitstream/feedback 16 (bitstream 20 for the idx+4 size field), MV 36, plus re-check len sanity in loop 2.
Verdict
Two-pass desynchronization in amdgpu_vce_ring_parse_cs: loop 2 re-reads len/cmd from IB dwords that amdgpu_vce_cs_reloc patched in the same pass. A buffer command (0x05000001/04/05/0d) with len=8 has its patched address dwords (idx+2=upper32, idx+3=lower32 of final) land exactly on the NEXT command's len/cmd header; the attacker arranges final=(len'<<32)|no-op-cmd with len' in 0..3 so idx += len/4 never advances, producing an infinite in-kernel loop inside the CS ioctl (unkillable CPU burn per submission) and letting post-desync synthesized commands skip the loop-1 validation pass entirely. Reachable unprivileged via /dev/dri/renderD128 on CIK/Tonga/Fiji (vce_v2_0 .parse_cs). Mechanism proven by dword-level trace (VERDICT.md); execution impossible on the audit guest (no AMD GPU). Distinct from DF-1625/1626/1627 (in-IB aliasing vs IB-end OOB; no size overflow; no 1<<-1). Still unfixed in Linux v6.6.
No comments yet.