# DF-5556 — VERDICT

**status: reproduced (leak) · fix validated · impact: cross-process/cross-exec FP-register info leak inside a vkernel**

## What was run

Rig (from the proven DF-5337/DF-5538 packs): DragonFly QEMU guest →
`/usr/obj/usr/src/sys/VKERNEL64/kernel.debug -m 512m -r /vkroot.img -n 1
-e init_path=/bin/vkinit` (single vCPU so all emulated processes share
one hardware FPU; VKERNEL64 linked with the DF-5474 `--defsym
__build_id_*` workaround).

* `victim` (normal static binary): loads
  `hi=0xDF555600DEADBEEF, lo=reg_index` into XMM0..15, then
  `fork()`+`execl("/root/attacker")`.
* `attacker` (static, `-nostartfiles`, raw syscalls): `_start`'s first
  instructions read XMM0..15 and write raw hex to stdout.
* control: attacker alone; prober variant: unrelated process fork+exec'ing
  readouts next to a spinning victim.

## Baseline (unfixed vkernel)

`run.log` (raw vkernel console; `leak_sample.txt` extracted):

```
# /root/victim fork; echo RUNA1_RC=$?; sleep 2
X00 0000000000000000 0000000000000000      <- clobbered by execl()'s libc SSE
X01 df555600deadbeef 0000000000000001      <- victim's marker
X02 df555600deadbeef 0000000000000002
...
X0f df555600deadbeef 000000000000000f
```

**3/3 runs: 15/16 registers carry the victim's exact 128-bit markers**
(xmm1-15; xmm0 is overwritten by the libc `execl` path of the victim's
own child — the child runs victim-image libc between fork and exec).
Control run: 16/16 zeros. Cross-process prober variant: 0 markers — the
prober's own user-mode re-entries (`go_user`→`npxdna` restores the
prober's saved state) overwrite the live registers before each child's
first entry; the channel therefore needs the victim to be the last FP
owner before the fresh image's first user entry (the fork+exec chain of
RUN A proves it deterministically; the exit-then-exec and
victim-execs-attacker-controlled-program shapes are the real-world
versions).

## Root cause (code)

* `sys/platform/vkernel64/x86_64/npx.c:248-258` — `npxsave()` does
  `fxsave`+`fninit()` and **no XMM wipe** (pc64 twin wipes via
  `fpurstor(&md->gd_zerofpu)` at `sys/platform/pc64/x86_64/npx.c:314`;
  vkernel64's `mdglobaldata` has no `gd_zerofpu` at all,
  `sys/platform/vkernel64/include/globaldata.h:66-84`).
* `sys/platform/vkernel64/x86_64/trap.c:1371-1377` — `go_user()` does an
  eager FP handoff on every entry to emulated user mode.
* `sys/platform/vkernel64/x86_64/npx.c:187-191,145` — a fresh image's
  first entry (TDF_USINGFP clear) runs `npxinit()` whose `fpusave()` is
  an `fxsave` of the **live** registers → the previous owner's XMM data
  becomes the new image's initial FP state (registers *and* its
  `td_savefpu`).
* `sys/platform/vkernel64/x86_64/npx.c:154-159` — `npxexit()` likewise
  only saves; a dying process's XMM state stays live for the next
  first-use thread.

The vkernel kernel itself is compiled `-mno-sse -msoft-float`, so no
kernel code clobbers XMM between owner and victim-image — the channel is
quiet and deterministic.

## Fix validation

`fix.diff` (pc64's wipe ported: `union savefpu gd_zerofpu` in vkernel64
`mdglobaldata` + `fpurstor(&mdcpu->gd_zerofpu, 0)` after `fninit()` in
`npxsave()`), applied to the guest `/usr/src`, `make nativekernel
KERNCONF=VKERNEL64` (RC=0, `npx.o` rebuilt, `fix-build.log`), exact same
PoC re-run:

```
RUN A ×3 : markers = 0/16, 0/16, 0/16   (run.fixed.log)
control   : 0/16 (unchanged)
```

Bad behavior gone; the fork-path `cpu_fork()` → `npxsave()` (and every
switch-out) now zeroes the live register file after saving.

## PoC quirks (honesty notes)

* The attacker's in-binary `LEAK_REGS=` counter printed 0 despite the
  raw hex lines showing markers — an unexplained miscompilation/artifact
  of the crt-free binary (the disassembled compare at 0x400182
  `cmp %rbp,%r13` looks correct; the sibling `IDX=` counter for the low
  halves worked). The canonical count is taken externally from the raw
  dump lines (`leak_sample.txt` / analysis in the run logs): 15/16 × 3.
* Early PoC iterations crashed for environment reasons, each fixed and
  documented: libc `%fs:0` errno path without crt (switched to raw
  syscalls); vkernel initial `%rsp` sits under the stack top page
  (switched to scalar-per-register output with a tiny frame); victim
  dumps went to the pipe instead of the console (moved to stderr); the
  readout must happen at `_start` (first user entry), not after a
  blocking syscall — the first entry is exactly where `TDF_USINGFP` gets
  set and the seed happens.

## Attempts

7 distinct guest iterations (build system fix, 2 PoC crashes, instrumented
victim proving load+syscall survival, readout-point fix, final baseline,
final fixed run).

## Impact ceiling

In-vkernel cross-process disclosure of FP-register-resident secrets
(AES-NI round keys, bignum limbs) — up to 256 bytes per observed handoff
(XMM1-15 in the repro). Not kernel-memory disclosure; requires the
victim to be the last FP owner before the attacker image's first user
entry (deterministic via the victim's own fork+exec; otherwise a race).
Severity: Medium (kernleak bucket), same class as DF-0938/DF-1056
(uninit/leftover FP state exposure) and the pc64 wipe exists precisely
to prevent it.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
