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

gpu_info firmware payload pointer built from unvalidated ucode_array_offset_bytes β€” kernel heap OOB read feeding AMDGPU_INFO disclosure and the DF-3698 walkers

Summary

amdgpu_device_parse_gpu_info_fw 'validates' the file with amdgpu_ucode_validate whose entire body is 'fw->datasize == hdr->size_bytes'. It then reads a 60-byte struct at data+ucode_array_offset_bytes - a raw u32 from the file with no range check. A 36-byte crafted file with offset 0x7fff0000 passes validation and makes the kernel read 60 bytes ~2GB past the firmware allocation. Parsed values land unclamped in gfx.config (max_shader_engines/max_sh_per_se - the exact fields DF-3698 showed drive cu_info[4][4] OOB writes) and in cu_info, which the AMDGPU_INFO ioctl copies back to unprivileged userspace (heap disclosure). Root-planted firmware file / compromised channel / image-distribution attack on VEGA10/12/RAVEN. Generator script + full chain in pack.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3860 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
make_gpuinfo_fw.py β€” 2.2 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 161 B view raw
build.log β€” 423 B view raw
run.log β€” 231 B view raw
env.txt β€” 483 B view raw
fix.diff β€” 2.1 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.0 KB view raw

DF-3860 β€” gpu_info firmware payload read at unvalidated ucode_array_offset_bytes (kernel heap OOB read)

File: sys/dev/drm/amd/amdgpu/amdgpu_device.c β€” amdgpu_device_parse_gpu_info_fw() (lines 1301–1407).

Root cause

  1. amdgpu_device_parse_gpu_info_fw() loads amdgpufw_<asic>_gpu_info via request_firmware() (amdgpu_device.c:1354) and calls amdgpu_ucode_validate() (amdgpu_device.c:1361).
  2. amdgpu_ucode_validate() (sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251–260) ONLY checks fw->datasize == le32_to_cpu(hdr->size_bytes). It performs no validation of hdr->ucode_array_offset_bytes and no check that the payload fits in the buffer.
  3. The parse then does (amdgpu_device.c:1375–1377):
        const struct gpu_info_firmware_v1_0 *gpu_info_fw =
                (const struct gpu_info_firmware_v1_0 *)(adev->firmware.gpu_info_fw->data +
                                                        le32_to_cpu(hdr->header.ucode_array_offset_bytes));

ucode_array_offset_bytes is a raw, attacker-chosen u32 inside the firmware file. Lines 1379–1396 then read 14 u32 members (gc_num_se … gc_lds_size, a 60-byte struct gpu_info_firmware_v1_0, amdgpu_ucode.h:135–151) through that pointer β€” up to ~4 GiB past the end of the firmware buffer.

  1. The read values are stored into adev->gfx.config.* and adev->gfx.cu_info.* and are (a) returned to unprivileged userspace via the AMDGPU_INFO_CU_COUNT-family paths of AMDGPU_INFO ioctl (amdgpu_kms.c copies adev->gfx.cu_info), i.e. heap memory disclosure; and (b) fed into the cu_info[4][4] walkers analysed in DF-3698 (max_shader_engines, max_sh_per_se β€” no clamp on this parse side).

The firmware-parse itself happens at driver probe for VEGA10/VEGA12/RAVEN (amdgpu_device.c:1337–1350).

Threat model

Requires control of the gpu_info firmware file (root-planted file in the firmware search path /usr/local/lib/firmware, a compromised firmware channel, or an image-distribution attack). Same precondition class as the already-filed DF-1256 (same loader) and DF-3698 (same consumer).

Trigger

make_gpuinfo_fw.py generates a minimal file whose common_firmware_header satisfies amdgpu_ucode_validate() (size_bytes == file size) but whose ucode_array_offset_bytes points far past the buffer (default 0x7fff0000). On hardware, placing this file and loading amdgpu makes the kernel read data + 0x7fff0000 … +60.

Build / run

  • ./build.sh β€” no compilation needed; runs the generator and hexdumps the file.
  • ./run.sh β€” on a host with a VEGA10-class AMD GPU: install the file, load amdgpu; on this audit guest: reports the hardware gate.

Expected result (on hardware)

Kernel reads 60 bytes at fw->data + attacker_offset. With a groomed firmware slab this is a kernel heap OOB read whose contents are reflected to userspace via AMDGPU_INFO (cu_info fields) β€” plus unclamped values flowing into the DF-3698 walker family (heap OOB write there). No crash is expected from the read itself; a KASAN/DEBUG kernel would log the OOB. On DragonFly the observable artifact is bogus cu_info values in AMDGPU_INFO output that differ from the real hardware census.

VERDICT.md
↓ download raw

DF-3860 VERDICT β€” not_reproduced at runtime (hardware-gated), source-certain

Reproduction attempt

  • Audit guest: DragonFly 6.5-DEVELOPMENT (QEMU/KVM, virtio display only). pciconf -lv shows a single vgapci0@pci0:0:2.0 virtio GPU (chip 0x11111234); zero AMD PCI display devices, no /dev/dri* nodes. kldload amdgpu succeeds (module present at /boot/kernel/amdgpu.ko) but binds nothing.
  • The vulnerable code only executes during amdgpu_device_ip_early_init() β†’ amdgpu_device_parse_gpu_info_fw() at driver probe of a VEGA10/VEGA12/RAVEN ASIC. Without an AMD GPU the path never runs on this guest β€” runtime reproduction is impossible on this system. status = not_reproduced, reproduced = 0, impact at runtime = none (documented source-level impact: kernel heap OOB read / info disclosure, Medium).

Source-level proof (why the bug is certain)

  1. amdgpu_device_parse_gpu_info_fw() amdgpu_device.c:1354 request_firmware(&adev->firmware.gpu_info_fw, fw_name, adev->dev).
  2. amdgpu_device.c:1361 amdgpu_ucode_validate(adev->firmware.gpu_info_fw).
  3. amdgpu_ucode_validate() amdgpu_ucode.c:251–260 β€” full body:
        const struct common_firmware_header *hdr =
                (const struct common_firmware_header *)fw->data;
        if (fw->datasize == le32_to_cpu(hdr->size_bytes))
                return 0;
        return -EINVAL;

Only a size self-consistency check; ucode_array_offset_bytes (a u32 in common_firmware_header, amdgpu_ucode.h:58 region) is never range-checked against fw->datasize, and there is no check that offset + sizeof(struct gpu_info_firmware_v1_0) <= datasize.

  1. amdgpu_device.c:1375–1377 constructs the payload pointer as fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes) β€” a fully attacker-chosen 32-bit byte offset. With a crafted file of N bytes (size_bytes set to N so (3) passes) and offset 0x7fff0000, the reads at amdgpu_device.c:1379–1396 (gc_num_se … gc_lds_size, 14 Γ— le32_to_cpu) touch fw->data + 0x7fff0000 … +60 β€” far outside the N-byte firmware allocation (firmware buffers are kmalloc'd by the loader; see firmware_get/load_firmware in sys/kern/subr_firmware.c which kmem_allocs datasize bytes).

  2. Consumers: the parsed values land in adev->gfx.config.max_shader_engines, max_sh_per_se, max_cu_per_sh … (amdgpu_device.c:1379–1396) with no clamping, i.e. exactly the fields DF-3698 showed are consumed by the cu_info->bitmap[i][j] / ao_cu_bitmap[i][j] walkers in gfx_v9_0 (fixed [4][4] arrays). Additionally adev->gfx.cu_info (wave_front_size, max_waves_per_simd, max_scratch_slots_per_cu, lds_size) is copied back to unprivileged userspace by amdgpu_info_ioctl() (AMDGPU_INFO read of cu_info) β†’ heap-memory disclosure channel.

Why not false-positive

The validation gap is total (single equality check) and the offset is used unchecked as a byte offset. The only precondition is content control of the firmware file β€” the same threat model under which DF-1256 and DF-3698 were accepted.

Fix validation

fix.diff authors the missing bounds checks (header fits, payload fits) plus sanity clamps on the SE/SH counts feeding the DF-3698 walkers. Not testable on this guest (no AMD hardware): fix_status = not_testable.

Exploit chain (for the record)

plant amdgpufw_vega10_gpu_info with ucode_array_offset_bytes aimed at a groomed neighbor slab β†’ probe-time 60-byte OOB read β†’ values readable by any render-node user via AMDGPU_INFO (cu_info) β†’ heap layout disclosure; plus oversized max_shader_engines/max_sh_per_se β†’ DF-3698 OOB write family in gfx_v9_0_get_cu_info / amdgpu_gfx_parse_rlc_astcp_pd_dimension style walkers.

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 adds header-size and payload-range validation plus SE/SH clamps at parse time; git apply --check clean against sys/; cannot be exercised on this guest (no AMD GPU)

findings/poc/DF-3860/fix.diff
↓ fix.diffper-fix-DF-3860

Confirmed kernel references

Detail

Exploit chain

plant amdgpufw_vega10_gpu_info with ucode_array_offset_bytes aimed at a groomed neighbor slab -> probe-time 60-byte OOB read -> values disclosed to any render-node user via AMDGPU_INFO cu_info -> heap layout info; oversized SE/SH counts additionally trigger the DF-3698 cu_info[4][4] OOB-write consumers

Evidence (decisive lines)

VERDICT.md (source trace + gating); make_gpuinfo_fw.py + build.log (crafted file passing validate: datasize==size_bytes==36, offset 0x7fff0000); run.log + env.txt (no AMD GPU on guest)

PoC changes

authored from scratch: header layout matched to real struct common_firmware_header (amdgpu_ucode.h) after first draft used a wrong 28-byte layout

Verified recommended fix

In amdgpu_device_parse_gpu_info_fw, require datasize >= sizeof(*hdr) and offset+sizeof(gpu_info_firmware_v1_0) <= datasize before reading, and clamp max_shader_engines/max_sh_per_se to 4 (DF-3698 consumers)

Verdict

Source-certain heap OOB read: amdgpu_device_parse_gpu_info_fw (amdgpu_device.c:1375-1396) builds its payload pointer from hdr->ucode_array_offset_bytes, a raw u32 in the firmware file, after amdgpu_ucode_validate (amdgpu_ucode.c:251-260) has checked ONLY fw->datasize == hdr->size_bytes; a 36-byte crafted amdgpufw__gpu_info with offset 0x7fff0000 passes validation and makes the kernel read 60 bytes ~2GB past the firmware allocation; parsed values (incl. max_shader_engines/max_sh_per_se, unclamped) land in adev->gfx.config/cu_info and are returned to unprivileged userspace via the AMDGPU_INFO ioctl, feeding the DF-3698 walker family. Not reproducible on the audit guest: it has no AMD GPU (pciconf shows a single virtio display device; /dev/dri absent), so the probe-time parse never executes. Hardware-gated; crafted-firmware precondition (same threat model as DF-1256/DF-3698).