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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4447 Β· 7 files
FileTypeDescriptionSize
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_cs second 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 notes
  • fix.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.

VERDICT.md
↓ download raw

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.

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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4447

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.