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

vega20_odn_edit_dpm_table VDDC_CURVE: one-sided signed bounds check (input_index > 2) lets negative user index through -> OOB read of od8_settings_array with values disclosed via pr_info (DF-3704 twin)

Summary

In the PP_OD_EDIT_VDDC_CURVE branch, the check is only input_index > 2 - every negative user token survives; od8_id goes negative and od8_settings[od8_id].min/.max_value are read from before the array embedded in the kzalloc'd backend, then both OOB-read values are printed by the pr_info range message - a kernel-heap disclosure to dmesg; attacker-chosen magnitudes (int32 token) hit unmapped kernel addresses -> page fault/panic in the sysfs write path. Unlike the vega10 twin (DF-3704 wild WRITE) this variant is read+leak. VERIFIED on guest: verbatim logic on a heap stand-in leaked exactly the marker bytes at od8_id=-2 and -12 while the in-bounds control passed. Root-gated pp_od_clk_voltage whose token parser accepts negative tokens; management agents relaying user-controlled overdrive strings are the realistic writer.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3859 Β· 10 files
FileTypeDescriptionSize
README.md β€” 4.0 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
odn_negidx.c β€” 4.7 KB view raw
build.sh β€” 121 B view raw
run.sh β€” 170 B view raw
run.log β€” 808 B view raw
env.txt β€” 467 B view raw
fix.diff β€” 403 B view raw
verdict.json β€” 4.3 KB view raw
manifest.json β€” 1.3 KB view raw

DF-3859 β€” vega20_odn_edit_dpm_table PP_OD_EDIT_VDDC_CURVE: signed-compare lets negative user index through β†’ OOB read of od8_settings_array (DF-3704 twin)

What

sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2656-2720, VDDC-curve branch of the OD sysfs editor:

int32_t input_index, input_clk, input_vol, i;    /* :2560 */
...
input_index = input[i];                          /* :2674 β€” long from kstrtol */
input_clk   = input[i + 1];
input_vol   = input[i + 2];

if (input_index > 2) { ... return -EINVAL; }     /* :2678 β€” SIGNED compare:
                                                    only the UPPER bound is
                    checked β€” negatives pass! */
od8_id = OD8_SETTING_GFXCLK_FREQ1 + 2 * input_index;      /* :2685 β†’ < 0 */
if (input_clk < od8_settings[od8_id].min_value ||          /* :2686 β€” OOB read */
    input_clk > od8_settings[od8_id].max_value) {          /* :2687 β€” OOB read */
        pr_info("clock freq %d is not within allowed range [%d - %d]\n",
                input_clk,
                od8_settings[od8_id].min_value,            /* leaked kernel bytes */
                od8_settings[od8_id].max_value);           /* leaked kernel bytes */
        return -EINVAL;
}
od8_id = OD8_SETTING_GFXCLK_VOLTAGE1 + 2 * input_index;    /* :2695 β€” again */

od8_settings is od8_settings_array[OD8_SETTING_COUNT=16] (vega20_hwmgr.h:429), each element 20 bytes (vega20_od8_single_setting, vega20_hwmgr.h:418-424), embedded in the kzalloc'd struct vega20_hwmgr backend (:383).

  • input_index = -1 β†’ od8_id = 0 (FMIN slot) β€” wrong setting, in-bounds.
  • input_index ≀ -2 β†’ od8_id < 0: small negatives read the vega20_hwmgr fields preceding the array (odn_data etc.) β€” and both garbage values are printed to the log by the pr_info range message (kernel-heap disclosure to dmesg).
  • Large negatives (attacker-chosen magnitude, e.g. -100000000 β†’ od8_id β‰ˆ -2Γ—10⁸ β†’ β‰ˆ -4 GB offset) read far outside the backend allocation β†’ kernel page fault β†’ panic.

The final switch (input_index) (:2705) only writes for 0/1/2, so unlike the vega10 twin (DF-3704, a wild write) this variant is a wild read + log leak. The SCLK (:2585) and MCLK (:2633) branches correctly reject non-0/1 and non-1 indices, so only vc is affected.

Reach (root-gated)

/sys/class/drm/card0/device/pp_od_clk_voltage is S_IWUSR (amdgpu_pm.c:1028); the parser kstrtol()s every whitespace-separated token into long parameter[...] with no sign restriction (amdgpu_pm.c:575-592), so the token stream vc -2 1000 900 delivers input_index = -2 verbatim:

