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)
PoC verification
Evidence pack
findings/poc/DF-1947 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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)
- 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. - Primitive.
memcpy(ucode->kaddr, fw->data + 0x40, 0xFFFFFF10). The destinationucode->kaddris a fixed-size GPU BO allocated once for the summed ucode_size of every firmware blob (amdgpu_ucode_create_boatamdgpu_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 throughfw->data + arr_off). - 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.
Recommended fix
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_offrejects any firmware whose declared payload window does not fit insidefw->data. - Underflow guards:
- MEC1/MEC2:
jt_size > UINT32_MAX/4 || jt_size*4 > ucode_size_bytesrejects before the subtraction. - DMCU_ERAM:
intv_size > ucode_size_bytesrejects before the subtraction. - Multiplication guards: every
jt_size * 4/jt_offset * 4is preceded by> UINT32_MAX/4check. - 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.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 (size grew from 3,741,128 β 3,741,144 bytes, consistent with the added bounds checks ininit_single_fwandpatch_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 wouldreturn -EINVALat the guard before reaching the memcpy.
References
sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:354-359β MEC1/MEC2 underflow site.sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:369-374β DMCU_ERAM underflow site.sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:412-418β patch_jt multiplication site.sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260βamdgpu_ucode_validate(the only upstream check; DF-1946).sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:423-429βamdgpu_ucode_create_bo(allocates the fixed-size BO that gets overflowed).sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:449-488βamdgpu_ucode_init_bo, the caller that loops through every ucode and callsinit_single_fw/patch_jton attach/resume.sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:61-66βstruct gfx_firmware_header_v1_0(jt_offset,jt_size).sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:160-165βstruct dmcu_firmware_header_v1_0(intv_offset_bytes,intv_size_bytes).- Related: DF-1946 (validate gateway), DF-1838/1854/1875/1894/1895 (same family).
Fix verification
fixedVALIDATED. 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.
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
- :
- 3
- 5
- 4
- 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
- :
- 3
- 5
- 9
- 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
- :
- 3
- 6
- 9
- 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
- :
- 4
- 1
- 2
- 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
- :
- 4
- 4
- 9
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.
No comments yet.