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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3597 Β· 9 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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

  1. 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 executes ind_offset += 2 and then reads register_list_format[ind_offset] at :4008/:4013 and writes it at :4020 β€” with no ind_offset < list_size re-check. When the last list word is not the 0xFFFFFFFF terminator, ind_offset becomes list_size (+1 word OOB) or list_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 into mmRLC_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] = indices writes 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 and BUG_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.

  1. Logic replica (deterministic). poc_dfr3597.c is 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): * mode oob: 18-element list ending in a truncated entry β†’ write of 0x1 at element 19 (+4 bytes past the 72-byte allocation) β€” canary clobbered. See run.log. * mode bugon: 8 unique indirect indices β†’ BUG_ON fires (kernel panic equivalent).

  2. Why not a kernel run. The guest is virtio-only: pciconf -l lists no AMD vendor (0x1002) device, kldstat -v | grep -c drm = 0, and /boot/modules does not exist β€” the amdgpu driver is neither loadable nor present. The in-kernel trigger requires a Carrizo/Stoney/Polaris11/12/VegaM machine plus a crafted amdgpufw_*_rlc firmware module. Hence the honest classification inconclusive for 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

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

Confirmed kernel references

Detail

Exploit chain

replace amdgpufw__rlc firmware module -> RLC v2.0 header sets reg_list_format_size_bytes (slab-class selection) and list content (truncation point => +4/+8 byte write offset) -> driver load parses list -> small-int (0..7) heap overwrite of adjacent object -> slab grooming to victim object corruption -> kernel code execution path; BUG_ON variant = load-time DoS. Not executed (no AMD HW).

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).