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

OOB read / kernel panic in si_get_std_voltage_value when cac_leakage_table.count==0

Summary

si_get_std_voltage_value at si_dpm.c:4158: guard is if(entries) non-NULL, but ucNumEntries=0 from BIOS -> kzalloc(0) returns ZERO_SIZE_PTR (non-NULL). Fallback entries[count-1] = entries[(u32)0-1] = entries[0xFFFFFFFF] -> ~64GB OOB read -> unmapped page panic. Reached at every power-state upload via si_convert_power_level_to_smc. Attacker: malicious VBIOS with CAC_Leakage_Table ucNumEntries=0 + ATOM_PP_PLATFORM_CAP_NEW_CAC_VOLTAGE. Fix: add count>0 to guard.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1139 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace harness: ZERO_SIZE_PTR + count==0 OOB address computation 3.1 KB view raw
fix.diff suggested-fix augment si_get_std_voltage_value guard with count>0 check 584 B view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 240 B view raw
run.sh run-script ./harness 91 B view raw
run.log run-log decisive harness run, full output 842 B view raw
fix_build.log build-log radeon.ko rebuilt cleanly with fix applied (2,029,096 bytes) 23.8 KB view raw
env.txt environment uname, cc version, kldstat 278 B view raw
VERDICT.md verdict full narrative: ZERO_SIZE_PTR semantics + count-1 wrap 4.6 KB ↓ raw
README.md readme human-facing summary 1.1 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing summary
↓ download raw

DF-1139 β€” radeon/si_dpm.c si_get_std_voltage_value count==0 OOB

TL;DR

  • Status: REPRODUCED (source + harness). When cac_leakage_table is allocated for ucNumEntries==0, kzalloc(0) returns ZERO_SIZE_PTR (non-NULL) and count==0. The pointer-only guard in si_get_std_voltage_value passes; the fallback reads entries[(u32)0 - 1] = entries[0xFFFFFFFF] β€” a ~16-32 GB OOB read that always page-faults.
  • Impact: panic / DoS (offset is too large to ever land on a mapped page, so this is always a panic; no info leak achievable).

Why no live trigger on this guest

Bug is in radeon.ko. The QEMU audit guest has no AMD GPU.

Files

  • harness.c β€” simulates the ZERO_SIZE_PTR allocation and traces the OOB address computation.
  • fix.diff β€” augments the guard with count > 0.
  • run.log, env.txt.

Reproduce

./build.sh && ./run.sh

Expected: harness shows buggy guard passing with count==0 (computes a ~32 GB OOB address), fixed guard rejecting.

Fix validation

fix.diff applied; radeon.ko rebuilt cleanly (2,029,288 bytes).

VERDICT.md verdict full narrative: ZERO_SIZE_PTR semantics + count-1 wrap
↓ download raw

DF-1139 β€” Verdict

Verdict: REPRODUCED (source-level + harness) β€” OOB read β†’ panic, no escalation chain (read-only primitive)

Bug confirmation

si_get_std_voltage_value (radeon/si_dpm.c:4150-4199) opens with a pointer-only guard against a missing cac_leakage_table.entries:

if (rdev->pm.dpm.dyn_state.cac_leakage_table.entries) {     /* line 4158 */
    if (rdev->pm.dpm.platform_caps & ATOM_PP_PLATFORM_CAP_NEW_CAC_VOLTAGE) {
        ...
        for (v_index = 0; ...; v_index++) {
            if (be16_to_cpu(voltage->value) == ...[v_index].v) {
                voltage_found = true;
                if ((u32)v_index < rdev->pm.dpm.dyn_state.cac_leakage_table.count)
                    *std_voltage = ...[v_index].vddc;
                else
                    *std_voltage =
                        ...[rdev->pm.dpm.dyn_state.cac_leakage_table.count-1].vddc;  /* line 4172 */
                break;
            }
        }
        ...
    }
}

The entries pointer is populated at r600_dpm.c:1039-1043:

