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

Heap OOB write in ci_get_svi2_voltage_table: VBIOS dependency-table count (u8, up to 255) copied into fixed entries[32] before any trim

Summary

ci_get_svi2_voltage_table() copies voltage_dependency_table->count entries into voltage_table->entries[MAX_VOLTAGE_ENTRIES=32] with no bound check. The count originates from a VBIOS u8 via amdgpu_parse_clk_voltage_dep_table() and is never capped at 32; the SMU7_MAX_LEVELS trims in ci_construct_voltage_tables() run after the copy. The three voltage tables sit at the tail of the kzalloc'd struct ci_power_info: the vddc/vddci variants overwrite subsequent in-object members (vddci/mvdd tables, ulv, power_containment_features, the powertune_defaults POINTER, caps, current_ps/requested_ps driver state); the mvdd (and vddci) variants run past the end of the heap allocation itself. Guest-executed geometry proof with exact struct layouts: OOB past entries[32] begins at count=33; past the END of the ci_power_info allocation at count>=149 (vddc) / >=116 (vddci) / >=82 (mvdd); count=255 writes 1784 bytes past entries[32], up to 1388 bytes past the object; write pattern = attacker u16 per 8-byte slot plus forced u32 zero (powertune_defaults forged to 0x0000beef00000000 in the live guard test; wild deref later in ci_populate_svi_load_line). Crafted VBIOS on Sea Islands/Bonaire/Hawaii (flash swap, VFIO passthrough ROM override, supply chain). Inconclusive at runtime (virtio-only guest).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3619 Β· 8 files
FileTypeDescriptionSize
ci_svi2_overflow.c β€” 8.6 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 116 B view raw
build.log β€” 76 B view raw
run.log β€” 1.1 KB view raw
env.txt β€” 547 B view raw
VERDICT.md β€” 4.6 KB ↓ raw
fix.diff β€” 326 B view raw
VERDICT.md
↓ download raw

DF-3619 VERDICT β€” Heap OOB write in ci_get_svi2_voltage_table

Bottom line

Real bug, source-certain; runtime inconclusive on this guest (no AMD GPU).

The sink is ci_get_svi2_voltage_table() at sys/dev/drm/amd/amdgpu/ci_dpm.c:2264-2268:

voltage_table->count = voltage_dependency_table->count;      /* u8 from VBIOS, ≀255 */
for (i = 0; i < voltage_table->count; i++) {
        voltage_table->entries[i].value = voltage_dependency_table->entries[i].v;
        voltage_table->entries[i].smio_low = 0;
}

entries[] is fixed at MAX_VOLTAGE_ENTRIES == 32 (sys/dev/drm/amd/amdgpu/amdgpu_atombios.h:119-133). The count comes from amdgpu_parse_clk_voltage_dep_table() (sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285-302), which trusts the u8 ucNumEntries from the VBIOS PowerPlay table β€” up to 255, fully attacker-chosen in the crafted-VBIOS model. The GPIO-LUT path (amdgpu_atombios_get_voltage_table, amdgpu_atombios.c:1539-1540) does bound-check > MAX_VOLTAGE_ENTRIES; the SVI2 path does not β€” that asymmetry is the bug.

Reachability (traced)

  • ci_dpm_init() (ci_dpm.c:6017-6041): voltage_control/vddci_control/mvdd_control become CISLANDS_VOLTAGE_CONTROL_BY_SVID2 when the VBIOS has SVID2 voltage objects β€” attacker includes them.
  • ci_dpm_hw_init() β†’ ci_dpm_enable() (ci_dpm.c:5300-5307) β†’ ci_construct_voltage_tables() (ci_dpm.c:2273-2333) β†’ the three ci_get_svi2_voltage_table() calls (2285/2303/2321) β€” the copy happens before the ci_trim_voltage_table_to_fit_state_table() calls (2292-2294 / 2310-2312 / 2328-2330) that reduce the count to SMU7_MAX_LEVELS_{VDDC,VDDCI,MVDD} = 8/4/4. Trims cannot undo the overflow.
  • Also re-entered on every resume (ci_dpm_resume β†’ ci_dpm_enable).

Overflow geometry (proven by executing the exact struct layouts on the guest)

struct ci_power_info tail (ci_dpm.h:230-297): the three 268-byte atom_voltage_tables are followed by ulv, pointers, caps, and driver-state structs (392 more bytes to the end of the object).

variant OOB past entries[32] starts at past END of ci_power_info alloc max bytes past object (count=255)
vddc (ci_dpm.c:2285) count β‰₯ 33 count β‰₯ 149 852
vddci (ci_dpm.c:2303) count β‰₯ 33 count β‰₯ 116 1120
mvdd (ci_dpm.c:2321) count β‰₯ 33 count β‰₯ 82 1388

