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

gfx_v9_0 gfx-ring emit_frame_size never reserves the 2-dword TMZ FRAME_CONTROL emission that amdgpu_ib_schedule() makes unconditionally β€” ring-reservation overrun of 2 dwords for every CS job with num_ibs == 24 (mod 64)

Summary

The frame-size accounting covers every other emission but has no TMZ term, while ib_schedule emits the 2-dword FRAME_CONTROL packet unconditionally on emit_tmz rings - with 256-dw alignment the slack is exactly zero at num_ibs==24 (mod 64): each such unpriv CS job overruns its reservation by 2 dwords (write-anyway + wptr drift, and near ring wrap the stray dwords overwrite unconsumed commands = cross-submission GPU corruption/hang). Contained in the ring BO (DoS-class). HARNESS-REPRODUCED byte-exactly on guest (2 canary dwords destroyed at 24/88/152, none otherwise); fix (+2 term) validated on full -Werror amdgpu.ko rebuild. DF-4864's sibling found by walking the same emit-vs-reserved contract.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4924 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
df4924_trigger.c β€” 6.6 KB view raw
build.sh β€” 112 B view raw
run.sh β€” 112 B view raw
build.log β€” 41 B view raw
run.log β€” 822 B view raw
run.2.log β€” 275 B view raw
fix_build.log β€” 20.5 KB view raw
env.txt β€” 522 B view raw
fix.diff β€” 589 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.9 KB view raw

DF-4924 β€” gfx_v9_0 gfx-ring emit_frame_size never reserves the TMZ FRAME_CONTROL emission that amdgpu_ib_schedule() always makes

What the bug is

amdgpu_ib_schedule() (amdgpu_ib.c:159-162) reserves emit_frame_size + num_ibs * emit_ib_size dwords on the ring, then emits a frame whose every component is supposed to be covered by the per-ASIC emit_frame_size breakdown. For the gfx_v9_0 gfx ring the funcs table sets .emit_tmz = gfx_v9_0_ring_emit_tmz (gfx_v9_0.c:4780), so ib_schedule unconditionally emits a 2-dword PACKET3_FRAME_CONTROL TMZ packet (amdgpu_ib.c:231-232, gfx_v9_0.c emit fn) β€” but the emit_frame_size sum (gfx_v9_0.c:4745-4764) has no TMZ term.

Worst-case frame = 160 dw (all terms: COND_EXEC Γ—2, PIPELINE_SYNC, TLB-flush wregs/reg-waits with soc15.h:31-32 constants 4/1, VM_FLUSH + its fence, GDS, double SWITCH_BUFFER, HDP flush/invalidate, VGT flush, CE/DE META, CNTX_CTRL, FENCE Γ—2, SWITCH_BUFFER). amdgpu_ring_alloc() (amdgpu_ring.c:66) aligns the reservation up to 256 dw (align_mask 0xff). The reservation has zero slack exactly when (160 + 4Β·num_ibs) % 256 == 0, i.e. num_ibs ≑ 24 (mod 64), and the 2 TMZ dwords then run past the allocation. amdgpu_ring_write() (amdgpu_ring.h:276-283) logs DRM_ERROR("writing more dwords to the ring than expected!") and writes anyway, so every such submission also spams the log and advances wptr past the booked window, eating into the next submission's space (and, at ring-wrap, into commands the GPU has not consumed β€” GPU command corruption β†’ ring hang β†’ device reset).

Any unprivileged DRI client can pick num_ibs freely (number of AMDGPU_CHUNK_ID_IB chunks in the CS ioctl, amdgpu_cs.c:181-183); sweeping num_ibs over 1..64 guarantees hitting the residue class without knowing the exact achieved frame total.

How to reproduce (harness β€” kernel path needs Vega10/Raven hardware)

build.sh   # cc -O2 -o df4924_trigger df4924_trigger.c   (guest, done as root under /root)
run.sh     # ./df4924_trigger

Expected (see run.log / run.2.log):

== baseline accounting (tree as-is: TMZ NOT in emit_frame_size) ==
num_ibs= 24 reserved= 256 emitted= 258 writes_past_count=2 canary_dwords_destroyed=2
num_ibs= 88 reserved= 512 emitted= 514 ... canary_dwords_destroyed=2
num_ibs=152 reserved= 768 emitted= 770 ... canary_dwords_destroyed=2
baseline: 3 overrun submission(s) in num_ibs 1..200: 24 88 152
== fixed accounting (emit_frame_size += 2 for TMZ) ==
fixed: 0 overrun submission(s) in num_ibs 1..200 (expect 0)
VERDICT: baseline overruns by exactly 2 dwords iff num_ibs % 64 == 24; fix eliminates all. REPLICATED.

The kernel-side path cannot run on the audit guest (Bochs VGA only, no AMD device, no /dev/dri, no drm modules β€” env.txt), exactly like the DF-4864 situation. The harness replicates the reservation math and ring-write discipline with the tree's exact constants.

