Wild OOB kernel read in btc_find_voltage when voltage-table count==0: entries[(u32)(count-1)] computes a ~31 GB wild index (0xffffffff)
Summary
btc_find_voltage() falls through to table->entries[table->count - 1].value when no entry matches; count is u32 and never checked for 0. count==0 is reachable: the VDDCI table stays empty when the VBIOS has neither VDDCI GPIO-LUT nor SVID2 object, and even the VDDC GPIO-LUT table can have count==0 (only >32 is rejected; the count==0 check exists only for mvdd). Index 0xffffffff -> read at entries+0x7fffffff8 -> guaranteed unmapped -> panic. Reached on every power-state recompute with BIOS voltages differing by > vddc_vddci_delta (default 200). VERIFIED: guest harness deterministic - 'index = 4294967295, byte offset = 0x7fffffff8 (~31 GB past the table)', child SIGSEGV => kernel-equivalent wild read -> page fault. Upstream-identical code. Crafted/sparse VBIOS on SI hardware; DoS only.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3587 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | poc | arithmetic replica of btc_find_voltage count==0 wild index | 3.1 KB | view raw |
| build.sh | build | guest build command | 112 B | view raw |
| run.sh | run | guest run command | 100 B | view raw |
| build.log | build-log | guest cc output | 147 B | view raw |
| run.log | run-log | decisive run 1 | 483 B | view raw |
| run.2.log | run-log | decisive run 2 (determinism) | 477 B | view raw |
| env.txt | env | guest uname + no-AMD-GPU evidence | 238 B | view raw |
| fix.diff | suggested-fix | count==0 guard in btc_find_voltage (git-apply-verified) | 503 B | view raw |
| VERDICT.md | verdict | full narrative | 3.3 KB | β raw |
| README.md | readme | human summary | 2.8 KB | β raw |
DF-3587 β Wild OOB kernel read in btc_find_voltage() when voltage table count == 0
File: sys/dev/drm/amd/amdgpu/si_dpm.c
Lines: 3206-3215 (sink), reached from 3312-3334 (btc_apply_voltage_delta_rules)
and 3626-3631 (si_apply_state_adjust_rules)
Severity: Medium (crafted-VBIOS kernel panic / wild read)
Verification: source-line proof + arithmetic replica harness on the guest
(kernel path itself HW-gated: no AMD GPU on the verification guest).
What this is
static u16 btc_find_voltage(struct atom_voltage_table *table, u16 voltage) /* si_dpm.c:3206 */
{
unsigned int i;
for (i = 0; i < table->count; i++)
if (voltage <= table->entries[i].value)
return table->entries[i].value;
return table->entries[table->count - 1].value; /* :3214 -- count==0 -> 0xffffffff */
}
struct atom_voltage_table.count is a u32; with count == 0 the index
table->count - 1 wraps to 0xFFFFFFFF, so the kernel evaluates
&entries[0] + 4294967295 * 8 β a ~31 GB wild offset β and reads a u16
from it: guaranteed unmapped address => page fault => panic.
How count can be 0 (all BIOS-controlled)
eg_pi->vddci_voltage_tableis only filled whenvddci_control(GPIO LUT object present,si_dpm.c:4446-4456) orvddci_control_svi2(:4457-4463). With neither VBIOS object,countstays 0 whilesi_construct_voltage_tables()still succeeds (VDDC control alone is sufficient) and DPM enables normally.- Even the filled VDDC table can have
count == 0:amdgpu_atombios_get_voltage_table(amdgpu_atombios.c:1539-1551) only rejectsucGpioEntryNum > 32β a GPIO LUT object advertising 0 entries yieldscount = 0and success. (si_construct_voltage_tablescheckscount == 0only for mvdd atsi_dpm.c:4474-4477β not for vddc/vddci.)
Trigger path (after dpm enable, on every power-state recompute)
amdgpu_pm_compute_clocks
-> si_dpm_pre_set_power_state (si_dpm.c:6935)
-> si_apply_state_adjust_rules (si_dpm.c:3425)
-> btc_apply_voltage_delta_rules (si_dpm.c:3626-3631)
-> btc_find_voltage(&eg_pi->vddci_voltage_table|vddc_voltage_table, ...) (:3324/:3330)
with BIOS power-state usVDDC/usVDDCI values such that
(vddc - vddci) > vddc_vddci_delta (default 200, si_dpm.c:7447) and neither
voltage is 0 (si_dpm.c:3319 guard).
How to reproduce (arithmetic replica)
cc -O2 -Wall -o /tmp/df3587_harness /tmp/df3587_harness.c # on the DragonFly guest /tmp/df3587_harness
Expected: index 4294967295, byte offset 0x7fffffff8, child SIGSEGV on the
wild read (run.log, deterministic over 2 runs).
Fix
fix.diff β return 0 when count == 0 (voltage-delta rule then degrades to the
max_vddc/max_vddci clamp already present in btc_apply_voltage_delta_rules).
DF-3587 Verification Verdict
Severity: Medium β crafted-VBIOS wild kernel read / panic (CWE-125 / CWE-191)
Verification method: source-line proof + arithmetic replica on the DragonFly
guest; kernel path HW-gated (no AMD GPU on guest) => inconclusive at kernel
level, certain at source level.
Proof
-
Sink:
sys/dev/drm/amd/amdgpu/si_dpm.c:3214return table->entries[table->count - 1].value;withcountau32that is never checked against 0 inbtc_find_voltage()(si_dpm.c:3206-3215).count == 0-> index0xFFFFFFFF-> addressentries + 0x7FFFFFFF8(~31 GB). -
count == 0 is reachable with a crafted (or merely sparse) VBIOS: * VDDCI: no GPIO-LUT and no SVID2 VDDCI object -> table never filled (
si_construct_voltage_tables, si_dpm.c:4446-4463 β vddci is optional; only VDDC control is mandatory,:4426-4444). * VDDC: GPIO LUT object withucGpioEntryNum == 0is accepted (amdgpu_atombios.c:1539-1540only rejects> 32,:1551setscount = ucGpioEntryNum). Thecount == 0sanity check exists only for the mvdd table (si_dpm.c:4474-4477), not for vddc/vddci. -
Call chain:
si_apply_state_adjust_rules(si_dpm.c:3626-3631) runs on every power-state recompute (si_dpm_pre_set_power_state, si_dpm.c:6943) and callsbtc_apply_voltage_delta_rules(si_dpm.c:3312) whenever the BIOS-supplied level voltages differ by more thanvddc_vddci_delta(200 mV default,si_dpm.c:7447);:3324(vddci table) or:3330(vddc table) then hits the uncheckedentries[count-1]. -
Impact: single wild
u16read from a non-canonical-for-KVA-distance address; no data is returned to the attacker (value feeds a voltage clamp). Result: kernel page fault -> panic (local DoS at driver-load/resume/state-change time, gated on crafted VBIOS contents).
Harness (guest, DragonFly 6.5-DEVELOPMENT, base gcc)
[2] index = (u32)(count - 1) = 4294967295 [2] byte offset = 4294967295 * 8 = 0x7fffffff8 (~31 GB past the table) [3] child killed by SIGSEGV => kernel-equivalent wild read -> page fault [4] RESULT: UNCHECKED count==0 -> WILD OOB READ CONFIRMED (kernel: panic)
Deterministic across 2 runs (run.log, run.2.log).
Why not kernel-level on this guest
No AMD PCI device (pciconf -l: virtio only; 0 devices with vendor 0x1002);
amdgpu.ko loads but no SI asic attaches, so si_dpm_* never executes.
Fix validation
fix.diff: if (table->count == 0) return 0; before the entries[count-1]
access. Logic-validated (the wrap is prevented; callers already clamp the result
to max_vddc/max_vddci, si_dpm.c:3326/3332). Patch is git-apply-verified
against the read-only sys/ tree (never applied there). Kernel build not
repeated this run (guest budget); change is a 3-line pure guard.
Verdict
inconclusive at kernel level (HW absent on verification guest);
source-certain wild-read defect; recommended one-line guard as in fix.diff.
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
inconclusiveBaseline replica reproduces the wild read (SIGSEGV); the fix.diff guard (return 0 for count==0) prevents the wrap - callers already clamp the result to max_vddc/max_vddci (si_dpm.c:3326/3332). Kernel-level validation impossible on this guest; patch is git-apply-verified against the read-only sys/ tree.
findings/poc/DF-3587/fix.diff; run.log vs fix logic (VERDICT.md 'Fix validation')
Confirmed kernel references
Detail
Exploit chain
crafted VBIOS (no VDDCI voltage object, or GPIO LUT with 0 entries; power states with |vddc-vddci| > 200 mV) -> first power-state recompute -> btc_apply_voltage_delta_rules -> btc_find_voltage(count==0) -> wild read at entries+0x7fffffff8 -> kernel panic.
Evidence (decisive lines)
run.log/run.2.log: 'index = 4294967295', 'byte offset ... 0x7fffffff8 (~31 GB past the table)', 'child killed by SIGSEGV', 'WILD OOB READ CONFIRMED'; VERDICT.md: full path:line chain; env.txt: no AMD PCI devices on guest.
PoC changes
no orchestrator seed; harness written from scratch (single iteration, compiled clean)
Verified recommended fix
Guard count==0 in btc_find_voltage before the entries[count-1] access (see fix.diff).
Verdict
Source-certain wild OOB read: btc_find_voltage (si_dpm.c:3214) returns table->entries[table->count - 1].value without checking count; count is a u32 that stays 0 for the VDDCI table when the VBIOS has neither a VDDCI GPIO-LUT nor SVID2 object (si_dpm.c:4446-4463 - vddci is optional while only VDDC control is mandatory), and even the filled VDDC GPIO-LUT table can legitimately end up with count==0 because amdgpu_atombios_get_voltage_table only rejects ucGpioEntryNum > 32 (amdgpu_atombios.c:1539-1540) and the count==0 sanity check exists only for mvdd (si_dpm.c:4474-4477). With count==0 the index wraps to 0xffffffff -> kernel reads entries + 0x7fffffff8 (~31 GB) -> guaranteed unmapped -> page fault/panic. Reached on every power-state recompute after dpm enable via si_apply_state_adjust_rules (3626-3631) -> btc_apply_voltage_delta_rules (3324/3330) whenever BIOS-supplied level voltages differ by more than vddc_vddci_delta (default 200, si_dpm.c:7447). Guest harness (verbatim loop replica) deterministically shows the wild index and SIGSEGV across 2 runs. Kernel-level reproduction impossible on the verification guest (no AMD GPU, virtio-only). No data is returned to userspace - panic/DoS class.
No comments yet.