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

Signed-compare bypass in smu7_odn_edit_dpm_table: negative sysfs level passes the bounds check and wraps to a u32 wild write index

Summary

'echo "s -1 <clk> <mV>" > pp_od_clk_voltage' parses -1 via kstrtol; the guard 'input[i] >= num_of_pl' compares signed long vs uint32_t (signed compare) so negatives pass, then 'uint32_t input_level = input[i]' wraps to 0xFFFFFFFF and indexes entries[8] of phm_odn_clock_levels: any u32 byte offset selectable (e.g. +51,539,607,540), writing range-checked-but-attacker-chosen clock/voltage values. Gated behind root sysfs AND od_enabled (cleared in the default mask) - root + non-default module param. HARNESS-VERIFIED: bounds check passes for -1; computed write offset 4294967295 * 12 = 51539607540 bytes.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3687 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
odn_negative_level.c β€” 2.7 KB view raw
build.sh β€” 112 B view raw
run.sh β€” 154 B view raw
build.log β€” 274 B view raw
run.log β€” 450 B view raw
env.txt β€” 1.1 KB view raw
fix.diff β€” 401 B view raw
verdict.json β€” 3.2 KB view raw
manifest.json β€” 980 B view raw

DF-3687 β€” Signed-compare bypass in smu7_odn_edit_dpm_table: negative sysfs level passes the bounds check and wraps to a u32 wild write index

File: sys/dev/drm/amd/powerplay/hwmgr/smu7_hwmgr.c (pass 2, GLM 5.3 re-audit) Severity: Low | CVSS: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H | CWE: CWE-197 Numeric Truncation Error; CWE-787 Out-of-bounds Write | Confidence: certain (source)

What this is

Source-certain wild-write primitive gated behind root + non-default module config; runtime-inconclusive on this guest (no AMD GPU). amdgpu_set_pp_od_clk_voltage (amdgpu_pm.c:543-611) parses 's ' triples with kstrtol, so the level may be any signed long including negatives. smu7_odn_edit_dpm_table's guard 'input[i] >= podn_dpm_table_in_backend->num_of_pl' (:4855) compares a 64-bit signed long against uint32_t num_of_pl -> the u32 converts to long -> SIGNED compare -> -1 passes. The value is then truncated into 'uint32_t input_level' (:4859) and used as the array index at :4864-4868 into entries[8] of struct phm_odn_clock_levels (hardwaremanager.h:384-396): any u32 offset is selectable (e.g. -1 -> 0xFFFFFFFF -> +51,539,607,540 bytes) and the written values (clock/vddc/vddgfx) are range-checked but attacker-chosen within the golden OD ranges. Gating: root-only sysfs (0644) AND hwmgr->od_enabled, which requires the ppfeaturemask module param to set PP_OVERDRIVE_MASK (0x4000) - the default 0xfffd3fff clears that bit, so this is off by default. Same scoring philosophy as DF-1169 (root-only). Harness reproduces the C conversion semantics and the resulting byte offset on the guest toolchain.

Reproduce (harness on the audit QEMU guest)

scp -F dfbsd-qemu/config odn_negative_level.c dfbsd-maxx:/tmp/DF-3687/
ssh -F dfbsd-qemu/config dfbsd-maxx 'cd /tmp/DF-3687 && cc -Wall -O0 -o t odn_negative_level.c && ./t'

Expected: the run prints the confirmed-primitive line (RESULT: ... CONFIRMED ...). See run.log (and run.2.log where present) for the decisive captured output, build.log for the guest toolchain transcript, env.txt for the guest environment (virtio-only, no AMD GPU β€” pciconf -l shows only 0x1237/0x1111 QEMU devices; drm.ko/amdgpu.ko exist in /boot/kernel but are not loaded and have no hardware to bind), which is why the in-kernel trigger itself is inconclusive on this guest.

Fix

fix.diff (authored against the read-only sys/ tree; never applied there).

VERDICT.md
↓ download raw

DF-3687 β€” verdict narrative

Finding

