From f84cfe236d8f5a676c873d57f480c31dc7afcf82 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Wed, 12 Aug 2026 14:57:29 -0700 Subject: [PATCH 1/2] feat(signals): added signal frame delivery mechanism Running a user handler needs the interrupted register and FP state saved onto the user stack and restored on rt_sigreturn. The per-arch frame builders, sanitizing restore path, and pending-handler selection provide that for the return-to-user hook. Co-authored-by: Cursor --- kernel/arch/aarch64/signal/delivery.cpp | 139 +++++++++++++++ kernel/arch/aarch64/signal/delivery.h | 52 ++++++ kernel/arch/x86_64/signal/delivery.cpp | 175 ++++++++++++++++++ kernel/arch/x86_64/signal/delivery.h | 53 ++++++ kernel/signals/signal.cpp | 31 ++++ kernel/signals/signal.h | 9 + kernel/tests/signals/delivery.test.cpp | 227 ++++++++++++++++++++++++ 7 files changed, 686 insertions(+) create mode 100644 kernel/arch/aarch64/signal/delivery.cpp create mode 100644 kernel/arch/aarch64/signal/delivery.h create mode 100644 kernel/arch/x86_64/signal/delivery.cpp create mode 100644 kernel/arch/x86_64/signal/delivery.h create mode 100644 kernel/tests/signals/delivery.test.cpp diff --git a/kernel/arch/aarch64/signal/delivery.cpp b/kernel/arch/aarch64/signal/delivery.cpp new file mode 100644 index 00000000..b1959a06 --- /dev/null +++ b/kernel/arch/aarch64/signal/delivery.cpp @@ -0,0 +1,139 @@ +#include "signal/delivery.h" +#include "sched/fpu.h" +#include "sched/sched.h" +#include "sched/task.h" +#include "signals/signal.h" +#include "mm/uaccess.h" +#include "mm/heap.h" + +namespace aarch64 { + +// PSTATE condition flags (NZCV). Everything else is forced so a restored +// context returns to EL0t (mode bits 0) with interrupts unmasked (DAIF 0). +constexpr uint64_t SPSR_NZCV_MASK = 0xF0000000ULL; + +static inline uint64_t align_down(uint64_t v, uint64_t a) { + return v & ~(a - 1); +} + +static inline void copy_vregs(uint8_t dst[32][16], const uint8_t src[32][16]) { + for (uint32_t i = 0; i < 32; i++) { + for (uint32_t j = 0; j < 16; j++) { + dst[i][j] = src[i][j]; + } + } +} + +__PRIVILEGED_CODE void pack_sigframe(rt_sigframe* frame, const trap_frame* tf, + int64_t saved_result, uint32_t sig, + signals::sig_set_t old_blocked, + const sched::fpu_state* fp) { + sigcontext& sc = frame->uc.uc_mcontext; + for (uint32_t i = 0; i < 31; i++) { + sc.regs[i] = tf->x[i]; + } + + sc.regs[0] = static_cast(saved_result); + sc.sp = tf->sp; + sc.pc = tf->elr; + sc.pstate = tf->spsr; + sc.fault_address = tf->far; + + fpsimd_context* fc = reinterpret_cast(sc.__reserved); + fc->magic = FPSIMD_MAGIC; + fc->size = sizeof(fpsimd_context); + fc->fpsr = fp->fpsr; + fc->fpcr = fp->fpcr; + copy_vregs(fc->vregs, fp->vregs); + + frame->uc.uc_sigmask = old_blocked; + frame->info.si_signo = static_cast(sig); + frame->info.si_code = SI_USER; +} + +__PRIVILEGED_CODE bool unpack_sigframe(const rt_sigframe* frame, trap_frame* tf, + sched::fpu_state* fp, + signals::sig_set_t* mask) { + const sigcontext& sc = frame->uc.uc_mcontext; + const fpsimd_context* fc = reinterpret_cast(sc.__reserved); + if (fc->magic != FPSIMD_MAGIC) { + return false; + } + + for (uint32_t i = 0; i < 31; i++) { + tf->x[i] = sc.regs[i]; + } + + tf->sp = sc.sp; + tf->elr = sc.pc; + tf->spsr = sc.pstate & SPSR_NZCV_MASK; + + fp->fpsr = fc->fpsr; + fp->fpcr = fc->fpcr; + copy_vregs(fp->vregs, fc->vregs); + + *mask = frame->uc.uc_sigmask; + return true; +} + +__PRIVILEGED_CODE int32_t build_signal_frame(trap_frame* tf, uint32_t sig, + const signals::k_sigaction* act, + signals::sig_set_t old_blocked, + int64_t saved_result) { + uint64_t frame_addr = align_down(tf->sp - sizeof(rt_sigframe), 16); + + rt_sigframe* frame = heap::kalloc_new(); + if (!frame) { + return -1; + } + sched::fpu_state fp; + fpu::save(&fp); + pack_sigframe(frame, tf, saved_result, sig, old_blocked, &fp); + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(frame_addr), frame, sizeof(*frame)); + heap::kfree_delete(frame); + + if (rc != mm::uaccess::OK) { + return rc; + } + + tf->sp = frame_addr; + tf->elr = act->handler; + tf->x[0] = sig; + tf->x[1] = frame_addr + __builtin_offsetof(rt_sigframe, info); + tf->x[2] = frame_addr + __builtin_offsetof(rt_sigframe, uc); + tf->x[30] = act->restorer; // LR, the handler returns into the restorer + return 0; +} + +__PRIVILEGED_CODE int64_t restore_signal_frame(trap_frame* tf) { + uint64_t frame_addr = tf->sp; + + rt_sigframe* frame = heap::kalloc_new(); + if (!frame) { + signals::die_from_signal(signals::SIGSEGV); + } + if (mm::uaccess::copy_from_user( + frame, reinterpret_cast(frame_addr), sizeof(*frame)) != mm::uaccess::OK) { + heap::kfree_delete(frame); + signals::die_from_signal(signals::SIGSEGV); + } + + sched::fpu_state fp; + signals::sig_set_t mask = 0; + if (!unpack_sigframe(frame, tf, &fp, &mask)) { + heap::kfree_delete(frame); + signals::die_from_signal(signals::SIGSEGV); + } + + signals::set_blocked(sched::current(), signals::SIG_SETMASK, &mask, nullptr); + fpu::restore(&fp); + + int64_t resume = static_cast(tf->x[0]); + heap::kfree_delete(frame); + + return resume; +} + +} // namespace aarch64 diff --git a/kernel/arch/aarch64/signal/delivery.h b/kernel/arch/aarch64/signal/delivery.h new file mode 100644 index 00000000..bca5e934 --- /dev/null +++ b/kernel/arch/aarch64/signal/delivery.h @@ -0,0 +1,52 @@ +#ifndef STELLUX_ARCH_AARCH64_SIGNAL_DELIVERY_H +#define STELLUX_ARCH_AARCH64_SIGNAL_DELIVERY_H + +#include "signal/sigframe.h" +#include "trap/trap_frame.h" +#include "sched/fpu_state.h" +#include "signals/signal_types.h" + +namespace aarch64 { + +/** + * @brief Fill a zeroed kernel-local signal frame from interrupted state. + * Pure marshaling with no user access, so it is unit-testable. The FP block + * is written in the kernel ABI field order, converting from fpu_state. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE void pack_sigframe(rt_sigframe* frame, const trap_frame* tf, + int64_t saved_result, uint32_t sig, + signals::sig_set_t old_blocked, + const sched::fpu_state* fp); + +/** + * @brief Apply a restored frame onto interrupted state (rt_sigreturn core). + * Forces PSTATE back to EL0 with unmasked interrupts so a forged frame can + * never return to EL1, and recovers the saved mask and FP. Returns false and + * leaves tf untouched on a corrupt FP record. Pure, unit-testable. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE bool unpack_sigframe(const rt_sigframe* frame, trap_frame* tf, + sched::fpu_state* fp, + signals::sig_set_t* mask); + +/** + * @brief Build a signal frame on the user stack and redirect tf to the + * handler. Returns 0 on success, negative when the user stack is unwritable. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t build_signal_frame(trap_frame* tf, uint32_t sig, + const signals::k_sigaction* act, + signals::sig_set_t old_blocked, + int64_t saved_result); + +/** + * @brief Restore interrupted state from the user signal frame and return + * the value to resume in x0. Kills the task with SIGSEGV on a bad frame. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int64_t restore_signal_frame(trap_frame* tf); + +} // namespace aarch64 + +#endif // STELLUX_ARCH_AARCH64_SIGNAL_DELIVERY_H diff --git a/kernel/arch/x86_64/signal/delivery.cpp b/kernel/arch/x86_64/signal/delivery.cpp new file mode 100644 index 00000000..0a6a04f0 --- /dev/null +++ b/kernel/arch/x86_64/signal/delivery.cpp @@ -0,0 +1,175 @@ +#include "signal/delivery.h" +#include "defs/segments.h" +#include "sched/fpu.h" +#include "sched/sched.h" +#include "sched/task.h" +#include "signals/signal.h" +#include "mm/uaccess.h" +#include "mm/heap.h" + +namespace x86 { + +// AMD64 leaf functions may use 128 bytes below RSP, preserve it. +constexpr uint64_t RED_ZONE = 128; + +// User half of the canonical address space, RIP must stay below it or +// SYSRET faults with kernel privilege on a non-canonical value. +constexpr uint64_t USER_ADDR_LIMIT = 0x0000800000000000ULL; + +// RFLAGS bits a restored context may carry (arithmetic, direction, trap, +// AC, ID). IF is forced on and bit 1 is reserved-must-be-one, everything +// else (IOPL, NT, VM) is dropped so a forged frame cannot gain privilege. +constexpr uint64_t RFLAGS_USER_MASK = + (1ULL << 0) | (1ULL << 2) | (1ULL << 4) | (1ULL << 6) | (1ULL << 7) | + (1ULL << 8) | (1ULL << 10) | (1ULL << 11) | (1ULL << 18) | (1ULL << 21); +constexpr uint64_t RFLAGS_IF = 1ULL << 9; +constexpr uint64_t RFLAGS_MB1 = 1ULL << 1; + +static inline uint64_t align_down(uint64_t v, uint64_t a) { + return v & ~(a - 1); +} + +__PRIVILEGED_CODE void pack_sigframe(rt_sigframe* frame, + const syscall_frame* ctx, + int64_t saved_result, uint32_t sig, + signals::sig_set_t old_blocked, + uint64_t user_fpstate) { + sigcontext& sc = frame->uc.uc_mcontext; + sc.r8 = ctx->r8; + sc.r9 = ctx->r9; + sc.r10 = ctx->r10; + sc.r12 = ctx->r12; + sc.r13 = ctx->r13; + sc.r14 = ctx->r14; + sc.r15 = ctx->r15; + sc.rdi = ctx->rdi; + sc.rsi = ctx->rsi; + sc.rbp = ctx->rbp; + sc.rbx = ctx->rbx; + sc.rdx = ctx->rdx; + sc.rax = static_cast(saved_result); + sc.rsp = ctx->rsp; + sc.rip = ctx->rip; + sc.eflags = ctx->rflags; + sc.cs = USER_CS; + sc.ss = USER_DS; + sc.fpstate = user_fpstate; + + frame->uc.uc_sigmask = old_blocked; + frame->info.si_signo = static_cast(sig); + frame->info.si_code = SI_USER; +} + +__PRIVILEGED_CODE bool unpack_sigframe(const rt_sigframe* frame, + syscall_frame* ctx, + signals::sig_set_t* mask) { + const sigcontext& sc = frame->uc.uc_mcontext; + if (sc.rip >= USER_ADDR_LIMIT) { + return false; + } + + ctx->r8 = sc.r8; + ctx->r9 = sc.r9; + ctx->r10 = sc.r10; + ctx->r12 = sc.r12; + ctx->r13 = sc.r13; + ctx->r14 = sc.r14; + ctx->r15 = sc.r15; + ctx->rdi = sc.rdi; + ctx->rsi = sc.rsi; + ctx->rbp = sc.rbp; + ctx->rbx = sc.rbx; + ctx->rdx = sc.rdx; + ctx->rsp = sc.rsp; + ctx->rip = sc.rip; + ctx->rflags = (sc.eflags & RFLAGS_USER_MASK) | RFLAGS_IF | RFLAGS_MB1; + + *mask = frame->uc.uc_sigmask; + return true; +} + +__PRIVILEGED_CODE int32_t build_signal_frame(syscall_frame* ctx, uint32_t sig, + const signals::k_sigaction* act, + signals::sig_set_t old_blocked, + int64_t saved_result) { + // FXSAVE image above the frame, frame base at RSP % 16 == 8 so the + // handler entry sees the ABI-required alignment after its return slot. + uint64_t sp = ctx->rsp - RED_ZONE; + uint64_t fpstate = align_down(sp - sizeof(sched::fpu_state), 16); + uint64_t frame_addr = align_down(fpstate - sizeof(rt_sigframe), 16) - 8; + + rt_sigframe* frame = heap::kalloc_new(); + if (!frame) { + return -1; + } + + pack_sigframe(frame, ctx, saved_result, sig, old_blocked, fpstate); + frame->pretcode = act->restorer; + + sched::fpu_state fp; + fpu::save(&fp); + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(frame_addr), frame, sizeof(*frame)); + + if (rc == mm::uaccess::OK) { + rc = mm::uaccess::copy_to_user( + reinterpret_cast(fpstate), &fp, sizeof(fp)); + } + + heap::kfree_delete(frame); + + if (rc != mm::uaccess::OK) { + return rc; + } + + ctx->rip = act->handler; + ctx->rsp = frame_addr; + ctx->rdi = sig; + ctx->rsi = frame_addr + __builtin_offsetof(rt_sigframe, info); + ctx->rdx = frame_addr + __builtin_offsetof(rt_sigframe, uc); + return 0; +} + +__PRIVILEGED_CODE int64_t restore_signal_frame(syscall_frame* ctx) { + // The handler's RET popped pretcode, so the frame sits one slot below. + uint64_t frame_addr = ctx->rsp - 8; + + rt_sigframe* frame = heap::kalloc_new(); + if (!frame) { + signals::die_from_signal(signals::SIGSEGV); + } + + signals::sig_set_t mask = 0; + bool ok = mm::uaccess::copy_from_user( + frame, reinterpret_cast(frame_addr), sizeof(*frame)) == mm::uaccess::OK; + + if (ok) { + ok = unpack_sigframe(frame, ctx, &mask); + } + + if (!ok) { + heap::kfree_delete(frame); + signals::die_from_signal(signals::SIGSEGV); + } + + signals::set_blocked(sched::current(), signals::SIG_SETMASK, &mask, nullptr); + + uint64_t fpstate = frame->uc.uc_mcontext.fpstate; + int64_t resume = static_cast(frame->uc.uc_mcontext.rax); + if (fpstate) { + // A bad FXSAVE pointer is a corrupt frame, kill like the other paths + sched::fpu_state fp; + if (mm::uaccess::copy_from_user( + &fp, reinterpret_cast(fpstate), sizeof(fp)) != mm::uaccess::OK) { + heap::kfree_delete(frame); + signals::die_from_signal(signals::SIGSEGV); + } + fpu::restore(&fp); + } + + heap::kfree_delete(frame); + return resume; +} + +} // namespace x86 diff --git a/kernel/arch/x86_64/signal/delivery.h b/kernel/arch/x86_64/signal/delivery.h new file mode 100644 index 00000000..095ce3c6 --- /dev/null +++ b/kernel/arch/x86_64/signal/delivery.h @@ -0,0 +1,53 @@ +#ifndef STELLUX_ARCH_X86_64_SIGNAL_DELIVERY_H +#define STELLUX_ARCH_X86_64_SIGNAL_DELIVERY_H + +#include "signal/sigframe.h" +#include "syscall/syscall_frame.h" +#include "signals/signal_types.h" + +namespace x86 { + +/** + * @brief Fill a zeroed kernel-local signal frame from interrupted state. + * Pure register marshaling with no user access, so it is unit-testable. + * user_fpstate is the user address the FXSAVE image will occupy, and the + * caller must have zeroed the frame so no kernel stack data leaks out. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE void pack_sigframe(rt_sigframe* frame, + const syscall_frame* ctx, + int64_t saved_result, uint32_t sig, + signals::sig_set_t old_blocked, + uint64_t user_fpstate); + +/** + * @brief Apply a restored frame onto interrupted state (rt_sigreturn core). + * Sanitizes user-controlled RFLAGS and rejects a non-canonical return RIP, + * since SYSRET would otherwise fault in Ring 0. Returns false and leaves + * ctx untouched on a bad frame. Pure, unit-testable. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE bool unpack_sigframe(const rt_sigframe* frame, + syscall_frame* ctx, + signals::sig_set_t* mask); + +/** + * @brief Build a signal frame on the user stack and redirect ctx to the + * handler. Returns 0 on success, negative when the user stack is unwritable. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t build_signal_frame(syscall_frame* ctx, uint32_t sig, + const signals::k_sigaction* act, + signals::sig_set_t old_blocked, + int64_t saved_result); + +/** + * @brief Restore interrupted state from the user signal frame and return + * the value to resume in RAX. Kills the task with SIGSEGV on a bad frame. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int64_t restore_signal_frame(syscall_frame* ctx); + +} // namespace x86 + +#endif // STELLUX_ARCH_X86_64_SIGNAL_DELIVERY_H diff --git a/kernel/signals/signal.cpp b/kernel/signals/signal.cpp index 6061224b..91804412 100644 --- a/kernel/signals/signal.cpp +++ b/kernel/signals/signal.cpp @@ -294,6 +294,37 @@ __PRIVILEGED_CODE bool interrupt_pending(sched::task* t) { return t && fatal_pending(t) != 0; } +__PRIVILEGED_CODE uint32_t next_deliverable(sched::task* t) { + if (!t || !t->group) { + return 0; + } + + sig_set_t pending = __atomic_load_n(&t->sig.pending, __ATOMIC_ACQUIRE) + | __atomic_load_n(&t->group->sig.shared_pending, __ATOMIC_ACQUIRE); + sig_set_t deliverable = pending + & ~__atomic_load_n(&t->sig.blocked, __ATOMIC_ACQUIRE); + + if (!deliverable) { + return 0; + } + + sync::irq_state irq = sync::spin_lock_irqsave(t->group->sig.lock); + uint32_t result = 0; + while (deliverable) { + uint32_t sig = static_cast(__builtin_ctzll(deliverable)) + 1; + deliverable &= deliverable - 1; + + uintptr_t handler = t->group->sig.actions[sig - 1].handler; + if (handler != SIG_DFL && handler != SIG_IGN) { + result = sig; + break; + } + } + + sync::spin_unlock_irqrestore(t->group->sig.lock, irq); + return result; +} + __PRIVILEGED_CODE void die_from_signal(uint32_t sig) { sched::task* self = sched::current(); sched::thread_group* tg = self->group; diff --git a/kernel/signals/signal.h b/kernel/signals/signal.h index 19b6e8a1..2cfd6ebc 100644 --- a/kernel/signals/signal.h +++ b/kernel/signals/signal.h @@ -85,6 +85,15 @@ __PRIVILEGED_CODE uint32_t fatal_pending(sched::task* t); */ __PRIVILEGED_CODE bool interrupt_pending(sched::task* t); +/** + * @brief Lowest-numbered signal ready for handler delivery, or 0. + * A signal qualifies when pending (thread or shared set), unblocked, and + * carrying an installed user handler. Selects what to deliver at a return + * to user mode. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE uint32_t next_deliverable(sched::task* t); + /** * @brief Terminate the current task because of signal sig. * Fatal signals kill the whole process: a non-leader records sig as the diff --git a/kernel/tests/signals/delivery.test.cpp b/kernel/tests/signals/delivery.test.cpp new file mode 100644 index 00000000..fe089962 --- /dev/null +++ b/kernel/tests/signals/delivery.test.cpp @@ -0,0 +1,227 @@ +#define STLX_TEST_TIER TIER_MM_ALLOC + +#include "stlx_unit_test.h" +#include "signals/signal.h" +#include "signal/delivery.h" +#include "sched/fpu.h" +#include "sched/task.h" +#include "mm/heap.h" +#include "dynpriv/dynpriv.h" + +TEST_SUITE(signal_delivery); + +// Minimal process: a thread group with a leader, for signal selection. +static sched::thread_group* g_tg; +static sched::task* g_leader; + +static int32_t setup_group() { + RUN_ELEVATED({ + g_leader = heap::kalloc_new(); + g_tg = heap::kalloc_new(); + }); + if (!g_leader || !g_tg) { + return -1; + } + + g_tg->lock = sync::SPINLOCK_INIT; + g_tg->leader = g_leader; + g_tg->pid = 1; + g_tg->threads.init(); + g_leader->group = g_tg; + return 0; +} + +static int32_t teardown_group() { + RUN_ELEVATED({ + if (g_tg) heap::kfree_delete(g_tg); + if (g_leader) heap::kfree_delete(g_leader); + }); + g_tg = nullptr; + g_leader = nullptr; + return 0; +} + +BEFORE_EACH(signal_delivery, setup_group); +AFTER_EACH(signal_delivery, teardown_group); + +static void install_handler(uint32_t sig) { + g_tg->sig.actions[sig - 1].handler = 0x400000; +} + +TEST(signal_delivery, selects_pending_handled_signal) { + uint32_t sig = 0; + install_handler(signals::SIGUSR1); + g_leader->sig.pending |= signals::sig_bit(signals::SIGUSR1); + RUN_ELEVATED({ sig = signals::next_deliverable(g_leader); }); + EXPECT_EQ(sig, signals::SIGUSR1); +} + +TEST(signal_delivery, ignores_default_and_blocked_signals) { + uint32_t sig = 0; + + // A pending signal left at its default action is not for a handler + g_leader->sig.pending |= signals::sig_bit(signals::SIGTERM); + RUN_ELEVATED({ sig = signals::next_deliverable(g_leader); }); + EXPECT_EQ(sig, 0U); + + // A handled signal that is blocked is not deliverable + install_handler(signals::SIGUSR1); + g_leader->sig.pending |= signals::sig_bit(signals::SIGUSR1); + g_leader->sig.blocked |= signals::sig_bit(signals::SIGUSR1); + RUN_ELEVATED({ sig = signals::next_deliverable(g_leader); }); + EXPECT_EQ(sig, 0U); +} + +TEST(signal_delivery, selects_lowest_handled_signal) { + uint32_t sig = 0; + install_handler(signals::SIGUSR1); // 10 + install_handler(signals::SIGUSR2); // 12 + + // A default-action SIGTERM pending alongside must not win over a handler + g_leader->sig.pending |= signals::sig_bit(signals::SIGUSR2); + g_leader->sig.pending |= signals::sig_bit(signals::SIGUSR1); + g_leader->sig.pending |= signals::sig_bit(signals::SIGTERM); + RUN_ELEVATED({ sig = signals::next_deliverable(g_leader); }); + EXPECT_EQ(sig, signals::SIGUSR1); +} + +#ifdef __x86_64__ +TEST(signal_delivery, x86_frame_round_trip_preserves_registers) { + x86::syscall_frame ctx; + ctx.rdi = 0x1000; ctx.rsi = 0x1001; ctx.rdx = 0x1002; ctx.r10 = 0x1003; + ctx.r8 = 0x1004; ctx.r9 = 0x1005; ctx.rbx = 0x1006; ctx.rbp = 0x1007; + ctx.r12 = 0x1008; ctx.r13 = 0x1009; ctx.r14 = 0x100a; ctx.r15 = 0x100b; + ctx.rsp = 0x7fff0000; ctx.rip = 0x401000; ctx.rflags = 0xFFFFFFFF; + + x86::rt_sigframe* frame = nullptr; + RUN_ELEVATED({ frame = heap::kalloc_new(); }); + ASSERT_TRUE(frame != nullptr); + + signals::sig_set_t mask = 0; + bool ok = false; + x86::syscall_frame out; + RUN_ELEVATED({ + x86::pack_sigframe(frame, &ctx, 0x2222, signals::SIGINT, 0xABCD, 0x7ffe0000); + ok = x86::unpack_sigframe(frame, &out, &mask); + }); + + EXPECT_TRUE(ok); + EXPECT_EQ(out.rdi, ctx.rdi); + EXPECT_EQ(out.r15, ctx.r15); + EXPECT_EQ(out.rsp, ctx.rsp); + EXPECT_EQ(out.rip, ctx.rip); + EXPECT_EQ(mask, 0xABCDULL); + EXPECT_EQ(frame->uc.uc_mcontext.rax, 0x2222ULL); + + // RFLAGS is sanitized: IF forced on, IOPL/NT cleared, user bits kept + EXPECT_TRUE((out.rflags & (1ULL << 9)) != 0); + EXPECT_TRUE((out.rflags & (3ULL << 12)) == 0); + EXPECT_TRUE((out.rflags & (1ULL << 14)) == 0); + EXPECT_TRUE((out.rflags & (1ULL << 0)) != 0); + + RUN_ELEVATED({ heap::kfree_delete(frame); }); +} + +TEST(signal_delivery, x86_rejects_non_canonical_return_rip) { + x86::syscall_frame ctx; + ctx.rdi = 0; ctx.rsi = 0; ctx.rdx = 0; ctx.r10 = 0; ctx.r8 = 0; ctx.r9 = 0; + ctx.rbx = 0; ctx.rbp = 0; ctx.r12 = 0; ctx.r13 = 0; ctx.r14 = 0; ctx.r15 = 0; + ctx.rsp = 0x7fff0000; ctx.rip = 0x401000; ctx.rflags = 0x2; + + x86::rt_sigframe* frame = nullptr; + RUN_ELEVATED({ frame = heap::kalloc_new(); }); + ASSERT_TRUE(frame != nullptr); + + signals::sig_set_t mask = 0; + bool ok = true; + x86::syscall_frame out; + out.rip = 0xDEAD; + RUN_ELEVATED({ + x86::pack_sigframe(frame, &ctx, 0, signals::SIGINT, 0, 0x7ffe0000); + frame->uc.uc_mcontext.rip = 0x0000800000000000ULL; // first kernel-half address + ok = x86::unpack_sigframe(frame, &out, &mask); + }); + + EXPECT_TRUE(!ok); + EXPECT_EQ(out.rip, 0xDEADULL); // ctx untouched on rejection + + RUN_ELEVATED({ heap::kfree_delete(frame); }); +} +#endif + +#ifdef __aarch64__ +TEST(signal_delivery, aarch64_frame_round_trip_preserves_state) { + aarch64::trap_frame tf; + for (uint32_t i = 0; i < 31; i++) { + tf.x[i] = 0x2000 + i; + } + tf.sp = 0x7fff0000; tf.elr = 0x401000; tf.far = 0; + tf.spsr = 0xF0000005; // NZCV set, EL1h mode bits that must be dropped + + sched::fpu_state fp; + RUN_ELEVATED({ fpu::init_state(&fp); }); + fp.fpsr = 0x11; fp.fpcr = 0x22; + fp.vregs[3][7] = 0x5A; + + aarch64::rt_sigframe* frame = nullptr; + RUN_ELEVATED({ frame = heap::kalloc_new(); }); + ASSERT_TRUE(frame != nullptr); + + aarch64::trap_frame out; + sched::fpu_state out_fp; + signals::sig_set_t mask = 0; + bool ok = false; + RUN_ELEVATED({ + fpu::init_state(&out_fp); + aarch64::pack_sigframe(frame, &tf, 0x2222, signals::SIGINT, 0xABCD, &fp); + ok = aarch64::unpack_sigframe(frame, &out, &out_fp, &mask); + }); + + EXPECT_TRUE(ok); + EXPECT_EQ(out.x[0], 0x2222ULL); // x0 holds the resumed syscall result + EXPECT_EQ(out.x[30], tf.x[30]); + EXPECT_EQ(out.sp, tf.sp); + EXPECT_EQ(out.elr, tf.elr); + EXPECT_EQ(mask, 0xABCDULL); + EXPECT_EQ(out_fp.fpsr, 0x11U); + EXPECT_EQ(out_fp.fpcr, 0x22U); + EXPECT_EQ(out_fp.vregs[3][7], 0x5A); + + // PSTATE is forced back to EL0t with interrupts unmasked, NZCV kept + EXPECT_EQ(out.spsr, 0xF0000000ULL); + + RUN_ELEVATED({ heap::kfree_delete(frame); }); +} + +TEST(signal_delivery, aarch64_rejects_corrupt_fpsimd_record) { + aarch64::trap_frame tf; + for (uint32_t i = 0; i < 31; i++) { + tf.x[i] = 0x2000 + i; + } + tf.sp = 0x7fff0000; tf.elr = 0x401000; tf.far = 0; tf.spsr = 0xF0000000; + + sched::fpu_state fp; + RUN_ELEVATED({ fpu::init_state(&fp); }); + + aarch64::rt_sigframe* frame = nullptr; + RUN_ELEVATED({ frame = heap::kalloc_new(); }); + ASSERT_TRUE(frame != nullptr); + + aarch64::trap_frame out; + out.elr = 0xDEAD; + sched::fpu_state out_fp; + signals::sig_set_t mask = 0; + bool ok = true; + RUN_ELEVATED({ + fpu::init_state(&out_fp); + aarch64::pack_sigframe(frame, &tf, 0, signals::SIGINT, 0, &fp); + frame->uc.uc_mcontext.__reserved[0] ^= 0xFF; // corrupt the FPSIMD magic + ok = aarch64::unpack_sigframe(frame, &out, &out_fp, &mask); + }); + + EXPECT_TRUE(!ok); + EXPECT_EQ(out.elr, 0xDEADULL); // tf untouched on rejection + + RUN_ELEVATED({ heap::kfree_delete(frame); }); +} +#endif From bb21423cc24c71ef96ab95ed078ae54f064b60f0 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Wed, 12 Aug 2026 15:21:08 -0700 Subject: [PATCH 2/2] fix(signals): prevented forged signal state from faulting in Ring 0 FXRSTOR and SYSRET both fault with kernel privilege on user-controlled values, and the trap path panics for a supervisor fault instead of killing the task. Masking MXCSR and rejecting an out-of-range handler close both. Co-authored-by: Cursor --- kernel/arch/x86_64/sched/fpu.h | 34 +++++++++++++++++++++++--- kernel/arch/x86_64/signal/delivery.cpp | 7 ++++++ kernel/arch/x86_64/signal/delivery.h | 3 ++- kernel/tests/signals/delivery.test.cpp | 30 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/kernel/arch/x86_64/sched/fpu.h b/kernel/arch/x86_64/sched/fpu.h index 5832f5c5..35a8c89a 100644 --- a/kernel/arch/x86_64/sched/fpu.h +++ b/kernel/arch/x86_64/sched/fpu.h @@ -5,6 +5,13 @@ namespace fpu { +// FXSAVE area layout: MXCSR and the CPU-reported mask of its usable bits +constexpr size_t MXCSR_OFFSET = 24; +constexpr size_t MXCSR_MASK_OFFSET = 28; + +// Architectural fallback when the CPU reports no mask, DAZ excluded +constexpr uint32_t MXCSR_DEFAULT_MASK = 0xFFBF; + /** * @note Privilege: **required** */ @@ -27,12 +34,31 @@ __PRIVILEGED_CODE inline void init_state(sched::fpu_state* state) { for (size_t i = 0; i < 512; i++) { area[i] = 0; } - // FCW (offset 0): 0x037F — mask all x87 exceptions, 64-bit precision, round-to-nearest + // FCW (offset 0): 0x037F - mask all x87 exceptions, 64-bit precision, round-to-nearest area[0] = 0x7F; area[1] = 0x03; - // MXCSR (offset 24): 0x1F80 — mask all SSE exceptions, round-to-nearest - area[24] = 0x80; - area[25] = 0x1F; + // MXCSR: 0x1F80 - mask all SSE exceptions, round-to-nearest + area[MXCSR_OFFSET] = 0x80; + area[MXCSR_OFFSET + 1] = 0x1F; +} + +/** + * @brief Clear MXCSR bits the CPU rejects from an untrusted FP image. + * FXRSTOR raises #GP in Ring 0 on a reserved bit, so an image that crossed + * the user boundary must pass through here before restore. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE inline void sanitize_user_mxcsr(sched::fpu_state* state) { + sched::fpu_state probe; + save(&probe); + + uint32_t mask = + *reinterpret_cast(&probe.fxsave_area[MXCSR_MASK_OFFSET]); + if (!mask) { + mask = MXCSR_DEFAULT_MASK; + } + + *reinterpret_cast(&state->fxsave_area[MXCSR_OFFSET]) &= mask; } } // namespace fpu diff --git a/kernel/arch/x86_64/signal/delivery.cpp b/kernel/arch/x86_64/signal/delivery.cpp index 0a6a04f0..a7b77c2e 100644 --- a/kernel/arch/x86_64/signal/delivery.cpp +++ b/kernel/arch/x86_64/signal/delivery.cpp @@ -92,6 +92,12 @@ __PRIVILEGED_CODE int32_t build_signal_frame(syscall_frame* ctx, uint32_t sig, const signals::k_sigaction* act, signals::sig_set_t old_blocked, int64_t saved_result) { + // SYSRET faults in Ring 0 on a non-canonical RIP, so a handler outside + // the user half must never reach the return path + if (act->handler >= USER_ADDR_LIMIT) { + return -1; + } + // FXSAVE image above the frame, frame base at RSP % 16 == 8 so the // handler entry sees the ABI-required alignment after its return slot. uint64_t sp = ctx->rsp - RED_ZONE; @@ -165,6 +171,7 @@ __PRIVILEGED_CODE int64_t restore_signal_frame(syscall_frame* ctx) { heap::kfree_delete(frame); signals::die_from_signal(signals::SIGSEGV); } + fpu::sanitize_user_mxcsr(&fp); fpu::restore(&fp); } diff --git a/kernel/arch/x86_64/signal/delivery.h b/kernel/arch/x86_64/signal/delivery.h index 095ce3c6..c43861bc 100644 --- a/kernel/arch/x86_64/signal/delivery.h +++ b/kernel/arch/x86_64/signal/delivery.h @@ -33,7 +33,8 @@ __PRIVILEGED_CODE bool unpack_sigframe(const rt_sigframe* frame, /** * @brief Build a signal frame on the user stack and redirect ctx to the - * handler. Returns 0 on success, negative when the user stack is unwritable. + * handler. Returns 0 on success, negative when the handler sits outside the + * user address space or the user stack is unwritable. * @note Privilege: **required** */ __PRIVILEGED_CODE int32_t build_signal_frame(syscall_frame* ctx, uint32_t sig, diff --git a/kernel/tests/signals/delivery.test.cpp b/kernel/tests/signals/delivery.test.cpp index fe089962..9d6c0822 100644 --- a/kernel/tests/signals/delivery.test.cpp +++ b/kernel/tests/signals/delivery.test.cpp @@ -147,6 +147,36 @@ TEST(signal_delivery, x86_rejects_non_canonical_return_rip) { RUN_ELEVATED({ heap::kfree_delete(frame); }); } + +TEST(signal_delivery, x86_rejects_non_canonical_handler) { + x86::syscall_frame ctx; + ctx.rsp = 0x7fff0000; + ctx.rip = 0xDEAD; + + signals::k_sigaction act = {}; + act.handler = 0x0000800000000000ULL; // first kernel-half address + + int32_t rc = 0; + RUN_ELEVATED({ rc = x86::build_signal_frame(&ctx, signals::SIGINT, &act, 0, 0); }); + + EXPECT_TRUE(rc < 0); + EXPECT_EQ(ctx.rip, 0xDEADULL); // ctx untouched on rejection +} + +TEST(signal_delivery, x86_sanitizes_restored_mxcsr) { + sched::fpu_state fp; + uint32_t mxcsr = 0; + + RUN_ELEVATED({ + fpu::init_state(&fp); + *reinterpret_cast(&fp.fxsave_area[fpu::MXCSR_OFFSET]) = 0xFFFFFFFF; + fpu::sanitize_user_mxcsr(&fp); + mxcsr = *reinterpret_cast(&fp.fxsave_area[fpu::MXCSR_OFFSET]); + }); + + EXPECT_EQ(mxcsr >> 16, 0U); // reserved bits never reach FXRSTOR + EXPECT_EQ(mxcsr & 0x1F80U, 0x1F80U); // supported control bits survive +} #endif #ifdef __aarch64__