gfx_v8_0_parse_ind_reg_list: trailing-entry heap OOB read/write past the RLC register-list copy plus post-write BUG_ON panic
Summary
The RLC save/restore indirect-list parser does ind_offset += 2 then reads and writes register_list_format[ind_offset] without re-checking ind_offset < list_size. A list ending on a truncated entry reads and writes 4-8 bytes past the end of the kmalloc(reg_list_format_size_bytes) copy - OOB even with DF-1133 fixed, since this is a private correctly-sized copy. The BUG_ONs fire after the writes they guard, so >9 entries or >7 unique indices panic the kernel. Distinct from pass-1 DF-1133 (init_microcode size arithmetic) - this is the parser's own bounds defect, reachable on CZ/Stoney/Polaris11/12/VegaM at every hw_init/rlc_resume. VERIFIED: verbatim parser replica built and run on the audit guest - 'HEAP OOB WRITE CONFIRMED canary at +4 bytes past 72-byte allocation CLOBBERED to 0x1' and 'BUG_ON fired' on the 8th unique index. Malicious/malformed rlc firmware module (root-replaceable, distro-supplied): firmware picks the allocation size (slab class) and truncation point -> small-int heap overwrite of the adjacent slab object -> slab-groom path to kernel code execution; BUG_ON variant is load-time DoS. Kernel chain needs AMD HW (virtio-only guest) - inconclusive at runtime.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3597 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_dfr3597.c | β | 5.1 KB | view raw | |
| build.sh | β | 96 B | view raw | |
| run.sh | β | 116 B | view raw | |
| build.log | β | 246 B | view raw | |
| run.log | β | 713 B | view raw | |
| README.md | β | 4.1 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-3597 β gfx_v8_0_parse_ind_reg_list: trailing-entry heap OOB read/write + BUG_ON panic (RLC save/restore list walk)
What
gfx_v8_0_parse_ind_reg_list() (sys/dev/drm/amd/amdgpu/gfx_v8_0.c:3974-4022)
walks the RLC "indirect register list" copied out of the RLC v2.0 firmware
header in 3-dword entries. Inside the loop it does ind_offset += 2 and then
dereferences register_list_format[ind_offset] (read at :4008/:4013, write
at :4020) without re-checking ind_offset < list_size. If the firmware list
ends on a truncated entry (last word != 0xFFFFFFFF at index list_size-1 or
list_size-2), the function:
- reads 4-8 bytes past the end of the
kmalloc(reg_list_format_size_bytes)heap buffer (gfx_v8_0.c:4033-4038 β a fresh copy, so this is OOB even when DF-1133's copy bounds are fixed), and - writes a small integer (the dedup index, 0..7) 4-8 bytes past the end of that heap allocation (gfx_v8_0.c:4020), corrupting the adjacent slab object, and
- additionally
BUG_ON()s (kernel panic) when the firmware declares more than 7 unique indirect indices (:4017) or more than 9 list entries (:3993) β the checks fire after the array writes they nominally guard.
Distinct from pass-1 DF-1133 (which covers the kmalloc(a+b) wrap and the
unvalidated reg_list_format_array_offset_bytes in gfx_v8_0_init_microcode);
this finding is the parser's own bounds defect on a correctly-sized local
copy, reachable even with DF-1133 fixed.
Reachability (source-proven)
gfx_v8_0_hw_init gfx_v8_0.c:4864
-> gfx_v8_0_rlc_resume gfx_v8_0.c:4872 -> :4176
-> gfx_v8_0_init_pg gfx_v8_0.c:4180 -> :4127
(CHIP_CARRIZO|CHIP_STONEY|CHIP_POLARIS11|CHIP_POLARIS12|CHIP_VEGAM)
-> gfx_v8_0_init_save_restore_list :4132/:4141 -> :4024
kmalloc(reg_list_format_size_bytes) :4033
memcpy(list copy) :4037
-> gfx_v8_0_parse_ind_reg_list :4040 -> :3974
Driver load / resume / GPU reset on any CZ, Stoney, Polaris11/12, VegaM APU parses the firmware-supplied list through this function.
Threat model
Malicious or malformed RLC firmware image (same model as DF-1133/DF-1134: the
firmware blob is amdgpufw_<asic>_rlc, loaded as a kernel firmware module β
root-replaceable, distro/mirror-supplied). The OOB write is a classic
small-value / attacker-chosen-offset heap overflow: the allocation size
(reg_list_format_size_bytes) selects the slab, the truncation point selects
whether +4 or +8 bytes past the end are clobbered.
Reproduce (logic replica β kernel path needs AMD HW)
./build.sh # cc -O0 -Wall -o poc_dfr3597 poc_dfr3597.c (guest has cc 8.3) ./run.sh
Expected (observed on the audit guest, DragonFly 6.5-DEVELOPMENT, cc 8.3):
mode=oob: buffer=18 live ints (72 bytes = reg_list_format_size_bytes) last live word [17] = 0xb0000 != 0xFFFFFFFF -> parser does ind_offset += 2 then READS register_list_format[19] (4-8 bytes past end) then WRITES register_list_format[19] = indices (heap OOB WRITE) RESULT: canary at live-index 19 (byte +4 past allocation) CLOBBERED to 0x1 HEAP OOB WRITE CONFIRMED (value written = small index int, offset 4 bytes past end) ... mode=bugon: list_size=38 elems, 8 unique indirect indices [BUG_ON fired: kernel would PANIC here] indices_count=8 bugon_fired=1 -> PANIC (BUG_ON) proven
The harness is a verbatim copy of the kernel parser body; only BUG_ON is
redefined to report instead of halting. Canary placement emulates the adjacent
slab object.
Kernel-side verification status
The audit QEMU guest is virtio-only (no AMD GPU; kldstat -v | grep -c drm
= 0, no /boot/modules, pciconf -l shows no 0x1002 device) so the in-kernel
path could not be executed: verdict inconclusive for the kernel run, while
the algorithm-level OOB is proven deterministically by the replica and by
line-by-line source analysis above.
Fix
fix.diff in this directory: bound-check ind_offset + 2 < list_size before
the dereference (dropping truncated entries with an error), and convert the
post-write BUG_ONs into pre-write WARN_ON guards that return.
DF-3597 β VERDICT
status: inconclusive (kernel run) / algorithm proven (replica) reproduced: 0 (no AMD hardware/module on the audit guest) source-level confidence: certain
What was verified and how
- Source trace (certain).
gfx_v8_0_parse_ind_reg_list(gfx_v8_0.c:3974-4022) is handed a private heap copy of the RLC firmware's register-list-format array (kmalloc:4033,memcpy:4037,list_size = reg_list_format_size_bytes >> 2:4042). The loop consumes 3-dword entries: at :4001 it executesind_offset += 2and then readsregister_list_format[ind_offset]at :4008/:4013 and writes it at :4020 β with noind_offset < list_sizere-check. When the last list word is not the 0xFFFFFFFF terminator,ind_offsetbecomeslist_size(+1 word OOB) orlist_size+1(+2 words OOB):
- OOB read of the kmalloc buffer at gfx_v8_0.c:4008 and :4013 β the
value read lands in
unique_indices[]and is later programmed intommRLC_SRM_INDEX_CNTL_{ADDR,DATA}_n(:4079-4080); no direct user-visible leak, but attacker-controlled garbage in RLC SRM index registers. - OOB write at gfx_v8_0.c:4020 β
register_list_format[ind_offset] = indiceswrites an int in [0,7] four or eight bytes past the end of a firmware-sized heap object. BUG_ON(*offset_count >= max_offset):3993 andBUG_ON(*indices_count >= max_indices):4017 fire after the array writes they guard; a list with a 10th entry or an 8th unique indirect index panics the kernel (firmware-controlled DoS).
This is independent of pass-1 DF-1133 (init_microcode size arithmetic and array offsets) β the parser walks past the end of an exactly-sized local copy even with DF-1133 fixed.
-
Logic replica (deterministic).
poc_dfr3597.cis the kernel parser verbatim (BUG_ON redefined to report). Built and executed on the audit guest (DragonFly 6.5-DEVELOPMENT x86_64, cc 8.3): * modeoob: 18-element list ending in a truncated entry β write of0x1at element 19 (+4 bytes past the 72-byte allocation) β canary clobbered. Seerun.log. * modebugon: 8 unique indirect indices β BUG_ON fires (kernel panic equivalent). -
Why not a kernel run. The guest is virtio-only:
pciconf -llists no AMD vendor (0x1002) device,kldstat -v | grep -c drm= 0, and/boot/modulesdoes not exist β the amdgpu driver is neither loadable nor present. The in-kernel trigger requires a Carrizo/Stoney/Polaris11/12/VegaM machine plus a craftedamdgpufw_*_rlcfirmware module. Hence the honest classificationinconclusivefor the kernel path; the algorithm and reachability are proven from source and the replica.
Exploit chain (would-be, for the maintainer's threat model)
replace amdgpufw_<asic>_rlc module β RLC header v2.0 fields
(reg_list_format_size_bytes = allocation size β slab-class selection;
list content = truncation point β +4/+8 byte write offset) β at driver load,
kmalloc'd copy parsed β small-int (0..7) heap overwrite of the adjacent
object's first 4-8 bytes β groom an adjacent victim whose type/flags/length
field is corrupted β classic slab-exploitation path to code execution in
kernel context. The BUG_ON variant is a straight load-time DoS.
Fix validation
fix.diff adds (a) if (ind_offset + 2 >= list_size) { DRM_ERROR(...);
return; } before the +=2 dereference, and (b) converts both post-write
BUG_ONs into pre-write WARN_ON guards. Not build-testable on this guest
(no kernel toolchain for the DRM module, module absent from the tree build);
fix_status: not_testable with the diff line-matched against
gfx_v8_0.c:3986-4021 in the current tree.
kernel_refs
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:3974-4022 (parser, OOB R/W)
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4024-4048 (kmalloc/memcpy/parse caller)
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4127-4144 (init_pg ASIC gating)
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4176-4183, 4864-4878 (resume/hw_init)
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 line-matched against gfx_v8_0.c:3986-4021 adds the missing ind_offset+2>=list_size guard and converts BUG_ONs to pre-write WARN_ON returns; single-fix kernel build not possible on the virtio-only guest (amdgpu module absent, no AMD hardware) so the patched behavior was not executed.
findings/poc/DF-3597/fix.diff; guest module-absence proof in build.log
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:3974
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4001
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4020
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4033
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4040
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4127
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4176
- sys/dev/drm/amd/amdgpu/gfx_v8_0.c:4872
Detail
Exploit chain
replace amdgpufw_
Evidence (decisive lines)
run.log (guest cc 8.3 run: 'HEAP OOB WRITE CONFIRMED ... +4 past allocation' and 'BUG_ON fired'); VERDICT.md (full path:line trace); fix.diff; build.log (guest uname + module-absence proof)
PoC changes
harness authored fresh (pass-2); parser body copied verbatim from gfx_v8_0.c:3974-4022 with only BUG_ON redefined to report instead of halt; canary-guarded heap emulates adjacent slab object
Verified recommended fix
In gfx_v8_0_parse_ind_reg_list, return (with error log) when ind_offset + 2 >= list_size before the dereference, and replace the two post-write BUG_ONs with pre-write WARN_ON guards; see fix.diff.
Verdict
gfx_v8_0_parse_ind_reg_list (gfx_v8_0.c:3974-4022) dereferences register_list_format[ind_offset] after ind_offset += 2 without re-checking ind_offset < list_size: a truncated final entry (last word != 0xFFFFFFFF) reads and WRITES a small int 4-8 bytes past the end of the kmalloc(reg_list_format_size_bytes) copy made at :4033-4037, and lists with an 8th unique indirect index or 10th entry hit post-write BUG_ONs (:3993, :4017) => panic. Reachable at driver load on CZ/Stoney/Polaris11/12/VegaM via hw_init->rlc_resume->init_pg->init_save_restore_list (:4872->:4176->:4180->:4132/:4141->:4024). Kernel run not possible on the virtio-only audit guest (no AMD PCI device, no drm module, no /boot/modules) -> inconclusive at runtime; the OOB write and BUG_ON were proven deterministically by a verbatim parser replica built and executed on the guest (canary at +4 bytes past a 72-byte allocation clobbered to 0x1; BUG_ON fired on 8th unique index). Distinct from pass-1 DF-1133 (init_microcode kmalloc wrap / array offsets).
No comments yet.