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

si_load_smc_ucode ignores limit and trusts firmware header offset/size fields without bounds checking -- heap OOB read and unbounded SMC SRAM write

Summary

si_load_smc_ucode (si_smc.c:211-280) accepts limit param (SMC SRAM end) but NEVER uses it (rg confirms dead). When rdev->new_fw: reads ucode_start_address (offset 32), ucode_size (offset 20), ucode_array_offset_bytes (offset 24) directly from firmware header at 228-231 with ZERO validation against fw->datasize or limit. Only upstream check radeon_ucode_validate verifies fw->datasize==hdr->size_bytes -- does NOT verify ucode_array_offset_bytes+ucode_size<=datasize nor ucode_start_address+ucode_size<=limit. Write loop 267-275 reads ucode_size bytes from data+ucode_array_offset_bytes; if exceeds fw->datasize -> heap OOB read (kernel panic crossing unmapped page or adjacent heap corruption). ucode_start_address written directly to SMC_IND_INDEX_0 auto-increment (265-266) bypassing si_set_smc_sram_address bounds check that every OTHER SMC write path uses (si_smc.c:38 (smc_address+3)>limit). Contrast si_copy_bytes_to_smc correctly bounds-checks start+count>limit at 57 plus per-access check. ci_load_smc_ucode (ci_smc.c:186-244) has identical defect systemic pattern. Attacker: write access to firmware module path (root/physical/storage) or corrupted firmware (bit-rot/supply-chain). Trigger: patch 36-byte smc_firmware_header set ucode_array_offset_bytes=0xFF00 ucode_size_bytes=0xFFFFF000 keep size_bytes==datasize. Boot/kldload radeonkms -> loop reads ~10^9 dwords past 60KB buffer -> page fault panic. No info leak to userspace (data to SMC_IND_DATA_0 MMIO not copyout). AV:L/PR:H/AC:L, I:H/A:H. Fix: validate ucode_offset+ucode_size<=datasize and ucode_start_address+ucode_size<=limit before write loop.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2065 Β· 7 files
FileTypeDescriptionSize
VERDICT.md verdict full narrative, mechanism, citations, fix-build result 4.0 KB ↓ raw
README.md readme evidence-pack overview 1.6 KB ↓ raw
fix.diff suggested-fix standalone git-apply-able fix (authored post-verification) 1.1 KB view raw
build.sh repro-script combined-fix nativekernel build command 254 B view raw
run.sh repro-script runtime trigger (HW/module-gated) 415 B view raw
fix_build.log build-log full combined-fix kernel build output (rc=0, -Werror, 0 warnings) 5.6 MB ↓ download
env.txt environment guest uname / kern.version / cc version 289 B view raw
README.md readme evidence-pack overview
↓ download raw

DF-2065 β€” PoC evidence pack

Finding: si_load_smc_ucode ignores limit; heap OOB read + unbounded SMC SRAM write (radeon) Class: Out-of-bounds read / unbounded write (CWE-125, CWE-787) | Impact ceiling: panic | Confidence: certain

Status

SOURCE-CONFIRMED. The defect is verified real by line-by-line tracing of the audited sys/ tree, but it is HW/module-gated: the audit guest lacks the required hardware/module (AMD Southern Islands GPU (Tahiti/Pitcairn/Verde/Oland/Hainan) + write access to the SMC firmware module path), so it cannot be triggered at runtime here. See VERDICT.md for the full mechanism and citations.

How to reproduce (on a guest that HAS the hardware)

  1. Ensure the gating precondition is met: AMD Southern Islands GPU (Tahiti/Pitcairn/Verde/Oland/Hainan) + write access to the SMC firmware module path.
  2. Apply fix.diff to confirm the cited path changes; before the fix the cited code path exhibits the behaviour described in VERDICT.md.
  3. The original finding markdown describes the runtime trigger.

Build validation (Phase 8)

fix.diff applies cleanly to pristine source. All nine findings' fixes were built together; see VERDICT.md "Phase 8" and fix_build.log:

