# DF-0938 — VERDICT

**Status:** REPRODUCED (info leak)
**Impact:** `leak:~300` bytes of uninitialized kernel stack per `/proc/self/fpregs` read (includes multiple kernel-virtual pointers — direct KASLR-defeat / kernel-address disclosure).
**Confidence:** certain
**Class:** CWE-908 (Use of Uninitialized Resource) / CWE-200 (Info Exposure)

---

## Verdict (one line)

REPRODUCED: reading `/proc/self/fpregs` as an unprivileged user leaks ~300 bytes of uninitialized kernel stack (including kernel `.text`/`.data`/heap pointers) because `procfs_dofpregs` declares `struct fpreg r` on the stack without zeroing and `fill_fpregs` (on the default `cpu_fxsr=true` amd64 path) only initializes 108 of the 512 bytes.

## Mechanism (every hop cited `path:line`)

1. **Stack-declared, unzeroed buffer.** `sys/vfs/procfs/procfs_fpregs.c:54` — `struct fpreg r;` with no initializer. `struct fpreg` is 512 bytes (`sys/cpu/x86_64/include/reg.h:74-84`: `fpr_env[4]=32 + fpr_acc[8][16]=128 + fpr_xacc[16][16]=256 + fpr_spare[12]=96`).

2. **Partial fill on the default path.** `procfs_fpregs.c:63` calls `procfs_read_fpregs(lp, &r)` → `fill_fpregs(lp, &r)` (`sys/platform/pc64/x86_64/machdep.c:3078`). On `cpu_fxsr==true` — which is set unconditionally on every SSE+FXSR CPU at boot (`sys/platform/pc64/x86_64/initcpu.c:227-230`, i.e. every modern x86_64 including this KVM guest) — `fill_fpregs` calls `fill_fpregs_xmm(&pcb_save.sv_xmm, (struct save87 *)fpregs)` (`machdep.c:3082-3085`).

3. **`fill_fpregs_xmm` writes only 108 bytes.** `machdep.c:3034-3053` writes only `env87` (28 B: `en_cw/sw/tw/fip/fcs/opcode/foo/fos`) + `sv_ac[8]` (8 × `fpacc87` = 80 B) = **108 B**. It never touches `sv_pad0[4]` nor `sv_pad[64]` of the `save87` overlay (`sys/cpu/x86_64/include/npx.h:67-80`), nor the 336-byte `fpr_xacc+fpr_spare` tail of `struct fpreg` that lies beyond the 176-byte `save87` overlay.

4. **Full-buffer copyout.** `procfs_fpregs.c:65` — `uiomove_frombuf(&r, sizeof(r), uio)` copies the full `sizeof(r)=512` bytes to userspace. Net: **~404 bytes** (`4 + 64 + 336`) of the buffer are uninitialized kernel stack → leaked to the reader.

5. **Reachability / privilege.** `procfs_fpregs.c:59` checks `p_trespass(curp->p_ucred, p->p_ucred)`; reading one's *own* `/proc/self/fpregs` returns 0 (`cr1==cr2`, `sys/kern/kern_prot.c:1025-1026`), so the read is always permitted for any local user with no privilege.

## Evidence (decisive)

Unprivileged user `maxx` (uid 1001, not in wheel), unpatched `6.5-DEVELOPMENT #0` kernel:

```
$ cc -O2 -o poc poc.c && ./poc
read 512 bytes
297 non-zero non-0xAA bytes in tail [108..512) (leaked kernel stack)
```

Hexdump of the 512-byte read (offset 0x70 onward — the unzeroed tail — is full of kernel pointers):

```
00000070  88 38 49 18 01 f8 ff ff  ...  fffff80118493888
00000080  78 35 49 18 01 f8 ff ff  53 4f 9d 80 ff ff ff ff   fffff80118493578  ffffffff809d4f53
000000a0  80 01 06 4f e9 03 00 00  00 8c cb 8d 00 f8 ff ff   ...  fffff8008dcb8c00
000000d0  0a 64 cb 8d 00 f8 ff ff  80 1d d1 16 01 f8 ff ff   fffff8008dcb640a  fffff80116d11d80
00000100  f0 95 93 16 01 f8 ff ff  f0 95 93 16 01 f8 ff ff   fffff801169395f0 (×2)
00000168  c0 db 0e 81 ff ff ff ff                            ffffffff810edbc0  ← kernel .text
                                                              (proc0 = 0xffffffff81176920)
```

**Variance proof (3 runs)** — confirms uninitialized stack, not deterministic FPU state:

