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

amdgpu_cgs_get_firmware_info indexes adev->firmware.ucode[fw_type_convert(type)] with no bound on the converter's default return (== the array size): one-past-end element whose .fw aliases firmware.fw_buf β€” type-confused deref of amdgpu_bo internals as struct firmware (latent: no in-tree caller passes an unmapped type)

Summary

The converter's default return is exactly AMDGPU_UCODE_ID_MAXIMUM == the array size, and the enum already ships an unhandled value - a future/garbage type reads ucode[MAXIMUM].fw which lands exactly on firmware.fw_buf (alias _Static_assert-proven and runtime-confirmed), bypasses the NULL guard, and reinterprets the amdgpu_bo as struct firmware (wild read -> info fields). Latent (all in-tree callers pass mapped types). Demonstrated in the DF-4862 harness scenario 4 (returns 0 with garbage image_size); patched twin rejects. Range-check-the-converter fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4863 Β· 13 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 β€” 100 B view raw
build.log β€” 447 B view raw
run.log β€” 3.5 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 β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 3.8 KB view raw

DF-4863 β€” adev->firmware.ucode[fw_type_convert(...)] one-past-end index on the default path

What

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

id = fw_type_convert(cgs_device, type);
ucode = &adev->firmware.ucode[id];
if (ucode->fw == NULL)              /* <-- OOB read here */
        return -EINVAL;

fw_type_convert() (amdgpu_cgs.c:111-156) returns AMDGPU_UCODE_ID_MAXIMUM (= 21) from its default case β€” exactly the declared size of adev->firmware.ucode[] (amdgpu_ucode.h:252-253), and the return value is never range-checked before indexing. Any CGS ucode id that fw_type_convert does not map β€” today that includes CGS_UCODE_ID_GMCON_RENG (cgs_common.h:58, which exists in the enum but is unhandled), CGS_UCODE_ID_MAXIMUM itself, or any future/garbage value β€” produces a one-past-the-end element:

  • ucode[MAXIMUM].fw (element offset +8) reads exactly adev->firmware.fw_buf (the struct amdgpu_bo * at struct offset 1016 β€” _Static_asserted in the harness, and proven at runtime: &ucode[MAXIMUM].fw == &firmware.fw_buf).
  • On SMU/PSP-load asics fw_buf is non-NULL after amdgpu_ucode_init_bo() (which smu7_request_smu_load_fw() invokes first thing, smu7_smumgr.c:343), so the NULL guard passes and header = (gfx_firmware_header_v1_0 *)ucode->fw->data treats struct amdgpu_bo internals as struct firmware (sys/sys/firmware.h:51, data at offset 8) and dereferences the result β€” a type-confused wild read (panic class), with garbage flowing into info->image_size / info->version / info->kptr for downstream SMU consumers.

Reachability today: latent β€” every in-tree caller (smu7/smu8/ci/iceland/vega10/vega12/vega20 smumgrs) passes only types that fw_type_convert maps. The defect is the missing bound on a converter whose default return is precisely the array size β€” a landmine for the next caller (the enum already contains the unhandled GMCON_RENG value). Filed as Low/hardening; the alias itself is proven at runtime.

Build / run / expected

Same harness as DF-4862 (sh build.sh && sh run.sh); this finding's trigger is scenario [4]. Expected baseline:

[4] fw_type_convert default -> AMDGPU_UCODE_ID_MAXIMUM one-past-end
  &firmware.ucode[MAXIMUM].fw = 0x403758  == &firmware.firmware... : ALIAS CONFIRMED
  cgs_get_firmware_info(CGS_UCODE_ID_GMCON_RENG) = 0
  type-confused header=bo+8 -> image_size=0x5a5a5a5a ... propagated to caller

Patched twin (index guard from fix.diff):

  cgs_get_firmware_info(CGS_UCODE_ID_GMCON_RENG) = -22 (one-past-end index rejected β€” FIX VALIDATED)

The harness sets firmware.fw_buf to a mapped mock whose +8 qword points into a readable page, so the verbatim code runs to completion and the garbage propagation is shown instead of crashing; on real hardware that qword is struct amdgpu_bo/ttm_buffer_object internals β€” an arbitrary pointer.

VERDICT.md
↓ download raw

DF-4863 β€” VERDICT

Status: reproduced (defect demonstrated in the verbatim-function harness, 3/3 deterministic runs) Impact: none (latent panic-class type confusion β€” no in-tree caller reaches the default path today) Severity filed: Low Β· confidence: certain (code defect) / reachability latent (documented)

What was run

The DF-4862 harness (cgs_mec_jt_oob.c), scenario [4], on the DragonFly 6.5-DEVELOPMENT guest. Struct layouts are 1:1 with amdgpu_ucode.h:236-268 and enforced at compile time:

  • _Static_assert(AMDGPU_UCODE_ID_MAXIMUM == 21)
  • _Static_assert(offsetof(struct amdgpu_firmware, fw_buf) == AMDGPU_UCODE_ID_MAXIMUM * 48 + 8) β€” i.e. ucode[21].fw (element offset +8 of the one-past-end element) lands exactly on fw_buf.

