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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4247 Β· 21 files
FileTypeDescriptionSize
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.sh re-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 in env.txt).
  • The fixed build is the same slice with this pack's fix.diff applied via patch --fuzz=0 (bidirectionally proven: forward-apply + reverse-apply restores the pristine file byte-exactly).
  • Shims (rig/rig_shims.h) touch no defect path: identity le*_to_cpu, calloc kzalloc, minimized struct pp_hwmgr with production names/types for every field the fix consults, and a smu_atom_get_data_table shim 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-written pp_table sysfs 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.

VERDICT.md
↓ download raw

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.sh extracts processpptables.c:37-1706 β€” the entire parser body (all get_*_offset/size helpers, all copy helpers, pp_tables_get_num_of_entries/get_entry, the VCE-state getters, pp_tables_initialize, the pptable_funcs vtable) 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.sh re-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-backed kzalloc/kfree, a minimized struct pp_hwmgr carrying production names/types for every field the parser and the fix consult (soft_pp_table, soft_pp_table_size exactly as hwmgr.h:718-719), and a smu_atom_get_data_table shim 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 is init_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_table memcpy 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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/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.
↓ fix.diffper-fix-DF-4247

Confirmed kernel references

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