No central bounds-validation layer for VBIOS PowerPlay parsing: offsets/counts/indices never checked against soft_pp_table_size (central fix for the DF-1468/1469/1470/1472 family)
Summary
get_powerplay_table records soft_pp_table_size but no code ever consults it: every u16 sub-table offset is added to the image base and dereferenced, every u8 entry count drives both allocation and source-read loops, and every record index is a raw u8. This is the shared root of the smu7/smu8 (PP_TABLE_V0) count family. CENTRAL FIX (731 lines, git-apply-check OK): pp_table_range_ok()/pp_table_clamp_count() at every deref site plus the off-by-one/NULL/size-gate fixes. COVERAGE ASSESSMENT (the radeon_pm.c-DF-4162 analog for powerplay): fully covers DF-1468/1469/1470/1471/1472 + DF-4248/4249; root-cause-only for the smumgr sink families (DF-1166, DF-4000..4006, DF-4036..4041, DF-1416/1609 still need per-file MIN clamps); NOT covering the independent parsers (DF-1574, DF-1586..1589, DF-1689/1690, DF-2005). CROSS-FILE LEAD: pp_dpm_set_pp_table memcpy heap overflow at amd_powerplay.c:684-689 (root-only, unbounded sysfs write vs kmemdup'd size).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4247 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.2 KB | β raw | |
| VERDICT.md | β | 9.1 KB | β raw | |
| verdict.json | β | 6.4 KB | view raw | |
| manifest.json | β | 2.6 KB | view raw | |
| fix.diff | β | 26.9 KB | view raw | |
| build.log | β | 2.5 KB | view raw | |
| run.log | β | 12.1 KB | view raw | |
| run.2.log | β | 12.1 KB | view raw | |
| run.3.log | β | 12.1 KB | view raw | |
| fix_validation.log | β | 4.6 KB | view raw | |
| env.txt | β | 915 B | view raw | |
| hw_probe.log | β | 425 B | view raw | |
| trigger.sh | β | 1.6 KB | view raw | |
| rig/gen_rig.sh | β | 4.3 KB | view raw | |
| rig/check_verbatim.sh | β | 3.4 KB | view raw | |
| rig/Makefile | β | 427 B | β download | |
| rig/pp_rig.c | β | 26.4 KB | view raw | |
| rig/rig_shims.h | β | 4.9 KB | view raw | |
| rig/build.sh | β | 494 B | view raw | |
| rig/run.sh | β | 844 B | view raw | |
| rig/code_hashes.txt | β | 605 B | view raw |
DF-4247 β Missing central bounds-validation layer for VBIOS PowerPlay table parsing (processpptables.c)
Status
REPRODUCED (verbatim-rig evidence class, same as DF-4165). All defect
families claimed by the finding were demonstrated in byte-identical
production parser code compiled into a guest-side harness and driven with
crafted PowerPlay tables; the pack's fix.diff was A/B-validated in the same
rig (fixed). In-kernel execution remains AMD-ASIC-gated on this guest (no AMD
GPU β proven, env.txt/hw_probe.log).
What was verified and how
The rig (rig/) sed-extracts the entire parser body β
processpptables.c lines 37-1706, every offset/count/index site β plus 22
verbatim struct/type header slices (pptable.h 26-751, power_state.h,
hwmgr/hardwaremanager slices, atombios FW-info tables, processpptables.h
interface), and compiles them into a harness on the DragonFly guest.
rig/check_verbatim.shre-proved all 23 blocks byte-identical, run on the guest against/usr/src/sysβ the tree its running kernel (Jul 2 2026 build) was compiled from (build.log:ALL VERBATIM CHECKS PASSED; host/guest file SHA256 equality inenv.txt).- The fixed build is the same slice with this pack's
fix.diffapplied viapatch --fuzz=0(bidirectionally proven: forward-apply + reverse-apply restores the pristine file byte-exactly). - Shims (
rig/rig_shims.h) touch no defect path: identityle*_to_cpu, calloc kzalloc, minimizedstruct pp_hwmgrwith production names/types for every field the fix consults, and asmu_atom_get_data_tableshim standing in for the ROM FirmwareInfo lookup (NULL-able, to prove DF-1471's missing NULL check). The crafted images mimic ATOM PowerPlay tables a crafted VBIOS (or root-writtenpp_tablesysfs blob) feeds the driver on smu7/smu8 (PP_TABLE_V0); each image ends at a PROT_NONE guard page or at a patterned victim region, making OOB source reads decisive.
Observed (run.log / run.2.log / run.3.log β stable across 3 runs)
Layout: PT=37/46/66/88 bytes, usVddcDependencyOnSCLKOffset@54,
usExtendendedHeaderOffset@44, dep record=5B, NONCLOCK=24B, VCE/UVD info=6B.
| mode (crafted input) | baseline (verbatim) | fixed (+fix.diff) |
|---|---|---|
| clean-valid | rc=0, count=3, cbΓ1 (PASS) | identical (no regression) |
dep-offset-oob (usVddcDependencyOnSCLKOffset=0xFF00, 128B image) |
SIGSEGV, offset 65280 Β» 128 (:1304-1309,385) |
rc=0, skipped |
| dep-count-255 (255 claimed, 3 records) | SIGSEGV, offset 146 β₯ 144 (:393-399) |
count clamped to 3, no fault |
| leak-canary (40 claimed, 5 records + victim) | 70/70 field values byte-exact with bytes past the image β dyn_state | clamped to 5, 0 leaked |
statearray-offset-oob (usStateArrayOffset=0xFF00) |
SIGSEGV, offset 65280 (:875-878) |
rc=-1 |
entry-v2-offbyone (entry_index==ucNumEntries) |
SIGSEGV, offset 142 vs 141 (:913-924, > should be >=; also get_state_entry_v2:784 <=) |
rc=-1 |
entry-v1-offbyone (fmt<6, entry_index==ucNumStates) |
SIGSEGV, offset 136 vs 136 (:937-947) |
rc=-1 |
nonclock-index-255 (nonClockInfoIndex=0xFF Γ ucEntrySize=0xFF) |
SIGSEGV, offset 65227 vs 242 (:923-926,685) |
rc=-1 |
clockinfo-index-255 (clockInfoIndex[0]=0xFF Γ 0xFF) |
SIGSEGV, offset 65187 (:929-932; deref in the clock-info callback, as in smu7/smu8) |
rc=-EINVAL, 0 callbacks |
uvd-array-index (ucUVDClockInfoIndex=0xFF, init-time) |
SIGSEGV, offset 1662 vs 134 (:1099-1107) |
rc=0, index clamped |
vce-clockinfo-index (ucVCEClockInfoIndex=0xFF) |
SIGSEGV, offset 1662 (:1571-1585) |
rc=-EINVAL |
| fwinfo-null (FirmwareInfo table absent) | SIGSEGV at NULL+2 (:1062-1066, DF-1471 half) |
rc=0 |
fix_validation.log = the full FIXED half of the decisive run.
Primitive and impact
Source-side OOB read family: firmware offsets (u16), counts (u8) and
indices (u8) index straight out of the soft_pp_table heap image (typically
2-4 KB) β up to 65280 bytes past it β with the read bytes flowing 1:1 into
hwmgr->dyn_state.* tables (leak-canary: 70/70 byte-exact), which userland
reads back through powerplay sysfs/pp_dpm_* (kernel-heap info leak), plus a
NULL-deref panic path when the ROM lacks FirmwareInfo. Write sinks are not in
this file (dest allocations over-reserve: 8+12n β₯ 4+8n β layout mode).
Gating: an AMD ASIC (smu7/smu8/smu10-shortcircuited) whose VBIOS carries a
crafted/corrupt PowerPlay table, or root re-parsing via
/sys/class/drm/card0/device/pp_table (amd_powerplay.c:672-706). No
unprivileged trigger on stock hardware.
Build / run (on the audit guest, or any DF box with the kernel tree)
scp -r rig fix.diff root@guest:/root/df4247/ cd /root/df4247/rig && sh ./build.sh /usr/src # verbatim checks + 2 binaries sh ./run.sh # baseline vs fixed, 13 modes
fix.diff corrections found during verification (this run)
The pre-verification fix.diff did not compile: (1) get_clock_voltage_limit
guard referenced table->numEntries β the struct member is ucNumEntries
(pptable.h:531); (2) two func(...) calls passed an integer as the
const void *clock_info argument (int-conversion). Both corrected in this
pack's fix.diff; the corrected diff applies cleanly (fuzz=0) and is what the
rig validated.
DF-4247 VERDICT (verify run, 2026-10-14)
Status: REPRODUCED (verbatim-rig evidence class). All defect families the finding enumerates were demonstrated in byte-identical production code; in-kernel execution is AMD-ASIC-gated on this guest (no AMD GPU β proven, env.txt / hw_probe.log). Fix A/B-validated in the same rig: fixed.
1. Method
The guest cannot attach amdgpu (Bochs SVGA 0x1234:1111, no 1002: device,
/boot/kernel/amdgpu.ko unloadable β env.txt). Following the DF-4165
evidence class for crafted-firmware parsers, the actual production parser
was compiled into a guest-side harness:
rig/gen_rig.shextracts processpptables.c:37-1706 β the entire parser body (allget_*_offset/size helpers, all copy helpers,pp_tables_get_num_of_entries/get_entry, the VCE-state getters,pp_tables_initialize, thepptable_funcsvtable) plus 22 header slices (pptable.h 26-751 = every ATOM_PPLIB struct; processpptables.h 28-49; power_state.h 26-187; hwmgr.h 88-186/335-345/347-362/364-373/375-409/ 454-465/485-493/595-624; hardwaremanager.h 55-252/254-258/283-300/326-333/ 344-370; atombios.h 207-214/2941-2945/3055-3131; atom-types.h; amd_asic_type.h; kgd_pp_interface.h 29-39; the PP_ASSERT_WITH_CODE macro).rig/check_verbatim.shre-verified all 23 blocks byte-identical β run on the guest against/usr/src/sys, the tree its running kernel was compiled from (build.log:ALL VERBATIM CHECKS PASSED; SHA256 equality of the three key files hostβguest in env.txt).- The fixed build is the same slice + exactly this pack's fix.diff
(applied to a copy of the pristine full file with
patch --fuzz=0; proven bidirectionally: forward apply succeeds with no rejects, reverse apply restores the pristine file byte-exactly). - Harness shims (rig_shims.h) touch no defect path: identity
le16_to_cpu/le32_to_cpu(LE host, like the target), calloc-backedkzalloc/kfree, a minimizedstruct pp_hwmgrcarrying production names/types for every field the parser and the fix consult (soft_pp_table,soft_pp_table_sizeexactly as hwmgr.h:718-719), and asmu_atom_get_data_tableshim that returns the harness FirmwareInfo blob (or NULL, to exercise the DF-1471 missing-NULL-check). The only live caller of that shim in the slice isinit_overdrive_limits(processpptables.c:1062-1064).
Crafted tables mimic real ATOM PowerPlay tables: POWERPLAYTABLE5 header (usTableSize gates the PT3/PT4/PT5 branches at :155/:1300/:1434), extended header for VCE/UVD/SAMU/ACP/PowerTune offsets, StateArray/ClockInfoArray/ NonClockInfoArray for the state paths. Images end exactly at a PROT_NONE guard page (making the first OOB source read fault at a computable offset) or at a 256-byte patterned victim region (leak mode, no guard so the parse completes and the captured bytes can be compared).
2. Observed (run.log; identical in run.2.log / run.3.log)
Exact layouts from the verbatim structs: PT=37, PT3=46, PT4=66, PT5=88;
usVddcDependencyOnSCLKOffset@54; usExtendendedHeaderOffset@44; dep
record=5 B, table hdr=6 B; ATOM_PPLIB_NONCLOCK_INFO=24; VCE/UVD ClockInfo=6.
Baseline β production code faults on every crafted input (10/10), each at exactly the predicted offset; classified by the SIGSEGV handler:
| mode | fault offset vs table size | verbatim site |
|---|---|---|
| dep-offset-oob | 65280 vs 128 | table->ucNumEntries read at base+0xFF00 β :1304-1309 β :385 |
| dep-count-255 | 146 vs 144 | entries[3] read past 3-record table β :393-399 |
| statearray-offset-oob | 65280 vs 128 | pstate_arrays->ucNumEntries β :875-878 |
| entry-v2-offbyone | 142 vs 141 | pstate_entry_v2->nonClockInfoIndex at &states[N] β :913-924 (guard > accepts one-past; get_state_entry_v2:784 <= walks it) |
| entry-v1-offbyone | 136 vs 136 | pstate_entry->ucNonClockStateIndex β :937-947 (same off-by-one) |
| nonclock-index-255 | 65227 vs 242 | nonClockInfo + 0xFF*0xFF deref in init_non_clock_fields β :923-926 β :685 |
| clockinfo-index-255 | 65187 vs 242 | clockInfo + 0xFF*0xFF handed to the clock-info callback β :929-932 (deref in the smu7/smu8 callback, mimicked by the harness cb) |
| uvd-array-index | 1662 vs 134 | array->entries[0xFF].ucVClkHigh at init time β :1099-1107 |
| vce-clockinfo-index | 1662 vs 134 | entries[0xFF].ucEVClkHigh via exported VCE getter β :1571-1585 |
| fwinfo-null | NULL+2 | fw_info->ucTableFormatRevision with fw_info==NULL β :1062-1066 (DF-1471 half) |
Leak leg (leak-canary): ucNumEntries=40 with only 5 records in image,
image followed by patterned victim bytes: the parse completes and
70/70 field values (clk+v of entries[5..39]) are byte-exact with the
victim bytes at the exact OOB source positions β kernel-heap data past
soft_pp_table flows 1:1 into hwmgr->dyn_state.vddc_dependency_on_sclk,
which userland reads back via powerplay sysfs (pp_dpm_sclk / OD tables).
Stable across 3 runs.
Regression control (clean-valid): a fully valid table parses identically in both builds (rc=0, count=3, one clock-info callback).
3. Fix validation (A/B, identical crafted inputs)
rig_fixed = same verbatim slice + this pack's fix.diff (checker-proven
identity, bidirectional patch proof). All 10 fault modes: no fault β
oversized offsets skipped (rc=0), counts clamped to entries-that-fit
(255β3, 40β5), off-by-one indices rejected (>= guards, rc=-1), indices
clamped/bounds-checked (rc=-EINVAL / entry 0), fw_info NULL-checked (rc=0);
leak-canary: 0 out-of-image values; clean-valid: identical behavior.
Baseline corrupts, patched slice is clean on the same input β fix
validated (fix_validation.log). A rebuilt-kernel validation was not
performed: the fix sites execute in-kernel only on AMD ASICs absent from
this guest; per the verbatim-rig protocol the fixed slice was A/B-validated
in the same rig. fix.diff is authored against the pristine read-only
sys/ tree and was never applied there.
fix.diff corrections (found by this verification β the pre-verification
draft did not compile):
1. get_clock_voltage_limit empty-table guard used table->numEntries; the
member is ucNumEntries (pptable.h:531). Fixed.
2. Two func(...) calls in pp_tables_get_entry passed an integer as the
const void *clock_info argument (int-conversion; error on modern
compilers). Cast added. Fixed.
Both corrections are in this pack's fix.diff and are what the rig validated.
4. Primitive and impact
Primitive: attacker-directed source OOB reads out of the kernel-heap
PowerPlay image β u16 offsets reach +65535, u8 count loops read linearly
past the image (fully value- and length-controlled: every count byte and
every index byte is firmware-chosen), u8 indices Γ u8 entry sizes reach
+65025 β with the leaked bytes converted 1:1 into dyn_state clock/voltage
tables exposed to userland (kernel-heap info leak, proven byte-exact), plus
a NULL-deref kernel panic when the ROM lacks a FirmwareInfo table. Write
sinks are NOT in this file (destination allocations over-reserve,
8+12n β₯ 4+8n, layout mode) β the downstream fixed-array overflows
(DF-1166, DF-4000..4006, DF-4036..4041, DF-1416/1609) remain separate
sink-side fixes, as the finding's coverage assessment states.
Threat model (the family's established one): crafted/corrupted GPU VBIOS
(flashed ROM, vfio-pci romfile override, malicious discrete device) parsed
at driver attach, or root re-parsing a crafted blob through
/sys/class/drm/card0/device/pp_table β pp_dpm_set_pp_table β
amd_powerplay_reset β pptable_init (amd_powerplay.c:672-706). No
unprivileged userland trigger on stock hardware. Demonstrated out-of-kernel
context (rig); impact recorded as leak (the proven, realistic ceiling of
this file's defects β info leak + oops-capable NULL deref), not panic
(the guest kernel never executed the path).
5. Honest limitations
- Demonstrated with production instructions and production struct layouts, but in a userland mapping, not the kernel's kmalloc heap. The leak leg proves the source bytes are captured; the slab neighborhood contents in kernel context are the allocator's, not a controlled victim.
- The off-by-one modes (entry-v2/v1) require a caller passing
entry_index == ucNumEntries; the stock caller loop (pp_tables_get_num_of_entries β 0..N-1) stays in range, so those two are latent wrong-guard defects (defense-in-depth), demonstrated by direct call. - clockinfo-index-255's fault lands in the clock-info callback (harness mimics the smu7/smu8 deref); the pointer computation itself is verbatim.
- The crafted images are synthetic minimal tables; a real crafted ROM must also satisfy earlier ATOM ROM parsing stages, which precede and do not constrain these fields.
- Cross-file lead unchanged (out of scope for this file):
pp_dpm_set_pp_tablememcpy heap overflow at amd_powerplay.c:684-689.
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
fixedA/B in the same rig on identical crafted inputs: baseline (verbatim) faults 10/10 and leaks 70/70; slice+fix.diff is clean 10/10, clamps counts to entries-that-fit, rejects one-past indices, NULL-checks fw_info, leaks 0 values, and parses a valid table identically. Kernel-rebuild validation N/A on this guest (AMD-ASIC-gated path); fix.diff applies to the pristine tree with patch --fuzz=0 and reverse-applies byte-exactly.
fix_validation.log (full FIXED half of the decisive run); build.log (bidirectional patch proof lines); VERDICT.md section 3; the two compile corrections made to fix.diff during verification are listed in README.md.
Confirmed kernel references
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:61
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:383
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:784
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:827
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:875
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:913
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:923
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:929
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:937
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1062
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1099
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1122
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1279
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1304
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1379
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1473
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1520
- sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:1571
- sys/dev/drm/amd/powerplay/amd_powerplay.c:672
Detail
Exploit chain
crafted VBIOS PowerPlay table (flashed ROM / vfio-pci romfile / malicious device) -> amdgpu attach on smu7/smu8 -> pp_tables_initialize + pp_tables_get_entry/VCE getters -> OOB reads up to +65280 past the ~2-4KB soft_pp_table kernel heap image -> leaked bytes stored 1:1 in hwmgr->dyn_state clock/voltage tables -> readable by userland via pp_dpm_* sysfs (proven 70/70 byte-exact in rig); plus NULL-deref panic path when the ROM lacks FirmwareInfo. No unprivileged local trigger; no write primitive in this file.
Evidence (decisive lines)
run.log/run.2.log/run.3.log: 10/10 baseline SIGSEGVs classified with fault offsets (65280 vs 128; 146 vs 144; 142 vs 141; 136 vs 136; 65227 vs 242; 65187; 1662 vs 134; NULL+2) and 70/70 leak values byte-exact; fix_validation.log: FIXED half clean on identical inputs; build.log: ALL VERBATIM CHECKS PASSED (23 blocks vs /usr/src/sys) + both binaries; env.txt: guest uname, host<->guest SHA256 equality, no-AMD-GPU proof; rig/: gen_rig.sh/check_verbatim.sh/Makefile/pp_rig.c/rig_shims.h.
PoC changes
Replaced the hardware-gated 'untested' verdict with the DF-4165 verbatim-rig protocol: whole-file parser slice (37-1706) + 22 verbatim header slices, guard-page/canary harness with SIGSEGV classifier, 13 modes; built and run on the guest against /usr/src. Fixed two compile bugs in the pack's own fix.diff (numEntries->ucNumEntries; const void* casts). Removed the stale hand-written processpptables_fixed.c (superseded by the checker-proven generated fixed slice).
Verified recommended fix
central pp_table_range_ok/pp_table_clamp_count layer exactly as in fix.diff (this pack's corrected version), plus fw_info NULL check, '>'->'>=' off-by-one guards and get_state_entry_v2 '<='->'<'
Verdict
REPRODUCED via the DF-4165 verbatim-rig evidence class: the ENTIRE production parser (processpptables.c:37-1706, checker-proven byte-identical on the guest against /usr/src/sys, the tree its running kernel was built from) compiled into a harness and driven with crafted PowerPlay tables. 10/10 defect modes faulted at exactly the predicted offsets in verbatim code: u16 sub-table offsets dereferenced at base+0xFF00 (65280B past a 128B image), u8 entry-count copy loops reading linearly past the image, u8 record indices reaching +65025 (nonclock/clockinfo/uvd/vce), both one-past state-index guards ('>' at :913/:937, '<=' at :784), and the fw_info==NULL deref at :1066 (NULL+2). The leak leg is proven byte-exact: with ucNumEntries=40 and 5 records in image, 70/70 dyn_state field values equal the memory bytes past the table image -- kernel-heap info leak surfaced to userland via powerplay sysfs. Fix A/B-validated in the same rig: with fix.diff applied, all 10 modes are clean (offsets skipped, counts clamped to entries-that-fit, off-by-one rejected, indices clamped, NULL checked), 0 leaked values, and a fully valid table parses identically (no regression). Verification also found and fixed two compile bugs in the pre-verification fix.diff (numEntries->ucNumEntries at get_clock_voltage_limit; integer passed as const void* clock_info twice). In-kernel execution remains AMD-ASIC-gated on this guest (no 0x1002 device): threat model = crafted/corrupt VBIOS at driver attach or root-written pp_table sysfs re-parse; no unprivileged trigger on stock hardware. Recorded impact 'leak' (proven ceiling of this file's source-read defects; the downstream fixed-array write overflows are separate sink findings per the finding's coverage assessment).
No comments yet.