Services & jobs
Single-page view · as markdown
peinit
Peios / Using Peios / Services & jobs
peinit is the init system of Peios — the first userspace process, running at PID 1 for the lifetime of the machine. It is a single-threaded program with one job: manage services, from the first one started at boot to the last one stopped at shutdown. Every supervised process on a Peios system — a long-running daemon, a one-shot setup task, a health check, a scheduled job — is started, watched, and reaped by peinit.
What makes peinit its own thing is that identity is built in. peinit does not just launch processes; it launches them as someone. Each service runs under a KACS token that decides what it can reach, and each service is itself a securable object with a security descriptor that decides who may start, stop, or query it. Service management and access control are the same system, not two systems bolted together.
This page sketches what peinit is responsible for, names the handful of ideas that make it unlike other init systems, and points at the pages that cover each one.
peinit in one sentence #
peinit is the sole service manager and PID 1 — it reads service definitions from the registry, starts them in dependency order under per-service identities, supervises them through a defined state machine, and tears them down in reverse at shutdown.
Everything in this topic is an elaboration of that sentence: what a service definition is, what dependency order means, what the state machine looks like, how identity is materialised, and how you drive the whole thing from a shell.
Three kinds of object #
peinit is easiest to understand through the three first-class objects it works with. Keeping them straight is most of the battle when you read a status output or an event log.
| Object | What it is | Lifetime |
|---|---|---|
| Service | A definition — a named unit of execution with identity, policy, and configuration, stored in the registry. It has a runtime state (the state machine) and, when running, a process. | Long-lived. Persists across restarts and reboots. |
| Job | A single process execution. Every time peinit forks — a service's main process, a hook, a health check, an ad-hoc run — that is one job, with its own GUID and exit result. | Short-lived. A restart creates a new job. |
| Operation | A requested action on a service — start, stop, restart, reload, reset. Control commands create operations that are validated, queued, and executed. | Short-lived. Resolves to a terminal state, then is dropped. |
A service is the what; a job is what actually ran; an operation is what someone asked for. A single restart command, for example, is one operation, which stops the current job and starts a new one, all against one service. Jobs and operations both carry GUIDs and are emitted to eventd as they complete, which is how the history of a service is reconstructed after the fact. They get their own page: Jobs and operations.
Underneath all three sits the fourth thing peinit is always handling — the token, the identity a service runs under. Tokens are not peinit's invention; they are the system-wide identity object. peinit's role is to obtain the right token for each service and install it before the process runs. That is the subject of Service identity and privileges.
What peinit is not #
peinit borrows vocabulary from init systems you may already know, and the familiarity is a trap. Clearing three wrong mental models is most of understanding what peinit actually is.
It is not systemd. There are no unit files. peinit does not read, parse, or translate .service files, and there is no migration shim. A service definition is a key in the registry under Machine\System\Services\, made of typed registry values — not a text file under /etc. Some interfaces are deliberately compatible where that helps: peinit speaks the sd_notify readiness protocol, and timer schedules use systemd's OnCalendar calendar-expression format. The compatibility stops at those wire formats; the model underneath is different.
It is not Windows SCM. The influence is real — services as securable objects with per-service descriptors, token-based service identity, an access-controlled control interface, a structured state machine — but it is architectural, not interface-level. peinit does not implement the SCM RPC protocol, Windows service types, or Windows control codes.
It is not a logging system. peinit holds a service's stdout/stderr pipes at birth, so it controls where output goes — but storage, indexing, and queries are eventd's job. peinit forwards; eventd is the historian. The same split applies to job and operation history: peinit emits structured events and forgets them; eventd keeps them. See Service output and logging.
A fourth, smaller correction: peinit does not supervise forking daemons. A service that double-forks to "daemonise" is solving a problem that does not exist when the manager tracks the process it spawned. peinit tracks every process by pidfd; a legacy binary that insists on double-forking must be wrapped at the package layer.
The one operational invariant #
peinit is single-threaded PID 1, and that one fact explains a surprising amount of its design. In a single-threaded PID 1, a blocking system call blocks everything — child reaping, watchdog expiry, shutdown signals, every other event stops while the call is stuck. So peinit's hard rule is that its event loop never blocks on a userspace service.
Two consequences you will meet repeatedly:
- peinit operates on an in-memory snapshot of the registry, not a live view. It reads all service definitions synchronously once, during boot, and thereafter works from an in-memory model that it updates from change notifications. Supervision never waits on the registry being available. This is why a configuration change does not always take effect immediately — see Defining a service.
- Anything that could block is pushed off the main loop. Filesystem condition checks run in a short-lived forked helper rather than a
stat()on the event loop; token requests to authd are driven by a timed state machine, not a blocking call; log delivery is best-effort and never exerts back-pressure on peinit.
You do not need to think about the event loop to operate peinit, but it is the reason behind several behaviours that would otherwise look arbitrary.
Where peinit sits in the system #
peinit is part of the Trusted Computing Base: a compromise of peinit is a compromise of the whole machine. It runs as SYSTEM with all privileges, and it never drops that identity. It depends on, and is depended on by, the other TCB components:
| peinit relies on | For |
|---|---|
| KACS | Service identity (installing tokens on children), control-interface authentication (peer token + AccessCheck), and per-service access control. See Access decisions. |
| LCS / the registry | Service definitions, boot configuration, and its own parameters. See The registry. |
| registryd | The registry source daemon — the first service peinit starts. peinit treats it as an opaque dependency. (registryd is an interface; the default implementation is loregd — see Boot and boot modes.) |
| authd | Minting tokens for non-platform services. peinit requests; authd mints. |
| eventd | Service logs (forwarded over a socket) and job/operation/audit events (emitted through KMES). |
| JFS | Delivering ad-hoc job submissions — a service asking peinit to run a process on a client's behalf. |
The bootstrapping puzzle — authd needs the registry, the registry needs to be started by something, and that something is peinit running before any of them exist — is resolved by the boot model in Boot and boot modes.
Where to start #
If you are configuring a system, start with Defining a service — what a service definition is, where it lives, and the rule that explains why some changes take effect immediately and others wait for a restart.
If you want to operate a running system — start, stop, query, and troubleshoot services — go to Controlling services for the command set and The service lifecycle to read what a status actually tells you.
If you care about what runs as whom, read Service identity and privileges and Who can manage a service — the two independent halves of the security model.
If you are investigating boot behaviour, Boot and boot modes covers Full, Safe, and Recovery modes and the escalation between them.
And when something is wrong, Troubleshooting peinit works backward from the symptom.
Defining a service
Peios / Using Peios / Services & jobs
A service is defined by a single key in the registry, under Machine\System\Services\<name>. The key's name is the service name, and the typed values inside it are the definition — the binary, who it runs as, what it depends on, how it is supervised. There is no file under /etc; there is a registry key.
peinit reads these definitions in two situations: once at boot, to build the service graph, and on demand, when an administrator starts a service or runs reload-config. Between those reads it works from an in-memory copy. That last fact is the source of the most common "why didn't my change take effect?" question, and the second half of this page is devoted to it.
Service names #
A service name must be made of characters from [A-Za-z0-9._-] and be 1–128 bytes long. Anything else is a validation error. Two characters are pointedly excluded:
/— names map directly onto cgroup ids and registry key names, and a slash would be ambiguous in both.:— reserved for peinit-internal synthetic names (for example, the way a hook job is labelled).
The name is how you refer to the service everywhere: in peiosctl commands, in another service's dependency list, in a ServiceSecurity descriptor, and in the per-service SID derived from it.
The definition schema #
A definition is a set of typed registry values. Rather than list all of them in one wall of rows, the tables below group the fields by what they are for, with a pointer to the page that explains each group in depth. Every field is optional unless noted; the only hard requirement is ImagePath. The full type-and-default catalog lives in the Registry key reference.
What to run — the binary and its execution context.
| Field | Default | Purpose |
|---|---|---|
ImagePath (required) | — | Absolute path to the service binary. |
Arguments | — | Argument list passed to the binary. |
WorkingDirectory | / | Working directory for the process. |
Environment | — | KEY=VALUE pairs added to the environment. |
RuntimeDirectories | — | Private directories created under /run just before the process starts. See The execution environment. |
LimitNOFILE, LimitCORE | — | RLIMIT_NOFILE / RLIMIT_CORE. |
What kind of service — type and readiness. See Simple and Oneshot services.
| Field | Default | Purpose |
|---|---|---|
Type | Simple | Simple (long-running) or Oneshot (run-to-completion). |
Readiness | Notify | Notify (READY=1 via sd_notify) or Alive (ready when the process exists). Ignored for Oneshot. |
RemainAfterExit | 0 | Oneshot only — stay in Completed after a successful exit. |
SuccessExitCodes | — | Non-zero exit codes to treat as success. |
When to start — triggers and conditions. See Triggers and timers.
| Field | Default | Purpose |
|---|---|---|
Triggers | — | boot and/or timer:<schedule>. Absent = demand-only. |
Disabled | 0 | If 1, triggers must not activate the service (manual start still allowed). |
SafeMode | 0 | If 1, attempt to start in Safe mode. |
Conditions, Asserts | — | Start-time checks. A failed condition skips; a failed assert fails. |
Who it runs as — identity and privileges. See Service identity and privileges.
| Field | Default | Purpose |
|---|---|---|
Identity | LocalService | Principal name or SID for the service token. |
RequiredPrivileges | — | Privilege allow-list; everything else is stripped from the token. |
HookIdentity | service's Identity | Identity for ExecStartPre/ExecStartPost hooks. |
How it relates to other services — dependencies. See Dependencies and ordering.
| Field | Purpose |
|---|---|
Requires | Hard dependencies — must be satisfied first; their failure fails this service. |
Wants | Soft dependencies — started first if present, but optional. |
BindsTo | Runtime coupling — if the target stops, this stops too. |
Conflicts | Mutual exclusion — starting this stops the named services. |
OnFailure | Service to start when this one enters Failed. |
How it is kept alive — supervision and health. See Keeping services running.
| Field | Default | Purpose |
|---|---|---|
ErrorControl | Normal | Normal (stay Failed) or Critical (sync + reboot on irrecoverable failure). |
RestartPolicy | OnFailure | Never / OnFailure / Always. |
RestartMaxRetries, RestartWindow, RestartDelay | 5 / 120 / 1 | Restart budget, the window of health that resets it, and the backoff base. |
HealthCheck, HealthCheckInterval, HealthCheckTimeout, HealthCheckRetries | — / 30 / 5 / 3 | Active health-check command and its timing. |
WatchdogTimeout | 0 | Expected interval between WATCHDOG=1 pings; 0 disables. |
The transition phases — hooks and timeouts. See The execution environment and The service lifecycle.
| Field | Default | Purpose |
|---|---|---|
ExecStartPre, ExecStartPost | — | Commands run before the binary / after readiness. |
ExecReload | (SIGHUP) | Reload command or signal:<NAME>. |
StartTimeout, StopTimeout | 30 / 10 | Seconds for the whole start sequence / between SIGTERM and SIGKILL. |
The remaining knobs — scheduling, notify, fds, metadata.
| Field | Default | Purpose |
|---|---|---|
TimerPersistent, TimerJitter | 1 / 0 | Catch up missed timer runs after reboot / random delay per firing. |
NotifyAccess | Main | Who may send sd_notify messages (only Main is supported). |
FdStoreMax | 0 | Size of the per-service fd store; 0 disables it. |
ServiceSecurity | inherit | Security descriptor controlling who may manage the service. |
DisplayName, Description | — | Human-readable labels for status output. |
Forward compatibility #
The schema version lives at Machine\System\Services\SchemaVersion (currently 1). peinit is deliberately forward-compatible:
- Unknown values are ignored. A definition written for a newer peinit does not break an older one.
- A newer schema version does not block boot. peinit logs a warning and continues.
- The schema only grows. New capability arrives as new optional fields, never as a breaking change to an existing one.
One defensive rule cuts the other way: a known field must not appear more than once in a collected definition. A duplicated known field is a validation error, even though the registry would ordinarily give you at most one value per name.
peinit works from a snapshot, not the live registry #
Here is the idea that explains most surprises. peinit does not re-read the registry every time it touches a service. It reads definitions at well-defined moments and operates on an in-memory model in between.
Two layers of snapshotting stack on top of each other:
- Boot generation. At the start of boot, peinit reads all definitions, builds the dependency graph, and validates it. The whole boot runs against that one snapshot. Changes made during boot — by an install script, a post-hook — do not perturb the boot in progress.
- Activation generation. When peinit starts a specific service, it snapshots that service's definition for the entire start. Pre-exec hooks, the token request, the readiness timeout, the first health checks — all use the values captured at activation. Edit a field while the service is
Starting, and the edit waits for the next start.
peinit learns about registry edits through change notifications: it subscribes to Machine\System\Services\ and Machine\System\Init\ at boot, and processes events in its event loop at a time of its choosing. If the notification queue overflows — a bulk admin operation, a flurry of scripted writes — peinit detects the overflow marker and does a full reload-config to resynchronise. You never have to think about the queue; you do have to know that a change is picked up, not pushed, and that when it takes effect depends on the field.
Field mutability: when a change takes effect #
Every field falls into one of four mutability classes. This table is the one to keep handy.
| Class | When a change takes effect | Fields |
|---|---|---|
| Immutable at runtime | Next restart only. | ImagePath, Type, Identity, RequiredPrivileges, ErrorControl |
| Apply on next start | Next start or explicit graph reload — not while running. | Requires, Wants, BindsTo, Conflicts, OnFailure, Conditions, Asserts |
| Hot-reloaded | Next relevant event, no restart. | ServiceSecurity (next control request) |
| Reloadable at runtime | Next relevant operation (restart, health-check cycle, …), no restart. | Arguments, SuccessExitCodes, all timeout/retry values, the health-check fields, RestartPolicy, Environment, WorkingDirectory, the hooks, Readiness, NotifyAccess, LimitNOFILE/LimitCORE, FdStoreMax, SafeMode, DisplayName, Description |
The practical reading: changing what a process is or runs as (ImagePath, Identity, Type, privileges, ErrorControl) is fundamental enough that it only applies when a fresh process starts — you must restart. Changing policy that peinit consults each time it acts (timeouts, restart behaviour, health checks) is picked up the next time peinit acts. And ServiceSecurity is special: it is re-read on every control request, so an access-control change takes effect on the very next command without touching the running service.
For inactive services the rules are simpler, because there is no running process to protect: a new service entry becomes available once the change notification is processed; a changed timer arms on the next evaluation; a changed dependency takes effect on the next start.
Removing a service definition #
Deleting a definition from the registry does not kill a running instance. peinit learns of the removal through the same notification path and behaves according to whether the service is running:
- Not running (Inactive, Failed, Completed, Skipped, Abandoned): peinit discards the in-memory entry immediately. There is nothing to supervise.
- Running (Active, Starting, Reloading, Backoff, Stopping): the running process is a job, and a job outlives its definition. peinit marks the entry definition-removed, keeps the cached definition only to finish supervising the existing instance, and does not restart it when it exits. Once it exits, the entry — and any stored fds — are discarded.
While an entry is definition-removed it keeps satisfying its dependents (it is still running), stop still works so you can drain it cleanly, but start, restart, and reload are rejected with UNKNOWN_SERVICE — there is no definition to start from. A status query reports it with a definition_removed: true flag so the draining instance is never invisible.
Where to start #
To understand the Type and Readiness fields and the Simple/Oneshot split, read Simple and Oneshot services.
To understand how a definition becomes a running, supervised process — and what each state in a status output means — read The service lifecycle.
For the complete type-and-default catalog of every registry key peinit reads, see the Registry key reference.
Simple and Oneshot services
Peios / Using Peios / Services & jobs
A service's type answers one question: does the process keep running, or does it run once and exit? peinit supports exactly two answers — Simple and Oneshot — set by the Type field (default Simple). The type is independent of when the service starts; a Oneshot can be boot-triggered, timer-triggered, or demand-only, exactly like a Simple service.
Simple services #
A Simple service is a long-running daemon. peinit forks, installs the token, and execs the binary. The process is the service: while it runs, the service is running; when it exits, the service has stopped. This is the default and covers the large majority of services — registryd, authd, sshd, application daemons.
The interesting question for a Simple service is when does it count as started — the moment its dependents are allowed to begin. That is the readiness model, set by the Readiness field.
Readiness: Notify vs Alive #
| Readiness | Ready when… | Use for |
|---|---|---|
| Notify (default) | The service sends READY=1 via sd_notify. | Any service that has meaningful startup work — opening a socket, loading state, connecting to a backend. The dependent genuinely should wait. |
| Alive | The process exists — readiness is immediate. | Services with no startup handshake, where "the process is up" is as good a signal as you will get. |
The distinction matters because it is a promise to dependents. With Notify, a service that declares itself a Requires target is telling peinit "do not start anything that needs me until I say READY=1." With Alive, peinit can only promise that the process was forked — not that it is functional.
Once ready, a Simple service transitions to Active and stays there until its process exits or it is stopped. What happens on exit — clean exit, crash, restart — is the subject of The service lifecycle and Keeping services running.
Oneshot services #
A Oneshot service is a run-to-completion task. peinit forks, installs the token, execs the binary, and waits for it to exit. It is the right type for setup work: creating directories, initialising a database, running a schema migration, applying a one-time fixup at boot.
For a Oneshot, readiness is exit, not a signal. The Readiness field is ignored entirely — READY=1 is meaningless for a process whose whole job is to finish. Success and failure are decided by the exit code:
- Exit 0 (or any code listed in
SuccessExitCodes) → the service succeeded. - Any other exit, or death by signal → the service failed, and the failure policy applies.
SuccessExitCodes is for binaries that signal a meaningful outcome with a non-zero code — for example, a tool that exits 2 to mean "nothing to do." Each entry is a decimal 0–255; code 0 is always success and need not be listed.
Completed, and RemainAfterExit #
A successful Oneshot transitions to Completed, and what happens next depends on RemainAfterExit:
RemainAfterExit | After a successful exit |
|---|---|
| 0 (default) | The service passes through Completed — releasing its dependents — then transitions to Inactive. |
| 1 | The service stays in Completed. |
Both forms satisfy dependents; the only difference is what a later status query shows. Use RemainAfterExit=1 when "this task is done" is a state worth seeing — a boot-time migration you want to confirm ran, say — rather than a service that quietly returns to Inactive.
Oneshot timing and hooks #
Two details distinguish a Oneshot's start sequence from a Simple one:
StartTimeoutcovers the entire run. For a Simple service the timeout covers startup until readiness; for a Oneshot it covers everything from the first pre-hook to process exit. A long-running Oneshot must raiseStartTimeoutaccordingly (or sendEXTEND_TIMEOUT_USECas it works — see Keeping services running).ExecStartPostruns only on success. Post-hooks fire after the successful exit. If the Oneshot fails,ExecStartPostdoes not run.
Restarting a Oneshot #
A Oneshot is not restarted on success, regardless of RestartPolicy — even Always. A successful exit is the goal, not a failure to retry. RestartPolicy governs only the response to failure (a non-zero exit). To re-run a Oneshot on a schedule, give it a timer trigger; that is the intended mechanism.
Simple vs Oneshot at a glance #
| Simple | Oneshot | |
|---|---|---|
| Process lifetime | Long-running; is the service | Runs once, exits |
| Readiness | Notify (READY=1) or Alive | Successful exit — Readiness ignored |
| Satisfies dependents when | Active | Completed (with or without RemainAfterExit) |
| Successful end state | Active (until stopped) | Completed, then Inactive (unless RemainAfterExit=1) |
ExecStartPost fires | After readiness | After successful exit only |
StartTimeout covers | Startup until readiness | The whole execution |
| Watchdog & health checks | Supervised while Active | None — WatchdogTimeout/HealthCheck are inert |
| Restart on success | n/a | Never (use a timer to re-run) |
The sd_notify contract #
A Notify-readiness service signals readiness by sending READY=1 over the sd_notify protocol — a datagram to the socket named in the NOTIFY_SOCKET environment variable, which peinit sets for every service. READY=1 is only the most visible message; the same channel carries watchdog keepalives (WATCHDOG=1), graceful-stop notice (STOPPING=1), timeout extensions (EXTEND_TIMEOUT_USEC), status strings (STATUS=), and the fd store. These are covered where they belong — readiness here, the rest in Keeping services running and Controlling services.
Readiness is always per start generation. A READY=1 left over from a previous incarnation of the process is never valid for the current start — peinit ties every notify message to the specific process it is currently supervising, verified by pidfd.
Who is allowed to send these messages is governed by NotifyAccess, which currently has a single value: Main (0, the default). Under Main, only the tracked main PID — the process peinit forked and supervises — is authorised to send sd_notify readiness and status messages; a message arriving from any other PID (a child, a helper, an unrelated process) is ignored. So if a worker your service spawns sends READY=1, peinit will not act on it — the readiness signal must come from the main process itself.
No forking daemons #
peinit does not support services that double-fork to background themselves. The whole reason a traditional daemon double-forks — to detach from the launcher — is moot when the launcher tracks the process it spawned. peinit obtains a pidfd for every process at fork time, which is a kernel handle to that exact process, immune to PID-reuse races. There is no MAINPID= mechanism for a service to redirect supervision onto a different process; peinit supervises what it forked.
If a legacy binary insists on daemonising, the fix lives at the packaging layer — wrap it so it stays in the foreground (commonly a --no-daemon/--foreground flag) — not at the init layer.
Where to start #
To control when either type of service starts, read Triggers and timers.
To follow a service from definition to running process and read its states, read The service lifecycle.
To configure restart, health checks, and watchdogs for a Simple service, read Keeping services running.
Triggers and timers
Peios / Using Peios / Services & jobs
A trigger decides when a service starts on its own. A service's triggers are independent of its type: a boot-triggered Oneshot and a timer-triggered Simple service are both perfectly ordinary. Triggers live in the Triggers field as a list, each entry written type or type:argument.
| Trigger | Form | Meaning |
|---|---|---|
| Boot | boot | Start during the Phase 2 boot sequence, in dependency order. |
| Timer | timer:<schedule> | Start on a schedule. The schedule is a calendar expression. |
A service with no triggers is demand-only: it never starts itself, and is brought up only by an explicit control command or because another service depends on it. Many services are demand-only by design.
Waiting for the boot to settle #
boot:settled is boot with one difference: the service starts once the Phase 2 boot set has stopped moving — every service in it reached a state it will not leave on its own — rather than during it.
It exists for services that write to a terminal. peinit reports its progress to /dev/console, and a service holding that same terminal writes there too, so a prompt started mid-boot gets written over: login emits Username: with no newline and waits, peinit appends a service line, and the result is unreadable until you press Enter and login redraws.
The wait is capped (Machine\System\Boot\SettleTimeout, 5 seconds by default). A service stuck in Starting therefore costs a deferred service a short delay rather than preventing it — which matters most exactly when something is broken and a console prompt is what you need.
A boot:settled service is deliberately not part of the boot plan. It does not consume the parallel-start budget, is not counted towards boot success, and cannot block another service. Waiting for quiet is a preference; it must not change what the boot means.
You can list more than one trigger, including more than one of the same type. ["timer:*-*-* 02:00:00", "timer:*-*-* 14:00:00"] runs at 2 am and 2 pm. The model is built to grow — future trigger types (path, device, event) will slot into the same list without a schema change.
The Disabled flag #
Disabled=1 suppresses automatic activation. A disabled service will not be started by any trigger — boot, timer, or anything added later — but it can still be started by hand through the control interface. It is the switch for "configured, but not running on its own right now."
Two related controls are easy to confuse with it:
| To… | Use |
|---|---|
| Stop a service from auto-starting, but keep manual start | Disabled=1 |
| Stop a service from being started at all, even manually | Deny SERVICE_START in its ServiceSecurity descriptor |
| Stop a running service | The stop command |
Enabling and disabling are registry writes performed by admin tooling, not peinit commands. peinit picks up a Disabled change through a registry notification or on reload-config. A disabled service's definition is still loaded into peinit's model, so it is ready for an on-demand start the instant you ask.
Calendar expressions #
Timer schedules are written in systemd's OnCalendar format. peinit's grammar is normative and self-contained — what follows is the whole language. The general shape is:
DayOfWeek Year-Month-Day Hour:Minute:Second Timezone
Every field is optional and has a default, so most real schedules are short.
| Field | Form | If omitted |
|---|---|---|
| DayOfWeek | weekday name(s) | any day (*) |
| Date | Year-Month-Day | *-*-* (any date) |
| Time | Hour:Minute:Second | 00:00:00 |
| Second | the :Second of Time | :00 |
| Timezone | IANA zone name | system-local time |
So 02:00:00 is a complete expression — date defaults to every day, and it means "every day at 02:00." Hour:Minute with no seconds defaults the seconds to 00.
Component syntax #
Each numeric component — year, month, day, hour, minute, second — and the weekday accept the same operators:
| Operator | Example | Matches |
|---|---|---|
| Wildcard | * | any value |
| List | 1,15 | any listed value |
| Range | Mon..Fri, 8..17 | the inclusive range |
| Repetition | 0/15 | 0, then every multiple of the step — 0, 15, 30, 45 |
| Range + step | 8..17/2 | 8, 10, 12, … up to and including 17 |
Weekdays are English names, case-insensitive, abbreviated (Mon) or full (Monday), and take lists and ranges.
Last day of the month #
A ~ in place of the - between Month and Day counts the day from the end of the month: ~01 is the last day, ~02 the second-to-last, and so on. So *-*~01 is the last day of every month. Repetition combines with it — Mon *-05~07/1 is "the last Monday in May."
Named shortcuts #
These stand in for the expression shown:
| Shortcut | Equivalent |
|---|---|
minutely | *-*-* *:*:00 |
hourly | *-*-* *:00:00 |
daily | *-*-* 00:00:00 |
weekly | Mon *-*-* 00:00:00 |
monthly | *-*-01 00:00:00 |
quarterly | *-01,04,07,10-01 00:00:00 |
semiannually | *-01,07-01 00:00:00 |
yearly / annually | *-01-01 00:00:00 |
Timezones and precision #
A timezone specifier follows the IANA database — Europe/London, US/Eastern, UTC. With none, the expression is interpreted in system-local time.
Precision is second-level. Unlike systemd, peinit does not accept sub-second fractions; a fractional second is a parse error. Service scheduling has no use for finer granularity.
Examples #
| Expression | Meaning |
|---|---|
*-*-* 02:00:00 | Every day at 2 am (system-local) |
Mon *-*-* 00:00:00 | Every Monday at midnight |
*-*-1,15 12:00:00 | 1st and 15th of each month at noon |
*-*~01 00:00:00 | Last day of each month at midnight |
Mon..Fri *-*-* 09:00:00 | Weekdays at 9 am |
*-*-* *:00/15:00 | Every 15 minutes |
*-*-* 02:00:00 Europe/London | Every day at 2 am London time |
What happens when a timer fires #
A timer firing does not blindly start the service — peinit considers the service's current state first, so a firing never collides with a run already in progress.
| Type | Current state | Action |
|---|---|---|
| Oneshot | Inactive / Completed / Failed | Start it (operation source Timer). |
| Oneshot | Active / Starting | Set a single pending flag — one catch-up run when the current one finishes. |
| Simple | Inactive / Failed | Start it. |
| Simple | Active / Starting | No-op; the firing is logged. |
The Oneshot "pending" flag is deliberately not a queue: many firings that land while a Oneshot is busy collapse into one catch-up run, taken when the current run ends. A service cannot stampede itself.
That single flag is shared across all of a Oneshot's timers, not one per timer. If a Oneshot has several timer triggers and two of them fire while it is busy, at most one run is still pending when the current one finishes. The timers are otherwise fully independent — each keeps its own next-firing time and its own last-run timestamp — but they can only ever queue up a single shared catch-up between them.
Each timer firing creates a normal start operation, so a timer-started service is observable exactly like an admin-started one — same GUIDs, same events.
Persistent timers and catch-up #
TimerPersistent (default 1) controls whether runs missed while the machine was off are caught up after a reboot.
peinit records the last-run timestamp of each timer in the registry. On boot, for each persistent timer it reads that timestamp, computes the next firing after it, and — if that time is already in the past — fires once, immediately, then resumes the normal schedule.
The catch-up is always a single run, never one-per-missed-occurrence. A daily timer that was off for five days fires once on the next boot, not five times. A non-persistent timer (TimerPersistent=0) ignores history entirely and simply computes its next future firing from now.
One subtlety worth knowing: the last-run timestamp is keyed by the schedule string. If you change a timer's schedule, the old timestamp is orphaned and the new schedule has no history, which causes exactly one spurious catch-up run. Schedule changes are rare and the cost is one extra run, so peinit accepts it rather than tracking trigger identity.
Jitter #
TimerJitter (default 0) adds a random delay of 0 to TimerJitter seconds to each firing, recomputed every time. A daily timer with TimerJitter=900 fires at a slightly different moment between 00:00 and 00:15 each day. Jitter only ever delays — a timer never fires before its scheduled time. It is the standard remedy for a fleet of machines all hammering the same backend at exactly midnight.
When the clock jumps #
Calendar timers are wall-clock schedules: *-*-* 02:00:00 means "02:00 on the wall," not "every 86,400 seconds." peinit arms them as absolute real-time timers that are cancelled and recomputed whenever the system clock is stepped, so the schedule stays anchored to the wall clock across corrections.
| Event | Behaviour |
|---|---|
| NTP step or manual clock set | Armed calendar timers are cancelled and recomputed against the new wall clock. A backward step pushes the next firing later; a forward step that crosses a missed time fires it once. |
| Suspend / resume | A scheduled time that elapsed while suspended fires once on resume — once, not once per missed occurrence. |
| A missed occurrence within one uptime | Fire once, then resume the normal schedule. peinit never replays every occurrence in the gap. |
| Daylight-saving transition (timezone-aware timer only) | A scheduled time in the skipped hour never fires; a scheduled time in the repeated hour fires once. See below. |
Daylight-saving is the case most likely to surprise you, and it only affects timers that name an IANA zone that observes DST (a bare 02:30:00 in system-local time follows whatever the system does). Say you schedule *-*-* 02:30:00 Europe/London:
- Spring forward — the clock jumps from 01:59 straight to 03:00, so 02:30 does not exist on that day. The timer does not fire; it simply resumes at the next valid
02:30. - Fall back — the clock reaches 02:30, then rewinds and reaches 02:30 a second time. The timer fires exactly once, on the first occurrence, not twice.
This is distinct from peinit's interval timers — the watchdog, health-check intervals, restart backoff, and the start/stop/reload phase timeouts. Those are genuine durations measured on the monotonic clock, so "wait 30 seconds" means 30 elapsed seconds and never lurches when the wall clock is set.
There is one boot-time case worth calling out on its own. When peinit reads a timer's stored last-run timestamp at boot and finds it is in the future relative to the current wall clock — meaning the clock has gone backwards since that run — it can't trust the timestamp, so it treats the timer as having an unknown last run and fires the persistent catch-up immediately. This only applies to the boot-time check; the runtime handling above is unaffected.
Where to start #
To see how a timer-started run appears as a job and an operation, read Jobs and operations.
To understand the states a timer-started service moves through, read The service lifecycle.
For the exact registry keys that store last-run timestamps, see the Registry key reference.
The service lifecycle
Peios / Using Peios / Services & jobs
A service managed by peinit is, at every instant, in exactly one state. The state is what a status query reports, what gates dependents, and what decides which commands are valid. Alongside the state, peinit records the cause of the most recent transition — the why behind the where. Reading a status is reading these two things together: "Failed, because RestartBudgetExhausted" tells a very different story from "Failed, because ValidationError."
This page is the reference for both. It is worth internalising before Controlling services and Troubleshooting, because both lean on it.
The states #
| State | Process? | Satisfies dependents? | What it means when you see it |
|---|---|---|---|
| Inactive | No | No | Not started, or stopped cleanly and not set to restart. The neutral resting state. |
| Starting | Maybe | No | Activation in progress — conditions, hooks, fork, or readiness wait. Dependents are blocked. |
| Active | Yes | Yes | Running and ready. The normal state of a healthy Simple service. |
| Reloading | Yes | Yes | Re-reading configuration. Still counts as running. |
| Stopping | Yes (briefly) | No | SIGTERM sent; waiting for exit or SIGKILL escalation. |
| Completed | No | Yes | Oneshot finished successfully. Stays here only with RemainAfterExit=1. |
| Backoff | No | No | A restart is pending; the service is waiting out its backoff delay before the next attempt. |
| Failed | No | No | Exited abnormally and restart policy is exhausted or not configured. |
| Abandoned | Yes (unkillable) | No | SIGKILL was sent but the process survived in uninterruptible sleep (D-state). peinit has given up supervising it; its cgroup is leaked. |
| Skipped | No | Yes | A start-time condition was not met. The service does not apply here. |
Three of these — Active, Completed, Skipped — satisfy dependents. Everything else blocks them. That single column is the rule the whole dependency system turns on: a service waiting on a Requires target does not move until that target reaches one of those three.
The common path #
Most of a service's life is a small loop. The diagram below shows the states a healthy Simple service moves through, plus the two ways it leaves Active.
stateDiagram-v2
[*] --> Inactive
Inactive --> Starting: start / trigger / dependency
Starting --> Active: ready (READY=1 or alive)
Active --> Stopping: stop / shutdown / conflict
Stopping --> Inactive: exited (clean stop / shutdown)
Stopping --> Failed: exited (conflict / BindsTo eviction)
Active --> Backoff: crash + restart allowed
Backoff --> Starting: backoff delay elapsed
Active --> Failed: crash + no restart
Starting --> Failed: timeout / hook / setup failure
Failed --> Starting: explicit start
A few things this picture makes concrete:
- An automatic restart always routes through Backoff, never through Failed.
Backoff → Startingis the retry;Failedis reached only when restarts are exhausted or disabled. This is whyOnFailurefires once at the end, not on every retry. Startingcan fail before any process exists — a parent-side setup error, a failed pre-hook, a condition or assert. The cause records which.Failed → Startingis how a manualstart(or a recovery path) revives a dead service; automatic restarts never originate fromFailed.Stoppinghas two exits. A clean stop or a shutdown lands inInactive. But a service stopped because it lost aConflictsrace (ConflictEviction) or because itsBindsTotarget went away (BindsToPropagation) comes to rest inFailed, carrying that cause — so an evicted or bound-out service shows as Failed instatusand needs areset(or, for a bound service, its target returning) before it starts again. It was not shut down on purpose, so peinit does not treat it as cleanly Inactive.
Oneshot services follow a parallel path through Completed instead of Active: Starting → Completed, and then either staying there (RemainAfterExit=1) or passing through to Inactive. See Simple and Oneshot services.
Transition causes #
Every transition carries a cause. peinit keeps the cause of the most recent transition on the service, and emits all of them to eventd. The full taxonomy, grouped by what kind of thing happened:
Why a service started
| Cause | Meaning |
|---|---|
ExplicitStart | An administrator or a trigger started it. |
DependencyStart | Started to satisfy another service's dependency. |
RestartPolicy | An automatic restart, after a backoff delay. |
BindsToRecovery | A bound target returned to Active; the dependent is auto-restarted. Does not consume the restart budget. |
Why a service stopped
| Cause | Meaning |
|---|---|
ExplicitStop | An administrator requested stop. |
ConflictEviction | A conflicting service started and won. |
BindsToPropagation | A bound target stopped, so this one stops too. |
ShutdownWave | The system is shutting down. |
Why a service failed or restarted — the diagnostic causes
| Cause | Meaning |
|---|---|
ProcessCrash | The main process exited non-zero or died on a signal. |
CleanExit | A Simple process exited successfully, and policy is not Always — not a crash; goes straight to Inactive. |
CleanExitRestart | A Simple process exited successfully but RestartPolicy=Always, so it is restarted. Logged clearly as not a crash. |
ReadinessTimeout | StartTimeout expired before the service became ready. |
WatchdogTimeout | A WATCHDOG=1 keepalive did not arrive in time. |
HealthCheckFailure | HealthCheckRetries consecutive health checks failed. |
PreHookFailure | An ExecStartPre hook exited non-zero. |
ParentSetupFailure | peinit could not even fork — resource exhaustion, cgroup error. No child was created. |
PreExecFailure | Setup after fork but before exec failed (token install, rlimits, environment). |
RestartBudgetExhausted | RestartMaxRetries reached within the window; no more retries. |
Why a service was rejected — definition and graph problems
| Cause | Meaning |
|---|---|
ValidationError | The definition failed validation (bad field, unresolvable conflict, illegal health-check timing…). |
CycleDetected | The service is part of a dependency cycle. |
DependencyFailure | A Requires dependency entered Failed or does not exist. |
AssertionError | A start-time Assert failed. |
ConditionSkipped | A start-time Condition was not met → Skipped (this is not a failure). |
ProcessUnkillable | The process survived SIGKILL (D-state) → Abandoned. |
ExplicitReset | An administrator cleared Failed/Abandoned/Skipped without starting. |
The grouping matters for what peinit does next: the diagnostic causes are mostly restart-eligible (peinit consults restart policy), whereas the definition/graph causes are never restarted — retrying a broken definition cannot help. The distinction is spelled out in Keeping services running.
Readiness gating during boot #
The dependent-satisfaction rule is what makes boot orderly. A service's dependents do not start until it satisfies them:
- A Simple service satisfies dependents when it signals
READY=1(Notify) or when its process exists (Alive). - A Oneshot service satisfies dependents when it exits successfully and reaches Completed.
- A Skipped service satisfies dependents immediately — it succeeded by not needing to run.
If a service does not reach readiness within its StartTimeout, its dependents diverge by relationship: Requires dependents transition to Failed (DependencyFailure); Wants dependents start anyway. That difference is the whole point of the two relationship types — see Dependencies and ordering.
The Abandoned state #
Abandoned is the one state that reflects a kernel-level problem rather than a service-level one. peinit reaches it when it has sent SIGKILL to a service's process group but the processes are still there after a grace period — the post-kill timeout, 5 seconds by default. If the service's cgroup has not emptied within it, the processes are wedged in uninterruptible kernel sleep (D-state), typically behind a hung mount or a broken storage controller, and the service is marked Abandoned (its cgroup leaked).
peinit cannot kill a D-state process; nothing in userspace can. So it stops trying: it marks the service Abandoned, leaks the cgroup (it cannot be removed while populated), and moves on rather than hanging. The leak is never silent — it shows up in the service's warnings and, on a later start, peinit creates a fresh "generational" cgroup so the new instance is unaffected by the stuck old one.
An Abandoned service is cleared with reset, which re-checks the cgroup: if it finally emptied, peinit cleans up; if it is still populated, peinit leaves it leaked and warns you. An Abandoned service is a sign of an underlying I/O fault that needs investigating, not something to paper over with a restart.
Invariants #
A handful of rules hold without exception:
- A service is in exactly one state at any moment.
- Only the transitions peinit defines are valid — there are no others.
- Only peinit changes a service's runtime state. No external process can reach in and set it.
- A service's securable identity (its ServiceSecurity descriptor — who can manage it) is independent of its process identity (its token — what it can access).
- Readiness is per start generation — a
READY=1from a previous incarnation is never honoured for the current start.
Where to start #
To understand what happens after a service crashes — restart policy, backoff, health checks, watchdogs — read Keeping services running.
To understand how dependents block on and are released by these states, read Dependencies and ordering.
To see which commands are valid in which state, read the command × state matrix in Controlling services.
Keeping services running
Peios / Using Peios / Services & jobs
Once a service is Active, peinit's job becomes keeping it that way — or deciding, deliberately, when to stop trying. That decision is governed by a small set of policies: a restart policy with a throttling budget, optional active health checks, an optional watchdog, and an error-control level that decides how serious a final failure is. This page covers all of them.
Restart policy #
When a service fails in a way that might be transient, peinit consults RestartPolicy:
| Policy | Value | Behaviour |
|---|---|---|
| Never | 0 | Never restart. The service goes straight to Failed. |
| OnFailure (default) | 1 | Restart on a crash or runtime failure. An exit code in SuccessExitCodes is not a failure and is not restarted. |
| Always | 2 | Restart on any failure, and — for a Simple service — restart even a successful clean exit (see clean exits). |
Not every failure is restart-eligible. peinit splits causes into three groups:
- Restart-eligible — peinit consults the policy and budget:
ProcessCrash,WatchdogTimeout,HealthCheckFailure,ReadinessTimeout,PreHookFailure,PreExecFailure,ParentSetupFailure. (The startup failures are eligible on purpose — a pre-hook that failed on a momentarily-missing mount deserves another try.) - Never restarted — retrying cannot help: an explicit stop or reset, shutdown, conflict eviction, a broken definition (
ValidationError,CycleDetected,DependencyFailure,AssertionError), an already-exhausted budget, or an unkillable process. - Budget-exempt —
BindsToRecovery, where a bound service came back and its dependents are revived. The dependent did not fail on its own, so this restart does not count against its budget.
The budget and backoff #
Restarting an instantly-crashing service in a tight loop is worse than useless, so every restart is throttled by two mechanisms working together.
Exponential backoff. Before each restart the service sits in the Backoff state for a delay that doubles on each consecutive failure, starting from RestartDelay (default 1 s) and capped at 60 s. So a crash-looping service backs off 1 s, 2 s, 4 s, 8 s, … up to a minute between attempts.
The budget. A consecutive-failure counter tracks how many times the service has failed in a row. Once it reaches RestartMaxRetries (default 5), the next failure is not restarted: the service transitions to Failed with cause RestartBudgetExhausted.
The reset. The counter resets to zero once the service stays Active for RestartWindow seconds (default 120) without failing. This is the crucial detail: the budget is not "N restarts ever" — it is "N restarts faster than the service can sustain RestartWindow of health." A service that crashes once a day and recovers cleanly each time never exhausts its budget, because every crash starts from a counter of zero. Only failures recurring faster than the service can stay healthy accumulate.
flowchart LR
A["Active"] -->|crash| B{"budget left?"}
B -->|yes| C["Backoff<br/>delay doubles"]
C -->|delay elapsed| D["Starting"]
D -->|ready, stays Active<br/>RestartWindow| A
B -->|no| E["Failed<br/>RestartBudgetExhausted"]
Clean exits #
For a Simple service, exiting with code 0 (or a SuccessExitCodes match) is success, not a crash — a daemon chose to quit. What peinit does next depends on the policy:
- Under
NeverorOnFailure, the cause isCleanExitand the service goes straight to Inactive, with no restart-policy consultation at all. - Under
Always, the cause isCleanExitRestartand the service is restarted — but through the same backoff and budget as a failure, so a daemon that exits cleanly in a tight loop cannot bypass throttling. Logs and status make clear it exited successfully and was restarted only because the policy is Always.
(A Oneshot clean exit is never restart-eligible regardless of policy — see service types.)
Health checks #
A live process is not always a working one — it can lose its database connection, wedge in a bad state, or return errors while still "up." An active health check closes that gap by running a command periodically and treating its exit code as a verdict: 0 is healthy, non-zero is a failure.
Health checks apply to Simple services only. peinit starts a service's health-check timer once the service becomes Active; a Oneshot has no long-running process to probe, so it never has one. HealthCheck and its companion fields are inert on a Oneshot — no timer is started — so don't set them there expecting supervision.
| Field | Default | Controls |
|---|---|---|
HealthCheck | — | The command to run (runs under the service's own token). |
HealthCheckInterval | 30 | Seconds between runs. |
HealthCheckTimeout | 5 | Seconds before a check is killed and counted as failed. |
HealthCheckRetries | 3 | Consecutive failures before the service is declared unhealthy. |
HealthCheckRetries consecutive failures mark the service unhealthy and restart it through the normal restart policy — backoff and budget included. A single success resets the failure count. If a check is still running when the next interval fires, the new one is skipped rather than stacked.
The health field in a status response reflects this: healthy, unhealthy (failing but not yet failed out), unknown (configured, no result yet), or null (no health check).
The flap guardrail. Health-check throttling only works if a failure cycle is shorter than the restart window — otherwise the counter resets between failures and the service restarts forever. peinit enforces this at validation time as a hard rule:
HealthCheckRetries × HealthCheckInterval < RestartWindow
A definition that violates it is a validation error, not a warning — the service is marked Failed with ValidationError rather than allowed to restart indefinitely.
A health check stuck in D-state is handled like any other stuck helper: its sub-cgroup is leaked and surfaced as a warning, but — unlike a stuck main process — it does not push the service to Abandoned. A health check holds no service resources; it is only a probe.
The watchdog #
The watchdog inverts the health check: instead of peinit asking the service if it is alive, the service must periodically tell peinit. Set WatchdogTimeout (seconds; 0 disables) and the service must send WATCHDOG=1 via sd_notify at least that often. Miss the deadline and peinit treats it as a failure (cause WatchdogTimeout) and applies the restart policy.
Like health checks, the watchdog is a Simple-only supervision mechanism. peinit arms the watchdog timer only once a Simple service becomes Active; on a Oneshot WatchdogTimeout is inert — no timer is started, and setting it changes nothing.
A service can adjust its own watchdog at runtime by sending WATCHDOG_USEC=<microseconds>: a positive value re-arms the timer with the new interval immediately; 0 disables the watchdog. This is for services whose phases have different latencies — a database might run a tight 5 s watchdog normally but widen it to 60 s during a compaction pass, because it knows its own phases better than the schema author did. The runtime value does not persist: a restart reverts to the schema's WatchdogTimeout.
Timeout extension #
A service that does genuinely slow work during a transition can ask for more time by sending EXTEND_TIMEOUT_USEC=<microseconds> while it is Starting, Stopping, or Reloading. Each message replaces the current phase deadline (it is not additive); a service that keeps sending them keeps proving it is making progress, and if it stops, the last deadline fires and peinit escalates normally.
The extension is capped to prevent a buggy service from extending forever:
| Phase | Base timeout | Maximum deadline |
|---|---|---|
| Starting | StartTimeout | StartTimeout × 4 |
| Stopping | StopTimeout | StopTimeout × 4 |
| Reloading | StartTimeout | StartTimeout × 4 |
During shutdown a second, global cap also applies — the deadline can never exceed the remaining time in the global shutdown timeout, and the stricter of the two caps wins. A message received outside a transition (the service is Active, Failed, …) is ignored — there is no timeout to extend.
OnFailure #
When a service enters Failed, an OnFailure field names another service to start in response — a fallback for graceful degradation (the main web UI failed; bring up a minimal emergency endpoint). It is not a monitoring or alerting hook; that is eventd's job.
OnFailure fires for runtime failures — ProcessCrash, WatchdogTimeout, HealthCheckFailure, the startup-failure causes, and non-Critical budget exhaustion. It deliberately does not fire for:
ShutdownWave— nothing new starts during shutdown.ValidationError,CycleDetected,DependencyFailure,AssertionError— these are definition or graph breakage, not a running service degrading. The fallback would likely sit in the same broken graph anyway.
Because a fallback can itself fail and carry its own OnFailure, peinit bounds the chain: it tracks which services it has already started for one originating failure, refuses to start one twice, and never follows the chain beyond a depth of 16. When the guard trips, it logs the loop and stops.
ErrorControl: when failure is not an option #
ErrorControl decides how serious an irrecoverable failure is:
| Level | Value | On irrecoverable failure |
|---|---|---|
| Normal (default) | 0 | The service stays in Failed. The rest of the system carries on. |
| Critical | 1 | peinit syncs filesystems and reboots immediately. |
A Critical service is one the system genuinely cannot run without — the platform daemons registryd, authd, lpsd, eventd are all Critical. "Irrecoverable" means the restart budget was exhausted: peinit tried, backed off, retried up to RestartMaxRetries, and the service still would not stay healthy. At that point the fastest path to a known-good system is a reboot.
A few consequences worth holding onto:
- Critical failure outranks
OnFailure. When a Critical service exhausts its budget, peinit reboots; it does not start theOnFailurehandler. ErrorControl=CriticalimpliesSafeMode. A Critical service is always eligible to start in Safe mode.- Critical services are OOM-protected. peinit sets their
oom_score_adjto-1000, so the kernel will not pick them as out-of-memory victims. - A runtime Critical crash does not enter Safe mode — it follows the reboot path. Safe mode is only for boot-time configuration errors (a cycle or conflict involving a Critical service). The escalation between reboots, the boot-attempt counter, and Recovery mode are covered in Boot and boot modes.
Where to start #
To see exactly which states these policies move a service through, read The service lifecycle.
To understand how a failed service propagates to the ones that depend on it, read Dependencies and ordering.
When a service will not stay up, work backward from the symptom in Troubleshooting peinit.
Dependencies and ordering
Peios / Using Peios / Services & jobs
Services rarely stand alone. A web app needs its database; a network daemon needs the loopback interface up; two implementations of the same role must never run at once. peinit expresses all of this with four relationship fields — Requires, Wants, BindsTo, and Conflicts — that together decide the order services start in, what happens when one stops, and how a failure spreads to its neighbours.
The relationships are declared on the dependent (the service that needs something), naming the target (the service it needs) by name. Conflicts is the exception — it is symmetric, and one side declaring it is enough.
The four relationships #
Requires — a hard dependency #
If A Requires B:
- Start. B must reach a satisfying state before A starts. If B is not running, peinit starts it first (cause
DependencyStart). If B fails, A is marked Failed withDependencyFailureand never even attempts to start. - Stop. Stopping B does not stop A. Requires is a start-ordering constraint, not a runtime leash — A keeps running.
- Runtime failure. If B crashes while A is already Active, A is unaffected. B's own restart policy handles B's recovery.
Requires is the workhorse: "I need this to have started, and if it can't, neither can I."
Wants — a soft dependency #
If A Wants B:
- Start. peinit starts B before A if B exists and is not Disabled — but if B fails to start, or does not exist, A starts anyway.
- Stop / failure. No effect either way. A and B are runtime-independent.
Wants is best-effort ordering: "start this first if you can, but I work without it."
BindsTo — a runtime coupling #
If A BindsTo B:
- Start. Identical to Requires — B must satisfy before A starts.
- Stop. If B stops for any reason — explicit stop, conflict, crash, shutdown — A is stopped too, transitioning to Stopping with cause
BindsToPropagation. When A finishes stopping it comes to rest in Failed (carryingBindsToPropagation), not Inactive — so astatusquery shows the bound service as Failed, making it clear it was taken down by its target rather than shut down cleanly. - Recovery. When B returns to Active, peinit automatically restarts A from that Failed state (cause
BindsToRecovery). This is reactive — peinit watches B's transitions — and it is budget-exempt: A did not fail on its own, so the restart does not count against its budget. If B never comes back, A stays Failed until youreset(or start) it.
BindsTo is Requires plus a runtime leash: "I need this to start, and I should not outlive it." It is the right choice for a sidecar that is meaningless without its principal. BindsTo implies Requires; listing both for the same target is harmless, and BindsTo semantics win.
Conflicts — mutual exclusion #
If A Conflicts with B:
- Start. Starting A while B is Active creates a stop operation for B (source
ConflictResolution), evicting it (causeConflictEviction) before A starts — and vice versa. If the loser will not stop within itsStopTimeout, SIGKILL escalation applies. The evicted loser does not land in Inactive: it comes to rest in Failed (carryingConflictEviction), so it shows as Failed instatusand needs aresetbefore it will start again. That is deliberate — an evicted service is not a clean stop, and leaving it Failed stops it from quietly restarting straight back into the conflict. - Symmetry. Conflicts is two-way. If A declares
Conflicts=["B"], starting either stops the other; B need not declare it back.
Conflicts is for true mutual exclusion — two services binding the same port, or two implementations of one role where exactly one must run — not for ordinary resource contention.
At a glance #
| Start order | Target failure stops dependent? | Target stop stops dependent? | |
|---|---|---|---|
| Requires | target first; dependent fails if target fails | At start time only | No |
| Wants | target first if present; dependent starts regardless | No | No |
| BindsTo | target first; dependent fails if target fails | Yes (and auto-recovers) | Yes (and auto-recovers) |
| Conflicts | starting one evicts the other | n/a | n/a |
Graph validation #
Before peinit starts anything, it builds the dependency graph and validates it. Validation runs once per graph build — at boot for the whole boot graph, and per request for an on-demand start's transitive closure. It is not incremental.
Cycles. peinit topologically sorts the graph; if the sort fails, there is a cycle. Every service in the cycle is marked Failed with CycleDetected, and peinit logs the full path (dependency cycle: A → B → C → A) so you can find and break it. A service may not depend on itself — a self-reference is rejected as a cycle. If any service in the cycle is ErrorControl=Critical, peinit downgrades to Safe mode rather than rebooting, because a reboot would just hit the same cycle.
Missing targets. What a missing target means depends on the relationship:
| Relationship | Missing or disabled target |
|---|---|
Requires | Dependent marked Failed (DependencyFailure). |
BindsTo | Treated as a missing Requires — Failed (DependencyFailure). |
Wants | Silently ignored — the entry is dropped. |
Conflicts | Silently ignored — nothing to conflict with. |
Unresolvable conflicts. If two boot-triggered services conflict with each other, there is no way to honour both, so both are marked Failed with ValidationError. As with cycles, if either is Critical, Safe mode applies.
Warnings. Some conditions are logged but do not block boot — most notably a Readiness=Alive service that others Requires (see service types). Warnings appear in the logs and do not change state.
Validation errors. Some conditions do fail the service — for example a health-check configuration that violates the flap constraint. These mark the service Failed with ValidationError.
When more than one finding applies to the same service, peinit records a single primary cause by precedence — CycleDetected > ValidationError > DependencyFailure — but it logs all findings, so the primary cause never hides the others.
Parallel start #
After validation, peinit starts services whose dependencies are all satisfied, and it does so in parallel up to a configurable limit:
| Registry key | Default | Meaning |
|---|---|---|
Machine\System\Boot\MaxParallelStarts | 10 | Maximum services starting concurrently. |
The scheduler is simple: every service with no unsatisfied dependencies is eligible; peinit starts up to MaxParallelStarts of them; as each one reaches a satisfying state its dependents become eligible and join the queue. The result is that independent subtrees of the graph come up at the same time, while ordering constraints are still honoured exactly.
Failure propagation #
When a service enters Failed during graph execution, the failure spreads along Requires and BindsTo edges — and only those:
- Every service that
Requiresthe failed one transitions to Failed withDependencyFailure. - Every service that merely
Wantsit is unaffected and starts normally. - Propagation is transitive: if A requires B and B requires C, and C fails, then B fails, then A fails — each with
DependencyFailure.
This is the payoff of the Requires/Wants distinction. A hard dependency failing takes its dependents down with it; a soft one failing is shrugged off. Choosing the right relationship is choosing how far a failure is allowed to travel.
On-demand starts #
When you start a service explicitly rather than at boot, peinit does the same graph work on a smaller scope — the requested service's transitive closure:
- Collect all transitive
RequiresandBindsTodependencies (and best-effortWants). - Validate that sub-graph (cycles, missing targets).
- Resolve
Conflicts— stop anything that conflicts. - Start the sub-graph with the same parallel scheduler.
Anything already in a satisfying state (Active, Completed, Skipped) is left alone — its dependency is already met, so there is no needless restart. Dependencies pulled in this way start with cause DependencyStart. If two on-demand starts need the same dependency at once, their start operations merge rather than racing.
Shutdown reverses the graph #
peinit does not need a separate stop-ordering configuration. Shutdown simply reverses the dependency graph: services with no dependents stop first, and services that others depend on stop last. A service is never stopped until everything that Requires or BindsTo it has already stopped.
The very last services to stop are the TCB daemons everything rests on — eventd, then authd, then lpsd, then registryd — with registryd stopped dead last, mirroring its position as the first service ever started. The full shutdown sequence is in Shutdown.
Where to start #
To see the states services move through as they start and stop in order, read The service lifecycle.
To understand how a target's failure interacts with the dependent's own restart policy, read Keeping services running.
To follow how dependency-driven starts appear as operations, read Jobs and operations.
Service identity and privileges
Peios / Using Peios / Services & jobs
Every service process runs under a KACS token — the kernel object that carries identity into every system call and is the input to every access decision. peinit's responsibility is to get the right token for a service and install it on the child process before the binary execs, so the service runs as the intended principal from its very first instruction.
One rule frames everything else: peinit never shares its own SYSTEM token with a service. Even a service configured Identity=SYSTEM gets a separately materialised token of its own — never peinit's. This keeps every service's identity independent and auditable, and it is one of peinit's security invariants.
How a token is materialised #
The Identity field decides where the token comes from:
Identity value | Token source |
|---|---|
SYSTEM | Minted by peinit from its own SYSTEM identity. |
| Any other principal name or SID | Requested from authd. |
| Absent or empty | authd, defaulting to LocalService. |
The SYSTEM path #
For a SYSTEM service, peinit mints an independent SYSTEM token using its own token as the template: it reads its own identity (user SID S-1-5-18, the group list, the privilege set) and mints a fresh primary token with the same identity, adding the service's per-service SID. The minted token is fully independent — the privilege trimming that follows affects only this token, never peinit's.
This path exists because of a bootstrapping problem: the platform services that make the normal token flow possible cannot use it, because they have to start before it exists. registryd, authd, lpsd, and eventd all run as SYSTEM and are all minted this way during boot, before authd is available to mint anything.
The authd path #
For any other identity, peinit asks authd for a token: it sends the Identity value verbatim, and authd routes it to the right source —
- Well-known principals (
LocalService,NetworkService, …) → a built-in identity with a predefined minimal privilege set; - Local service accounts → lpsd, the local principal store;
- Domain accounts → the directory connector;
— resolves the principal's SIDs, mints a token, creates a logon session, and hands the token back to peinit to install. peinit neither knows nor cares whether the identity is local or domain; routing is authd's job.
The per-service SID #
Every service token — minted or authd-issued — carries a per-service SID in its group list. It uses authority S-1-5-80 and is derived deterministically from the service name: the SHA-1 of the uppercased name (as UTF-16LE), with the digest split into five sub-authorities. authd adds it automatically for the tokens it mints; peinit computes and adds it itself for SYSTEM tokens, no authd involvement needed.
The point of the per-service SID is fine-grained access control even when services share an identity. A dozen services might all run as LocalService, but each has a unique service SID, so an ACL can grant access to exactly one of them without inventing a dedicated account. It is also what keeps the platform services — all running as SYSTEM — distinguishable to AccessCheck. See Well-known principals for the SID landscape this fits into.
Privilege restriction #
A token comes from its source (authd or the SYSTEM mint) with a default set of privileges. RequiredPrivileges lets a definition trim that set down to only what the service needs:
- peinit reads the
RequiredPrivilegesallow-list and removes every privilege not on it from the token before exec. - Restriction is purely subtractive. peinit removes privileges; it never adds them. A service cannot acquire a privilege its token's source did not grant.
- If
RequiredPrivilegesis absent, the token's default privilege set is used unchanged.
This is least-privilege made concrete: eudev, for instance, runs as SYSTEM (it needs device access during early boot) but with its privilege set stripped to the minimum it actually uses, so a compromise of eudev does not hand an attacker the full SYSTEM privilege set. Removal is permanent for the life of that token — it is not a disable that the service can re-enable. See Privileges for what the individual privileges grant.
Which identity runs what #
A service is not just its main process — it has hooks, health checks, and a reload command, and each runs under a defined identity:
| Context | Runs as |
|---|---|
| Main process | The service's Identity. |
ExecStartPre / ExecStartPost | HookIdentity if set, otherwise the service's Identity. |
| Health checks | The service's Identity, always. |
ExecReload (command form) | The service's Identity, always — HookIdentity does not apply. |
| Ad-hoc jobs | The token captured by JFS — the submitter's (possibly impersonated) identity. |
Token materialisation for hooks follows the same rules: a HookIdentity of SYSTEM is minted; any other principal is requested from authd, at the point the hook runs.
HookIdentity exists so hooks can run with different — usually higher — privileges than the service itself: creating directories in privileged locations, or running a database migration as an admin identity, before the long-running daemon drops to a lesser identity.
Security invariants #
The identity model rests on a few rules peinit never breaks:
- peinit never shares its SYSTEM token. Even
Identity=SYSTEMservices get a separately minted token. - peinit never drops its own SYSTEM identity. PID 1 runs as SYSTEM for the life of the system — its identity is axiomatic, granted by the kernel at boot.
- Privileges are subtractive only.
RequiredPrivilegesremoves; it can never add. - Identity is deterministic. Every service runs as a known principal.
SYSTEMmust be declared explicitly; an emptyIdentityis the minimalLocalService, never SYSTEM.
Where to start #
Identity decides what a service can do; the ServiceSecurity descriptor decides who can manage it — two independent concerns. Read Who can manage a service for the other half.
To see how the token is installed alongside the rest of the process setup, read The execution environment.
For the identity primitives themselves — tokens, SIDs, privileges — start at Tokens.
The execution environment
Peios / Using Peios / Services & jobs
When peinit starts a service, the process it hands you is built deliberately — a clean context with a known identity, a known environment, and only the resources peinit chose to give it. This page covers everything about that context except the token (its own page): the cgroup the process lives in, its standard streams, the environment it sees, the limits on it, and the hooks and checks that run around the start.
A clean context #
A service inherits only what peinit explicitly hands it — its standard streams and any stored file descriptors. Every other descriptor peinit holds — the control socket, the notify socket, the event-loop fd, the registry and authd and eventd connections — is opened close-on-exec, so it closes automatically at exec and can never leak into a service. peinit also resets the child's signal state to defaults (it runs with all signals blocked for its own signalfd; a service must not inherit that). The guarantee is that a service starts from a clean slate, not from peinit's privileged one.
The cgroup tree #
Every service runs in its own cgroup tree under /sys/fs/cgroup/peinit/, with separate sub-cgroups for the main process, hooks, and health checks:
/sys/fs/cgroup/peinit/<service>/
├── main/ the main process
├── hooks/ pre/post hooks
└── health/ health checks
peinit uses cgroups for two things only: tracking every process a service spawns (so a child that forks its own children is still accounted to the service), and clean kill (terminating the entire tree at once, so nothing is orphaned). It does not use cgroups for resource accounting or limits.
The split into sub-cgroups serves a real purpose: it satisfies cgroup v2's "no internal processes" rule, and it lets peinit kill a service's hooks or health checks without touching the main process.
When an old tree cannot be removed — because a process survived SIGKILL in D-state and leaked it — peinit creates a fresh generational tree (<service>.gen<N>) for the next start, so a stuck old instance never blocks a new one. Leaks are surfaced in the service's warnings; they are a sign of an underlying I/O fault, covered in Keeping services running.
Standard streams #
peinit wires a service's standard streams before exec:
- stdin is redirected to
/dev/null. peinit never gives a service an interactive input channel; a service that needs input must obtain it explicitly (a socket, a stored fd). - stdout and stderr are redirected to pipes peinit holds, so it can capture and forward every line. This is the subject of Service output and logging.
Attaching a terminal #
A service that sets TTYPath to an absolute terminal path — /dev/console, /dev/tty1, a serial line — gets that terminal on all three standard streams instead of the wiring above, and becomes a session leader owning it as its controlling terminal. That last part is what makes job control work: Ctrl-C interrupts, fg/bg behave, and a hangup reaches the process group. It is what a boot shell or a login prompt needs.
It carries a path rather than a flag because which terminal is a per-service question: a maintenance shell on tty1 while the kernel console is a serial line is an ordinary thing to want.
The environment #
peinit constructs each service's environment in layers, lowest precedence first — it does not pass through its own near-empty startup environment.
- Compiled-in base. A fixed floor peinit always provides — currently just
PATH:/bin. - Global
EnvVars. Each value underMachine\System\Init\EnvVars\becomes a variable (value name → variable name).EnvVars\PATHoverrides the basePATH; other names add. Every service gets this layer except registryd, which serves the key itself — see the warning below for why that exemption exists. - Per-service
Environment. The definition's ownKEY=VALUEentries, which override layers 1–2. - Protocol variables.
NOTIFY_SOCKET(always) andLISTEN_FDS/LISTEN_FDNAMES(only when stored fds are injected). These have the highest precedence and cannot be overridden — a service that clobberedNOTIFY_SOCKETwould break sd_notify.
A change to EnvVars\ takes effect on a service's next start, like the per-service Environment field — it is not pushed into running services.
peinit deliberately does not set HOME, USER, LOGNAME, SHELL, or TERM. Peios identity is a token (a SID), not a passwd entry, so there is no canonical home directory or login shell to fill in. A service that needs any of these supplies it through EnvVars\ or its own Environment.
Working directory and limits #
| Field | Effect |
|---|---|
WorkingDirectory | The process's working directory. Must be a non-empty absolute path; defaults to /. |
LimitNOFILE | RLIMIT_NOFILE — the maximum number of open file descriptors. |
LimitCORE | RLIMIT_CORE — the maximum core-dump size, in bytes. |
peinit also sets oom_score_adj: -1000 (OOM-immune) for ErrorControl=Critical services, and the default 0 for everyone else — so the kernel never picks a Critical platform daemon as an out-of-memory victim.
Runtime directories #
RuntimeDirectories gives a service private scratch space under /run without an ExecStartPre that has to mkdir it by hand. Each name you list becomes a directory created directly under /run — RuntimeDirectories=myapp yields /run/myapp — set up immediately before the main process starts. Each one gets a security descriptor granting full access to SYSTEM, Administrators, and the service's own SID, so the service can write to its directory while other principals cannot. If a directory can't be created, or its descriptor can't be applied, the start fails with ParentSetupFailure.
peinit does not remove these directories when the service stops. /run is a boot-scoped tmpfs, so they simply disappear at the next boot rather than being torn down on each stop — a restarting service finds its runtime directory (though not necessarily its contents) already in place.
For the exact naming rules on each entry and the field's type and default, see the Registry key reference.
Pre-exec and post-exec hooks #
Hooks let a service do work around its main process without baking it into the binary.
ExecStartPreruns before the main binary. Each command runs in sequence, in thehooks/sub-cgroup; any hook exiting non-zero aborts the start (the whole cgroup is killed and the service Fails withPreHookFailure). Use it for prerequisites that must hold before the service starts — creating a runtime directory, waiting on a precondition.ExecStartPostruns after readiness (Simple) or after a successful exit (Oneshot). A post-hook failure is logged but does not fail the service — by the time it runs, the service is already up. (A Oneshot that fails never runs its post-hooks.)
Hooks run under HookIdentity if set, otherwise the service's own identity. Their output is captured and tagged like the main process's, labelled with the hook (e.g. jellyfin/ExecStartPre[0]).
Command string parsing #
The command fields — ExecStartPre, ExecStartPost, ExecReload, HealthCheck — are parsed into an argument list with simple, predictable rules. peinit never invokes a shell.
- Whitespace splits the command into arguments; double quotes group text containing spaces into one argument and are not retained (
--name="hello world"→ one argument--name=hello world). - There is no shell expansion, variable substitution, or globbing. Backslash is literal; a single quote is an ordinary character.
- The first argument is the executable and must be an absolute path beginning with
/. Relative names and PATH search are validation errors. - An empty or whitespace-only command, or an unclosed quote, is a validation error.
If a command genuinely needs shell features, wrap it in a script and point the field at the script.
ExecReload can also be a signal, written signal:<NAME> — for example signal:SIGHUP. The name must be an exact, canonical Linux signal name (uppercase, no aliases, no numbers); SIGKILL and SIGSTOP are rejected because they cannot be handled as a reload. With no ExecReload at all, reload sends SIGHUP. The reload lifecycle is covered in Controlling services.
Conditions and asserts #
Before peinit runs any hook or the main process, it evaluates conditions and asserts — start-time checks in the same type:argument form. The difference is what failure means:
- A failed Condition skips the service — it transitions to Skipped, which satisfies its dependents (it succeeded by not needing to run). Conditions express "only run here if it makes sense."
- A failed Assert fails the service — it transitions to Failed with
AssertionError. Asserts express "this was expected to run, and a precondition is missing."
Conditions are checked first; only if all pass are asserts checked. All entries are AND-ed. The check types:
| Type | Form | Passes when |
|---|---|---|
path | path:<path> | the path exists (any type) |
file | file:<path> | a regular file exists |
directory | directory:<path> | a directory exists |
registry | registry:<key> | the registry key exists |
Two constraints follow from peinit being single-threaded PID 1, which must never block:
registry:checks are cache-only. Aregistry:check may only name a key peinit already caches (underMachine\System\Services\orMachine\System\Init\); it is evaluated against the in-memory model, never a live read. Naming any other key is a validation error.- Filesystem checks run off the main loop.
path:/file:/directory:checks are performed by a short-lived forked helper, not astat()on the event loop, becausestat()can wedge on a hung mount.
That timeout is the PreStartCheckTimeout field, which defaults to 5 seconds. A filesystem check that does not finish within it — say a stat() wedged on a hung mount — is treated as not satisfied, fail-safe: peinit kills the helper and treats the check as unmet, so a Condition skips the service and an Assert fails it, rather than letting the event loop hang.
The fd store #
The fd store lets a service preserve file descriptors across restarts — most usefully a listening socket, so a stateful daemon can restart without dropping connections. It is opt-in: FdStoreMax (default 0) sets the maximum number of fds peinit will hold; 0 disables it.
The mechanics ride on sd_notify:
- A service stores an fd by sending
FDSTORE=1with the fd attached (and optionallyFDNAME=<name>; the default name isstored). peinit holds it. - It removes one with
FDSTOREREMOVE=1andFDNAME=<name>. - On the next start, peinit injects the stored fds starting at fd 3, sets
LISTEN_FDSto the count andLISTEN_FDNAMESto the colon-separated names, then clears the store. (This is the sameLISTEN_FDSconvention systemd uses, so software with existing fd-passing support works unmodified.)
The store survives an automatic restart (crash → restart policy → new start) — that is the whole point. It is cleared on an explicit stop or shutdown (the service is not coming back), and when a removed definition is finally discarded.
Where to start #
For the identity half of the launch — how the token is chosen and installed — read Service identity and privileges.
For what happens to the stdout/stderr pipes peinit holds, read Service output and logging.
For how conditions, asserts, and hooks fit into the start sequence and its states, read The service lifecycle.
Controlling services
Peios / Using Peios / Services & jobs
peiosctl is the command-line tool for driving peinit at runtime — starting and stopping services, querying their state, reloading configuration, and shutting the system down.
peiosctl <command> [service] [flags]
$ peiosctl status jellyfin # current state of one service
$ peiosctl start jellyfin # start it, wait until Active or Failed
$ peiosctl list # every service you can query
$ peiosctl shutdown reboot # graceful reboot
Underneath, peiosctl is a thin client over peinit's control socket at /run/services/peinit/control.sock. The wire protocol — not the CLI — is the normative interface, so everything here (commands, rights, semantics) holds regardless of which front-end you use.
How the control interface works #
The socket speaks newline-delimited JSON: one request object per line, one response object per line. A request and its success response look like this:
Two properties are worth knowing even if you only ever use peiosctl:
- Every command is access-controlled. When you connect, peinit captures your token from the kernel and runs AccessCheck against the target service's descriptor for every command. There is no "trust localhost," no override. Who may do what is the subject of Who can manage a service.
- Lifecycle commands create operations. A
start/stop/restart/reload/resetreturns anoperation_id— a GUID you can poll. Conflict resolution between concurrent commands happens at the operation layer, which is why two simultaneousstarts merge instead of colliding.
Service commands #
| Command | Does | Required right |
|---|---|---|
start | Run the service through its full start sequence. | SERVICE_START |
stop | SIGTERM, then SIGKILL after StopTimeout. | SERVICE_STOP |
restart | Stop then start, as one operation. | SERVICE_STOP + SERVICE_START |
reload | Re-read configuration (ExecReload, or SIGHUP). | SERVICE_INTERROGATE |
reset | Clear Failed/Abandoned/Skipped → Inactive. | SERVICE_STOP |
status | Report state, cause, PID, uptime, health, current job and operation, warnings. | SERVICE_QUERY_STATUS |
list | List services and states (filtered to what you can query). | (per-service SERVICE_QUERY_STATUS) |
$ peiosctl restart jellyfin
$ peiosctl reload nginx
$ peiosctl reset failed-migration # clear a Failed state without starting
System commands #
| Command | Does | Required right |
|---|---|---|
shutdown <type> | Graceful shutdown. type is poweroff, reboot, or halt. | SYSTEM_SHUTDOWN |
reload-config | Re-read all definitions and rebuild the graph (atomic). | SYSTEM_RELOAD_CONFIG |
operation-status <id> | Report the state of an operation by GUID. | SERVICE_QUERY_STATUS on its target |
$ peiosctl shutdown poweroff
$ peiosctl reload-config
$ peiosctl operation-status a1b2c3d4-...
Wait semantics #
By default, a lifecycle command blocks until its operation reaches a terminal state — start waits for Active (or Failed), stop waits for Inactive, and so on. Pass --no-wait to get the operation_id back immediately and poll with operation-status instead.
| Command | Default | Waits for |
|---|---|---|
start | wait | Active (Simple) / Completed or Inactive (Oneshot), or Failed |
stop | wait | Inactive |
restart | wait | the successful start target, or Failed |
reload | no-wait | (with --wait) the Reloading state to resolve |
reset | immediate | — |
reload is the exception — it returns immediately by default, because a reload may have no observable completion. With --wait, the response carries a mode:
mode | Meaning |
|---|---|
confirmed | The service signalled READY=1 (and, for a command reload, the command exited 0). |
advisory | The reload was issued and the detection window elapsed without explicit confirmation. |
failed | The ExecReload command exited non-zero or timed out. The service stays Active — a failed reload never takes down a running service. |
The detection window is a fixed 2 seconds — it is built in and is not configurable via the registry. If the service says nothing within that window, the reload resolves as advisory and the service stays Active.
The command × state matrix #
A command sent to a service in an unexpected state returns an error, not a silent no-op. This matrix is the authority on what each command does in each state:
| Command | Inactive | Starting | Active | Reloading | Stopping | Completed | Backoff | Failed | Abandoned | Skipped |
|---|---|---|---|---|---|---|---|---|---|---|
| start | Start | MERGE | ALREADY | ALREADY | QUEUE | Start | DEFER | Start | ERROR | Start |
| stop | NOOP | Cancel+Stop | Stop | Stop | MERGE | Clear | Cancel | NOOP | ERROR | NOOP |
| restart | Start | QUEUE | Restart | Restart | QUEUE | Start | Restart | Start | ERROR | Start |
| reload | ERROR | ERROR | Reload | MERGE | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR |
| reset | NOOP | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR | Clear | Clear | Clear |
| status | OK | OK | OK | OK | OK | OK | OK | OK | OK | OK |
Legend:
- MERGE — an operation of this type is already running; your command merges into it and you get its GUID.
- DEFER — your
startis accepted and creates a Pending start operation, but it does not run yet: it waits out the service's existing backoff deadline and then starts. If a deferred start is already waiting, you merge into it and get its GUID. - ALREADY — already in the target state; returns the current status, not an error.
- QUEUE — queued as Pending; runs after the current operation finishes.
- NOOP — no effect; returns the current status.
- Clear / Cancel — clear the state to Inactive / abort the current operation, then proceed.
- ERROR — invalid for this state; returns an error with an explanation.
The Backoff column is the subtle one: the service is down with an automatic restart pending, so start is deferred — it creates a Pending start that honours the remaining backoff delay and only runs once that deadline expires (a second start merges into the one already waiting), stop cancels the pending restart and any deferred start (the service goes Inactive), restart cancels the automatic one and does an admin restart, and reload/reset are invalid because no process exists.
Reading status #
status returns the full picture for one service:
stateandcauseare the lifecycle pair — read them together.status_textis the latestSTATUS=string the service sent via sd_notify (nullif never sent; cleared on each restart).current_jobandcurrent_operationare the job and operation GUIDs, ornull.healthishealthy,unhealthy,unknown, ornull(no health check).definition_removedistruewhen the definition was deleted but an instance is still draining.warningslists leaked sub-cgroups and other operator-relevant notices.
list returns a compact summary of every service you can query — services you lack SERVICE_QUERY_STATUS on are simply omitted, not denied:
operation-status returns one operation by GUID; an unknown or expired GUID is the UNKNOWN_OPERATION error. (Operations are dropped after a short retention grace once terminal — long enough for a polling client to read the result, not forever.)
Error codes #
An error response is {"status": "error", "code": "...", "message": "..."}. The code is one of:
| Code | Meaning |
|---|---|
ACCESS_DENIED | AccessCheck denied the command against the target descriptor. |
UNKNOWN_SERVICE | No such service definition (also returned for start/restart/reload on a definition-removed service). |
UNKNOWN_OPERATION | No such operation GUID — never existed, or dropped after its retention grace. |
MALFORMED_REQUEST | The request line is not a single valid JSON object. |
REQUEST_TOO_LARGE | The request exceeds MaxRequestSize. |
INVALID_COMMAND | The command field is missing or unknown. |
INVALID_ARGUMENTS | A required field is missing or malformed (e.g. shutdown with no valid type). |
INVALID_STATE | Not valid for the service's current state (an ERROR cell above), or rejected because the system is already shutting down. |
OPERATION_TIMEOUT | A --wait operation did not reach a terminal state within its timeout. |
INTERNAL_ERROR | peinit hit an internal failure executing the command. |
The message is human-readable and non-normative — read it for context, key off the code.
Connection limits #
peinit enforces hard limits on the control socket, all tunable under Machine\System\Init\:
| Key | Default | Limits |
|---|---|---|
MaxControlConnections | 32 | Concurrent client connections (excess are refused at connect). |
MaxRequestSize | 65536 | Bytes per request. |
ConnectionTimeout | 30 | Seconds an idle connection may sit before it is closed. |
Exit status #
| Code | Meaning |
|---|---|
0 | The command succeeded. |
1 | A usage error. |
| non-zero | The command failed — an access denial, unknown service, invalid state, or timeout. |
Where to start #
To understand the states the matrix refers to, read The service lifecycle.
To understand the operations every lifecycle command creates and how to poll them, read Jobs and operations.
To configure who may run each command, read Who can manage a service.
Who can manage a service
Peios / Using Peios / Services & jobs
A service is a securable object. Just as a file or a registry key carries a security descriptor that decides who may touch it, a service carries one that decides who may start, stop, query, or reload it. peinit enforces it on every control command — there is no command that skips the check.
This is the second, independent half of the security model. The first half, Service identity and privileges, is about what a service can do (its token). This page is about who can manage the service — a completely separate question, answered by a completely separate descriptor.
Two descriptors, two questions #
A service is associated with two descriptors that are easy to conflate but answer different questions and are enforced by different components:
| Descriptor | Question | Stored | Enforced by |
|---|---|---|---|
| Registry key SD | Who can read or edit the definition? | On the Machine\System\Services\<name> key | LCS, at key-open time |
| ServiceSecurity SD | Who can manage the running service? | As the ServiceSecurity binary value on that key | peinit, on every control command |
The registry key SD is ordinary registry access control and not peinit's concern — peinit reads definitions as SYSTEM, which has full access. The ServiceSecurity SD is peinit's domain, and the rest of this page is about it.
They are genuinely independent. An administrator might be able to query a service's status (ServiceSecurity grants SERVICE_QUERY_STATUS) but not read its configuration (the registry key SD denies read) — or the reverse. Runtime control and configuration access are separate concerns, and both combinations are valid.
Service access rights #
The ServiceSecurity descriptor grants these rights:
| Right | Bit | Grants |
|---|---|---|
SERVICE_QUERY_STATUS | 0x0001 | Query state, PID, cause, health, warnings. |
SERVICE_START | 0x0002 | Start the service. |
SERVICE_STOP | 0x0004 | Stop the service. |
SERVICE_INTERROGATE | 0x0008 | Reload the service. |
SERVICE_ALL_ACCESS | 0x000F | All of the above — the "full access" granted to SYSTEM by default. |
restart requires both SERVICE_STOP and SERVICE_START, since it is a stop followed by a start. reset requires SERVICE_STOP.
When peinit evaluates the descriptor it maps the generic rights as follows, so a descriptor written with generic rights behaves sensibly:
| Generic | Maps to |
|---|---|
GENERIC_READ | SERVICE_QUERY_STATUS |
GENERIC_WRITE | SERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE |
GENERIC_EXECUTE | SERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE |
GENERIC_ALL | SERVICE_ALL_ACCESS |
How a command is authorised #
Every control command runs the same gate:
- peinit captures the caller's token from the kernel (
kacs_open_peer_token) — the caller's effective identity at connection time, so if the caller is impersonating, the impersonated identity is what is checked. - peinit resolves the target service and its ServiceSecurity descriptor.
- peinit runs AccessCheck: the caller's token against the descriptor, for the right the command needs.
- Denied → return
ACCESS_DENIEDand log the attempt (caller SID, target service, requested right). - Granted → execute the command.
The default descriptor #
If a service has no ServiceSecurity value, it inherits its parent key's. If no ancestor sets one either, peinit applies a built-in default:
- SYSTEM (
S-1-5-18) — full access. - Administrators (
S-1-5-32-544) — query and stop only.
So out of the box, administrators can see and stop a service but not start or reload it unless a descriptor grants more — a conservative default that you widen deliberately.
ServiceSecurity is hot-reloaded: a change to the value in the registry takes effect on the next control request, with no service restart. peinit picks the change up through a registry notification. This is why ServiceSecurity is in its own mutability class — access policy should be able to change without disturbing a running service.
The system control descriptor #
Some operations are not about any one service — shutdown and reload-config act on the whole system. These are checked against peinit's own descriptor, stored at Machine\System\Init\ControlSecurity:
| Right | Bit | Grants |
|---|---|---|
SYSTEM_SHUTDOWN | 0x0001 | Initiate poweroff, reboot, or halt. |
SYSTEM_RELOAD_CONFIG | 0x0002 | Re-read all definitions and rebuild the graph. |
Its generic mapping deliberately gives GENERIC_READ nothing — there is no "read" of the system control object, only the two actions:
| Generic | Maps to |
|---|---|
GENERIC_READ | (nothing) |
GENERIC_WRITE | SYSTEM_RELOAD_CONFIG |
GENERIC_EXECUTE | SYSTEM_SHUTDOWN |
GENERIC_ALL | SYSTEM_SHUTDOWN | SYSTEM_RELOAD_CONFIG |
The default grants SYSTEM full access and Administrators both rights. peinit loads this descriptor at boot and hot-reloads it on registry change, exactly like ServiceSecurity.
The list command filters, it does not deny #
list is access-control-aware in a quieter way: it returns only the services the caller has SERVICE_QUERY_STATUS on, and simply omits the rest. A caller with no query rights gets an empty list, not a denial. This means a low-privilege principal cannot even enumerate the services it cannot see — the existence of a service is itself information the descriptor controls.
The boundaries that hold #
A few invariants are worth stating outright, because they are what make this trustworthy:
- peinit never bypasses AccessCheck for a control operation. No backdoor, no override flag, no "trust localhost."
- The descriptors are the only policy inputs. peinit consults the ServiceSecurity and ControlSecurity descriptors and nothing else — not config files, not environment variables, not hardcoded lists.
- One service's state is never exposed to another without a check.
statusis per-service access-controlled;listfilters.
Where to start #
For the other half of the security model — what a service can reach once it is running — read Service identity and privileges.
For the commands these rights gate, read Controlling services.
For the descriptor and AccessCheck machinery itself, read Security descriptors and Access decisions.
Jobs and operations
Peios / Using Peios / Services & jobs
A service is a long-lived definition. But when you read a status output or query the event log, two shorter-lived objects show up alongside it: the job (one process execution) and the operation (one requested action). They are what make peinit observable — every run and every command carries a GUID you can follow. This page covers both, and the special case of ad-hoc jobs.
Jobs: what actually ran #
Every time peinit forks a process — a service's main binary, a pre- or post-hook, a health check, or an ad-hoc run — that single execution is a job, with its own GUID and exit result. If a service is the what, a job is what actually ran. A restart does not reuse a job; it creates a new one. The service always tracks its current job GUID, and a status query reports it.
Jobs come in five types:
| Type | Created for |
|---|---|
ServiceMain | A service's main process. |
PreExecHook | One ExecStartPre command. |
PostExecHook | One ExecStartPost command. |
HealthCheck | One health-check run. |
AdHoc | A process submitted via JFS. |
Their lifecycle is simpler than a service's, because a job only tracks a process — not policy:
| State | Meaning |
|---|---|
Created | The job object exists but the process is not forked yet (pre-hooks may be running). |
Running | The process is alive. |
Completed | The process exited successfully. |
Failed | The process failed — or peinit classified the job failed before fork (a parent-side setup error). |
Abandoned | The process survived SIGKILL (D-state). |
A job record carries the things you would want for forensics: the resolved identity and a token summary, the image path and arguments, created/started/ended timestamps, the exit code or signal, the cgroup, the failure cause, and the operation_id that created it (null for ad-hoc jobs). The clean division of ownership is: the service owns policy (restart, dependencies, health schedule, current state); the job owns the execution facts (PID, exit, timestamps, identity, cgroup, log correlation).
Retention and log correlation #
peinit keeps only active jobs in memory. When a job reaches a terminal state it emits a structured event (through KMES) carrying the full record, then drops the job. peinit keeps no job history — eventd is the historian, consuming those events from the kernel ring buffer. The lifecycle events are job.created, job.started, and job.ended.
Separately, all of a job's stdout/stderr is forwarded to eventd tagged with the job's GUID. That is what lets a query like "show me the logs for job X" return exactly that execution's output — not interleaved with the run before or after it.
Ad-hoc jobs #
An ad-hoc job is an arbitrary process a service asks peinit to run on behalf of one of its clients. It is not tied to a persistent definition — it runs once, reports its result, and is cleaned up. The motivating case: a service has impersonated a user and wants a process run as that user, but supervised centrally by peinit rather than by the service itself.
The obstacle is identity delegation: a service cannot simply forward an impersonation token over a socket. JFS (the Job Forwarding Subsystem) solves this in the kernel — it captures the caller's effective token and delivers it to peinit along with the job request. peinit forks, installs that captured token, and supervises the result like any other job.
An ad-hoc request carries a deliberately small subset of the service fields — ImagePath, Arguments, Environment, WorkingDirectory, Timeout, Description. A couple are worth pinning down: Timeout is in seconds, and 0 means no limit (the job runs for as long as it needs). WorkingDirectory defaults to / when you leave it out. The policy fields (restart, dependencies, health checks, ErrorControl, triggers) are not available — those belong to persistent definitions. An ad-hoc job runs once in its own cgroup, routes its output to eventd, and on exit emits job.ended and is dropped. If it overruns its Timeout, peinit escalates SIGTERM → SIGKILL exactly as a service stop does.
Ad-hoc jobs bypass the operation model entirely — there is no service to "start," so the request creates a job directly. The job is the whole lifecycle.
Operations: what was requested #
An operation is a first-class object representing a requested state-machine action on a service. Rather than letting control commands mutate state directly, peinit turns each one into an operation that is validated, queued, and executed by its event loop. This is what gives concurrent callers — admin tools, automated triggers, dependency propagation — explicit, observable conflict resolution instead of races.
There are five operation types — Start, Stop, Restart, Reload, Reset — and each carries a source recording why peinit created it:
| Source | Created by |
|---|---|
Admin | A control client. |
Boot | The Phase 2 boot lifecycle. |
Shutdown | The shutdown lifecycle. |
DependencyPropagation | A start pulling in a dependency. |
RestartPolicy | An automatic restart. |
Timer | A timer firing. |
BindsToRecovery | A bound target returning to Active. |
BindsToPropagation | A bound target stopping. |
ConflictResolution | A Conflicts eviction. |
OnFailure | A failed service's fallback handler. |
The source is why a status showing current_operation.source: "restart_policy" tells you the service is being auto-restarted, while "admin" tells you a person asked.
Operation lifecycle #
flowchart LR
P["Pending"] --> R["Running"]
R --> C["Completed"]
R --> F["Failed"]
R --> A["Aborted"]
P --> M["Merged"]
P --> X["Cancelled"]
P --> F
| State | Meaning |
|---|---|
Pending | Validated and queued, waiting on a precondition (e.g. a prior stop to finish). |
Running | Executing — the service is transitioning. |
Completed | Achieved its goal (start reached Active/Completed; stop reached Inactive/Failed). |
Failed | Did not achieve its goal — including timing out while still Pending. |
Merged | Folded into an identical operation already in flight (records the survivor's GUID). |
Cancelled | Terminated while still Pending — never ran. |
Aborted | Terminated while Running — interrupted in progress. |
Cancelled and Aborted are the same idea at two points: cancelled never ran, aborted was running. The reason (admin action, supersession) is a property of the event, not the state.
Conflict resolution and merging #
When a command arrives for a service that already has an operation in flight, peinit resolves the collision deterministically — the same logic the command × state matrix summarises:
- Same type (start over start, stop over stop, reload over reload) → merge. The new caller transparently receives the existing operation's GUID; from their side, their request is in progress.
- Stop wins over start. An explicit stop cancels a pending start or aborts a running one. The admin said stop.
- Later supersedes earlier, and a start while a stop is in flight is queued to run after the stop.
Timeout and retention #
An operation inherits its target's timeout as its maximum lifetime — StartTimeout for start/reload/reset, StopTimeout for stop, and the sum of both legs for restart. Crucially, the clock starts at operation creation, including queue time — from the caller's perspective they have been waiting since they sent the command, so a long queue can time an operation out before it even runs.
Terminal operations are emitted as events (operation.requested, .started, .completed, .failed, .cancelled, .merged, .aborted) and dropped from memory after a short grace (default 60 s — long enough for a polling client to read the result). As with jobs, peinit keeps no operation history; eventd does.
How they surface #
You meet jobs and operations in three places:
status—current_jobandcurrent_operationgive the GUIDs of the service's active execution and active action (ornull).operation-status <id>— polls one operation to a terminal state; a--waitlifecycle command does this for you under the hood.- eventd — the durable history. Every job and operation lifecycle transition lands there as a structured event, which is how you reconstruct what happened after the objects themselves are gone from peinit's memory.
Where to start #
To create and poll operations from the command line, read Controlling services.
To understand the service states an operation drives a service through, read The service lifecycle.
To query the durable job and operation history, read Auditing.
Service output and logging
Peios / Using Peios / Services & jobs
peinit is not a logging system — storage, indexing, and queries belong to eventd. But peinit holds the stdout/stderr pipes of every service at the moment it forks, so it is unavoidably in charge of where output goes, and it has to cope with the window early in boot before eventd even exists. This page is about that responsibility.
Capturing output #
When peinit forks a service it creates pipes for stdout and stderr, redirects the child's streams to the write ends, and keeps the read ends — monitoring them in its event loop. (The child's stdin is /dev/null; see The execution environment.) It reads output line by line and tags each line with:
- the service name,
- the stream (stdout or stderr),
- a timestamp,
- the job GUID.
The job GUID is the important one: it is what lets a later query return exactly one execution's output, never interleaved with the run before or after it. Hook output is captured too, labelled with the hook (e.g. jellyfin/ExecStartPre[0]), and so is health-check output — invaluable for diagnosing why a check failed.
Logs are best-effort; audit events are not #
This is the distinction to internalise, because it explains why some things survive a crash and others do not. peinit handles two different streams of information with two different guarantees:
| Service logs | Audit events | |
|---|---|---|
| What | stdout/stderr from services and hooks | Access denials, critical failures, recovery-mode entry, graph errors, job/operation lifecycle |
| Path | Forwarded to eventd's log socket | Emitted into the KMES kernel ring buffer |
| Guarantee | Best-effort — may be dropped under load | Durable — survive eventd restarts and reboots |
| Available from | Once eventd is up (buffered before) | The moment PKM loads — before eventd, before the registry |
Service logs are a convenience: useful, but lossy by design. Audit events are part of the security posture and go through KMES, which persists them in the kernel from the instant the module loads. So peinit keeps a pre-eventd buffer for logs but needs none for events — eventd picks events up from the ring buffer whenever it attaches, no matter how late.
The pre-eventd buffer #
Before eventd is running there is nowhere to send logs — its socket does not exist yet. peinit therefore buffers captured output in a fixed-size in-memory pre-eventd buffer (default 1 MB); when it fills, the oldest entries are dropped.
In practice the only services that run before eventd are registryd, lpsd, authd, and eudev. The first three are Peios-owned with controlled output; eudev is the real overrun risk (verbose device enumeration), and the restart budget naturally bounds output from a crash loop.
Flood protection #
A noisy service must never starve peinit's event loop. Several limits enforce that:
Key (Machine\System\Init\) | Default | Limits |
|---|---|---|
MaxLogLineLength | 8192 | Bytes per line; longer lines are truncated with a [truncated] marker. |
MaxLogBufferPerService | 65536 | Bytes buffered per service pipe before back-pressure. |
Two mechanisms back these up:
- A per-iteration read budget. Each pass of the event loop reads at most a bounded number of bytes from service pipes before moving on to other events — so no single chatty service can monopolise a loop iteration.
- Back-pressure, not dropping. If a service writes faster than peinit reads, the kernel pipe buffer fills and the service's own
write()calls block. This is deliberate: the service slows down. While reading the pipe, peinit does not drop output — the no-silent-drop guarantee covers reading the pipe. It is only downstream (the pre-eventd buffer, and eventd's datagram socket) that delivery becomes lossy.
The eventd handoff #
When eventd reaches Active, peinit switches from buffering to forwarding:
- It begins sending to eventd's log datagram socket (path from
Machine\System\eventd\LogSocketPath). - It replays the pre-eventd buffer, oldest first, preserving each line's timestamp and metadata. This replay is best-effort — the records are datagrams on a loss-tolerant socket and some may be dropped under load; peinit never blocks waiting to deliver them.
- It switches to real-time forwarding — new output goes out as it arrives.
- It clears the buffer.
From then on peinit is a pipe relay: read a line, tag it, forward it as a datagram. The log socket is non-blocking and loss-tolerant — if eventd cannot drain it fast enough the kernel drops further datagrams silently, because log ingestion must never exert back-pressure on the whole system through PID 1. peinit keeps no unbounded outbound buffer and never blocks on a send to eventd.
If eventd itself crashes after starting, peinit (which supervises it like any service) notices, re-enables the pre-eventd buffer, and repeats the handoff when eventd comes back. There is a log gap across the restart, bounded by the buffer size — but audit events are unaffected, because they were going to KMES the whole time and eventd resumes consuming them from the last persisted sequence.
Console output #
peinit writes its own operational messages to /dev/console:
- Phase 1 progress (mount results, registryd start),
- Phase 2 progress (service starts and failures, dependency errors),
- shutdown progress,
- recovery-mode entry,
- critical service failures.
Service stdout/stderr is not echoed to the console by default — the console is for peinit's status messages only. This keeps the console legible during boot and, crucially, usable in Recovery mode, where it may be the only interface an administrator has.
Where to start #
To see how the pipes are wired and what else the process inherits, read The execution environment.
To follow the job GUID that tags every log line, read Jobs and operations.
For where the logs and events actually go — storage, indexing, and queries — read Auditing.
Boot and boot modes
Peios / Using Peios / Services & jobs
peinit's boot has two phases and a chicken-and-egg problem to solve. The problem: peinit reads everything it does from the registry, but the registry is served by a daemon that something has to start first — and the identity authority that would mint that daemon's token does not exist yet either. The solution is to split boot into a hardcoded bootstrap that needs no registry, and a registry-driven phase that starts once the registry is up. The boundary between them is registryd.
Where peinit takes over #
peinit is PID 1, but it is not the first thing that runs. The initramfs assembles and mounts the real root — decryption, RAID/LVM, the root filesystem itself — and then hands control to peinit. The contract peinit relies on is narrow:
- The real root is already mounted read-write, and
/proc,/sys,/devare mounted and moved into it. - peinit is exec'd as PID 1 from a fixed path on the real root.
peinit does not assemble, decrypt, repair, or even re-mount the root — those need tools and configuration that belong to the initramfs. It also does not fsck the root or mount non-root storage (a data partition is mounted by an ordinary Oneshot service, not by peinit). For the trust and identity side of this handoff — signatures, the SYSTEM token peinit inherits — see peinit at PID 1.
Phase 1: the hardcoded bootstrap #
Phase 1 is compiled into peinit. It cannot change at runtime and touches no registry. It does the minimum to make Phase 2 possible:
- Confirm the root is writable with a single probe write. registryd's storage needs a writable root even for reads, so a read-only root cannot support Phase 2 → Recovery.
- Mount the remaining virtual filesystems —
/dev/pts,/dev/shm,/run,/sys/fs/cgroup— mounting each only if absent. A failure here → Recovery. - Restore the persisted random seed from
/var/state/peinit/random-seed, mixing it into the kernel's entropy pool early so anything that needs randomness during boot gets it. A missing seed is normal — first boots and stateless live boots have none — so peinit just carries on; a seed problem is never fatal and never sends boot to Recovery. - Establish the machine-id from
/lcl/etc/machine-id— a stable, opaque identifier for this install (used for log correlation, instance identity, and software compatibility). It is not a security principal: it is not a SID, an account, or a credential, and no authorisation decision depends on it. If the file is missing, empty, or malformed, peinit generates a fresh 128-bit ID and writes it before continuing. - Set the clock from the hardware RTC, so early timestamps and the boot counter are meaningful. A failure here → Recovery.
- Start registryd and wait for it to signal readiness, then probe-read the schema-version key to confirm it is actually serving reads. Any failure → Recovery — there is no Phase 2 without a registry.
- Provision boot-time paths. With registryd up and before Phase 2 starts, peinit applies the entries under
Machine\System\Init\ProvisionedPaths\— the registry-driven equivalent of tmpfiles.d, creating directories and files (with Peios security descriptors) that no single service owns. Best-effort entries that fail are logged and skipped, but an entry markedRequired=1that cannot be provisioned sends boot → Recovery. The individual keys are cataloged in the registry key reference. - Infrastructure setup — create the control socket, open the JFS device for ad-hoc jobs, bring up the loopback interface. Control-socket failure → Recovery; the other two are logged as warnings and boot continues.
Most Phase 1 failures are fatal to a normal boot, because none of the later machinery can run without this foundation — the only outcome is Recovery mode. The exceptions are the fail-soft steps called out above: a missing or unusable random seed, a regenerated machine-id, and best-effort provisioned paths all let boot continue.
registryd and loregd #
registryd is an interface, not a specific program. It is the path peinit execs to get a registry source daemon — the component that implements the registry's persistent storage and answers peinit's reads. The implementation behind that interface can vary; by default it is loregd.
This split matters in exactly one place: Recovery mode. In normal operation you only ever deal with the registryd abstraction — peinit starts it, treats it as opaque, and reads the registry through it. But when the registry itself is what broke, you need tools that work without a running registry, and those tools talk to the implementation directly. That is why the recovery tooling is named loregd (loregd --inspector, --recover-from-backup, …): in recovery you are working with the storage implementation, not the registry abstraction. It is the one context where the distinction is visible to an administrator.
Phase 2: the registry-driven boot #
With registryd serving reads, peinit boots the rest of the system from the registry:
- Read all definitions under
Machine\System\Services\. (A registry read timing out here → Recovery.) - Build and validate the dependency graph from the boot-triggered services and their transitive dependency closure. Validation runs before anything starts.
- Start services in dependency order, in parallel up to
MaxParallelStarts, with readiness gating releasing each service's dependents as it becomes satisfied.
Only services with a boot trigger are start candidates; demand-only services are pulled in only if something boot-triggered depends on them, and Disabled services are excluded from the graph (but kept in the model for on-demand start). The whole boot runs against one snapshot — mid-boot registry edits do not perturb it.
The platform daemons come up first because everything rests on them. They are all SYSTEM, all minted by peinit (no authd yet), and all ErrorControl=Critical:
| Service | Phase | Identity source | Readiness | ErrorControl |
|---|---|---|---|---|
| registryd | 1 | minted by peinit | sd_notify | Critical |
| eudev | 2 | minted by peinit (privileges stripped) | process alive | Normal |
| lpsd | 2 | minted by peinit | sd_notify | Critical |
| authd | 2 | minted by peinit | sd_notify | Critical |
| eventd | 2 | minted by peinit | sd_notify | Critical |
| networking | 2 | authd | sd_notify | Normal |
| sshd | 2 | authd | process alive | Normal |
| application services | 2 | authd | per-service | Normal |
Once authd is up, every subsequent service gets its token through the normal authd flow. The order above is emergent from the standard role definitions' dependencies, not hardcoded — change the dependencies and the order changes. In practice login services such as sshd come up last, so the system is fully operational before it starts accepting user sessions.
The three boot modes #
The three modes form an escalation path from normal operation to last-resort maintenance.
flowchart TD
F["Full boot"] -->|success| OK["operational<br/>counter reset"]
F -->|cycle/conflict w/ Critical| S["Safe mode<br/>(no reboot)"]
F -->|Critical failure| RB["sync + reboot"]
S -->|success| OKS["operational (reduced)<br/>counter reset"]
S -->|Critical failure| RB
RB --> CNT["counter increments"]
CNT -->|counter ≥ N| REC["Recovery mode"]
F -.->|Phase 1 failure| REC
Full mode #
The default. All boot-triggered services start in dependency order. A boot is successful — and the boot-attempt counter resets to 0 — when every Critical service has reached and held a dependent-satisfying state (Active, Completed, or Skipped) for a grace period (BootSuccessGrace, default 30 s). The criterion is "satisfying," not "Active," so that a Critical Oneshot that reaches Completed can still mark the boot good.
Safe mode #
Safe mode starts a reduced set of boot-triggered services and rebuilds the graph from scratch using only those, dropping dependencies on excluded services. Two categories are eligible:
- Critical services (
ErrorControl=Critical) — must start; a Critical failure here still follows the normal reboot path. - SafeMode services (
SafeMode=1) — best-effort; if they fail, Safe mode carries on without them.
Everything else is skipped. Safe mode is entered when:
- graph validation finds a cycle involving a Critical service, or
- an unresolvable conflict involving a Critical service, or
- the kernel command line says
peios.safemode=1.
The first two are configuration errors — rebooting would just hit them again — so peinit downgrades without rebooting. Safe mode is not entered by a Critical service crashing at runtime; that follows the reboot path. And it is purely a boot-sequencing concern: once booted, you can start any service by hand exactly as in Full mode. It is the mode for "the configuration is broken but the TCB is healthy."
Recovery mode #
Recovery mode is the last resort, and it offers no TCB guarantee — the administrator gets an unrestricted SYSTEM shell on the console and must treat it with corresponding care. It is a maintenance environment, not a degraded boot.
In Recovery, peinit completes the Phase 1 basics, tries to start registryd (ignoring failure — the shell must appear regardless), skips all Phase 2 services, and execs a SYSTEM shell on /dev/console (/bin/recsh if present, else /bin/sh), respawning it if it exits. It is entered when:
- the boot-attempt counter reaches N (default 3),
- the kernel command line says
peios.recovery=1, or - registryd fails during Phase 1 — entered immediately, with no reboot and no counter increment, because there is no Phase 2 to attempt.
Because the registry itself may be what broke, recovery provides tools that work without it — talking to the loregd implementation directly:
| Tool | Does |
|---|---|
loregd --inspector | Read the storage database directly for diagnosis. |
loregd --recover-from-backup | Restore from an automatic backup taken on every registryd startup. |
loregd --dangerously-clear-database | Wipe the registry entirely. Recoverable, because role definitions are the source of truth for service config. |
The boot-attempt counter #
The counter is what turns a crash-looping Critical service into an eventual Recovery shell instead of an infinite reboot loop. It is a plain integer in a file at /.peinit/boot-attempts — not in the registry, because the registry may be the very thing that is broken.
- peinit reads it at startup, before choosing a mode. The Recovery threshold (
counter ≥ N) is checked against this pre-increment value, so the default N of 3 admits exactly three attempts before Recovery. A missing file counts as 0; a corrupt or unreadable one → Recovery. Override N withpeios.bootattempts=Non the kernel command line, or set it to0to disable the check when the counter is itself the fault. - peinit increments it once per boot, right after confirming the root is writable and before Phase 2 — never before the root is known writable, or a read-only root would silently lose the increment and defeat escalation.
- The counter is reset to 0 on a successful Full or Safe boot (after the grace period).
- If the counter file cannot be written (disk full), peinit treats it as 0 and continues — a write failure must not by itself trigger Recovery.
A Critical service exhausting its restart budget — at boot or at runtime — triggers a sync and reboot, which increments the counter on the next boot. Repeat that enough and the counter crosses N, and peinit stops trying and hands you a Recovery shell. The counter deliberately does not try to catch a peinit too broken to reach its own increment; that is a binary-integrity problem, not a boot-loop problem.
Boot configuration #
| Key | Default | Controls |
|---|---|---|
Machine\System\Boot\MaxParallelStarts | 10 | Services starting concurrently (must be > 0; invalid → Recovery). |
Machine\System\Boot\BootSuccessGrace | 30 | Seconds a Critical service must hold a satisfying state before boot counts as successful. |
Machine\System\Boot\ShutdownTimeout | 90 | Maximum seconds for the whole shutdown sequence. |
Machine\System\Boot\PostKillTimeout | 5 | Seconds a service cgroup may take to drain after SIGKILL before it counts as stuck. |
The kernel command line #
peinit reads four peios.* tokens. They are deliberately few: everything peinit can read after registryd is serving belongs in the registry instead, where it can be inspected, secured and changed without editing a boot entry. What is left is either a per-boot mode decision or a Phase 1 value — one peinit needs before there is a registry to ask.
| Token | Effect |
|---|---|
peios.safemode=1 | Force Safe mode. |
peios.recovery=1 | Force Recovery mode, whatever the counter says. |
peios.bootattempts=N | Override the boot-attempt threshold; 0 disables the check. |
peios.notifysocket=PATH | Move the sd_notify socket. Phase 1, because registryd is the first service to use it and it must be bound before registryd starts. |
peios.quiet=N | How much peinit may write to the console: 0 write everything, 1 (default) stay out of a terminal a service owns, 2 also drop ordinary progress. See below. |
Console noise: peios.quiet #
peinit reports its progress to /dev/console, and so does any service that claims that terminal with TTYPath. Two writers, one device: a login prompt started while peinit is still narrating gets written over, and the reader cannot tell input from log. peios.quiet decides who yields.
It sets two independent rules, which is why it is a level and not a flag:
| Terminal a service owns | Everywhere else | |
|---|---|---|
peios.quiet=0 | peinit writes anyway | everything |
peios.quiet=1 (default) | only messages that mean the machine is about to be lost | everything |
peios.quiet=2 | only messages that mean the machine is about to be lost | errors only |
The rules stack rather than scale: errors are never less visible at 2 than at 1. An error overrides a requested blackout — silence was a preference, and an error is news — but it does not override terminal ownership, which is not peinit's to override. Only losing the machine (dropping to Recovery, halting with no shell, a Critical service about to force a reboot) is worth one corrupted line of somebody else's session.
prelude honours peios.quiet=2 as well, since it writes to the same console. The other two levels mean nothing there — prelude exits before the first service starts, so no terminal has an owner yet.
A few lines escape all of this: whatever peinit and prelude print before they have read the command line. Neither can honour a preference it has not seen yet, and those lines are also the only evidence either started at all.
Unknown peios.* tokens are ignored, as is a malformed value on either of the two valued tokens — this parser runs before anything exists to report a diagnostic to, and refusing to boot over a typo in a tuning knob is the worse outcome.
Where to start #
To understand the graph validation and parallel start that drive Phase 2, read Dependencies and ordering.
To understand the Critical-failure reboot path that feeds the boot counter, read Keeping services running.
For the trust and token side of early boot, read peinit at PID 1.
Shutdown
Peios / Using Peios / Services & jobs
Shutdown is boot run in reverse. peinit stops services in reverse dependency order — dependents before the things they depend on — escalating from a polite SIGTERM to a forced SIGKILL, all bounded by a global timeout, and then unmounts, syncs, and performs the final power action. This page covers how it is triggered and how it runs.
How shutdown is triggered #
There are four paths into shutdown, and they are not equal — three are graceful, one is not.
A control command. An administrator with SYSTEM_SHUTDOWN runs peiosctl shutdown <type>:
| Type | Result |
|---|---|
poweroff | Stop everything, unmount, power off. |
reboot | Stop everything, unmount, reboot. |
halt | Stop everything, unmount, halt (CPU stopped, power stays on). |
Signals. As PID 1, peinit treats a handful of signals as shutdown requests:
| Signal | Meaning |
|---|---|
| SIGINT | Reboot — the kernel sends this on Ctrl-Alt-Del. |
| SIGTERM | Poweroff. |
| SIGPWR | Poweroff — a compatibility path for environments that surface power failure or power-button policy as a signal. |
The power button. On Linux, a physical power-button press is a graceful poweroff — peinit watches the machine's input devices under /dev/input and treats a KEY_POWER press (the key going down, not its release or auto-repeat) as a shutdown request. This path is deliberately minimal and fail-soft: if /dev/input is missing, an input device cannot be opened, or a device stops responding, peinit just keeps running and drops that device — the button quietly stops working, but the machine stays up and you can still shut down over the control socket or with a signal. It is not a full power-management policy engine; a future acpid/logind-style daemon can layer richer policy on top by sending control-socket commands.
A Critical service failure — the ungraceful path. When an ErrorControl=Critical service exhausts its restart budget, peinit syncs and reboots immediately. There is no service-stop ordering: the system is in an undefined state and the fastest route to a known one is a reboot. This is what feeds the boot-attempt counter, and a Critical service that fails identically every boot becomes a reboot loop that the counter eventually breaks by escalating to Recovery.
The graceful sequence #
For a poweroff, reboot, or halt, peinit runs an ordered sequence:
- Enter the shutdown state. A flag is set, and while it holds: no new services may start, timer triggers are disarmed, and new control commands are rejected with
INVALID_STATE— exceptstatusqueries, which keep working so you can watch progress. - Suspend Critical-failure semantics. If a Critical service fails during shutdown, it is logged but does not trigger a reboot — the system is already going down, and rebooting would loop.
- Clear Completed services. Oneshot services sitting in Completed (with
RemainAfterExit) have no process; peinit moves them to Inactive so their dependents can be stopped cleanly. - Stop services in reverse dependency order, in waves. Each graceful-stop-eligible service (those in Active or Reloading) gets SIGTERM and
StopTimeoutseconds to exit; if it does not, peinit SIGKILLs its whole cgroup. A service is never stopped until everything thatRequiresorBindsToit has already stopped. Services that are Starting are not stopped gracefully — their startup is cancelled and the cgroup SIGKILLed. - Enforce the global timeout. The whole sequence is bounded by
ShutdownTimeout(default 90 s). When it expires, all remaining services are SIGKILLed; any whose cgroups do not empty within the post-kill timeout (default 5 s) become Abandoned (leaked), and shutdown continues regardless. - Save the random seed. With every service stopped and before any filesystem is touched, peinit writes a fresh seed drawn from the kernel's CSPRNG to
/var/state/peinit/random-seed— this is the entropy the next boot starts from. The write is crash-conscious (written to a temporary file, flushed, then atomically swapped in), and it is best-effort: if it fails, peinit logs it and carries on. Shutdown never stalls on it, and the forced and Critical-reboot paths skip it entirely in favour of getting the machine down fast. - Unmount filesystems. peinit snapshots the mount table and tries to unmount every remaining non-root mount in its namespace — not just the ones it mounted itself — working from the deepest mount points outward. That includes the Phase 1 set (
/proc,/sys,/dev,/dev/pts,/dev/shm,/run,/sys/fs/cgroup) wherever those are still mounted. A mount that is already gone counts as done. If one won't unmount because it is busy, peinit falls back to remounting it read-only; anything it still can't clean up is logged and left for the finalsync()and the kernel, never blocking shutdown. The root filesystem (/) is never unmounted — once the rest are handled, peinit remounts root read-only. - Sync and finish.
sync()to flush pending writes, then the final action — power off, reboot, or halt.
The reverse ordering falls straight out of the dependency graph; there is no separate stop-order configuration. The last services to stop are the TCB daemons everything rests on — eventd, then authd, then lpsd, then registryd dead last, mirroring its position as the first ever started.
STOPPING=1 and timeout extension #
A service that begins shutting down on its own — say, in response to an internal error — can tell peinit by sending STOPPING=1 via sd_notify. peinit acknowledges it and, importantly, stops sending SIGTERM to that service: it is already on its way down, and a redundant signal could interfere.
STOPPING=1 is a courtesy, not a timeout extension. The StopTimeout keeps running; if the process has not exited when it expires, peinit escalates to SIGKILL regardless. A service that genuinely needs longer must say so with EXTEND_TIMEOUT_USEC (see Keeping services running) — and during shutdown those extensions are capped not only by the per-service StopTimeout × 4 but also by the remaining global ShutdownTimeout, whichever is smaller.
Forced shutdown #
When something is wedged and you need the machine down now, repeated SIGINT forces it: three Ctrl-Alt-Del presses within five seconds skip the graceful sequence entirely — SIGKILL everything, sync, reboot. It is the escape hatch for a graceful shutdown that is itself stuck behind an unresponsive service.
Shutdown during boot #
A shutdown requested while peinit is still in Phase 2 boot takes effect immediately: services still Starting are SIGKILLed (they never reached Active, so there is nothing to stop gracefully), services that did reach Active are stopped per the normal sequence, and the boot is abandoned.
How peinit handles signals #
peinit handles all signals through a signalfd read from its event loop — every signal is blocked and read as data, so there are no async signal handlers and no async-safety hazards. PID 1 cannot be killed by any signal; the kernel protects it.
| Signal | Behaviour |
|---|---|
| SIGCHLD | Reap children; match exits to services and jobs. Also reaps orphaned processes the system reparented to PID 1. |
| SIGINT | Reboot request. Repeated within the window → forced reboot. |
| SIGTERM | Poweroff request. |
| SIGPWR | Poweroff request — compatibility path for power-button/power-failure policy. |
| SIGHUP | Ignored — PID 1 has no controlling terminal. |
| SIGPIPE | Ignored — a broken control-socket pipe must never crash PID 1. |
All other signals are ignored.
Where to start #
To understand the reverse ordering and which relationships gate it, read Dependencies and ordering.
To understand the Critical-failure path that bypasses graceful shutdown, read Keeping services running.
For the boot side of the lifecycle and the reboot/recovery escalation, read Boot and boot modes.
Troubleshooting peinit
Peios / Using Peios / Services & jobs
Almost every peinit problem is diagnosed the same way: read the state and the cause. A status query gives you both, and the cause taxonomy tells you what the cause means. This page works backward from common symptoms to the cause and the fix.
$ peiosctl status <service> # state + cause + health + warnings
$ peiosctl list # everything you can see, at a glance
For history beyond what peinit holds in memory — past jobs, prior failures, the exact log line a service died on — query eventd; peinit emits every job and operation transition there.
A service won't start #
status shows it Failed or Skipped. The cause says why:
| Cause | What happened | What to do |
|---|---|---|
ValidationError | The definition is malformed — a bad field, an unresolvable conflict, an illegal health-check timing. | Check the logs for the specific field. Fix the definition; the message names what is wrong. |
DependencyFailure | A Requires/BindsTo target failed or does not exist. | Fix the target first — status it. The dependent recovers once the target can start. |
ConditionSkipped (→ Skipped) | A start-time condition was not met. Not a failure — the service decided it does not apply here. | If it should run, the condition's premise is false (a missing path/file/key). This is often correct behaviour. |
AssertionError | A start-time assert failed — a required precondition is missing. | Create the missing precondition (path, file, directory, registry key), then start again. |
PreHookFailure | An ExecStartPre hook exited non-zero. | Run the hook command by hand as the hook identity to see why. |
ParentSetupFailure | peinit could not even fork — fd/PID exhaustion, a cgroup error. No process was created. | A system-resource problem, not the service's fault. Check for fd/PID limits and cgroup health. |
PreExecFailure | Setup after fork failed — token install, rlimits, environment. | Usually identity: check Identity, that authd is up, and that RequiredPrivileges names real privileges. |
ReadinessTimeout | The process started but never became ready within StartTimeout. | Either the service is genuinely slow (raise StartTimeout, or have it send EXTEND_TIMEOUT_USEC) or it never sends READY=1 (wrong Readiness, or a bug). |
A service keeps restarting #
The service flaps — up, down, up, down. This is restart policy doing its job, but it points at an unstable service.
- It eventually settles in
FailedwithRestartBudgetExhausted. It crashedRestartMaxRetriestimes faster than it could stay healthy forRestartWindow. The fix is the service, not the policy — read its logs for the crash. Raising the budget only delays the inevitable. - It flaps forever and never exhausts the budget. It is briefly reaching Active each cycle and resetting the counter. Either genuinely stabilise it, or — if a health check is failing it after it starts — check the health command; a flaky check restarts a perfectly good service.
- It restarts on a clean exit you did not expect.
RestartPolicy=Alwaysrestarts even successful exits (causeCleanExitRestart). If the service is meant to exit, useOnFailureinstead.
The machine keeps rebooting #
A reboot loop almost always means a Critical service is failing. A ErrorControl=Critical service that exhausts its budget makes peinit sync and reboot; if it fails the same way every boot, you loop.
The boot-attempt counter is designed to break this: after N attempts (default 3) peinit stops and drops you into Recovery mode. From the Recovery shell, find the failing Critical service (its logs are in eventd, which persists across reboots), fix or disable it, and reboot. If you need to intervene before the counter trips, boot with peios.recovery=1 on the kernel command line.
A service is Abandoned #
Abandoned means peinit SIGKILLed the process but it survived — stuck in uninterruptible kernel sleep (D-state), the signature of hung I/O (a dead NFS mount, a failing disk). Nothing in userspace can kill a D-state process.
- The underlying I/O fault is the real problem — investigate the storage or mount, not peinit.
- The service's cgroup is leaked and shows in
warnings. A later start uses a fresh generational cgroup, so the new instance is unaffected. peiosctl reset <service>clears the Abandoned state and re-checks the cgroup: if the stuck process finally died, peinit cleans up; if not, it stays leaked and warns you. Leaked cgroups clear fully only on reboot.
I changed the config and nothing happened #
peinit works from an in-memory snapshot, and each field has a mutability class that decides when a change applies:
- Immutable at runtime (
ImagePath,Type,Identity,RequiredPrivileges,ErrorControl) → takes effect only onrestart. - Apply on next start (dependencies, conditions,
OnFailure) → next start or graph reload. - Reloadable at runtime (timeouts, restart policy, health checks, environment, hooks…) → next time peinit acts on the service.
- Hot-reloaded (
ServiceSecurity) → next control request.
If a change is not landing, check its class first. For a wholesale re-read of every definition, run peiosctl reload-config — it rebuilds and validates the whole graph atomically and swaps it in only if validation passes (running services are untouched). And remember peinit pulls changes from registry notifications rather than having them pushed, so there can be a brief lag.
ACCESS_DENIED #
A control command returns ACCESS_DENIED. peinit ran AccessCheck on your token against the target's descriptor and the right was not granted:
- A service command needs the matching right in the service's ServiceSecurity descriptor (
startneedsSERVICE_START,restartneeds both stop and start, …). shutdownandreload-configneedSYSTEM_SHUTDOWN/SYSTEM_RELOAD_CONFIGin peinit's control descriptor.- The check uses your effective identity at connect time — if you are impersonating, that is what is checked.
Every denial is logged with the caller SID, target, and requested right. Walk it through Debugging a denial.
A dependent never started #
You started (or booted) a service, but something that depends on it never came up:
- A
Requires/BindsTodependent stays blocked until its target reaches a satisfying state (Active, Completed, or Skipped). If the target is stuck in Starting or Failed, so is the dependent — fix the target. - A dependent that connected to its target on boot and got connection refused usually means the target uses
Readiness=Alive— peinit only waited for the process to exist, not to be serving. Switch the target toReadiness=Notifyso it signalsREADY=1when actually ready. (peinit logs this as a validation warning at boot.) - A
Wantsdependent starts regardless of its target — if you needed it to wait,Wantswas the wrong relationship; useRequires.
Booted into Safe mode #
Safe mode starts only Critical and SafeMode=1 services. You land here when graph validation finds a cycle or unresolvable conflict involving a Critical service, or when peios.safemode=1 is on the kernel command line. The TCB is healthy; the configuration is broken.
Fix the graph: the logs name the cycle path (A → B → C → A) or the conflicting pair. Once the cycle is broken or the conflict resolved, a normal boot returns. You can start any service by hand from Safe mode in the meantime — it is only auto-start that is restricted.
Booted into Recovery mode #
Recovery mode gives you a SYSTEM shell on the console and starts nothing else. You reach it when the boot counter hits N, when peios.recovery=1 is set, or when registryd failed in Phase 1 (entered immediately, no counter).
If the registry itself is the problem, use the offline tools that bypass it — they talk to the loregd implementation directly:
$ loregd --inspector # read the storage directly to diagnose
$ loregd --recover-from-backup # restore the backup taken each registryd start
$ loregd --dangerously-clear-database # last resort — wipe; roles re-supply config
Recovery has no TCB protections — it is a SYSTEM shell, full stop. Treat it accordingly, and remember it needs console access (physical, IPMI, or serial); there is no remote recovery yet.
Where to start #
To read states and causes fluently, keep The service lifecycle handy.
For restart, backoff, and Critical-reboot behaviour, see Keeping services running.
For the commands and their outputs, see Controlling services.
Registry key reference
Peios / Using Peios / Services & jobs
This page is the reference catalog for everything peinit reads from or writes to the registry: the full service-definition schema with types and defaults, and every key peinit touches outside the service definitions. For the meaning of each item, follow the link in its row; this page is for looking up a type, a default, or a path.
Registry value types #
Service definition fields map onto registry value types as follows:
| Schema type | Registry type | Is |
|---|---|---|
| string | REG_SZ | A UTF-8 string. |
| multi_string | REG_MULTI_SZ | An ordered list of strings. |
| dword | REG_DWORD | A 32-bit unsigned integer. |
| binary | REG_BINARY | A raw byte sequence. |
| (timestamp) | REG_QWORD | A 64-bit value — used for timer last-run timestamps. |
The service definition schema #
Every service is a key under Machine\System\Services\<name>; these are the values inside it. Only ImagePath is required. Fields are grouped by purpose; for full semantics see Defining a service and the linked pages.
Execution — see The execution environment
| Field | Type | Default | Meaning |
|---|---|---|---|
ImagePath | string | (required) | Absolute path to the service binary. |
Arguments | multi_string | — | Command-line arguments. |
WorkingDirectory | string | / | Working directory (non-empty absolute path). |
TTYPath | string | — | Absolute path to a terminal to attach as the service's stdio and controlling terminal, e.g. /dev/console or /dev/tty1. Absent or empty means the daemon default (/dev/null stdin, captured output). Attaching a terminal suppresses log capture. |
Environment | multi_string | — | KEY=VALUE pairs added to the environment. |
RuntimeDirectories | multi_string | — | Directories created directly under /run just before the main process starts, each secured for SYSTEM, Administrators, and the service's own SID. |
LimitNOFILE | dword | — | RLIMIT_NOFILE. |
LimitCORE | dword | — | RLIMIT_CORE, in bytes. |
Type and readiness — see Simple and Oneshot services
| Field | Type | Default | Meaning |
|---|---|---|---|
Type | dword | 0 (Simple) | 0 = Simple, 1 = Oneshot. |
Readiness | dword | 0 (Notify) | 0 = Notify (READY=1), 1 = Alive. Ignored for Oneshot. |
RemainAfterExit | dword | 0 | Oneshot only — stay Completed after a successful exit. |
SuccessExitCodes | multi_string | — | Non-zero exit codes treated as success (each 0–255). |
Activation — see Triggers and timers
| Field | Type | Default | Meaning |
|---|---|---|---|
Triggers | multi_string | — | boot and/or timer:<schedule>. Absent = demand-only. |
Disabled | dword | 0 | If 1, triggers must not activate the service. |
SafeMode | dword | 0 | If 1, attempt to start in Safe mode. Critical implies SafeMode. |
Conditions | multi_string | — | Start-time checks; a failure skips the service. |
Asserts | multi_string | — | Start-time checks; a failure fails the service. |
PreStartCheckTimeout | dword | 5 | Seconds allowed for the helper that evaluates filesystem Conditions/Asserts; if it overruns, it is killed and the check counts as not satisfied (a Condition skips, an Assert fails). |
TimerPersistent | dword | 1 | Catch up a missed timer run after a reboot. |
TimerJitter | dword | 0 | Maximum random delay (seconds) added to each firing. |
Identity — see Service identity and privileges
| Field | Type | Default | Meaning |
|---|---|---|---|
Identity | string | LocalService | Principal name or SID for the service token. |
RequiredPrivileges | multi_string | — | Privilege allow-list; all others are removed from the token. |
HookIdentity | string | (service's Identity) | Identity for ExecStartPre/ExecStartPost. |
Dependencies — see Dependencies and ordering
| Field | Type | Default | Meaning |
|---|---|---|---|
Requires | multi_string | — | Hard dependencies. |
Wants | multi_string | — | Soft dependencies. |
BindsTo | multi_string | — | Runtime coupling. |
Conflicts | multi_string | — | Mutual exclusion. |
OnFailure | string | — | Service to start when this one enters Failed. |
Supervision and health — see Keeping services running
| Field | Type | Default | Meaning |
|---|---|---|---|
ErrorControl | dword | 0 (Normal) | 0 = Normal, 1 = Critical (sync + reboot on irrecoverable failure). |
RestartPolicy | dword | 1 (OnFailure) | 0 = Never, 1 = OnFailure, 2 = Always. |
RestartMaxRetries | dword | 5 | Consecutive restarts before Failed. |
RestartWindow | dword | 120 | Seconds of Active health that resets the restart counter. |
RestartDelay | dword | 1 | Backoff base (doubles per failure, capped at 60). |
HealthCheck | string | — | Periodic health-check command. |
HealthCheckInterval | dword | 30 | Seconds between health checks. |
HealthCheckTimeout | dword | 5 | Seconds before a check is killed and failed. |
HealthCheckRetries | dword | 3 | Consecutive failures before unhealthy. |
WatchdogTimeout | dword | 0 | Seconds between expected WATCHDOG=1 pings; 0 disables. |
Transition phases — see The service lifecycle and Controlling services
| Field | Type | Default | Meaning |
|---|---|---|---|
ExecStartPre | multi_string | — | Commands run before the binary (sequential; any failure aborts start). |
ExecStartPost | multi_string | — | Commands run after readiness / successful exit (failure logged, not fatal). |
ExecReload | string | — (SIGHUP) | Reload command, or signal:<NAME>. |
StartTimeout | dword | 30 | Seconds for the whole start sequence. |
StopTimeout | dword | 10 | Seconds between SIGTERM and SIGKILL. |
Notify, fds, security, metadata
| Field | Type | Default | Meaning |
|---|---|---|---|
NotifyAccess | dword | 0 (Main) | Who may send sd_notify (only Main is supported). |
FdStoreMax | dword | 0 | Max fds in the fd store; 0 disables it. |
ServiceSecurity | binary | inherit parent | Descriptor controlling who may manage the service. |
DisplayName | string | — | Human-readable name for status display. |
Description | string | — | Description of the service. |
Service definition keys #
| Key | Type | Purpose | See |
|---|---|---|---|
Machine\System\Services\ | (parent key) | Parent of all service definitions; each child key is a service. | Defining a service |
Machine\System\Services\SchemaVersion | dword | Schema version guard (currently 1). | Defining a service |
Machine\System\Services\<name>\LastTimerRun | REG_QWORD | Last-run timestamp for a single-trigger persistent timer. Written by peinit. | Triggers and timers |
Machine\System\Services\<name>\TimerState\ | (subkey) | Per-trigger last-run timestamps for multi-trigger services. Each value is named by the percent-encoded schedule string and holds a REG_QWORD. | Triggers and timers |
Boot configuration #
Under Machine\System\Boot\:
| Key | Type | Default | Purpose | See |
|---|---|---|---|---|
MaxParallelStarts | dword (> 0) | 10 | Maximum services starting concurrently. Missing → default; zero/invalid → Recovery. | Boot and boot modes |
BootSuccessGrace | dword | 30 | Seconds a Critical service must hold a satisfying state before boot is successful. | Boot and boot modes |
ShutdownTimeout | dword | 90 | Maximum seconds for the whole graceful shutdown. | Shutdown |
SettleTimeout | dword | 5 | Seconds to wait for the boot set to settle before starting boot:settled services anyway. | Triggers and timers |
PostKillTimeout | dword | 5 | Seconds peinit waits for a service cgroup to drain after SIGKILL before treating it as stuck. ShutdownTimeout bounds the shutdown as a whole; this bounds one service's last stage of it. | Shutdown |
Operational parameters #
Under Machine\System\Init\:
| Key | Type | Default | Purpose | See |
|---|---|---|---|---|
ControlSecurity | binary | SYSTEM full; Administrators shutdown + reload-config | Descriptor for system-level control operations. | Who can manage a service |
MaxControlConnections | dword | 32 | Maximum concurrent control-socket connections. | Controlling services |
MaxRequestSize | dword | 65536 | Maximum control-socket request size (bytes). | Controlling services |
ConnectionTimeout | dword | 30 | Seconds before an idle control connection is closed. | Controlling services |
MaxLogLineLength | dword | 8192 | Maximum bytes per service output line before truncation. | Service output and logging |
MaxLogBufferPerService | dword | 65536 | Maximum bytes buffered per service pipe before back-pressure. | Service output and logging |
LogReadBytesPerEvent | dword | 16384 | Bytes drained from a service's output pipe per readable event. Larger favours throughput on chatty services, smaller favours fairness between them. | Service output and logging |
PreEventdBuffer | dword | 1048576 | Total bytes of service output held in memory before eventd is up to receive it. Overflow drops the oldest output; it never blocks a service. | Service output and logging |
EnvVars\ | (parent key) | empty | Default environment variables injected into Phase 2 services (value name = variable, REG_SZ data = value). The descriptor here is security-critical. | The execution environment |
ProvisionedPaths\ | (parent key) | empty | Boot-time path provisioning: each child key names one directory or file that peinit creates and secures after registryd starts and before Phase 2 — the registry-backed equivalent of tmpfiles.d. | Boot and boot modes |
ProvisionedPaths\<name>\Kind | string | (required) | directory or file — what to create at Path. | Boot and boot modes |
ProvisionedPaths\<name>\Path | string | (required) | Absolute path to create or verify. | Boot and boot modes |
ProvisionedPaths\<name>\Security | binary | SYSTEM + Administrators full, users read/traverse | Peios file security descriptor to apply to the object. | Boot and boot modes |
ProvisionedPaths\<name>\Required | dword | 0 | If 1, a failure to provision this entry sends boot to Recovery mode before Phase 2 starts. | Boot and boot modes |
Keys peinit reads from other subsystems #
| Key | Type | Purpose | See |
|---|---|---|---|
Machine\System\eventd\LogSocketPath | string | Path of eventd's log datagram socket, where peinit forwards service output. | Service output and logging |
Where to start #
For how these fields combine into a working definition and when changes take effect, read Defining a service.
To look up the meaning, valid range, and effect-timing of any key on a live system, use regman.