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

count==0 underflow: ptable->entries[ptable->count - 1] reads index 0xFFFFFFFF in smu8_dpm_update_vce_dpm / smu8_dpm_update_uvd_dpm (smu10_hwmgr.c:851 twin)

Summary

Both update paths index entries[count-1] with count the raw u8 numEntries from the VBIOS extension table. count==0 passes the only load-time guard (<= SMU8_MAX_HARDWARE_POWERLEVELS - 0 <= 8), init completes, and the first VCE/UVD ungate under StablePState or en_umd_pstate (user-reachable via the power_dpm_force_performance_level profile_* sysfs) computes entries[0xFFFFFFFF] - a wild kernel read ~16GB from the table base -> fault -> panic. The init-time helper rejects count<=0 but these runtime paths never re-check. smu8's own instance of the smu10:851 underflow already flagged. Zero-count guard fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4171 Β· 21 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 6.6 KB ↓ raw
env.txt β€” 1.3 KB view raw
fix.diff β€” 1.2 KB view raw
build.log β€” 2.7 KB view raw
run.log β€” 10.4 KB view raw
run.2.log β€” 10.4 KB view raw
run.3.log β€” 10.4 KB view raw
verdict.json β€” 7.0 KB view raw
manifest.json β€” 2.3 KB view raw
rig/gen_rig.sh β€” 7.2 KB view raw
rig/check_verbatim.sh β€” 5.5 KB view raw
rig/rig_shims.h β€” 7.0 KB view raw
rig/smu8_rig.c β€” 23.7 KB view raw
rig/Makefile β€” 448 B ↓ download
rig/build.sh β€” 509 B view raw
rig/run.sh β€” 1.2 KB view raw
rig/smu8_slice.inc β€” 9.3 KB ↓ download
rig/smu8_slice_fixed.inc β€” 9.2 KB ↓ download
rig/smu8_slice_verbatim.h β€” 36.7 KB view raw
rig/rig_fix.diff β€” 1.1 KB view raw

DF-4171 β€” count==0 underflow: entries[count-1] reads before the table in smu8_dpm_update_vce_dpm / smu8_dpm_update_uvd_dpm

  • File: sys/dev/drm/amd/powerplay/hwmgr/smu8_hwmgr.c sites: smu8_dpm_update_vce_dpm :1251-1278 (read at :1260-1261), smu8_dpm_update_uvd_dpm :1846-1874 (read at :1856-1857)
  • count source: raw u8 numEntries from the VBIOS powerplay extension table (processpptables.c get_vce/uvd_clock_voltage_limit_table :1114-1141); the only load-time guard is count <= SMU8_MAX_HARDWARE_POWERLEVELS (smu8_hwmgr.c:471-474), which accepts 0.
  • Triggers: PP_CAP(PHM_PlatformCaps_StablePState) OR hwmgr->en_umd_pstate (user-reachable: power_dpm_force_performance_level profile_* sysfs β†’ pp_dpm_en_umd_pstate, amd_powerplay.c:310-335) on the first VCE/UVD DPM update after ungate.
  • Class: kernel heap OOB read, CWE-125.

Verification (2026-10-15, verbatim-rig evidence class)

