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

amdgpu_cgs_get_firmware_info CP_MEC branch: info->image_size = jt_offset << 2 from an unvalidated firmware-header dword β€” SMU-side OOB DMA read of up to ~4 GB from the staged fw_buf window (DF-4774 shape on the CP branch, kernel-side fully in-bounds)

Summary

THE DF-4774 FOLLOW-UP found the same unchecked-header-window shape on the CP_MEC branch: image_size = jt_offset<<2 (a raw MEC header dword) never bounded against the file or staged copy - it flows into the smu7 TOC entry data_size_byte and PPSMC_MSG_LoadUcodes commands the SMU DMA to read an 8x-oversized window from fw_buf staged at only ALIGN(ucode)+ALIGN(jt). HARNESS-REPRODUCED 3/3 with verbatim-function methodology (mock GTT + mock SMU DMA): crafted self-consistent header -> 57344 bytes SMU-side OOB DMA read beyond the window + unmapped-page fault class, with kernel-side OOB reads = 0 (distinguishes from DF-1947); patched twin rejects at construction. Root-planted fw on SMU-load asics, at attach. Bound-jt_offset-at-construction fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4862 Β· 14 files
FileTypeDescriptionSize
cgs_mec_jt_oob.c β€” 24.9 KB view raw
cgs_mec_jt_oob_patched.c β€” 23.0 KB view raw
build.sh β€” 188 B view raw
run.sh β€” 168 B view raw
build.log β€” 447 B view raw
run.log β€” 3.4 KB view raw
run.2.log β€” 1.8 KB view raw
run.3.log β€” 1.8 KB view raw
env.txt β€” 299 B view raw
fix.diff β€” 1.4 KB view raw
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 6.0 KB view raw

DF-4862 β€” amdgpu_cgs CP_MEC image_size = jt_offset << 2 unbounded (SMU-side OOB DMA read)

What

amdgpu_cgs_get_firmware_info() non-SMU branch (sys/dev/drm/amd/amdgpu/amdgpu_cgs.c:234-235):

if (CGS_UCODE_ID_CP_MEC == type)
        info->image_size = le32_to_cpu(header->jt_offset) << 2;

jt_offset is a raw dword from the MEC gfx firmware file header (gfx_firmware_header_v1_0.jt_offset, amdgpu_ucode.h:64). It is never bounded β€” not against ucode_size_bytes, not against the file, not against the staged copy window. <<2 wraps mod 2^32, so any value 0..0xFFFFFFFC can be produced. This is the same unchecked-header-window shape as DF-4774 (which bounded the SMU branch); the CP branch was missed.

Consumer chain (why it matters)

  • smu7_populate_single_firmware_entry() smu7_smumgr.c:305-327: entry->data_size_byte = info.image_size (:327)
  • smu7_request_smu_load_fw() smu7_smumgr.c:405 populates the CP_MEC TOC entry, :424 memcpy_toios the TOC to SMU-visible DRAM, :431 sends PPSMC_MSG_LoadUcodes β€” after which the SMU DMA engine copies data_size_byte bytes from entry.image_addr, the MEC1 window inside adev->firmware.fw_buf staged by amdgpu_ucode_init_bo() (amdgpu_ucode.c:449-485) as ALIGN(ucode_size_bytes,PAGE) (+ JT pages).
  • With a crafted MEC header the SMU reads far beyond the staged window: adjacent GTT pages are disclosed into SMC SRAM (device-side), and the unmapped range beyond triggers a GPU page fault / SMU hang (driver-load failure β†’ repeated GPU resets β†’ system-level DoS).
  • The only validation anywhere on the path is amdgpu_ucode_validate()'s datasize == size_bytes (amdgpu_ucode.c:251-259, DF-1946).

Distinct from DF-1947 (staging memcpy OOB in amdgpu_ucode.c): scenario [2] of the harness has zero kernel-side OOB reads β€” the file is entirely self-consistent β€” yet the SMU-side window is 8x oversized. The kernel never notices; the GPU-side DMA does the damage.

Build / run / expected

sh build.sh    # cc -O0 -g -Wall both binaries (baseline + patched twin)
sh run.sh      # runs baseline then patched twin

