# DF-5556 — vkernel `npxsave()` missing FPU security wipe: previous FP owner's XMM registers are handed to the next exec'd image (cross-process/cross-exec FP state leak)

## What

`sys/platform/vkernel64/x86_64/npx.c:248-258` — the vkernel's `npxsave()`
saves the outgoing thread's FP state and calls `fninit()`, but **never
wipes the SSE register file**:

```c
void
npxsave(union savefpu *addr)
{
	crit_enter();
	fpusave(addr, 0);
	mdcpu->gd_npxthread = NULL;
	fninit();                       /* x87 only — XMM0-15 keep old data */
	crit_exit();
}
```

pc64's twin (`sys/platform/pc64/x86_64/npx.c:303-317`) got the security
wipe years ago:

```c
	fninit();
	fpurstor(&md->gd_zerofpu, npx_xcr0_mask);	/* security wipe */
```

with `union savefpu gd_zerofpu` (BSS-zeroed) in pc64's `mdglobaldata`
(`sys/platform/pc64/include/globaldata.h:72`). The vkernel copy never
got it — `sys/platform/vkernel64/include/globaldata.h` has no
`gd_zerofpu` at all.

## Why it leaks (mechanism, all on this file / its platform)

The vkernel multiplexes its user processes inside ONE host thread, and
`go_user()` (`sys/platform/vkernel64/x86_64/trap.c:1371-1377`) does an
**eager** FP switch on every entry to emulated user mode:

```c
	if (mdcpu->gd_npxthread != curthread) {
		if (mdcpu->gd_npxthread)
			npxsave(mdcpu->gd_npxthread->td_savefpu);
		npxdna(tf);
	}
```

For a thread whose `TDF_USINGFP` is clear (any freshly exec'd image),
`npxdna()` (npx.c:187-191) calls `npxinit()`, whose
`fpusave(curthread->td_savefpu, 0)` (npx.c:145) is an `fxsave` of the
**live** registers — the previous FP owner's leftovers — followed by
`fpurstor()` of the same seeded image (npxdna, npx.c:222). Nothing in
the chain ever clears XMM0-15, and the vkernel kernel itself is compiled
`-mno-sse` so kernel code never clobbers them either.

Net effect: **whatever a process leaves in XMM0-15 survives process
boundaries** — through `exit()` (npxexit only saves, npx.c:154-159) and,
deterministically, through the `fork`+`exec` of the next image (the
child's first user-mode entry seeds from the live registers;
`cpu_fork()` at `sys/platform/vkernel64/x86_64/vm_machdep.c:110-112`
saves the parent's FP via the same non-wiping `npxsave`, and nothing
between that and the child's first entry touches XMM).

FP-resident secrets are the classic target: AES-NI round-key schedules
(openssl keeps expanded keys in XMM), bignum limbs (MPIR/GMP), hashed
key material — exactly the data class that OSes wipe FP registers for
(this is why pc64 has the wipe; FreeBSD fixed the same class in
SA-05:14/netisr advisory history).

## Verified (this pack)

Inside a vkernel (`kernel.debug -m 512m -r /vkroot.img -n 1`, built from
the audited tree, on the DragonFly QEMU guest):

* **victim** loads a distinct 128-bit marker (`hi=0xDF555600DEADBEEF`,
  `lo=register index`) into XMM0-15, then `fork()`+`execl()`s the
  **attacker** (a different program image).
* **attacker** is a crt-free static binary whose `_start` reads
  XMM0-15 as its very first instructions and writes raw hex to stdout.
* Result (`run.log`), 3/3 runs: **15 of 16 registers contain the
  victim's exact marker bytes** (xmm0 is clobbered by the exec path;
  xmm1-15 leak):
  ```
  X01 df555600deadbeef 0000000000000001
  X02 df555600deadbeef 0000000000000002
  ...
  X0f df555600deadbeef 000000000000000f
  ```
* Control run (attacker alone, no victim): all registers zero.
* Cross-process probe variant (`prober`: unrelated process fork+exec'ing
  readouts while an independent victim spins): no markers — the prober's
  *own* user-mode re-entries run `npxdna()` which fxrstor's the prober's
  own saved state over the live registers before each child's first
  entry. The leak channel therefore requires the victim (or a
  victim-descended exec chain) to be the last FP owner before the
  probe's first user entry — which is exactly the
  shell/mail-client/crypto-app-that-execs shape, and any attacker
  controlling a program the victim execs, or racing the victim's exit
  with an exec probe (no FP-using intermediary scheduled in between).
* Fix validation: applying `fix.diff` (the pc64 wipe ported to the
  vkernel), rebuilding VKERNEL64 and re-running the exact PoC:
  **0/16 markers, registers read back zero** (`run.fixed.log`).

## Fix

Port pc64's wipe (`fix.diff`): add `union savefpu gd_zerofpu;` to
vkernel64's `mdglobaldata` and `fpurstor(&mdcpu->gd_zerofpu, 0);` after
`fninit()` in `npxsave()`.

## Build / Run

See `build.sh` / `build_image.sh` / `run.sh` (rig adapted from the
proven DF-5337/DF-5538 vkernel packs; needs the DF-5474 SYSTEM_LD
workaround to link VKERNEL64).