Fix

fix.diff β€” add the missing 2-dword TMZ term to gfx_v9_0.c emit_frame_size. Applied to the guest's /usr/src and the full amdgpu.ko module rebuilt clean under -Werror (fix_build.log, EXIT=0, final link includes gfx_v9_0.o); source reverted afterwards (hashes recorded in VERDICT.md).

Artifacts

  • df4924_trigger.c β€” harness (constants transcribed with path:line comments)
  • build.sh, run.sh β€” exact commands
  • build.log, run.log, run.2.log β€” guest output (stable across runs)
  • fix_build.log β€” patched amdgpu.ko module build (clean, -Werror)
  • env.txt β€” guest uname/compiler/GPU inventory
  • fix.diff β€” git-apply-able unified diff (authored against read-only sys/)
VERDICT.md
↓ download raw

DF-4924 VERDICT β€” unreserved TMZ FRAME_CONTROL emission in the amdgpu_ib_schedule frame budget (gfx_v9_0 gfx ring)

Status: not_reproduced (runtime) β€” the kernel path is hardware-gated on this guest; the arithmetic and write pattern are harness-replicated byte-exactly and source-certain.

Reproduced?

  • Kernel-side: no β€” only because the AMD hardware is absent. The guest has a single Bochs std-VGA (chip 0x11111234), no 0x1002 PCI device, no /dev/dri, and no drm/amdgpu modules (env.txt). amdgpu_ib_schedule() on a gfx_v9_0 gfx ring requires an attached Vega10/Raven ASIC.
  • Harness-side: yes, deterministic (run.log, run.2.log β€” identical): with the tree's exact constants (emit_frame_size breakdown = 160 dw with SOC15_FLUSH_GPU_TLB_NUM_WREG=4 / NUM_REG_WAIT=1 per soc15.h:31-32, emit_ib_size=4, align_mask=0xff, TMZ emit=2 dw), submissions overrun the reserved window by exactly 2 dwords for num_ibs ∈ {24, 88, 152} in the 1..200 sweep and for no other values β€” matching the closed-form rule num_ibs ≑ 24 (mod 64). With the +2 TMZ term added to the accounting (fix.diff), zero overruns for all 200 values.

Root cause (path:line)

  • Reservation: amdgpu_ib_schedule() computes alloc_size = ring->funcs->emit_frame_size + num_ibs * ring->funcs->emit_ib_size (sys/dev/drm/amd/amdgpu/amdgpu_ib.c:159-162) and amdgpu_ring_alloc() aligns it up to align_mask (amdgpu_ring.c:64-76) β€” 256 dw for gfx9.
  • Emission: for any ring with .emit_tmz set β€” gfx_v9_0 gfx ring, gfx_v9_0.c:4780 β€” ib_schedule emits a 2-dword PACKET3_FRAME_CONTROL packet unconditionally (amdgpu_ib.c:231-232 β†’ gfx_v9_0_ring_emit_tmz, two amdgpu_ring_write() calls).
  • Accounting: the gfx funcs emit_frame_size sum (gfx_v9_0.c:4745-4764) contains every other emission site (COND_EXEC Γ—2, PIPELINE_SYNC, TLB flush wregs/reg-waits, VM_FLUSH + fence, GDS, double SWITCH_BUFFER, HDP flush, VGT flush, CE/DE META, CNTX_CTRL, HDP_INVL, FENCE Γ—2, SWITCH_BUFFER) but no TMZ term.
  • Consequence mechanics: amdgpu_ring_write() (amdgpu_ring.h:276-283) logs DRM_ERROR("writing more dwords to the ring than expected!") when count_dw <= 0 but writes the dword anyway, so the last 2 dwords of the frame land past the booked window: inside the ring BO (buf_mask masks the index β€” NOT a kernel-memory OOB) but in space belonging to the next submission, or β€” when wptr approaches rptr β€” on top of not-yet-consumed commands, corrupting the GPU command stream.

Impact ceiling

  • Every worst-case gfx CS job with num_ibs ≑ 24 (mod 64) (trivially chosen: number of AMDGPU_CHUNK_ID_IB chunks, amdgpu_cs.c:181-183) books 256Β·k dwords and emits 256Β·k+2. Unprivileged DRI client; no races.
  • Guaranteed: count_dw underrun + DRM_ERROR per submission; wptr drift of 2 dwords per submission accumulates into subsequent submissions' space (the next ring_alloc() starts at the drifted wptr, so corruption is transiently overwritten β€” but at ring wrap the 2 dwords can land on unconsumed commands β†’ GPU command corruption β†’ ring hang β†’ device reset).
  • Contained in the ring BO (no kernel-memory write) β€” hence Low severity, DoS-class ceiling, cross-submission command-corruption potential.

