# DF-0592 — PoC: uninitialized kernel stack leak via `fairq_getqstats` copyout

Privileged local info-leak PoC.  `fairq_getqstats`
(`sys/net/altq/altq_fairq.c:278`) declares `struct fairq_classstats stats;`
on the stack without zeroing (line 282).  The helper `get_class_stats`
(`altq_fairq.c:974-1001`) only populates a subset of the struct fields —
compiler-inserted padding bytes plus the entire `red[3]` array (when the
class uses `Q_DROPTAIL`, the default) remain uninitialized.
`copyout((caddr_t)&stats, ubuf, sizeof(stats))` at `altq_fairq.c:312`
copies the full 224-byte struct unconditionally, leaking up to **176 bytes
of stale kernel stack per `DIOCGETQSTATS` call**.

The same defect exists in `altq_priq.c:priq_getqstats` and
`altq_hfsc.c:hfsc_getqstats` (identical stack-declared, partially-populated
`*_classstats` struct); `fix.diff` closes all three.

## Files

- `fairq_leak.c`        — full driver: `DIOCXBEGIN`/`DIOCADDALTQ` x2
  (discipline + Q_DROPTAIL class) / `DIOCXCOMMIT` / `DIOCGETALTQS` /
  `DIOCGETALTQ` / `DIOCGETQSTATS`, then hex-dumps the uninitialized
  regions of the returned struct.
- `build.sh`            — `cc -O2 -o fairq_leak fairq_leak.c`
- `run.sh`              — `kldload pf.ko` (system setup) + `./fairq_leak vtnet0`
- `run.log`             — decisive unpatched run (LEAK CONFIRMED)
- `run.2.log`,`run.3.log` — additional leak runs (variance)
- `baseline_unpatched.log` — Phase 8 baseline reproduction on `#0`
- `fix_run.log`         — Phase 8 patched-kernel run (leak GONE)
- `fix_build.log`       — single-fix kernel build log (35k lines, rc=0)
- `leak_sample.txt`     — 4-run variance sample
- `env.txt`             — guest uname / cc / kldstat
- `fix.diff`            — git-apply-able fix for fairq + priq + hfsc
- `VERDICT.md`          — full narrative
- `manifest.json`       — catalog

## Build & run

```
./build.sh                       # cc -O2 -o fairq_leak fairq_leak.c
sudo ./run.sh                    # kldload pf.ko ; ./fairq_leak vtnet0
```

(Or directly: `cc -O2 -o fairq_leak fairq_leak.c && sudo ./fairq_leak vtnet0`.)

## Expected output (unpatched kernel)

Hex dump of the returned `struct fairq_classstats` showing **non-zero** bytes
at struct offsets 12-15, 52-55, and 56-223 (the uninitialized regions).  Many
of those bytes look like kernel text/data pointers (`0xffffffff8xxxxxxx` on
amd64) — stale stack frames recovered from prior kernel call paths.  On the
default GENERIC `with-src` kernel a typical leak is **124-138 non-zero bytes
out of 176**, with **27-29 kernel-pointer-looking 8-byte windows** per call.

```
=== uninitialized-region analysis ===
  region [ 12.. 15] (  4 bytes) padding(qlimit->xmit_cnt) : 4 non-zero bytes
  region [ 52.. 55] (  4 bytes) padding(qtype->red[0])    : 4 non-zero bytes
  region [ 56..223] (168 bytes) red[3] (Q_DROPTAIL: ...)  : 130 non-zero bytes

TOTAL non-zero bytes in uninitialized regions: 138 / 176
kernel-pointer-looking 8-byte windows in red[] region: 29
RESULT: LEAK CONFIRMED — 138 bytes of uninitialized kernel stack returned to userspace.
```

## Expected output (patched kernel — `fix.diff` applied)

```
  region [ 12.. 15] (  4 bytes) padding(qlimit->xmit_cnt) : 0 non-zero bytes
  region [ 52.. 55] (  4 bytes) padding(qtype->red[0])    : 0 non-zero bytes
  region [ 56..223] (168 bytes) red[3] (Q_DROPTAIL: ...)  : 0 non-zero bytes

TOTAL non-zero bytes in uninitialized regions: 0 / 176
RESULT: no leak — all uninitialized regions are zero.
```

## Notes

- `/dev/pf` is `0600 root:wheel` (`sys/net/pf/pf_ioctl.c:3360`).  The PoC
  must run as root (or with `/dev/pf` delegated, e.g. a jail with `/dev/pf`).
  This is a **Low** info-leak finding (CVSS `PR:H/C:L`); the realistic
  attacker is already privileged.  No privilege escalation; the leak is the
  whole impact.
- `kldload pf.ko` is **system setup**, not part of any escalation chain —
  it makes the pf subsystem available.  The kernel-side bug
  (`fairq_getqstats`) ships in the base GENERIC kernel (compiled in via
  `options ALTQ_FAIRQ`); `pf.ko` is the standard userland-facing module.
- The same defect exists in `altq_priq.c:priq_getqstats` and
  `altq_hfsc.c:hfsc_getqstats`.  `fix.diff` patches all three with the
  identical `memset(&stats, 0, sizeof(stats))` immediately after the
  declaration in each `*_getqstats` function.
