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)
PoC verification
Evidence pack
findings/poc/DF-4924 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- File audited (pass 2):
sys/dev/drm/amd/amdgpu/amdgpu_ib.c - Root-cause seat:
sys/dev/drm/amd/amdgpu/gfx_v9_0.c:4745-4764(reservation) vsamdgpu_ib.c:231-232(unaccounted emission) - Severity: Low (GPU command-stream overrun contained inside the ring BO β GPU hang / cross-submission command corruption; not a kernel-memory OOB)
- Bucket:
base:dev
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 commandsbuild.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 inventoryfix.diffβ git-apply-able unified diff (authored against read-only sys/)
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_sizebreakdown = 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 fornum_ibs β {24, 88, 152}in the 1..200 sweep and for no other values β matching the closed-form rulenum_ibs β‘ 24 (mod 64). With the+2TMZ term added to the accounting (fix.diff), zero overruns for all 200 values.
Root cause (path:line)
- Reservation:
amdgpu_ib_schedule()computesalloc_size = ring->funcs->emit_frame_size + num_ibs * ring->funcs->emit_ib_size(sys/dev/drm/amd/amdgpu/amdgpu_ib.c:159-162) andamdgpu_ring_alloc()aligns it up toalign_mask(amdgpu_ring.c:64-76) β 256 dw for gfx9. - Emission: for any ring with
.emit_tmzset β gfx_v9_0 gfx ring, gfx_v9_0.c:4780 β ib_schedule emits a 2-dwordPACKET3_FRAME_CONTROLpacket unconditionally (amdgpu_ib.c:231-232 βgfx_v9_0_ring_emit_tmz, twoamdgpu_ring_write()calls). - Accounting: the gfx funcs
emit_frame_sizesum (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) logsDRM_ERROR("writing more dwords to the ring than expected!")whencount_dw <= 0but writes the dword anyway, so the last 2 dwords of the frame land past the booked window: inside the ring BO (buf_maskmasks 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_dwunderrun + DRM_ERROR per submission; wptr drift of 2 dwords per submission accumulates into subsequent submissions' space (the nextring_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.diffadds2 + /* FRAME_CONTROL (TMZ) */to gfx_v9_0.cemit_frame_size.- Applied inside the guest's /usr/src (patch hunk @4759, clean), full
amdgpu.komodule rebuilt under-Werrorβ EXIT=0, finalcc -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_testableat 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_testablefix.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.
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_ib.c:159-166
- sys/dev/drm/amd/amdgpu/amdgpu_ib.c:231-232
- sys/dev/drm/amd/amdgpu/gfx_v9_0.c:4745-4764
- sys/dev/drm/amd/amdgpu/gfx_v9_0.c:4780
- sys/dev/drm/amd/amdgpu/amdgpu_ring.c:64-76
- sys/dev/drm/amd/amdgpu/amdgpu_ring.h:276-283
- sys/dev/drm/amd/amdgpu/soc15.h:31-32
- sys/dev/drm/amd/amdgpu/amdgpu_cs.c:181-183
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).
No comments yet.