These docs are under active development and cover the v0.20 Kobicha security model.
On this page
Concept 7 min read

Signal Information

When a signal handler is installed with SA_SIGINFO, it receives a siginfo_t struct alongside the signal number. The struct describes the signal — what caused it, who sent it, what data it carries — and is the receiver's primary tool for distinguishing one signal from another.

This page covers what siginfo contains, how the kernel populates it, and how to obtain stronger sender identity than the default fields can express.

The struct

typedef struct {
    int      si_signo;   // signal number
    int      si_errno;   // associated errno, or 0
    int      si_code;    // cause code (see below)
    pid_t    si_pid;     // sender PID (when applicable)
    uid_t    si_uid;     // sender effective UID (when applicable)
    int      si_status;  // exit status / signal (SIGCHLD)
    clock_t  si_utime;   // user CPU time consumed (SIGCHLD)
    clock_t  si_stime;   // system CPU time consumed (SIGCHLD)
    sigval_t si_value;   // payload (sigqueue / timer)
    int      si_int;     // si_value as int
    void    *si_ptr;     // si_value as pointer
    int      si_overrun; // timer overrun count
    int      si_timerid; // POSIX timer ID
    void    *si_addr;    // faulting address (SIGSEGV/SIGBUS/SIGILL/SIGFPE)
    long     si_band;    // band event (SIGPOLL)
    int      si_fd;      // file descriptor (SIGPOLL)
    short    si_addr_lsb;// LSB of fault address (SIGBUS hardware errors)
    void    *si_call_addr; // (SIGSYS) address of the offending instruction
    int      si_syscall;   // (SIGSYS) syscall number
    unsigned int si_arch;  // (SIGSYS) architecture
} siginfo_t;

Not every field is meaningful for every signal — siginfo is a tagged union with si_code as the tag, and which fields are populated depends on the signal and the cause. The struct is fixed-size (128 bytes total on every Linux architecture) and Peios honours the same layout for ABI compatibility.

si_code: where the signal came from

Every siginfo carries a si_code indicating the cause of the signal. The receiver uses this to know which other fields are meaningful and how to interpret them.

si_code Meaning Other fields valid
SI_USER Sent by a userspace process via kill() or raise(). si_pid, si_uid
SI_KERNEL Generated by the kernel (not in response to a userspace send). none — sender is the kernel
SI_QUEUE Sent via sigqueue(). si_pid, si_uid, si_value
SI_TIMER POSIX timer expiry. si_timerid, si_overrun, si_value
SI_MESGQ POSIX message queue state change. si_value
SI_ASYNCIO Asynchronous I/O completion. si_value
SI_TKILL Sent via tkill() or tgkill(). si_pid, si_uid
SI_SIGIO SIGIO/SIGPOLL queued via F_SETSIG. si_band, si_fd

For SIGCHLD, si_code is one of CLD_EXITED, CLD_KILLED, CLD_DUMPED, CLD_TRAPPED, CLD_STOPPED, CLD_CONTINUED — describing how the child changed state.

For SIGSEGV: SEGV_MAPERR (no mapping at the address), SEGV_ACCERR (mapping exists but the access is denied), SEGV_PKUERR (memory protection key denied access).

For SIGBUS: BUS_ADRALN (alignment), BUS_ADRERR (no such address — typically beyond end of mmaped file), BUS_OBJERR (hardware error), BUS_MCEERR_AR (machine-check action-required), BUS_MCEERR_AO (machine-check action-optional).

A handler should always check si_code before reading any of the cause-specific fields. Reading si_addr after a SIGSEGV is fine; reading it after SIGCHLD is undefined.

si_pid and si_uid: the sender

For signals with si_code of SI_USER, SI_QUEUE, or SI_TKILL, two fields identify the sender:

  • si_pid is the sender's process ID at the time of the send.
  • si_uid is the sender's effective UID at the time of the send.

These fields are populated by the kernel when the send happens; they are not under the sender's direct control and cannot be forged by passing crafted data into kill() or sigqueue().

Truth-projected si_uid on Peios

On Linux, si_uid is the sender's real UID. On Peios, where the real-UID concept is largely cosmetic (KACS uses tokens, not UIDs, for actual security decisions), Linux's si_uid semantics would yield a value disconnected from the sender's true identity.

