10.5 The Notification Socket

peinit binds one Unix datagram socket for service notifications, by default at /run/services/peinit/notify.sock. Its path is an implementation detail: services receive it through NOTIFY_SOCKET and nothing hardcodes it. The kernel command line can override it with peios.notifysocket=.

There is one socket, not one per service, and the bind unlinks any stale path first. It carries the same inherited descriptor as the control socket (§10.1).

The protocol itself is PSPU §4. What follows is how peinit decides whether to believe a datagram.

10.5.1 Authenticating a sender #

SO_PASSCRED is enabled on the socket, so every datagram arrives with a kernel-attested SCM_CREDENTIALS control message. A datagram without one is rejected outright. Descriptors for the fd store arrive alongside, as SCM_RIGHTS.

Authentication then runs five steps, and each closes a hole the previous one leaves:

  1. Find the sender. Scan for the current service-main job whose PID equals the sender's. Only a main job is ever a candidate, which is what makes NotifyAccess=Main the only mode there is — a hook or a health check cannot notify on a service's behalf.
  2. The job is Running. A job still in pending setup has not exec'd.
  3. The job has a pidfd.
  4. The pidfd still refers to that PID. This is the step that matters: PID matching alone is racy, because a PID can be recycled between the sender writing and peinit reading. The pidfd was obtained atomically at fork, so verifying the PID against it is what makes the match sound rather than probable.
  5. The generation matches. A job whose activation generation is not the service's current one is a previous incarnation, and its notifications are rejected. This is invariant 5 of §6.1 in force: a READY=1 from the process that just crashed cannot mark its replacement ready.

Anything that fails is dropped and recorded as a notify.rejected event carrying the sender's PID and the reason.

The UID and GID in the credentials are parsed and never used. They are not policy inputs, and peinit does not consult them for anything — identity on Peios is a token, and the token here is established by which job the sender is, not by what UID it claims.

10.5.2 Applying a datagram #

A datagram may carry several newline-separated lines, and peinit applies every one. Parsing happens before application and is all-or-nothing: if any line is malformed, nothing from that datagram is applied, and any descriptors it carried are dropped and closed. Partial application of an ambiguous service-control message is structurally impossible rather than merely avoided.

A rejection is recorded after authentication, so the event can name the service where one could be established.

10.5.3 What the fields do #

Most are handled elsewhere: READY=1 and RELOADING=1 in §6.5, WATCHDOG=1 and WATCHDOG_USEC and EXTEND_TIMEOUT_USEC in §6.6, STOPPING=1 in §12.2, and the fd store fields in §10.6.

Three are event-emitting. STATUS=, ERRNO= and EXIT_STATUS= are authenticated and then emitted as KMES events — notify.status, notify.errno, notify.exit_status — whose payloads carry the service name, the job identifier, the operation identifier and the activation generation, alongside the value. They take the same path as job and operation events, not a forward to eventd.

STATUS= is additionally stored on the service's runtime state and exposed as status_text in a status query. It is cleared to null at the start of every activation generation, in the same step that increments the generation, so a status string cannot survive a restart and describe a process that no longer exists.

ERRNO= and EXIT_STATUS= are not stored. They are emitted and otherwise not retained.

10.5.4 Bounds #

A datagram is read into a fixed 64 KiB buffer, and the control message buffer is sized for 64 descriptors. Neither MSG_TRUNC nor MSG_CTRUNC is inspected, so a larger datagram is truncated silently and descriptors beyond the sixty-fourth are dropped by the kernel before peinit sees them.

Edit this page