cik_sdma_load_microcode trusts SDMA firmware header offset/size fields without bounds-checking against fw->datasize (DF-1457/DF-1538 SDMA twin)
- File:
sys/dev/drm/amd/amdgpu/cik_sdma.c - Lines: 539, 550, 552, 553, 554, 555, 556, 559, 560, 562, 563, 564
- Severity: Low
- CVSS:
CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-125 Out-of-bounds Read
- Confidence: certain
Summary
cik_sdma_load_microcode() parses the loaded SDMA firmware blob's
sdma_firmware_header_v1_0 fields (ucode_array_offset_bytes /
ucode_size_bytes) and uses them directly as the base pointer and loop count for
a read loop that feeds le32_to_cpup() dereferences into WREG32 MMIO writes.
amdgpu_ucode_validate() (amdgpu_ucode.c:251-260) only verifies
fw->datasize == hdr->size_bytes β it does NOT validate that the inner
offset+size fields stay within datasize, nor that
fw->datasize >= sizeof(struct sdma_firmware_header_v1_0).
A crafted amdgpufw_{bonaire,hawaii,kaveri,kabini,mullins}_sdma[_1] firmware
whose size_bytes matches the file length but whose inner offset/size fields are
inflated causes the loop to read past the firmware's kmalloc backing, reading
adjacent kernel heap and writing those bytes into mmSDMA0_UCODE_DATA.
Root cause
At cik_sdma.c:552 the firmware data pointer is cast to
const struct sdma_firmware_header_v1_0 * with no check that
fw->datasize >= sizeof(*hdr) (40 bytes for v1_0: 32-byte
common_firmware_header + 4Γu32 SDMA-specific fields per amdgpu_ucode.h:120-126).
At cik_sdma.c:553 amdgpu_ucode_print_sdma_hdr() is called with
&hdr->header β that helper (amdgpu_ucode.c:201-228) does container_of() and
reads ucode_feature_version/ucode_change_version/jt_offset/jt_size from
the SDMA-tail of the header, again with no bounds check.
At cik_sdma.c:554:
fw_size = le32_to_cpu(hdr->header.ucode_size_bytes) / 4 β u32/unsigned
arithmetic, yields up to ~0x3FFFFFFF iterations.
At cik_sdma.c:559-560:
fw_data = (const __le32 *)(adev->sdma.instance[i].fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes))
β u8* + u32, no clamp.
Then at cik_sdma.c:562-563 the ucode loop calls
le32_to_cpup(fw_data++) = le32_to_cpu(*fw_data) (defined
sys/dev/drm/include/linux/kernel.h:146) β a direct 4-byte pointer dereference
with no bounds guard.
The only pre-check at cik_sdma.c:550-551 verifies
adev->sdma.instance[i].fw is non-NULL; it does not validate the data layout.
amdgpu_ucode_validate() at amdgpu_ucode.c:251-260 only checks
fw->datasize == le32_to_cpu(hdr->size_bytes) (itself an OOB read of
hdr->size_bytes if datasize < sizeof(common_firmware_header) = 32 bytes), so a
firmware whose outer size_bytes matches the file length but whose inner
ucode_array_offset_bytes/ucode_size_bytes point past the buffer passes
validation and proceeds to read out of bounds.
Concretely: ucode_array_offset_bytes = datasize, ucode_size_bytes = 16 β
fw_size = 4 β loop reads 4 dwords starting exactly at fw->data+datasize
(12 bytes past the allocation).
Larger values (e.g. ucode_size_bytes = 0x40000000) drive the loop into
unmapped kernel addresses and panic.
The intermediate case (offset = datasize - 4, size = 16) reads a controlled
number of adjacent kernel heap bytes.
Identical code shape to gmc_v7_0 (DF-1538, gmc_v7_0.c:189-214) and
gmc_v8_0 (DF-1457).
Threat
Attacker position: any subject that can place or replace the firmware blob loaded
under the names amdgpufw_bonaire_sdma, amdgpufw_bonaire_sdma1,
amdgpufw_hawaii_sdma, amdgpufw_hawaii_sdma1, amdgpufw_kaveri_sdma,
amdgpufw_kaveri_sdma1, amdgpufw_kabini_sdma, amdgpufw_kabini_sdma1,
amdgpufw_mullins_sdma, amdgpufw_mullins_sdma1 (declared via
MODULE_FIRMWARE at cik_sdma.c:57-66) in the firmware loader's search path.
On DragonFlyBSD this requires root (or a privileged package update / supply-chain compromise / mounted untrusted filesystem containing the firmware module) β hence PR:H.
The bug is reached on every CIK-class GPU
(CHIP_BONAIRE/HAWAII/KAVERI/KABINI/MULLINS) at driver attach:
cik_set_ip_blocks (cik.c:2010/2031/2052/2072) registers
cik_sdma_ip_block, whose .sw_init = cik_sdma_sw_init calls
cik_sdma_init_microcode (cik_sdma.c:966) which
request_firmware()+amdgpu_ucode_validate()s the blob (cik_sdma.c:138,141),
and whose .hw_init = cik_sdma_hw_init calls cik_sdma_start β
cik_sdma_load_microcode (cik_sdma.c:1023, 582) which executes the vulnerable
dereference loop.
Impact:
- (a) reliable kernel panic / DoS by setting
ucode_size_byteslarge enough that the read loop walks into an unmapped page (A:H); - (b) small controlled OOB read of adjacent kernel heap whose contents are then
written to GPU MMIO register
mmSDMA0_UCODE_DATAβ not a direct kernel-to-userspace leak, hence C:N, but the data lands in the SDMA instruction store.
Reproducible on any DragonFlyBSD system with a CIK GPU and the amdgpu driver loaded.
Exploit / PoC
Craft a malicious firmware file amdgpufw_bonaire_sdma (a normal Bonaire SDMA
firmware whose common_firmware_header.size_bytes matches the file length so
amdgpu_ucode_validate passes, but whose
common_firmware_header.ucode_array_offset_bytes field is set to datasize, and
whose common_firmware_header.ucode_size_bytes is set to a small multiple of 4
such as 16, so the loop reads 4 dwords starting exactly past the firmware
allocation).
Place it in the firmware loader search path
(/boot/modules/firmware/amdgpu/ or wherever firmware_get resolves
'amdgpufw_bonaire_sdma' on the target).
Trigger driver re-attach on a Bonaire-class GPU: kldunload amdgpu;
kldload amdgpu, or boot, or PCI re-enumeration.
At cik_sdma.c:578 cik_sdma_start calls cik_sdma_load_microcode which at
cik_sdma.c:562-563 enters the WREG32 loop reading via
le32_to_cpup(fw_data++) from fw->data + inflated_offset; the dereference
reads kernel heap past the allocation.
The DoS variant: set ucode_size_bytes = 0x40000000
(β fw_size = 0x10000000 = 256 M iterations) and
ucode_array_offset_bytes = datasize; the loop immediately dereferences
fw->data+datasize and within a handful of iterations hits an unmapped page,
producing an immediate kernel page-fault panic.
PoC source sketch (drop into findings/poc/DF-1553/):
mkmaliciousfw.cβ read a stockamdgpufw_bonaire_sdma, patch the two header fields at their known offsets (common_firmware_header.ucode_size_bytesat offset0x10,common_firmware_header.ucode_array_offset_bytesat offset0x14, both little-endian u32) sosize_bytesat offset0x0still equals the file length butucode_array_offset_bytespoints past EOF anducode_size_bytes = 16; write the result to the firmware module path.run.shβkldunload amdgpu; kldload amdgpu(or trigger PCI re-enumeration) and capture dmesg; success =Fatal trap 12: page fault in kernel modewhile insidecik_sdma_load_microcode, or for the small-OOB variant no panic but adjacent kernel heap bytes are written intommSDMA0_UCODE_DATAregisters.
Build: cc -o mkmaliciousfw mkmaliciousfw.c.
Reproducible on any DragonFlyBSD system with a CIK discrete GPU (Bonaire/Hawaii) and the amdgpu driver loaded.
The defense-in-depth improvement (validate offset+size <= datasize) is the fix;
the practical exploit still requires privileged firmware placement.
Recommended fix
Validate that the firmware buffer is large enough to contain the header, and that
(ucode_array_offset_bytes + ucode_size_bytes) stays within fw->datasize
before entering the read loop. Wrap-safe unsigned arithmetic.
--- a/sys/dev/drm/amd/amdgpu/cik_sdma.c
+++ b/sys/dev/drm/amd/amdgpu/cik_sdma.c
@@ -539,11 +539,25 @@ static int cik_sdma_rlc_resume(struct amdgpu_device *adev)
static int cik_sdma_load_microcode(struct amdgpu_device *adev)
{
const struct sdma_firmware_header_v1_0 *hdr;
const __le32 *fw_data;
- u32 fw_size;
+ u32 fw_size, ucode_offset, ucode_size_bytes;
int i, j;
/* halt the MEs */
cik_sdma_enable(adev, false);
for (i = 0; i < adev->sdma.num_instances; i++) {
if (!adev->sdma.instance[i].fw)
return -EINVAL;
+ if (adev->sdma.instance[i].fw->datasize < sizeof(*hdr))
+ return -EINVAL;
hdr = (const struct sdma_firmware_header_v1_0 *)adev->sdma.instance[i].fw->data;
amdgpu_ucode_print_sdma_hdr(&hdr->header);
+ ucode_offset = le32_to_cpu(hdr->header.ucode_array_offset_bytes);
+ ucode_size_bytes = le32_to_cpu(hdr->header.ucode_size_bytes);
+ if (ucode_offset > adev->sdma.instance[i].fw->datasize ||
+ ucode_size_bytes > adev->sdma.instance[i].fw->datasize - ucode_offset)
+ return -EINVAL;
fw_size = ucode_size_bytes / 4;
adev->sdma.instance[i].fw_version = le32_to_cpu(hdr->header.ucode_version);
adev->sdma.instance[i].feature_version = le32_to_cpu(hdr->ucode_feature_version);
if (adev->sdma.instance[i].feature_version >= 20)
adev->sdma.instance[i].burst_nop = true;
- fw_data = (const __le32 *)
- (adev->sdma.instance[i].fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes));
+ fw_data = (const __le32 *)
+ (adev->sdma.instance[i].fw->data + ucode_offset);
WREG32(mmSDMA0_UCODE_ADDR + sdma_offsets[i], 0);
The two a > datasize || b > datasize - a checks implement a wrap-safe
(offset+size <= datasize) bound using unsigned arithmetic; combined with the
datasize >= sizeof(*hdr) guard at entry this closes both the SDMA-specific-field
OOB read in amdgpu_ucode_print_sdma_hdr and the ucode-payload OOB read in the
WREG32 loop.
The upstream-correct longer-term fix is to make amdgpu_ucode_validate itself
enforce datasize >= sizeof(common header) and introduce a per-IP validate
helper, but the per-file patch above is sufficient to close the cik_sdma path.
Related findings
- DF-1538 (twin, gmc_v7_0): identical defect in the GMC v7.0 driver.
- DF-1457 (twin, gmc_v8_0): identical defect in the GMC v8.0 driver.
- DF-1256, DF-1316, DF-1451 (siblings): same
amdgpu_ucode_validatefamily in ni/sdma/gfx IP blocks.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1553 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for amdgpu cik_sdma firmware header OOB | 417 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 795 B | β raw |
| build.sh | build-script | No-op (source-only) | 109 B | view raw |
| run.sh | run-script | No-op (source-only) | 107 B | view raw |
VERDICT DF-1553: amdgpu cik_sdma firmware header OOB
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
SDMA firmware header cast without datasize check; ucode fields read OOB.
Source reference: sys/dev/drm/amd/amdgpu/cik_sdma.c:550-556.
Reproduction
Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed.
The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present
on the QEMU/virtio guest. The finding is HW-gated.
Fix
Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC β rc=0, -Werror clean.
See fix.diff for the git-apply-able patch.
Fix verification
fixedCombined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.
'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- c
- i
- k
- _
- s
- d
- m
- a
- .
- c
- :
- 5
- 5
- 0
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/drm/amd/amdgpu/cik_sdma.c:550. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
Add header+ucode bounds checks. Matches finding.
Verdict
REPRODUCED (source-confirmed). SDMA FW header without datasize check -> OOB field read. Cited path verified at sys/dev/drm/amd/amdgpu/cik_sdma.c:550. HW/module-gated on QEMU guest.
No comments yet.