amdgpu_dpm: VBIOS power-table parsing reads heap OOB via unchecked offsets and indices
| Field | Value |
|---|---|
| ID | DF-1674 |
| File | sys/dev/drm/amd/amdgpu/amdgpu_dpm.c |
| Lines | 282, 295, 386, 448, 495, 534, 567, 583, 600 |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-125 Out-of-bounds Read |
| Confidence | certain |
| Status | new |
| CVE match | variant (VBIOS PowerPlay parser count/offset family β DF-1468/1469/1470/1471/1574/1586/1660/1674; same class as Linux upstream drm/amdgpu accumulated bounds checks) |
| Created | 2026-07-18 |
Summary
amdgpu_parse_extended_power_table() and
amdgpu_parse_clk_voltage_dep_table() dereference u16 offsets and u8
index fields read directly from the GPU VBIOS image to compute pointers
into the kmalloc'd BIOS buffer, without ever validating that the
resulting pointer or array index falls within adev->bios_size. A
crafted VBIOS image causes the parser to read kernel heap memory adjacent
to the BIOS allocation, and the leaked bytes are stored into DPM
dynamic-state tables that are subsequently surfaced to userspace via sysfs
clock/voltage attributes.
Root cause
The BIOS buffer is kmalloc'd with a tracked size (adev->bios_size in
amdgpu.h) but atom_context (atom.h:126-144) does not carry that
size, and the parsing code never consults it. Three distinct
unchecked-dereference patterns:
(a) Offset-based table lookup without bounds
Every sub-table pointer is computed as
mode_info->atom_context->bios + data_offset + le16_to_cpu(offset_field)
where offset_field is a raw u16 from the VBIOS. Examples at
amdgpu_dpm.c:387-389 (usVddcDependencyOnSCLKOffset), :398-400
(usVddciDependencyOnMCLKOffset), :409-411, :420-422, :432-434,
:451-452, :498-499, :533-534, :538-539, :602-603, :638-639,
:664-665, :697-698, :721-722, :760-761. If
data_offset + offset exceeds bios_size, the pointer lands in adjacent
heap.
(b) Loop reads entries[0..ucNumEntries-1] without verifying table size
amdgpu_parse_clk_voltage_dep_table lines 294-301 iterate
ucNumEntries times reading sizeof(ATOM_PPLIB_Clock_Voltage_Dependency_Record)
per step. Same pattern at :464-474 (phase shedding), :507-524 (CAC
leakage), :564-578 (VCE limits), :619-632 (UVD limits), :651-659
(SAMU), :709-717 (ACP).
(c) Array index without bounds check
At lines 567-569 and 583-585, entry->ucVCEClockInfoIndex and
state_entry->ucVCEClockInfoIndex (u8 from VBIOS) are multiplied by
sizeof(VCEClockInfo) and added to &array->entries[0] without checking
< array->ucNumEntries. Identical pattern for UVD at lines 621-623
(entry->ucUVDClockInfoIndex). An index >= ucNumEntries reads past the
VCEClockInfoArray into whatever follows in the BIOS buffer.
Threat model
Primary attacker position: a malicious PCIe device (e.g., external
Thunderbolt/USB-C GPU, or a compromised discrete GPU with reflashed VBIOS)
presents a crafted AtomBIOS image during device probe. The VBIOS is copied
into a kmalloc heap buffer at boot/init (amdgpu_bios.c), and the
unchecked offsets/indexes cause OOB reads of adjacent slab objects during
the single-shot parse at ci_dpm_init/kv_dpm_init/si_dpm_init
(ci_dpm.c:5875, kv_dpm.c:2806, si_dpm.c:7339). The parsed values
populate adev->pm.dpm.dyn_state.* tables and vce_states[], some of
which are readable by any local user via pp_dpm_sclk / pp_dpm_mclk /
pp_od_clk_voltage sysfs attributes (amdgpu_pm.c).
Secondary position: root can reflash VBIOS via flashrom.
NOT directly reachable from an unprivileged local user without malicious
hardware β the parse runs once at module load, never from a sysfs write
handler (amdgpu_set_pp_table routes to the powerplay layer, not this
code).
PoC
findings/poc/DF-1674/:
This is a hardware-supply-chain / malicious-VBIOS OOB-read primitive, not a pure local-user exploit. To demonstrate:
- Obtain a dump of a real AMD GPU VBIOS (e.g., from a Sea Islands card).
- Patch the VBIOS image: set
usVddcDependencyOnSCLKOffset(offset within_ATOM_PPLIB_POWERPLAYTABLE4) to a value near the end of the BIOS image, and set the target table'sucNumEntriesto 255. The resulting pointerbios+data_offset+offsetwill exceedbios_size, and the loop will read255 * sizeof(record)bytes past the BIOS allocation into adjacentkmallocslab objects. - Alternatively, patch a
VCEClockInfoArrayto haveucNumEntries=1, then set aVCE_Clock_Voltage_Limit_Record.ucVCEClockInfoIndexto 254. The pointer computation atamdgpu_dpm.c:567-569reads254 * sizeof(VCEClockInfo)bytes pastarray->entries[0]. - Load the patched VBIOS: use QEMU with
vfio-pcipassthrough of an AMD GPU, or use amdgpu's experimental VBIOS override path, or test on a physical card viaflashrom. - After amdgpu module loads, read the parsed clock tables via sysfs:
sh
cat /sys/class/drm/card0/device/pp_dpm_sclk
cat /sys/class/drm/card0/device/pp_od_clk_voltage
Entries that show implausible clock/voltage values are leaked heap
bytes interpreted as clock data.
6. Confirm with kasan (if enabled) which will report slab-out-of-bounds
reads in amdgpu_parse_clk_voltage_dep_table /
amdgpu_parse_extended_power_table.
Success criterion: kasan splat showing OOB read past kmalloc-2048/4096
slab object in amdgpu_dpm.c, OR sysfs clock values that differ from the
legitimate VBIOS contents (indicating leaked adjacent heap data).
Recommended fix
Pass bios_size into the parsing functions and validate every offset and
count before dereferencing. The minimal fix adds a bounds helper and
applies it at each dereference site. Since
mode_info->atom_context->bios == adev->bios and adev->bios_size is
available, the caller (amdgpu_parse_extended_power_table receives adev)
already has the size.
Concretely:
--- a/sys/dev/drm/amd/amdgpu/amdgpu_dpm.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_dpm.c
@@ -279,6 +279,21 @@ union fan_info {
struct _ATOM_PPLIB_FANTABLE3 fan3;
};
+/* Validate that [bios_ptr, bios_ptr + len) is within the mapped VBIOS image. */
+static bool amdgpu_dpm_bios_bounds(struct amdgpu_device *adev,
+ const void *ptr, size_t len)
+{
+ const u8 *bios = adev->mode_info.atom_context->bios;
+ const u8 *end = bios + adev->bios_size;
+ const u8 *p = (const u8 *)ptr;
+
+ if (p < bios || p > end)
+ return false;
+ if (len > (size_t)(end - p))
+ return false;
+ return true;
+}
+
static int amdgpu_parse_clk_voltage_dep_table(struct amdgpu_clock_voltage_dependency_table *amdgpu_table,
ATOM_PPLIB_Clock_Voltage_Dependency_Table *atom_table)
{
@@ -289,6 +304,11 @@ static int amdgpu_parse_clk_voltage_dep_table(struct amdgpu_clock_voltage_depend
u32 size = atom_table->ucNumEntries *
sizeof(struct amdgpu_clock_voltage_dependency_entry);
+ /* Validate the atom table header + all entries are within the BIOS image. */
+ if (!amdgpu_dpm_bios_bounds(adev, atom_table,
+ sizeof(*atom_table) +
+ (size_t)atom_table->ucNumEntries * sizeof(ATOM_PPLIB_Clock_Voltage_Dependency_Record)))
+ return -EINVAL;
+
int i;
ATOM_PPLIB_Clock_Voltage_Dependency_Record *entry;
Then pass adev into amdgpu_parse_clk_voltage_dep_table and apply the
same amdgpu_dpm_bios_bounds() check to every
bios + data_offset + le16_to_cpu(offset) computed pointer, and to every
ucVCEClockInfoIndex/ucUVDClockInfoIndex before indexing into the
VCEClockInfoArray/UVDClockInfoArray (validate index <
array->ucNumEntries AND that the resulting element is within bios bounds).
The upstream Linux drm/amdgpu tree has accumulated many such checks over
the years; this DragonFlyBSD snapshot predates most of them.
Related findings
VBIOS PowerPlay parser count/offset family:
- DF-1468/1469/1470/1471 (processpptables.c)
- DF-1574 (process_pptables_v1_0.c)
- DF-1586 (vega10_processpptables.c)
- DF-1560 (amd_powerplay.c)
- DF-1566 (r600_dpm.c)
- DF-1609 (smu10_hwmgr.c)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1674 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for amdgpu DPM VBIOS power table OOB | 360 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 814 B | β raw |
| build.sh | build-script | No-op (source-only) | 109 B | view raw |
| run.sh | run-script | No-op (source-only) | 107 B | view raw |
VERDICT DF-1674: amdgpu DPM VBIOS power table OOB
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
u16 offsets and u8 indices from VBIOS dereferenced without offset+size<bios_size validation.
Source reference: sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285-300.
Reproduction
Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed.
The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present
on the QEMU/virtio guest. The finding is HW-gated.
Fix
Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC β rc=0, -Werror clean.
See fix.diff for the git-apply-able patch.
Fix verification
fixedCombined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.
'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- d
- p
- m
- .
- c
- :
- 2
- 8
- 5
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
Add entry count bounds check. Matches finding.
Verdict
REPRODUCED (source-confirmed). VBIOS u16/u8 offsets dereferenced without bounds validation. Cited path verified at sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285. HW/module-gated on QEMU guest.
No comments yet.