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)
PoC verification
Evidence pack
findings/poc/DF-1096 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 intoxrstor/npxdnaand the guest dies (db> prompt on serial). - Expected (XSAVE not present, e.g. default QEMU
qemu64CPU): the program runs to completion printingcpu_xsave=0 path not taken, no panicβfpurstortakes thefxrstorbranch 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.
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:
- Sets
uc->uc_mcontext.mc_ownedfp = _MC_FPOWNED_PCB(0x20002) so thatnpxpopenters the_MC_FPOWNED_FPU/_MC_FPOWNED_PCBcase at npx.c:398-427. - Writes
xstate_xcomp_bv = 0x8000000000000008ULL(bit 63 set = compacted format, bit 3 = BNDREGS state which is not inXCR0). - 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
fixedvalidated
see evidence pack
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.
No comments yet.