| run | non-zero tail bytes | qword @ 0x100 | SHA256(512B) |
|-----|--------------------|---------------|--------------|
| 1 | 300 | `fffff801169395f0` | `c3c54806...` |
| 2 | 299 | `fffff80116939d20` | `81660cce...` |
| 3 | 298 | `fffff80116939c80` | `4b338660...` |

The pointer at 0x100 changes every run; the buffer hash changes every run → stale stack residue.

## Exploit chain

**None — this is a pure info leak (no write/corruption primitive).** Impact ceiling is **KASLR defeat / kernel address disclosure**: ~404 bytes per read, including kernel `.text`/`.data`/heap pointers (e.g. `0xffffffff810xxxxx` near `proc0`, `0xfffff80116xxxxxx` heap, `0xfffff8008dxxxxxx` text). Repeatable per-read with fresh residue → an attacker can aggregate pointers to deterministically resolve kernel symbols, enabling a separate kernel memory-corruption primitive that requires a known kernel VA. No `uid=0` derivable from this bug alone.

## PoC changes

PoC (`findings/poc/DF-0938/poc.c`) was correct as delivered by the reviewer — it compiled and ran first try, no source edits needed. The only changes I made were to the evidence pack: added `build.sh`, `run.sh`, `VERDICT.md`, `manifest.json`, full `build.log`/`run.log`, `leak_sample.txt`, `env.txt`, `run1.bin`, and the validated `fix.diff`.

## Fix (`fix.diff`)

Zero-initialize `struct fpreg r` before `fill_fpregs` touches it, so the unpopulated regions cannot leak stack bytes. Adds `#include <sys/systm.h>` (for `bzero`, matching the idiom in the sibling `procfs_vfsops.c:86`) and `bzero(&r, sizeof(r))` immediately after the declaration:

```c
struct fpreg r;
bzero(&r, sizeof(r));   /* zero unpopulated tail so uiomove_frombuf can't leak stack */
```

This **supersedes** the finding markdown's proposal (which used `memset` without the required `#include <sys/systm.h>` — the kernel builds with `-Werror=implicit-function-declaration`, so the literal markdown fix fails to compile; switching to `bzero` + the systm.h include compiles clean). The defensive suggestion in the markdown (also zero in `procfs_doregs`/`procfs_dodbregs`) is sound but out of scope for this single finding.

## Fix validation (Phase 8)

Built a **single-fix kernel** (`make -j6 nativekernel`, .c-only change → fast incremental) on the `with-src` base, installed `kernel.stripped` → `/boot/kernel/kernel`, rebooted into `6.5-DEVELOPMENT #1: Sun Jul 12 12:25:37 UTC 2026` (sha256 `b97a7729c4309033e2d9e719793c5702612ac59ac21f3230a3ff628b2a9aeb2a`).

| kernel | PoC result |
|--------|-----------|
| `#0` unpatched (baseline) | **297 non-zero bytes** in tail; hexdump full of `fffff8..`/`ffffffff810..` kernel pointers |
| `#1` single-fix | **0 non-zero bytes** in tail; hexdump shows `00 00 ...` from offset 0x6c onward (deterministic across 3 runs) |

→ **`fixed`**: the leak is gone on the patched kernel and present on the unpatched baseline. Clean before/after.

## Kernel references (verified during this run)

- `sys/vfs/procfs/procfs_fpregs.c:54` — `struct fpreg r;` declared on stack, unzeroed.
- `sys/vfs/procfs/procfs_fpregs.c:63` — `procfs_read_fpregs(lp, &r)` → `fill_fpregs`.
- `sys/vfs/procfs/procfs_fpregs.c:65` — `uiomove_frombuf(&r, sizeof(r), uio)` copies full 512 B.
- `sys/platform/pc64/x86_64/machdep.c:3078-3085` — `fill_fpregs` branches to `fill_fpregs_xmm` when `cpu_fxsr`.
- `sys/platform/pc64/x86_64/machdep.c:3034-3053` — `fill_fpregs_xmm` writes only 108 B (env87 + sv_ac[8]).
- `sys/platform/pc64/x86_64/initcpu.c:227-230` — `cpu_fxsr=1` on every SSE+FXSR CPU (default on modern x86_64).
- `sys/cpu/x86_64/include/reg.h:74-84` — `struct fpreg` is 512 bytes.
- `sys/cpu/x86_64/include/npx.h:67-80` — `struct save87` (176 B) overlay; `sv_pad0[4]` + `sv_pad[64]` never written by `fill_fpregs_xmm`.
- `sys/kern/kern_prot.c:1025-1026` — `p_trespass` returns 0 when reading own cred (unprivileged reachability).
