# DF-1096 — Unvalidated XSAVE header in npxpop

## Claim
`npxpop()` at `sys/platform/pc64/x86_64/npx.c:416` does
`bcopy(mctx->mc_fpregs, td->td_savefpu, sizeof(*td->td_savefpu))`, copying
1024 bytes of attacker-controlled FPU state into the kernel FPU save area.
The XSAVE header (`struct xstate_hdr` at byte 512 of `union savefpu`,
npx.h:174) contains `xstate_bv` and `xstate_xcomp_bv` and is **never zeroed
or validated**. The MXCSR field is only logged at npx.c:417-425 (it is later
masked at npxdna:252 / npxdna_quick:277).

When `fpurstor()` (npx.c:498) executes `XRSTOR` (npx.c:502-503) on an
XSAVE-capable CPU with a crafted `XCOMP_BV` (bit 63 set = compacted format
+ any bit not in XCR0), the CPU raises `#GP(0)` per Intel SDM Vol 2.
That `#GP` fires inside `npxdna()` (called from the `#NM` trap handler) or
`npxdna_quick()` (called from `swtch.s`); because `gd_intr_nesting_level > 0`
there, the trap handler routes to `trap_fatal` → `panic` at trap.c:1101.

No code execution — `XRSTOR` validates the header atomically before touching
FPU state.

## Build / Run / Expected
- **Build:** `cc -O2 -o df1096 df1096.c`
- **Run:** `./df1096`  (as any unprivileged user)
- **Expected (bug present, XSAVE-capable CPU):** kernel panic
  `fatal trap 13: general protection fault ... cpuid = ...; mp_lock ...`
  pointing into `xrstor` / `npxdna` and the guest dies (db> prompt on serial).
- **Expected (XSAVE not present, e.g. default QEMU `qemu64` CPU):** the
  program runs to completion printing `cpu_xsave=0 path not taken, no panic`
  — `fpurstor` takes the `fxrstor` branch at npx.c:506 instead, which
  ignores the XSAVE header.

## Reproduction notes
The default audit guest's CPU advertises `Features=0x1783fbfd` which lacks
the XSAVE bit (CPUID.01H:ECX[26]). `cpu_xsave` is therefore 0 in the kernel,
`fpurstor` selects `fxrstor`, and the malicious XSAVE header is silently
ignored. To reproduce the panic the guest must be booted on an XSAVE-capable
CPU model (e.g. `-cpu host` under KVM on a host that exposes XSAVE). The
PoC is written so it works either way: it always tries the bad-header path
and reports which branch the kernel ended up taking.

See `VERDICT.md` for the full per-kernel results.
