# 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)

1. `k8_get_curfreq()` (powernow.c:204-224) returns `-1` ("Not reached",
   powernow.c:222-223) when the CPU's **current** fid/vid (MSR
  `0xc0010042`) 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.
2. `powernow_init()` (powernow.c:392) calls
   `k8_powernow_setperf(k8_get_curfreq())`.  The callee takes
   `unsigned int freq` (powernow.c:227): `-1` becomes `0xFFFFFFFF`.
3. The state-match loop (powernow.c:245-251) compares
   `state_table[i].freq >= freq` with `freq` unsigned — **no** MHz value
   can match `UINT_MAX`, so the loop runs to completion:
   `i == cstate->n_states`, `fid == vid == 0`.
4. 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;` with `i == n_states`.
5. `struct k8pnow_cpu_state` is allocated with
   `kmalloc(..., M_DEVBUF, M_WAITOK)` (powernow.c:366) — **not zeroed** —
   and `k8pnow_decode_pst()` (powernow.c:139-164) initialises only the
   first `n_states` of the 16 `state_table[]` entries.  For
   `n_states < 16` the read at step 4 therefore returns an
   **uninitialised heap word** (still inside the allocation).  For
   `n_states == 16` it reads the `n_states` member itself (benign value).
6. `cpuspeed` is handed to any local user via the world-readable sysctl
   `machdep.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 < 16` so `state_table[n_states]` is uninitialised heap.

## What was run (see VERDICT.md)

* Guest is Intel/QEMU: `kldload powernow` prints
  `module_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 as `uid=1001` on
  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`.
