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

sdma_v3_0 reads SDMA firmware sub-header fields (ucode_version @+16, ucode_feature_version @+32, ucode_size_bytes @+24) after only the datasize==size_bytes equality check β€” heap OOB read on a short root-installed firmware blob (DF-1451/1553/1598 family, v3 variant)

Summary

Each SDMA image is validated solely via amdgpu_ucode_validate (equality-only) and then read beyond the common header: a 4-byte blob declaring size_bytes=4 passes and every sub-header field read runs past the allocation; ucode_size_bytes also inflates fw_size, feeding the unbounded payload memcpy (the DF-1947 sink; live path for VI asics). Primitive PROVEN on the guest (guard-page stand-in): 'validate(4-byte blob)=0 accepted' then SIGSEGV at exactly the +16 read. HW-gated in-kernel. Minimum-size guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4238 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
fwcheck.c β€” 4.3 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 116 B view raw
build.log β€” 110 B view raw
run.log β€” 195 B view raw
env.txt β€” 254 B view raw
fix.diff β€” 683 B view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.1 KB view raw

DF-4238 β€” sdma_v3_0: SDMA firmware sub-header fields read after only

datasize==size_bytes validation β€” heap OOB read on a short (root-installed) firmware blob (DF-1451/1553/1598 family, v3 variant)

What

sdma_v3_0_init_microcode() (sys/dev/drm/amd/amdgpu/sdma_v3_0.c) validates each SDMA firmware image only through amdgpu_ucode_validate(), which checks a single equality β€” fw->datasize == hdr->size_bytes (amdgpu_ucode.c:251-260) β€” and then immediately reads SDMA sub-header fields that live beyond the common header:

  • sdma_v3_0.c:316 hdr->header.ucode_version (common header +16)
  • sdma_v3_0.c:317 hdr->ucode_feature_version (sdma v1_0 header +32; struct sdma_firmware_header_v1_0 is 48 bytes β€” amdgpu_ucode.h:120-126)
  • sdma_v3_0.c:324-326 header->ucode_size_bytes (common header +24) folded into adev->firmware.fw_size, which sizes the firmware BO later memcpy'd from fw->data + ucode_array_offset_bytes (amdgpu_ucode.c:347-351, the DF-1947 sink).

A blob as small as 4 bytes (declaring size_bytes = 4) passes validation; every field read above is then a heap out-of-bounds read on the request_firmware buffer.

Threat model

Firmware files (amdgpufw_tonga_sdma etc., sdma_v3_0.c:54-68) are root- installed module firmware β€” same gate as DF-1451 (sdma_v4_0), DF-1553 (cik_sdma), DF-1598 (sdma_v2_4). Low severity: root-triggerable kernel heap OOB read (leak into fw_version/feature_version printed to dmesg, and fw_size inflation feeding the DF-1947 memcpy sink).

Evidence in this pack

  • fwcheck.c β€” harness replicating the exact validate-then-read sequence on a 4-byte blob placed against a guard page. Built and run on the audit guest (DragonFly cc 8.3): 1. amdgpu_ucode_validate(4-byte blob) = 0 (accepted) 2. the first sub-header read (ucode_version at +16, the exact first field sdma_v3_0.c:316 reads) faults into the guard page β†’ SIGSEGV, exit 139 β€” the out-of-bounds access proven without ASan.
  • run.log β€” full untrimmed guest output.
  • fix.diff β€” minimum-size guard in sdma_v3_0_init_microcode (the shared root fix β€” offset/size cross-validation in amdgpu_ucode_validate β€” is DF-1946 and out of scope for this file).

Reproduce

# on the DragonFly guest (no AMD hardware needed for the harness)
cc -O2 -Wall -o fwcheck fwcheck.c
./fwcheck; echo $?     # prints validate=0, then dies SIGSEGV (139) at the +16 read

The kernel path itself is hardware-gated (Tonga/Polaris/Fiji/Carrizo/Stoney SDMA engine β€” none on the guest, see env.txt), so the finding is not_reproduced / hardware-gated; the parsing primitive is proven by the harness.

VERDICT.md
↓ download raw

DF-4238 VERDICT β€” sdma_v3_0 firmware sub-header OOB read (DF-1451/1553/1598 family, v3 variant)

Status: not_reproduced (hardware-gated: guest has no AMD GPU; parsing primitive proven by a guest-run harness). Confidence: certain (code). Impact: heap OOB read on root-installed firmware β†’ Low.

1. Root cause (path:line)

  • amdgpu_ucode.c:251-260 β€” amdgpu_ucode_validate() checks ONLY fw->datasize == le32_to_cpu(hdr->size_bytes). No minimum size, no header_size/ucode_size/offset cross-validation (root finding DF-1946).
  • sdma_v3_0.c:309-317 β€” after request_firmware + the equality-only validate, the code casts fw->data to struct sdma_firmware_header_v1_0 (48 bytes; amdgpu_ucode.h:120-126) and reads:
  • hdr->header.ucode_version β€” common header offset 16 (sdma_v3_0.c:316),
  • hdr->ucode_feature_version β€” offset 32 (sdma_v3_0.c:317).
  • sdma_v3_0.c:321-326 β€” header->ucode_size_bytes (offset 24) is folded into adev->firmware.fw_size with no bound vs datasize; the payload is later memcpy'd from fw->data + ucode_array_offset_bytes for ucode_size_bytes bytes into the firmware BO (amdgpu_ucode.c:347-351 β€” the DF-1947 sink; on VI the load type is AMDGPU_FW_LOAD_SMU, amdgpu_ucode.c:291-300, so the BO path is the live one for Tonga et al.).

