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

amdgpu_ucode_validate only checks datasize==size_bytes: no minimum size, no header cross-validation (root of DF-1838 family)

Summary

amdgpu_ucode_validate L251-260: only checks fw->datasize==le32_to_cpu(hdr->size_bytes). No check (a) datasize>=sizeof(common_firmware_header)=32 (b) header_size_bytes sanity (c) ucode_array_offset_bytes+ucode_size_bytes<=datasize overflow-safe. 4-byte firmware with size_bytes=datasize passes; callers dereference hdr->ucode_size_bytes/ucode_array_offset_bytes (offsets 20-27) reading OOB. Root validation gateway for 30+ callers gfx_v7/8/9_0.c psp_v3_1/v10/v11_0.c gmc_v7/8_0.c amdgpu_uvd/vce.c. Root cause of DF-1838/1854/1875/1894/1895 family. Root attacker places/replaces firmware kld module. Impact: kernel heap OOB read feeds heap OOB write in init_single_fw. Fires on every amdgpu attach/resume. Fix: add sizeof check header_size_bytes bounds ucode_array_offset+ucode_size<=datasize overflow-safe.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1946 Β· 14 files
FileTypeDescriptionSize
validate_bypass.c trigger-source userspace replica of amdgpu_ucode_validate (vanilla + fixed) with 5 test cases 8.5 KB view raw
build.sh build-script cc -O2 -Wall -Wextra -o validate_bypass validate_bypass.c 213 B view raw
run.sh run-script executes ./validate_bypass 176 B view raw
build.log build-log final successful build, full output 69 B view raw
run.log run-log decisive run, full output (5 cases) 2.2 KB view raw
fix.diff suggested-fix git-apply-able: harden amdgpu_ucode_validate with sizeof check + overflow-safe payload bounds 1.2 KB view raw
fix_build.log build-log Phase 8: amdgpu.ko build with fix applied, rc=0 5.9 KB view raw
env.txt environment uname, cc version, sysctls 190 B view raw
dmesg.txt dmesg proof no AMD GPU is present on the guest (HW-gated) 1.2 KB view raw
VERDICT.md verdict full narrative + Phase 6 hard-blocker analysis 6.1 KB ↓ raw
README.md readme human-facing summary + reproduce instructions 1.9 KB ↓ raw
manifest.json manifest this catalog 3.5 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing summary + reproduce instructions
↓ download raw

DF-1946 β€” amdgpu_ucode_validate bypass harness

Proof-of-concept for the missing-size / missing-bounds checks in amdgpu_ucode_validate at sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260.

The bug is HW-gated (requires an AMD GPU to fire at runtime in the kernel); this folder contains the source+harness proof plus a git apply-able fix that builds cleanly into amdgpu.ko.

Files

File Purpose
validate_bypass.c Userspace harness replicating amdgpu_ucode_validate (vanilla + fixed).
build.sh cc -O2 -Wall -Wextra -o validate_bypass validate_bypass.c
run.sh Runs the harness.
build.log Final build output.
run.log Decisive run output (5 cases).
fix.diff Standalone fix for the validate gateway.
fix_build.log Phase 8: amdgpu.ko build output after applying fix (rc=0).
env.txt Guest environment.
dmesg.txt Proof no AMD GPU is present on the guest.
VERDICT.md Full narrative.
manifest.json Artifact catalog for the static site.

Reproduce

./build.sh && ./run.sh

Expected: for each of the 4 malicious cases, vanilla amdgpu_ucode_validate prints PASS (rc=0) <- bug: caller will deref OOB and the fixed version prints REJECT (rc=-22). The legitimate baseline passes both.

Build the fix

cd /usr/src
patch -p1 < fix.diff
cd sys/dev/drm/amd/amdgpu
make KERNCONF=X86_64_GENERIC    # ~30 s; rc=0; amdgpu.ko rebuilt
VERDICT.md verdict full narrative + Phase 6 hard-blocker analysis
↓ download raw

