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

amdgpu_uvd sw_init/resume trust unvalidated firmware ucode extent fields (ucode_size_bytes/ucode_array_offset_bytes) β€” sw_init bo_size u32 wrap, resume ~4GiB OOB read past the firmware blob and underflowed OOB memset_io (DF-1946 family instance)

Summary

The only gate is amdgpu_ucode_validate (equality-only); the extent fields are then consumed unbounded: the bo_size '+8' wraps to 0 at 0xFFFFFFF8 (bo allocated with no firmware space); resume does memcpy_to_io(fw->data+offset, 0xFFFFFFF8) - a ~4GiB OOB read; and 'size -= ucode_size_bytes' underflows into an OOB memset_io write. Root-controlled firmware install path (malicious/supply-chain). Guest deterministic arithmetic replica. Extent-bound fix (resume inherits).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4405 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
fw_hdr_overflow.c β€” 4.2 KB view raw
build.sh β€” 118 B view raw
run.sh β€” 122 B view raw
build.log β€” 70 B view raw
run.log β€” 1.2 KB view raw
run.2.log β€” 1.2 KB view raw
env.txt β€” 708 B view raw
fix.diff β€” 884 B view raw
verdict.json β€” 2.9 KB view raw
manifest.json β€” 1.2 KB view raw

DF-4405 β€” amdgpu_uvd_sw_init/amdgpu_uvd_resume trust unvalidated firmware header extent fields (DF-1946 family instance)

File: sys/dev/drm/amd/amdgpu/amdgpu_uvd.c:195-202 (only gate), :250-253 (bo_size wrap), :403-411 (OOB copy / underflowed memset). Severity: Low (requires attacker-installed firmware β€” root-only supply path β€” on AMD hardware) Β· Confidence: certain (arithmetic demo + line-certain inspection; family root = DF-1946 in amdgpu_ucode.c:251).

amdgpu_ucode_validate() only checks fw->datasize == hdr->size_bytes (sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260). The UVD loader then trusts hdr->ucode_size_bytes and hdr->ucode_array_offset_bytes without any bound against the blob:

  • amdgpu_uvd.c:253 β€” AMDGPU_GPU_PAGE_ALIGN(ucode_size_bytes + 8): with ucode_size_bytes = 0xFFFFFFF8 the u32 +8 wraps to 0 and the UVD bo is allocated without any firmware space;
  • amdgpu_uvd.c:405-407 β€” resume copies ucode_size_bytes bytes from fw->data + ucode_array_offset_bytes into VRAM: with the same header that is a ~4 GiB OOB read past a 32 KiB kernel firmware buffer;
  • amdgpu_uvd.c:408-411 β€” size -= ucode_size_bytes underflows and the following memset_io(ptr, 0, size) writes past the bo's ioremap extent.

Build

cc -O2 -Wall -o fw_hdr_overflow fw_hdr_overflow.c      # or ./build.sh (guest)

Run

./fw_hdr_overflow                                     # or ./run.sh (guest)

Expected output

Exact-arithmetic replica showing: amdgpu_ucode_validate returns 0 (ACCEPTED) for a 32 KiB blob whose header claims ucode_size_bytes = 0xFFFFFFF8 at offset 0x1000; the +8 wrap zeroes the bo_size term; resume would copy 0xfffffff8 bytes (4 GiB) from the blob β€” 4,294,938,616 bytes past its end β€” and then memset_io 8 bytes past the bo end. Exit 0.

Not-a-PoC note

Executing the real path needs an AMD GPU and a root-installed malicious firmware file (/boot/modules/amdgpufw_*_uvd); the audit guest has neither the hardware (QEMU bochs VGA only, no /dev/dri) nor, as policy, root-attacker scope. Status: inconclusive; the arithmetic is deterministic C semantics and is demonstrated in full (see run.log).

VERDICT.md
↓ download raw

DF-4405 β€” VERDICT

Finding: the UVD firmware loader in sys/dev/drm/amd/amdgpu/amdgpu_uvd.c consumes common_firmware_header.ucode_size_bytes and .ucode_array_offset_bytes with no bound against the actual firmware blob β€” the only gate, amdgpu_ucode_validate() (sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260), checks a different field (fw->datasize == hdr->size_bytes).

