Missing VBIOS PowerPlay table size validation allows heap out-of-bounds read
- File:
sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c - Lines: 64β75 (
check_powerplay_tables), 259 / 217β224 / 252β253 (init_powerplay_table_information) - Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U:C:L/I:N/A:H - CWE: CWE-125 Out-of-bounds Read
- Confidence: likely
- Status: new
Summary
check_powerplay_tables() validates only format_revision >= 9 and
structuresize > 0, never verifying that the VBIOS-supplied PowerPlay table
is large enough to contain ATOM_Vega12_POWERPLAYTABLE.
The cached soft_pp_table_size (from the data table header, line 58) is never
consulted.
init_powerplay_table_information() then reads fixed-offset fields and
performs a memcpy of sizeof(PPTable_t) (~1 KB+) from
powerplay_table->smcPPTable (line 259), plus array copies of
ODSettingsMax[15] and PowerSavingClockMax[10].
If the actual table is truncated β feasible when the VBIOS image is attacker-controlled β these reads go out of bounds of the BIOS heap buffer, leaking adjacent kernel memory or causing a kernel panic.
Root cause
get_powerplay_table() (lines 44-62) fetches the table via
smu_atom_get_data_table(), which returns bios + data_start where
data_start is a u16 offset read from the VBIOS image
(amdgpu_atom_parse_data_header at atom.c:1401:
*data_start = idx = CU16(ctx->data_table + offset)).
It caches the pointer in hwmgr->soft_pp_table and the VBIOS-declared size in
hwmgr->soft_pp_table_size (line 58).
check_powerplay_tables() (lines 64-75) then gates on only two conditions:
powerplay_table->sHeader.format_revision >= ATOM_VEGA12_TABLE_REVISION_VEGA12 (9)powerplay_table->sHeader.structuresize > 0
Neither hwmgr->soft_pp_table_size nor structuresize is compared to
sizeof(ATOM_Vega12_POWERPLAYTABLE).
Consequently, init_powerplay_table_information() reads at fixed struct
offsets that may exceed the actual table:
- line 209 reads
ODSettingsMax[0]at ~offset 164, - lines 217-224 copy the full 15-element
ODSettingsMax/Minarrays (60 bytes each), - lines 252-253 copy 10-element
PowerSavingClockarrays (40 bytes each), and - critically line 259 does
memcpy(dst, &powerplay_table->smcPPTable, sizeof(PPTable_t))reading ~1 KB starting at ~offset 244 into the table.
The BIOS buffer allocation size is in several code paths derived from the VBIOS
image itself (amdgpu_read_bios_from_rom at amdgpu_bios.c:172:
len = AMD_VBIOS_LENGTH(header)), so an attacker-controlled VBIOS can produce
a small allocation (e.g. 512 bytes) with a data_start near its end, causing
these reads to overflow into adjacent slab objects.
Threat model
Attacker position: a malicious hypervisor supplying a crafted VBIOS option ROM to a guest VM with AMD GPU passthrough, or a physical attacker with a malicious PCIe GPU card.
No userspace privilege or interaction is required β the parser runs during
amdgpu driver init (vega12_hwmgr_init at vega12_hwmgr.c:2420 sets
pptable_func; pptable_init is called during powerplay backend setup).
Impact:
- Kernel heap OOB read of up to ~1 KB past the BIOS buffer, potentially
leaking adjacent kernel memory into
pptable_information->smc_pptablewhich may be exposed to userspace via sysfs attributes (pp_dpm_sclk,pp_od_clk_voltage, etc.). - Kernel panic (DoS) if the read crosses a page boundary into unmapped memory.
The igp_read_bios_from_vram path (amdgpu_bios.c:93) uses a fixed 256 KB
allocation which would contain the OOB read within the buffer (less likely to
panic, still an info leak of adjacent BIOS data); the
amdgpu_read_bios_from_rom path uses an attacker-controlled allocation size
making panic likely.
Proof of concept
Craft a minimal ATOM VBIOS image (~2 KB) that:
(a) passes check_atom_bios signature validation
(ATOM_BIOS_MAGIC = 0xAA55 at offset 0, ATOM at offset 0x30,
ATOM ROM table magic);
(b) contains a powerplay data table entry in the master data table with
data_start pointing near the end of the buffer and size/structuresize
set to small nonzero values;
(c) sets sHeader.format_revision = 9 and sHeader.structuresize = 1
(passes both checks in check_powerplay_tables).
Load via QEMU with -device vfio-pci,romfile=malicious.rom or as a custom PCI
device option ROM. On guest boot with the AMDGPU driver,
vega12_pp_tables_initialize() will call init_powerplay_table_information()
which memcpys sizeof(PPTable_t) bytes from the truncated table, reading
past the BIOS heap buffer.
Success: kernel panic (unmapped page) or dmesg showing corrupted
powerplay values (info leak).
No userspace compilation needed β this is triggered by kernel driver init
against the crafted ROM. To verify info leak, compile a userspace program
reading /sys/class/drm/card0/device/pp_dpm_sclk or pp_od_clk_voltage on a
system that survives the boot (VRAM path with 256 KB buffer).
Recommended fix
Add a size bound check in check_powerplay_tables() using the cached
soft_pp_table_size, which was set by get_powerplay_table() from the data
table header. Also validate structuresize for defense in depth.
--- a/sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c
@@ -68,9 +68,19 @@ static void set_hw_cap(struct pp_hwmgr *hwmgr, bool enable,
static int check_powerplay_tables(
struct pp_hwmgr *hwmgr,
const ATOM_Vega12_POWERPLAYTABLE *powerplay_table)
{
+ uint16_t struct_size = le16_to_cpu(powerplay_table->sHeader.structuresize);
+
PP_ASSERT_WITH_CODE((powerplay_table->sHeader.format_revision >=
ATOM_VEGA12_TABLE_REVISION_VEGA12),
"Unsupported PPTable format!", return -1);
PP_ASSERT_WITH_CODE(powerplay_table->sHeader.structuresize > 0,
"Invalid PowerPlay Table!", return -1);
+ PP_ASSERT_WITH_CODE((hwmgr->soft_pp_table_size >=
+ sizeof(ATOM_Vega12_POWERPLAYTABLE)),
+ "PowerPlay Table size smaller than expected structure!",
+ return -1);
+ PP_ASSERT_WITH_CODE((struct_size >= sizeof(ATOM_Vega12_POWERPLAYTABLE)),
+ "PowerPlay Table structuresize too small!",
+ return -1);
return 0;
}
This ensures both the BIOS-declared data table size (soft_pp_table_size) and
the in-table structuresize field are at least as large as the full
ATOM_Vega12_POWERPLAYTABLE before any field is accessed.
A further hardening (out of scope for this file) would be to validate
data_start + size <= bios_size inside smu_atom_get_data_table() or
amdgpu_atom_parse_data_header() in atom.c, since ctx->bios has no
recorded length today.
References
sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:64-75βcheck_powerplay_tablesweak validationsys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:259βmemcpy(sizeof(PPTable_t))from truncated tablesys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:217-224,252-253β fixed-array copiessys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:44-62βget_powerplay_tablefetchesbios + data_startsys/dev/drm/amd/amdgpu/amdgpu_bios.c:172β BIOS alloc size derived from VBIOS imagesys/dev/drm/amd/amdgpu/atom.c:1401βdata_start = CU16(...)attacker-influenced offset
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2005 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | PoC trigger description (firmware-driven, HW-gated) | 1.6 KB | β raw |
| VERDICT.md | verdict | Full source-trace + HW-gating + offset math + fix rationale | 9.8 KB | β raw |
| fix.diff | suggested-fix | git-apply-able diff: add soft_pp_table_size + structuresize bounds checks in check_powerplay_tables | 806 B | view raw |
| build.sh | build-script | Applies fix.diff in guest, builds amdgpu.ko with -Werror | 873 B | view raw |
| run.sh | run-script | Informational: confirms HW-gating (no AMD GPU on guest) | 1.2 KB | view raw |
| fix_build.log | build-log | Full untrimmed amdgpu.ko build output, rc=0, -Werror enforced | 1.2 MB | β download |
| env.txt | environment | uname, kern.version, vga pci (no AMD), sha256 of patched amdgpu.ko | 1.0 KB | view raw |
DF-2005 PoC β Heap OOB read via truncated VBIOS PowerPlay table
Trigger (firmware-driven)
Craft a minimal ATOM VBIOS image (~2 KB) that:
(a) passes check_atom_bios signature validation
(ATOM_BIOS_MAGIC = 0xAA55 at offset 0, ATOM at offset 0x30,
ATOM ROM table magic);
(b) contains a powerplay data table entry in the master data table with
data_start pointing near the end of the buffer and size/structuresize
set to small nonzero values;
(c) sets sHeader.format_revision = 9 and sHeader.structuresize = 1
(passes both checks in check_powerplay_tables).
Load vectors
- QEMU with
-device vfio-pci,romfile=malicious.rom - Custom PCI device option ROM (physical attacker card)
- Malicious hypervisor supplying crafted VBIOS to a guest VM with AMD GPU passthrough
Expected output
On guest boot with AMDGPU driver, vega12_pp_tables_initialize() calls
init_powerplay_table_information() which memcpys sizeof(PPTable_t) bytes
(~1 KB) from the truncated table, reading past the BIOS heap buffer.
Success:
- amdgpu_read_bios_from_rom path (attacker-controlled alloc size): kernel panic (page fault on unmapped memory).
- igp_read_bios_from_vram path (fixed 256 KB buffer): info leak into
pptable_information->smc_pptableβ read back via/sys/class/drm/card0/device/pp_dpm_sclkorpp_od_clk_voltage.
Success criterion
dmesg shows "Fatal trap 12: page fault while in kernel mode" rooted in
init_powerplay_table_information for the panic path, or corrupted/non-zero
powerplay values in the sysfs reads for the info-leak path.
DF-2005 β VERDICT
Status: INCONCLUSIVE (HW-gated; source-only confirmation) Reproduced: 0 (no trigger possible on this guest) Impact: none (on this guest) β latent OOB-read primitive in HW-gated code Confidence: certain (source-trace); speculative (runtime β never exercised) Class: CWE-125 out-of-bounds read in VBIOS PowerPlay table parser
Verdict (one line)
The bug is real at the source level β check_powerplay_tables() performs
no size validation against sizeof(ATOM_Vega12_POWERPLAYTABLE) before the
caller reads sizeof(PPTable_t) (~470+ bytes) at fixed offset ~244 via
memcpy β but the vulnerable code is HW-gated (no AMD GPU on the guest,
amdgpu is not in X86_64_GENERIC, no AMD GPU device present), so the
primitive cannot be triggered on this guest. Status inconclusive per the
HW-gated rule; the fix.diff is validated by building the patched
amdgpu.ko module with -Werror rc=0.
Why it cannot fire on this guest (HW-gated)
sys/config/X86_64_GENERIClistsdevice amd(AMD 53C974 SCSI) anddevice amdtemp(AMD CPU temp sensor). Neither isamdgpu.kldstatshows no amdgpu module loaded.pciconf -lvshows the only VGA device isvgapci0: chip=0x11111234(QEMU std-VGA, vendor 0x1234 β not AMD vendor 0x1002). No AMD GPU hardware is present foramdgputo bind.vega12_processpptables.cis only compiled intoamdgpu.ko(seesys/dev/drm/amd/amdgpu/Makefile:181). On real AMD Vega10/Vega12 hardware,vega12_hwmgr_init()registersvega12_pptable_funcsand the powerplay backend callsvega12_pp_tables_initialize(). None of this runs without an AMD GPU.
The audit guest has no AMD GPU passthrough, so the driver never probes, and the parsing code path is dead at runtime. This is a latent bug reachable only on real Vega-class AMD hardware (or via malicious hypervisor supplying a crafted VBIOS option ROM to a guest with AMD GPU passthrough).
Mechanism (source-trace confirmation)
Vulnerable file: sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c
-
get_powerplay_table()(lines 44-62) fetches the table pointer asbios + data_start, wheredata_startis au16offset read from the VBIOS data-table header (atom.c:1401inamdgpu_atom_parse_data_header). At line 58 the declared size is cached:hwmgr->soft_pp_table_size = size; -
check_powerplay_tables()(lines 64-75) gates on only two checks: -powerplay_table->sHeader.format_revision >= 9(line 68-70) -powerplay_table->sHeader.structuresize > 0(line 71-72)
It never compares soft_pp_table_size nor structuresize against
sizeof(ATOM_Vega12_POWERPLAYTABLE). This is the root-cause omission.
-
vega12_pp_tables_initialize()(lines 267-294) callscheck_powerplay_tables()at line 280, and on success proceeds toinit_powerplay_table_information()at line 289. -
init_powerplay_table_information()(lines 191-264) reads fixed struct offsets that may exceed the actual table allocation: - lines 209-215: indexed reads ofODSettingsMax[]- lines 217-224:phm_copy_overdrive_settings_limits_arraycopies the full 15-elementODSettingsMax/ODSettingsMinarrays (60 bytes each) - lines 252-253:phm_copy_clock_limits_arraycopies the full 10-elementPowerSavingClockMax/PowerSavingClockMinarrays (40 bytes each) - line 259:memcpy(pptable_information->smc_pptable, &(powerplay_table->smcPPTable), sizeof(PPTable_t));β the critical sink. Reads ~470+ bytes starting at the offset ofsmcPPTable.
Offset math (packed struct, #pragma pack(push,1), vega12_pptable.h:26)
ATOM_Vega12_POWERPLAYTABLE layout before smcPPTable:
- sHeader (atom_common_table_header: u16+u8+u8) = 4
- ucTableRevision = 1
- usTableSize = 2
- ulGoldenPPID, ulGoldenRevision = 8
- usFormatID = 2
- ulPlatformCaps = 4
- ucThermalControllerType = 1
- 6 Γ USHORT (usSmallPowerLimit1, β¦, usSoftwareShutdownTemp) = 12
- PowerSavingClockMax[10] = 40
- PowerSavingClockMin[10] = 40
- ODSettingsMax[15] = 60
- ODSettingsMin[15] = 60
- usReserve[5] = 10
Total before smcPPTable: 244 bytes. The line-259 memcpy then copies
sizeof(PPTable_t) (β470+ bytes, defined at
sys/dev/drm/amd/powerplay/inc/vega12/smu9_driver_if.h:26-510), so reads
extend from offset 244 to β714+. A truncated VBIOS table that passes
check_powerplay_tables (e.g. with structuresize=1, format_revision=9,
actual buffer < 244 bytes) makes all of these reads OOB.
BIOS allocation size is attacker-influenced
amdgpu_read_bios_from_rom (amdgpu_bios.c:172) derives
len = AMD_VBIOS_LENGTH(header) from a header field inside the VBIOS image
itself, so an attacker-controlled VBIOS can produce a small allocation with
data_start near its end, guaranteeing OOB. (igp_read_bios_from_vram uses
a fixed 256 KB buffer β OOB stays in-buffer, still an info leak into
pptable_information->smc_pptable, potentially exposed via sysfs
pp_dpm_sclk / pp_od_clk_voltage.)
Threat model
Attacker position: malicious hypervisor supplying a crafted VBIOS option ROM to a guest VM with AMD GPU passthrough, or a physical attacker with a malicious PCIe GPU card. No userspace privilege or interaction required β the parser runs during amdgpu driver init.
Realistic impact ceiling on real hardware: kernel heap OOB read of up to
~1 KB, leaking adjacent slab data into pptable_information->smc_pptable
(likely exfiltrable via sysfs), or kernel panic if the read crosses a
page boundary into unmapped memory.
Why no live PoC / no escalation chain
There is no corruption primitive to develop (read-only OOB), and the path is not reachable on this guest (no AMD GPU). Per the Phase 6 valid-hard-blocker list: the vulnerable code path is dead/unreachable at runtime on this guest AND no harness can exercise it β a userspace harness cannot drive VBIOS parsing without an AMD GPU device for amdgpu to bind. This is a valid hard blocker; the primitive is documented at the source level instead.
Fix
fix.diff adds two PP_ASSERT_WITH_CODE guards in
check_powerplay_tables(), matching the surrounding macro style:
hwmgr->soft_pp_table_size >= sizeof(ATOM_Vega12_POWERPLAYTABLE)β the cached VBIOS-declared data-table size must accommodate the full struct.powerplay_table->sHeader.structuresize >= sizeof(ATOM_Vega12_POWERPLAYTABLE)β defense in depth on the in-table declared size.
Both checks use the existing PP_ASSERT_WITH_CODE macro (defined in
sys/dev/drm/amd/powerplay/inc/pp_debug.h:37-43), so they emit a pr_warn
and return -1 on failure, short-circuiting vega12_pp_tables_initialize
before any field is read.
This matches the finding proposal in
findings/DF-2005-β¦md:## Recommended fix with one stylistic improvement:
the in-table sHeader.structuresize is read as host-endian (matching the
existing line 71-72 check and the un-annotated uint16_t structuresize
field in atom_common_table_header, atomfirmware.h:224-229), avoiding a
spurious le16_to_cpu that would imply an __le16 annotation the struct
does not have.
Phase 8 β fix validation (amdgpu.ko module build)
The bug is HW-gated, so the patched kernel cannot be runtime-tested on this
guest. Phase 8 is therefore the module-build form of validation:
apply fix.diff to in-guest /usr/src, build amdgpu.ko with -Werror,
and confirm rc=0.
- Before (unpatched):
sys/dev/drm/amd/amdgpu/Makefilebuildsvega12_processpptables.c(line 181) β file compiles, the missing-check bug is silently present in the shipped.ko. - After (patched): apply
fix.diff, rebuildamdgpu.ko; the fix adds only standardPP_ASSERT_WITH_CODE(...)calls with no new headers, types, or symbols βmake -Werrorpassesrc=0. Seefix_build.log.
Since the fix is provably a pure addition of two macro invocations against an existing public struct type, and the module links cleanly, this constitutes the strongest validation possible on a guest without the required hardware.
Reproduce
On a system with a Vega-class AMD GPU and DragonFlyBSD amdgpu:
- Build
amdgpu.koagainstsys/with thisfix.diffapplied. - Craft a truncated ATOM VBIOS image (per
README.md) βformat_revision=9,structuresize=1,data_startnear end of a small buffer. - Boot with the malicious VBIOS via
-device vfio-pci,romfile=...or as a PCIe option ROM. - Without fix: kernel panic in
init_powerplay_table_information(or corrupted sysfs reads via the VRAM path). - With fix:
dmesgshowsamdgpu: [powerplay] PowerPlay Table size smaller than ATOM_Vega12_POWERPLAYTABLE!and amdgpu init gracefully fails β no OOB read.
Kernel references (verified in source)
sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:64-75βcheck_powerplay_tablesweak validationsys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:58βsoft_pp_table_sizecached but never consultedsys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:259βmemcpy(sizeof(PPTable_t))from truncated tablesys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:217-224,252-253β fixed-array copiessys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:280-289βcheck_powerplay_tablesgatesinit_powerplay_table_informationsys/dev/drm/amd/powerplay/hwmgr/vega12_pptable.h:75-105β packedATOM_Vega12_POWERPLAYTABLElayout (244 B preamble +smcPPTable)sys/dev/drm/amd/include/atomfirmware.h:224-229βatom_common_table_header(4 B)sys/dev/drm/amd/powerplay/inc/pp_debug.h:37-43βPP_ASSERT_WITH_CODEmacro semanticssys/dev/drm/amd/amdgpu/Makefile:181β file compiled intoamdgpu.kosys/config/X86_64_GENERIC:88,186β onlydevice amd(SCSI) anddevice amdtemp, no amdgpu
Fix verification
fixedVALIDATED module build. Patch applies, amdgpu.ko rebuilds rc=0 -Werror.
AMDGPU_KMOD_DONE rc=0; amdgpu.ko 3741464B sha256 b59b807c.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- v
- e
- g
- a
- 1
- 2
- _
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 6
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- v
- e
- g
- a
- 1
- 2
- _
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 2
- 5
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- v
- e
- g
- a
- 1
- 2
- _
- p
- p
- t
- a
- b
- l
- e
- .
- h
- :
- 7
- 5
Detail
Exploit chain
none (HW-gated, read-only OOB). Primitive: ~1KB heap OOB read. Requires real Vega AMD HW or malicious hypervisor option ROM.
Evidence (decisive lines)
pciconf: only QEMU stdvga. amdgpu not in GENERIC. Module build rc=0 -Werror.
Verified recommended fix
Add two PP_ASSERT_WITH_CODE guards in check_powerplay_tables: soft_pp_table_size >= sizeof(ATOM_Vega12_POWERPLAYTABLE) and sHeader.structuresize >= sizeof(...).
Verdict
HW-GATED (no AMD GPU). Source-CONFIRMED. check_powerplay_tables() at vega12_processpptables.c:64-75 only checks format_revision>=9 and structuresize>0 β no bounds vs sizeof(ATOM_Vega12_POWERPLAYTABLE). Line 259 memcpy(sizeof(PPTable_t)~470B) from offset 244 OOBs any truncated VBIOS table.
No comments yet.