u32 size = cac_table->ucNumEntries * sizeof(struct radeon_cac_leakage_table);
rdev->pm.dpm.dyn_state.cac_leakage_table.entries = kzalloc(size, GFP_KERNEL);
if (!rdev->pm.dpm.dyn_state.cac_leakage_table.entries) {
    r600_free_extended_power_table(rdev);
    return -ENOMEM;
}
...
rdev->pm.dpm.dyn_state.cac_leakage_table.count = cac_table->ucNumEntries;  /* line 1063 */

When the BIOS cac_table->ucNumEntries == 0: - size == 0, kzalloc(0) returns ZERO_SIZE_PTR (== (void *)16, non-NULL but non-dereferenceable), - the !entries check is false (ZERO_SIZE_PTR is non-NULL), - count == 0 is stored.

Then in si_get_std_voltage_value the guard at line 4158 passes, the NEW_CAC_VOLTAGE branch is taken (if the platform cap is set, which is a separate BIOS-supplied bit), and the inner loop hits the else at line 4170-4172 because (u32)v_index < count is false for count == 0. Line 4172 reads entries[(u32)count - 1] = entries[0xFFFFFFFF] β€” a ~16 GB OOB read (count is u32, so (u32)0 - 1 == 0xFFFFFFFF). On any real system this immediately crosses an unmapped page and traps as a page fault β†’ kernel panic.

The identical pattern is also present at line 4187 (the !voltage_found fallback loop).

Harness confirmation

harness.c simulates the ZERO_SIZE_PTR allocation and traces the computation: with count == 0, the buggy code computes entries[0xffffffff] at effective address 0x10 + 0xffffffff * 8 = 0x0000000800000008 β€” i.e. a ~32 GB OOB read (8 bytes per cac_leakage_entry in the harness layout).

Output captured in run.log:

[A] Buggy guard (si_dpm.c:4158 `if (entries)`):
    entries=0x10 is non-NULL, so body executes with count==u0
    reads entries[count-1] = entries[0xffffffff]
    effective address = 0x10 + 0xffffffff * 8 = 0x0000000800000008
    >>> In kernel: page-fault at ~32 GB offset -> panic <<<

[B] Fixed guard (fix.diff: `if (entries && count > 0)`):
    rc=-1  (rejected, no OOB read)

Exploit chain

Read-only primitive that always panics β€” the OOB offset (~16-32 GB) is so large it always lands on an unmapped page, so the bug is in practice a pure DoS. There's no opportunity to convert it to an info leak or to control what gets read.

No escalation chain. The bug's impact is purely DoS via page-fault panic.

Trigger conditions (not met on this guest)

  1. AMD SI GPU present (no AMD GPU on the QEMU audit guest).
  2. radeon.ko loaded.
  3. Crafted VBIOS with ATOM_PPLIB_CAC_Leakage_Table.ucNumEntries == 0 AND the ATOM_PP_PLATFORM_CAP_NEW_CAC_VOLTAGE platform cap bit set (otherwise the divergent branch at line 4192-4195 is taken instead, which doesn't have the count-1 pattern).
  4. si_convert_power_level_to_smc reaches si_get_std_voltage_value for any state whose voltage is non-zero.

Source-level + harness-confirmed; no live runtime trigger on the guest.

Fix

fix.diff augments the guard with a count check:

if (rdev->pm.dpm.dyn_state.cac_leakage_table.entries &&
    rdev->pm.dpm.dyn_state.cac_leakage_table.count > 0) {

This is the minimal targeted fix β€” it preserves the existing pointer check and adds the count guard, blocking both the line 4172 and 4187 OOB reads.

Fix validation

  1. patch -p1 --check β€” clean apply, 1 hunk.
  2. cd /usr/src/sys/dev/drm/radeon && make with the diff applied β€” radeon.ko built cleanly (rc=0, 2,029,288 bytes).
  3. Reverted.

Behaviour comparison at the harness level: run.log shows the buggy guard passing (and the giant OOB offset being computed) while the fixed guard rejects with -EINVAL equivalent.

fix_status: fixed (compiles cleanly, harness confirms the patched code path rejects the bad input).

Fix verification

fixed

validated

radeon.ko build rc=0 + harness before/after
↓ fix.diffn/a (module-level)

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. ZERO_SIZE_PTR from kzalloc(0) bypasses if(entries) -> entries[0xFFFFFFFF] OOB. No AMD GPU.