Expected (baseline):

  • [1] legit header (jt_offset=0x300 dwords): data_size_byte=0xc00, SMU DMA fully in-bounds β€” no false positive.
  • [2] crafted header (jt_offset=0x4000 dwords, everything else sane, validate PASSes): data_size_byte=0x10000 vs staged window 0x2000 β†’ RESULT: SMU-SIDE OOB DMA READ CONFIRMED: 57344 bytes beyond the staged fw_buf window + unmapped-page fault class, with kernel-side OOB reads during init_bo/patch_jt: 0 bytes.
  • [3] jt_offset=0x3F000000: child killed by SIGSEGV β€” the kernel-side patch_jt wild source (DF-1947 overlap) faults first at driver load; shows the extreme end of the same unvalidated field.
  • [4] (also DF-4863): ucode[AMDGPU_UCODE_ID_MAXIMUM].fw aliases firmware.fw_buf β€” ALIAS CONFIRMED, garbage propagated to caller.

Expected (patched twin, fix.diff at the construction): [1] still loads identically; [2] populate CP_MEC entry: -22 (rejected at the cgs construction β€” FIX VALIDATED); [4] -22 one-past-end index rejected.

Harness methodology

No AMD GPU exists on the audit guest, so (per DF-4774 / DF-4665 / DF-3683 / DF-2096) the harness copies the cited function bodies 1:1 β€” fw_type_convert, amdgpu_get_firmware_version, the non-SMU branch of amdgpu_cgs_get_firmware_info, amdgpu_ucode_validate, the staging math of amdgpu_ucode_init_single_fw/amdgpu_ucode_patch_jt/amdgpu_ucode_init_bo, and smu7_populate_single_firmware_entry β€” with 1:1 struct layouts (_Static_asserted against the real field offsets) and a mock GTT/SMU-DMA engine whose mapping model (staged BO + adjacent mapped page + unmapped guard) mirrors the real GPU address space. [VERBATIM] markers in the source cite each block's kernel lines.

Deterministic: 3/3 runs identical (run.log, run.2.log, run.3.log).

VERDICT.md
↓ download raw

DF-4862 β€” VERDICT

Status: reproduced (verbatim-function harness, 3/3 deterministic runs) Impact: dos (device-side OOB DMA read of up to ~4 GB from the staged firmware window + SMU/GPU fault β†’ driver-load DoS; limited device-side disclosure of adjacent GTT pages into SMC SRAM, no userland readback primitive absent a second bug β€” same ceiling/convention as DF-4774) Severity filed: Medium Β· bucket: kernleak Β· confidence: certain

What was run

build.sh && run.sh on the DragonFly 6.5-DEVELOPMENT guest (cc 8.3 [DragonFly]), baseline + patched twin, plus two extra baseline runs for determinism. No AMD GPU is present on the guest; per house methodology (DF-4774, DF-4665, DF-3683, DF-2096) the harness executes the verbatim kernel function bodies with 1:1 struct layouts against a mock GTT/SMU-DMA model.

