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)
PoC verification
Evidence pack
findings/poc/DF-1139 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
DF-1139 β radeon/si_dpm.c si_get_std_voltage_value count==0 OOB
TL;DR
- Status: REPRODUCED (source + harness). When
cac_leakage_tableis allocated forucNumEntries==0,kzalloc(0)returnsZERO_SIZE_PTR(non-NULL) andcount==0. The pointer-only guard insi_get_std_voltage_valuepasses; the fallback readsentries[(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 withcount > 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).
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)
- AMD SI GPU present (no AMD GPU on the QEMU audit guest).
radeon.koloaded.- Crafted VBIOS with
ATOM_PPLIB_CAC_Leakage_Table.ucNumEntries == 0AND theATOM_PP_PLATFORM_CAP_NEW_CAC_VOLTAGEplatform cap bit set (otherwise the divergent branch at line 4192-4195 is taken instead, which doesn't have the count-1 pattern). si_convert_power_level_to_smcreachessi_get_std_voltage_valuefor 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
patch -p1 --checkβ clean apply, 1 hunk.cd /usr/src/sys/dev/drm/radeon && makewith the diff applied βradeon.kobuilt cleanly (rc=0, 2,029,288 bytes).- 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
fixedvalidated
radeon.ko build rc=0 + harness before/after
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.
No comments yet.