⬢ DragonFlyBSD Kernel Audit
DF-5556 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5556",
  "status": "reproduced",
  "reproduced": true,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "REPRODUCED + FIX VALIDATED. sys/platform/vkernel64/x86_64/npx.c:248-258 npxsave() saves the outgoing threads FP state and fninit()s the x87 unit but never wipes the SSE register file \u2014 pc64s twin wipes via fpurstor(&md->gd_zerofpu) (pc64 npx.c:314, gd_zerofpu in pc64 globaldata.h:72); vkernel64s mdglobaldata has no gd_zerofpu at all. Because go_user() (trap.c:1371-1377) eagerly hands the FPU over on every entry to emulated user mode and a fresh images first entry runs npxdna->npxinit whose fpusave (npx.c:145) fxsave()s the LIVE registers into the new threads td_savefpu and fxrstor()s them back (npxdna npx.c:222), whatever the previous FP owner left in XMM0-15 becomes the new program images initial FP state; the vkernel kernel is compiled -mno-sse so nothing in between clobbers it. PoC inside a vkernel on the guest: victim loads hi=0xDF555600DEADBEEF/lo=index into XMM0-15 then fork()+exec()s the attacker (crt-free static reader whose _start reads XMM first thing): 3/3 runs, 15/16 registers contain the victims exact marker bytes (xmm0 clobbered by the victim-childs libc execl SSE use; see leak_sample.txt + run.log). Control run: all zeros. Unrelated-prober variant: 0 markers (the probers own user re-entries restore its own state first) \u2014 the channel needs the victim to be the last FP owner before the fresh images first user entry, which the fork+exec chain provides deterministically and which the victim-execs-attacker-program / exit-then-exec shapes provide in practice. Applying fix.diff (port of pc64s wipe: gd_zerofpu in vkernel64 mdglobaldata + fpurststor of it in npxsave), rebuilding VKERNEL64 and re-running the exact PoC: 0/16 markers in all 3 runs (run.fixed.log). Impact ceiling: in-vkernel cross-process disclosure of FP-resident secrets (AES-NI key schedules, bignum limbs), 256 bytes per observed handoff \u2014 Medium, kernleak bucket.",
  "exploit_chain": "1) victim process in the vkernel performs FP-resident crypto (AES-NI expanded keys in XMM). 2) victim execs another program (or exits) with keys still in XMM. 3) npxsave/npxexit save the state but leave the live registers untouched (no wipe). 4) the execd (or next first-entry) images first user-mode entry runs npxdna->npxinit which seeds its FP state from the live registers. 5) the new images _start reads XMM0-15 -> previous images key material. Demonstrated with distinct per-register markers: 15/16 registers leaked, 3/3 runs, deterministic via fork+exec.",
  "evidence": "run.log (raw vkernel console, baseline: X01..X0f = df555600deadbeef + reg index, 3 RUNA sections, control zeros), leak_sample.txt (extracted marker lines), run.fixed.log (post-fix console: 0 markers), fix.diff (gd_zerofpu + npxsave wipe), fix-build.log excerpt in build log (RC=0, npx.o rebuilt), VERDICT.md (full narrative incl. PoC quirks), env.txt (guest uname, image md5)",
  "kernel_refs": [
    "sys/platform/vkernel64/x86_64/npx.c:248",
    "sys/platform/vkernel64/x86_64/npx.c:145",
    "sys/platform/vkernel64/x86_64/npx.c:187",
    "sys/platform/vkernel64/x86_64/npx.c:222",
    "sys/platform/vkernel64/x86_64/npx.c:154",
    "sys/platform/vkernel64/x86_64/trap.c:1371",
    "sys/platform/vkernel64/include/globaldata.h:66",
    "sys/platform/pc64/x86_64/npx.c:314",
    "sys/platform/pc64/include/globaldata.h:72"
  ],
  "poc_changes": "Wrote everything from scratch (no seed). Iterations: (1) libc-based crt-free attacker crashed in libc .cerror (%fs:0 errno TLS) -> raw syscall stubs; (2) 460-byte stack frame crossed the vkernels initial stack-top page -> scalar per-register readout with tiny frame; (3) victim dumps initially went into the pipe -> stderr; (4) decisive fix: the readout must be the FIRST instructions of _start \u2014 TDF_USINGFP is set and the seed happens at the images first user-mode entry (go_user eager npxdna), so reading after a blocking syscall returns only the attackers own restored state; (5) fork+exec protocol for deterministic victim->new-image handoff. The in-binary LEAK_REGS counter prints 0 despite the raw hex showing markers (unexplained crt-free-binary artifact; sibling IDX counter works) \u2014 canonical count taken externally from the raw dump lines.",
  "attempts": 7,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "runtime_sec": 4680,
  "guest_dirty": 1,
  "build_cmd": "guest: cc -static -O2 -fno-stack-protector -nostartfiles -o attacker attacker.c; cc -static -O2 -o victim victim.c; cc -static -O2 -o prober prober.c; sh build_image.sh (vkroot.img); cd /usr/src && make -j6 nativekernel KERNCONF=VKERNEL64 (with DF-5474 defsym link workaround)",
  "run_cmd": "guest: sh /root/run.sh  (boots /usr/obj/usr/src/sys/VKERNEL64/kernel.debug -m 512m -r /vkroot.img -n 1; control + 3x RUNA + prober variant; raw console in /tmp/vkconsole.log)",
  "code_hash": "b55da38c83bc80c64bdc38ac2b94ff36df4680675d1a431496438ff0981700e6 (attacker.c) / 49c8b3d12cf600367e925712a220c6f5ce83ce491b57954cb91dff2978d6321c (victim.c) / 0c10c3d586b5ce3053b065ac0808a6459a9e125d5f3ebfa6941f364760930ccb (prober.c)",
  "notes": "vkernel-only divergence from pc64 (pc64 has the wipe). VKERNEL64 needs the DF-5474 link workaround to build. Cross-process via unrelated prober did not observe markers (probers own re-entries restore its state first) \u2014 documented in VERDICT.md; the fork+exec handoff is the deterministic shape. Guest was left dirty (patched /usr/src for fix validation, vm.vkernel_enable=1); reset with-src performed before return. Twin filed as DF-5557 (npxprobemask mxcsr_mask==0 fallback, DF-5526 twin, untested \u2014 hardware precondition absent on rig).",
  "recommended_fix": "Port pc64s security wipe: add union savefpu gd_zerofpu to vkernel64 mdglobaldata and fpurstor(&mdcpu->gd_zerofpu, 0) after fninit() in npxsave()",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "Baseline vkernel: RUN A x3 = 15/16 XMM registers hold the victims markers. After applying fix.diff to guest /usr/src and rebuilding VKERNEL64 (RC=0, npx.o rebuilt), the exact same PoC: 0/16 markers in all 3 runs, control unchanged (zeros). The fork-path cpu_fork()->npxsave() and every switch-out now zero the live register file after saving. Bad behavior gone.",
  "fix_evidence": "fix.diff; run.fixed.log (0 markers x3); run.log (baseline 15/16 x3); leak_sample.txt"
}