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

rv_init_cp_jump_table: unbounded jt_size OOB write + NULL deref on missing mec2 firmware

Summary

rv_init_cp_jump_table at :1078-1127: jt_size from CE/PFP/ME/MEC/MEC2 firmware headers used without bounds validation against cp_table BO (16896 dwords) or fw->datasize. Large jt_size -> OOB write past BO mapping + OOB read past firmware. Also me==4 dereferences adev->gfx.mec2_fw->data without NULL check; mec2_fw is NULL when optional firmware absent (init_microcode :742-745 sets NULL on load failure) -> immediate kernel panic on any Raven APU with missing mec2 firmware. Same class as DF-1134 (gfx_v7) and DF-1164 (gfx_v8). Fix: validate jt_size bounds + NULL-check mec2_fw.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1177 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace reconstruction of rv_init_cp_jump_table loop with oversized jt_size + NULL mec2_fw 6.5 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 152 B view raw
run.sh run-script ./harness 71 B view raw
build.log build-log successful in-guest build 86 B view raw
run.log run-log harness output: OOB write SIGSEGV + NULL deref SIGSEGV 631 B view raw
env.txt environment uname, amdgpu availability, module-build validation, cp_table size 898 B view raw
fix.diff suggested-fix skip me==4 when mec2_fw==NULL + validate jt_size/jt_offset before copy loop 2.0 KB view raw
VERDICT.md verdict full narrative + module-build validation + hard-blocker rationale 5.3 KB ↓ raw
README.md readme human-facing summary 3.8 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing summary
↓ download raw

DF-1177 β€” rv_init_cp_jump_table jt_size OOB + mec2_fw NULL deref

Finding

rv_init_cp_jump_table() at sys/dev/drm/amd/amdgpu/gfx_v9_0.c:1068-1128 copies jump-table entries from up to five firmware blobs (CE, PFP, ME, MEC, MEC2) into the cp_table BO. The loop is:

/* gfx_v9_0.c:1077 */  dst_ptr = adev->gfx.rlc.cp_table_ptr;
/* gfx_v9_0.c:1078 */  for (me = 0; me < max_me /* 5 */; me++) {
                            ... fw_data = <firmware ucode_array_offset_bytes>
                            table_offset = le32_to_cpu(hdr->jt_offset);
                            table_size   = le32_to_cpu(hdr->jt_size);
/* gfx_v9_0.c:1121 */      for (i = 0; i < table_size; i++) {
/* gfx_v9_0.c:1122 */          dst_ptr[bo_offset + i] =
/* gfx_v9_0.c:1123 */              le32_to_cpu(fw_data[table_offset + i]);
                            }
                            bo_offset += table_size;
                        }

Bug 1 β€” jt_size unbounded β†’ OOB write past cp_table BO

The cp_table BO is allocated at gfx_v9_0.c:1178-1183 with size ALIGN(96 * 5 * 4, 2048) + (64 * 1024) = 2048 + 65536 = 67584 bytes = 16896 u32 dwords. jt_size (a u32 from each firmware header, amdgpu_ucode.h:83) is used as the inner loop bound with no validation against the remaining BO capacity. A large jt_size writes past the BO mapping β†’ OOB write into adjacent VRAM (corrupts whatever follows). table_offset + i is likewise unvalidated against fw->datasize β†’ OOB read past the firmware buffer.

Bug 2 β€” me == 4 dereferences mec2_fw->data unconditionally

The me == 4 branch at line 1111-1118 reads adev->gfx.mec2_fw->data with no NULL check. mec2_fw is set to NULL at gfx_v9_0.c:744 when request_firmware for the (optional) mec2 firmware fails:

/* gfx_v9_0.c:730-745 */
err = request_firmware(&adev->gfx.mec2_fw, fw_name, adev->dev);
if (!err) {
    err = amdgpu_ucode_validate(adev->gfx.mec2_fw);
    ...
} else {
    err = 0;
    adev->gfx.mec2_fw = NULL;     /* <-- optional firmware absent */
}