echo "vc -2 1000 900" > /sys/class/drm/card0/device/pp_od_clk_voltage

dmesg then shows clock freq 1000 is not within allowed range [<heap bytes> - <heap bytes>]. A confused-deputy writer (root-owned service/agent that passes through user-controlled OD strings) escalates to kernel-heap info leak or panic.

Reproduce (arithmetic-level; kernel reach hardware-gated)

build.sh                        # cc -O2 -Wall -o /tmp/odn_negidx /tmp/odn_negidx.c
run.sh                          # control idx=1, attacks idx=-2 and idx=-7

Expected (observed on the audit guest, run.log):

attack (vc -2 1000):
  input_index=-2 -> od8_id=-2 (byte offset -40 from array)
  clock freq 1000 is not within allowed range [-1515870811 - -1515870811]
  ^--- OOB-read kernel bytes printed to the log        (0xA5A5A5A5 marker bytes)

attack (vc -7 1000):
  input_index=-7 -> od8_id=-12 (byte offset -240 from array)

The control (vc 1 1000, od8_id=4, in-bounds) passes the range check normally. -1515870811 is the exact int32 of the marker bytes placed at the OOB location β€” demonstrating that whatever memory precedes the array is disclosed via the error message, precisely as the pr_info does in-kernel.

Impact

Root-gated (S_IWUSR) OOB read: small leaks of vega20_hwmgr backend heap contents into the kernel log; large magnitudes page-fault (panic). Confused deputy / management-agent exposure; not directly unprivileged.

VERDICT.md
↓ download raw

DF-3859 VERDICT β€” vega20_odn_edit_dpm_table negative-index OOB read (DF-3704 twin)

Status: not_reproduced (kernel reach hardware-gated) β€” index arithmetic and OOB read-with-leak reproduced verbatim on the guest heap stand-in.

Root cause (path:line, sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c

unless noted)

