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

ucNumDPMLevels==0 pplib state leaves ps->num_levels==0: levels[num_levels-1] == levels[0xFFFFFFFF] wild read ~96 GB past the kzalloc'd sumo_ps (deterministic page-fault panic) in set_uvd_clock before/after (:847/:865), program_bsp (:350), program_at (:426), get_sclk (:1891) β€” sumo twin of DF-4145/3954/3888

Summary

num_levels is only advanced per parsed clock level; a state with ucNumDPMLevels==0 (u8, unvalidated) keeps num_levels==0 (only BOOT states get patched to 1). num_levels is u32 and levels[] is the FIRST member, so every 'last level' access levels[num_levels-1] indexes levels[0xFFFFFFFF] = object + ~96 GB. program_bsp additionally underflows its write loop to 2^32 MMIO writes and feeds garbage into the DF-1438 div0 sink. Any DPM state change selecting the crafted state panics. Harness-proven (SIGSEGV at exactly &requested_ps + 0xFFFFFFFF*24; fixed rejects). Reject-zero-level-states fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4189 Β· 10 files
FileTypeDescriptionSize
harness.c trigger-source verbatim replica; mode 4189 = zero-DPM-level state -> levels[0xFFFFFFFF] wild read -> SIGSEGV at predicted object+96GB 34.8 KB view raw
build.sh build-script cc -O2 -Wall; harness_fixed with -DFIX_ALL 65 B view raw
run.sh run-script all modes, baseline + fixed 120 B view raw
build.log build-log final successful builds (guest cc 8.3) 1018 B view raw
run.log run-log decisive run incl. fault-address prediction line 3.5 KB view raw
fix.diff suggested-fix reject power table when a state parses zero DPM levels (k==0 after the j-loop) 852 B view raw
radeon_fix_apply.log apply-log patch(1) hunks on guest /usr/src (hunk 5 = this fix) 808 B view raw
radeon_module_build.log fix-build-log radeon.ko build with all four fixes, -Werror, MAKE_RC=0 139.5 KB view raw
env.txt environment guest uname, cc, no radeon loaded 568 B view raw
VERDICT.md verdict full narrative, all six consumer sites listed 4.6 KB ↓ raw
VERDICT.md verdict full narrative, all six consumer sites listed
↓ download raw

DF-4189 β€” VERDICT

Status: REPRODUCED (logic-level harness; kernel-level repro hardware-gated) Impact: deterministic kernel page-fault panic on DPM state change (plus garbage-driven clock/voltage programming when the read happens to be mapped) Confidence: certain

Root cause

sumo_parse_pplib_clock_info() sets ps->num_levels = index + 1 β€” but it is only called from inside the j < ucNumDPMLevels loop (sumo_dpm.c:1508-1520). A pplib power state whose ATOM_PPLIB_STATE_V2.ucNumDPMLevels == 0 (pptable.h:428, u8 from the VBIOS, never validated) leaves the kzalloc'd struct sumo_ps with num_levels == 0. Only BOOT-classified states get patched up (sumo_patch_boot_state, :1395-1403, forces 1); every other state keeps 0.

struct sumo_ps (sumo_dpm.h:44-49) begins with struct sumo_pl levels[SUMO_MAX_HARDWARE_POWERLEVELS] (24-byte elements), and num_levels is u32. Every consumer that asks for the "last" level executes

ps->levels[ps->num_levels - 1]        /* == levels[0xFFFFFFFF] */

= base + 0xFFFFFFFF Γ— 24 β‰ˆ +96 GB past the allocation:

  • sumo_set_uvd_clock_before_set_eng_clock β€” sumo_dpm.c:847-848
  • sumo_set_uvd_clock_after_set_eng_clock β€” sumo_dpm.c:865-866
  • sumo_program_bsp β€” sumo_dpm.c:350 (feeds sumo_calculate_bsp, whose 65535*100/high_clk is the known DF-1438 div0 sink when the garbage is 0)
  • sumo_program_at β€” sumo_dpm.c:426-427 (r[num_levels-1]/l[num_levels-1], stack OOB below u32 r[5]/l[5])
  • sumo_dpm_get_sclk β€” sumo_dpm.c:1891 (sysfs/debugfs sclk read path)
  • sumo_program_bsp's write loop for (i = 0; i < ps->num_levels - 1; i++) (:357) underflows to 2^32 iterations β†’ MMIO write storm (hardware hang) if the load itself somehow returned.

Trigger / threat model