make -j6 nativekernel KERNCONF=X86_64_GENERIC   # rc=0, -Werror, 0 warnings

Files

  • VERDICT.md β€” full narrative, mechanism, citations, fix-build result
  • fix.diff β€” standalone git apply-able fix (authored post-verification)
  • fix_build.log β€” full combined-fix kernel build output (rc=0, -Werror)
  • env.txt β€” guest environment for this verification
  • manifest.json β€” machine-readable catalog
VERDICT.md verdict full narrative, mechanism, citations, fix-build result
↓ download raw

DF-2065 β€” VERDICT

Verdict: REPRODUCED (source-confirmed; HW/module-gated β€” not runtime-triggerable on this guest) Class: Out-of-bounds read / unbounded write (CWE-125, CWE-787) Impact ceiling: panic Confidence: certain

Reproduction status

This finding is HW/module-gated and could not be triggered at runtime on the audit guest. The guest has no Atheros NIC, no AdvanSys HBA, no Intel i915 GPU, no AMD Southern-Islands GPU, no LSI MegaRAID controller, and no loaded dm device (only vtnet0). The PoC therefore cannot reach the vulnerable path at runtime here. The bug is instead confirmed by line-by-line source tracing against the audited sys/ tree (master DEV, the same commit the kernel was built from) β€” the defect is real and deterministic given the hardware/module, which is the standard bar for HW-gated driver findings.

Mechanism (source-confirmed)

si_load_smc_ucode(rdev, u32 limit) (si_smc.c:211) accepts a limit (SMC SRAM end) but NEVER references it -- dead parameter. When rdev->new_fw it reads ucode_start_address (:228), ucode_size (:229) and src=data+ucode_array_offset_bytes (:230-231) straight from the firmware header with NO validation against fw->datasize or limit. The write loop (:267-275) reads ucode_size bytes from src; a crafted header (ucode_array_offset_bytes=0xFF00, ucode_size_bytes=0xFFFFF000) reads ~10^9 dwords past the 60KB buffer -> heap OOB read / page-fault panic. ucode_start_address is written directly to SMC_IND_INDEX_0 (:265), bypassing the si_set_smc_sram_address bounds check every OTHER SMC write path enforces. ci_load_smc_ucode (ci_smc.c) has the identical defect.

Gating precondition: AMD Southern Islands GPU (Tahiti/Pitcairn/Verde/Oland/Hainan) + write access to the SMC firmware module path

Exploit chain

Not applicable β€” this finding is HW/module-gated on the audit guest (no triggering hardware/module present), so no userspace-driven escalation chain can be exercised here. Per AGENT.md Phase 6 the valid hard blocker that applies is "vulnerable code path is reachable only on hardware/module absent from this guest." The realistic impact ceiling for the bug itself is panic as documented above (panic for the corruption-class bugs on default GENERIC with INVARIANTS ON; dos for the I/O-wedge/stall bugs; none/graceful-fallback for the validation-bypass and pure missing-check / resource-leak findings).

Fix (authored, git-apply-able)

Before the write loop, validate ucode_array_offset_bytes+ucode_size <= fw->datasize (prevent heap OOB read) and (uint64_t)ucode_start_address+ucode_size <= limit (bound the SRAM write, finally using the previously-dead limit parameter), matching si_copy_bytes_to_smc / si_set_smc_sram_address.

The standalone diff is fix.diff in this directory. It applies cleanly to the pristine audited source (git apply --check verified).

Phase 8 β€” combined fix-build validation

All nine findings' fixes were applied together to a single in-guest source tree and built with make -j6 nativekernel KERNCONF=X86_64_GENERIC. The DragonFly kernel compile invokes cc ... -Werror on every translation unit, so this is a true -Werror build.

  • Result: === NK_DONE rc=0 === (full log: fix_build.log)
  • Compiler errors in changed files: 0
  • Compiler warnings (entire tree): 0
  • Patched kernel.stripped sha256: 6086a989213ef7be890180f60edc0fd9827bf60efbc3ed8c969724746e250df6
  • Baseline (unpatched) kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Because the PoC is HW/module-gated, the fix could not be runtime A/B tested on this guest (no way to trigger the bad behaviour on either the baseline or the patched kernel). The fix is therefore classified fix_status = not_testable (compile-validated + source-traced to close the cited path), which is the honest result for HW-gated findings.