Baseline observations (run.log)

  • Scenario [2] β€” crafted MEC header with jt_offset=0x4000 dwords, jt_size=0x100 dwords, ucode_size_bytes=0x1000, file 0x12000 bytes, size_bytes == datasize so amdgpu_ucode_validate PASSES:
  • kernel-side OOB reads during init_bo/patch_jt: 0 bytes β€” the file is fully self-consistent, so nothing in the kernel staging path (including patch_jt's source read) ever leaves the allocation. This is what makes the finding distinct from DF-1947 (staging memcpy OOB) β€” that bug's kernel-side read window is never entered.
  • TOC entry: data_size_byte=0x10000 (65536) with image_addr pointing at the MEC1 staged window [..0x3000, ..0x5000) (0x2000 bytes).
  • Mock SMU DMA (walks the read like the SMU would, through the GPU mapping): 8192 bytes in-bounds, 57344 bytes BEYOND the staged window and the walk hits the unmapped page at +0x13000 β†’ GPU page fault / SMU hang class.
  • Marker: RESULT: SMU-SIDE OOB DMA READ CONFIRMED: 57344 bytes beyond the staged fw_buf window + unmapped-page fault class.
  • Scenario [1] β€” legit header (jt_offset=0x300): 0xc00 bytes, fully in-bounds. No false positive.
  • Scenario [3] β€” jt_offset=0x3F000000: the child dies on SIGSEGV inside the verbatim patch_jt emulation because the JT source (fw->data + array_offset + jt_offset*4 = ~4 GB past the blob) is unmapped β€” on real hardware this is the kernel-side panic through the DF-1947 site at driver attach. Included to show the field is unbounded end-to-end; the clean DF-4862 window is scenario [2], which needs no kernel-side violation at all.
  • image_size from jt_offset<<2 also wraps mod 2^32 (0x3F000000<<2 = 0xFC000000), i.e. the reported window is attacker-chosen arbitrarily.

Root cause chain (path:line)

  1. sys/dev/drm/amd/amdgpu/amdgpu_cgs.c:234-235 β€” info->image_size = le32_to_cpu(header->jt_offset) << 2; with no bound against data_size (ucode_size_bytes, line 221), the file, or the staged window.
  2. sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-259 β€” the only validation is datasize == size_bytes (DF-1946).
  3. sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:449-485 β€” the staged MEC1 window is ALIGN(ucode_size_bytes,PAGE) + ALIGN(jt_size<<2,PAGE); nothing checks the TOC-reported window against it.
  4. sys/dev/drm/amd/powerplay/smumgr/smu7_smumgr.c:305-327 β€” entry->data_size_byte = info.image_size; :405 CP_MEC entry; :424 TOC β†’ SMU DRAM; :431 PPSMC_MSG_LoadUcodes β†’ SMU DMA of data_size_byte bytes from image_addr.
  5. Reachability: gfx_v8_0.c:1204 registers ucode[CP_MEC1].fw on exactly the SMU-load asics (Topaz/Tonga/Fiji/Polaris/VegaM per amdgpu_ucode.c:290-304), driver attach β†’ powerplay start_smu β†’ smu7_request_smu_load_fw. Threat: crafted root-placed MEC firmware file (or compromised firmware source) β€” same threat model as DF-4774.

Fix validation (fix.diff)

Patched twin = every byte identical except the bound at the construction (jt_offset<<2 > ucode_size_bytes β†’ -EINVAL, plus the DF-4863 index guard). - Scenario [1] legit header: unchanged, 0xc00 in-bounds, still loads. - Scenario [2]: populate CP_MEC entry: -22 (rejected at the cgs construction β€” FIX VALIDATED) β€” no TOC entry, no SMU DMA attempted. - 3/3 runs. Full kernel rebuild not performed: no AMD GPU exists on the audit guest, so the verbatim-function harness is the faithful validation surface (same methodology as DF-4774, whose fix.diff used the identical pattern for the SMU branch).

Notes

  • hwmgr->not_vf VF info.image_size -= 20 underflow (smu7_smumgr.c:326) is a pre-existing wart noted in DF-4774 β€” not re-filed, not part of this fix.
  • The JT1/JT2 branch (amdgpu_cgs.c:223-227) is self-consistent with the staging layout (jt window ALIGN(jt_size<<2,PAGE)), no finding there.
  • CGS_UCODE_ID_GMCON_RENG one-past-end aliasing is filed separately as DF-4863 (scenario [4] of the same harness).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched-twin harness (fix.diff bound jt_offset<<2 <= ucode_size_bytes at the amdgpu_cgs.c:234-235 construction + the DF-4863 index guard; every other byte identical) rejects the crafted header with -EINVAL before any TOC entry is populated (no SMU DMA attempted), while the legit-header scenario [1] still loads 3072 bytes fully in-bounds. 3/3 deterministic runs. Full kernel rebuild not performed: no AMD GPU exists on the audit guest, so the verbatim-function harness is the faithful validation surface (same methodology as DF-4774, whose SMU-branch fix used the identical pattern).

findings/poc/DF-4862/run.log patched-twin sections: scenario [1] 'SMU DMA: 3072 bytes in-bounds, 0 bytes BEYOND' (unchanged) and scenario [2] 'populate CP_MEC entry: -22 (rejected at the cgs construction - FIX VALIDATED)'; fix.diff passes git apply --check against sys/.
↓ fix.diffper-fix-DF-4862

Confirmed kernel references

Detail

Exploit chain

crafted MEC gfx firmware file (root-placed /usr/lib/firmware/amdgpu, or compromised fw source) -> driver attach on SMU-load asics (Topaz/Tonga/Fiji/Polaris/VegaM: gfx_v8_0.c:1204 registers ucode[CP_MEC1].fw) -> amdgpu_ucode_validate passes (datasize==size_bytes only) -> amdgpu_ucode_init_bo stages ALIGN(ucode_size,PAGE)+ALIGN(jt<<2,PAGE) -> powerplay start_smu -> smu7_request_smu_load_fw populates CP_MEC TOC entry with data_size_byte = jt_offset<<2 (unbounded) -> TOC uploaded, PPSMC_MSG_LoadUcodes -> SMU DMA engine reads data_size_byte bytes from the MEC1 staged page: up to ~4 GB window; adjacent GTT pages disclosed into SMC SRAM (device-side, no userland readback absent a second bug), unmapped range -> GPU page fault / SMU hang / repeated failed loads -> system-level DoS. Ceiling: DoS + limited device-side disclosure (kernleak family convention, same as DF-4774).

Evidence (decisive lines)

findings/poc/DF-4862/run.log: '[2] ... TOC entry[0]: data_size_byte=0x10000 ... staged MEC window=[0x100003000,0x100005000)' then 'SMU DMA: 8192 bytes in-bounds, 57344 bytes BEYOND the staged window (hit UNMAPPED GPU page at +0x13000 -> GPU page fault / SMU hang)' and 'RESULT: SMU-SIDE OOB DMA READ CONFIRMED', with 'kernel-side OOB reads during init_bo/patch_jt: 0 bytes (fully in-bounds)'; '[1]' legit header 3072 bytes fully in-bounds; patched-twin '[2]' shows 'populate CP_MEC entry: -22 (rejected at the cgs construction - FIX VALIDATED)' and '[1]' unchanged. Identical markers in run.2.log and run.3.log.

PoC changes

Harness authored fresh for this finding (no prior seed): verbatim fw_type_convert + amdgpu_get_firmware_version + non-SMU branch of amdgpu_cgs_get_firmware_info + amdgpu_ucode_validate + init_single_fw/patch_jt/init_bo staging math + smu7_populate_single_firmware_entry; _Static_assert-proven struct offsets; mock GTT (staged BO + adjacent mapped page + PROT_NONE guard) walked by a mock SMU DMA engine. Patched twin generated from the same source with only the two fix.diff checks added.

Verified recommended fix

In amdgpu_cgs_get_firmware_info (amdgpu_cgs.c:234-235), bound the CP_MEC window: reject unless (uint64_t)jt_offset<<2 <= ucode_size_bytes (data_size) before assigning info->image_size; also range-check fw_type_convert's return before indexing adev->firmware.ucode[] (fix.diff).

Verdict

Harness-verified on the DragonFly 6.5-DEVELOPMENT guest using verbatim function bodies with 1:1 struct layouts (no AMD GPU on the audit guest; methodology identical to DF-4774/DF-4665/DF-3683): amdgpu_cgs_get_firmware_info's CP_MEC branch (amdgpu_cgs.c:234-235) sets info->image_size = jt_offset<<2 from an unvalidated firmware-header dword. A crafted but fully self-consistent MEC header (jt_offset=0x4000 dwords, validate PASSES) yields TOC entry data_size_byte=0x10000 against a staged window of 0x2000 -> the SMU DMA engine (PPSMC_MSG_LoadUcodes via smu7_smumgr.c:405/424/431) reads 57344 bytes beyond the staged fw_buf window (adjacent GTT pages disclosed device-side, unmapped range -> GPU page fault / SMU hang -> driver-load DoS), with ZERO kernel-side OOB reads in the staging path (distinct from DF-1947: image_size<<2 up to 0xFFFFFFFC, wraps mod 2^32). Legit header fully in-bounds (no false positive). Patched twin (bound jt_offset<<2 <= ucode_size_bytes at the construction) rejects the crafted header before any TOC entry while the legit header still loads; 3/3 deterministic runs.