Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

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.

ObjectWhat it isLifetime
ServiceA 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.
JobA 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.
OperationA 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 onFor
KACSService identity (installing tokens on children), control-interface authentication (peer token + AccessCheck), and per-service access control. See Access decisions.
LCS / the registryService definitions, boot configuration, and its own parameters. See The registry.
registrydThe 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.)
authdMinting tokens for non-platform services. peinit requests; authd mints.
eventdService logs (forwarded over a socket) and job/operation/audit events (emitted through KMES).
JFSDelivering 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.

FieldDefaultPurpose
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.

FieldDefaultPurpose
TypeSimpleSimple (long-running) or Oneshot (run-to-completion).
ReadinessNotifyNotify (READY=1 via sd_notify) or Alive (ready when the process exists). Ignored for Oneshot.
RemainAfterExit0Oneshot 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.

FieldDefaultPurpose
Triggers—boot and/or timer:<schedule>. Absent = demand-only.
Disabled0If 1, triggers must not activate the service (manual start still allowed).
SafeMode0If 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.

FieldDefaultPurpose
IdentityLocalServicePrincipal name or SID for the service token.
RequiredPrivileges—Privilege allow-list; everything else is stripped from the token.
HookIdentityservice's IdentityIdentity for ExecStartPre/ExecStartPost hooks.

How it relates to other services — dependencies. See Dependencies and ordering.

FieldPurpose
RequiresHard dependencies — must be satisfied first; their failure fails this service.
WantsSoft dependencies — started first if present, but optional.
BindsToRuntime coupling — if the target stops, this stops too.
ConflictsMutual exclusion — starting this stops the named services.
OnFailureService to start when this one enters Failed.

How it is kept alive — supervision and health. See Keeping services running.

FieldDefaultPurpose
ErrorControlNormalNormal (stay Failed) or Critical (sync + reboot on irrecoverable failure).
RestartPolicyOnFailureNever / OnFailure / Always.
RestartMaxRetries, RestartWindow, RestartDelay5 / 120 / 1Restart budget, the window of health that resets it, and the backoff base.
HealthCheck, HealthCheckInterval, HealthCheckTimeout, HealthCheckRetries— / 30 / 5 / 3Active health-check command and its timing.
WatchdogTimeout0Expected interval between WATCHDOG=1 pings; 0 disables.

The transition phases — hooks and timeouts. See The execution environment and The service lifecycle.

FieldDefaultPurpose
ExecStartPre, ExecStartPost—Commands run before the binary / after readiness.
ExecReload(SIGHUP)Reload command or signal:<NAME>.
StartTimeout, StopTimeout30 / 10Seconds for the whole start sequence / between SIGTERM and SIGKILL.

The remaining knobs — scheduling, notify, fds, metadata.

FieldDefaultPurpose
TimerPersistent, TimerJitter1 / 0Catch up missed timer runs after reboot / random delay per firing.
NotifyAccessMainWho may send sd_notify messages (only Main is supported).
FdStoreMax0Size of the per-service fd store; 0 disables it.
ServiceSecurityinheritSecurity 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.

ClassWhen a change takes effectFields
Immutable at runtimeNext restart only.ImagePath, Type, Identity, RequiredPrivileges, ErrorControl
Apply on next startNext start or explicit graph reload — not while running.Requires, Wants, BindsTo, Conflicts, OnFailure, Conditions, Asserts
Hot-reloadedNext relevant event, no restart.ServiceSecurity (next control request)
Reloadable at runtimeNext 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.

Note

reload-config is the explicit, atomic way to make peinit re-read everything. It builds and validates a complete new graph snapshot and swaps it in only if validation passes; running services are not disturbed. It is covered with the rest of the command set in Controlling services.

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.

Important

Removing a definition is not how you stop a service. It stops future management, not work in progress. To actually stop a running service, stop it. To prevent it from auto-starting, set Disabled=1 (see Triggers and timers). To prevent it from being started at all, deny SERVICE_START in its ServiceSecurity descriptor.

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 #

ReadinessReady 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.
AliveThe 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.

Warning

Using Readiness=Alive on a service that other services Requires is a common configuration smell. peinit will let it boot, but it logs a validation warning: Alive readiness gives no guarantee the service is actually serving before its dependents start. If a dependent connects on boot and gets connection-refused, this is usually why. Prefer Notify wherever the service can signal it.

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:

RemainAfterExitAfter a successful exit
0 (default)The service passes through Completed — releasing its dependents — then transitions to Inactive.
1The 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:

  • StartTimeout covers 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 raise StartTimeout accordingly (or send EXTEND_TIMEOUT_USEC as it works — see Keeping services running).
  • ExecStartPost runs only on success. Post-hooks fire after the successful exit. If the Oneshot fails, ExecStartPost does 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 #