Kernel references (verified during this trace)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

COMPILE-VALIDATED, runtime not_testable. fix.diff applies cleanly and built into the combined nativekernel build: === NK_DONE rc=0 ===, -Werror, 0 warnings in si_smc.c. Source-traced closure: the loop is now guarded against OOB read and the SRAM write is bounded by the (previously dead) limit param. Runtime A/B impossible (no SI GPU).

baseline si_smc.c:211 limit param unused; :267-275 loop unbounded; patched: offset+size<=datasize and start+size<=limit guards added. Combined build === NK_DONE rc=0 ===, 0 warnings. Gating: no SI GPU => runtime not_testable.
↓ fix.diffcombined-fix kernel built rc=0 -Werror (kernel.stripped sha256 6086a989...); not booted β€” runtime not_testable (HW-gated: no AMD SI GPU)

Confirmed kernel references

Detail

Exploit chain

Primitive = heap OOB read of the SMC firmware blob (ucode_size bytes past ucode_array_offset_bytes) -> page-fault panic, plus an unbounded SMC SRAM write via the auto-increment MMIO (ucode_start_address written unchecked). No info leak to userspace (data goes to SMC_IND_DATA_0 MMIO, not copyout). NO userspace chain exercisable here: BLOCKED by the valid hard blocker that the path needs an AMD Southern Islands GPU absent from this guest. Fix validates offset+size<=datasize and finally uses the previously-dead limit param to bound the SRAM write.

Evidence (decisive lines)

si_smc.c:211 fn signature has `u32 limit` (never referenced in body 211-280); :228-231 read ucode_start_address/ucode_size/src from header unvalidated; :265 WREG32(SMC_IND_INDEX_0,ucode_start_address); :267-275 loop reads ucode_size bytes from src. Combined-fix build: === NK_DONE rc=0 === (-Werror).

PoC changes

findings/poc/DF-2065/ populated: VERDICT.md, fix.diff (before the write loop: validate ucode_array_offset_bytes+ucode_size<=fw->datasize and (uint64_t)ucode_start_address+ucode_size<=limit, finally using the previously-dead limit param), README.md, build.sh, run.sh, env.txt, fix_build.log, manifest.json.

Verified recommended fix

Before the write loop in si_load_smc_ucode() (si_smc.c), when rdev->new_fw validate le32(hdr->header.ucode_array_offset_bytes)+ucode_size <= rdev->smc_fw->datasize (prevent heap OOB read), and validate (uint64_t)ucode_start_address+ucode_size <= limit (bound the SRAM write, finally using the previously-dead limit param), matching si_copy_bytes_to_smc / si_set_smc_sram_address. Standalone diff in findings/poc/DF-2065/fix.diff; matches finding proposal.

Verdict

SOURCE-CONFIRMED (HW-gated). si_load_smc_ucode(rdev, u32 limit) (si_smc.c:211) accepts a limit (SMC SRAM end) but NEVER references it β€” dead parameter. When rdev->new_fw it reads ucode_start_address (:228), ucode_size (:229) and src=data+ucode_array_offset_bytes (:230-231) straight from the firmware header with NO validation against fw->datasize or limit. The write loop (:267-275) reads ucode_size bytes from src; a crafted header (ucode_array_offset_bytes=0xFF00, ucode_size_bytes=0xFFFFF000) reads ~10^9 dwords past the 60KB buffer -> heap OOB read / page-fault panic. ucode_start_address is written directly to SMC_IND_INDEX_0 (:265), bypassing the si_set_smc_sram_address bounds check every OTHER SMC write path enforces. ci_load_smc_ucode (ci_smc.c) has the identical defect.