On this page
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_pidis the sender's process ID at the time of the send.si_uidis 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:
- Read
si_pidfrom the siginfo. - Open a pidfd against the sender:
int pidfd = pidfd_open(si_pid, 0);. This is race-free against PID reuse. - Open the sender's primary token:
int token_fd = kacs_open_process_token(pidfd, TOKEN_QUERY);. Subject to the standardPROCESS_QUERY_INFORMATIONplus PIP dominance check on the sender's process SD. - 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_addris the faulting memory address (for SIGSEGV, SIGBUS) or the instruction address (for SIGILL, SIGTRAP, SIGFPE).si_addr_lsbis 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_timerididentifies which timer fired. Multiple timers can deliver to the same signal, distinguished by this field.si_overruncounts 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_addris the address of the offending instruction.si_syscallis the syscall number that was denied.si_archis the architecture identifier (AUDIT_ARCH_X86_64etc.) — 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_fdis the file descriptor that became ready.si_bandis 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
- Signals on Peios — the security model and access checks.
- Real-time signals —
sigqueue, payloads, POSIX timers. - Handlers and masks — how to install an
SA_SIGINFOhandler. - Process security descriptors — the SD model that gates
kacs_open_process_tokenfor strong-identity lookups. - Credential projection — how SIDs project to UIDs and the truth-projection rules.