summaryrefslogtreecommitdiff
path: root/na/kernel/signal.cc
diff options
context:
space:
mode:
Diffstat (limited to 'na/kernel/signal.cc')
-rw-r--r--na/kernel/signal.cc442
1 files changed, 442 insertions, 0 deletions
diff --git a/na/kernel/signal.cc b/na/kernel/signal.cc
new file mode 100644
index 0000000..53d26da
--- /dev/null
+++ b/na/kernel/signal.cc
@@ -0,0 +1,442 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Signal Delivery — Queue, Dequeue, Default Actions, and do_signal
+ *
+ * Implements the core signal infrastructure:
+ * - send_signal(): enqueue a signal to a task's pending set
+ * - force_sig_fault(): fault-induced signal (trap handler API)
+ * - do_signal(): called on return-to-user, delivers pending signals
+ * - next_signal(): dequeue the highest-priority deliverable signal
+ *
+ * Signal frame construction (arch-specific) is in arch/riscv/signal.cc.
+ *
+ * Reference: Linux kernel/signal.c
+ */
+
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/wait.h>
+#include <lume/console.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <arch/trapframe.h>
+
+/* Forward declaration: arch-specific signal frame setup */
+int setup_signal_frame(arch::TrapFrame* tf, int signo,
+ kernel_sigaction* ka, kernel_siginfo* info);
+
+/* ================================================================== */
+/* sighand_struct lifecycle (refcount-shared, Linux copy_sighand) */
+/* ================================================================== */
+
+sighand_struct* alloc_sighand()
+{
+ auto* sh = new sighand_struct;
+ if (sh) sh->init();
+ return sh;
+}
+
+sighand_struct* copy_sighand(sighand_struct* parent)
+{
+ /* Linux: kernel/fork.c:copy_sighand() when !CLONE_SIGHAND copies
+ * the action table verbatim so the child inherits dispositions
+ * without sharing future modifications. */
+ if (!parent) return alloc_sighand();
+ auto* sh = new sighand_struct;
+ if (!sh) return nullptr;
+ sh->init();
+ LockGuard guard(parent->siglock);
+ for (int i = 0; i < _NSIG; i++) sh->action[i] = parent->action[i];
+ return sh;
+}
+
+void get_sighand(sighand_struct* sh)
+{
+ if (!sh) return;
+ LockGuard guard(sh->siglock);
+ sh->refcount++;
+}
+
+void put_sighand(sighand_struct* sh)
+{
+ if (!sh) return;
+ uint32 remaining;
+ {
+ LockGuard guard(sh->siglock);
+ remaining = --sh->refcount;
+ }
+ if (remaining == 0) delete sh;
+}
+
+/* ================================================================== */
+/* next_signal — find lowest-numbered deliverable signal */
+/* ================================================================== */
+
+/* Manual count-trailing-zeros (no libgcc __ctzdi2 dependency) */
+static inline int sig_ctz64(uint64 x)
+{
+ if (x == 0) return 64;
+ int n = 0;
+ if ((x & 0xFFFFFFFF) == 0) { n += 32; x >>= 32; }
+ if ((x & 0x0000FFFF) == 0) { n += 16; x >>= 16; }
+ if ((x & 0x000000FF) == 0) { n += 8; x >>= 8; }
+ if ((x & 0x0000000F) == 0) { n += 4; x >>= 4; }
+ if ((x & 0x00000003) == 0) { n += 2; x >>= 2; }
+ if ((x & 0x00000001) == 0) { n += 1; }
+ return n;
+}
+
+static int next_signal(sigpending* pending, const sigset_t* mask)
+{
+ /* Standard signals: check bitmap & ~blocked */
+ for (int i = 0; i < _NSIG_WORDS; i++) {
+ uint64 bits = pending->signal.sig[i] & ~mask->sig[i];
+ if (bits != 0) {
+ int sig = i * 64 + sig_ctz64(bits) + 1;
+ return sig;
+ }
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* dequeue_signal — remove a signal from the pending set */
+/* ================================================================== */
+
+/*
+ * Dequeue the next deliverable signal. MUST be called with
+ * task->sighand->siglock held. Mirrors Linux's __dequeue_signal()
+ * (kernel/signal.c) which requires the same lock.
+ */
+static int dequeue_signal_locked(TaskControlBlock* task, kernel_siginfo* info)
+{
+ int sig = next_signal(&task->pending, &task->blocked);
+ if (sig == 0) return 0;
+
+ /* Clear the bitmap bit */
+ task->pending.signal.del(sig);
+
+ /* Search the queue for a matching sigqueue entry */
+ list_node* node = task->pending.list.next;
+ while (node != &task->pending.list) {
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ if (q->info.si_signo == sig) {
+ /* Remove from queue */
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ if (info) *info = q->info;
+ delete q;
+ return sig;
+ }
+ node = node->next;
+ }
+
+ /* No queued entry — fill in minimal info */
+ if (info) {
+ info->si_signo = sig;
+ info->si_errno = 0;
+ info->si_code = 0; /* SI_USER */
+ }
+ return sig;
+}
+
+/* ================================================================== */
+/* send_signal — enqueue a signal to a task */
+/* ================================================================== */
+
+/*
+ * Drop a queued sigqueue entry with the given signo from `task`.
+ *
+ * MUST be called with task->sighand->siglock held — Linux's
+ * flush_sigqueue_mask() has the same precondition (kernel/signal.c).
+ */
+static bool discard_pending_locked(TaskControlBlock* task, int signo)
+{
+ bool any = false;
+ list_node* node = task->pending.list.next;
+ while (node != &task->pending.list) {
+ list_node* nxt = node->next;
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ if (q->info.si_signo == signo) {
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ delete q;
+ any = true;
+ }
+ node = nxt;
+ }
+ task->pending.signal.del(signo);
+ return any;
+}
+
+static inline bool is_stop_sig(int sig)
+{
+ return sig == SIGSTOP || sig == SIGTSTP ||
+ sig == SIGTTIN || sig == SIGTTOU;
+}
+
+/*
+ * send_signal — queue a signal to a task.
+ *
+ * Acquires task->sighand->siglock for the full update (bitmap, queue,
+ * stop/cont side-effects, scheduler wake). Mirrors Linux's
+ * __send_signal_locked() which also requires this lock (see
+ * kernel/signal.c:1048 lockdep_assert_held).
+ *
+ * Returns 0 on success or a negative errno.
+ */
+int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task)
+{
+ if (sig < 1 || sig > _NSIG) return -EINVAL;
+ if (!task) return -ESRCH;
+ if (!task->sighand) return -ESRCH; /* eager-alloc invariant; tasks
+ * without sighand can't receive
+ * signals (idle / early kthreads) */
+
+ LockGuard guard(task->sighand->siglock);
+
+ /*
+ * Linux's prepare_signal(): a stop signal cancels any pending
+ * SIGCONT and vice versa, so the most recent decision wins.
+ */
+ if (is_stop_sig(sig)) {
+ discard_pending_locked(task, SIGCONT);
+ } else if (sig == SIGCONT) {
+ discard_pending_locked(task, SIGSTOP);
+ discard_pending_locked(task, SIGTSTP);
+ discard_pending_locked(task, SIGTTIN);
+ discard_pending_locked(task, SIGTTOU);
+
+ /* SIGCONT's resume effect happens atomically with delivery. */
+ if (task->state == TaskState::Stopped) {
+ task->state = TaskState::Runnable;
+ task->notify_continued = true;
+ task->stopped_code = 0;
+ sched_enqueue(task);
+ if (task->parent) task->parent->wait_chld.wake_all();
+ }
+ }
+
+ /* Already pending? For standard signals, just mark the bitmap */
+ if (sig < SIGRTMIN && task->pending.signal.has(sig))
+ return 0;
+
+ /* Enqueue a sigqueue entry */
+ auto* q = new sigqueue;
+ if (!q) {
+ /* Can't allocate — just set the bitmap (signal still delivered) */
+ task->pending.signal.add(sig);
+ return 0;
+ }
+
+ q->list.init();
+ if (info) {
+ q->info = *info;
+ } else {
+ q->info.si_signo = sig;
+ q->info.si_errno = 0;
+ q->info.si_code = 0; /* SI_USER */
+ }
+ q->info.si_signo = sig; /* ensure consistent */
+
+ /* Add to tail of queue */
+ list_node* head = &task->pending.list;
+ q->list.prev = head->prev;
+ q->list.next = head;
+ head->prev->next = &q->list;
+ head->prev = &q->list;
+
+ /* Set bitmap */
+ task->pending.signal.add(sig);
+
+ /* Wake the task if it's sleeping in an interruptible wait. */
+ if (task->state == TaskState::Sleeping) {
+ task->state = TaskState::Runnable;
+ sched_enqueue(task);
+ }
+
+ /* SIGKILL must run even if the target is Stopped. */
+ if (sig == SIGKILL && task->state == TaskState::Stopped) {
+ task->state = TaskState::Runnable;
+ task->stopped_code = 0;
+ sched_enqueue(task);
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* force_sig_fault — trap handler API */
+/* ================================================================== */
+
+void force_sig_fault(int signo, int code, uint64 addr)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return;
+
+ /* Kernel threads cannot handle signals — kill immediately */
+ if (curr->flags & TF_KTHREAD) {
+ pr_err("signal: kernel thread %s received signal %d\n",
+ curr->name ? curr->name : "(noname)", signo);
+ task_kill_current(signo);
+ return;
+ }
+
+ /* Sighand must already exist for user-mode tasks (allocated at
+ * fork / kernel_execve time). If it's somehow missing, the only
+ * safe action is to terminate. */
+ if (!curr->sighand) {
+ pr_err("signal: pid %d has no sighand, killing\n", curr->pid);
+ task_kill_current(signo);
+ return;
+ }
+
+ kernel_siginfo info{};
+ info.si_signo = signo;
+ info.si_code = code;
+ info._sifields._sigfault._addr = addr;
+
+ /* Force: bypass blocked mask for fatal signals (lock to keep
+ * blocked-mask updates consistent with sigprocmask). */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ curr->blocked.del(signo);
+ }
+
+ send_signal(signo, &info, curr);
+}
+
+/* ================================================================== */
+/* signal_pending — check if unblocked signals are pending */
+/* ================================================================== */
+
+bool signal_pending(TaskControlBlock* task)
+{
+ /* Advisory check (matches Linux's recalc_sigpending() usage):
+ * callers re-validate under siglock before acting. The bitmap is
+ * a uint64 word; a torn read can only produce false-positives or
+ * false-negatives transiently, never a corrupt pointer. */
+ if (!task) return false;
+ sigset_t unblocked = task->pending.signal & ~task->blocked;
+ return !unblocked.is_empty();
+}
+
+/* ================================================================== */
+/* do_signal — called before returning to user mode */
+/* ================================================================== */
+
+void do_signal(arch::TrapFrame* tf)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return;
+
+ /* Kernel threads never deliver signals via frames */
+ if (curr->flags & TF_KTHREAD) return;
+
+ /* No sighand means no signal infrastructure for this task */
+ if (!curr->sighand) return;
+
+ while (true) {
+ kernel_siginfo info{};
+ kernel_sigaction ka_copy;
+ int sig;
+
+ /*
+ * Take siglock for dequeue + action snapshot. Action processing
+ * (default actions like SIG_ACT_TERM, or constructing a signal
+ * frame for a user handler) is done OUTSIDE the lock — it can
+ * touch user memory and even call schedule(). This matches
+ * Linux's get_signal() loop in kernel/signal.c: it takes
+ * siglock around dequeue_signal+ka_copy, drops it before
+ * dispatching, and re-acquires next iteration.
+ */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ sig = dequeue_signal_locked(curr, &info);
+ if (sig == 0) break;
+ ka_copy = curr->sighand->action[sig - 1];
+ }
+ kernel_sigaction* ka = &ka_copy;
+
+ /* SIG_IGN: discard */
+ if (ka->sa_handler == SIG_IGN && !sig_kernel_only(sig))
+ continue;
+
+ /* SIG_DFL: apply default action */
+ if (ka->sa_handler == SIG_DFL) {
+ SigDefaultAction act = sig_default_action(sig);
+ switch (act) {
+ case SIG_ACT_IGN:
+ continue;
+ case SIG_ACT_TERM:
+ case SIG_ACT_CORE:
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d killed by signal %d\n",
+ curr->pid, sig);
+ task_kill_current(sig);
+ return; /* never reached */
+ case SIG_ACT_STOP: {
+ /*
+ * Enter Stopped state. Record the encoded wstatus so
+ * the parent's wait4(WUNTRACED) can observe and report.
+ * The schedule() below does NOT re-enqueue Stopped
+ * tasks — when SIGCONT eventually arrives, send_signal
+ * flips us back to Runnable and re-enqueues.
+ */
+ curr->stopped_code = W_STOPCODE(sig);
+ curr->state = TaskState::Stopped;
+ if (curr->parent)
+ curr->parent->wait_chld.wake_all();
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d stopped by sig %d\n",
+ curr->pid, sig);
+ schedule();
+ /*
+ * Resumed (either by SIGCONT or SIGKILL). Loop back to
+ * deliver any remaining pending signals before returning
+ * to user mode.
+ */
+ continue;
+ }
+ case SIG_ACT_CONT:
+ /* SIGCONT's resume side-effect already happened in
+ * send_signal(). The only thing left at handler time
+ * is to consume the signal (no-op for SIG_DFL). */
+ continue;
+ }
+ }
+
+ /* User handler: construct signal frame */
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d delivering sig %d handler=%llx\n",
+ curr->pid, sig, reinterpret_cast<uint64>(ka->sa_handler));
+ int ret = setup_signal_frame(tf, sig, ka, &info);
+ if (ret < 0) {
+ pr_err("signal: failed to set up frame for sig %d\n", sig);
+ task_kill_current(sig);
+ return;
+ }
+
+ /* Post-delivery bookkeeping under siglock — both the per-task
+ * blocked mask and the shared action table (SA_RESETHAND) are
+ * touched, so we must hold the same lock the senders use. */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ if (!(ka->sa_flags & SA_NODEFER)) curr->blocked.add(sig);
+ curr->blocked = curr->blocked | ka->sa_mask;
+ if (ka->sa_flags & SA_RESETHAND) {
+ curr->sighand->action[sig - 1].sa_handler = SIG_DFL;
+ }
+ }
+
+ /* Only deliver one signal per return-to-user */
+ break;
+ }
+}