Write pattern per 8-byte slot: u16 attacker value at +0 (VBIOS usVoltage), u32 0 at +4. Clobbers in-object members including the powertune_defaults pointer (forged to 0x0000beef00000000 in the live guard test β†’ wild pointer deref in ci_populate_svi_load_line, ci_dpm.c:445-448), and past the allocation into adjacent heap for the vddci/mvdd variants.

Exploit chain (why this matters beyond a panic)

In the crafted-VBIOS threat model the attacker additionally controls heap grooming timing at driver bind (kzalloc order is deterministic during amdgpu init). A count in the 82-255 range yields a linear heap overwrite of up to ~1.4 KB with 2 controlled bytes per 8 β€” sufficient to corrupt adjacent allocations' length fields/pointers toward UAF/arb-write, though the u32-zero-every-8-bytes pattern makes it markedly harder than a dense overwrite. At minimum: reliable kernel memory corruption / panic at boot.

Verification narrative

  • Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64; virtio-only; vgapci0 chip=0x11111234 (ramfb) β€” no AMD ASIC β†’ ci_dpm unreachable at runtime β†’ status inconclusive (not false-positive).
  • /boot/kernel/amdgpu.ko present (124,519,320 bytes) and loads cleanly ([drm] amdgpu kernel modesetting enabled.) β€” module-presence check done. (Unloading the 124 MB module wedges the DragonFly linker; guest was reset with vm.sh reset with-src afterward.)
  • The logic/geometry PoC (ci_svi2_overflow.c) replicates the exact member sequence of ci_dpm.h:230-297 + amdgpu_atombios.h structs and the exact sink loop, compiled and executed on the guest (build.log/run.log): confirms 1784 bytes past entries[32], past-object thresholds 149/116/82, and 48/64 guard bytes corrupted behind the allocation in the live run.

Fix validation

Not possible on this guest (hardware absent): fix_status: not_testable. fix.diff adds the same > MAX_VOLTAGE_ENTRIES β†’ -EINVAL bound the GPIO path already has.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored (MAX_VOLTAGE_ENTRIES bound, mirroring amdgpu_atombios_get_voltage_table:1539-1540) but not kernel-buildable/testable: guest lacks the AMD hardware required to reach ci_dpm_enable.

fix.diff; baseline/patched kernel runs impossible on virtio-only guest
↓ fix.diffper-fix-DF-3619

Confirmed kernel references

Detail

Exploit chain

crafted VBIOS with VOLTAGE_OBJ_SVID2 + dependency count>32 -> ci_dpm_enable -> ci_construct_voltage_tables -> linear heap overwrite (u16 controlled per 8 bytes) of ci_power_info tail incl. powertune_defaults pointer -> wild deref in ci_populate_svi_load_line (panic) or adjacent-heap corruption toward UAF; full uid0 chain not developed (hardware absent on guest).

Evidence (decisive lines)

run.log lines showing '1784 bytes past entries[32]', per-variant past-object thresholds, and live run 'guard bytes corrupted behind object: 48' + 'powertune_defaults now = 0x0000beef00000000'; build.log clean cc build on the DragonFly guest; env.txt module/GPU facts.

PoC changes

authored from scratch (no seed); replicated ci_dpm.h:230-297 member sequence and the sink loop verbatim; added stdlib.h after first guest build warning

Verified recommended fix

Reject dependency tables larger than the fixed voltage table: if (voltage_dependency_table->count > MAX_VOLTAGE_ENTRIES) return -EINVAL; in ci_get_svi2_voltage_table (matches the existing GPIO-path bound in amdgpu_atombios_get_voltage_table).

Verdict

Source-certain heap OOB write: ci_get_svi2_voltage_table (ci_dpm.c:2264-2268) copies a VBIOS-controlled u8 count (<=255, from amdgpu_parse_clk_voltage_dep_table, amdgpu_dpm.c:285-302) into struct atom_voltage_table.entries[32] with no bound check; the SMU7 trims in ci_construct_voltage_tables run after the copy. Geometry proven on the guest with the exact struct layouts: OOB begins at count=33; the mvdd variant exits the ci_power_info allocation at count>=82 and writes up to 1388 bytes past it (vddc 852 / vddci 1120 at count=255), clobbering the powertune_defaults pointer, caps, and driver-state structs. Runtime verdict inconclusive: the QEMU guest has no AMD ASIC (ramfb 0x1234:1111 only) so ci_dpm cannot execute; amdgpu.ko itself is present and loads. Threat model: crafted VBIOS (flash swap / VFIO ROM override), same as DF-1141/DF-1142 and the si_dpm twin DF-3585.