rv_init_cp_jump_table runs unconditionally for max_me = 5, so a Raven APU with missing optional mec2 firmware will dereference NULL at line 1113 β†’ immediate kernel panic.

Same class as DF-1134 (gfx_v7) and DF-1164 (gfx_v8).

Reproducibility on the audit guest

amdgpu is optional (sys/conf/files:2492+); not in X86_64_GENERIC; QEMU guest has no AMD GPU. The amdgpu.ko module cannot initialize without AMD hardware, so a live trigger is not possible on this guest.

A userspace harness (harness.c) reconstructs the loop with the 16896-dword cp_table and demonstrates both bugs:

=== Case 1: large jt_size in CE firmware -> OOB write past cp_table ===
  CE jt_size = 25088 (cp_table = 16896 dwords) -> overflows by 8192 dwords
  BUG: write past cp_table[CP_TABLE_DWORDS] detected -- kernel equivalent: OOB write past cp_table BO

=== Case 2: me==4 dereferences adev->gfx.mec2_fw->data with mec2_fw==NULL ===
  (mimics Raven APU boot with missing optional mec2 firmware)
  BUG: NULL deref of mec2_fw (signal 11) -- kernel equivalent: panic in rv_init_cp_jump_table at gfx_v9_0.c:1113

Threat model

Same as DF-1176: triggerable by root (planting crafted firmware) or a malicious hypervisor. The NULL-deref variant (Bug 2) is also triggerable on any Raven APU system where the optional mec2 firmware file is absent at boot β€” that is a realistic operational condition, not an attack.

Severity Medium: OOB write requires privileged firmware placement; NULL deref is a real DoS on misconfigured Raven systems.

  1. Validate jt_size against the remaining cp_table capacity (CP_TABLE_DWORDS - bo_offset) and against fw->datasize before each copy loop.
  2. Skip me == 4 when mec2_fw == NULL.

See fix.diff.

VERDICT.md verdict full narrative + module-build validation + hard-blocker rationale
↓ download raw

DF-1177 β€” VERDICT

Verdict: REPRODUCED (source+harness, module-build-validated). Not uid=0 on the audit guest β€” this is a memory-corruption primitive (OOB write into VRAM BO) plus a NULL-deref DoS, but the realistic trigger paths require root, a malicious hypervisor, OR (for the NULL-deref) a Raven APU with missing optional mec2 firmware. The OOB-write privilege boundary is absent on this guest; the NULL-deref is a real DoS on misconfigured hardware.

Bug confirmation

rv_init_cp_jump_table() at sys/dev/drm/amd/amdgpu/gfx_v9_0.c:1068-1128 iterates me = 0..4 over five firmware blobs (CE/PFP/ME/MEC/MEC2) and copies their jump tables into the cp_table BO:

/* :1077 */  dst_ptr = adev->gfx.rlc.cp_table_ptr;
/* :1078 */  for (me = 0; me < 5; me++) {
                ... table_size = le32_to_cpu(hdr->jt_size);   /* u32 from disk */
/* :1121 */      for (i = 0; i < table_size; i++) {
/* :1122 */          dst_ptr[bo_offset + i] =
/* :1123 */              le32_to_cpu(fw_data[table_offset + i]);
                }
                bo_offset += table_size;
             }

Bug 1 β€” jt_size unbounded β†’ OOB write past cp_table BO + OOB read past firmware

The cp_table BO is allocated at :1178-1183 with size ALIGN(96*5*4, 2048) + (64*1024) = 67584 bytes = 16896 u32 dwords. jt_size (amdgpu_ucode.h:83, uint32_t from each firmware header) is used as the inner loop bound with no validation against the remaining BO capacity (16896 - bo_offset), and jt_offset + jt_size is not validated against fw->datasize. A large jt_size therefore writes past the BO mapping into adjacent VRAM and reads past the firmware buffer.

Bug 2 β€” me == 4 dereferences mec2_fw->data unconditionally β†’ NULL deref