Observed (run.log, scenario [4])

  • &firmware.ucode[MAXIMUM].fw = 0x403758 == &firmware.fw_buf = 0x403758 : ALIAS CONFIRMED β€” the one-past-end read is proven to fetch the fw_buf pointer.
  • With fw_buf non-NULL (its state after amdgpu_ucode_init_bo() on SMU-load asics β€” smu7_request_smu_load_fw() calls it first thing at smu7_smumgr.c:343), the verbatim ucode->fw == NULL guard passes and the code proceeds: header = ucode->fw->data reads the mock bo's +8 qword as const uint8_t *data (sys/sys/firmware.h:53), and info.image_size=0x5a5a5a5a / info.version=0x5a5a garbage propagates to the caller. The harness points that qword at a readable page so the run completes; on real hardware it is ttm_buffer_object internals β€” an arbitrary pointer, and le32_to_cpu(header->header.ucode_size_bytes) (amdgpu_cgs.c:221) dereferences it immediately: wild read, panic class.

Root cause

sys/dev/drm/amd/amdgpu/amdgpu_cgs.c:214-215 β€” the return of fw_type_convert() (:111-156) is used as an array index without a bound; its default return (AMDGPU_UCODE_ID_MAXIMUM, :114) equals ARRAY_SIZE(adev->firmware.ucode) (amdgpu_ucode.h:252-253). The enum cgs_ucode_id already contains an unhandled value (CGS_UCODE_ID_GMCON_RENG, cgs_common.h:58).

Reachability (why Low, not higher)

All in-tree cgs_get_firmware_info callers pass mapped types: smu7 (smu7_smumgr.c:312,489,492 via smu7_convert_fw_type_to_cgs β€” all cases mapped, smu7_smumgr.c:231-276), smu8 (smu8_smumgr.c:189,569 via smu8_convert_fw_type_to_cgs β€” all firmware_list[] entries mapped), ci (ci_smumgr.c:2316 SMU), iceland (:192 SMU), vega10/12/20 (SMU). So today the path is dead code for unmapped types; the defect is a missing defensive bound whose trigger is one future caller (or one enum value gaining a user) away.

Fix validation

fix.diff (same file as DF-4862's pack) adds if (id >= AMDGPU_UCODE_ID_MAXIMUM) return -EINVAL; before the indexing. Patched twin: scenario [4] returns -22 (one-past-end index rejected β€” FIX VALIDATED); no other scenario changes. 3/3 runs.

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 adds 'if (id >= AMDGPU_UCODE_ID_MAXIMUM) return -EINVAL;' before the ucode[] indexing (fix.diff, same patch as DF-4862): the one-past-end call returns -22 before any OOB element read, while all mapped-type scenarios behave identically. Full kernel rebuild not performed (no AMD GPU on the audit guest; verbatim-function harness is the faithful surface).

findings/poc/DF-4863/run.log patched-twin scenario [4]: 'cgs_get_firmware_info(CGS_UCODE_ID_GMCON_RENG) = -22 (one-past-end index rejected - FIX VALIDATED)'.
↓ fix.diffper-fix-DF-4863

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4863/run.log scenario [4]: '&firmware.ucode[MAXIMUM].fw = 0x403758  == &firmware.fw_buf = 0x403758  : ALIAS CONFIRMED'; baseline 'cgs_get_firmware_info(CGS_UCODE_ID_GMCON_RENG) = 0' with 'type-confused header=bo+8 -> image_size=0x5a5a5a5a version=0x5a5a propagated to caller'; patched twin '-22 (one-past-end index rejected - FIX VALIDATED)'. Compile-time proof in cgs_mec_jt_oob.c (_Static_assert(offsetof(amdgpu_firmware, fw_buf) == MAXIMUM*48+8)).

PoC changes

No separate PoC authored: the defect is demonstrated by scenario [4] of the DF-4862 harness built in the same run (same verbatim cgs function, same struct replicas); this pack carries the same sources and the scenario-[4]-focused logs/verdicts.

Verified recommended fix

Range-check fw_type_convert()'s return before indexing: if (id >= AMDGPU_UCODE_ID_MAXIMUM) return -EINVAL; at amdgpu_cgs.c:214 (fix.diff).

Verdict

Harness-verified on the DragonFly 6.5-DEVELOPMENT guest (scenario [4] of the DF-4862 verbatim-function harness, 1:1 struct layouts enforced by _Static_assert): amdgpu_cgs.c:214-215 indexes adev->firmware.ucode[] with fw_type_convert()'s return without any bound check, and that helper's DEFAULT return is AMDGPU_UCODE_ID_MAXIMUM == 21 == the array size. Runtime proof: &ucode[MAXIMUM].fw == &firmware.fw_buf (ALIAS CONFIRMED) - the OOB read fetches the amdgpu_bo* fw_buf field; with fw_buf non-NULL (its state after amdgpu_ucode_init_bo, which smu7_request_smu_load_fw calls first) the NULL guard passes and the verbatim code reinterprets amdgpu_bo internals as struct firmware, dereferencing ->data (sys/sys/firmware.h:53) and propagating garbage (image_size=0x5a5a5a5a) to the caller - on real hardware a wild read, panic class. Reachability is LATENT: every in-tree caller (smu7/smu8/ci/iceland/vega10/12/20 smumgrs) passes only mapped types today; the enum already ships an unmapped value (CGS_UCODE_ID_GMCON_RENG). Patched twin rejects the one-past-end index with -EINVAL (FIX VALIDATED); 3/3 deterministic runs.