Source-certain wild-write primitive gated behind root + non-default module config; runtime-inconclusive on this guest (no AMD GPU). amdgpu_set_pp_od_clk_voltage (amdgpu_pm.c:543-611) parses 's ' triples with kstrtol, so the level may be any signed long including negatives. smu7_odn_edit_dpm_table's guard 'input[i] >= podn_dpm_table_in_backend->num_of_pl' (:4855) compares a 64-bit signed long against uint32_t num_of_pl -> the u32 converts to long -> SIGNED compare -> -1 passes. The value is then truncated into 'uint32_t input_level' (:4859) and used as the array index at :4864-4868 into entries[8] of struct phm_odn_clock_levels (hardwaremanager.h:384-396): any u32 offset is selectable (e.g. -1 -> 0xFFFFFFFF -> +51,539,607,540 bytes) and the written values (clock/vddc/vddgfx) are range-checked but attacker-chosen within the golden OD ranges. Gating: root-only sysfs (0644) AND hwmgr->od_enabled, which requires the ppfeaturemask module param to set PP_OVERDRIVE_MASK (0x4000) - the default 0xfffd3fff clears that bit, so this is off by default. Same scoring philosophy as DF-1169 (root-only). Harness reproduces the C conversion semantics and the resulting byte offset on the guest toolchain.

Root cause (kernel references)

What was verified, and how

  1. Source-level certainty. Full-file pass-2 audit of smu7_hwmgr.c (5147 lines) with the parse-side preconditions traced into processpptables.c / process_pptables_v1_0.c (VBIOS u8 counts unvalidated; offset==0 subtables parse-legal NULL) and the struct geometry verified in ppatomctrl.h / smu7_hwmgr.h / hardwaremanager.h / dm_pp_interface.h.
  2. Harness on the guest (DragonFly 6.5-DEVELOPMENT x86_64, cc base toolchain): odn_negative_level.c reproduces the exact kernel code shape and/or arithmetic with 1:1 struct layouts. Output in run.log. Compiled with -O0 β€” at -O2 the compiler detects the array-index UB and misoptimizes the accounting loops (itself a hint of what the kernel code does).
  3. Hardware absence. The guest is virtio-only: no AMD GPU on PCI, amdgpu not loaded. The in-kernel trigger requires an SMU7 ASIC (CI/Topaz/Tonga/Fiji/Polaris/ Vegam) with a crafted VBIOS, VFIO passthrough of a malicious card, or a root sysfs pp_table write. Per pass-2 spawn instructions this is recorded as inconclusive (matching DF-3585/DF-3619/DF-3644 precedent for the same class), impact none on this guest.

Fix validation

fix.diff authored post-verification against the read-only sys/ tree. Not built: a kernel rebuild with drm cannot be exercised on a GPU-less guest, so fix validation is not_testable; the diffs are minimal, local, and mechanically checkable against the cited lines.

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.diff authored against the read-only sys/ tree after source+harness verification; not built (no AMD hardware on guest).

findings/poc/DF-3687/fix.diff
↓ fix.diffper-fix-DF-3687

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 'bounds check ... (long -1 vs u32 8): PASSED (signed compare, negative passes)', 'uint32_t input_level = 0xffffffff', 'write offset ... = 4294967295 * 12 = 51539607540 bytes past entries[0]'; VERDICT.md; fix.diff (reject input[i] < 0)

PoC changes

No seed; wrote an arithmetic harness reproducing kstrtol('-1') -> signed compare -> u32 truncation -> byte offset, using the exact C semantics shared by userland and kernel.

Verified recommended fix

Reject negative levels: 'if (i + 3 > size || input[i] < 0 || input[i] >= num_of_pl) return -EINVAL;' in smu7_odn_edit_dpm_table.

Verdict

Source-certain wild-write primitive gated behind root + non-default module config; runtime-inconclusive on this guest (no AMD GPU). amdgpu_set_pp_od_clk_voltage (amdgpu_pm.c:543-611) parses 's ' triples with kstrtol, so the level may be any signed long including negatives. smu7_odn_edit_dpm_table's guard 'input[i] >= podn_dpm_table_in_backend->num_of_pl' (:4855) compares a 64-bit signed long against uint32_t num_of_pl -> the u32 converts to long -> SIGNED compare -> -1 passes. The value is then truncated into 'uint32_t input_level' (:4859) and used as the array index at :4864-4868 into entries[8] of struct phm_odn_clock_levels (hardwaremanager.h:384-396): any u32 offset is selectable (e.g. -1 -> 0xFFFFFFFF -> +51,539,607,540 bytes) and the written values (clock/vddc/vddgfx) are range-checked but attacker-chosen within the golden OD ranges. Gating: root-only sysfs (0644) AND hwmgr->od_enabled, which requires the ppfeaturemask module param to set PP_OVERDRIVE_MASK (0x4000) - the default 0xfffd3fff clears that bit, so this is off by default. Same scoring philosophy as DF-1169 (root-only). Harness reproduces the C conversion semantics and the resulting byte offset on the guest toolchain.