DF-1946 β€” amdgpu_ucode_validate missing minimum-size / bounds checks

Verdict: REPRODUCED (harness proof) β€” fix builds clean (rc=0)

The bug is real and exactly as the finding describes. amdgpu_ucode_validate() at sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260 performs exactly one check:

if (fw->datasize == le32_to_cpu(hdr->size_bytes))
    return 0;
return -EINVAL;

It never verifies that fw->datasize >= sizeof(struct common_firmware_header) (32 bytes), nor that the header's declared payload window [ucode_array_offset_bytes, ucode_array_offset_bytes + ucode_size_bytes) lies inside fw->data. A 4-byte firmware with size_bytes == datasize == 4 passes; every caller (30+, including gfx_v7/8/9_0.c, psp_v3_1/v10/v11_0.c, gmc_v7/8_0.c, amdgpu_uvd/vce.c) then dereferences fields past offset 4 β€” ucode_size_bytes at offset 20, ucode_array_offset_bytes at offset 24 β€” which are out of bounds. The OOB read of those fields then feeds the DF-1947 OOB write.

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger. An attacker who can plant or replace a firmware file in the kernel firmware search path supplies a 4-byte (or any tiny) image whose first 4 bytes equal the file size. amdgpu_ucode_validate returns 0.
  2. Primitive. The caller casts fw->data to struct common_firmware_header * and reads hdr->ucode_size_bytes (offset 20) and hdr->ucode_array_offset_bytes (offset 24). For a 4-byte image, both reads are OOB and the values come from whatever kernel heap / slab follows the firmware buffer. The caller then issues memcpy(kaddr, fw->data + arr_off, ucode_size) with attacker-supplied arr_off and attacker-or-residue ucode_size.
  3. Effect. The OOB read alone is a kernel-info-leak of at least 24 bytes of kernel heap. When the resulting sizes are large or attacker-crafted, the read feeds the DF-1947 OOB write (cross-finding amplification).

Threat model & Phase 6 (escalation)

HW-gated. amdgpu_ucode_validate runs only on the amdgpu attach/resume path, which requires an AMD GPU to be present. The QEMU guest has no AMD GPU (pciconf -lv shows only Intel 440FX + a QEMU Standard VGA; see dmesg.txt), so the kernel code path cannot be exercised at runtime on this guest. There is therefore no uid=0 escalation chain to pursue: the bug's trigger is in dead code on this guest.

This is a valid hard blocker (per Phase 6: "vulnerable code path is dead / unreachable at runtime on this guest"). The primitive is real and is fully characterized at the harness level:

  • Primitive class: integer/struct-bounds violation β†’ OOB kernel-heap read of β‰₯24 bytes per call, attacker-controllable when the attacker controls the firmware file.
  • Realistic impact ceiling: on a host with an AMD GPU, this is the entry point for the entire DF-1838/1854/1875/1894/1895/1947 family. An attacker who can plant a malicious firmware file (root, or any writable path in the firmware search dir) crosses the rootβ†’kernel boundary into heap corruption. This is a defense-in-depth gap: root-should-not-trivially-corrupt-kernel.

The reproduction is therefore a source+harness proof (the harness replicates the validate logic verbatim and shows the malicious inputs pass), not a kernel runtime trigger.

PoC changes

validate_bypass.c is a self-contained userspace C harness that: - Replicates amdgpu_ucode_validate verbatim. - Replicates the proposed fixed version. - Runs 5 cases: 4 malicious (4-byte, 16-byte, OOB-payload-claim, wrap-around-offset) and 1 well-formed baseline.

For every malicious case the vanilla function returns 0 ("PASS, caller will deref OOB"); the fixed function returns -EINVAL. The harness also prints exactly how many bytes past fw->data the caller's deref / memcpy would touch.

How to reproduce

./build.sh && ./run.sh