vega20_odn_edit_dpm_table() :2549, branch PP_OD_EDIT_VDDC_CURVE (:2656-2720):

  1. :2560 declares int32_t input_index, input_clk, input_vol;; :2567+ input is the caller's long * array of kstrtol-parsed user tokens.
  2. :2674 input_index = input[i]; β€” a user-controlled signed value.
  3. :2678 if (input_index > 2) β€” only an upper-bound check; every negative value passes. (Compare the SCLK branch :2585 input_index != 0 && input_index != 1 and the MCLK branch :2633 input_index != 1, which are exact-match and safe.)
  4. :2685 od8_id = OD8_SETTING_GFXCLK_FREQ1 + 2 * input_index; β€” for input_index ≀ -2 this is negative; od8_settings[od8_id] at :2686-2687 reads od8_settings_array[od8_id] before the 16-element array (vega20_hwmgr.h:400-429, element size 20, embedded in the kzalloc'd struct vega20_hwmgr backend, vega20_hwmgr.c:383).
  5. On the (almost certain) range-check failure, :2688-2692 pr_infos od8_settings[od8_id].min_value/.max_value β€” the OOB-read kernel heap bytes land in the log. :2695-2703 repeats the identical negative index for the voltage slots.
  6. :2705-2718 the write switch (input_index) matches only 0/1/2, so no OOB write occurs (this is the read-only sibling of vega10's DF-3704 wild write).

Offsets: input_index=-2 β†’ od8_id=-2 β†’ -40 bytes (reads vega20_hwmgr.odn_data/disable_dpm_mask region); input_index=-7 β†’ -240 bytes; the user token is an arbitrary int32, e.g. vc -100000000 1000 900 β†’ od8_id = -199999998 β†’ β‰ˆ -3.7 GB offset β†’ unmapped kernel address β†’ page fault β†’ panic in the sysfs write path.

Reach

pp_od_clk_voltage (amdgpu_pm.c:1028, S_IRUGO | S_IWUSR) β†’ amdgpu_set_pp_od_clk_voltage β†’ token parser amdgpu_pm.c:575-592 (kstrtol accepts -2; PP_OD_EDIT_VDDC_CURVE selected by the "vc" prefix at :566) β†’ amdgpu_dpm_odn_edit_dpm_table (:594-596) β†’ vega20_odn_edit_dpm_table. Requires root (or a confused deputy that relays user OD strings to the sysfs node) on a Vega20 with OD8 curve support. hwmgr->pm_en gate at pp_dpm_odn_edit_dpm_table (amd_powerplay.c) passes like DF-3858's analysis.

What ran on the Guest

odn_negidx.c replicates the enum, the 20-byte vega20_od8_single_setting, the array embedded after 512 marker ("adjacent heap") bytes inside a heap allocation, and the verbatim :2685-2693 index/range/pr_info logic.

Observed (run.log, guest cc 8.3 -O2): - control vc 1 1000 β†’ od8_id=4 (+80 bytes, in-bounds), check passes. - attack vc -2 1000 β†’ od8_id=-2 (-40 bytes) β†’ error message prints [-1515870811 - -1515870811] = 0xA5A5A5A5 β€” exactly the marker bytes read out of bounds and disclosed in the message, the same information flow as the kernel pr_info. - attack vc -7 1000 β†’ od8_id=-12 (-240 bytes), same leak.

The guest kernel path itself cannot run (0 AMD PCI devices, no /dev/dri, env.txt); the wild read/leak semantics are nevertheless fully demonstrated on the identical index arithmetic and access expression.

Fix validation

fix.diff rejects negative indices explicitly in the VDDC_CURVE branch (input_index < 0 || input_index > 2, matching upstream's later fix style for this handler). git-apply-clean against the read-only sys/ tree; no Vega20 ASIC on this guest, so a patched-kernel run is not_testable.

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 adds the lower-bound check (one-line, matches the SCLK/MCLK branches' reject style) and is git-apply-clean against the read-only sys/ tree; no Vega20 ASIC on this guest so a patched-kernel run is not_testable

fix.diff; VERDICT.md; env.txt
↓ fix.diffper-fix-DF-3859

Confirmed kernel references

Detail

Exploit chain

root or confused-deputy writer on a Vega20 with OD8 GFXCLK-curve support: echo "vc -N 1000 900" > /sys/class/drm/card0/device/pp_od_clk_voltage -> amdgpu_pm.c parser (kstrtol accepts -N) -> vega20_odn_edit_dpm_table PP_OD_EDIT_VDDC_CURVE -> od8_settings[2 + 2*(-N)] OOB read -> pr_info discloses the OOB bytes (small N: vega20_hwmgr backend internals; large N: unmapped kernel address -> page fault/panic).

Evidence (decisive lines)

run.log (control in-bounds; attacks at -2/-7 leaking marker bytes 0xA5A5A5A5=-1515870811 in the range message); VERDICT.md (full trace incl. safe SCLK/MCLK branch comparison and why no write occurs); fix.diff (git-apply-clean); env.txt (no AMD ASIC)

PoC changes

Authored from scratch. One build fix: missing for EINVAL in the replica. First guest build ran before the chained && list reached it (od_div0 error aborted the chain) - rebuilt standalone.

Verified recommended fix

Change the VDDC_CURVE bounds check to 'if (input_index < 0 || input_index > 2)' in vega20_odn_edit_dpm_table

Verdict

Source-certain negative-index OOB read in the PP_OD_EDIT_VDDC_CURVE branch of vega20_odn_edit_dpm_table (vega20_hwmgr.c:2656-2720), the vega10 DF-3704 signed-compare twin: the bounds check at :2678 tests only input_index > 2, so any negative user token survives; od8_id = OD8_SETTING_GFXCLK_FREQ1 + 2*input_index (:2685, :2695) goes negative and od8_settings[od8_id].min/.max_value are read (:2686-2687, :2696-2697) from before the 16x20-byte od8_settings_array embedded in the kzalloc'd vega20_hwmgr backend - and both garbage values are then printed by the pr_info range message (:2688-2692), leaking kernel heap bytes into the log; attacker-chosen magnitudes (int32 token, e.g. -100000000 -> ~-3.7GB offset) page-fault and panic the sysfs write path. Unlike vega10's twin there is no OOB write (the :2705 switch only writes indices 0/1/2). Trigger: root-gated pp_od_clk_voltage (S_IWUSR, amdgpu_pm.c:1028) whose kstrtol token parser (amdgpu_pm.c:575-592) accepts negative tokens; 'echo "vc -2 1000 900" > .../pp_od_clk_voltage'. Guest demonstration: the verbatim index/access/message logic on a heap stand-in leaked exactly the 0xA5A5A5A5 marker bytes at od8_id=-2 (-40 bytes) and -12 (-240 bytes) while the in-bounds control (od8_id=4) passed cleanly. Kernel path not runnable here (0 AMD PCI devices, no /dev/dri). Impact ceiling: root/confused-deputy kernel-heap info leak to dmesg + controllable-offset wild read (panic).