β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1096

Unvalidated XSAVE header in npxpop allows unprivileged user to panic kernel via XRSTOR #GP

Summary

npxpop at npx.c:416 does bcopy(mctx->mc_fpregs, td->td_savefpu, sizeof(*td->td_savefpu)) copying 1024 bytes of attacker-controlled FPU state including XSAVE header (struct xstate_hdr at byte 512 of union savefpu). XCOMP_BV field (xstate_xcomp_bv at byte offset 520) is NEVER zeroed or validated. MXCSR IS checked/masked at :417/:252/:277 but XSAVE header has no analogous sanitization. When fpurstor executes XRSTOR (npx.c:502-503) on any XSAVE-capable CPU, crafted XCOMP_BV with bit 63 set (compacted format) and any bit not present in XCR0 causes #GP(0) per Intel SDM Vol 2. #GP fires inside npxdna (from #NM trap handler) or npxdna_quick (from context switch swtch.s:546). Since gd_intr_nesting_level>0 at that point, trap handler falls through to trap_fatal -> panic at trap.c:1101. Any unprivileged local user triggers reliable kernel panic: install SA_SIGINFO handler, modify ucontext mc_ownedfp to _MC_FPOWNED_PCB and XCOMP_BV to 0x8000000000000008, sigreturn, then execute any FP instruction. No code execution (XRSTOR validates header atomically before modifying FPU state). Fix: zero XCOMP_BV and sanitize XSTATE_BV in npxpop after bcopy.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1096 Β· 16 files
FileTypeDescriptionSize
df1096.c trigger-source sigreturn-based XSAVE header trigger 3.1 KB view raw
build.sh build-script cc -O2 -o df1096 df1096.c 148 B view raw
run.sh run-script ./df1096 372 B view raw
panic.txt panic-signature fatal trap 9 GP fault in fpurstor+0x13 (unpatched #0 with -cpu host) 557 B view raw
run_unpatched_xsaves.log run-log decisive baseline run on unpatched #0 with XSAVE (panic) 656 B view raw
run_default_cpu.log run-log baseline run on unpatched #0 default qemu64 (no panic; FXRSTOR path) 517 B view raw
run_patched.log run-log decisive patched run on #1 with XSAVE (no panic) 297 B view raw
fix_build.log build-log full nativekernel build output of fix.diff (NK_DONE rc=0) 5.6 MB ↓ download
all_fixes_build.log build-log full nativekernel build combining all 5 fixes (NK_DONE rc=0) 5.6 MB ↓ download
env.txt environment patched kernel env: uname, kern.version, cc version, CPU features (-cpu host) 560 B view raw
env_default.txt environment default guest env: uname, kern.version, CPU features (qemu64) 446 B view raw
fix.diff suggested-fix zero XCOMP_BV + mask XSTATE_BV to npx_xcr0_mask in npxpop 1.4 KB view raw
VERDICT.md verdict full narrative + Phase 6 hard-blocker analysis 5.1 KB ↓ raw
README.md readme claim summary + build/run/expected 2.2 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme claim summary + build/run/expected
↓ download raw

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.

VERDICT.md verdict full narrative + Phase 6 hard-blocker analysis
↓ download raw

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.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 20:09:57 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live panic on XSAVE CPU). npxpop bcopy 1024B FP state unvalidated -> XRSTOR #GP panic. Needs XSAVE CPU (qemu64 lacks it). Kernel rebuild fix.