fpu: save each process's x87/SSE registers; keep the kernel off them

Nothing ever saved the FPU and SSE registers on a context switch.
musl's memcpy, the compiler's struct copies and any arithmetic on
doubles keep live values in %xmm, so two processes sharing a CPU wrote
over each other's: two shells counting in a loop on one processor took
a NULL+8 page fault within seconds, at a different rip every time. It
reproduces on kernels from before this week too -- it had always been
there, and every crash nobody could explain was a candidate.

sched() now fxsaves the process leaving the CPU and scheduler()
fxrstors the one arriving. A new process starts from the ABI's state
(FCW 0x37F, MXCSR 0x1F80), fork copies the parent's live registers,
exec resets them, and a signal handler's are put back by rt_sigreturn
so the interrupted code finds what it had.

The kernel was free to use %xmm too -- GCC vectorised struct copies
and memset into 153 instructions of it -- so an interrupt could
clobber the registers of the program it interrupted with no switch at
all. Kernel, lwIP and uACPI objects are built -mgeneral-regs-only now
and the image has none; cmd/ keeps SSE.

Checked under QEMU: three concurrent loops three times over, on 1 CPU
and on 4, no faults; before, every run faulted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 15:02:01 +02:00
commit 0e39e711f1
6 changed files with 69 additions and 0 deletions

View file

@ -389,6 +389,10 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev)
console_takeover();
switchuvm(myproc());
// A new program starts with the FPU as the ABI promises it, not with the
// old one's registers.
memmove(myproc()->fpu, fpu_default, sizeof(myproc()->fpu));
fxrstor(myproc()->fpu);
freevm(oldpgdir);
kfree((char*)scratch);
kfree(bounce);

View file

@ -319,6 +319,7 @@ found:
// fork() then copies the parent's vruntime over this, which is what stops a
// process from getting a fresh share of the CPU simply by forking.
sched_init_proc(p, 0);
memmove(p->fpu, fpu_default, sizeof(p->fpu));
p->signal_mask = 0;
p->in_signal = 0;
@ -434,6 +435,10 @@ fork(void)
if((np = allocproc()) == 0)
return -1;
// The parent's registers are live in the CPU -- the kernel never touches
// them -- so the child's copy is taken straight from there.
fxsave(np->fpu);
if((np->pgdir = copyuvm(myproc()->pgdir, myproc()->sz)) == 0){
np->kstack = 0; // the slot keeps its stack; see kstacks[]
np->state = UNUSED;
@ -815,6 +820,29 @@ proc_wedge(const char *name)
return found;
}
// --- The FPU ------------------------------------------------------------
//
// Every process has its own x87 and SSE registers, and they were never saved.
// musl's memcpy, the compiler's struct copies and anything doing arithmetic on
// doubles keep live values in %xmm across a context switch, so two processes
// sharing a CPU wrote over each other's: two shells counting in a loop on one
// processor took a NULL+8 fault within seconds, at a different rip each time.
// It had always been there, and every crash with no cause anyone could find
// was a candidate.
//
// The kernel is built with -mgeneral-regs-only, so it never touches these
// registers itself and they stay the user's from trap entry to return. That
// makes saving them a switch-time job and nothing more: sched() saves the
// process leaving the CPU, scheduler() restores the one arriving. No lazy
// switching (CR0.TS and a #NM handler), which would save the copy for a
// process that never uses SSE -- there is no such process; musl's string
// functions use it.
__attribute__((aligned(16)))
const uint8 fpu_default[512] = {
[0] = 0x7F, [1] = 0x03, // FCW 0x037F
[24] = 0x80, [25] = 0x1F, // MXCSR 0x1F80
};
// --- The scheduler ------------------------------------------------------
//
// It was round-robin: walk ptable from end to end, run everything RUNNABLE in
@ -1179,6 +1207,7 @@ scheduler(void)
proc_set(p);
switchuvm(p);
fxrstor(p->fpu);
p->state = RUNNING;
swtch(&mycpu()->scheduler, p->context);
switchkvm();
@ -1211,6 +1240,7 @@ sched(void)
// way out means the vruntime the scheduler then compares is already
// up to date rather than a tick stale.
sched_charge(myproc());
fxsave(myproc()->fpu);
swtch(&myproc()->context, mycpu()->scheduler);
mycpu()->intena = intena;

View file

@ -2,6 +2,7 @@
#include "defs.h"
#include "param.h"
#include "proc.h"
#include "x86.h"
#include "syscall.h"
#include "uaccess.h"
#include "errno.h"
@ -131,6 +132,7 @@ sys_rt_sigreturn(void)
{
addr_t saved_rax = myproc()->saved_sigtf.rax;
*myproc()->tf = myproc()->saved_sigtf;
fxrstor(myproc()->sigfpu); // what the interrupted code had
myproc()->in_signal = 0;
// If this handler interrupted a sigsuspend(), put its pre-suspend
// signal mask back now that the handler has finished.
@ -226,6 +228,9 @@ deliver_signals(struct trapframe *tf)
myproc()->pending_signals &= ~(1ULL << sig);
myproc()->saved_sigtf = *tf;
// The handler is free to use %xmm; the code it interrupted had values
// there. Put back by rt_sigreturn.
fxsave(myproc()->sigfpu);
myproc()->in_signal = 1;
tf->rsp = sp;