Peios closes this gap. si_uid is truth-projected from the sender's effective token at send time — the impersonation token if the sender has one active, otherwise the primary token. This matches the same fsuid-style truth projection used on SO_PEERCRED for Unix sockets, and means receivers using the standard Linux API to authenticate signal senders get a UID derived from real KACS state, not a self-asserted value.

si_pid is the sender's PID, unmodified.

The siginfo struct itself is unchanged from the Linux ABI — there is no Peios-specific si_sid field. Embedding additional identity in siginfo would risk forward-compatibility collisions with future Linux extensions. Receivers that need stronger identity than a UID use a separate path, described below.

Strong sender identity

For receivers that need actual SID-level identity (for example, an authenticated message bus authorising a command based on which principal sent the wake-up signal), the path is:

  1. Read si_pid from the siginfo.
  2. Open a pidfd against the sender: int pidfd = pidfd_open(si_pid, 0);. This is race-free against PID reuse.
  3. Open the sender's primary token: int token_fd = kacs_open_process_token(pidfd, TOKEN_QUERY);. Subject to the standard PROCESS_QUERY_INFORMATION plus PIP dominance check on the sender's process SD.
  4. Inspect the token's user SID via kacs_get_token_information.

This is somewhat heavier than reading si_uid directly, but it goes through KACS-strength access control: the receiver gets sender identity only if it has authority to inspect the sender's token. Receivers performing this kind of identity check typically queue the work to a non-handler thread (handler context is restricted to async-signal-safe functions, so doing pidfd/token operations there is not safe) — running the lookup in a worker thread is both safe and natural.

When the sender is the kernel

For signals where si_code is SI_KERNEL (or any of the kernel-generated codes — SEGV_*, BUS_*, CLD_*, SI_TIMER, SI_ASYNCIO, etc.), there is no userspace sender. si_pid is 0 and si_uid is 0. These zeros are not "the kernel sent it as root" — they are sentinel values indicating "no userspace agent was responsible for this send." The receiver should always check si_code before treating si_pid/si_uid as a meaningful sender identity.

This distinction matters: a SIGTERM with si_code == SI_USER and si_uid == 0 means SYSTEM (or another root-equivalent principal) sent the signal. A SIGSEGV with si_code == SEGV_MAPERR and si_uid == 0 means the kernel detected a fault — nobody sent anything.

si_value: the payload

For signals sent via sigqueue() or generated by POSIX timers, si_value carries a sender-supplied data value. The union has two members:

  • si_value.sival_int — a 32-bit integer.
  • si_value.sival_ptr — a pointer-sized value.

The sender chooses which to send; the receiver reads whichever it expects. The full discussion of payload-bearing signals is on Real-Time Signals.

Fault-specific fields

For hardware-fault signals (SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP), additional fields describe the fault location:

  • si_addr is the faulting memory address (for SIGSEGV, SIGBUS) or the instruction address (for SIGILL, SIGTRAP, SIGFPE).
  • si_addr_lsb is the least-significant bit of the address corruption for SIGBUS hardware-error variants — useful for memory-error reporting.

A signal handler that catches SIGSEGV can inspect si_addr to determine which mapping faulted, potentially perform recovery (a JIT compiler installing a guard-page handler that maps the missing page on demand), and either return (resume execution) or terminate.

Timer-specific fields

For SIGEV_SIGNAL-delivered POSIX timer expiries (si_code == SI_TIMER):

  • si_timerid identifies which timer fired. Multiple timers can deliver to the same signal, distinguished by this field.
  • si_overrun counts how many additional expiries happened between the previous delivery and this one — relevant when a fast-firing timer outruns a slow handler.

Seccomp-specific fields

For SIGSYS raised by seccomp filters (si_code == SYS_SECCOMP):

  • si_call_addr is the address of the offending instruction.
  • si_syscall is the syscall number that was denied.
  • si_arch is the architecture identifier (AUDIT_ARCH_X86_64 etc.) — relevant for code that runs under multiple architectures (seccomp filters can match per-architecture).

These let a SIGSYS handler diagnose which syscall the program tried to make and respond accordingly — even synthesise a fake return value and continue, in carefully-designed sandboxes that use SIGSYS as a recoverable trap rather than a kill signal.

SIGIO/SIGPOLL fields

For signals delivered via the legacy F_SETSIG/F_SETOWN async-I/O mechanism:

  • si_fd is the file descriptor that became ready.
  • si_band is the event mask (POLLIN/POLLOUT/etc.).

This API is largely obsolete — epoll and io_uring replaced it. New code should not generate signals for I/O readiness.

See also