← board

Libc-free POSIX signal handler infrastructure (rt_sigaction)

Goal

PXX is libc-free, so installing a signal handler means raw rt_sigaction + the kernel signal-return contract, per target:

Why (consumers — this is GENERAL signal infrastructure, not a math-error

helper; math traps are just one client. Scope per user 2026-07-02.)

  1. Graceful termination: SIGINT (Ctrl-C) and SIGTERM (system shutdown, kill, service managers) → run cleanup hooks (flush files, release locks, restore terminal state — ansiterm/lineedit/TUI programs currently leave the terminal raw on Ctrl-C) before exiting, and a user-facing API to register handlers (SetSignalHandler(SIGINT, @MyProc) / FPC-compatible surface). Long-running demos/servers (http, dns, scheduler) are the immediate users.
  2. SIGPIPE: networking code (net/sockets/tls/http) dies silently on a closed peer today unless every write is guarded; standard practice = ignore SIGPIPE process-wide and surface EPIPE as an error return.
  3. Float exception traps ([[feature-float-exception-mask-control]]): FPC unmasks FPU exceptions by default; emulating that (opt-in for us — user prefers quiet IEEE inf/NaN propagation as the default) requires catching SIGFPE and converting it to a runtime error / raised exception.
  4. Math traps not worth pre-checking: the still-unguarded Low(Int64) div -1 overflow trap; an alternative div-zero path on x86 (pre-divide check already landed, v135).
  5. Diagnostics: SIGSEGV/SIGBUS → "segmentation fault at $ADDR in proc X" instead of a bare core dump (huge for self-host debugging); SIGCHLD if process spawning ever lands.

Scope notes

Acceptance

Install a handler for a chosen signal libc-free on x86-64; handler runs and process resumes correctly (restorer works under strace scrutiny); works with --threadsafe; smoke test in make test.

Constraints (user, 2026-07-02)

Progress — 2026-07-02, first slice LANDED (x86-64, v136)

Core infrastructure + user API live on the primary target:

Remaining (this ticket stays open): i386/arm32/aarch64 (per-target sigaction layouts + restorer conventions), SIGPIPE policy for the net stack (decided NOT default-ignored for now — a write-loop program must die on closed stdout; revisit with the net library), sigaltstack (hook on a guard- page fault reuses the faulting stack today), thread interaction beyond "handler table is process-wide", FPC-compat Signal()/sigaction surface, and the float-mask consumer ([[feature-float-exception-mask-control]]).

2026-07-14 — the x86-64 handler slice is DONE and now PINNED (b336)

Auditing this ticket found the "remaining" work largely shipped already and DEFAULT-ON for x86-64 Linux (ir_codegen.inc's signal stubs: SA_RESTORER trampoline -> rt_sigreturn, a dispatch stub with a 64-slot BSS hook table, SetSignalHandler as a compiler intrinsic; --no-signals opts out). It had NO test. Verified and pinned tonight:

PalGetpid surfaced in platform.pas (the backend always had it).

What actually remains

2026-07-14 — AARCH64 slice DONE (b370)

The per-arch port the ticket flagged as "the delicate part". aarch64 is NOT a copy of the x86-64 stubs — two contract differences, both load-bearing:

Syscalls: rt_sigaction=134, getpid=172, kill=129. Every instruction's hex was verified against aarch64-linux-gnu-as (including the branch offsets — the first cut had three wrong, computed against a layout that omitted the 3-word glob-load, and the symptom was the no-hook path silently resuming instead of dying).

Both existing tests now run on aarch64 under qemu and are wired into make test-aarch64: the callback path (hook fires ×2, program RESUMES at the interruption point) and the default-revert path (no hook -> SIG_DFL + re-raise -> exit 143).

What remains

2026-07-14 — ALL FIVE HOSTED TARGETS DONE (b371). Per-arch ports complete.

i386, arm32 and riscv32 join x86-64 (b336) and aarch64 (b370). The "delicate per-arch part" this ticket warned about was real, and it was NOT the same delicacy on each arch — three genuinely different kernel contracts:

1. Does the arch have sa_restorer at all?

2. sigreturn vs rt_sigreturn — the trap that cost the most. arm32 and i386 have TWO signal-frame shapes and the kernel picks by SA_SIGINFO, not by which sigaction syscall installed the handler. With no SA_SIGINFO the frame is a plain sigframe, so the restorer must call sigreturn (arm 119 / i386 119), NOT rt_sigreturn (173). Getting this wrong makes the kernel restore a garbage context — observed on arm32 as pc=sp=lr=0 and an instant SIGSEGV, found under gdb after the emitted code disassembled perfectly. x86-64 gets away with rt_sigreturn only because it has no legacy frame; aarch64/riscv32 likewise. (If SA_SIGINFO is ever set — the ucontext slice — these MUST flip to 173.)

3. i386's restorer needs a leading pop eax. setup_frame() wedges the signal number between the return address and the context, so after the handler's ret esp = frame+4, while sys_sigreturn recovers the frame as (sp - 8) and needs frame+8. glibc's i386 trampoline opens with the same pop.

Other per-arch notes: arm32's r7 is callee-saved AND the syscall register, so install/sethook frame it; i386 takes the signal number ON THE STACK (cdecl void h(int)), every other target in a register; aarch64/riscv32/arm32 must frame the link register across the hook call (x86-64/i386 get it free from the stack).

Every instruction was verified against the real assembler for its arch (aarch64-linux-gnu-as, arm-linux-gnueabi-as, GNU as --32) — that is also what caught three bad aarch64 branch offsets in b370.

All five run both tests under qemu and are wired into their cross suites: hook fires ×2 + program RESUMES, and no-hook -> SIG_DFL + re-raise -> exit 143. ESP bare-metal is unaffected (signals gated off there).

What remains (ticket stays open)

Log