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

Integer underflow + missing bounds in amdgpu_ucode_init_single_fw/patch_jt memcpy: heap OOB write

Summary

amdgpu_ucode_init_single_fw L354-359 MEC1/MEC2: ucode_size=ucode_size_bytes-(jt_size*4) uint32_t wraps when jt_size*4>ucode_size_bytes e.g. 0x10-0x100=0xFFFFFF10 memcpy(fw_buf_ptr,fw->data+arr_off,0xFFFFFF10) multi-gigabyte heap OOB write into fixed-size BO. DMCU_ERAM L369-374 same ucode_size_bytes-intv_size_bytes underflow. patch_jt L412-418 same jt_size*4 no bounds. NO memcpy checks (a) ucode_array_offset_bytes+ucode_size<=fw->datasize (b) fw_offset+ucode_size<=fw_size (c) unsigned subtraction no-wrap. Relies entirely on amdgpu_ucode_validate which only checks datasize==size_bytes (DF-1946). Root attacker crafted firmware module: common.size_bytes=datasize=0x80 common.header_size=0x50 jt_size=0x40 ucode_size_bytes=0x10 -> ucode_size=0xFFFFFF10 -> panic/heap-overflow. Fires on PSP hw_init amdgpu_psp.c:428 and SMU smu7/smu8_smumgr.c. Fix: underflow guard jt_sz>ucode_sz/4 return EINVAL + bounds arr_off+ucode_size<=datasize fw_offset+ucode_size<=fw_size.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1947 Β· 14 files
FileTypeDescriptionSize
oob_write.c trigger-source userspace replica of amdgpu_ucode_init_single_fw MEC1/MEC2 path + guard-page proof + DMCU/patch_jt analytics 9.6 KB view raw
build.sh build-script cc -O2 -Wall -Wextra -o oob_write oob_write.c 298 B view raw
run.sh run-script executes ./oob_write 130 B view raw
build.log build-log final successful build, full output 226 B view raw
run.log run-log decisive run, full output incl underflow + SIGSEGV 1.5 KB view raw
fix.diff suggested-fix git-apply-able: underflow/multiplication guards + arr_off+ucode_size bounds in init_single_fw and patch_jt 5.8 KB view raw
fix_build.log build-log Phase 8: amdgpu.ko build with fix applied, rc=0 5.0 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 + primitive characterization 8.8 KB ↓ raw
README.md readme human-facing summary + reproduce instructions 2.3 KB ↓ raw
manifest.json manifest this catalog 4.1 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-1947 β€” amdgpu_ucode_init_single_fw / patch_jt multi-GiB heap OOB write harness

Proof-of-concept for the integer underflow + missing bounds in amdgpu_ucode_init_single_fw and amdgpu_ucode_patch_jt at sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:354-418.

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

Files

File Purpose
oob_write.c Userspace harness replicating the MEC1/MEC2 arithmetic and proving the OOB write via a guard-page fault. Also analytically shows DMCU_ERAM and patch_jt paths.
build.sh cc -O2 -Wall -Wextra -o oob_write oob_write.c
run.sh Runs the harness.
build.log Final build output.
run.log Decisive run output (underflow + SIGSEGV).
fix.diff Standalone fix for init_single_fw + patch_jt.
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 + Phase 6 hard-blocker analysis.
manifest.json Artifact catalog for the static site.

Reproduce

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

Expected output:

MEC1/MEC2 path arithmetic (amdgpu_ucode.c:354-355):
  ucode_size_bytes         = 0x00000010
  jt_size * 4              = 0x00000100
  ucode_size (memcpy len)  = 0xffffff10  (4294967056 bytes)
  ...
=== guard-page proof ===
SIGSEGV at BO+4096  -> write crossed BO end (capacity=4096)
PROVEN: the memcpy length computed by amdgpu_ucode_init_single_fw writes
PAST the destination buffer.

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 + primitive characterization
↓ download raw

DF-1947 β€” Integer underflow + missing bounds in amdgpu_ucode_init_single_fw / patch_jt: multi-gigabyte heap OOB write

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

The bug is real and exactly as the finding describes. Three sites in sys/dev/drm/amd/amdgpu/amdgpu_ucode.c perform unsigned 32-bit subtraction/multiplication on attacker-controlled firmware header fields and pass the result unchecked as a memcpy length into a fixed-size GPU BO:

