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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3585 Β· 12 files
FileTypeDescriptionSize
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
README.md readme human summary
↓ download 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:4410 voltage_table->count = voltage_dependency_table->count;
  • si_dpm.c:4411-4414 loop writes entries[i] for i < 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 on eg_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 the si_power_info allocation.

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.sh
  • build.log, run.log, run.2.log β€” full untrimmed guest output
  • env.txt β€” guest uname, PCI inventory (virtio only, no AMD GPU), amdgpu.ko present + loads (no si_dpm attach), dmesg showing harness SIGSEGV children
  • fix.diff β€” git-apply-able fix (clamp both construction and population)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md verdict full narrative
↓ download raw

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

  1. Attacker-controlled count, no clamp: sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285 β€” ucNumEntries (raw VBIOS u8) sizes the dependency entries (kcalloc) and :302 sets count = ucNumEntries (0..255). No upper clamp anywhere between parse and use.

  2. Sink 1 (write, controlled data): sys/dev/drm/amd/amdgpu/si_dpm.c:4410 β€” voltage_table->count = voltage_dependency_table->count; then :4411-4414 writes voltage_table->entries[i] for i < count. struct atom_voltage_table.entries is fixed [MAX_VOLTAGE_ENTRIES=32] (sys/dev/drm/amd/amdgpu/amdgpu_atombios.h:119-133), embedded in struct evergreen_power_info (si_dpm.h:668) inside the single kzalloc(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].v of the dependency table) plus zeros.

  3. Reachability gating (all VBIOS-controlled): * si_dpm_init si_dpm.c:7389-7399: voltage_control (GPIO LUT) false, voltage_control_svi2 (SVID2 object present) true; * si_dpm_enable si_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.

  4. Sink 2 (R-M-W): si_dpm.c:4505-4506 table->lowSMIO[i] |= ... for i < 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 across SISLANDS_SMC_STATETABLE and past the si_power_info allocation end for i >= ~154 (statetable sits at the tail of the allocation, si_dpm.h:1002-1012).

  5. First-hop corruption detail (harness-verified): the first OOB slot (entries[32]) lands exactly on eg_pi->vddci_voltage_table.count, replacing it with the BIOS-chosen u16 (demo: 0x1020). Every later consumer of the vddci table (si_populate_voltage_value si_dpm.c:4578-4584, btc_find_voltage si_dpm.c:3210-3214, si_populate_smc_voltage_table 4505) then walks an attacker-sized table β€” compounding OOB reads/writes. current_rps.ps_priv / requested_rps.ps_priv pointer slots (si_dpm.h:675-678) sit ~540 bytes past the overflow origin and are partially overwritten (2 controlled bytes + 4 zeroed) -> subsequent si_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_info for 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

inconclusive
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.