A firmware blob of datasize = 4 with size_bytes = 4 passes validation; reads at +16/+24/+32 are heap OOB. The reads at :316/:317 feed dmesg (fw_version/feature_version influence burst_nop at :318-319) β€” an OOB-read β†’ dmesg leak channel; fw_size at :325-326 is inflated by the OOB value read at +24.

2. Harness verification (guest)

fwcheck.c replicates the sequence against a 4-byte blob placed at the end of a mapped page with a PROT_NONE guard page after it (stand-in for the exact-size request_firmware allocation). Guest run (DragonFly 6.5-DEVELOPMENT, cc 8.3 β€” run.log):

amdgpu_ucode_validate(4-byte blob) = 0 (0 == accepted)
validate passed: no minimum-size check (DF-1946 root), sdma_v3_0.c proceeds to read sub-header fields
RUN_EXIT=139 139=SIGSEGV          <- fault at the +16 read (ucode_version)
Segmentation fault (core dumped)

The first field read by sdma_v3_0.c:316 is the one that faults β€” the exact access order of the kernel function.

3. Honest classification

  • The guest kernel has /boot/kernel/amdgpu.ko but no AMD PCI device (pciconf -l β†’ zero 0x1002 vendors; virtio-vga only β€” env.txt), so the kernel path cannot execute β†’ not_reproduced, impact none (nothing demonstrated in-kernel). Same discipline as the family findings DF-1451/DF-1553/DF-1598 and DF-4027.
  • Trigger requires replacing an amdgpufw_* firmware file (root) and booting/loading amdgpu on Tonga/Polaris/Fiji/Carrizo/Stoney hardware.
  • Severity Low (privileged input; OOB read; fw_size inflation feeding the DF-1947 memcpy sink). CWE-125 / CWE-20.

4. Fix

fix.diff: reject any SDMA firmware whose datasize < sizeof(struct sdma_firmware_header_v1_0) before the sub-header reads (sdma_v3_0.c:312-317). This is the v3 twin of the DF-1598 fix; the shared root fix (full header cross-validation in amdgpu_ucode_validate) is DF-1946's scope, and the memcpy sink hardening is DF-1947's β€” deliberately not duplicated here.

fix_status: not_testable (no AMD hardware on the guest; the guard is trivially exercised by the harness's accepted-then-rejected sequence).

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

not_testable (no AMD hardware on the guest); fix.diff rejects datasize < sizeof(struct sdma_firmware_header_v1_0) before the sub-header reads - the v3 twin of the DF-1598 fix. Full header cross-validation belongs in amdgpu_ucode_validate (DF-1946); memcpy sink hardening is DF-1947.

fix.diff (patch --dry-run clean against unmodified sys/dev/drm/amd/amdgpu/sdma_v3_0.c); harness output demonstrates the accepted-then-rejected sequence the guard implements.
↓ fix.diffper-fix-DF-4238

Confirmed kernel references

Detail

Exploit chain

root replaces /boot amdgpufw__sdma[_1] with a 4-byte file whose first u32 is 4 -> amdgpu_ucode_validate passes -> sdma_v3_0.c:316/:317/:326 read 16/24/32 bytes past the allocation -> heap OOB read: garbage fw_version/feature_version (dmesg leak, burst_nop misconfig) and attacker-influenced OOB-read value inflates fw_size feeding the DF-1947 unbounded memcpy of ucode_size_bytes from fw->data+ucode_array_offset_bytes.

Evidence (decisive lines)

run.log: 'amdgpu_ucode_validate(4-byte blob) = 0 (0 == accepted)' then RUN_EXIT=139 (SIGSEGV) at the +16 read; VERDICT.md has the full line-cited trace; fix.diff applies cleanly to the unmodified sys file (patch --dry-run verified).

PoC changes

No seed PoC; fwcheck.c is a guard-page harness (no ASan needed on the DragonFly guest) replicating the validate-then-read sequence with struct layouts mirrored from amdgpu_ucode.h.

Verified recommended fix

In sdma_v3_0_init_microcode, reject firmware with fw->datasize < sizeof(struct sdma_firmware_header_v1_0) (and ideally ucode_array_offset_bytes+ucode_size_bytes > datasize) before reading any sub-header field.

Verdict

Parsing primitive proven by a guest-run harness; the kernel path is hardware-gated (no AMD GPU on the audit guest - pciconf shows zero 0x1002 devices, env.txt). sdma_v3_0_init_microcode() validates each SDMA firmware image only via amdgpu_ucode_validate() (amdgpu_ucode.c:251-260, single equality fw->datasize == hdr->size_bytes) and then reads beyond the common header: hdr->header.ucode_version at sdma_v3_0.c:316 (offset +16), hdr->ucode_feature_version at :317 (offset +32; sdma_firmware_header_v1_0 is 48 bytes), and header->ucode_size_bytes at :324-326 (offset +24) folded into adev->firmware.fw_size which later sizes the firmware BO memcpy'd at amdgpu_ucode.c:347-351 (DF-1947 sink; VI load type is AMDGPU_FW_LOAD_SMU per amdgpu_ucode.c:291-300). A 4-byte blob declaring size_bytes=4 passes validation; harness (fwcheck.c, guard-page stand-in for the exact-size request_firmware allocation) shows validate returns 0 and the very first sub-header read faults with SIGSEGV exit 139 - the exact access order of the kernel function. Root-installed firmware gate, same threat model/severity as family findings DF-1451 (sdma_v4_0), DF-1553 (cik_sdma), DF-1598 (sdma_v2_4).