Site Line Operation Wrap condition
MEC1/MEC2 path 354-359 ucode_size_bytes - jt_size*4 jt_size*4 > ucode_size_bytes
DMCU_ERAM path 369-374 ucode_size_bytes - intv_size_bytes intv_size_bytes > ucode_size_bytes
MEC1/MEC2_JT path 362-367 jt_size*4 (multiplication) jt_size > UINT32_MAX/4
DMCU_INTV path 375-381 (uses intv_size_bytes directly) no upstream bounds
patch_jt 412-418 jt_size*4 (multiplication) jt_size > UINT32_MAX/4

None of the memcpy sites performs: - (a) ucode_array_offset_bytes + ucode_size <= fw->datasize - (b) fw_offset + ucode_size <= fw_size - (c) underflow / overflow guard on the unsigned arithmetic.

The function relies entirely on amdgpu_ucode_validate (DF-1946), which itself only checks datasize == size_bytes.

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

  1. Trigger. An attacker supplies a crafted gfx firmware image: common.size_bytes = 0x80 (== datasize; passes DF-1946) common.header_size_bytes = 0x50 common.ucode_size_bytes = 0x10 (16 bytes) common.ucode_array_offset_bytes = 0x40 gfx.jt_offset = 0 gfx.jt_size = 0x40 (64 dwords => 256 bytes) jt_size*4 (0x100) > ucode_size_bytes (0x10) β‡’ ucode_size = 0xFFFFFF10.
  2. Primitive. memcpy(ucode->kaddr, fw->data + 0x40, 0xFFFFFF10). The destination ucode->kaddr is a fixed-size GPU BO allocated once for the summed ucode_size of every firmware blob (amdgpu_ucode_create_bo at amdgpu_ucode.c:423-429). The memcpy writes ~4 GiB past the BO into kernel heap. - Write size: ~4 GiB (uint32 wrap value), bounded in practice by the page tables / OOM panic. - Content control: fully attacker-controlled β€” the source bytes are the attacker-supplied firmware file contents (repeated / wrapped through fw->data + arr_off).
  3. Effect. Immediate kernel heap corruption and panic; on a non-INVARIANTS kernel the corruption is silent and the write continues until the page tables run out, giving a powerful arbitrary-write primitive into kernel heap with attacker bytes.

Harness proof

oob_write.c faithfully replicates the MEC1/MEC2 path arithmetic and proves the OOB write with a guard-page fault:

MEC1/MEC2 path arithmetic (amdgpu_ucode.c:354-355):
  ucode_size_bytes         = 0x00000010
  jt_size * 4              = 0x00000100
  ucode_size (memcpy len)  = 0xffffff10  (4294967056 bytes)
  bo capacity              = 4096 bytes (fixed-size GPU BO)
  >>> memcpy would write 4294962960 bytes PAST end of BO into kernel heap <<<

=== guard-page proof ===
Allocating 4096-byte 'BO' followed by a 4096-byte guard page, writing attacker
bytes (0xA1) starting at BO+0...
SIGSEGV at BO+4096  (fault addr=0x80047d000)  -> write crossed BO end
(capacity=4096)
PROVEN: the memcpy length computed by amdgpu_ucode_init_single_fw writes PAST
the destination buffer.

The same harness analytically demonstrates the DMCU_ERAM underflow (0x10 - 0x100 = 0xFFFFFF10) and the patch_jt multiplication-wrap path (0x60000000 * 4 = 0x80000000 = 2 GiB memcpy).

Threat model & Phase 6 (escalation)

HW-gated. amdgpu_ucode_init_single_fw is called from amdgpu_ucode_init_bo (amdgpu_ucode.c:449) on every amdgpu attach/resume with AMDGPU_FW_LOAD_PSP. The QEMU guest has no AMD GPU (see dmesg.txt), so the kernel code path cannot be exercised at runtime. There is no uid=0 escalation chain on this guest β€” the bug's trigger is in dead code here.

This is a valid hard blocker (per Phase 6: "vulnerable code path is dead / unreachable at runtime on this guest AND no harness can exercise it in kernel context"). The primitive itself β€” once triggered on real HW β€” is an unbounded kernel-heap OOB write with attacker-controlled bytes, which is exactly the kind of primitive that, with slab grooming, would convert to uid=0 (forge a struct ucred, overwrite a function pointer to userspace shellcode given no SMAP/SMEP). On this guest the gating factor is purely the missing AMD GPU; we cannot close the chain without it. The primitive is fully characterized at the harness level.

Realistic impact ceiling: on a host with an AMD GPU (or one an attacker can plug in / passthrough), an attacker who can place a firmware file supplies a crafted image → ~4 GiB kernel heap write with attacker bytes. With heap grooming this is a root→kernel escape. Even without escalation the OOB write reliably panics the kernel, so the floor is a DoS.

