# DF-1096 — VERDICT

**Verdict: REPRODUCED (panic / DoS) on an XSAVE-capable CPU; not testable
on the default qemu64 guest CPU (FXRSTOR path, header ignored).**

## Mechanism

`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 FP state from a signal context into the
kernel FPU save area. The XSAVE header (`struct xstate_hdr` at byte 512 of
`union savefpu`, `sys/cpu/x86_64/include/npx.h:174`) contains
`xstate_bv` and `xstate_xcomp_bv` and is **never zeroed or validated**.

The only existing check is a *log-only* MXCSR check at npx.c:417-425 (it
does not even mask — that happens later in `npxdna()` at npx.c:252 and
`npxdna_quick()` at npx.c:277).

When the kernel later restores the FPU state via `fpurstor()` (npx.c:498)
on an XSAVE-capable CPU, it executes `xrstor(addr, mask)` (npx.c:502-503).
`XRSTOR` validates the XSAVE header atomically before modifying FPU state
(Intel SDM Vol 2A, XRSTOR): if bit 63 of `XCOMP_BV` is set (compacted
format) together with any bit not present in `XCR0`, the CPU raises
`#GP(0)`. The trap fires inside `npxdna()` (called from the `#NM` trap
handler for an FP instruction) or `npxdna_quick()` (called from
`swtch.s`); at that point `gd_intr_nesting_level > 0`, so the trap handler
in `trap.c` routes to `trap_fatal` → `panic`.

## Trigger

The PoC (`df1096.c`) installs a `SA_SIGINFO` handler for SIGUSR1, sends
itself SIGUSR1, and in the handler:

1. Sets `uc->uc_mcontext.mc_ownedfp = _MC_FPOWNED_PCB` (0x20002) so that
   `npxpop` enters the `_MC_FPOWNED_FPU/_MC_FPOWNED_PCB` case at
   npx.c:398-427.
2. Writes `xstate_xcomp_bv = 0x8000000000000008ULL` (bit 63 set =
   compacted format, bit 3 = BNDREGS state which is *not* in `XCR0`).
3. Returns.

After sigreturn, any FP instruction triggers `#NM → npxdna → fpurstor →
XRSTOR` → `#GP(0)` → panic.

## Reproduction evidence

### XSAVE-capable kernel (QEMU `-cpu host`, unpatched #0)

```
Fatal trap 9: general protection fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer     = 0x8:0xffffffff80bcc163
stack pointer           = 0x10:0xfffff8011835d960
frame pointer           = 0x10:0xfffff8011835d978
processor eflags        = interrupt enabled, resume, IOPL = 0
current process         = 869
kernel: type 9 trap, code=0
Stopped at      fpurstor+0x13:
db>
```

`fpurstor+0x13` is the `xrstor` instruction at npx.c:503 — exactly the
sink the finding cited. The IP maps to `fpurstor`, not `fxrstor`, proving
the XSAVE path was taken.

### Default qemu64 guest CPU (no XSAVE bit in CPUID.01H:ECX)

The same PoC runs to completion, exit 0, no panic. The kernel takes the
`fxrstor` branch at npx.c:506 instead, which only consumes the 512-byte
FXSAVE area and ignores the XSAVE header. The bug is still present in
source but cannot manifest without XSAVE.

## Exploit chain (Phase 6)

**Primitive class:** attacker controls 1024 bytes that reach the kernel
FPU save area, but those bytes are only consumed by `XRSTOR`, which
atomically validates the XSAVE header before modifying any FPU state.
There is **no kernel-memory write primitive** — `XRSTOR` either succeeds
(valid header, FPU state loaded but no kernel-state corruption) or faults
(`#GP`, panic). The kernel's FPU save area is local to the attacker's own
thread (`td->td_savefpu`); other processes never see it.

**Valid hard blocker (read-only primitive in effect):** the only consumer
of the attacker-controlled XSAVE header is `XRSTOR`, which is documented
to validate before writing. There is no escalation path — the attacker
can deny service to themselves (panic their own login session, taking
down the whole guest) but cannot corrupt kernel memory, cannot reach
another process's state, and cannot redirect control flow (the panic
happens at a fixed instruction inside `fpurstor`).

Per the Phase-6 procedure: "If the primitive is not memory corruption
(pure info leak, logic/auth bug, DoS, divide-by-zero), there is no chain
to develop." This is a DoS via a CPU validation fault — correctly
classified as Medium / panic-only impact. No `uid=0` achievable.

## Fix

`fix.diff` mirrors the existing MXCSR-sanitization pattern: after the
bcopy at npx.c:416, when `cpu_xsave` is set, mask `xstate_bv` against
`npx_xcr0_mask` and clear `xstate_xcomp_bv`. This forces XRSTOR to take
the standard (non-compacted) form with only XCR0-supported features,
matching what the kernel itself produces via `xsave`.

## Fix validation

Built a single-fix kernel (`6.5-DEVELOPMENT #1`, sha256
`aa582919...`) on the XSAVE-capable `-cpu host` config, installed it,
and re-ran the same PoC:

| Kernel | CPU | Result |
|---|---|---|
| `#0` unpatched baseline | qemu64 (no XSAVE) | no panic (FXRSTOR path) |
| `#0` unpatched baseline | `-cpu host` (XSAVE) | **panic**, fatal trap 9, `fpurstor+0x13` |
| `#1` patched with fix.diff | `-cpu host` (XSAVE) | **no panic**, PoC exits 0, guest up |

The fix closes the bug deterministically on the XSAVE-capable CPU.

## PoC changes

- Wrote `df1096.c` — sigreturn-based trigger (the finding summary described
  the trigger; this is the working implementation).
- Wrote `fix.diff`, `build.sh`, `run.sh`.