REPRODUCED in byte-identical production code on the DragonFly guest (no AMD GPU β€” the in-kernel path is ASIC-gated; see VERDICT.md Β§1), with a MECHANISM CORRECTION: count is uint8_t, so count - 1 promotes to int βˆ’1 and the access is entries[-1] at tableβˆ’8 (entries at +4, record 12 B) β€” NOT index 0xFFFFFFFF / ~16 GB. The rig prints the predicted address before each run and the fault address matches exactly. In-kernel this is a silent 8-byte heap under-read feeding hard_min_clk/SMU messages, not a panic (the finding's availability impact is overstated; see VERDICT.md Β§3).

cd rig && sh build.sh /usr/src    # 26 verbatim blocks checker-proven, builds rig_base + rig_fixed
sh run.sh                         # baseline + fixed (this pack's fix.diff) over 11 modes

Expected (see run.log):

  • baseline 4171-vce-count0 (StablePState cap) and 4171-uvd-count0-en-umd (en_umd_pstate, the sysfs route): SIGSEGV READ classified "8 bytes BEFORE the start of the 8B table object: NEGATIVE-INDEX UNDERFLOW READ", at exactly the predicted tableβˆ’8;
  • fixed build: both modes return rc=βˆ’EINVAL, no fault; clean-valid byte-identical to baseline; 4169-* modes still fault under this fix (orthogonal finding DF-4169).

Logs: build.log, run.log, run.2.log, run.3.log (three byte-identical runs). Full narrative: VERDICT.md. fix.diff authored against the read-only sys/ tree, A/B validated in the rig, never applied to sys/.

VERDICT.md
↓ download raw

DF-4171 VERDICT (verify run, 2026-10-15)

Status: REPRODUCED with a MECHANISM CORRECTION (verbatim-rig evidence class). The count==0 out-of-bounds read in both smu8_dpm_update_vce_dpm and smu8_dpm_update_uvd_dpm was demonstrated in byte-identical production code at the exact computed address β€” but it is an entries[-1] NEGATIVE-INDEX read 8 bytes BEFORE the table object (uint8_t count promotes to int βˆ’1), NOT the filed "index 0xFFFFFFFF / ~16 GB wild read / panic". The load-time guard passthrough (0 ≀ 8) and both user-reachable triggers (StablePState cap; en_umd_pstate via the power_dpm_force_performance_level profile_* sysfs) are confirmed in-tree. The pack's fix.diff was A/B-validated: fixed. In-kernel execution is AMD-ASIC-gated on this guest (no AMD GPU β€” proven, env.txt).

1. Method

Same rig as DF-4169 (same run, one guest): the cited production functions extracted byte-identical from smu8_hwmgr.c (:1251-1278 smu8_dpm_update_vce_dpm, :1846-1874 smu8_dpm_update_uvd_dpm + helpers :49-97/:99-158/:248-252 and the DF-4169 cast/level sites), 26 blocks checker-verified on the guest against /usr/src/sys (build.log: ALL VERBATIM CHECKS PASSED). The VCE/UVD tables are built exactly as get_vce_clock_voltage_limit_table / get_uvd_clock_voltage_limit_table build them (processpptables.c:1114-1141: table_size = sizeof(unsigned long) + sizeof(table) * numEntries, count = raw numEntries u8) and allocated page-aligned right after a PROT_NONE page, so the first byte BEFORE the object faults: the underflow detector.

2. Observed (run.log; byte-identical in run.2.log/run.3.log, md5-equal)

mode observation verbatim site
4171-vce-count0 count=0 table (0 passes the only load-time guard count <= SMU8_MAX_HARDWARE_POWERLEVELS, :473), StablePState cap set β†’ smu8_dpm_update_vce_dpm β†’ SIGSEGV READ at tableβˆ’8 β€” predicted address printed beforehand and matched exactly (entries at +4, record 12 B β‡’ entries[-1].ecclk at baseβˆ’8) :1260-1261
4171-uvd-count0-en-umd count=0 table, no StablePState cap, hwmgr->en_umd_pstate = true (the user sysfs route: power_dpm_force_performance_level β†’ pp_dpm_en_umd_pstate, amd_powerplay.c:310-335) β†’ smu8_dpm_update_uvd_dpm(hwmgr, false) β†’ SIGSEGV READ at tableβˆ’8 :1856-1857
clean-valid count=2 tables, stable+umd paths: hard_min_clk = entries[1].ecclk/vclk exactly, SMU msgs [0x20,0x1]/[0x1c,0x1], no fault :1260/:1856

Fixed (this pack's fix.diff, identical inputs): both modes return rc=βˆ’EINVAL without faulting (if (!ptable || ptable->count == 0) return -EINVAL; before both entries reads); clean-valid byte-identical; the DF-4169 modes still fault under this fix (orthogonality β€” see DF-4169's pack, whose FIXED section conversely leaves these two modes faulting). Fix A/B validated.

3. Mechanism correction (why "0xFFFFFFFF" is wrong here)

ptable->count is uint8_t (struct phm_vce_clock_voltage_dependency_ table, hwmgr.h:183-186; same for uvd, hwmgr.h:137-141). In ptable->entries[ptable->count - 1] the usual arithmetic promotions apply: (int)count βˆ’ 1 with count==0 is int βˆ’1, so the access is entries[-1] β€” 8 bytes BEFORE the allocation (entries at offset 4, record size 12). The ~16 GB/0xFFFFFFFF arithmetic would require a 32-bit count type (that is the smu10_hwmgr.c:851 twin's shape). The rig proves the actual address: predicted tableβˆ’8 == faulting address, both modes.

Consequences of the correction:

  • No panic in-kernel. The read lands inside the preceding slab page (kmalloc'd objects are surrounded by mapped kernel heap), so the realistic in-kernel behavior is a silent 8-byte heap under-read, not a fault. The finding's CVSS availability impact is overstated.
  • What the bytes feed: the read value becomes data->vce_dpm.hard_min_clk / data->uvd_dpm.hard_min_clk and is then passed to the SMU (SetEclkHardMin/SetUvdHardMin via the level helpers) β€” wrong-clock programming from heap garbage, i.e. an integrity/robust- ness defect in the DPM engine. No path returning the raw value to userland was found (the sysfs exposes level indices computed by the level helpers, not the clock), so a confidentiality leak is NOT demonstrated.

The security-relevant core of the finding β€” an unvalidated firmware u8 count that passes the only load-time check and indexes before the allocation at runtime, on a user-reachable trigger β€” is confirmed verbatim. The bucket classification (kernleak/CWE-125 OOB read) stands; the impact narrative (wild read/panic) is corrected to bounded negative-index OOB read.

4. Fix validation

rig_fixed = same slice + this pack's fix.diff (header-normalized apply, bidirectional patch proof, hunk-body cmp-identity β€” see DF-4169 pack; both packs' fixed builds were built on the guest in the same session). Both 4171 modes guarded (rc=βˆ’22, no fault), clean-valid byte-identical, 4169 modes unaffected (orthogonal). Rebuilt-kernel validation not performed (AMD-ASIC-gated on this guest); fix.diff never applied to the read-only sys/ tree; guest /usr/src pristine before and after (md5 a0cb59b723434a78e88bf0580d5a5508).

5. Honest limitations

  • Demonstrated in the rig (guard page before the object makes the underflow fault); in-kernel it would not fault β€” no in-kernel manifestation was observable on this guest (no AMD GPU), so the in-kernel impact statement rests on the promotion analysis + exact address match, not on a guest panic.
  • The count==0 table is constructed directly (production table-builder's output for numEntries==0), not by parsing a full crafted VBIOS.
  • The :454 hunk of this pack's fix.diff (upload-path NULL-table guard, DF-4170 residual) is outside the sliced functions and was not exercised by this rig.

6. Bottom line

The OOB read is real, reproduced in byte-identical production code at the exact computed address, on both cited sites, under both cited triggers, and the pack's zero-count guard eliminates it on identical input without changing valid-table behavior. The filed mechanism (0xFFFFFFFF index, ~16 GB, panic) is corrected to entries[-1] (uint8_t→int promotion); realistic impact is a bounded kernel-heap under-read feeding SMU clock parameters, not a panic.

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

A/B validated in the same rig: rig_fixed = same verbatim slice + this pack's fix.diff (forward apply --fuzz=0 no rejects, reverse apply restores pristine byte-exactly, hunk bodies cmp-identical). On identical inputs both 4171 modes return rc=-EINVAL with no fault/no underflow read; clean-valid output byte-identical to baseline (hard_min_clk entries[1].ecclk=300000/vclk=133300, SMU msgs [0x20,0x1]/[0x1c,0x1]); the DF-4169 modes still fault under this fix, proving orthogonality. Rebuilt-kernel validation not performed: AMD-ASIC-gated on this guest (verbatim-rig protocol).

run.log FIXED sections: '4171-vce-count0 rc=-22 / returned WITHOUT faulting (guard active in fixed build)' and same for '4171-uvd-count0-en-umd'; clean-valid lines identical to baseline; build.log: 'ALL VERBATIM CHECKS PASSED' incl. fixed-slice == pristine+fix.diff and reverse-apply restore proof
↓ fix.diffper-fix-DF-4171

Confirmed kernel references

Detail

Exploit chain

crafted/corrupted VBIOS (or root-written pp_table blob) on a Carrizo/Bristol Ridge SMU8 APU ships a VCE or UVD clock/voltage limit table with numEntries==0 -> processpptables get_vce/uvd_clock_voltage_limit_table allocates the table with count=0 (the :471-474 load-time guard accepts 0) -> user writes profile_standard/profile_peak/... to power_dpm_force_performance_level -> pp_dpm_en_umd_pstate sets en_umd_pstate -> first VCE/UVD DPM update after ungate computes entries[count-1] = entries[-1] -> 8-byte kernel-heap read before the table object -> garbage hard_min_clk programmed to the SMU. Bounded negative-index OOB read; no demonstrated userland exfiltration, no in-kernel fault (mechanism corrected from the filed 0xFFFFFFFF/panic claim).

Evidence (decisive lines)

findings/poc/DF-4171/run.log: '[BASELINE] MODE 4171-vce-count0 -> :1261 reads entries[count-1].ecclk = entries[-1] at 0x800475ff8 (table-8 ...) -> !! SIGSEGV (addr=0x800475ff8, READ) => fault 8 bytes BEFORE the start of the 8B table object: NEGATIVE-INDEX UNDERFLOW READ'; same for '4171-uvd-count0-en-umd' at :1857; layout mode prints the promotion arithmetic ('uint8_t -> int -1 ... table - 8') and the guard passthrough ('count=0 <= 8 -> PASSES'); FIXED sections show rc=-22/no fault for both modes; build.log: 26 verbatim blocks ALL PASSED on the guest against /usr/src; run.2/run.3.log md5-identical

PoC changes

Seed pack had no runnable PoC. Built the verbatim rig per the DF-4388 pattern (same rig as DF-4169, run in the same guest session): gen_rig.sh/check_verbatim.sh extract and guest-verify 26 byte-identical blocks; tables are allocated with the production formula and page-aligned after a PROT_NONE leading-guard page so the underflow faults at the exact predicted address; modes added for both update paths and both triggers (StablePState cap; en_umd_pstate). Fixed build applies this pack's fix.diff (header-normalized, hunk bodies cmp-identical, bidirectional patch proof).

Verified recommended fix

Zero-count guard before both entries[count-1] reads: apply fix.diff (if (!ptable || ptable->count == 0) return -EINVAL; in smu8_dpm_update_vce_dpm :1258-1259 and smu8_dpm_update_uvd_dpm :1854-1855; the !ptable half also covers DF-4170's runtime residual).

Verdict

The count==0 out-of-bounds read was demonstrated in byte-identical production smu8_hwmgr.c code on the DragonFly guest (verbatim-rig evidence class; in-kernel execution is AMD-ASIC-gated, no AMD GPU in the guest), on both cited sites and under both cited triggers -- but with a MECHANISM CORRECTION that changes the impact ceiling. ptable->count is uint8_t (hwmgr.h:137-141 uvd / :183-186 vce), so 'ptable->entries[ptable->count - 1]' with count==0 promotes to int -1: the access is entries[-1] at exactly table-8 (entries at offset 4, record 12B), NOT index 0xFFFFFFFF at ~16GB. The rig prints the predicted address before the call and the fault lands on it exactly (SIGSEGV READ '8 bytes BEFORE the start of the 8B table object: NEGATIVE-INDEX UNDERFLOW READ'), for smu8_dpm_update_vce_dpm under the StablePState cap (:1260-1261) and for smu8_dpm_update_uvd_dpm under en_umd_pstate with no cap (:1856-1857) -- the user-reachable route (power_dpm_force_performance_level profile_* sysfs -> pp_dpm_en_umd_pstate, amd_powerplay.c:310-335, confirmed in-tree). count==0 passing the only load-time guard (count <= SMU8_MAX_HARDWARE_POWERLEVELS, :471-474) is shown arithmetically. Consequences of the correction: in-kernel the read lands in the preceding slab page, so it does NOT fault/panic (the finding's availability impact is overstated); the realistic manifestation is a silent 8-byte kernel-heap under-read whose value becomes vce_dpm/uvd_dpm.hard_min_clk and is programmed into the SMU as a hard-min clock (wrong-clock integrity defect). No userland readback of the raw value was found, so no confidentiality leak is demonstrated either -- the CWE-125 OOB-read class itself is confirmed verbatim. The pack's fix.diff was A/B-validated in the same rig: both modes return -EINVAL with no fault, clean-valid byte-identical, and the DF-4169 modes keep faulting under it (orthogonality). Three runs byte-identical.