Build is cc -O2 -Wall -Wextra -o validate_bypass validate_bypass.c. Expected output: 4 malicious cases each show "vanilla: PASS (rc=0) <- bug" and "fixed: REJECT (rc=-22)"; the legitimate baseline shows both pass.

fix.diff is a standalone git apply-able unified diff against sys/dev/drm/amd/amdgpu/amdgpu_ucode.c. It tightens amdgpu_ucode_validate to:

  1. Reject any firmware with datasize < sizeof(struct common_firmware_header) (32 bytes) β€” fixes CASE 1 & 2.
  2. Reject if datasize != size_bytes (unchanged behavior).
  3. Reject if `(uint64_t)ucode_array_offset_bytes + (uint64_t)ucode_size_bytes

    size_bytes` β€” overflow-safe β€” fixes CASE 3 & 5.

This supersedes the finding markdown's proposal (the proposal said "add sizeof check, header_size_bytes bounds, ucode_array_offset+ucode_size<=datasize overflow-safe"; the implemented diff performs exactly that, in 64-bit math).

Fix validation (Phase 8)

  • Baseline (#0 unpatched, INVARIANTS ON, GENERIC): amdgpu.ko builds clean with the unfixed source.
  • Applied fix.diff to /usr/src, removed amdgpu_ucode.o, rebuilt amdgpu.ko with make -j6 KERNCONF=X86_64_GENERIC from /usr/src/sys/dev/drm/amd/amdgpu β†’ rc=0, amdgpu.ko rebuilt with the fixed amdgpu_ucode_validate (symbol present in .ko).
  • Runtime kernel PoC of the fix is not feasible on this guest: the path fires only on amdgpu attach (no AMD GPU). The harness's side-by-side comparison of vanilla vs fixed amdgpu_ucode_validate IS the before/after evidence: vanilla returns 0 for all 4 malicious inputs; fixed returns -EINVAL for the same 4 inputs and 0 for the legitimate baseline.

References

  • sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260 β€” the vulnerable function.
  • sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:26-37 β€” struct common_firmware_header layout (32 bytes; ucode_size_bytes at offset 20, ucode_array_offset_bytes at offset 24).
  • Callers (sampling): gfx_v7_0.c, gfx_v8_0.c, gfx_v9_0.c, psp_v3_1.c, psp_v10_0.c, psp_v11_0.c, gmc_v7_0.c, gmc_v8_0.c, amdgpu_uvd.c, amdgpu_vce.c, smu7_smumgr.c, smu8_smumgr.c.
  • Related findings: DF-1947 (the OOB write that this OOB read feeds), DF-1838/1854/1875/1894/1895.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. baseline 4 PASS buggy; fixed 4 REJECT. amdgpu.ko 3741160B.

baseline PASS on all 4 malicious; patched REJECT on 4, pass on legit 64B baseline. amdgpu.ko rc=0.
↓ fix.diff6.5-DEV #0 (amdgpu.ko rebuilt rc=0)

Confirmed kernel references

Detail

Exploit chain

none (read-only at this site - gateway for DF-1947 write). HW-gated (no AMD GPU).

Evidence (decisive lines)

CASE1 4B fw: vanilla PASS (caller derefs OOB at offset 20), fixed REJECT. CASE3 32B fw claims 256B payload at offset 256: vanilla PASS, fixed REJECT.

PoC changes

validate_bypass.c harness, fix.diff (3 checks: min size 32B, size_bytes match, overflow-safe array+ucode bounds).

Verified recommended fix

Harden amdgpu_ucode_validate: (1) reject datasize < sizeof(common_firmware_header)=32; (2) keep existing size_bytes check; (3) reject if ucode_array_offset+ucode_size > size_bytes (64-bit math).

Verdict

REPRODUCED source+harness. amdgpu_ucode_validate at amdgpu_ucode.c:251-260 only checks fw->datasize==size_bytes. No minimum/header sanity/overflow-safe payload bounds. Harness: 4 malicious firmware images (4B, 16B, OOB-payload, wrap) all PASS vanilla, fixed rejects all 4.