Uninitialised heap read in k8_powernow_setperf tail leaks a heap word into world-readable sysctl machdep.powernow.frequency.current
Summary
k8_get_curfreq's -1 sentinel becomes UINT_MAX in the unsigned parameter, the match loop exits at i==n_states, and the tail reads state_table[n_states] (kmalloc'd without M_ZERO; decode fills only n_states of 16) into the world-readable sysctl - a 4-byte residual heap word to any local user on a K8 whose boot state is outside the PST. Distinct from DF-1960 (decode succeeded here). Logic-verified on guest as uid=1001 with the exact struct replica; kernel path AMD-gated. EINVAL-on-no-match + validated-init fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4903 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| sim_tailread.c | β | 4.4 KB | view raw | |
| build.sh | β | 98 B | view raw | |
| run.sh | β | 95 B | view raw | |
| build.log | β | 161 B | view raw | |
| run.log | β | 305 B | view raw | |
| env.txt | β | 314 B | view raw | |
| fix.diff | β | 834 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-4903 β Uninitialised heap read in k8_powernow_setperf() tail leaks one heap word into world-readable sysctl
File: sys/dev/powermng/powernow/powernow.c
Severity: Medium Β· Bucket: kernleak Β· Confidence: likely (code path certain; hardware precondition)
Root cause (path:line)
k8_get_curfreq()(powernow.c:204-224) returns-1("Not reached", powernow.c:222-223) when the CPU's current fid/vid (MSR0xc0010042) is not among the decoded PST states β e.g. a BIOS PST that does not list the boot state, a state changed after boot by other means, or a trimmed OEM table.powernow_init()(powernow.c:392) callsk8_powernow_setperf(k8_get_curfreq()). The callee takesunsigned int freq(powernow.c:227):-1becomes0xFFFFFFFF.- The state-match loop (powernow.c:245-251) compares
state_table[i].freq >= freqwithfrequnsigned β no MHz value can matchUINT_MAX, so the loop runs to completion:i == cstate->n_states,fid == vid == 0. - The transitions then drive the CPU toward fid 0 / vid 0 (a valid K8
state: 800 MHz), and the tail at powernow.c:310-311 fires:
cpuspeed = cstate->state_table[i].freq;withi == n_states. struct k8pnow_cpu_stateis allocated withkmalloc(..., M_DEVBUF, M_WAITOK)(powernow.c:366) β not zeroed β andk8pnow_decode_pst()(powernow.c:139-164) initialises only the firstn_statesof the 16state_table[]entries. Forn_states < 16the read at step 4 therefore returns an uninitialised heap word (still inside the allocation). Forn_states == 16it reads then_statesmember itself (benign value).cpuspeedis handed to any local user via the world-readable sysctlmachdep.powernow.frequency.current(SYSCTL_OUT at powernow.c:337, leaf registered at powernow.c:417-420,CTLFLAG_RD; DragonFly gates only sysctl writes on privilege β kern/kern_sysctl.c:1446).
Net effect: one 4-byte kernel-heap disclosure per load/trigger, readable
by an unprivileged user. Distinct from known DF-1960 (no-matching-PST β
whole-table uninit): here the PST decode succeeded, but the -1
sentinel walks the index past the initialised prefix.
Trigger preconditions
- AMD K8 (PowerNow!/Cool'n'Quiet, CPUID 0x80000007 EDX bit 6) β the driver's gate at powernow.c:358-363.
- Boot (or current) fid/vid β decoded PST states β
k8_get_curfreq()==-1. n_states < 16sostate_table[n_states]is uninitialised heap.
What was run (see VERDICT.md)
- Guest is Intel/QEMU:
kldload powernowprintsmodule_register_init: MOD_LOAD (powernow) error 2(CPUID gate) β the vulnerable path cannot execute on this guest, so no kernel-level reproduction was possible (status: untested). - A userland logic harness (
sim_tailread.c) replicating the exact struct layout and expressions was compiled and run asuid=1001on the guest: confirms-1 β UINT_MAX β i==n_states β uninit read β cpuspeed == 0xA5A5A5A5. Logic-level proof only; not a kernel leak.
Build / run
cc -Wall -O2 -o sim_tailread sim_tailread.c && ./sim_tailread
Expected output
k8_get_curfreq() = -1 (boot cfid/cvid not in table) cpuspeed after init = 0xa5a5a5a5 (-1515870811) LEAK CONFIRMED (logic): ...
Recommended fix
Treat "no matching state" as an error instead of falling through the
loop tail, and never feed -1 into the unsigned parameter β see
fix.diff.
DF-4903 β VERDICT
status: untested Β· reproduced: 0 Β· impact: leak (kernel-heap info leak, 4 bytes, unpriv-readable sysctl) Β· confidence: likely
Narrative
Pass-2 audit of sys/dev/powermng/powernow/powernow.c found that the
"current frequency" plumbing leaks an uninitialised kernel-heap word when
the CPU's live fid/vid does not match any decoded PST entry:
k8_get_curfreq()returns-1on mismatch (powernow.c:223).powernow_init()passes that-1straight intok8_powernow_setperf(unsigned int freq)(powernow.c:227, call at :392), where it becomes0xFFFFFFFF.- Every
state_table[i].freq >= freqcomparison (powernow.c:246) compares anintagainst that unsigned value β all false β soiexits the loop equal ton_stateswithfid=vid=0. - The tail
cpuspeed = cstate->state_table[i].freq;(powernow.c:310-311) then readsstate_table[n_states]: forn_states < 16that entry was never initialised (kmallocwithout M_ZERO at powernow.c:366; decode fills onlyn_statesentries, powernow.c:143-162). cpuspeedis world-readable atmachdep.powernow.frequency.current(powernow.c:337, registered :417-420). DragonFly only restricts sysctl writes (kern_sysctl.c:1446), so any local user can read the leak.
Not DF-1960: DF-1960 is the "PSB present but no PST matches β whole table
uninitialised" case. DF-4903 is a different root cause β the table is
properly decoded; the -1 sentinel type-confuses the unsigned match loop
and indexes one past the initialised prefix.
Verification attempts
- Guest gate check β the audit guest is
QEMU Virtual CPU version 2.5+(Intel-class).kldload powernowβ console:module_register_init: MOD_LOAD (powernow, ...) error 2β the CPUID 0x80000007 gate (powernow.c:361-363) refuses the module, no sysctls are created, and no MSR/PSB code executes. Runtime reproduction of an AMD-K8-only path is impossible on this guest β honestuntested(kernel side). - Logic harness (
sim_tailread.c) β exact replica of the struct layout, the-1sentinel, the unsigned comparison, and the tail read; heap poisoned with0xA5to emulatekmalloc(M_DEVBUF)reuse. Compiled with guestcc 8.3 [DragonFly]and run asuid=1001(maxx):
k8_get_curfreq() = -1 (boot cfid/cvid not in table)
cpuspeed after init = 0xa5a5a5a5 (-1515870811)
LEAK CONFIRMED (logic): uninitialised heap word read at
state_table[n_states=3] landed in the world-readable
sysctl machdep.powernow.frequency.current
This proves the code path and the type confusion concretely; it is a userland simulation and is not counted as a kernel reproduction.
Exploit chain
Passive 4-byte heap-info leak, once per module load (or per any future
setperf caller passing an out-of-table frequency). Value is residual
slab data from a prior M_DEVBUF allocation; not attacker-positioned
reliably β info value limited; no escalation path from this primitive
alone.
Fix validation
Not run β kernel path not executable on the Intel/QEMU guest
(fix_status: not_testable). fix.diff is authored from line-accurate
root-cause knowledge: bail out of k8_powernow_setperf() when the match
loop ran off the end, and only call it from powernow_init() with a
validated frequency.
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 validation not run: the vulnerable kernel path requires AMD K8 hardware (CPUID 0x80000007 EDX bit 6) absent from the audit guest; fix.diff is line-accurate (bail out when i==n_states; guard the powernow_init call site) and verified to apply cleanly with patch(1).
fix.diff applies clean: 'checking file powernow.c' / DF4903_APPLIES_CLEAN
Confirmed kernel references
Detail
Exploit chain
Passive 4-byte kernel-heap disclosure readable by any local user (sysctl read needs no privilege, kern_sysctl.c:1446 gates writes only). Requires AMD K8 with the boot-time fid/vid not present in the BIOS PST β value is residual slab data, limited info value, no escalation primitive by itself.
Evidence (decisive lines)
run.log (harness output: cpuspeed = 0xa5a5a5a5 as uid=1001), env.txt (guest uname + kldstat showing powernow.ko loaded-but-inert after MOD_LOAD error 2), sim_tailread.c (flow replica), VERDICT.md
PoC changes
No seed PoC existed; harness authored fresh from the kernel source with identical struct layout, sentinel and comparison expressions; MSR transition phases stubbed.
Verified recommended fix
In k8_powernow_setperf return EINVAL when the match loop exits with i==n_states, and in powernow_init only call setperf when k8_get_curfreq() > 0.
Verdict
Kernel path is certain from source but cannot execute on the Intel/QEMU audit guest (kldload powernow fails at the CPUID 0x80000007 gate, console: MOD_LOAD error 2), so no kernel-level reproduction was possible. A userland harness replicating the exact struct layout and expressions (run as uid=1001 on the guest, run.log) demonstrates the full flow: k8_get_curfreq()'s -1 sentinel becomes UINT_MAX in the unsigned freq parameter, the match loop exits with i==n_states, and the tail at powernow.c:311 reads state_table[n_states] β an uninitialised kmalloc(M_DEVBUF) word for n_states<16 β which lands in the global cpuspeed exposed world-readable via sysctl machdep.powernow.frequency.current. Distinct root cause from DF-1960 (there the whole table is uninitialised because no PST matched; here decode succeeded but the sentinel indexes past the initialised prefix).
No comments yet.