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

phm_start_thermal_controller is the only phm_* dispatcher in hardwaremanager.c without PHM_FUNC_CHECK β€” unchecked hwmgr->adev (:238), hwmgr->hwmgr_func (:240) and stores through unchecked adev (:249-250): latent kernel NULL-deref (DF-1913/DF-4831 family)

Summary

The sole unguarded wrapper among ~25 siblings - derefs hwmgr, hwmgr_func, and stores through adev with no checks. Reachability dead today (shadowed by DF-1913's earlier deref on the init path; the resume path is pm_en-gated and every chip case assigns hwmgr_func or aborts). One-line PHM_FUNC_CHECK fix. Also: the set_pp_table lead does NOT pass through this file (copies directly in amd_powerplay.c) and the vega10_hwmgr.c:4915 clamp-underflow lead logged.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4886 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
trigger_sketch.c β€” 1.0 KB view raw
fix.diff β€” 558 B view raw
verdict.json β€” 3.5 KB view raw
manifest.json β€” 1.2 KB view raw
env.txt β€” 333 B view raw

DF-4886 β€” phm_start_thermal_controller: the only phm_* dispatcher in hardwaremanager.c without PHM_FUNC_CHECK (DF-1913/DF-4831 family)

File: sys/dev/drm/amd/powerplay/hwmgr/hardwaremanager.c:234-253

What

Every public wrapper in hardwaremanager.c opens with PHM_FUNC_CHECK(hwmgr) (hardwaremanager.c:33-37: if ((hw) == NULL || (hw)->hwmgr_func == NULL) return -EINVAL;) β€” every one except phm_start_thermal_controller, which:

  1. dereferences hwmgr at :238 (struct amdgpu_device *adev = hwmgr->adev;) with no NULL test on hwmgr itself;
  2. dereferences hwmgr->hwmgr_func at :240 (if (hwmgr->hwmgr_func->get_thermal_temperature_range)) with no NULL test on hwmgr_func;
  3. writes through the unchecked adev at :249-250 (adev->pm.dpm.thermal.min_temp/max_temp = ...).

A NULL hwmgr_func (or NULL hwmgr) reaching this function is an immediate kernel NULL-pointer dereference β†’ panic (local DoS class at worst; no controllable write).

Why latent (not reachable today β€” proven)

  • Callers of phm_start_thermal_controller: hwmgr_hw_init (hwmgr/hwmgr.c:249) and hwmgr_resume (hwmgr/hwmgr.c:321).
  • On the hwmgr_hw_init path, the DF-1913 site (hwmgr.c:214-216, !hwmgr->hwmgr_func->backend_init deref) fires first for any NULL hwmgr_func β€” the wrapper's missing guard is shadowed.
  • hwmgr_resume gates only on !hwmgr || !hwmgr->pm_en (hwmgr.c:309-311), but pm_en can only be true after pp_early_init β†’ hwmgr_early_init (hwmgr.c:80-177) succeeded, and every chip-family case there assigns hwmgr->hwmgr_func via smu7/smu8/vega10/vega12/vega20/smu10 _init_function_pointers() / *_hwmgr_init() (smu7_hwmgr.c:5140, smu8_hwmgr.c:2005, vega10_hwmgr.c:4992, vega12_hwmgr.c:2422, vega20_hwmgr.c:3604, smu10_hwmgr.c:1225) β€” unknown ASICs hit a default: return -EINVAL and pp_early_init propagates it (amd_powerplay.c:85-87), aborting the probe before hw_init/resume can run.

So: same class as DF-1913 (Info) and DF-4831 (Info) β€” a latent NULL-discipline hole that only a future chip path leaving hwmgr_func NULL with pm_en true (or memory corruption) could arm. Mirror-the-guard one-line fix.

Reproduce

Not runnable on the audit guest: requires an AMD GPU driven by powerplay (guest PCI tree has no AMD device β€” see env.txt). Static finding, code-certain by inspection.

Fix

Add PHM_FUNC_CHECK(hwmgr); as the first statement of phm_start_thermal_controller (and optionally NULL-check adev).

VERDICT.md
↓ download raw

DF-4886 VERDICT

Status: untested (latent/static; the guest has no AMD GPU β€” pciconf -l shows no AMD/ATI device, env.txt) β€” same treatment as the DF-1913/DF-4831 family findings this belongs to.

What is certain (by inspection)

phm_start_thermal_controller (hardwaremanager.c:234-253) is the only public dispatcher in sys/dev/drm/amd/powerplay/hwmgr/hardwaremanager.c that does not begin with PHM_FUNC_CHECK(hwmgr) (macro at :33-37, used by all ~25 siblings). It:

  • :238 β€” derefs hwmgr->adev with hwmgr itself unchecked;
  • :240 β€” derefs hwmgr->hwmgr_func->get_thermal_temperature_range with hwmgr_func unchecked (this is the exact load that NULL-faults);
  • :249-250 β€” stores range.min/max through the unchecked adev.

Reachability trace (killed as reachable, kept as latent)

  1. Callers: hwmgr_hw_init (hwmgr.c:249), hwmgr_resume (hwmgr.c:321).
  2. hw_init path: a NULL hwmgr_func crashes earlier at the DF-1913 site (hwmgr.c:216 !hwmgr->hwmgr_func->backend_init) β€” our site is shadowed.
  3. resume path: gated only by !hwmgr || !pm_en (hwmgr.c:309-311). pm_en is set in amd_powerplay_create (amd_powerplay.c:51) and flipped false only by hw_init's failure; but reaching resume at all requires pp_early_init → hwmgr_early_init (hwmgr.c:80-177) to have succeeded, and every family case there assigns hwmgr_func (CI/CZ/VI→smu7_*, RV→ smu10, AI→vega10/12/20) or returns -EINVAL, which pp_early_init propagates (amd_powerplay.c:85-87), aborting the probe.
  4. Conclusion: no in-tree path delivers NULL hwmgr_func to this wrapper. The hole arms only for a future chip family added to hwmgr_early_init without an initializer, or under prior memory corruption. Hence Info / latent hardening, mirroring how DF-1913 and DF-4831 were graded.

Impact ceiling

NULL-deref panic (local DoS) β€” the out-of-range writes at :249-250 are to a fixed offset of adev->pm.dpm.thermal with fixed kernel-produced values (range = backend-filled {0..80000}); no attacker-controlled write.

Fix

One line: PHM_FUNC_CHECK(hwmgr); at the top of the function (plus, if desired, if (!adev) return -EINVAL; after :238). A git-apply-able fix.diff is included for completeness even though no runtime validation was possible.

Negative results recorded while in-file (pass-2 depth)

  • phm_get_clock_info :382 hardwareActivityPerformanceLevels - 1 index underflow β€” killed: every backend assigns a nonzero constant ({SMU7,SMU8,SMU10,VEGA10,VEGA12,VEGA20}_MAX_HARDWARE_POWERLEVELS) during backend_init (smu7_hwmgr.c:2576, smu8_hwmgr.c:1107, smu10_hwmgr.c:503, vega10_hwmgr.c:883, vega12_hwmgr.c:404, vega20_hwmgr.c:414) and all amd_powerplay.c callers gate on pm_en (which implies backend_init ran). Cross-file lead, not this file's bug: vega10_get_performance_level (vega10_hwmgr.c:4915-4917) clamps with index > count-1 ? count-1 : index β€” with performance_level_count==0 the clamp underflows to 0xFFFFFFFF and passes any index through; index is still ≀7 from this wrapper, so at most an uninitialized-array read. Filed for the vega10_hwmgr.c owner, not here.
  • set_pp_table lead (amd_powerplay.c:693 memcpy(hardcode_pp_table, buf, size)): hardwaremanager.c does not participate β€” pp_dpm_set_pp_table copies the table directly in amd_powerplay.c and only afterwards calls hwmgr_hw_fini/init + hwmgr_handle_task; no hardwaremanager wrapper takes a size or pointer to the pp table (verified: the only phm_* on that path are phm_setup_asic/phm_enable_dsm/phm_start_thermal_controller β€” all no-arg table-free dispatches).
  • Unchecked pass-through params (clock, clocks, clock_ranges, pstate1/2, level, equal) in the other wrappers β€” killed: all callers are amd_powerplay.c pp_* functions that NULL-check inputs before calling (e.g. pp_display_clock_voltage_request amd_powerplay.c:1173-1181 checks !clock; pp_set_watermarks_for_clocks_ranges checks !clock_ranges), and the objects are kernel-internal dpm structs, never user pointers.
  • Thermal range capping (:237-250): range defaults {0, 80000}; only the backend can widen it; values land in adev->pm.dpm.thermal for display only. No user-controlled source. Killed.
  • u32 kHz clock values (min_dcef_deep_sleep_set_clk, display clock requests): kernel-computed by the DC layer, pass-through only; wrap arithmetic in backends is a backend concern (vega10_* audited clean on these). Killed.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix validation not possible: latent hardware-gated finding (no AMD GPU on the audit guest); fix is a trivial one-line guard mirroring every sibling wrapper - static review only

fix.diff (git-apply-able vs read-only sys/ tree; never applied)
↓ fix.diffper-fix-DF-4886

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (guard-vs-sibling analysis with path:line); VERDICT.md (reachability trace + pass-2 negative results incl. the set_pp_table non-participation proof and the vega10_get_performance_level count==0 cross-file lead); trigger_sketch.c; fix.diff

PoC changes

sketch only; kernel-internal NULL-discipline hole, no userspace trigger exists

Verified recommended fix

add PHM_FUNC_CHECK(hwmgr) as the first statement of phm_start_thermal_controller (one line, matches all sibling wrappers)

Verdict

Not runnable on the audit guest (no AMD GPU / powerplay device; PCI tree contains no AMD device). Code-certain by inspection: phm_start_thermal_controller (hardwaremanager.c:234-253) is the ONLY public phm_* dispatcher in hardwaremanager.c that omits PHM_FUNC_CHECK (macro at :33-37, used by all ~25 siblings) - it derefs hwmgr->adev at :238 with hwmgr unchecked, derefs hwmgr->hwmgr_func->get_thermal_temperature_range at :240 with hwmgr_func unchecked, and stores through the unchecked adev at :249-250. Latent, DF-1913/DF-4831 family: the hw_init caller (hwmgr.c:249) is shadowed by the DF-1913 deref at hwmgr.c:216 which fires first on any NULL hwmgr_func; the resume caller (hwmgr.c:321) is gated on pm_en, which requires hwmgr_early_init (hwmgr.c:80-177) to have succeeded - and every chip-family case there assigns hwmgr_func (smu7_hwmgr.c:5140, smu8_hwmgr.c:2005, vega10_hwmgr.c:4992, vega12_hwmgr.c:2422, vega20_hwmgr.c:3604, smu10_hwmgr.c:1225) or returns -EINVAL which pp_early_init propagates (amd_powerplay.c:85-87), aborting the probe. Impact ceiling is a NULL-deref panic (no attacker-controlled write); arms only for a future chip path leaving hwmgr_func NULL with pm_en true. One-line mirror-the-guard fix.