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
318 lines
13 KiB
C
318 lines
13 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "proc.h"
|
|
#include "x86.h"
|
|
#include "syscall.h"
|
|
#include "uaccess.h"
|
|
#include "errno.h"
|
|
#include "posix.h" /* SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK, signal numbers */
|
|
|
|
// Linux sigset_t puts signal N at bit N-1; kernel-internal masks
|
|
// (pending_signals / signal_mask / deliver_signals) put signal N at bit N.
|
|
// Convert at the user boundary — storing user sets raw silently shifted
|
|
// every mask by one signal (blocking SIGCHLD actually blocked SIGCONT),
|
|
// which let mksh's SIGCHLD handler run inside the very critical sections
|
|
// sigprocmask() was guarding, wrecking its job table.
|
|
static uint64 sigset_from_user(uint64 uset) { return uset << 1; }
|
|
static uint64 sigset_to_user(uint64 kset) { return kset >> 1; }
|
|
|
|
addr_t
|
|
sys_sigfillset(void)
|
|
{
|
|
uint64 *set;
|
|
if (argptr(0, (char**)&set, sizeof(uint64)) < 0)
|
|
return -1;
|
|
if (set) *set = ~0ULL;
|
|
return 0;
|
|
}
|
|
|
|
addr_t
|
|
sys_sigprocmask(void)
|
|
{
|
|
int how;
|
|
addr_t setp, oldsetp;
|
|
argint(0, &how);
|
|
argaddr(1, &setp);
|
|
argaddr(2, &oldsetp);
|
|
|
|
// oldset is optional (NULL = don't return previous mask)
|
|
if (oldsetp) {
|
|
uint64 old = sigset_to_user(myproc()->signal_mask);
|
|
put_user(old, oldsetp);
|
|
}
|
|
// set is optional (NULL = just query)
|
|
if (setp) {
|
|
uint64 uset;
|
|
if (copy_from_user(&uset, setp, sizeof(uset)) == 0) {
|
|
uint64 set = sigset_from_user(uset);
|
|
if (how == SIG_BLOCK) myproc()->signal_mask |= set;
|
|
else if (how == SIG_UNBLOCK) myproc()->signal_mask &= ~set;
|
|
else myproc()->signal_mask = set;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
addr_t
|
|
sys_sigwait(void)
|
|
{
|
|
uint64 *set_ptr;
|
|
int *sig_ptr;
|
|
if (argptr(0, (char**)&set_ptr, sizeof(uint64)) < 0 ||
|
|
argptr(1, (char**)&sig_ptr, sizeof(int)) < 0)
|
|
return -1;
|
|
uint64 set = sigset_from_user(*set_ptr);
|
|
acquire(&tickslock);
|
|
while (!(myproc()->pending_signals & set)) {
|
|
if (myproc()->killed) { release(&tickslock); return -1; }
|
|
sleep(myproc(), &tickslock);
|
|
}
|
|
for (int i = 0; i < 64; i++) {
|
|
if ((myproc()->pending_signals & set) & (1ULL << i)) {
|
|
myproc()->pending_signals &= ~(1ULL << i);
|
|
*sig_ptr = i;
|
|
break;
|
|
}
|
|
}
|
|
release(&tickslock);
|
|
return 0;
|
|
}
|
|
|
|
// musl's struct sigaction (musl 1.2.5 signal.h) for this target:
|
|
// sa_handler (8 bytes) at offset 0, then a 128-byte sigset_t (musl uses
|
|
// 128/sizeof(long) = 16 longs regardless of NSIG), sa_flags (4 bytes) +
|
|
// padding, then sa_restorer (8 bytes) at offset 144. We only care about
|
|
// those two 8-byte fields.
|
|
#define SA_HANDLER_OFF 0
|
|
#define SA_RESTORER_OFF 144
|
|
|
|
// rt_sigaction: store the handler + restorer so deliver_signals() (called
|
|
// from syscall()'s tail, kernel/sys/syscall.c) can actually jump to user
|
|
// code instead of silently doing nothing. sig 0 is invalid (matches
|
|
// sigaction(2)); we don't validate against NSIG beyond our own array size.
|
|
//
|
|
// User accesses go through copy_from_user/put_user (uaccess.h), which are
|
|
// page-safe. An earlier version bulk-copied the ~152-byte struct through a
|
|
// single uva2ka() translation of its start address; when the user's
|
|
// act/oldact pointer landed close enough to a page boundary, the write ran
|
|
// past the mapped page into an unrelated physical page, silently crashing
|
|
// every freshly exec'd /bin/sh (mksh calls sigaction() during startup).
|
|
addr_t
|
|
sys_rt_sigaction(void)
|
|
{
|
|
int sig;
|
|
addr_t actp, oldactp;
|
|
argint(0, &sig);
|
|
argaddr(1, &actp);
|
|
argaddr(2, &oldactp);
|
|
if (sig <= 0 || sig >= 32) return -1;
|
|
|
|
if (oldactp) {
|
|
put_user(myproc()->sig_handler[sig], oldactp + SA_HANDLER_OFF);
|
|
put_user(myproc()->sig_restorer[sig], oldactp + SA_RESTORER_OFF);
|
|
}
|
|
if (actp) {
|
|
copy_from_user(&myproc()->sig_handler[sig], actp + SA_HANDLER_OFF,
|
|
sizeof(myproc()->sig_handler[sig]));
|
|
copy_from_user(&myproc()->sig_restorer[sig], actp + SA_RESTORER_OFF,
|
|
sizeof(myproc()->sig_restorer[sig]));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// rt_sigreturn: entered via `syscall` (rax=15) from musl's __restore_rt
|
|
// trampoline, which is exactly what we set as the manufactured return
|
|
// address in deliver_signals() below — the handler function's own `ret`
|
|
// lands here once it's done. Restore the trapframe deliver_signals()
|
|
// stashed before redirecting to the handler, so syscall()'s normal
|
|
// sysretq path resumes the interrupted code exactly where it left off.
|
|
addr_t
|
|
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.
|
|
if (myproc()->sigsusp_active) {
|
|
myproc()->signal_mask = myproc()->sigsusp_oldmask;
|
|
myproc()->sigsusp_active = 0;
|
|
}
|
|
// syscall() does `tf->rax = <our return value>` right after this
|
|
// returns — hand back the already-restored rax so that assignment is
|
|
// a harmless no-op instead of clobbering what we just restored.
|
|
return saved_rax;
|
|
}
|
|
|
|
// Called from syscall()'s tail (kernel/sys/syscall.c), right before
|
|
// returning to user mode, for every syscall a process makes (this is the
|
|
// only "check point" we have without also hooking the interrupt-return
|
|
// path in trap.c — sufficient for what actually needed this: a process
|
|
// blocked in a syscall, like sigsuspend(), waiting for its own next
|
|
// syscall return to notice a signal arrived).
|
|
//
|
|
// If there's a pending, unblocked signal with a registered handler,
|
|
// redirect tf to it: rdi = signal number (SysV ABI first arg), rip =
|
|
// handler.
|
|
//
|
|
// The return address (what the handler's own `ret` lands on) can't be
|
|
// sig_restorer[sig] — this musl build has no SA_RESTORER defined at all
|
|
// (checked: not defined anywhere in musl 1.2.5), so sigaction() never
|
|
// fills it in; it's always 0. Pushing 0 as a return address is exactly
|
|
// what crashed every mksh instance the first time a signal was ever
|
|
// actually delivered (handler ran, `ret` jumped to address 0). Instead,
|
|
// inject a tiny trampoline directly onto the user's own stack — 7 bytes
|
|
// of machine code for `mov eax, 15; syscall` (rt_sigreturn) — below the
|
|
// current stack pointer (clear of the SysV red zone) and point the
|
|
// return address at that, the standard way a kernel supplies sigreturn
|
|
// when the C library doesn't hand it one itself.
|
|
void
|
|
deliver_signals(struct trapframe *tf)
|
|
{
|
|
if (myproc()->in_signal) return; // no nested delivery
|
|
uint64 deliverable = myproc()->pending_signals & ~myproc()->signal_mask;
|
|
if (!deliverable) return;
|
|
|
|
for (int sig = 1; sig < 32; sig++) {
|
|
if (!(deliverable & (1ULL << sig))) continue;
|
|
|
|
// SIG_DFL is 0 and SIG_IGN is 1 -- neither is an address to jump to.
|
|
//
|
|
// Only the 0 case was checked here. A process that had asked to
|
|
// ignore a signal got its handler "called" at address 1, which is a
|
|
// page fault at rip=0x1 and a dead process. Nothing sent SIGPIPE
|
|
// until pipes learned to, at which point every shell that ignores
|
|
// SIGPIPE -- which is every shell -- died the first time a pipeline
|
|
// ended early. `cat big | head -c 5` was enough.
|
|
//
|
|
// The default action for a signal with no handler has already been
|
|
// applied by sigsend(); all that is left here is to stop it being
|
|
// pending, which the old `continue` never did, so an ignored signal
|
|
// was re-examined on the way out of every syscall forever after.
|
|
if (myproc()->sig_handler[sig] <= 1) {
|
|
myproc()->pending_signals &= ~(1ULL << sig);
|
|
continue;
|
|
}
|
|
|
|
uint64 tramp_addr = (tf->rsp - 256) & ~0xFULL;
|
|
uint8 *tramp = (uint8*)uva2ka(myproc()->pgdir, (char*)tramp_addr);
|
|
if (!tramp) continue; // can't deliver safely; try another pending signal
|
|
|
|
uint64 sp = tramp_addr - 8;
|
|
uint64 *retaddr = (uint64*)uva2ka(myproc()->pgdir, (char*)sp);
|
|
if (!retaddr) continue;
|
|
|
|
// The return trampoline: `mov eax, <rt_sigreturn> ; syscall`.
|
|
//
|
|
// **Which number depends on the process's ABI**, and it used to be
|
|
// hardcoded to 15. Fifteen is Linux's rt_sigreturn, which is right for
|
|
// anything musl-linked -- every port, and the shells. It is *native*
|
|
// syscall 15 for a first-party binary, and native 15 is `open`. So a
|
|
// handler in any cmd/ program ran, returned into the trampoline, called
|
|
// open() with the handler's leftover registers as arguments, got
|
|
// -ENOENT, and carried on executing the bytes after the trampoline --
|
|
// which are stack, not code.
|
|
//
|
|
// See table.inc: SYSCALL(97, 15, rt_sigreturn). syscall_map is the
|
|
// thing that says which numbering this process speaks.
|
|
uint32 sret = myproc()->syscall_map ? 15 : SYS_rt_sigreturn;
|
|
tramp[0] = 0xb8; // mov eax, imm32
|
|
tramp[1] = (uint8)(sret );
|
|
tramp[2] = (uint8)(sret >> 8);
|
|
tramp[3] = (uint8)(sret >> 16);
|
|
tramp[4] = (uint8)(sret >> 24);
|
|
tramp[5] = 0x0f; tramp[6] = 0x05; // syscall
|
|
*retaddr = tramp_addr;
|
|
|
|
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;
|
|
tf->rdi = (uint64)sig;
|
|
// Both return paths must point at the handler: iretq-based returns
|
|
// use tf->rip, but syscall_trapret exits via sysretq, which takes
|
|
// the user RIP from RCX and *discards* tf->rip (trapasm.S pops rcx
|
|
// from the frame, then `addq $40` throws rip/cs/rflags away). With
|
|
// only tf->rip set, the "delivered" handler never actually ran on
|
|
// the syscall path: user code resumed at the old rcx with the
|
|
// shifted rsp, and its ret fell into the sigreturn trampoline,
|
|
// silently undoing everything. exec() handles this same quirk by
|
|
// setting tf->rcx = elf.entry.
|
|
tf->rip = myproc()->sig_handler[sig];
|
|
tf->rcx = myproc()->sig_handler[sig];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// sigaltstack: set/get alternate signal stack.
|
|
// We don't deliver signals, so just store/return zeros.
|
|
addr_t sys_sigaltstack(void) { return 0; }
|
|
|
|
// rt_sigpending: write the set of pending signals to user buffer.
|
|
addr_t sys_rt_sigpending(void)
|
|
{
|
|
addr_t setp;
|
|
argaddr(0, &setp);
|
|
if (setp) {
|
|
uint64 pend = sigset_to_user(myproc()->pending_signals);
|
|
put_user(pend, setp);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// rt_sigtimedwait: wait for a signal from the set; returns -EAGAIN immediately.
|
|
addr_t sys_rt_sigtimedwait(void) { return -EAGAIN; }
|
|
|
|
// rt_sigsuspend: atomically replace the signal mask and suspend until a
|
|
// signal not blocked by the *temporary* mask is pending, then return -EINTR.
|
|
//
|
|
// The atomic mask swap is the entire point of sigsuspend(2): mksh blocks
|
|
// SIGCHLD while it inspects its job table, then calls sigsuspend() with the
|
|
// old (SIGCHLD-unblocked) mask to wait for a child. The previous stub
|
|
// ignored the mask argument and just slept one tick — SIGCHLD stayed
|
|
// blocked, deliver_signals() could never fire the handler, and the shell
|
|
// hung forever after running any external command (unreaped zombie child,
|
|
// shell parked in sigsuspend: the "can't type after a command" hang).
|
|
//
|
|
// The temporary mask must still be in effect when deliver_signals() runs at
|
|
// syscall()'s tail (that's what allows the delivery), so the old mask can't
|
|
// be restored here — sys_rt_sigreturn() restores it once the handler
|
|
// finishes, via sigsusp_oldmask/sigsusp_active.
|
|
addr_t sys_rt_sigsuspend(void)
|
|
{
|
|
addr_t maskp;
|
|
uint64 newmask;
|
|
argaddr(0, &maskp);
|
|
|
|
uint64 oldmask = myproc()->signal_mask;
|
|
if (maskp && copy_from_user(&newmask, maskp, sizeof(newmask)) == 0)
|
|
myproc()->signal_mask = sigset_from_user(newmask);
|
|
|
|
acquire(&tickslock);
|
|
while (!(myproc()->pending_signals & ~myproc()->signal_mask) && !myproc()->killed)
|
|
sleep(&ticks, &tickslock);
|
|
release(&tickslock);
|
|
|
|
// Will any pending signal actually reach a handler? If so, the mask is
|
|
// restored by sigreturn after the handler runs; otherwise (no handler
|
|
// installed, or we were killed) restore it right now.
|
|
uint64 deliverable = myproc()->pending_signals & ~myproc()->signal_mask;
|
|
int will_deliver = 0;
|
|
for (int sig = 1; sig < 32; sig++)
|
|
if ((deliverable & (1ULL << sig)) && myproc()->sig_handler[sig])
|
|
{ will_deliver = 1; break; }
|
|
|
|
if (will_deliver && !myproc()->in_signal) {
|
|
myproc()->sigsusp_oldmask = oldmask;
|
|
myproc()->sigsusp_active = 1;
|
|
} else {
|
|
myproc()->signal_mask = oldmask;
|
|
}
|
|
return -EINTR;
|
|
}
|