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:
parent
a2d987db5f
commit
0e39e711f1
6 changed files with 69 additions and 0 deletions
8
Makefile
8
Makefile
|
|
@ -425,6 +425,14 @@ CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -MP -ggdb
|
|||
CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT)
|
||||
CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA)
|
||||
|
||||
# The kernel keeps its hands off the FPU and SSE registers: they belong to
|
||||
# whichever process is running, and are saved only when it is switched out
|
||||
# (see "The FPU" in kernel/proc/proc.c). Without this GCC vectorises struct
|
||||
# copies and memset into %xmm, and an interrupt then scribbles over the
|
||||
# registers of the program it interrupted. Kernel objects only -- cmd/ is
|
||||
# built with these CFLAGS too and is welcome to use SSE.
|
||||
$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o: CFLAGS += -mgeneral-regs-only
|
||||
|
||||
# lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip).
|
||||
# Only the network files are compiled with these: putting them in the global
|
||||
# CFLAGS would let any kernel file include lwIP by accident, and the stack is
|
||||
|
|
|
|||
|
|
@ -308,9 +308,18 @@ struct proc {
|
|||
uint64 slice_used; // how much of it has been spent, in nanoseconds
|
||||
uint64 nivcsw, nvcsw; // preempted / voluntary context switches
|
||||
|
||||
// The process's x87/SSE registers while it is off the CPU, as fxsave
|
||||
// writes them; and a copy taken when a signal handler starts, put back by
|
||||
// rt_sigreturn. See "The FPU" above scheduler() in kernel/proc/proc.c.
|
||||
uint8 fpu[512] __attribute__((aligned(16)));
|
||||
uint8 sigfpu[512] __attribute__((aligned(16)));
|
||||
};
|
||||
#define FD_CLOEXEC 1
|
||||
|
||||
// What a process's FPU starts as: x87 control word 0x37F, every exception
|
||||
// masked, MXCSR 0x1F80 -- the state FNINIT and a reset leave, and what the
|
||||
// System V ABI promises a new program. In proc.c.
|
||||
extern const uint8 fpu_default[512];
|
||||
|
||||
// wait(2) status encoding, as understood by musl's WIFEXITED/WEXITSTATUS/
|
||||
// WIFSIGNALED/WTERMSIG macros:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,19 @@ rdtsc(void)
|
|||
return ((uint64)hi << 32) | lo;
|
||||
}
|
||||
|
||||
// The x87/MMX/SSE register file, to and from a 512-byte, 16-byte-aligned area.
|
||||
static inline void
|
||||
fxsave(void *area)
|
||||
{
|
||||
asm volatile("fxsave64 (%0)" : : "r"(area) : "memory");
|
||||
}
|
||||
|
||||
static inline void
|
||||
fxrstor(const void *area)
|
||||
{
|
||||
asm volatile("fxrstor64 (%0)" : : "r"(area) : "memory");
|
||||
}
|
||||
|
||||
static inline void
|
||||
insl(int port, void *addr, int cnt)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue