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)
PoC verification
Evidence pack
findings/poc/DF-3860 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
amdgpu_device_parse_gpu_info_fw()loadsamdgpufw_<asic>_gpu_infoviarequest_firmware()(amdgpu_device.c:1354) and callsamdgpu_ucode_validate()(amdgpu_device.c:1361).amdgpu_ucode_validate()(sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251β260) ONLY checksfw->datasize == le32_to_cpu(hdr->size_bytes). It performs no validation ofhdr->ucode_array_offset_bytesand no check that the payload fits in the buffer.- 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.
- The read values are stored into
adev->gfx.config.*andadev->gfx.cu_info.*and are (a) returned to unprivileged userspace via theAMDGPU_INFO_CU_COUNT-family paths of AMDGPU_INFO ioctl (amdgpu_kms.c copiesadev->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.
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 -lvshows a singlevgapci0@pci0:0:2.0virtio GPU (chip 0x11111234); zero AMD PCI display devices, no/dev/dri*nodes.kldload amdgpusucceeds (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)
amdgpu_device_parse_gpu_info_fw()amdgpu_device.c:1354request_firmware(&adev->firmware.gpu_info_fw, fw_name, adev->dev).- amdgpu_device.c:1361
amdgpu_ucode_validate(adev->firmware.gpu_info_fw). 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.
-
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) touchfw->data + 0x7fff0000 β¦ +60β far outside the N-byte firmware allocation (firmware buffers are kmalloc'd by the loader; seefirmware_get/load_firmwarein sys/kern/subr_firmware.c which kmem_allocs datasize bytes). -
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 thecu_info->bitmap[i][j]/ao_cu_bitmap[i][j]walkers in gfx_v9_0 (fixed [4][4] arrays). Additionallyadev->gfx.cu_info(wave_front_size, max_waves_per_simd, max_scratch_slots_per_cu, lds_size) is copied back to unprivileged userspace byamdgpu_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_testablefix.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
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_
No comments yet.