Crafted VBIOS with one BOOT state plus one non-BOOT state carrying ucNumDPMLevels == 0, on a Sumo/Ontario/Palm APU. radeon_dpm_pick_power_state selects the crafted state for any matching UI/thermal class; the first radeon_dpm_change_power_state_locked() (display change, thermal event, profile switch β€” regular operation) reaches sumo_dpm_pre_set_power_state β†’ sumo_update_requested_ps (working copy keeps num_levels == 0) β†’ sumo_dpm_set_power_state β†’ the wild read β†’ page-fault panic. This is the sumo twin of DF-4145 (trinity) / DF-3954 (kv) / DF-3888 (amdgpu kv).

Harness proof (mode 4189)

Two-state crafted table (state 0: ucNumDPMLevels=0, PERFORMANCE, vclk≠boot to force the UVD comparison; state 1: BOOT, 1 level — parse succeeds, boot_ps valid). After sumo_update_requested_ps + sumo_update_current_ps (the sumo_dpm_pre_set_power_state replica), the first sumo_set_uvd_clock_before_set_eng_clock call faults:

expected fault address for levels[num_levels-1].sclk ~= 0x20004e2128 (object + ~95 GB)
*** SIGNAL 11 at address 0x20004b5258 ***

0x20004b5258 βˆ’ 0xFFFFFFFFΓ—24 = 0x8004b5270 = &pi->requested_ps β€” i.e. the fault is exactly &pi->requested_ps.levels[0xFFFFFFFF].sclk, the working-copy object the kernel uses at the same call site. An unmapped-address read is deterministic: in the kernel this is a page-fault panic, not a recoverable garbage read.

(The harness faults at the uvd_before site because the load there feeds the observable sumo_setup_uvd_clocks stub; in the kernel all six sites are live. The earlier variant that "survived" uvd_before and faulted inside sumo_program_bsp instead showed the same address β€” both are the same levels[0xFFFFFFFF] load.)

Fix validation

  1. harness_fixed 4189 (-DFIX_DF4189: reject the table when a state parsed zero levels): "power state 0 has no DPM levels", parse returns -22, no crash, canary_violations = 0, exit 0.
  2. fix.diff applied with the other three sumo fixes on guest /usr/src (radeon_fix_apply.log hunk 5): radeon.ko builds under -Werror, MAKE_RC=0 (radeon_module_build.log).

Kernel references

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

harness_fixed 4189 rejects the crafted table (DRM_ERROR, -EINVAL, exit 0); combined fix builds radeon.ko under -Werror (MAKE_RC=0)

run.log fixed section; radeon_fix_apply.log hunk 5; radeon_module_build.log MAKE_RC=0
↓ fix.diffper-fix-DF-4189

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log '===== mode 4189 =====': 'expected fault address ... ~= 0x20004e2128 (object + ~95 GB)' then 'SIGNAL 11 at address 0x20004b5258' (same formula on the requested_ps copy); fixed section: 'power state 0 has no DPM levels', exit=0

PoC changes

wrote harness from scratch (no seed); note: num_levels is u32 so the index is levels[0xFFFFFFFF] (wild read +96 GB), not levels[-1] β€” verified against the kv/trinity family finding language

Verified recommended fix

reject the power table when a state parses zero DPM levels (k == 0 after the j-loop in sumo_parse_power_table), freeing everything cleanly; long-term validate ucNumDPMLevels at parse

Verdict

Reproduced at logic level with the verbatim replica (harness mode 4189): a pplib state with ucNumDPMLevels==0 leaves ps->num_levels==0 (u32), so every levels[num_levels-1] consumer indexes levels[0xFFFFFFFF] = object + 0xFFFFFFFF24 (~96 GB past the kzalloc'd sumo_ps, whose levels[] is the first member). The harness SIGSEGVs at exactly the predicted address (0x20004b5258 = &pi->requested_ps + 0xFFFFFFFF24) inside sumo_set_uvd_clock_before_set_eng_clock (:847); the kernel equivalents (:847/:865/:350/:426/:1891) page-fault deterministically on any DPM state change to the crafted state. Also feeds garbage into sumo_calculate_bsp (DF-1438 div0 sink when the garbage is 0) and a 2^32-iteration MMIO write storm in sumo_program_bsp's underflowed loop. Sumo twin of DF-4145 (trinity) / DF-3954 (kv) / DF-3888 (amdgpu kv). Kernel-level repro hardware-gated (no ATI APU on guest). Fix proven: harness_fixed rejects the table cleanly; radeon.ko builds with the fix under -Werror.