SimpleOneshot
Process lifetimeLong-running; is the serviceRuns once, exits
ReadinessNotify (READY=1) or AliveSuccessful exit — Readiness ignored
Satisfies dependents whenActiveCompleted (with or without RemainAfterExit)
Successful end stateActive (until stopped)Completed, then Inactive (unless RemainAfterExit=1)
ExecStartPost firesAfter readinessAfter successful exit only
StartTimeout coversStartup until readinessThe whole execution
Watchdog & health checksSupervised while ActiveNone — WatchdogTimeout/HealthCheck are inert
Restart on successn/aNever (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.

TriggerFormMeaning
BootbootStart during the Phase 2 boot sequence, in dependency order.
Timertimer:<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.

Note

Reach for this only to keep console output legible. If a service genuinely needs something to be running first, that is a dependency — say so with Requires. A console login uses both: boot:settled for when, and Requires on its authority for what must be true.

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 startDisabled=1
Stop a service from being started at all, even manuallyDeny SERVICE_START in its ServiceSecurity descriptor
Stop a running serviceThe 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.

FieldFormIf omitted
DayOfWeekweekday name(s)any day (*)
DateYear-Month-Day*-*-* (any date)
TimeHour:Minute:Second00:00:00
Secondthe :Second of Time:00
TimezoneIANA zone namesystem-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:

OperatorExampleMatches
Wildcard*any value
List1,15any listed value
RangeMon..Fri, 8..17the inclusive range
Repetition0/150, then every multiple of the step — 0, 15, 30, 45
Range + step8..17/28, 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:

ShortcutEquivalent
minutely*-*-* *:*:00
hourly*-*-* *:00:00
daily*-*-* 00:00:00
weeklyMon *-*-* 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 #

ExpressionMeaning
*-*-* 02:00:00Every day at 2 am (system-local)
Mon *-*-* 00:00:00Every Monday at midnight
*-*-1,15 12:00:001st and 15th of each month at noon
*-*~01 00:00:00Last day of each month at midnight
Mon..Fri *-*-* 09:00:00Weekdays at 9 am
*-*-* *:00/15:00Every 15 minutes
*-*-* 02:00:00 Europe/LondonEvery 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.

TypeCurrent stateAction
OneshotInactive / Completed / FailedStart it (operation source Timer).
OneshotActive / StartingSet a single pending flag — one catch-up run when the current one finishes.
SimpleInactive / FailedStart it.
SimpleActive / StartingNo-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.

Note

The timestamp is written when the timer fires and the start is initiated, not when the run completes. A service that crashes mid-run is not re-triggered on the next boot — the run was attempted, not missed.

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.

EventBehaviour
NTP step or manual clock setArmed 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 / resumeA scheduled time that elapsed while suspended fires once on resume — once, not once per missed occurrence.
A missed occurrence within one uptimeFire 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.

Caution

If a machine boots with a wildly wrong clock (a dead RTC battery) and NTP corrects it only later, the boot-time persistent catch-up decision is made against the wrong time and may fire spuriously or not at all. The runtime half self-heals once NTP steps the clock, but the boot-time decision is already made by then. This is a known edge case short of NTP-aware rescheduling — worth knowing if you run timers on hardware with unreliable clocks.

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 #

StateProcess?Satisfies dependents?What it means when you see it
InactiveNoNoNot started, or stopped cleanly and not set to restart. The neutral resting state.
StartingMaybeNoActivation in progress — conditions, hooks, fork, or readiness wait. Dependents are blocked.
ActiveYesYesRunning and ready. The normal state of a healthy Simple service.
ReloadingYesYesRe-reading configuration. Still counts as running.
StoppingYes (briefly)NoSIGTERM sent; waiting for exit or SIGKILL escalation.
CompletedNoYesOneshot finished successfully. Stays here only with RemainAfterExit=1.
BackoffNoNoA restart is pending; the service is waiting out its backoff delay before the next attempt.
FailedNoNoExited abnormally and restart policy is exhausted or not configured.
AbandonedYes (unkillable)NoSIGKILL was sent but the process survived in uninterruptible sleep (D-state). peinit has given up supervising it; its cgroup is leaked.
SkippedNoYesA 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 → Starting is the retry; Failed is reached only when restarts are exhausted or disabled. This is why OnFailure fires once at the end, not on every retry.
  • Starting can fail before any process exists — a parent-side setup error, a failed pre-hook, a condition or assert. The cause records which.
  • Failed → Starting is how a manual start (or a recovery path) revives a dead service; automatic restarts never originate from Failed.
  • Stopping has two exits. A clean stop or a shutdown lands in Inactive. But a service stopped because it lost a Conflicts race (ConflictEviction) or because its BindsTo target went away (BindsToPropagation) comes to rest in Failed, carrying that cause — so an evicted or bound-out service shows as Failed in status and needs a reset (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.

Note

A crash while Reloading is still a crash. If a service's main process dies while it is re-reading its configuration, peinit treats that as an ordinary ProcessCrash and routes it through the restart policy — Reloading → Backoff if a restart is warranted, or Reloading → Failed if it is not. This is why you can see a service jump straight from Reloading into Backoff or Failed. It is distinct from an ExecReload command failing: a failed reload command leaves the running process untouched and the service stays Active, reporting the failure without changing state.

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

CauseMeaning
ExplicitStartAn administrator or a trigger started it.
DependencyStartStarted to satisfy another service's dependency.
RestartPolicyAn automatic restart, after a backoff delay.
BindsToRecoveryA bound target returned to Active; the dependent is auto-restarted. Does not consume the restart budget.

Why a service stopped

CauseMeaning
ExplicitStopAn administrator requested stop.
ConflictEvictionA conflicting service started and won.
BindsToPropagationA bound target stopped, so this one stops too.
ShutdownWaveThe system is shutting down.

Why a service failed or restarted — the diagnostic causes

CauseMeaning
ProcessCrashThe main process exited non-zero or died on a signal.
CleanExitA Simple process exited successfully, and policy is not Always — not a crash; goes straight to Inactive.
CleanExitRestartA Simple process exited successfully but RestartPolicy=Always, so it is restarted. Logged clearly as not a crash.
ReadinessTimeoutStartTimeout expired before the service became ready.
WatchdogTimeoutA WATCHDOG=1 keepalive did not arrive in time.
HealthCheckFailureHealthCheckRetries consecutive health checks failed.
PreHookFailureAn ExecStartPre hook exited non-zero.
ParentSetupFailurepeinit could not even fork — resource exhaustion, cgroup error. No child was created.
PreExecFailureSetup after fork but before exec failed (token install, rlimits, environment).
RestartBudgetExhaustedRestartMaxRetries reached within the window; no more retries.

Why a service was rejected — definition and graph problems

CauseMeaning
ValidationErrorThe definition failed validation (bad field, unresolvable conflict, illegal health-check timing…).
CycleDetectedThe service is part of a dependency cycle.
DependencyFailureA Requires dependency entered Failed or does not exist.
AssertionErrorA start-time Assert failed.
ConditionSkippedA start-time Condition was not met → Skipped (this is not a failure).
ProcessUnkillableThe process survived SIGKILL (D-state) → Abandoned.
ExplicitResetAn 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.

Tip

When a service is Failed, the cause is the first thing to read. A ProcessCrash or WatchdogTimeout points at the service's own code or health; a DependencyFailure points at something it needs; a ValidationError points at its definition. Troubleshooting peinit is organised around exactly this lookup.

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:

  1. A service is in exactly one state at any moment.
  2. Only the transitions peinit defines are valid — there are no others.
  3. Only peinit changes a service's runtime state. No external process can reach in and set it.
  4. A service's securable identity (its ServiceSecurity descriptor — who can manage it) is independent of its process identity (its token — what it can access).
  5. Readiness is per start generation — a READY=1 from 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:

PolicyValueBehaviour
Never0Never restart. The service goes straight to Failed.
OnFailure (default)1Restart on a crash or runtime failure. An exit code in SuccessExitCodes is not a failure and is not restarted.
Always2Restart 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"]

Tip

If a service flaps — restarting endlessly without ever sticking — and never hits its budget, the usual cause is that it briefly reaches Active each time and resets the counter. Either it is recovering long enough to reset (raise RestartWindow) or you are looking at a health-check flap, which has its own guardrail below.

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 Never or OnFailure, the cause is CleanExit and the service goes straight to Inactive, with no restart-policy consultation at all.
  • Under Always, the cause is CleanExitRestart and 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.

FieldDefaultControls
HealthCheck—The command to run (runs under the service's own token).
HealthCheckInterval30Seconds between runs.
HealthCheckTimeout5Seconds before a check is killed and counted as failed.
HealthCheckRetries3Consecutive 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.

Caution

Be extremely cautious with health checks on ErrorControl=Critical services. A false-positive health-check failure on a Critical service will, after exhausting the restart budget, reboot the machine — and a flaky check can do this repeatedly. Critical platform services are better served by a passive watchdog with a generous timeout: the service itself knows whether it is healthy and simply stops sending keepalives if not. peinit applies the same health-check semantics to Critical services as to any other — there is no special-casing to save you.

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:

PhaseBase timeoutMaximum deadline
StartingStartTimeoutStartTimeout × 4
StoppingStopTimeoutStopTimeout × 4
ReloadingStartTimeoutStartTimeout × 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:

LevelValueOn irrecoverable failure
Normal (default)0The service stays in Failed. The rest of the system carries on.
Critical1peinit 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 the OnFailure handler.
  • ErrorControl=Critical implies SafeMode. A Critical service is always eligible to start in Safe mode.
  • Critical services are OOM-protected. peinit sets their oom_score_adj to -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.

Warning

A Critical service that fails the same way on every boot becomes a reboot loop, which the boot-attempt counter is designed to break by escalating to Recovery mode after a few attempts. If you see a machine rebooting repeatedly, a crashing Critical service is the first suspect. See Troubleshooting peinit.

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 with DependencyFailure and 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 (carrying BindsToPropagation), not Inactive — so a status query 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 you reset (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 (cause ConflictEviction) before A starts — and vice versa. If the loser will not stop within its StopTimeout, SIGKILL escalation applies. The evicted loser does not land in Inactive: it comes to rest in Failed (carrying ConflictEviction), so it shows as Failed in status and needs a reset before 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 orderTarget failure stops dependent?Target stop stops dependent?
Requirestarget first; dependent fails if target failsAt start time onlyNo
Wantstarget first if present; dependent starts regardlessNoNo
BindsTotarget first; dependent fails if target failsYes (and auto-recovers)Yes (and auto-recovers)
Conflictsstarting one evicts the othern/an/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:

RelationshipMissing or disabled target
RequiresDependent marked Failed (DependencyFailure).
BindsToTreated as a missing Requires — Failed (DependencyFailure).
WantsSilently ignored — the entry is dropped.
ConflictsSilently 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 keyDefaultMeaning
Machine\System\Boot\MaxParallelStarts10Maximum 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.

Note

Boot order is emergent, not hardcoded. The typical sequence — eudev, lpsd, authd, eventd, networking, then application and login services — is simply what the standard role definitions' dependencies produce. Change the dependencies and you get a different order. The platform daemons come up first because everything else, directly or transitively, requires them.

Failure propagation #

When a service enters Failed during graph execution, the failure spreads along Requires and BindsTo edges — and only those:

  1. Every service that Requires the failed one transitions to Failed with DependencyFailure.
  2. Every service that merely Wants it is unaffected and starts normally.
  3. 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:

  1. Collect all transitive Requires and BindsTo dependencies (and best-effort Wants).
  2. Validate that sub-graph (cycles, missing targets).
  3. Resolve Conflicts — stop anything that conflicts.
  4. 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 valueToken source
SYSTEMMinted by peinit from its own SYSTEM identity.
Any other principal name or SIDRequested from authd.
Absent or emptyauthd, 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.

Note

There is no allow-list restricting which services may be SYSTEM. The security boundary is the registry key descriptor on Machine\System\Services\ — an administrator who can create service definitions is already trusted to assign any identity, so a second gate would add nothing. The dangerous case is never implicit, though: SYSTEM must be written out explicitly; an empty Identity defaults to the minimal LocalService, never to SYSTEM.

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.

Important

Every non-SYSTEM service start depends on authd. peinit interacts with it over a non-blocking, timed channel — if authd is unreachable or unresponsive, the service start fails rather than hanging PID 1. This is why a broken authd takes down all non-platform service starts but leaves the SYSTEM-minted platform services unaffected. The authd wire interface is owned by authd, not by peinit, and is still being specified.

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 RequiredPrivileges allow-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 RequiredPrivileges is 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:

ContextRuns as
Main processThe service's Identity.
ExecStartPre / ExecStartPostHookIdentity if set, otherwise the service's Identity.
Health checksThe service's Identity, always.
ExecReload (command form)The service's Identity, always — HookIdentity does not apply.
Ad-hoc jobsThe 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.

Warning

peinit does not validate filesystem permissions on hook binaries. If HookIdentity grants elevated privileges, it is the administrator's responsibility to ensure the hook binary and every parent directory are not writable by a lower-privileged identity — otherwise a less-trusted principal could replace the hook and run code as the elevated identity. (FACS enforces KACS descriptors on managed filesystems, but peinit itself performs no such check — protecting hook binaries still depends on correct SDs from packaging and controlled paths.)

Security invariants #

The identity model rests on a few rules peinit never breaks:

  1. peinit never shares its SYSTEM token. Even Identity=SYSTEM services get a separately minted token.
  2. 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.
  3. Privileges are subtractive only. RequiredPrivileges removes; it can never add.
  4. Identity is deterministic. Every service runs as a known principal. SYSTEM must be declared explicitly; an empty Identity is the minimal LocalService, 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.

Warning

Attaching a terminal suppresses log capture — the pipes eventd would read are closed, so the service's output goes to the terminal and nowhere else. Don't set TTYPath on a service whose output you expect to find in the logs.

Two services pointed at the same terminal will fight over it: both read the same input, and neither is told about the other. peinit does not currently detect this. Until it does, treat "one service per terminal" as a rule you enforce yourself, or declare a Conflicts between them.

The environment #

peinit constructs each service's environment in layers, lowest precedence first — it does not pass through its own near-empty startup environment.

  1. Compiled-in base. A fixed floor peinit always provides — currently just PATH: /bin.
  2. Global EnvVars. Each value under Machine\System\Init\EnvVars\ becomes a variable (value name → variable name). EnvVars\PATH overrides the base PATH; other names add. Every service gets this layer except registryd, which serves the key itself — see the warning below for why that exemption exists.
  3. Per-service Environment. The definition's own KEY=VALUE entries, which override layers 1–2.
  4. Protocol variables. NOTIFY_SOCKET (always) and LISTEN_FDS / LISTEN_FDNAMES (only when stored fds are injected). These have the highest precedence and cannot be overridden — a service that clobbered NOTIFY_SOCKET would 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.

Warning

Write access to Machine\System\Init\EnvVars\ is equivalent to compromising every service peinit starts — a principal who can set it can inject LD_PRELOAD, LD_LIBRARY_PATH, and the like into all of them. peinit deliberately does not filter variable names; the key's security descriptor is the control boundary. Keep it tight — the recommended default is SYSTEM full, Administrators read-only.

This is also why registryd is the one service that never receives this layer: it is the daemon that serves the key. Letting the key inject into the process that enforces who may write it would make the control boundary self-referential. Every other service, platform daemons included, gets the ordinary layering.

Working directory and limits #

FieldEffect
WorkingDirectoryThe process's working directory. Must be a non-empty absolute path; defaults to /.
LimitNOFILERLIMIT_NOFILE — the maximum number of open file descriptors.
LimitCORERLIMIT_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.

  • ExecStartPre runs before the main binary. Each command runs in sequence, in the hooks/ sub-cgroup; any hook exiting non-zero aborts the start (the whole cgroup is killed and the service Fails with PreHookFailure). Use it for prerequisites that must hold before the service starts — creating a runtime directory, waiting on a precondition.
  • ExecStartPost runs 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:

TypeFormPasses when
pathpath:<path>the path exists (any type)
filefile:<path>a regular file exists
directorydirectory:<path>a directory exists
registryregistry:<key>the registry key exists

Two constraints follow from peinit being single-threaded PID 1, which must never block:

  • registry: checks are cache-only. A registry: check may only name a key peinit already caches (under Machine\System\Services\ or Machine\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 a stat() on the event loop, because stat() 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=1 with the fd attached (and optionally FDNAME=<name>; the default name is stored). peinit holds it.
  • It removes one with FDSTOREREMOVE=1 and FDNAME=<name>.
  • On the next start, peinit injects the stored fds starting at fd 3, sets LISTEN_FDS to the count and LISTEN_FDNAMES to the colon-separated names, then clears the store. (This is the same LISTEN_FDS convention 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:

{"command": "start", "service": "jellyfin", "wait": true}
{"status": "ok", "operation_id": "a1b2c3d4-...", "service": "jellyfin", "state": "active", "cause": "explicit_start", "warnings": []}

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/reset returns an operation_id — a GUID you can poll. Conflict resolution between concurrent commands happens at the operation layer, which is why two simultaneous starts merge instead of colliding.

Service commands #

CommandDoesRequired right
startRun the service through its full start sequence.SERVICE_START
stopSIGTERM, then SIGKILL after StopTimeout.SERVICE_STOP
restartStop then start, as one operation.SERVICE_STOP + SERVICE_START
reloadRe-read configuration (ExecReload, or SIGHUP).SERVICE_INTERROGATE
resetClear Failed/Abandoned/Skipped → Inactive.SERVICE_STOP
statusReport state, cause, PID, uptime, health, current job and operation, warnings.SERVICE_QUERY_STATUS
listList 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 #

CommandDoesRequired right
shutdown <type>Graceful shutdown. type is poweroff, reboot, or halt.SYSTEM_SHUTDOWN
reload-configRe-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.

CommandDefaultWaits for
startwaitActive (Simple) / Completed or Inactive (Oneshot), or Failed
stopwaitInactive
restartwaitthe successful start target, or Failed
reloadno-wait(with --wait) the Reloading state to resolve
resetimmediate—

reload is the exception — it returns immediately by default, because a reload may have no observable completion. With --wait, the response carries a mode:

modeMeaning
confirmedThe service signalled READY=1 (and, for a command reload, the command exited 0).
advisoryThe reload was issued and the detection window elapsed without explicit confirmation.
failedThe 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.

Note

A connection blocked on a --wait operation is not counted as idle, so it is not closed by ConnectionTimeout. It stays open until the operation resolves, bounded by the operation's own timeout (e.g. StartTimeout).

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:

CommandInactiveStartingActiveReloadingStoppingCompletedBackoffFailedAbandonedSkipped
startStartMERGEALREADYALREADYQUEUEStartDEFERStartERRORStart
stopNOOPCancel+StopStopStopMERGEClearCancelNOOPERRORNOOP
restartStartQUEUERestartRestartQUEUEStartRestartStartERRORStart
reloadERRORERRORReloadMERGEERRORERRORERRORERRORERRORERROR
resetNOOPERRORERRORERRORERRORERRORERRORClearClearClear
statusOKOKOKOKOKOKOKOKOKOK

Legend:

  • MERGE — an operation of this type is already running; your command merges into it and you get its GUID.
  • DEFER — your start is 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:

{
    "status": "ok",
    "service": "jellyfin",
    "state": "active",
    "cause": "explicit_start",
    "status_text": "Listening on port 8096",
    "current_job": {"id": "a1b2...", "type": "service_main", "pid": 1234, "started_at": "...", "identity": "jellyfin-svc"},
    "current_operation": {"id": "e5f6...", "type": "start", "source": "admin"},
    "health": "healthy",
    "uptime_seconds": 86400,
    "definition_removed": false,
    "warnings": []
}
  • state and cause are the lifecycle pair — read them together.
  • status_text is the latest STATUS= string the service sent via sd_notify (null if never sent; cleared on each restart).
  • current_job and current_operation are the job and operation GUIDs, or null.
  • health is healthy, unhealthy, unknown, or null (no health check).
  • definition_removed is true when the definition was deleted but an instance is still draining.
  • warnings lists 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:

{"status": "ok", "services": [
    {"service": "jellyfin", "state": "active", "cause": "explicit_start", "health": "healthy"},
    {"service": "registryd", "state": "active", "cause": "dependency_start", "health": null}
]}

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:

CodeMeaning
ACCESS_DENIEDAccessCheck denied the command against the target descriptor.
UNKNOWN_SERVICENo such service definition (also returned for start/restart/reload on a definition-removed service).
UNKNOWN_OPERATIONNo such operation GUID — never existed, or dropped after its retention grace.
MALFORMED_REQUESTThe request line is not a single valid JSON object.
REQUEST_TOO_LARGEThe request exceeds MaxRequestSize.
INVALID_COMMANDThe command field is missing or unknown.
INVALID_ARGUMENTSA required field is missing or malformed (e.g. shutdown with no valid type).
INVALID_STATENot valid for the service's current state (an ERROR cell above), or rejected because the system is already shutting down.
OPERATION_TIMEOUTA --wait operation did not reach a terminal state within its timeout.
INTERNAL_ERRORpeinit 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\:

KeyDefaultLimits
MaxControlConnections32Concurrent client connections (excess are refused at connect).
MaxRequestSize65536Bytes per request.
ConnectionTimeout30Seconds an idle connection may sit before it is closed.

Exit status #

CodeMeaning
0The command succeeded.
1A usage error.
non-zeroThe 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:

DescriptorQuestionStoredEnforced by
Registry key SDWho can read or edit the definition?On the Machine\System\Services\<name> keyLCS, at key-open time
ServiceSecurity SDWho can manage the running service?As the ServiceSecurity binary value on that keypeinit, 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:

RightBitGrants
SERVICE_QUERY_STATUS0x0001Query state, PID, cause, health, warnings.
SERVICE_START0x0002Start the service.
SERVICE_STOP0x0004Stop the service.
SERVICE_INTERROGATE0x0008Reload the service.
SERVICE_ALL_ACCESS0x000FAll 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:

GenericMaps to
GENERIC_READSERVICE_QUERY_STATUS
GENERIC_WRITESERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE
GENERIC_EXECUTESERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE
GENERIC_ALLSERVICE_ALL_ACCESS

How a command is authorised #

Every control command runs the same gate:

  1. 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.
  2. peinit resolves the target service and its ServiceSecurity descriptor.
  3. peinit runs AccessCheck: the caller's token against the descriptor, for the right the command needs.
  4. Denied → return ACCESS_DENIED and log the attempt (caller SID, target service, requested right).
  5. Granted → execute the command.

Note

Access denials are always logged — caller, target, and the right requested. Silent denial is a specification violation. If you are debugging a denial, the audit record has everything you need; see Debugging a denial.

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:

RightBitGrants
SYSTEM_SHUTDOWN0x0001Initiate poweroff, reboot, or halt.
SYSTEM_RELOAD_CONFIG0x0002Re-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:

GenericMaps to
GENERIC_READ(nothing)
GENERIC_WRITESYSTEM_RELOAD_CONFIG
GENERIC_EXECUTESYSTEM_SHUTDOWN
GENERIC_ALLSYSTEM_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. status is per-service access-controlled; list filters.

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:

TypeCreated for
ServiceMainA service's main process.
PreExecHookOne ExecStartPre command.
PostExecHookOne ExecStartPost command.
HealthCheckOne health-check run.
AdHocA process submitted via JFS.

Their lifecycle is simpler than a service's, because a job only tracks a process — not policy:

StateMeaning
CreatedThe job object exists but the process is not forked yet (pre-hooks may be running).
RunningThe process is alive.
CompletedThe process exited successfully.
FailedThe process failed — or peinit classified the job failed before fork (a parent-side setup error).
AbandonedThe 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.

Important

An ad-hoc job runs with the submitter's own (possibly impersonated) token — there is no identity field in an ad-hoc request. A caller cannot name an arbitrary identity; it can only pass through a token it already holds. This is what prevents escalation: a service cannot manufacture a job running as an identity it does not already possess.

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.

Note

JFS is a kernel subsystem with its own byte-level interface, still being specified. peinit is its consumer; the shape of the protocol is settled, but the wire details are owned by JFS, so this path is not fully implementable from the peinit specification alone.

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:

SourceCreated by
AdminA control client.
BootThe Phase 2 boot lifecycle.
ShutdownThe shutdown lifecycle.
DependencyPropagationA start pulling in a dependency.
RestartPolicyAn automatic restart.
TimerA timer firing.
BindsToRecoveryA bound target returning to Active.
BindsToPropagationA bound target stopping.
ConflictResolutionA Conflicts eviction.
OnFailureA 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
StateMeaning
PendingValidated and queued, waiting on a precondition (e.g. a prior stop to finish).
RunningExecuting — the service is transitioning.
CompletedAchieved its goal (start reached Active/Completed; stop reached Inactive/Failed).
FailedDid not achieve its goal — including timing out while still Pending.
MergedFolded into an identical operation already in flight (records the survivor's GUID).
CancelledTerminated while still Pending — never ran.
AbortedTerminated 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_job and current_operation give the GUIDs of the service's active execution and active action (or null).
  • operation-status <id> — polls one operation to a terminal state; a --wait lifecycle 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 logsAudit events
Whatstdout/stderr from services and hooksAccess denials, critical failures, recovery-mode entry, graph errors, job/operation lifecycle
PathForwarded to eventd's log socketEmitted into the KMES kernel ring buffer
GuaranteeBest-effort — may be dropped under loadDurable — survive eventd restarts and reboots
Available fromOnce 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\)DefaultLimits
MaxLogLineLength8192Bytes per line; longer lines are truncated with a [truncated] marker.
MaxLogBufferPerService65536Bytes 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.

Note

Signals are never starved. In every loop iteration, child reaping (SIGCHLD) and shutdown handling take priority over all other event sources, including log reading. A flood of service output can never delay peinit noticing that a service died or that the system is shutting down.

The eventd handoff #

When eventd reaches Active, peinit switches from buffering to forwarding:

  1. It begins sending to eventd's log datagram socket (path from Machine\System\eventd\LogSocketPath).
  2. 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.
  3. It switches to real-time forwarding — new output goes out as it arrives.
  4. 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, /dev are 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:

  1. 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.
  2. Mount the remaining virtual filesystems — /dev/pts, /dev/shm, /run, /sys/fs/cgroup — mounting each only if absent. A failure here → Recovery.
  3. 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.
  4. 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.
  5. Set the clock from the hardware RTC, so early timestamps and the boot counter are meaningful. A failure here → Recovery.
  6. 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.
  7. 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 marked Required=1 that cannot be provisioned sends boot → Recovery. The individual keys are cataloged in the registry key reference.
  8. 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.

Caution

Packaged images, VM templates, and live ISOs must not ship a populated /lcl/etc/machine-id or a /var/state/peinit/random-seed file. A shipped machine-id gives every clone the same identity, and a shipped seed is a public value that is not acceptable entropy. Clone and reset tooling should remove or truncate /lcl/etc/machine-id so peinit generates a fresh ID on the next boot, and should never bake a seed into the image — if you need strong first-boot randomness for a stateless image, provide a real kernel entropy source (hardware RNG or virtio-rng) instead.

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:

  1. Read all definitions under Machine\System\Services\. (A registry read timing out here → Recovery.)
  2. Build and validate the dependency graph from the boot-triggered services and their transitive dependency closure. Validation runs before anything starts.
  3. 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:

ServicePhaseIdentity sourceReadinessErrorControl
registryd1minted by peinitsd_notifyCritical
eudev2minted by peinit (privileges stripped)process aliveNormal
lpsd2minted by peinitsd_notifyCritical
authd2minted by peinitsd_notifyCritical
eventd2minted by peinitsd_notifyCritical
networking2authdsd_notifyNormal
sshd2authdprocess aliveNormal
application services2authdper-serviceNormal

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."

Note

SafeMode=1 and ErrorControl=Critical are filters within the boot-triggered set. They do not make a demand-only service auto-start in Safe mode — a service with no boot trigger stays demand-only regardless.

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:

ToolDoes
loregd --inspectorRead the storage database directly for diagnosis.
loregd --recover-from-backupRestore from an automatic backup taken on every registryd startup.
loregd --dangerously-clear-databaseWipe the registry entirely. Recoverable, because role definitions are the source of truth for service config.

Important

Recovery mode requires console access — physical, IPMI, or serial. There is no remote recovery in the current design (emergency SSH and registry historical reversion are noted as post-v1 work). Plan console access for any machine you need to be able to recover.

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 with peios.bootattempts=N on the kernel command line, or set it to 0 to 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 #

KeyDefaultControls
Machine\System\Boot\MaxParallelStarts10Services starting concurrently (must be > 0; invalid → Recovery).
Machine\System\Boot\BootSuccessGrace30Seconds a Critical service must hold a satisfying state before boot counts as successful.
Machine\System\Boot\ShutdownTimeout90Maximum seconds for the whole shutdown sequence.
Machine\System\Boot\PostKillTimeout5Seconds 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.

TokenEffect
peios.safemode=1Force Safe mode.
peios.recovery=1Force Recovery mode, whatever the counter says.
peios.bootattempts=NOverride the boot-attempt threshold; 0 disables the check.
peios.notifysocket=PATHMove 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=NHow 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 ownsEverywhere else
peios.quiet=0peinit writes anywayeverything
peios.quiet=1 (default)only messages that mean the machine is about to be losteverything
peios.quiet=2only messages that mean the machine is about to be losterrors 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.

Warning

Suppressed messages are dropped, not stored. Until peinit can forward them to eventd, peios.quiet=1 means you lose peinit's narrative from the moment a login prompt appears. On a machine you are debugging, boot with peios.quiet=0.

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.

Note

There is no token that selects services. Which services start is decided entirely by what is defined under Machine\System\Services — see Defining a service. A console shell or a login prompt is an ordinary service definition with a TTYPath, not a boot flag.

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>:

TypeResult
poweroffStop everything, unmount, power off.
rebootStop everything, unmount, reboot.
haltStop everything, unmount, halt (CPU stopped, power stays on).

Signals. As PID 1, peinit treats a handful of signals as shutdown requests:

SignalMeaning
SIGINTReboot — the kernel sends this on Ctrl-Alt-Del.
SIGTERMPoweroff.
SIGPWRPoweroff — 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:

  1. 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 — except status queries, which keep working so you can watch progress.
  2. 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.
  3. 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.
  4. Stop services in reverse dependency order, in waves. Each graceful-stop-eligible service (those in Active or Reloading) gets SIGTERM and StopTimeout seconds to exit; if it does not, peinit SIGKILLs its whole cgroup. A service is never stopped until everything that Requires or BindsTo it has already stopped. Services that are Starting are not stopped gracefully — their startup is cancelled and the cgroup SIGKILLed.
  5. 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.
  6. 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.
  7. 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 final sync() and the kernel, never blocking shutdown. The root filesystem (/) is never unmounted — once the rest are handled, peinit remounts root read-only.
  8. 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.

SignalBehaviour
SIGCHLDReap children; match exits to services and jobs. Also reaps orphaned processes the system reparented to PID 1.
SIGINTReboot request. Repeated within the window → forced reboot.
SIGTERMPoweroff request.
SIGPWRPoweroff request — compatibility path for power-button/power-failure policy.
SIGHUPIgnored — PID 1 has no controlling terminal.
SIGPIPEIgnored — a broken control-socket pipe must never crash PID 1.

All other signals are ignored.

Note

Reaping orphans matters: when any process anywhere on the system dies leaving children, those children are reparented to PID 1. peinit reaps them so they do not accumulate as zombies — even though they were never its services. This is part of the job of being PID 1, distinct from supervising services.

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:

CauseWhat happenedWhat to do
ValidationErrorThe 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.
DependencyFailureA 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.
AssertionErrorA start-time assert failed — a required precondition is missing.Create the missing precondition (path, file, directory, registry key), then start again.
PreHookFailureAn ExecStartPre hook exited non-zero.Run the hook command by hand as the hook identity to see why.
ParentSetupFailurepeinit 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.
PreExecFailureSetup after fork failed — token install, rlimits, environment.Usually identity: check Identity, that authd is up, and that RequiredPrivileges names real privileges.
ReadinessTimeoutThe 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).

Tip

A startup failure (ReadinessTimeout, PreHookFailure, PreExecFailure, ParentSetupFailure) is restart-eligible — peinit will have retried it through Backoff before landing in Failed. So by the time you see Failed, it has already failed repeatedly. The logs show each attempt.

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 Failed with RestartBudgetExhausted. It crashed RestartMaxRetries times faster than it could stay healthy for RestartWindow. 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=Always restarts even successful exits (cause CleanExitRestart). If the service is meant to exit, use OnFailure instead.

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 on restart.
  • 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 (start needs SERVICE_START, restart needs both stop and start, …).
  • shutdown and reload-config need SYSTEM_SHUTDOWN / SYSTEM_RELOAD_CONFIG in 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.

Note

If list shows fewer services than you expect, that is not a bug — list omits services you lack SERVICE_QUERY_STATUS on rather than denying them. You are seeing exactly what your token can see.

A dependent never started #

You started (or booted) a service, but something that depends on it never came up:

  • A Requires/BindsTo dependent 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 to Readiness=Notify so it signals READY=1 when actually ready. (peinit logs this as a validation warning at boot.)
  • A Wants dependent starts regardless of its target — if you needed it to wait, Wants was the wrong relationship; use Requires.

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.

Note

The registry stores a value's type but not its meaning — that is what regman and these docs are for. A value peinit accepts at write time can still be rejected when peinit reads and validates it; this page lists the schema, not the validation rules (those live on each field's page).

Registry value types #

Service definition fields map onto registry value types as follows:

Schema typeRegistry typeIs
stringREG_SZA UTF-8 string.
multi_stringREG_MULTI_SZAn ordered list of strings.
dwordREG_DWORDA 32-bit unsigned integer.
binaryREG_BINARYA raw byte sequence.
(timestamp)REG_QWORDA 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

FieldTypeDefaultMeaning
ImagePathstring(required)Absolute path to the service binary.
Argumentsmulti_string—Command-line arguments.
WorkingDirectorystring/Working directory (non-empty absolute path).
TTYPathstring—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.
Environmentmulti_string—KEY=VALUE pairs added to the environment.
RuntimeDirectoriesmulti_string—Directories created directly under /run just before the main process starts, each secured for SYSTEM, Administrators, and the service's own SID.
LimitNOFILEdword—RLIMIT_NOFILE.
LimitCOREdword—RLIMIT_CORE, in bytes.

Note

Each RuntimeDirectories entry is a plain relative name — just foo, never a path — and must not contain /, \, ., .., NUL, or control characters. For an entry foo, peinit creates /run/foo immediately before launching the main process and applies a Peios file security descriptor granting full access to SYSTEM, Administrators, and this service's own SID. If the directory can't be created or the 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 that the next boot clears for you.

Type and readiness — see Simple and Oneshot services

FieldTypeDefaultMeaning
Typedword0 (Simple)0 = Simple, 1 = Oneshot.
Readinessdword0 (Notify)0 = Notify (READY=1), 1 = Alive. Ignored for Oneshot.
RemainAfterExitdword0Oneshot only — stay Completed after a successful exit.
SuccessExitCodesmulti_string—Non-zero exit codes treated as success (each 0–255).

Activation — see Triggers and timers

FieldTypeDefaultMeaning
Triggersmulti_string—boot and/or timer:<schedule>. Absent = demand-only.
Disableddword0If 1, triggers must not activate the service.
SafeModedword0If 1, attempt to start in Safe mode. Critical implies SafeMode.
Conditionsmulti_string—Start-time checks; a failure skips the service.
Assertsmulti_string—Start-time checks; a failure fails the service.
PreStartCheckTimeoutdword5Seconds 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).
TimerPersistentdword1Catch up a missed timer run after a reboot.
TimerJitterdword0Maximum random delay (seconds) added to each firing.

Identity — see Service identity and privileges

FieldTypeDefaultMeaning
IdentitystringLocalServicePrincipal name or SID for the service token.
RequiredPrivilegesmulti_string—Privilege allow-list; all others are removed from the token.
HookIdentitystring(service's Identity)Identity for ExecStartPre/ExecStartPost.

Dependencies — see Dependencies and ordering

FieldTypeDefaultMeaning
Requiresmulti_string—Hard dependencies.
Wantsmulti_string—Soft dependencies.
BindsTomulti_string—Runtime coupling.
Conflictsmulti_string—Mutual exclusion.
OnFailurestring—Service to start when this one enters Failed.

Supervision and health — see Keeping services running

FieldTypeDefaultMeaning
ErrorControldword0 (Normal)0 = Normal, 1 = Critical (sync + reboot on irrecoverable failure).
RestartPolicydword1 (OnFailure)0 = Never, 1 = OnFailure, 2 = Always.
RestartMaxRetriesdword5Consecutive restarts before Failed.
RestartWindowdword120Seconds of Active health that resets the restart counter.
RestartDelaydword1Backoff base (doubles per failure, capped at 60).
HealthCheckstring—Periodic health-check command.
HealthCheckIntervaldword30Seconds between health checks.
HealthCheckTimeoutdword5Seconds before a check is killed and failed.
HealthCheckRetriesdword3Consecutive failures before unhealthy.
WatchdogTimeoutdword0Seconds between expected WATCHDOG=1 pings; 0 disables.

Transition phases — see The service lifecycle and Controlling services

FieldTypeDefaultMeaning
ExecStartPremulti_string—Commands run before the binary (sequential; any failure aborts start).
ExecStartPostmulti_string—Commands run after readiness / successful exit (failure logged, not fatal).
ExecReloadstring— (SIGHUP)Reload command, or signal:<NAME>.
StartTimeoutdword30Seconds for the whole start sequence.
StopTimeoutdword10Seconds between SIGTERM and SIGKILL.

Notify, fds, security, metadata

FieldTypeDefaultMeaning
NotifyAccessdword0 (Main)Who may send sd_notify (only Main is supported).
FdStoreMaxdword0Max fds in the fd store; 0 disables it.
ServiceSecuritybinaryinherit parentDescriptor controlling who may manage the service.
DisplayNamestring—Human-readable name for status display.
Descriptionstring—Description of the service.

Service definition keys #

KeyTypePurposeSee
Machine\System\Services\(parent key)Parent of all service definitions; each child key is a service.Defining a service
Machine\System\Services\SchemaVersiondwordSchema version guard (currently 1).Defining a service
Machine\System\Services\<name>\LastTimerRunREG_QWORDLast-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\:

KeyTypeDefaultPurposeSee
MaxParallelStartsdword (> 0)10Maximum services starting concurrently. Missing → default; zero/invalid → Recovery.Boot and boot modes
BootSuccessGracedword30Seconds a Critical service must hold a satisfying state before boot is successful.Boot and boot modes
ShutdownTimeoutdword90Maximum seconds for the whole graceful shutdown.Shutdown
SettleTimeoutdword5Seconds to wait for the boot set to settle before starting boot:settled services anyway.Triggers and timers
PostKillTimeoutdword5Seconds 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\:

KeyTypeDefaultPurposeSee
ControlSecuritybinarySYSTEM full; Administrators shutdown + reload-configDescriptor for system-level control operations.Who can manage a service
MaxControlConnectionsdword32Maximum concurrent control-socket connections.Controlling services
MaxRequestSizedword65536Maximum control-socket request size (bytes).Controlling services
ConnectionTimeoutdword30Seconds before an idle control connection is closed.Controlling services
MaxLogLineLengthdword8192Maximum bytes per service output line before truncation.Service output and logging
MaxLogBufferPerServicedword65536Maximum bytes buffered per service pipe before back-pressure.Service output and logging
LogReadBytesPerEventdword16384Bytes 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
PreEventdBufferdword1048576Total 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)emptyDefault 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)emptyBoot-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>\Kindstring(required)directory or file — what to create at Path.Boot and boot modes
ProvisionedPaths\<name>\Pathstring(required)Absolute path to create or verify.Boot and boot modes
ProvisionedPaths\<name>\SecuritybinarySYSTEM + Administrators full, users read/traversePeios file security descriptor to apply to the object.Boot and boot modes
ProvisionedPaths\<name>\Requireddword0If 1, a failure to provision this entry sends boot to Recovery mode before Phase 2 starts.Boot and boot modes

Note

A few ProvisionedPaths rules worth knowing before you rely on them: Kind=file never truncates an existing file (it only makes sure the file is present), and peinit does not create missing parent directories for you — define each directory you need as its own entry, or depend on the package that owns the parent. A malformed entry is logged and skipped rather than blocking boot; only an entry you mark Required=1 can send boot to Recovery mode, and only when it genuinely can't be created or secured.

Keys peinit reads from other subsystems #

KeyTypePurposeSee
Machine\System\eventd\LogSocketPathstringPath 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.

Peios Learn — documentation for the Peios project.

Built with Trail.