Verdict: inconclusive β€” the kernel path cannot be executed on the audit guest (no AMD GPU: pciconf shows only QEMU bochs VGA, /dev/dri absent, amdgpu.ko loads but never attaches) and triggering it additionally requires a root-installed malicious firmware file. The defect itself is certain:

  1. amdgpu_uvd.c:253 β€” bo_size += AMDGPU_GPU_PAGE_ALIGN(le32_to_cpu(hdr->ucode_size_bytes) + 8): ucode_size_bytes + 8 is u32 arithmetic; at 0xFFFFFFF8 it wraps to 0 and the UVD bo is created without firmware space (demo: run.log, sw_init block).
  2. amdgpu_uvd.c:405-407 β€” resume (when saved_bo == NULL, the normal first-resume path for direct-load firmware) executes memcpy_toio(cpu_addr, fw->data + offset, ucode_size_bytes) β€” a 4 GiB OOB read past the firmware buffer for the same header (demo: run.log, resume block: 4,294,938,616 bytes past a 32 KiB blob).
  3. amdgpu_uvd.c:408-411 β€” size -= ucode_size_bytes underflows u32 and the subsequent memset_io(ptr, 0, size) runs past the bo's mapped extent.

This is the amdgpu_uvd.c instance of the DF-1946 family (root: amdgpu_ucode.c:251; siblings DF-1164 gfx_v7_0, DF-1451 sdma_v4_0, DF-1598 sdma_v2_4, DF-1854 psp_asd …). Pass 1 of this file did not file it (only DF-1595/DF-1596 exist for this file).

Severity rationale

Low: the attacker must control a firmware file under /boot/modules/ (root-only write) and the machine must have AMD hardware. The impact if those preconditions hold is a kernel OOB read of arbitrary length and an OOB memset_io β€” memory corruption β€” so the family fix is worth backporting, but the privilege model keeps it Low.

Attempts

One build+run cycle on the guest (exact arithmetic replication, deterministic, exit 0, two runs β€” run.log, run.2.log). No kernel execution possible.

Fix

fix.diff (git-apply clean) bounds the ucode extent against adev->uvd.fw->datasize in sw_init, immediately after amdgpu_ucode_validate() β€” the same style as the family fix; this also inoculates resume (firmware blob is immutable between init and resume). A patched-kernel run is not possible on this guest (no AMD GPU), so fix_status: not_testable.

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 bounds the ucode extent in sw_init (which resume then inherits, blob immutable); git apply --check clean; patched-kernel A/B impossible on this guest (no AMD GPU).

fix.diff; git apply --check; VERDICT.md severity/family rationale
↓ fix.diffper-fix-DF-4405

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log/run.2.log (validate=ACCEPTED, +8 wrap -> PAGE_ALIGN(0)=0, 4GiB OOB read length, memset underflow); VERDICT.md; fix.diff (git apply --check clean)

PoC changes

Trigger written fresh (no seed): deterministic u32 arithmetic replica of both consumer sites for a header that passes amdgpu_ucode_validate; initial value 0xFFFFFF00 corrected to 0xFFFFFFF8 to demonstrate the clean +8 wrap.

Verified recommended fix

In amdgpu_uvd_sw_init, after amdgpu_ucode_validate, reject firmware whose (u64)ucode_array_offset_bytes + ucode_size_bytes > fw->datasize (see fix.diff); mirrors the DF-1946 family fix.

Verdict

amdgpu_uvd.c consumes common_firmware_header.ucode_size_bytes/ucode_array_offset_bytes without bounding them against the firmware blob: amdgpu_uvd.c:253 PAGE_ALIGN(ucode_size_bytes+8) wraps to 0 at 0xFFFFFFF8 (bo allocated with no fw space), amdgpu_uvd.c:405-407 resume memcpy_toio performs a ~4GiB OOB read past the kernel firmware buffer, amdgpu_uvd.c:408-411 size-=ucode_size_bytes underflows into an OOB memset_io. Only gate, amdgpu_ucode_validate (amdgpu_ucode.c:251-260), checks a different field (datasize==size_bytes). DF-1946 family instance in this file, unfiled by pass 1. Runtime execution impossible on the audit guest (no AMD GPU, /dev/dri absent) and requires root-installed firmware; exact arithmetic demonstrated deterministically (run.log): certain defect, inconclusive runtime.