SchOS/kernel/sys/sys_sig.c
Nexoniarz f4d7f9100d Threads
A thread is a struct proc that shares three counted objects with others of
its group: the address space (struct mm: page tables, heap top, VMAs and a
lock for changing them), the descriptor table, and the signal dispositions.
clone() shares what its flags say and fork() copies.

- clone(): pthread_create's shape (VM, FILES, SIGHAND, THREAD, SETTLS,
  PARENT_SETTID, CHILD_CLEARTID) and posix_spawn's (VM, VFORK, a stack).
- futex: WAIT, WAIT_BITSET with timeouts, WAKE, REQUEUE, CMP_REQUEUE, keyed
  by the word's kernel address so shared pages meet. set_tid_address keeps
  the word that exit zeroes and wakes, for pthread_join.
- exit ends a thread; exit_group, a fatal signal, or the leader exiting ends
  them all. reapd frees exited threads; the leader is reaped by its parent
  once the rest are gone. getpid is the group, gettid the thread.
- Stops and resumes reach every thread; other process signals the leader.
- exec kills the other threads and unshares the dispositions.
- No TLB shootdown, so pages unmapped from a shared address space are freed
  only once every CPU has reloaded CR3 or is idle.
- procfs: /proc lists processes, task/ lists threads, comm, Threads:, Tgid.
- The console honours VMIN 0 / VTIME (btop's input drain hung on it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 03:03:50 +01:00

357 lines
15 KiB
C

#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "core/proc.h"
#include "arch/x86.h"
#include "core/syscall.h"
#include "core/uaccess.h"
#include "uapi/errno.h"
#include "uapi/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;
}
// The native musl hands over its own struct sigaction (musl 1.2.5 signal.h):
// sa_handler at 0, a 128-byte sigset_t, sa_flags (4 bytes) at 136, then
// sa_restorer at 144.
//
// A Linux-ABI process does not. Its musl converts to the kernel's
// k_sigaction first -- handler at 0, flags at 8, restorer at 16, mask at 24,
// thirty-two bytes in all -- and this used the native offsets for both, so
// every Alpine program that asked for the old action had 8 bytes written 112
// bytes past the end of the struct, into whatever its stack held there.
#define SA_HANDLER_OFF 0
#define SA_FLAGS_OFF(p) ((p)->syscall_map ? 8 : 136)
#define SA_RESTORER_OFF(p) ((p)->syscall_map ? 16 : 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;
struct proc *p = myproc();
if (oldactp) {
put_user(p->sighand->handler[sig], oldactp + SA_HANDLER_OFF);
put_user(p->sighand->restorer[sig], oldactp + SA_RESTORER_OFF(p));
if (p->syscall_map) {
put_user(p->sighand->flags[sig], oldactp + SA_FLAGS_OFF(p));
} else {
uint32 fl = (uint32)p->sighand->flags[sig];
put_user(fl, oldactp + SA_FLAGS_OFF(p));
}
}
if (actp) {
uint64 h, r, fl = 0;
if (copy_from_user(&h, actp + SA_HANDLER_OFF, sizeof(h)) < 0 ||
copy_from_user(&r, actp + SA_RESTORER_OFF(p), sizeof(r)) < 0 ||
copy_from_user(&fl, actp + SA_FLAGS_OFF(p),
p->syscall_map ? 8 : 4) < 0)
return -EFAULT;
// SIGKILL and SIGSTOP cannot be caught or ignored.
if (sig == SIGKILL || sig == SIGSTOP)
return -EINVAL;
p->sighand->handler[sig] = h;
p->sighand->restorer[sig] = r;
p->sighand->flags[sig] = fl;
}
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.
//
// Except for a signal that was blocked when it arrived: sigsend()
// left it pending without acting, and now that it is deliverable its
// default action happens here.
// SIGSTOP cannot be caught or ignored; the other three stop only by
// default. Stopped here and resumed here, by SIGCONT or SIGKILL.
if (sig == SIGSTOP ||
(sig_default_stops(sig) && myproc()->sighand->handler[sig] == 0)) {
myproc()->pending_signals &= ~(1ULL << sig);
proc_stop(sig);
if (myproc()->killed)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
continue;
}
if (myproc()->sighand->handler[sig] <= 1) {
myproc()->pending_signals &= ~(1ULL << sig);
if (myproc()->sighand->handler[sig] == 0 && sig_default_kills(sig)) {
myproc()->killed = 1;
if (myproc()->killsig == 0)
myproc()->killsig = 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()->sighand->handler[sig];
tf->rcx = myproc()->sighand->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()->sighand->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;
}