Fix validation

  • fix.diff adds 2 + /* FRAME_CONTROL (TMZ) */ to gfx_v9_0.c emit_frame_size.
  • Applied inside the guest's /usr/src (patch hunk @4759, clean), full amdgpu.ko module rebuilt under -Werror β€” EXIT=0, final cc -r -o amdgpu.ko ... gfx_v9_0.o ... link line present (fix_build.log).
  • Harness A/B: baseline accounting β†’ 3 overruns (n=24/88/152); fixed accounting β†’ 0 overruns across the same sweep (run.log).
  • Source reverted afterwards; guest gfx_v9_0.c sha256 back to baseline 119af8855ea0b484531066ee0a13cbc2c8005920abae895488991ff9d8a1a67c (identical to the audit tree's file).
  • Runtime kernel A/B impossible on this guest (no AMD GPU) β†’ fix_status not_testable at runtime, arithmetic-validated at harness level.

Not pushed further

A uid0 chain is not applicable: the overrun writes are masked inside the ring BO (device memory mapping), so no kernel-memory write primitive exists. The impact ceiling is GPU command corruption / DoS.

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 applied to guest /usr/src (hunk @4759 clean), full amdgpu.ko module rebuilt under -Werror EXIT=0 with gfx_v9_0.o in the final link (fix_build.log); harness A/B: baseline 3 overruns (n=24/88/152) vs fixed 0 overruns (run.log); runtime kernel A/B impossible on this guest (no AMD GPU); source reverted, sha256 back to baseline 119af8855ea0b484531066ee0a13cbc2c8005920abae895488991ff9d8a1a67c.

findings/poc/DF-4924/fix_build.log (patch + 'cc -r -o amdgpu.ko ... gfx_v9_0.o' EXIT=0); run.log '== fixed accounting ==' section (0 overruns); VERDICT.md 'Fix validation' section.
↓ fix.diffper-fix-DF-4924

Confirmed kernel references

Detail

Exploit chain

On real Vega10/Raven hardware: unpriv DRI client -> CS ioctl with num_ibs == 24 (mod 64) IB chunks targeting the gfx ring -> amdgpu_ib_schedule reserves ALIGN(160+4n,256) dwords, emits 162+4n -> 2 dwords past the window per submission (accumulating) + DRM_ERROR spam; near ring wrap the stray dwords overwrite unconsumed commands -> GPU command corruption -> ring hang / device reset (local DoS). No kernel-memory write primitive (buf_mask contains writes), so no uid0 chain.

Evidence (decisive lines)

run.log lines 'num_ibs= 24 reserved= 256 emitted= 258 writes_past_count=2 canary_dwords_destroyed=2' (and n=88/152), 'fixed: 0 overrun submission(s)', final 'VERDICT: ... REPLICATED.'; run.2.log identical; fix_build.log (patch hunk @4759, full amdgpu.ko -Werror build EXIT=0, gfx_v9_0.o in final link); env.txt (guest has no AMD GPU); VERDICT.md arithmetic + revert hashes.

PoC changes

No seed PoC existed (pass-2 discovery); harness authored from scratch, constants transcribed from the tree with path:line comments; first draft had inverted ring_write indexing (caught by review, fixed before the guest run).

Verified recommended fix

Add the missing TMZ term to gfx_v9_0.c emit_frame_size: '2 + / FRAME_CONTROL (TMZ) /' (fix.diff).

Verdict

gfx_v9_0 gfx-ring emit_frame_size (gfx_v9_0.c:4745-4764, = 160 dw worst case with soc15.h constants 4/1) has no term for the 2-dword PACKET3_FRAME_CONTROL TMZ emission that amdgpu_ib_schedule() makes unconditionally whenever ring->funcs->emit_tmz is set (gfx_v9_0.c:4780 + amdgpu_ib.c:231-232). amdgpu_ring_alloc() aligns the reservation to 256 dw (align_mask 0xff), so the reservation has zero slack exactly when (160 + 4*num_ibs) % 256 == 0, i.e. num_ibs == 24 (mod 64); the TMZ dwords then write past the booked window. amdgpu_ring_write() (amdgpu_ring.h:276-283) DRM_ERRORs and writes anyway, so every such submission also spams the log and drifts wptr into the next submission's space; at ring wrap the 2 dwords can land on unconsumed commands (GPU command-stream corruption -> ring hang). Writes are masked inside the ring BO, so this is NOT a kernel-memory OOB β€” DoS-class ceiling from any unprivileged DRI client (num_ibs = number of AMDGPU_CHUNK_ID_IB chunks, amdgpu_cs.c:181-183). Kernel path is hardware-gated on this guest (Bochs VGA, no AMD device, no /dev/dri β€” env.txt), same situation as DF-4864; the arithmetic/write pattern is replicated byte-exactly by the harness (run.log: 2 canary dwords destroyed iff num_ibs in {24,88,152} of 1..200; fixed accounting: 0).