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

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:

  1. powerplay_table->sHeader.format_revision >= ATOM_VEGA12_TABLE_REVISION_VEGA12 (9)
  2. 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/Min arrays (60 bytes each),
  • lines 252-253 copy 10-element PowerSavingClock arrays (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:

  1. Kernel heap OOB read of up to ~1 KB past the BIOS buffer, potentially leaking adjacent kernel memory into pptable_information->smc_pptable which may be exposed to userspace via sysfs attributes (pp_dpm_sclk, pp_od_clk_voltage, etc.).
  2. 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).

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2005 Β· 7 files
FileTypeDescriptionSize
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
README.md readme PoC trigger description (firmware-driven, HW-gated)
↓ download 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

  1. QEMU with -device vfio-pci,romfile=malicious.rom
  2. Custom PCI device option ROM (physical attacker card)
  3. 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_sclk or pp_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.

VERDICT.md verdict Full source-trace + HW-gating + offset math + fix rationale
↓ download raw

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_GENERIC lists device amd (AMD 53C974 SCSI) and device amdtemp (AMD CPU temp sensor). Neither is amdgpu.
  • kldstat shows no amdgpu module loaded.
  • pciconf -lv shows the only VGA device is vgapci0: chip=0x11111234 (QEMU std-VGA, vendor 0x1234 β€” not AMD vendor 0x1002). No AMD GPU hardware is present for amdgpu to bind.
  • vega12_processpptables.c is only compiled into amdgpu.ko (see sys/dev/drm/amd/amdgpu/Makefile:181). On real AMD Vega10/Vega12 hardware, vega12_hwmgr_init() registers vega12_pptable_funcs and the powerplay backend calls vega12_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

  1. get_powerplay_table() (lines 44-62) fetches the table pointer as bios + data_start, where data_start is a u16 offset read from the VBIOS data-table header (atom.c:1401 in amdgpu_atom_parse_data_header). At line 58 the declared size is cached: hwmgr->soft_pp_table_size = size;

  2. 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.

  1. vega12_pp_tables_initialize() (lines 267-294) calls check_powerplay_tables() at line 280, and on success proceeds to init_powerplay_table_information() at line 289.

  2. init_powerplay_table_information() (lines 191-264) reads fixed struct offsets that may exceed the actual table allocation: - lines 209-215: indexed reads of ODSettingsMax[] - lines 217-224: phm_copy_overdrive_settings_limits_array copies the full 15-element ODSettingsMax/ODSettingsMin arrays (60 bytes each) - lines 252-253: phm_copy_clock_limits_array copies the full 10-element PowerSavingClockMax/PowerSavingClockMin arrays (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 of smcPPTable.

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:

  1. hwmgr->soft_pp_table_size >= sizeof(ATOM_Vega12_POWERPLAYTABLE) β€” the cached VBIOS-declared data-table size must accommodate the full struct.
  2. 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/Makefile builds vega12_processpptables.c (line 181) β€” file compiles, the missing-check bug is silently present in the shipped .ko.
  • After (patched): apply fix.diff, rebuild amdgpu.ko; the fix adds only standard PP_ASSERT_WITH_CODE(...) calls with no new headers, types, or symbols β€” make -Werror passes rc=0. See fix_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:

  1. Build amdgpu.ko against sys/ with this fix.diff applied.
  2. Craft a truncated ATOM VBIOS image (per README.md) β€” format_revision=9, structuresize=1, data_start near end of a small buffer.
  3. Boot with the malicious VBIOS via -device vfio-pci,romfile=... or as a PCIe option ROM.
  4. Without fix: kernel panic in init_powerplay_table_information (or corrupted sysfs reads via the VRAM path).
  5. With fix: dmesg shows amdgpu: [powerplay] PowerPlay Table size smaller than ATOM_Vega12_POWERPLAYTABLE! and amdgpu init gracefully fails β€” no OOB read.

Kernel references (verified in source)

Fix verification

fixed
baseline no→ patch + rebuild →patched clean

VALIDATED module build. Patch applies, amdgpu.ko rebuilds rc=0 -Werror.

AMDGPU_KMOD_DONE rc=0; amdgpu.ko 3741464B sha256 b59b807c.
↓ fix.diffmodule build rc=0 -Werror (amdgpu.ko)

Confirmed kernel references

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.