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)
PoC verification
Evidence pack
findings/poc/DF-3859 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_dataetc.) β and both garbage values are printed to the log by thepr_inforange 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.
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):
:2560declaresint32_t input_index, input_clk, input_vol;;:2567+inputis the caller'slong *array of kstrtol-parsed user tokens.:2674input_index = input[i];β a user-controlled signed value.:2678if (input_index > 2)β only an upper-bound check; every negative value passes. (Compare the SCLK branch:2585input_index != 0 && input_index != 1and the MCLK branch:2633input_index != 1, which are exact-match and safe.):2685od8_id = OD8_SETTING_GFXCLK_FREQ1 + 2 * input_index;β for input_index β€ -2 this is negative;od8_settings[od8_id]at:2686-2687readsod8_settings_array[od8_id]before the 16-element array (vega20_hwmgr.h:400-429, element size 20, embedded in the kzalloc'dstruct vega20_hwmgrbackend,vega20_hwmgr.c:383).- On the (almost certain) range-check failure,
:2688-2692pr_infosod8_settings[od8_id].min_value/.max_valueβ the OOB-read kernel heap bytes land in the log.:2695-2703repeats the identical negative index for the voltage slots. :2705-2718the writeswitch (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_testablefix.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
Confirmed kernel references
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2560
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2674
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2678
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2685
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2686
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2688
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:2695
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.h:429
- sys/dev/drm/amd/amdgpu/amdgpu_pm.c:1028
- sys/dev/drm/amd/amdgpu/amdgpu_pm.c:575
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
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).
No comments yet.