# HG changeset patch # User Taylor R Campbell # Date 1783623531 0 # Thu Jul 09 18:58:51 2026 +0000 # Branch trunk # Node ID 3b774021144609cc52de856e495ed3e542a46104 # Parent 8002d24a07f69540f9ece24d842ed3eeaf5f37fb # EXP-Topic riastradh-pr60426-x86xsavesignal WIP: x86: Save and restore all extended CPU state on signals. While here, disable Intel AMX, whose state size (>>8 KiB) exceeds MINSIGSTKSZ (8 KiB), until we are ready to safely update the ABI for sigaltstack(2). This isn't a regression: we've never had a release with Intel AMX support. Previously, on signal delivery, we would only save and restore at most what FXSAVE does, which is the x87 and SSE registers that always exist on amd64. To save and restore the upper halves of the YMM or ZMM registers (AVX/AVX2/AVX512), or the AVX512 registers ZMM16..ZMM31, or the enormous AMX state, we need to do more. And we need to do that even for programs that don't use AVX instructions, because an SSE instruction modifying xmmN will, as a side effect, zero the high half of ymmN. Fortunately, the x86 architecture has an extensible mechanism for saving and restoring extended CPU state, the XSAVE/XRSTOR instruction family, so we don't need to update this code for every extension -- just handling the generic XSAVE/XRSTOR mechanism will work for a while. When delivering a signal to a thread that has actually made use of these extended registers, we: 1. allocate a separate space on the user's stack for an XSAVE area of whatever size is necessary (as long as it fits in the sigaltstack, if one was provided), and 2. store a pointer to and the length of the separate XSAVE area in an architecturally unused section of the FXSAVE area of the mcontext_t, with the _UC_XSAVE bit set. This way existing applications that have SA_SIGINFO signal handlers and expect to find an FXSAVE area in the mcontext_t with x87 and SSE register content will continue to work, but if the thread has used any state beyond that like AVX registers requiring XSAVE/XRSTOR, on return from signal it can restore everything. Unfortunately, the _size_ that XSAVE needs is variable and might be expanded by architectural extensions, such as Intel AMX. So the minimum stack size to store it all might exceed MINSIGSTKSZ. The current MINSIGSTKSZ is plenty for AVX/AVX2/AVX-512. But before raising MINSIGSTKSZ to support larger XSAVE sizes by adding them to XCR0_FPU, we need to make sure that older binaries built with the old MINSIGSTKSZ continue to work -- of course, they won't be able to use (e.g.) Intel AMX state. To make sure we don't break the ABI accidentally, I've added some __CTASSERTs relating XSAVE_MAX_BYTES, the maximum supported XSAVE area size, and MINSIGSTKSZ, and a panic at boot if the CPU advertises an XSAVE size larger than XSAVE_MAX_BYTES, which would mean either: (a) the CPU's advertised size for various architecturally defined extended CPU state does not match the documentation, or (b) we have inadvertently bitten off more extended state CPU state than we can chew in XCR0_FPU and we have to adjust the ABI and add compatibility for userland programs. Fortunately for getcontext(2), all of the registers in question are caller-saves, so it doesn't need to store any extra state. PR kern/60426: Signal handler corrupts AVX (YMM) registers diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/amd64/amd64/machdep.c --- a/sys/arch/amd64/amd64/machdep.c Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/amd64/amd64/machdep.c Thu Jul 09 18:58:51 2026 +0000 @@ -329,6 +329,9 @@ void init_bootspace(void); void init_slotspace(void); void init_x86_64(paddr_t); +static int cpu_getmcontext_xsave(struct lwp *, mcontext_t *, unsigned *, + const struct xsave_header *, size_t, struct xsave_header *); + /* * Machine-dependent startup code */ @@ -594,6 +597,9 @@ sendsig_siginfo(const ksiginfo_t *ksi, c struct sigframe_siginfo *fp, frame; sig_t catcher = SIGACTION(p, sig).sa_handler; struct trapframe *tf = l->l_md.md_regs; + const struct xsave_header *xsavebuf = NULL; + size_t xsavelen = 0; + struct xsave_header *user_xsave = NULL; char *sp; KASSERT(mutex_owned(p->p_lock)); @@ -604,16 +610,67 @@ sendsig_siginfo(const ksiginfo_t *ksi, c (SIGACTION(p, sig).sa_flags & SA_ONSTACK) != 0; /* Allocate space for the signal handler context. */ - if (onstack) + if (onstack) { + KASSERT(l->l_sigstk.ss_size >= MINSIGSTKSZ); sp = ((char *)l->l_sigstk.ss_sp + l->l_sigstk.ss_size); - else + } else { /* AMD64 ABI 128-bytes "red zone". */ sp = (char *)tf->tf_rsp - 128; - + } + + /* + * The maximum amount of space we might use, including padding + * for alignment, had better fit in MINSIGSTKSZ. + * + * If this changes because you have increased XSAVE_MAX_BYTES, + * you need to work out the ABI change for MINSIGSTKSZ. + */ + __CTASSERT(8 + STACK_ALIGNBYTES + sizeof(struct sigframe_siginfo) + + (XSAVE_ALIGN - 1) + XSAVE_MAX_BYTES <= MINSIGSTKSZ); + + /* + * Find whether we need to allocate a separate XSAVE area, + * because the user program has used extended CPU state beyond + * the x87/SSE registers, or whether we can get by with just an + * FXSAVE area. + */ + if (process_xsave_needed_p(l)) { + process_read_xsave(l, &xsavebuf, &xsavelen); + KASSERT(xsavebuf != NULL); + KASSERT(xsavelen <= XSAVE_MAX_BYTES); + + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(!onstack || + sp - (char *)l->l_sigstk.ss_sp >= xsavelen); + sp -= xsavelen; + + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(!onstack || + sp - (char *)l->l_sigstk.ss_sp >= XSAVE_ALIGN - 1); + sp = (char *)((uintptr_t)sp & ~(XSAVE_ALIGN - 1)); + + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(((uintptr_t)sp & (XSAVE_ALIGN - 1)) == 0); + user_xsave = (void *)sp; + } + + /* + * Reserve space for a struct sigframe_siginfo. The first + * member is a return address, as if we are entering a + * procedure with a CALL instruction with a stack frame aligned + * to a multiple of 16 bytes; the rest of the structure is that + * stack frame. So make sure the structure starts at an + * address congruent to 8 modulo 16. + */ + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(!onstack || (size_t)(sp - (char *)l->l_sigstk.ss_sp) >= + 8 + STACK_ALIGNBYTES + sizeof(struct sigframe_siginfo)); sp -= sizeof(struct sigframe_siginfo); /* Round down the stackpointer to a multiple of 16 for the ABI. */ fp = (struct sigframe_siginfo *)(((unsigned long)sp & ~STACK_ALIGNBYTES) - 8); + KASSERT(!onstack || (char *)fp >= (char *)l->l_sigstk.ss_sp); + KASSERT(((uintptr_t)fp & STACK_ALIGNBYTES) == 8); memset(&frame, 0, sizeof(frame)); frame.sf_ra = (uint64_t)ps->sa_sigdesc[sig].sd_tramp; @@ -627,8 +684,21 @@ sendsig_siginfo(const ksiginfo_t *ksi, c mutex_exit(p->p_lock); cpu_getmcontext(l, &frame.sf_uc.uc_mcontext, &frame.sf_uc.uc_flags); + + /* + * If we have to use XSAVE, copy out that area separately -- + * and be ready to bail if it failed. + */ + if (xsavebuf) { + error = cpu_getmcontext_xsave(l, &frame.sf_uc.uc_mcontext, + &frame.sf_uc.uc_flags, xsavebuf, xsavelen, user_xsave); + if (error != 0) + goto relock; + } + /* Copyout all the fp regs, the signal handler might expect them. */ error = copyout(&frame, fp, sizeof frame); +relock: mutex_enter(p->p_lock); if (error != 0) { @@ -2123,25 +2193,95 @@ cpu_getmcontext(struct lwp *l, mcontext_ mcp->_mc_tlsbase = (uintptr_t)l->l_private; *flags |= _UC_TLSBASE; - process_read_fpregs_xmm(l, (struct fxsave *)&mcp->__fpregs); + process_read_fpregs_xmm(l, (struct fxsave *)&mcp->__fpregs.__fxsave); *flags |= _UC_FPU; } +/* + * cpu_getmcontext_xsave(l, mcp, flags, xsavebuf, xsavelen, user_xsave) + * + * Copy out xsavebuf[0..xsavelen) to user_xsave, set mcp to point + * there, and set _UC_XSAVE in flags. Caller must have already + * used cpu_getmcontext to initialize mcp's FXSAVE area. + * + * May fail if the copyout fails. + */ +static int +cpu_getmcontext_xsave(struct lwp *l, mcontext_t *mcp, unsigned int *flags, + const struct xsave_header *xsavebuf, size_t xsavelen, + struct xsave_header *user_xsave) +{ + int error; + + KASSERT(*flags & _UC_FPU); + + /* + * Copy out the XSAVE area. + */ + error = copyout(xsavebuf, user_xsave, xsavelen); + if (error != 0) + return error; + + /* + * Record a pointer to the real XSAVE area in the + * architecturally unused bits mcontext_t's FXSAVE area. + */ + mcp->__fpregs.__xsave.__xsaveptr = (__greg_t)(uintptr_t)user_xsave; + mcp->__fpregs.__xsave.__xsavelen = (__greg_t)xsavelen; + + /* + * Set the _UC_XSAVE flag so cpu_setmcontext will be able to + * restore the full state from the XSAVE area. + */ + *flags |= _UC_XSAVE; + + /* Success! */ + return 0; +} + int cpu_setmcontext(struct lwp *l, const mcontext_t *mcp, unsigned int flags) { struct trapframe *tf = l->l_md.md_regs; const __greg_t *gr = mcp->__gregs; struct proc *p = l->l_proc; + struct xsave_header *xsavebuf = NULL; + size_t xsavelen = 0; int error; int64_t rflags; CTASSERT(sizeof (mcontext_t) == 26 * 8 + 8 + 512); + /* + * If there's an external XSAVE area, copy it in and validate + * it before we irreversibly modify the trapframe. + * + * We could check the length against the state components + * included, but we currently don't: if it's truncated, it will + * be as if the truncated part were zero-filled -- this is + * implemented in process_write_xsave, called a little below. + */ + if ((flags & _UC_XSAVE) != 0) { + const struct xsave_header *user_xsave = + (void *)(uintptr_t)mcp->__fpregs.__xsave.__xsaveptr; + + xsavelen = mcp->__fpregs.__xsave.__xsavelen; + error = process_verify_xsavelen(l, xsavelen); + if (error != 0) + goto out; + xsavebuf = kmem_alloc(xsavelen, KM_SLEEP); + error = copyin(user_xsave, xsavebuf, xsavelen); + if (error != 0) + goto out; + error = process_verify_xsave(l, xsavebuf, xsavelen); + if (error != 0) + goto out; + } + if ((flags & _UC_CPU) != 0) { error = cpu_mcontext_validate(l, mcp); if (error != 0) - return error; + goto out; tf->tf_rdi = gr[_REG_RDI]; tf->tf_rsi = gr[_REG_RSI]; @@ -2175,8 +2315,13 @@ cpu_setmcontext(struct lwp *l, const mco l->l_md.md_flags |= MDL_IRET; } - if ((flags & _UC_FPU) != 0) - process_write_fpregs_xmm(l, (const struct fxsave *)&mcp->__fpregs); + if ((flags & _UC_XSAVE) != 0) { + KASSERT(xsavebuf != NULL); + process_write_xsave(l, xsavebuf, xsavelen); + } else if ((flags & _UC_FPU) != 0) { + process_write_fpregs_xmm(l, + (const struct fxsave *)&mcp->__fpregs.__fxsave); + } if ((flags & _UC_TLSBASE) != 0) lwp_setprivate(l, (void *)(uintptr_t)mcp->_mc_tlsbase); @@ -2188,7 +2333,12 @@ cpu_setmcontext(struct lwp *l, const mco l->l_sigstk.ss_flags &= ~SS_ONSTACK; mutex_exit(p->p_lock); - return 0; + /* Success! */ + error = 0; + +out: if (xsavebuf) + kmem_free(xsavebuf, xsavelen); + return error; } int diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/amd64/amd64/netbsd32_machdep.c --- a/sys/arch/amd64/amd64/netbsd32_machdep.c Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/amd64/amd64/netbsd32_machdep.c Thu Jul 09 18:58:51 2026 +0000 @@ -111,6 +111,9 @@ int check_sigcontext32(struct lwp *, con void netbsd32_buildcontext(struct lwp *, struct trapframe *, void *, sig_t, int); +static int cpu_getmcontext32_xsave(struct lwp *, mcontext32_t *, unsigned *, + const struct xsave_header *, size_t, struct xsave_header *); + #ifdef EXEC_AOUT /* * There is no native a.out -- this function is required @@ -219,6 +222,10 @@ netbsd32_sendsig_siginfo(const ksiginfo_ sig_t catcher = sa->sa_handler; struct trapframe *tf = l->l_md.md_regs; stack_t * const ss = &l->l_sigstk; + const struct xsave_header *xsavebuf = NULL; + size_t xsavelen = 0; + struct xsave_header *user_xsave = NULL; + char *sp; /* Do we need to jump onto the signal stack? */ onstack = @@ -226,15 +233,59 @@ netbsd32_sendsig_siginfo(const ksiginfo_ (sa->sa_flags & SA_ONSTACK) != 0; /* Allocate space for the signal handler context. */ - if (onstack) - fp = (struct netbsd32_sigframe_siginfo *) - ((char *)ss->ss_sp + ss->ss_size); - else - fp = (struct netbsd32_sigframe_siginfo *)tf->tf_rsp; + if (onstack) { + KASSERT(ss->ss_size >= MINSIGSTKSZ); + sp = (char *)ss->ss_sp + ss->ss_size; + } else { + sp = (char *)tf->tf_rsp; + } + + /* + * The maximum amount of space we might use, including padding + * for alignment, had better fit in MINSIGSTKSZ. + * + * If this changes because you have increased XSAVE_MAX_BYTES, + * you need to work out the ABI change for MINSIGSTKSZ. + */ + __CTASSERT(STACK_ALIGNBYTES32 + + sizeof(struct netbsd32_sigframe_siginfo) + + (XSAVE_ALIGN - 1) + XSAVE_MAX_BYTES <= MINSIGSTKSZ); + /* + * Find whether we need to allocate a separate XSAVE area, + * because the user program has used extended CPU state beyond + * the x87/SSE registers, or whether we can get by with just an + * FXSAVE area. + */ + if (process_xsave_needed_p(l)) { + process_read_xsave(l, &xsavebuf, &xsavelen); + KASSERT(xsavebuf != NULL); + KASSERT(xsavelen <= XSAVE_MAX_BYTES); + + KASSERT(!onstack || sp >= (char *)ss->ss_sp); + KASSERT(!onstack || + (size_t)(sp - (char *)ss->ss_sp) >= xsavelen); + sp -= xsavelen; + + KASSERT(!onstack || sp >= (char *)ss->ss_sp); + KASSERT(!onstack || (size_t)(sp - (char *)ss->ss_sp) >= + XSAVE_ALIGN - 1); + sp = (char *)((uintptr_t)sp & ~(XSAVE_ALIGN - 1)); + + KASSERT(!onstack || sp >= (char *)ss->ss_sp); + KASSERT(((uintptr_t)sp & (XSAVE_ALIGN - 1)) == 0); + user_xsave = (void *)sp; + } + + KASSERT(!onstack || sp >= (char *)ss->ss_sp); + KASSERT(!onstack || (size_t)(sp - (char *)ss->ss_sp) >= + STACK_ALIGNBYTES32 + sizeof(struct netbsd32_sigframe_siginfo)); + fp = (struct netbsd32_sigframe_siginfo *)sp; fp--; fp = (struct netbsd32_sigframe_siginfo *)((uintptr_t)fp & ~STACK_ALIGNBYTES32); + KASSERT(!onstack || (char *)fp >= (char *)ss->ss_sp); + KASSERT(((uintptr_t)fp & STACK_ALIGNBYTES32) == 0); /* Build stack frame for signal trampoline. */ switch (ps->sa_sigdesc[sig].sd_vers) { @@ -263,7 +314,20 @@ netbsd32_sendsig_siginfo(const ksiginfo_ mutex_exit(p->p_lock); cpu_getmcontext32(l, &frame.sf_uc.uc_mcontext, &frame.sf_uc.uc_flags); + + /* + * If we have to use XSAVE, copy out that area separately -- + * and be ready to bail if it failed. + */ + if (xsavebuf) { + error = cpu_getmcontext32_xsave(l, &frame.sf_uc.uc_mcontext, + &frame.sf_uc.uc_flags, xsavebuf, xsavelen, user_xsave); + if (error != 0) + goto relock; + } + error = copyout(&frame, fp, sizeof(frame)); +relock: mutex_enter(p->p_lock); if (error != 0) { @@ -798,8 +862,37 @@ cpu_setmcontext32(struct lwp *l, const m struct trapframe *tf = l->l_md.md_regs; const __greg32_t *gr = mcp->__gregs; struct proc *p = l->l_proc; + struct xsave_header *xsavebuf = NULL; + size_t xsavelen = 0; int error; + /* + * If there's an external XSAVE area, copy it in and validate + * it before we irreversibly modify the trapframe. + * + * We could check the length against the state components + * included, but we currently don't: if it's truncated, it will + * be as if the truncated part were zero-filled -- this is + * implemented in process_write_xsave, called a little below. + */ + if ((flags & _UC_XSAVE) != 0) { + const __greg32_t xsaveptr = + mcp->__fpregs.__fp_reg_set.__xsave.__xsaveptr; + + xsavelen = mcp->__fpregs.__fp_reg_set.__xsave.__xsavelen; + error = process_verify_xsavelen(l, xsavelen); + if (error != 0) + goto out; + xsavebuf = kmem_alloc(xsavelen, KM_SLEEP); + error = copyin((const void *)(uintptr_t)xsaveptr, xsavebuf, + xsavelen); + if (error != 0) + goto out; + error = process_verify_xsave(l, xsavebuf, xsavelen); + if (error != 0) + goto out; + } + /* Restore register context, if any. */ if ((flags & _UC_CPU) != 0) { /* @@ -832,7 +925,10 @@ cpu_setmcontext32(struct lwp *l, const m lwp_setprivate(l, (void *)(uintptr_t)mcp->_mc_tlsbase); /* Restore floating point register context, if any. */ - if ((flags & _UC_FPU) != 0) { + if ((flags & _UC_XSAVE) != 0) { + KASSERT(xsavebuf != NULL); + process_write_xsave(l, xsavebuf, xsavelen); + } else if ((flags & _UC_FPU) != 0) { /* Assume fxsave context */ process_write_fpregs_xmm(l, (const struct fxsave *) &mcp->__fpregs.__fp_reg_set.__fp_xmm_state); @@ -845,7 +941,12 @@ cpu_setmcontext32(struct lwp *l, const m l->l_sigstk.ss_flags &= ~SS_ONSTACK; mutex_exit(p->p_lock); - return 0; + /* Success! */ + error = 0; + +out: if (xsavebuf) + kmem_free(xsavebuf, xsavelen); + return error; } void @@ -892,6 +993,54 @@ cpu_getmcontext32(struct lwp *l, mcontex *flags |= _UC_FXSAVE | _UC_FPU; } +/* + * cpu_getmcontext32_xsave(l, mcp, flags, xsavebuf, xsavelen, user_xsave) + * + * Copy out xsavebuf[0..xsavelen) to user_xsave, set mcp to point + * there, and set _UC_XSAVE in flags. Caller must have already + * used cpu_getmcontext32 to initialize mcp's FXSAVE area. + * + * May fail if the copyout fails. + */ +static int +cpu_getmcontext32_xsave(struct lwp *l, mcontext32_t *mcp, unsigned int *flags, + const struct xsave_header *xsavebuf, size_t xsavelen, + struct xsave_header *user_xsave) +{ + int error; + + KASSERT(*flags & _UC_FPU); + KASSERT(*flags & _UC_FXSAVE); + KASSERT((uintptr_t)user_xsave == (__greg32_t)(uintptr_t)user_xsave); + KASSERT(xsavelen == (__greg32_t)xsavelen); + __CTASSERT(XSAVE_MAX_BYTES <= ~(__greg32_t)0); + + /* + * Copy out the XSAVE area. + */ + error = copyout(xsavebuf, user_xsave, xsavelen); + if (error != 0) + return error; + + /* + * Record a pointer to the real XSAVE area in the + * architecturally unused bits mcontext_t's FXSAVE area. + */ + mcp->__fpregs.__fp_reg_set.__xsave.__xsaveptr = + (__greg32_t)(uintptr_t)user_xsave; + mcp->__fpregs.__fp_reg_set.__xsave.__xsavelen = + (__greg32_t)xsavelen; + + /* + * Set the _UC_XSAVE flag so cpu_setmcontext will be able to + * restore the full state from the XSAVE area. + */ + *flags |= _UC_XSAVE; + + /* Success! */ + return 0; +} + void startlwp32(void *arg) { diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/amd64/include/mcontext.h --- a/sys/arch/amd64/include/mcontext.h Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/amd64/include/mcontext.h Thu Jul 09 18:58:51 2026 +0000 @@ -56,7 +56,28 @@ typedef __greg_t __gregset_t[_NGREG]; * which requires 16 byte alignment. However the mcontext version * is never directly accessed. */ -typedef char __fpregset_t[512] __aligned(8); +typedef union { + char __fxsave[512] __aligned(8); + struct { + /* + * `The XSAVE feature set does not use bytes 511:416; + * bytes 463:416 are reserved.' + * + * We take a part out of this to form a pointer to an + * external XSAVE area. This way, we can replicate the + * FXSAVE parts for the benefit of userland programs + * that aren't aware of the XSAVE pointer, have used + * the extended CPU registers (ymmN/zmmN/&c.), and want + * to examine the x87/SSE register state in a signal + * handler. The kernel does not use this part. + */ + char __fxsave[416]; + char __rsvd[48]; + __greg_t __xsaveptr; + __greg_t __xsavelen; + char __pad[32]; + } __xsave; +} __fpregset_t; typedef struct { __gregset_t __gregs; @@ -75,6 +96,7 @@ typedef struct { #define _UC_MACHINE_SET_PC(uc, pc) _UC_MACHINE_PC(uc) = (pc) #define _UC_TLSBASE _UC_MD_BIT19 +#define _UC_XSAVE _UC_MD_BIT20 /* * mcontext extensions to handle signal delivery. @@ -127,6 +149,13 @@ typedef struct { struct { char __fp_xmm[512]; } __fp_xmm_state; + struct { + char __fxsave[416]; + char __rsvd[48]; + __greg32_t __xsaveptr; + __greg32_t __xsavelen; + char __pad[40]; + } __xsave; } __fp_reg_set; int __fp_pad[33]; /* Historic padding */ } __fpregset32_t; diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/i386/i386/machdep.c --- a/sys/arch/i386/i386/machdep.c Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/i386/i386/machdep.c Thu Jul 09 18:58:51 2026 +0000 @@ -253,6 +253,9 @@ void initgdt(union descriptor *); static void i386_proc0_pcb_ldt_init(void); +static int cpu_getmcontext_xsave(struct lwp *, mcontext_t *, unsigned *, + const struct xsave_header *, size_t, struct xsave_header *); + int *esym; int *eblob; extern int boothowto; @@ -670,11 +673,72 @@ sendsig_siginfo(const ksiginfo_t *ksi, c struct sigacts *ps = p->p_sigacts; int onstack, error; int sig = ksi->ksi_signo; - struct sigframe_siginfo *fp = getframe(l, sig, &onstack), frame; + struct sigframe_siginfo *fp, frame; sig_t catcher = SIGACTION(p, sig).sa_handler; + struct trapframe *tf = l->l_md.md_regs; + const struct xsave_header *xsavebuf = NULL; + size_t xsavelen = 0; + struct xsave_header *user_xsave = NULL; + char *sp; KASSERT(mutex_owned(p->p_lock)); + /* Do we need to jump onto the signal stack? */ + onstack = + (l->l_sigstk.ss_flags & (SS_DISABLE | SS_ONSTACK)) == 0 && + (SIGACTION(p, sig).sa_flags & SA_ONSTACK) != 0; + + /* Allocate space for the signal handler context. */ + if (onstack) { + KASSERT(l->l_sigstk.ss_size >= MINSIGSTKSZ); + sp = ((char *)l->l_sigstk.ss_sp + l->l_sigstk.ss_size); + } else { + sp = (char *)tf->tf_esp; + } + + /* + * The maximum amount of space we might use, including padding + * for alignment, had better fit in MINSIGSTKSZ. + * + * If this changes because you have increased XSAVE_MAX_BYTES, + * you need to work out the ABI change for MINSIGSTKSZ. + */ + __CTASSERT(STACK_ALIGNBYTES + sizeof(struct sigframe_siginfo) + + (XSAVE_ALIGN - 1) + XSAVE_MAX_BYTES <= MINSIGSTKSZ); + + /* + * Find whether we need to allocate a separate XSAVE area, + * because the user program has used extended CPU state beyond + * the x87/SSE registers, or whether we can get by with just an + * FXSAVE area. + */ + if (process_xsave_needed_p(l)) { + process_read_xsave(l, &xsavebuf, &xsavelen); + KASSERT(xsavebuf != NULL); + KASSERT(xsavelen <= XSAVE_MAX_BYTES); + + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(!onstack || + sp - (char *)l->l_sigstk.ss_sp >= xsavelen); + sp -= xsavelen; + + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(!onstack || + sp - (char *)l->l_sigstk.ss_sp >= XSAVE_ALIGN - 1); + sp = (char *)((uintptr_t)sp & ~(XSAVE_ALIGN - 1)); + + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(((uintptr_t)sp & (XSAVE_ALIGN - 1)) == 0); + user_xsave = (void *)sp; + } + + /* + * Reserve space for an aligned struct sigframe_siginfo. + */ + KASSERT(!onstack || sp >= (char *)l->l_sigstk.ss_sp); + KASSERT(!onstack || (size_t)(sp - (char *)l->l_sigstk.ss_sp) >= + STACK_ALIGNBYTES + sizeof(struct sigframe_siginfo)); + fp = (struct sigframe_siginfo *)sp; fp--; fp = (struct sigframe_siginfo *)((uintptr_t)fp & ~STACK_ALIGNBYTES); @@ -694,7 +758,20 @@ sendsig_siginfo(const ksiginfo_t *ksi, c mutex_exit(p->p_lock); cpu_getmcontext(l, &frame.sf_uc.uc_mcontext, &frame.sf_uc.uc_flags); + + /* + * If we have to use XSAVE, copy out that area separately -- + * and be ready to bail if it failed. + */ + if (xsavebuf) { + error = cpu_getmcontext_xsave(l, &frame.sf_uc.uc_mcontext, + &frame.sf_uc.uc_flags, xsavebuf, xsavelen, user_xsave); + if (error != 0) + goto relock; + } + error = copyout(&frame, fp, sizeof(frame)); +relock: mutex_enter(p->p_lock); if (error != 0) { @@ -1660,6 +1737,50 @@ cpu_getmcontext(struct lwp *l, mcontext_ *flags |= _UC_FXSAVE | _UC_FPU; } +/* + * cpu_getmcontext_xsave(l, mcp, flags, xsavebuf, xsavelen, user_xsave) + * + * Copy out xsavebuf[0..xsavelen) to user_xsave, set mcp to point + * there, and set _UC_XSAVE in flags. Caller must have already + * used cpu_getmcontext to initialize mcp's FXSAVE area. + * + * May fail if the copyout fails. + */ +static int +cpu_getmcontext_xsave(struct lwp *l, mcontext_t *mcp, unsigned int *flags, + const struct xsave_header *xsavebuf, size_t xsavelen, + struct xsave_header *user_xsave) +{ + int error; + + KASSERT(*flags & _UC_FPU); + KASSERT(*flags & _UC_FXSAVE); + + /* + * Copy out the XSAVE area. + */ + error = copyout(xsavebuf, user_xsave, xsavelen); + if (error != 0) + return error; + + /* + * Record a pointer to the real XSAVE area in the + * architecturally unused bits mcontext_t's FXSAVE area. + */ + mcp->__fpregs.__fp_reg_set.__xsave.__xsaveptr = + (__greg_t)(uintptr_t)user_xsave; + mcp->__fpregs.__fp_reg_set.__xsave.__xsavelen = (__greg_t)xsavelen; + + /* + * Set the _UC_XSAVE flag so cpu_setmcontext will be able to + * restore the full state from the XSAVE area. + */ + *flags |= _UC_XSAVE; + + /* Success! */ + return 0; +} + int cpu_mcontext_validate(struct lwp *l, const mcontext_t *mcp) { @@ -1686,8 +1807,36 @@ cpu_setmcontext(struct lwp *l, const mco struct trapframe *tf = l->l_md.md_regs; const __greg_t *gr = mcp->__gregs; struct proc *p = l->l_proc; + struct xsave_header *xsavebuf = NULL; + size_t xsavelen = 0; int error; + /* + * If there's an external XSAVE area, copy it in and validate + * it before we irreversibly modify the trapframe. + * + * We could check the length against the state components + * included, but we currently don't: if it's truncated, it will + * be as if the truncated part were zero-filled -- this is + * implemented in process_write_xsave, called a little below. + */ + if ((flags & _UC_XSAVE) != 0) { + const __greg_t user_xsave = + mcp->__fpregs.__fp_reg_set.__xsave.__xsaveptr; + + xsavelen = mcp->__fpregs.__fp_reg_set.__xsave.__xsavelen; + error = process_verify_xsavelen(l, xsavelen); + if (error != 0) + goto out; + xsavebuf = kmem_alloc(xsavelen, KM_SLEEP); + error = copyin((const void *)user_xsave, xsavebuf, xsavelen); + if (error != 0) + goto out; + error = process_verify_xsave(l, xsavebuf, xsavelen); + if (error != 0) + goto out; + } + /* Restore register context, if any. */ if ((flags & _UC_CPU) != 0) { error = cpu_mcontext_validate(l, mcp); @@ -1719,7 +1868,10 @@ cpu_setmcontext(struct lwp *l, const mco lwp_setprivate(l, (void *)(uintptr_t)mcp->_mc_tlsbase); /* Restore floating point register context, if given. */ - if ((flags & _UC_FPU) != 0) { + if ((flags & _UC_XSAVE) != 0) { + KASSERT(xsavebuf != NULL); + process_write_xsave(l, xsavebuf, xsavelen); + } else if ((flags & _UC_FPU) != 0) { __CTASSERT(sizeof (struct fxsave) == sizeof mcp->__fpregs.__fp_reg_set.__fp_xmm_state); __CTASSERT(sizeof (struct save87) == @@ -1740,7 +1892,13 @@ cpu_setmcontext(struct lwp *l, const mco if (flags & _UC_CLRSTACK) l->l_sigstk.ss_flags &= ~SS_ONSTACK; mutex_exit(p->p_lock); - return (0); + + /* Success! */ + error = 0; + +out: if (xsavebuf) + kmem_free(xsavebuf, xsavelen); + return error; } #define DEV_IO 14 /* iopl for compat_10 */ diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/i386/include/mcontext.h --- a/sys/arch/i386/include/mcontext.h Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/i386/include/mcontext.h Thu Jul 09 18:58:51 2026 +0000 @@ -40,6 +40,7 @@ #define _UC_CLRSTACK _UC_MD_BIT17 #define _UC_VM _UC_MD_BIT18 #define _UC_TLSBASE _UC_MD_BIT19 +#define _UC_XSAVE _UC_MD_BIT20 /* * Layout of mcontext_t according to the System V Application Binary Interface, @@ -85,6 +86,27 @@ typedef struct { char __fp_xmm[512]; } __fp_xmm_state; /* x87 and xmm regs in fxsave format */ int __fp_fpregs[128]; + struct { + /* + * `The XSAVE feature set does not use bytes + * 511:416; bytes 463:416 are reserved.' + * + * We take a part out of this to form a pointer + * to an external XSAVE area. This way, we can + * replicate the FXSAVE parts for the benefit + * of userland programs that aren't aware of + * the XSAVE pointer, have used the extended + * CPU registers (ymmN/zmmN/&c.), and want to + * examine the x87/SSE register state in a + * signal handler. The kernel does not use + * this part. + */ + char __fxsave[416]; + char __rsvd[48]; + __greg_t __xsaveptr; + __greg_t __xsavelen; + char __pad[40]; + } __xsave; } __fp_reg_set; int __fp_pad[33]; /* Historic padding */ } __fpregset_t; diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/x86/include/cpu_extended_state.h --- a/sys/arch/x86/include/cpu_extended_state.h Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/x86/include/cpu_extended_state.h Thu Jul 09 18:58:51 2026 +0000 @@ -142,6 +142,8 @@ struct xsave_header { }; __CTASSERT(sizeof(struct xsave_header) == 512 + 64); +#define XSAVE_ALIGN 64 + /* * The ymm save area actually follows the xsave_header. */ diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/x86/include/fpu.h --- a/sys/arch/x86/include/fpu.h Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/x86/include/fpu.h Thu Jul 09 18:58:51 2026 +0000 @@ -46,6 +46,12 @@ int process_read_xstate(struct lwp *, st int process_verify_xstate(const struct xstate *); int process_write_xstate(struct lwp *, const struct xstate *); +bool process_xsave_needed_p(struct lwp *); +void process_read_xsave(struct lwp *, const struct xsave_header **, size_t *); +int process_verify_xsavelen(struct lwp *, size_t); +int process_verify_xsave(struct lwp *, const struct xsave_header *, size_t); +void process_write_xsave(struct lwp *, const struct xsave_header *, size_t); + #endif #endif /* _X86_FPU_H_ */ diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/x86/include/specialreg.h --- a/sys/arch/x86/include/specialreg.h Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/x86/include/specialreg.h Thu Jul 09 18:58:51 2026 +0000 @@ -182,16 +182,82 @@ "b\077" "X\0" /* - * Known FPU bits, only these get enabled. The save area is sized for all the - * fields below. + * XCR0_FPU: Known FPU bits, only these get enabled. The save area is + * sized for all the fields below. + * + * Any bits added to this will expand the extended CPU state that we + * may have to save and restore with XSAVE for userland processes, + * either in the kernel when preempting threads, or on the user's stack + * when delivering a signal. + * + * The kernel can dyanmically allocate larger sizes (on amd64, anyway, + * though not currently on i386 or Xen PV). But if the XSAVE area is + * expanded so much that it and mcontext_t exceed MINSIGSTKSZ + * (currently 8192), a userland ABI change and compatibility layer is + * required to accommodate that, because existing programs may use + * sigaltstack(2) with stacks sized for the old MINSIGSTKSZ. + * + * The current stack requirement is 3160 bytes of space plus up to + * 63+15+8=86 bytes of padding for alignment (could be reduced by + * around 512 bytes by having mcontext_t overlap with the XSAVE area a + * little in machdep.c cpu_getmcontext_xsave, but we don't do that + * right now): + * + * - mcontext_t (728 bytes: general registers and 512-byte FXSAVE area) + * - XSAVE header (576 bytes: 512 bytes of FXSAVE, 64 bytes of metadata) + * - AVX state: ymm0..ymm15 high 128-bit halves (256 bytes) + * - AVX-512 state: + * . k0..k7 opmask registers (64 bytes) + * . zmm0..zmm15 high 256-bit halves (512 bytes) + * . zmm16..zmm31 registers (1024 bytes) + * + * Likely future extensions that would expand the state beyond + * MINSIGSTKSZ: + * + * - AMX (Advanced Matrix Extensions) and ACE (AI Compute Extensions) + * state: + * . [AMX/ACE] TILECFG (64 bytes) + * . [AMX/ACE] TILEDATA (8192 bytes) + * . [ACE] SCALEDATA (128 bytes) + * + * As a precaution against ABI breakage, x86/identcpu.c will panic at + * boot if the XSAVE state size enabled in XCR0 exceeds MINSIGSTKSZ. + * + * References: + * + * - Intel 64 and IA-32 Architectures Software Developer's Manual, + * Volume 1: Basic Architecture, Intel, Order Number: 253665-092US, + * June 2026, Sec. 13.1 `XSAVE-Supported Features and State-Component + * Bitmaps', pp. 13-1 -- 13-2. + * https://web.archive.org/web/20260709150417/https://cdrdv2-public.intel.com/922477/253665-092-sdm-vol-1.pdf + * + * - AI Compute Extensions (ACE) Specification, x86 Ecosystem Advisory + * Group, Version 1.15, 2026-05-15, Sec 15.4.1 `XSAVE State + * Components', p. 86. + * https://web.archive.org/web/20260619062626/https://x86ecosystem.org/wp-content/uploads/2026/06/ACE_v1_Specification_public_1_15.pdf */ #if defined __i386__ || defined XENPV /* XXX XENPV PR kern/59371 */ #define XCR0_FPU (XCR0_X87 | XCR0_SSE | XCR0_YMM_Hi128 | \ XCR0_Opmask | XCR0_ZMM_Hi256 | XCR0_Hi16_ZMM) #else #define XCR0_FPU (XCR0_X87 | XCR0_SSE | XCR0_YMM_Hi128 | \ - XCR0_Opmask | XCR0_ZMM_Hi256 | XCR0_Hi16_ZMM | \ - XCR0_TILECFG | XCR0_TILEDATA) + XCR0_Opmask | XCR0_ZMM_Hi256 | XCR0_Hi16_ZMM) +#endif + +/* + * Maximum size of XSAVE state that we can handle without ABI changes + * to userland. Must match usage in cpu_getmcontext. Extra 8 is neeed + * on amd64 to have space for return address in 16-byte-aligned stack + * frame. + */ +#ifdef __x86_64__ +#define XSAVE_MAX_BYTES \ + (MINSIGSTKSZ - (8 + STACK_ALIGNBYTES + \ + sizeof(struct sigframe_siginfo) + (XSAVE_ALIGN - 1))) +#else +#define XSAVE_MAX_BYTES \ + (MINSIGSTKSZ - (STACK_ALIGNBYTES + \ + sizeof(struct sigframe_siginfo) + (XSAVE_ALIGN - 1))) #endif /* diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/x86/x86/fpu.c --- a/sys/arch/x86/x86/fpu.c Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/x86/x86/fpu.c Thu Jul 09 18:58:51 2026 +0000 @@ -1039,3 +1039,140 @@ process_write_xstate(struct lwp *l, cons return 0; } + +/* + * process_xsave_needed_p(l) + * + * True if l's FPU state requires XSAVE, false if it can be + * faithfully saved and restored with only FXSAVE at most. Since + * FXSAVE has been available for longer in mcontext_t (and thus + * more likely to be understood by userland software), and + * requires no external buffer for userland mcontext_t, it is + * preferable to use FXSAVE where possible instead of XSAVE. + */ +bool +process_xsave_needed_p(struct lwp *l) +{ + union savefpu *fpu_save; + + /* + * If we're not using XSAVE at all on this CPU, then this + * thread doesn't require it. + */ + if (x86_fpu_save < FPU_SAVE_XSAVE) + return false; + + /* + * If we only use x87 and SSE state on this CPU, then this + * thread doesn't require XSAVE -- FXSAVE is good enough. + */ + if ((x86_xsave_features & ~(XCR0_X87|XCR0_SSE)) == 0) + return false; + + /* + * If _this thread_ has yet to use any state other than x87 or + * SSE, then it doesn't require XSAVE -- FXSAVE is good enough. + */ + fpu_save = fpu_lwp_area(l); + if ((fpu_save->sv_xsave_hdr.xsh_xstate_bv & ~(XCR0_X87|XCR0_SSE)) == 0) + return false; + return true; +} + +/* + * process_read_xsave(l, &xsavebuf, &xsavelen) + * + * Set xsavebuf to l's current XSAVE area, and xsavelen to the + * size of that area to copy out to userland. It can later be + * restored with process_write_xsave. + */ +void +process_read_xsave(struct lwp *l, const struct xsave_header **xsavebufp, + size_t *xsavelenp) +{ + union savefpu *area = fpu_lwp_area(l); + + *xsavebufp = &area->sv_xsave_hdr; + KASSERT(((*xsavebufp)->xsh_xstate_bv & ~x86_xsave_features) == 0); + + /* + * XXX Consider shrinking this to just the components that are + * actually represented in XSTATE_BV. + */ + *xsavelenp = x86_fpu_save_size; +} + +/* + * process_verify_xsavelen(l, xsavelen) + * + * Verify that a putative XSAVE area length is plausible, before + * copying that many bytes in from userland. Return 0 on success, + * nonzero error code on failure. + */ +int +process_verify_xsavelen(struct lwp *l, size_t xsavelen) +{ + + if (xsavelen < sizeof(struct xsave_header)) + return EINVAL; + if (xsavelen > x86_fpu_save_size) + return EINVAL; + return 0; +} + +/* + * process_verify_xsave(l, xsavebuf, xsavelen) + * + * Verify that a putative XSAVE area from userland is valid. + * Return 0 on success, nonzero error code on failure. Caller + * must have already checked process_verify_xsavelen, before even + * trying to copyin the content of xsavebuf. + */ +int +process_verify_xsave(struct lwp *l, const struct xsave_header *xsavebuf, + size_t xsavelen) +{ + + KASSERT(process_verify_xsavelen(l, xsavelen) == 0); + + if ((xsavebuf->xsh_xstate_bv & ~x86_xsave_features) != 0) + return EINVAL; + /* + * XXX Consider verifying that any components claimed present + * are actually there within xsavelen. Not a big deal if they + * aren't, though: we will just zero-fill them, so it is as if + * they were there but all zero. + */ + return 0; +} + +/* + * process_write_xsave(l, xsavebuf, xsavelen) + * + * Given an XSAVE area copied in from userland, load l's FPU state + * from that area. If xstatelen is shorter than the CPU's + * x86_fpu_save_size, zero-fill it. Caller must validate it first + * with process_verify_xsave, along with any other mcontext + * validation before modifying l's state. + * + * Additionally, clear any invalid bits in the mxcsr, like + * process_write_fpregs_xmm does. XXX Is this necessary if + * there's no SSE state being restored? Can that happen? + */ +void +process_write_xsave(struct lwp *l, const struct xsave_header *xsavebuf, + size_t xsavelen) +{ + union savefpu *fpu_save = fpu_lwp_area(l); + + KASSERT(process_verify_xsave(l, xsavebuf, xsavelen) == 0); + + memcpy(fpu_save, xsavebuf, xsavelen); + memset((char *)fpu_save + xsavelen, 0, x86_fpu_save_size - xsavelen); + + /* + * Invalid bits in mxcsr or mxcsr_mask will cause faults. + */ + fpu_save->sv_xmm.fx_mxcsr_mask &= x86_fpu_mxcsr_mask; + fpu_save->sv_xmm.fx_mxcsr &= fpu_save->sv_xmm.fx_mxcsr_mask; +} diff -r 8002d24a07f6 -r 3b7740211446 sys/arch/x86/x86/identcpu.c --- a/sys/arch/x86/x86/identcpu.c Thu Jul 09 02:01:07 2026 +0000 +++ b/sys/arch/x86/x86/identcpu.c Thu Jul 09 18:58:51 2026 +0000 @@ -51,6 +51,7 @@ #include #include +#include #include #include @@ -844,9 +845,21 @@ cpu_probe_fpu(struct cpu_info *ci) /* Get component offsets and sizes for the save area */ for (i = XSAVE_YMM_Hi128; i < __arraycount(x86_xsave_offsets); i++) { if (x86_xsave_features & __BIT(i)) { + size_t size; x86_cpuid2(0x0d, i, descs); x86_xsave_offsets[i] = descs[1]; x86_xsave_sizes[i] = descs[0]; + + /* + * Verify the total XSAVE size requires no + * userland ABI change. + */ + size = x86_xsave_offsets[i] + x86_xsave_sizes[i]; + if (size > XSAVE_MAX_BYTES) { + panic("XSAVE size >=%zu" + " exceeds ABI maximum %zu", + size, (size_t)XSAVE_MAX_BYTES); + } } } } diff -r 8002d24a07f6 -r 3b7740211446 tests/kernel/t_signal_and_fpu.c --- a/tests/kernel/t_signal_and_fpu.c Thu Jul 09 02:01:07 2026 +0000 +++ b/tests/kernel/t_signal_and_fpu.c Thu Jul 09 18:58:51 2026 +0000 @@ -384,8 +384,7 @@ ATF_TC_HEAD(ymm, tc) } ATF_TC_BODY(ymm, tc) { - test_signal_fpu(&ymm_supported, &test_ymm, &trash_ymm, - "PR kern/60426: Signal handler corrupts AVX (YMM) registers"); + test_signal_fpu(&ymm_supported, &test_ymm, &trash_ymm, NULL); } #endif