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)
PoC verification
Evidence pack
findings/poc/DF-4238 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_0is 48 bytes β amdgpu_ucode.h:120-126) - sdma_v3_0.c:324-326
header->ucode_size_bytes(common header +24) folded intoadev->firmware.fw_size, which sizes the firmware BO later memcpy'd fromfw->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_versionat +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 insdma_v3_0_init_microcode(the shared root fix β offset/size cross-validation inamdgpu_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.
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 ONLYfw->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 castsfw->datatostruct 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 intoadev->firmware.fw_sizewith no bound vsdatasize; the payload is later memcpy'd fromfw->data + ucode_array_offset_bytesforucode_size_bytesbytes 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.kobut no AMD PCI device (pciconf -lβ zero 0x1002 vendors; virtio-vga only βenv.txt), so the kernel path cannot execute βnot_reproduced, impactnone(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_testablenot_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.
Confirmed kernel references
Detail
Exploit chain
root replaces /boot amdgpufw_
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).
No comments yet.