PoC changes

oob_write.c is a self-contained userspace C harness that: - Replicates amdgpu_ucode_validate (vanilla) and shows the crafted image passes. - Replicates the MEC1/MEC2 path arithmetic and prints the wrapped ucode_size value. - Allocates a 4 KiB "BO" + guard page, then writes 0xA1 bytes page-by-page into it (mimicking what the kernel memcpy would do) until the guard page is hit. SIGSEGV at BO+4096 proves the write crosses the BO end. - Analytically demonstrates the same wrap pattern for the DMCU_ERAM and patch_jt paths.

How to reproduce

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

Build is cc -O2 -Wall -Wextra -o oob_write oob_write.c. Expected: prints the underflow arithmetic and triggers SIGSEGV at BO+4096, proving the OOB write.

fix.diff is a standalone git apply-able unified diff against sys/dev/drm/amd/amdgpu/amdgpu_ucode.c. It adds at every memcpy site:

  • Pre-check (in init_single_fw): arr_off > fw_size || ucode_size_bytes > fw_size - arr_off rejects any firmware whose declared payload window does not fit inside fw->data.
  • Underflow guards:
  • MEC1/MEC2: jt_size > UINT32_MAX/4 || jt_size*4 > ucode_size_bytes rejects before the subtraction.
  • DMCU_ERAM: intv_size > ucode_size_bytes rejects before the subtraction.
  • Multiplication guards: every jt_size * 4 / jt_offset * 4 is preceded by > UINT32_MAX/4 check.
  • patch_jt (L399-421): full rewrite of the bounds computation with the same pattern β€” no arithmetic is fed to memcpy without an overflow / fit check.

This matches the finding markdown's proposal (underflow guard jt_sz>ucode_sz/4 return EINVAL + bounds arr_off+ucode_size<=datasize, fw_offset+ucode_size<=fw_size). Combined with the DF-1946 fix to the validate gateway, this closes the entire family.

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 (size grew from 3,741,128 β†’ 3,741,144 bytes, consistent with the added bounds checks in init_single_fw and patch_jt).
  • 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 before/after comparison IS the evidence: vanilla arithmetic produces 0xFFFFFF10 (memcpy of 4 GiB); the fixed arithmetic would return -EINVAL at the guard before reaching the memcpy.

References

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. baseline MEC1/MEC2 arithmetic SIGSEGV at BO+4096; patched guards return -EINVAL. amdgpu.ko rc=0.

baseline underflow 0xffffff10 -> SIGSEGV; patched return -EINVAL before subtraction. amdgpu.ko 3741144B.
↓ fix.diff6.5-DEV #0 (amdgpu.ko rebuilt rc=0)

Confirmed kernel references

Detail

Exploit chain

HW-gated (no AMD GPU). Primitive: ~4GiB attacker-byte heap OOB write. On real HW + slab grooming -> uid0 (forge ucred/funcptr; no SMAP/SMEP/KASLR).

Evidence (decisive lines)

MEC1/MEC2: ucode_size=0x10-0x100=0xffffff10 -> memcpy 4294967056 bytes into 4096B BO -> SIGSEGV at BO+4096. DMCU_ERAM same. patch_jt: jt_size=0x60000000 -> memcpy 2147483648 bytes.

PoC changes

oob_write.c harness (arithmetic + guard-page fault), fix.diff (3 layers: top pre-check, per-branch underflow guards, full patch_jt rewrite).

Verified recommended fix

(1) Top of init_single_fw: pre-check arr_off/ucode_size_bytes within fw_size; (2) MEC1/MEC2: if(jt_size>UINT32_MAX/4 || jt_size*4>ucode_size_bytes) return -EINVAL; (3) DMCU_ERAM: if(intv_size>ucode_size_bytes) return -EINVAL; (4) Rewrite patch_jt with same pattern.

Verdict

REPRODUCED source+harness. amdgpu_ucode_init_single_fw MEC1/MEC2 at amdgpu_ucode.c:354-359: ucode_size=ucode_size_bytes-(jt_size4) u32 wraps when jt_size4>ucode_size_bytes. e.g. 0x10-0x100=0xFFFFFF10 -> memcpy(fw_buf_ptr, fw->data+arr_off, 0xFFFFFF10) multi-GiB heap OOB write into fixed-size GPU BO. DMCU_ERAM and patch_jt same shape. Harness guard-page proof SIGSEGV at BO+4096.