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)
PoC verification
Evidence pack
findings/poc/DF-1946 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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)
- 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_validatereturns 0. - Primitive. The caller casts
fw->datatostruct common_firmware_header *and readshdr->ucode_size_bytes(offset 20) andhdr->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 issuesmemcpy(kaddr, fw->data + arr_off, ucode_size)with attacker-suppliedarr_offand attacker-or-residueucode_size. - 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.
Recommended fix
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:
- Reject any firmware with
datasize < sizeof(struct common_firmware_header)(32 bytes) β fixes CASE 1 & 2. - Reject if
datasize != size_bytes(unchanged behavior). - 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.kobuilds clean with the unfixed source. - Applied
fix.diffto/usr/src, removedamdgpu_ucode.o, rebuiltamdgpu.kowithmake -j6 KERNCONF=X86_64_GENERICfrom/usr/src/sys/dev/drm/amd/amdgpuβrc=0,amdgpu.korebuilt with the fixedamdgpu_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_validateIS the before/after evidence: vanilla returns 0 for all 4 malicious inputs; fixed returns-EINVALfor the same 4 inputs and0for 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_headerlayout (32 bytes;ucode_size_bytesat offset 20,ucode_array_offset_bytesat 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
fixedVALIDATED. 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.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- u
- c
- o
- d
- e
- .
- c
- :
- 2
- 5
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- u
- c
- o
- d
- e
- .
- h
- :
- 2
- 6
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.
No comments yet.