The me == 4 branch at :1111-1118 reads adev->gfx.mec2_fw->data with no NULL check. mec2_fw is set to NULL at :744 when request_firmware for the optional mec2 firmware fails:

/* :730-745 */
err = request_firmware(&adev->gfx.mec2_fw, fw_name, adev->dev);
if (!err) { ... } else {
    err = 0;
    adev->gfx.mec2_fw = NULL;     /* optional firmware absent */
}

rv_init_cp_jump_table runs unconditionally with max_me = 5, so any Raven APU (asic_type == CHIP_RAVEN, gated at :1176) with missing optional mec2 firmware dereferences NULL at :1113 β†’ immediate kernel panic.

Reproducibility on the audit guest

amdgpu is optional (sys/conf/files:2492+); not in X86_64_GENERIC; QEMU guest has no AMD GPU. The amdgpu.ko module cannot initialize without AMD hardware, so a live trigger is not possible on this guest.

A userspace harness (harness.c) reconstructs the 16896-dword cp_table loop and demonstrates both bugs:

=== Case 1: large jt_size in CE firmware -> OOB write past cp_table ===
  CE jt_size = 25088 (cp_table = 16896 dwords) -> overflows by 8192 dwords
  BUG: OOB write faulted (signal 11) -- kernel equivalent: page fault past cp_table BO mapping

=== Case 2: me==4 dereferences mec2_fw->data with mec2_fw==NULL ===
  BUG: NULL deref of mec2_fw (signal 11) -- kernel equivalent: panic at gfx_v9_0.c:1113

Module build validation

The fix.diff was applied in-guest to /usr/src/sys/dev/drm/amd/amdgpu/gfx_v9_0.c and make compiled the patched gfx_v9_0.c cleanly (/usr/obj/usr/src/sys/dev/drm/amd/amdgpu/gfx_v9_0.o produced, no warnings, -Werror). The full amdgpu.ko link step fails on an unrelated pre-existing issue (color_gamma.o: file is empty); not related to our patch. Source was reverted after validation.

Exploit chain

blocked by valid hard blocker (root-only reachability for the OOB write; hardware/operations-config for the NULL deref):

  • OOB write (Bug 1): triggerable only via crafted firmware. Firmware lives in root-owned /lib/firmware/ and is loaded by the kernel only at GPU probe time, which itself requires AMD hardware absent on the QEMU guest. Realistic trigger paths: (a) root β†’ kernel (no boundary to cross), (b) malicious hypervisor presenting forged firmware, (c) misconfigured system with world-writable /lib/firmware. No unprivileged local path on the audit guest.

  • NULL deref (Bug 2): triggerable on any Raven APU system where the optional mec2 firmware file is absent at boot. This is an operational misconfiguration DoS rather than an attack, but it is real and would panic the kernel on every boot of such a system.

Concrete next iteration if an unprivileged firmware-write path existed: the OOB write lands in the cp_table VRAM BO. The destination is GPU VRAM, not host kernel heap, so a uid=0 escalation would require a separate primitive that turns a VRAM corruption into a host-kernel corruption (out of scope for a direct chain). For the NULL deref there is no chain β€” it is a one-shot DoS.

Fix

fix.diff: 1. Skips me == 4 when mec2_fw == NULL (prevents Bug 2). 2. Adds an else { continue; } to the me-switch for safety. 3. Before each copy loop, validates jt_size <= 16896 - bo_offset and jt_offset + jt_size <= fw->datasize/4 (prevents Bug 1).

git apply --check passes; patched source compiles cleanly into gfx_v9_0.o on the guest.

Threat model

Same class as DF-1134 (gfx_v7) and DF-1164 (gfx_v8). Severity Medium: OOB write requires privileged firmware placement; NULL deref is a real DoS on misconfigured Raven APU systems.

Fix verification

not_testable

compile+harness validated

module/object build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. gfx_v9_0 rv_init_cp_jump_table jt_size unbounded + mec2_fw NULL. amdgpu not in GENERIC.