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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3587 Β· 10 files
FileTypeDescriptionSize
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
README.md readme human summary
↓ download 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_table is only filled when vddci_control (GPIO LUT object present, si_dpm.c:4446-4456) or vddci_control_svi2 (:4457-4463). With neither VBIOS object, count stays 0 while si_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 rejects ucGpioEntryNum > 32 β€” a GPIO LUT object advertising 0 entries yields count = 0 and success. (si_construct_voltage_tables checks count == 0 only for mvdd at si_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).

VERDICT.md verdict full narrative
↓ download raw

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

  1. Sink: sys/dev/drm/amd/amdgpu/si_dpm.c:3214 return table->entries[table->count - 1].value; with count a u32 that is never checked against 0 in btc_find_voltage() (si_dpm.c:3206-3215). count == 0 -> index 0xFFFFFFFF -> address entries + 0x7FFFFFFF8 (~31 GB).

  2. 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 with ucGpioEntryNum == 0 is accepted (amdgpu_atombios.c:1539-1540 only rejects > 32, :1551 sets count = ucGpioEntryNum). The count == 0 sanity check exists only for the mvdd table (si_dpm.c:4474-4477), not for vddc/vddci.

  3. 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 calls btc_apply_voltage_delta_rules (si_dpm.c:3312) whenever the BIOS-supplied level voltages differ by more than vddc_vddci_delta (200 mV default, si_dpm.c:7447); :3324 (vddci table) or :3330 (vddc table) then hits the unchecked entries[count-1].

  4. Impact: single wild u16 read 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

inconclusive
baseline reproduced→ patch + rebuild →patched clean

Baseline 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')
↓ fix.diffper-fix-DF-3587

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.