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)
PoC verification
Evidence pack
findings/poc/DF-3619 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_controlbecomeCISLANDS_VOLTAGE_CONTROL_BY_SVID2when 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 threeci_get_svi2_voltage_table()calls (2285/2303/2321) β the copy happens before theci_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.kopresent (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 withvm.sh reset with-srcafterward.)- 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 pastentries[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_testablefix.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
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.
No comments yet.