Heap OOB write in si_get_svi2_voltage_table (and lowSMIO sink in si_populate_smc_voltage_table): BIOS dependency-table count (u8, up to 255) copied into fixed entries[32]/lowSMIO[32] arrays
Summary
si_construct_voltage_tables() builds VDDC/VDDCI tables from SVI2 objects by copying a clock/voltage dependency table whose count is a raw VBIOS u8 (0..255) with no clamp, writing entries[i] for i<count into struct atom_voltage_table.entries[MAX_VOLTAGE_ENTRIES=32] embedded in the single kzalloc'd si_power_info -> up to 1784 bytes of heap OOB write with BIOS-chosen u16 values at 8-byte stride; the same unclamped count later drives table->lowSMIO[i] (fixed 32) in si_populate_smc_voltage_table, overrunning the SISLANDS_SMC_STATETABLE and the si_power_info allocation end for i>=~154. First OOB slot lands exactly on eg_pi->vddci_voltage_table.count (corrupted with an attacker u16), cascading OOB into every later consumer; current_rps.ps_priv/requested_rps.ps_priv pointer slots partially overwritten. GPIO-LUT route is bounded - only the SVI2 route is unbounded. Inherited verbatim from the Linux amdgpu sync. VERIFIED: logic-replica harness on the guest (verbatim structs+loops, deterministic 2+ runs): count=255 -> first OOB slot corrupts vddci count to 4128, 446 non-zero bytes rewritten past entries[32], SIGSEGV at lowSMIO[32], fix clamp -> zero corruption. Threat: crafted VBIOS on SI (Tahiti/Pitcairn/Verde/Oland/Hainan) - fires at boot/module-load/resume during si_dpm_enable, before any power-state validation. Kernel-level chain not exercisable on virtio-only guest.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3585 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | poc | logic replica with verbatim struct defs + verbatim loops, canary/guard instrumentation, fix-validation part | 12.6 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 (CC_RC=0) + uname | 158 B | view raw |
| run.log | run-log | decisive run 1 (full, untrimmed) | 1.4 KB | view raw |
| run.2.log | run-log | decisive run 2 (determinism) | 1.4 KB | view raw |
| fix_run.log | fix-log | baseline vs patched-logic harness output | 1.7 KB | view raw |
| fix_build.log | build-log | same build as build.log | 158 B | view raw |
| env.txt | env | guest uname, PCI inventory (no AMD GPU), amdgpu.ko load proof, serial log with harness SIGSEGVs | 1.7 KB | view raw |
| fix.diff | suggested-fix | clamp BIOS count in si_get_svi2_voltage_table + si_populate_smc_voltage_table (git-apply-verified) | 1.0 KB | view raw |
| VERDICT.md | verdict | full narrative | 5.8 KB | β raw |
| README.md | readme | human summary | 4.2 KB | β raw |
DF-3585 β Heap OOB write in si_get_svi2_voltage_table() + si_populate_smc_voltage_table() (SVI2 voltage tables)
File: sys/dev/drm/amd/amdgpu/si_dpm.c
Lines: 4410-4414 (primary sink), 4505-4506 (secondary sink), reached via 4436-4441 / 4457-4463
Severity: High (kernel heap memory corruption, crafted-VBIOS gated)
Verification: logic-replica harness on the DragonFly guest (guest has no AMD GPU β
hardware trigger not testable); source-level certainty.
What this is
si_construct_voltage_tables() builds the VDDC/VDDCI voltage tables either from a
GPIO LUT atom object (bounded: amdgpu_atombios.c:1539 rejects ucGpioEntryNum > 32)
or, on SVI2 platforms, by copying a clock/voltage dependency table whose count
is a raw BIOS u8 with no clamp:
si_dpm.c:4410voltage_table->count = voltage_dependency_table->count;si_dpm.c:4411-4414loop writesentries[i]fori < count
voltage_dependency_table->count comes from
amdgpu_parse_clk_voltage_dep_table() (amdgpu_dpm.c:285,302) =
ucNumEntries straight from the VBIOS PowerPlayInfo
VddcDependencyOnMCLK / VddciDependencyOnMCLK tables (0..255).
The destination struct atom_voltage_table.entries[] is a fixed 32-entry array
(MAX_VOLTAGE_ENTRIES, amdgpu_atombios.h:119-133) embedded in the single
kzalloc'd struct si_power_info (si_dpm.c:7311).
=> up to (255-32) * 8 = 1784 bytes of heap OOB write with BIOS-chosen 16-bit
values at 8-byte strides (each slot: value u16 + 2 stale pad bytes + smio_low=0).
- First OOB slot (
entries[32]) lands exactly oneg_pi->vddci_voltage_table.count-> corrupted with a BIOS-chosen u16, cascading into further OOB in every later consumer (si_populate_voltage_value,btc_find_voltage, ...). - Overflow smashes
bootup_arb_registers,ulv,ats,current_rps.ps_priv/requested_rps.ps_priv(kernel pointer slots) and everything after, inside thesi_power_infoallocation.
Second sink, same root cause: si_populate_smc_voltage_table() (si_dpm.c:4505-4506)
does table->lowSMIO[i] |= entries[i].smio_low for i < count; lowSMIO[] is fixed
SISLANDS_MAX_NO_VREG_STEPS = 32 (sislands_smc.h:207,215) inside
SISLANDS_SMC_STATETABLE smc_statetable β with an SVI2 VDDCI table (mixed
GPIO-VDDC + SVI2-VDDCI config, all BIOS-advertised) the loop runs to count = up to
255, doing an OOB read-modify-write across the statetable and (for i >= ~154) past
the end of the si_power_info heap allocation.
Reachability
Crafted VBIOS on an SI (Tahiti/Pitcairn/Verde/Oland/Hainan) card:
1. no VOLTAGE_OBJ_GPIO_LUT VDDC object + a VOLTAGE_OBJ_SVID2 VDDC object
(si_dpm_init, si_dpm.c:7390-7399) -> si_pi->voltage_control_svi2 = true;
2. VddcDependencyOnMCLK table with ucNumEntries > 32 (any u8);
3. on si_dpm_enable() (boot / module load / resume) si_construct_voltage_tables()
(si_dpm.c:6792-6798 -> 4436-4441) fires the overflow before any other
power-state validation.
Inherited verbatim from the Linux amdgpu sync (KMS 3.27-era, Linux ~4.5/4.6); the
same unclamped loops exist upstream.
How to reproduce (logic replica β guest has no AMD GPU)
# on the DragonFly guest (files transferred to /tmp): cc -O2 -Wall -o /tmp/df3585_harness /tmp/df3585_harness.c /tmp/df3585_harness
Expected (see run.log, deterministic across runs):
* Part A: OOB WRITE CONFIRMED β 446 non-zero bytes rewritten past entries[32],
vddci_voltage_table.count becomes 4128 (0x1020, the BIOS u16 we planted at
dependency entry 32).
* Part B: child SIGSEGV β the same stores leave the object's mapping.
* Part C: child SIGSEGV at lowSMIO[32] β second sink is OOB.
* Part D: with the fix.diff clamp β table rejected, no corruption.
Artifacts
harness.cβ replica with verbatim struct defs (amdgpu_atombios.h:119-133, sislands_smc.h:207-224) and verbatim loops (si_dpm.c:4407-4414, 4503-4506)build.sh,run.shbuild.log,run.log,run.2.logβ full untrimmed guest outputenv.txtβ guest uname, PCI inventory (virtio only, no AMD GPU), amdgpu.ko present + loads (no si_dpm attach), dmesg showing harness SIGSEGV childrenfix.diffβ git-apply-able fix (clamp both construction and population)VERDICT.md,manifest.json,verdict.json
DF-3585 Verification Verdict
Severity: High β kernel heap OOB write (crafted-VBIOS gated)
Verification method: source-line proof + deterministic logic-replica harness run
on the DragonFly QEMU guest (guest has no AMD GPU β virtio-only; the kernel code
path cannot be driven by real hardware on this guest, so this is inconclusive at
the kernel level per the honest-status mapping, while the memory-safety defect itself
is certain at the source level and demonstrated at the logic level).
Source-level proof chain
-
Attacker-controlled count, no clamp:
sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285βucNumEntries(raw VBIOS u8) sizes the dependencyentries(kcalloc) and:302setscount = ucNumEntries(0..255). No upper clamp anywhere between parse and use. -
Sink 1 (write, controlled data):
sys/dev/drm/amd/amdgpu/si_dpm.c:4410βvoltage_table->count = voltage_dependency_table->count;then:4411-4414writesvoltage_table->entries[i]fori < count.struct atom_voltage_table.entriesis fixed[MAX_VOLTAGE_ENTRIES=32](sys/dev/drm/amd/amdgpu/amdgpu_atombios.h:119-133), embedded instruct evergreen_power_info(si_dpm.h:668) inside the singlekzalloc(sizeof(struct si_power_info))(si_dpm.c:7311). => up to 1784 bytes OOB write, each 8-byte slot carrying a BIOS u16 (entries[i].vof the dependency table) plus zeros. -
Reachability gating (all VBIOS-controlled): *
si_dpm_initsi_dpm.c:7389-7399:voltage_control(GPIO LUT) false,voltage_control_svi2(SVID2 object present) true; *si_dpm_enablesi_dpm.c:6788-6798->si_construct_voltage_tables(); *si_dpm.c:4436-4441(VDDC SVI2) or:4457-4463(VDDCI SVI2). Fired at boot / module load / resume β before any power-state sanity check. -
Sink 2 (R-M-W):
si_dpm.c:4505-4506table->lowSMIO[i] |= ...fori < count,lowSMIO[32]fixed (sislands_smc.h:207,215); with a mixed GPIO-VDDC/SVI2-VDDCI config (si_dpm.c:4538-4543) the vddci table count (up to 255) drives OOB accesses acrossSISLANDS_SMC_STATETABLEand past thesi_power_infoallocation end fori >= ~154(statetable sits at the tail of the allocation,si_dpm.h:1002-1012). -
First-hop corruption detail (harness-verified): the first OOB slot (
entries[32]) lands exactly oneg_pi->vddci_voltage_table.count, replacing it with the BIOS-chosen u16 (demo: 0x1020). Every later consumer of the vddci table (si_populate_voltage_valuesi_dpm.c:4578-4584,btc_find_voltagesi_dpm.c:3210-3214,si_populate_smc_voltage_table4505) then walks an attacker-sized table β compounding OOB reads/writes.current_rps.ps_priv/requested_rps.ps_privpointer slots (si_dpm.h:675-678) sit ~540 bytes past the overflow origin and are partially overwritten (2 controlled bytes + 4 zeroed) -> subsequentsi_get_ps()returns a corrupted pointer.
Harness result (DragonFly 6.5-DEVELOPMENT guest, base gcc)
[A] ... vddci_voltage_table.count -> now 4128 (BIOS-chosen u16 0x1020) [A] bytes of the heap object rewritten past entries[32] = 446 ... OOB WRITE CONFIRMED [B] child killed by SIGSEGV => the i>=32 stores leave the object mapping [C] child killed by SIGSEGV at lowSMIO[32] => si_dpm.c:4505-4506 sink is OOB [D] with fix.diff clamp: ret=-1 ... FIX VALIDATED at harness level (no OOB writes) summary: A=1 B=1 C=1 (1=confirmed) D_fix_ok=1
Deterministic across 2+ runs (run.log, run.2.log). The guest serial log
(env.txt) independently shows the harness children dying on signal 11.
Why not a kernel-level PoC on this guest
pciconf -l: only QEMU/virtio devices; the sole VGA is vgapci0 chip=0x11111234
(Bochs/virtio). amdgpu.ko exists in /boot/kernel and loads
([drm] amdgpu kernel modesetting enabled), but no SI device exists, so
si_dpm_* never executes. Driving the real path requires an SI card whose VBIOS
PowerPlayInfo tables are attacker-crafted (malicious/flashed VBIOS or a passed-
through device with a custom ROM image).
Exploitability assessment
- Write primitive: length fully BIOS-controlled (33..255 entries), content = 16-bit BIOS values at 8-byte stride with 4-byte zeroing β enough to corrupt adjacent heap-object fields (demonstrated: vddci.count, pointer slots).
- Placement: corruption is intra-
si_power_infofor the primary sink; sink 2 and the count-cascade can reach past the allocation. - Ceiling on realism: requires crafted VBIOS (physical / malicious-card / flashed image), same threat model as the pass-1 family (DF-1127/1128/1130). No unprivileged-local software-only trigger exists.
Fix validation
fix.diff clamps both the construction (si_get_svi2_voltage_table) and the
population (si_populate_smc_voltage_table) to MAX_VOLTAGE_ENTRIES /
SISLANDS_MAX_NO_VREG_STEPS. Harness part D: with the clamp, the oversized BIOS
table is rejected (-EINVAL) and zero corruption occurs. A full kernel build was
not repeated for this fix (guest kernel build budget); the patch is 2 two-line
guards with no type/control-flow changes, and mirrors the existing bound checks at
si_dpm.c:4432-4435 (trim path) and amdgpu_atombios.c:1539-1540.
Verdict
inconclusive at kernel level (no SI hardware on the verification guest);
certain at source level β the unclamped BIOS-controlled count flows into two
fixed-size kernel heap arrays and the OOB write arithmetic is deterministically
reproduced by the logic replica. Recommended fix 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 harness reproduces the OOB writes (A/B/C); with the fix.diff clamp applied the oversized BIOS table is rejected (-EINVAL) and zero corruption occurs (harness part D, fix_run.log). Kernel-level build/run validation not possible on this guest (no SI hardware); patch is two pure bounds guards, git-apply-verified against the read-only sys/ tree.
findings/poc/DF-3585/fix.diff; fix_run.log (baseline OOB CONFIRMED vs patched FIX VALIDATED)
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/si_dpm.c:4410-4414
- sys/dev/drm/amd/amdgpu/si_dpm.c:4505-4506
- sys/dev/drm/amd/amdgpu/si_dpm.c:4436-4441
- sys/dev/drm/amd/amdgpu/si_dpm.c:6792-6798
- sys/dev/drm/amd/amdgpu/amdgpu_atombios.h:119-133
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285-302
- sys/dev/drm/amd/amdgpu/sislands_smc.h:207-215
Detail
Exploit chain
crafted VBIOS (SVID2 VDDC object + VddcDependencyOnMCLK ucNumEntries>32) -> si_dpm_enable at boot/module-load/resume -> si_construct_voltage_tables -> si_get_svi2_voltage_table heap OOB write of up to 1784 bytes with BIOS u16 values at 8-byte stride -> vddci_voltage_table.count overwritten with attacker u16 (cascading OOB in every table consumer) + pointer slots (current_rps.ps_priv/requested_rps.ps_priv) partially overwritten (2 controlled bytes + 4 zeroed) -> subsequent si_get_ps() deref of corrupted pointer / lowSMIO sink overruns the si_power_info allocation end for count>=~154 -> kernel memory corruption; on this virtio-only guest no further escalation was reachable (no target hardware).
Evidence (decisive lines)
run.log/run.2.log: '[A] first OOB slot i=32 lands at vddci_voltage_table.count -> now 4128 (BIOS-chosen u16 0x1020)', '[A] OOB WRITE CONFIRMED', '[B] child killed by SIGSEGV => the i>=32 stores leave the object mapping', '[C] child killed by SIGSEGV at lowSMIO[32]', '[D] FIX VALIDATED at harness level'; env.txt: no AMD PCI devices, amdgpu.ko loads without si_dpm attach; VERDICT.md: full path:line proof chain.
PoC changes
no orchestrator seed existed (pass-2 new finding); harness written from scratch with verbatim struct/loop copies; two iterations to fix harness instrumentation (canary placement -> pristine-diff, lowSMIO demo -> guard page because the OR operand is 0 for SVI2 tables).
Verified recommended fix
Clamp the BIOS-supplied dependency-table count to MAX_VOLTAGE_ENTRIES in si_get_svi2_voltage_table and to SISLANDS_MAX_NO_VREG_STEPS in si_populate_smc_voltage_table (see fix.diff).
Verdict
Source-certain heap OOB write: si_get_svi2_voltage_table (si_dpm.c:4410-4414) copies a BIOS dependency-table count (raw u8, up to 255, amdgpu_dpm.c:285/302) into the fixed entries[MAX_VOLTAGE_ENTRIES=32] array inside the single kzalloc'd si_power_info, and si_populate_smc_voltage_table (si_dpm.c:4505-4506) drives the same unclamped count past the fixed lowSMIO[32] (sislands_smc.h:207-215). Reached from si_dpm_enable -> si_construct_voltage_tables (si_dpm.c:6792-6798, 4436-4441/4457-4463) whenever the VBIOS advertises SVID2 voltage objects. A deterministic logic-replica harness on the DragonFly guest (same struct layouts, verbatim loops) shows 1784 bytes of OOB writes for count=255, the first OOB slot landing exactly on vddci_voltage_table.count (replaced with a BIOS-chosen u16), stores leaving the object mapping (guard-page SIGSEGV), the lowSMIO sink OOB (guard-page SIGSEGV), and the proposed fix eliminating all corruption. Kernel-level reproduction impossible on this guest: pciconf shows zero AMD (0x1002) devices (virtio-only); amdgpu.ko loads but no SI asic attaches, so si_dpm never executes. Triggering requires crafted VBIOS on SI hardware (malicious card / flashed ROM), same threat model as pass-1 findings DF-1127/1128/1130; inherited verbatim from the Linux amdgpu sync.
No comments yet.