peinit
Technical Reference Manual — the init system and service manager: boot, the service model, identity, supervision, jobs and operations, and shutdown.
Single-page view · as markdown
1.1 Overview
Peios / Advanced Peios / peinit / Introduction
peinit is PID 1. It is the only service manager on a Peios system: every supervised process on the machine — a platform daemon, an application service, a startup hook, a health probe, a job some other service asked for on a user's behalf — is forked by peinit and watched by peinit until it exits.
It is a single-threaded Rust process. That is the constraint the rest of its design answers to. A blocking syscall in PID 1 stops everything: child reaping, watchdog expiry, shutdown signals, the control socket. So peinit keeps a complete in-memory model of every service it knows about, reads the registry synchronously only twice — at boot and on an explicit reload — and pushes anything that could block off the main loop into a forked helper or a pollable descriptor.
1.1.1 What makes it not systemd #
Two things, and both run deep enough to change the shape of the daemon.
Services are securable objects. A service carries a Security Descriptor that says who may start it, stop it, query it, or reload it, and peinit evaluates that descriptor against the caller's KACS token on every control request. There is no "root can do anything" path, because there is no root — there is a token, and AccessCheck is the only thing that decides.
Service identity is a token, not a user. peinit never sets a UID, a GID, or a Linux capability on a service process. It obtains a KACS token — minted from its own for the platform daemons that start before an authority exists, requested from authd for everything else — restricts its privileges to what the definition asked for, and installs it on the child before exec. Every service also carries a per-service SID derived from its name, so two services sharing an identity are still distinguishable to an access check.
Configuration follows from the second one: service definitions live in
the registry, under Machine\System\Services\, where they are protected
by the registry's own descriptors rather than by file permissions. There
are no unit files, no generators, and no translation layer.
1.1.2 The shape of the daemon #
Boot is two phases with registryd as the boundary. Phase 1 is compiled in and has no registry dependency at all: it confirms the root is writable, mounts what is missing, restores entropy, settles the clock, and starts registryd. Phase 2 reads the service graph out of the registry and boots the system from it. When Phase 1 cannot complete, there is no Phase 2 to fall back to, and peinit drops into a recovery shell.
Once booted, peinit is an event loop over a handful of descriptors: a signalfd, the control socket, the notify socket, one timerfd per armed timer, a pidfd per supervised process, a pipe pair per service's output, the registry's change-notification descriptor, and the JFS device. Work arriving on any of them turns into an operation — a queued, observable request to move a service through its state machine — and executing an operation eventually forks a job.
Those two objects are how peinit stays comprehensible under concurrency. Operations exist so that two administrators issuing conflicting commands get a defined answer instead of a race. Jobs exist so that "what actually ran" is a thing with an identifier, a token summary, an exit status and a log correlation key, rather than a PID that may already have been reused.
peinit keeps no history of either. A job or an operation that reaches a terminal state is emitted as a structured event into the KMES kernel ring buffer and dropped. eventd is the historian.
1.1.3 What peinit is not #
It does not assemble storage. The initramfs delivers a mounted, writable root, and peinit neither decrypts, nor assembles, nor checks it. It has no mount feature beyond the fixed Phase 1 set — mounting a data partition is a Oneshot service's job.
It does not store logs. It holds the pipes at birth, tags each line, and forwards it to eventd; before eventd exists it buffers, and when the buffer fills it drops the oldest.
It does not authenticate anyone. authd mints tokens; peinit installs them. It does not resolve identities, and it does not know or care whether a principal is local or from a domain.
And it does not support forking daemons. It tracks the process it spawned, through a pidfd obtained at fork, and there is no mechanism for a service to point supervision somewhere else.
1.2 What This Manual Covers
Peios / Advanced Peios / peinit / Introduction
This manual describes peinit as it is built: the boot sequence, the service model and its registry schema, how a service acquires its identity, the exact sequence between "start this" and "the binary is running", the state machine and its causes, dependencies, jobs and operations, timers, output handling, shutdown, and the security model.
1.2.1 What is a contract and what is not #
Two of peinit's interfaces are specified separately, in PSPU §4: the control socket, spoken by administrative tools and by any program that manages services, and the notification socket, spoken by every service that reports readiness, keepalives, or a stored descriptor. Those are contracts. A third party implements one side of each, and what is written there binds both.
This manual covers the other side of that boundary — how peinit fulfils them, and everything that is not a contract at all:
- how a command becomes an operation, and what happens when two of them collide (§8.2, §8.3)
- what a command does to a service in each state (§10.3)
- how a notification is authenticated and applied (§10.5, §10.6)
Where a chapter touches the contract it references PSPU §4 rather than restating it.
The service definition schema is here rather than in PSPU. It is registry configuration, protected by the registry's descriptors and administered by the same tools as the rest of the registry, and it is documented for the people who write service definitions rather than specified as a wire format. §3.2 is the reference.
1.2.2 What this manual does not cover #
- KACS — tokens, Security Descriptors, AccessCheck, and the per-service SID algorithm peinit reproduces. Peios Kernel TRM §3.
- LCS — the registry syscalls, watches, and layer resolution peinit reads through. Peios Kernel TRM §5.
- KMES — the ring buffer peinit emits its events into. Peios Kernel TRM §2.
- loregd — the storage behind registryd. Its own TRM.
- eventd — where the logs and events go. Its own manual.
- authd — token minting and identity routing. peinit's requirements of it are described in §4.3; its interface is authd's own.
- JFS — the kernel side of ad-hoc job submission. §8.5 describes what peinit does with what JFS hands it.
- Using peinit — writing service definitions, the
svctlcommand surface, and everyday administration are covered in Using Peios.
1.2.3 Constants and keys #
Registry keys and compiled-in constants are collected in the appendices, so a chapter's reference material does not interrupt the prose that explains it. Where an appendix defines a value the body references it rather than repeating it.
1.3 Terminology
Peios / Advanced Peios / peinit / Introduction
Terms defined where they are introduced — activation generation, boot generation, transition cause, cgroup generation, start generation — are not repeated here.
Service. A named, supervised unit of execution: a definition in the registry, a runtime state, a Security Descriptor, and at most one running main process. Services are the primary unit of management and the thing dependencies are expressed between.
Job. One supervised process execution. Every fork peinit performs is a job — a service's main binary, a pre-exec hook, a health check invocation, an ad-hoc submission — with a GUID, a lifecycle, a token summary and a log correlation key. Jobs are the observable unit of what actually ran.
Operation. A first-class object representing a requested state machine action on a service. Control commands do not mutate state directly; they create operations that are validated, queued, resolved against whatever else is in flight, and executed by the event loop.
Trigger. A rule in a service definition saying when the service starts automatically: at boot, once the boot set has settled, or on a schedule. A service with no triggers starts only when something asks for it.
Phase 1. The compiled-in bootstrap: root writability, the remaining virtual filesystems, the persisted random seed, the local machine ID, the clock, registryd, boot-time path provisioning, and infrastructure setup. No registry access happens before registryd is serving.
Phase 2. The registry-driven boot: read the service definitions, build and validate the dependency graph, and start the boot-triggered services in dependency order.
ErrorControl. The per-service policy for an irrecoverable failure. Normal leaves the service Failed; Critical syncs the filesystems and reboots.
Token. The per-thread KACS identity object — a user SID, group SIDs, a privilege bitmask, an integrity level, and metadata. Tokens are the sole identity mechanism: peinit sets no UIDs, GIDs, or Linux capabilities on service processes. Peios Kernel TRM §3.2.
Security Descriptor (SD). The KACS structure controlling access to a securable object. peinit uses two: a ServiceSecurity descriptor per service, controlling who may manage it, and its own control descriptor, controlling system-level operations. Peios Kernel TRM §3 and PCDS §5.
AccessCheck. The KACS function that evaluates a token against a descriptor to produce a decision. peinit calls it for every control request. Peios Kernel TRM §3.8.
Per-service SID. A SID under authority S-1-5-80 derived
deterministically from the service name, carried in the group list of
every service token. Two services running as the same principal are
still distinguishable to an access check. §4.4.
registryd. The userspace registry source daemon serving the persistent hives. peinit starts it in Phase 1 from a compiled-in definition and treats it as opaque thereafter. Its implementation is loregd, a distinction visible only in recovery mode.
Registry. The configuration system LCS and its sources provide
together. peinit reads service definitions from
Machine\System\Services\, boot configuration from
Machine\System\Boot\, and its own parameters from
Machine\System\Init\.
KMES. The kernel-mediated event subsystem. peinit emits its structured events — job and operation lifecycle, audit records — into its ring buffer, where they survive eventd restarts and reboots. Peios Kernel TRM §2.
JFS. The Job Forwarding Subsystem: the kernel bridge that captures a
caller's effective token and delivers it, with a job definition, to
whatever holds /dev/jfs open. peinit is the consumer. §8.5.
pidfd. A descriptor referring to one specific process, obtained
atomically at fork through clone3(CLONE_PIDFD). Every process peinit
supervises is tracked by one, which is what makes supervision immune to
PID reuse.
cgroup. peinit uses cgroups v2 for process tracking and clean kill
only — not for resource accounting or limits. Every service gets its own
tree under /sys/fs/cgroup/peinit/. §5.1.
sd_notify. The datagram protocol services use to report readiness,
status, keepalives and stored descriptors, over the socket named by
NOTIFY_SOCKET. Specified in PSPU §4.
TCB. The Trusted Computing Base: the kernel, KACS, LCS, KMES, peinit, registryd, authd, lpsd and eventd. A compromise of any of them compromises the system.
1.4 Compatibility and Prior Art
Peios / Advanced Peios / peinit / Introduction
peinit is not a port or a reimplementation of anything. The choices that give it its shape — identity as a token, configuration in the registry, operations as objects, jobs as observable executions — were made for Peios. But several interfaces deliberately match existing conventions, because the convention is good and breaking it buys nothing.
1.4.1 sd_notify #
peinit speaks systemd's sd_notify datagram protocol: a service reports
readiness, keepalives, status and stored descriptors by sending
KEY=VALUE lines to the socket named in NOTIFY_SOCKET. Existing
software that supports sd_notify works unmodified.
The compatibility is not total. MAINPID= is not supported, because
peinit does not supervise forking daemons — it tracks the process it
forked through a pidfd, and there is no way to redirect supervision
somewhere else. BUSERROR= is not supported because Peios has no D-Bus.
The protocol as peinit speaks it, including which fields it accepts and how a sender is authenticated, is specified in PSPU §4.
1.4.2 The fd store #
FDSTORE=1, FDNAME=, FDSTOREREMOVE=1 and FDPOLL=0 work as they do
under systemd, and descriptors come back to a restarted service at fd 3
onwards with LISTEN_FDS and LISTEN_FDNAMES set. A daemon written to
survive a restart without dropping its listening sockets keeps working.
1.4.3 Calendar expressions #
Timer schedules use systemd's OnCalendar format, including weekday
names, lists, ranges, repetition, the ~ last-day-of-month form, IANA
timezone suffixes and the named shortcuts. §9.1 gives the grammar peinit
actually parses; the one deliberate subtraction is sub-second precision,
which service scheduling has no use for.
1.4.4 Windows Service Control Manager #
The service model owes its architecture to the Windows SCM: services as securable objects carrying their own descriptors, identity as a token rather than a user account, an access-controlled control interface, and a structured state machine. The debt is architectural. peinit implements none of the SCM's RPC protocol, none of its service types, and none of its control codes.
1.4.5 What is deliberately absent #
There is no systemd unit file support: no reader, no parser, no generator, no migration path. Service definitions are registry keys. Roles are how a package declares them.
There is no socket activation in the systemd sense — peinit does not listen on a service's behalf and hand it a connection. The fd store covers the case that matters, which is a service keeping its own listener across its own restart.
There is no resource control. peinit uses cgroups for tracking and for
clean kill, and sets RLIMIT_NOFILE and RLIMIT_CORE if a definition
asks, but it does not do accounting, slices, or limits.
1.4.6 Features that belong to other components #
| Concern | Component |
|---|---|
| Authentication and token minting | authd |
| The local identity database | lpsd |
| Log storage, indexing and queries | eventd |
| Registry storage | registryd, over LCS |
| Packaging and installation | peipkg and the role system |
| Device management | eudev |
| Network configuration | a dedicated service |
| File access control enforcement | FACS |
2.1 The Initramfs Contract
Peios / Advanced Peios / peinit / Boot
peinit starts with the root filesystem already assembled. Everything that makes a root mountable — LUKS decryption, LVM activation, RAID assembly, any filesystem check — belongs to the initramfs and has happened before peinit exists. peinit performs no root assembly, no decryption, no repair, and no fsck, and it does not assume one has been done.
2.1.1 The handoff #
The initramfs transfers control by chroot-ing into the assembled root
and exec'ing peinit there. Not switch_root, and not pivot_root: the
kernel refuses to relocate onto the initramfs rootfs, so that route is
closed. The consequence is that the initramfs rootfs does not go away.
It remains the mount-namespace root — emptied, and unreachable from
peinit's view, but present. peinit therefore never assumes a clean
single-root mount topology, and never attempts pivot_root.
peinit is installed in package storage at /usr/bin/peinit2 and reached
through the fixed runtime path /bin/peinit2. The boot-image tooling
sets the kernel init= to the runtime path. Before the transfer, the
initramfs has assembled the base StrataFS topology, including the /bin
and /sbin views, because peinit reaches every binary it execs through
them.
At handoff:
- the real root is mounted read-write at
/; /proc,/sysand/devare mounted and have been moved into the real root;- the environment holds
TERMand nothing else, and argv is just peinit's own path.
The read-write requirement is registryd's, not peinit's. loregd's storage backend needs to write its write-ahead log and shared-memory files even to answer a read, so a read-only root cannot support Phase 2 at all. Delivering the root writable is the initramfs's job; peinit only confirms it.
peinit inherits nothing from that environment. It does not rely on having been given anything, and it does not pass its own near-empty startup environment through to services — the environment a service receives is constructed from scratch (§5.5).
2.1.2 What stays outside #
Non-root storage is not peinit's concern. Data partitions and additional
filesystems are mounted at the services layer, typically by a Oneshot
service that runs mount. peinit has no mount feature beyond the fixed
Phase 1 set.
2.2 Bootstrap Identity
Peios / Advanced Peios / peinit / Boot
The steady-state identity flow is: peinit asks authd for a token, authd mints it, peinit installs it on the child. That flow cannot start the system, because authd depends on lpsd, lpsd depends on registryd, and registryd has to be running before any of them. The bootstrap model breaks the circle.
2.2.1 Platform services run as SYSTEM #
A service whose definition says Identity=SYSTEM gets a token peinit
mints from its own, with kacs_create_token (§4.2). No authd
interaction is involved — which is the point, since authd does not exist
when the first of these services starts.
Four services use it:
| Service | Why |
|---|---|
| registryd | Starts before authd exists at all. |
| lpsd | Must be running before authd can resolve a local identity. |
| authd | Needs SeTcbPrivilege and SeCreateTokenPrivilege; it is the minter for everything else. |
| eventd | Starts early, before authd is necessarily available. |
Nothing restricts which services may declare Identity=SYSTEM. There is
no allowlist, because an allowlist would be enforcing a boundary that is
already enforced somewhere better: the Security Descriptor on
Machine\System\Services\. Anyone who can create a service definition
is by definition trusted to choose its identity, and adding a second
list to maintain would only create a way for the two to disagree.
Every SYSTEM token peinit mints carries the service's per-service SID in
its group list, computed by peinit itself from the service name (§4.4).
That is what keeps platform services distinguishable to an access check
despite all of them running as S-1-5-18.
2.2.2 After authd #
Once authd and lpsd are running, every subsequent service gets its token
through the ordinary authd flow (§4.3). A definition with no Identity
field defaults to LocalService — a well-known principal with a minimal
privilege set — and authd adds the per-service SID to the token it
mints.
2.3 Phase 1
Peios / Advanced Peios / peinit / Boot
Phase 1 is compiled into peinit. It does not change at runtime and has no registry dependency, because its whole purpose is to reach the point where a registry exists. It does the minimum needed to make Phase 2 possible, and most of its failures are fatal to the boot.
2.3.1 Step 1: Confirm the root is writable #
The initramfs delivers the root mounted read-write. peinit does not remount it — mount flags belong to the initramfs, and a redundant remount of an already-writable or overlay root can fail for reasons that have nothing to do with the root being usable.
Instead peinit probes. It creates /.peinit/ if it is absent, writes a
uniquely named file there — the name is derived from peinit's own PID
and a namespace identifier, so two probes cannot collide — writes to it,
and removes it. If any part of that fails, the root is not usable for
Phase 2 and peinit enters recovery mode.
2.3.2 Step 2: Mount what is missing #
/proc, /sys and /dev are already mounted. peinit does not
blindly mount them again: a redundant mount stacks a second filesystem
over the populated one, and on some kernel and flag combinations returns
EBUSY instead.
peinit reads /proc/self/mountinfo to find out what is already there,
and mounts only what is not:
| Mount point | Filesystem | Flags | Provided by |
|---|---|---|---|
/proc | proc | nosuid, nodev, noexec | initramfs |
/sys | sysfs | nosuid, nodev, noexec | initramfs |
/dev | devtmpfs | nosuid | initramfs |
/dev/pts | devpts | nosuid, noexec | peinit |
/dev/shm | tmpfs | nosuid, nodev | peinit |
/run | tmpfs | nosuid, nodev | peinit |
/sys/fs/cgroup | cgroup2 | nosuid, nodev, noexec | peinit |
Each mount(2) passes the filesystem name as both the source and the
filesystem type, passes only the listed flags, and passes null mount
data. Mount points that do not exist are created first.
There is a bootstrap wrinkle in reading mountinfo at all: the file lives
in /proc, which is one of the things being checked for. If the read
fails with ENOENT or ENOTDIR, peinit mounts /proc from the table
and retries. Any other failure to read or parse mountinfo sends peinit
to recovery.
For the three initramfs-provided rows, an already-mounted filesystem is
success, and so is an EBUSY from an attempted mount. For the four
peinit owns, a mount failure sends peinit to recovery.
2.3.2.1 Seeding descriptors on the new filesystems #
Three of the four filesystems peinit mounts are fresh and empty:
/dev/shm, /run and /sys/fs/cgroup. Under KACS an inode with no
Security Descriptor is denied to every caller, and there is nothing on a
newly mounted tmpfs for a new inode to inherit from — so peinit stamps
the mount root with a descriptor that grants SYSTEM and Administrators
full control and is marked inheritable by both containers and objects:
O:SY G:SY D:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)
Everything created underneath — the control socket, the notify socket,
per-service runtime directories, the cgroup hierarchy — inherits from
it. This is why peinit never sets mode bits on the sockets it creates;
under KACS they would mean nothing, and the descriptor is the thing that
does the work. It is the same descriptor the initramfs seeds onto the
root filesystem, and the two are kept identical on purpose: this one
inheritable ACL is, in practice, the access policy of everything under
these mounts, so an ACE missing here is missing from every per-service
directory under /run/services.
Failure to apply the descriptor sends peinit to recovery. Without it every file peinit later creates on that filesystem would be unreachable to everything, including peinit.
/proc, /sys and /dev are not stamped: they arrive from the
initramfs already populated.
2.3.2.2 Device node policy #
/dev arrives with that same inheritable descriptor on its root and on
every node, which is the right default — whatever the root grants, a
disk hot-plugged later inherits, so the root must be acceptable on a raw
block device — but it leaves /dev/null unusable by anyone who is not
an administrator. A single inherited descriptor cannot say "/dev/null
for everyone, the disks for administrators", so peinit enumerates the
exceptions. Once the mounts are up it stamps each of these nodes with a
descriptor of its own:
| Node | DACL |
|---|---|
/dev/null, /dev/zero, /dev/full, /dev/random, /dev/urandom, /dev/tty, /dev/ptmx | D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FRFW;;;WD) |
Everyone may open the node for reading and writing and may stat it;
nobody but SYSTEM and Administrators may change its descriptor. Only the
DACL is replaced — owner and group stay as the seed left them — and the
ACEs carry no inheritance flags, because a device node has no children.
/dev/console is deliberately not on the list: it is the SYSTEM
console.
The step is advisory. A node that cannot be stamped is reported as a warning and stays on the inherited default, usable by administrators and denied to everyone else; a node that does not exist is noted and skipped. Neither sends peinit to recovery.
2.3.3 Step 3: Restore the persisted random seed #
peinit restores the seed at /var/state/peinit/random-seed once /dev
is available and before registryd starts. The seed is a machine-local
entropy cache for the kernel CSPRNG. It is not configuration, and
shipping one in a packaged image, live ISO or VM template would hand
every instance of that image the same starting entropy.
If the file is absent, that is an ordinary first boot or a stateless live boot, and peinit continues silently. When a seed is present peinit mixes it into the kernel pool, preferring the interface that credits entropy for a locally persisted seed; if that fails it mixes the bytes without crediting and records the failure. A seed file that is empty, or larger than 4096 bytes, is treated as an error.
Nothing in this step can send peinit to recovery. A system with no entropy cache still boots; it just starts with less entropy, which is a problem for the image builder to solve with a hardware or virtio RNG rather than with a seed baked into the image.
The initramfs may perform the same restore earlier, once the persistent root is mounted. peinit's restore stays as the fallback for initramfs images that do not participate and for boots that have no initramfs.
2.3.4 Step 4: Ensure the local machine ID #
/lcl/etc/machine-id holds a stable local install identifier used for
software compatibility, log correlation and instance identity. It is not
a security principal: not a credential, not a SID, not an account, and
not an input to any authorisation decision.
The format is 128 bits as exactly 32 lowercase hexadecimal characters followed by one newline. A valid existing file is left alone. A file that is absent, empty, all zeroes, the wrong length, not hexadecimal, or missing its trailing newline is replaced: peinit draws 128 bits from the kernel CSPRNG and writes a valid file atomically, through a temporary file and a rename, with the result flushed.
Any failure of this step — an unreadable file, a CSPRNG failure, or an
unwritable path — sends peinit to recovery. The write does not create
parent directories, so an image that ships without /lcl/etc/ present
fails here rather than at first use.
Images and templates are expected to ship with no machine ID, or with an empty file as a reset marker. Clone tooling that wants a new identity removes or truncates the file and lets the next boot generate one. Stateless live boots without a persistent overlay get an ephemeral ID for that boot.
2.3.5 Step 5: Set the clock from the hardware RTC #
peinit reads the hardware clock and calls clock_settime() before
registryd starts, so that timestamps on registry operations, log entries
and the boot attempt counter mean something.
It opens /dev/rtc, falling back to /dev/rtc0 if that device is
absent, and reads it with RTC_RD_TIME. The returned struct rtc_time
is interpreted as UTC and converted to CLOCK_REALTIME seconds with
zero nanoseconds.
Every failure in this step sends peinit to recovery: no openable RTC
device, a failed read, a value that is invalid or before the Unix epoch,
a failed clock_settime, and — deliberately — a failure to close the
descriptor after a successful read. Leaking a descriptor in PID 1 during
bootstrap is a symptom of something being badly wrong, not a detail to
swallow.
2.3.6 Step 6: Start registryd #
peinit holds a compiled-in definition for registryd — the only compiled-in service definition there is:
| Field | Value |
|---|---|
| ImagePath | /sbin/registryd |
| Arguments | Machine=/var/state/loregd/Machine.hive, Users=/var/state/loregd/Users.hive |
| Identity | SYSTEM |
| Readiness | Notify |
| ErrorControl | Critical |
The hive paths are peinit's choice, not registryd's: where the machine registry lives is a boot-policy decision, and it is made here because this is the one service start that cannot consult configuration.
peinit mints a SYSTEM token including registryd's per-service SID,
creates the cgroup tree, forks with the token installed, and execs
/sbin/registryd through the runtime StrataFS view. Two separate
timeouts bound the start, both 30 seconds: one on process setup, driven
synchronously because there is no event loop yet, and one on readiness.
registryd's READY=1 means "accepting and serving registry requests",
not "the process is alive" — it does not signal until its storage
backend is open, its schema is validated and it can answer a read.
2.3.6.1 The schema-version guard #
After readiness, peinit ensures the base registry structure exists and
then probes it. Ensuring comes first: peinit creates Machine\System,
Machine\System\Services and Machine\System\Init if they are absent
and stamps Machine\System\Services\SchemaVersion with the current
schema version, 1. Only then does it read the value back.
The read is what verifies registryd is genuinely serving. A value that
is present but not a REG_DWORD, or that is a REG_DWORD of the wrong
length, fails the probe. A key or value that is absent reads as zero and
passes — the structure was just created, so absence at this point means
the write did not take effect, and the failure that matters is the
provisioning failure, which is reported directly.
The consequence is that the guard is self-healing. An unprovisioned
first boot, or a registry cleared by the recovery tools, comes up with
an empty Machine\System\Services\ and boots into a Phase 2 with no
services rather than into recovery.
2.3.6.2 Keeping registryd #
When registryd passes readiness and the probe, peinit retains the activation as an ordinary runtime service instance: its state, its pidfd-tracked main process, its cgroup generation and paths, its output pipe ownership, its notify generation, its job identity, and any cleanup evidence. Ownership is not dropped at the Phase 2 boundary.
During Phase 2 the registry's own definition of registryd, if there is
one, is merged onto the retained activation. peinit does not create a
second inactive record and does not restart registryd because a
definition has appeared. If the registry definition is absent or
invalid, the ordinary graph validation rules apply.
If registryd fails to start, its readiness times out, or the probe fails, peinit enters recovery. There is no Phase 2 without a registry.
2.3.7 Step 7: Autorun scripts #
Between registryd starting and path provisioning, peinit runs every
non-directory entry in /lcl/policy/autorun.d, in sorted order, by
absolute path, with the working directory / and PATH=/sbin:/bin.
Each runs under peinit's own SYSTEM token.
The step is fail-open at every point: a missing directory, an unreadable directory, a spawn failure and a non-zero exit are all console warnings and none of them stops the boot. Its console output bypasses the quiet policy (§2.6), because a script that ran this early and went wrong needs to be visible.
2.3.8 Step 8: Provision boot-time paths #
Covered in §2.4.
2.3.9 Step 9: Infrastructure setup #
Three things, before Phase 2 begins:
- The control socket at
/run/services/peinit/control.sock, which serves every runtime command for the lifetime of the system. - The JFS device: peinit opens
/dev/jfsand adds the descriptor to its event loop, enabling ad-hoc job submission once Phase 2 runs. - Loopback: peinit brings up
loover netlink, because services that bind127.0.0.1need it.
Control socket creation failing sends peinit to recovery — without it there is no way to administer the system. The other two are warnings: a JFS open failure, a JFS event-loop registration failure and a loopback bring-up failure all let Phase 2 proceed.
2.3.10 Failure summary #
| Failure | Response |
|---|---|
| Root writability probe fails | Recovery |
| A mount point cannot be created | Recovery |
| A peinit-owned filesystem fails to mount | Recovery |
| A mounted filesystem cannot be stamped with its descriptor | Recovery |
/proc/self/mountinfo unreadable or unparseable | Recovery |
/proc, /sys or /dev already mounted, or EBUSY | Tolerated as success |
| A device node in the policy list cannot be stamped | Warning; node keeps the inherited default |
| A device node in the policy list does not exist | Noted; boot continues |
| Random seed absent, oversized, empty, or unrestorable | Warning; boot continues |
| Machine ID read, generation, or write fails | Recovery |
| Machine ID absent, empty, or malformed | Regenerated; boot continues |
| Any RTC or clock failure | Recovery |
| registryd fails to start, or setup times out | Recovery |
| registryd readiness times out | Recovery |
| Base registry provisioning fails | Recovery |
| Schema-version probe returns a wrong type or length | Recovery |
| An autorun script is missing, unspawnable, or exits non-zero | Warning; boot continues |
| A provisioning entry is malformed | Warning; entry skipped |
| An optional provisioned path fails | Warning; boot continues |
| A required provisioned path fails | Recovery |
| Control socket creation fails | Recovery |
| JFS open or registration fails | Warning; boot continues |
| Loopback bring-up fails | Warning; boot continues |
2.4 Path Provisioning
Peios / Advanced Peios / peinit / Boot
Some filesystem objects belong to no single service. A directory two packages both write into, a state file created before anything runs, a path that has to exist with a particular Security Descriptor before the first service that uses it starts — none of these has an owner in the service model, and creating them from a service's pre-exec hook makes their existence depend on start ordering.
Boot-time path provisioning is the registry-backed answer, and the equivalent of the tmpfiles.d role elsewhere. peinit applies it after registryd is serving and before any Phase 2 service is planned or started.
2.4.1 Entries #
Each child key under Machine\System\Init\ProvisionedPaths\ is one
entry. Unknown values on an entry are ignored.
| Value | Type | Required | Default | Meaning |
|---|---|---|---|---|
Kind | string | yes | — | directory or file. |
Path | string | yes | — | Absolute path to create or verify. |
Security | binary | no | built-in | The Peios file Security Descriptor to apply. |
Required | dword | no | 0 | If 1, failing this entry prevents Phase 2. |
For Kind=directory peinit ensures the path exists as a directory; for
Kind=file it ensures the path exists as a regular file. In either case
a path that exists with a different file type fails the entry. An
existing file is opened rather than created, so provisioning never
truncates one.
peinit does not create parent directories. The parent is checked, and has to already be a directory. A package that needs a hierarchy declares each directory explicitly, or depends on the package that owns the parent — which keeps the ownership of every directory traceable to a package rather than to whichever entry happened to run first.
2.4.2 Descriptors #
When Security is present, peinit applies the supplied binary
descriptor. A malformed or rejected descriptor fails the entry.
When it is absent, peinit applies a built-in default:
O:SY G:SY D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FR;;;BU)
SYSTEM and Administrators get full control; ordinary users get
FILE_GENERIC_READ.
2.4.3 Failure #
Entries with Required=0 are fail-soft: peinit logs and continues.
Entries with Required=1 are fail-closed: peinit logs and enters
recovery before Phase 2 starts.
An entry that is malformed — a missing or unrecognised Kind, a missing
or relative Path, a value of the wrong type — is logged as a warning
and skipped, regardless of Required. Required marks a path as
essential to boot; it does not make a broken entry more dangerous than a
missing one.
2.5 Phase 2
Peios / Advanced Peios / peinit / Boot
With registryd serving, peinit reads the service graph and boots the system from it. Phase 2 is entirely registry-driven.
2.5.1 Reading the definitions #
peinit reads every key under Machine\System\Services\. The reads are
bounded by LCS's request timeout: if registryd hangs mid-read, peinit
receives ETIMEDOUT and enters recovery.
Decoding is per key. A definition that fails to decode — an invalid
service name, a malformed trigger, an unclosed quote in a command, a
registry: check naming an uncacheable key, a duplicate known field, an
unrecognised value for an enumerated dword — fails that service, which
is marked Failed with cause ValidationError. The boot proceeds with
every other definition, and anything that depended on the failed service
fails in turn through the ordinary dependency propagation.
Only services carrying a boot trigger are root candidates. A service
with no triggers is demand-only and is not a root, though it can still
be pulled into the boot transaction as somebody's dependency. A service
with Disabled=1 is excluded from the boot graph entirely, but its
definition is still loaded into the in-memory model so it can be started
by hand later.
2.5.2 Building and validating the graph #
The boot graph is every boot-triggered root candidate plus the
transitive closure of their Requires, BindsTo, and existing
non-disabled Wants dependencies. Requires and BindsTo pull their
target in even if the target has no boot trigger. Wants targets come
in as best-effort members; missing or disabled ones are ignored. A
missing or disabled Requires or BindsTo target blocks the dependent
with cause DependencyFailure, and that blocking propagates.
peinit topologically sorts the graph and validates it before starting anything. The rules are in §7.2; the outcomes that matter here are:
- A cycle fails every service in it. A cycle involving a Critical service downgrades the boot to Safe mode without rebooting.
- An unresolvable conflict fails both services. Same downgrade if either is Critical.
- A missing
Requirestarget fails the dependent. - Warnings — a
Readiness=Aliveservice with dependents that require it — are logged and do not prevent boot.
2.5.3 Starting #
peinit walks the graph and starts services, parallelising wherever the graph allows, up to a configurable limit:
| Key | Default | Meaning |
|---|---|---|
Machine\System\Boot\MaxParallelStarts | 10 | Services starting concurrently. |
An absent key uses the default. A value of zero, a type mismatch, or a malformed payload is invalid boot configuration and sends peinit to recovery — running the scheduler with an effective limit of zero would hang the boot rather than fail it.
As each service reaches a dependent-satisfying state — Active for
Simple, Completed for Oneshot with or without RemainAfterExit, Skipped
for a service whose conditions did not hold — its dependents become
eligible and join the start queue. A Oneshot without RemainAfterExit
passes through Completed, releasing its dependents, and then goes
Inactive.
Dependents blocked on a Requires or BindsTo target wait for that
target to reach a satisfying state. Dependents blocked on a Wants
target wait only for it to reach any terminal state, satisfying or
not — which is what makes Wants ordering rather than dependency.
2.5.3.1 The typical order #
Nothing below is hardcoded. It falls out of the dependency graph that the standard role definitions produce, and an administrator who changes a dependency gets a different order.
- eudev — device management. SYSTEM, with
RequiredPrivilegesstripped to the minimum it needs. Starts before authd exists. - lpsd — the local identity database. Depends only on registryd.
- authd — the identity authority. Depends on registryd and lpsd. Once it is ready, token minting is available.
- eventd — logging and audit. Services started before it log into peinit's pre-eventd buffer.
- Networking — the first service to receive a token from authd.
- Application services, in dependency order.
- Login services, last, so the system is operational before it accepts a session.
2.5.3.2 The bootstrap matrix #
| Service | Phase | Identity | Token from | Readiness | ErrorControl |
|---|---|---|---|---|---|
| registryd | 1 | SYSTEM | minted by peinit | Notify | Critical |
| eudev | 2 | SYSTEM | minted by peinit, privileges stripped | Alive | Normal |
| lpsd | 2 | SYSTEM | minted by peinit | Notify | Critical |
| authd | 2 | SYSTEM | minted by peinit | Notify | Critical |
| eventd | 2 | SYSTEM | minted by peinit | Notify | Critical |
| networking | 2 | service token | authd | Notify | Normal |
| sshd | 2 | service token | authd | Alive | Normal |
| application services | 2 | service token | authd | per-service | Normal |
2.5.4 Deferred starts #
A service whose trigger is boot:settled is not part of the boot plan
at all. It is not a root, it does not consume the parallel-start budget,
it is not counted towards boot success, and it cannot block or delay
anything. peinit starts it after the plan, once the boot has stopped
moving.
"Stopped moving" is precise: every service in the plan — those that were started and those that were blocked — is in a state it will not leave without help. Active, Completed, Failed, Skipped, Abandoned and Inactive all count as settled. Starting, Reloading, Stopping and Backoff do not, because a service between restart attempts is going to produce more output.
A deadline bounds the wait:
| Key | Default | Meaning |
|---|---|---|
Machine\System\Boot\SettleTimeout | 5 | Seconds before deferred services start regardless. |
The deadline is measured from the moment the plan was observed. An absent key uses the default; a type mismatch or a bad length sends peinit to recovery. Zero is legal and means "start on the next turn, settled or not".
Whichever comes first — the set settling or the deadline expiring — the deferred services start, once, each independently. A start that fails is recorded and dropped: one refusing service does not stop the others and does not affect the boot. The dispatch carries a flag saying whether the deadline expired rather than the set settling, so a service that cares whether the boot was still moving can be told.
2.5.5 Boot success #
A boot is successful once every Critical service has held a dependent-satisfying state continuously for a grace period:
| Key | Default | Meaning |
|---|---|---|
Machine\System\Boot\BootSuccessGrace | 30 | Seconds of held health before the boot counts. |
The criterion is satisfying, not Active. A Critical Oneshot reaches Completed and never reaches Active, so a test for Active would make such a service unable to ever mark a boot successful. Skipped counts too.
Success resets the boot attempt counter to zero (§2.7).
2.5.6 Failure summary #
| Failure | Response |
|---|---|
| A service definition fails to decode | Recovery |
| A registry read times out | Recovery |
Invalid MaxParallelStarts or SettleTimeout | Recovery |
| A dependency cycle | All services in it Failed; Safe mode if any is Critical (see below) |
| An unresolvable conflict | Both Failed; Safe mode if either is Critical (see below) |
A missing Requires target | The dependent Failed |
| A Critical service fails during boot | Restart budget, then reboot |
| A non-Critical service fails | Failed; its Requires dependents fail; the rest continue |
| authd unavailable when a service needs a token | That service Failed |
The two Safe-mode rows carry a caveat. When the downgrade fires, peinit rebuilds the graph in Safe mode and discards the Full-mode one, so the services that caused it are not marked Failed — they are never entered into the blocked set at all. What records them is the boot-level downgrade finding described in Boot modes.
2.6 Boot Modes
Peios / Advanced Peios / peinit / Boot
peinit boots in one of three modes, forming an escalation path from normal operation to last-resort maintenance.
Full boot ---+-- success ----------------> counter reset, operational
|
+-- cycle w/ Critical ------> Safe mode (no reboot)
|
+-- conflict w/ Critical ---> Safe mode (no reboot)
|
+-- Critical failure -------> sync + reboot --+
|
Safe boot ---+-- success ----------------> counter reset, |
| operational (reduced) |
+-- Critical failure -------> sync + reboot --+
|
counter increments <----------+
|
counter >= N ---------> Recovery mode
Phase 1 failure ------> Recovery mode
2.6.1 Full mode #
The default. Every boot-triggered service starts in dependency order, as §2.5 describes.
2.6.2 Safe mode #
Safe mode starts a reduced set. Eligibility is a filter within the
boot-triggered set, not a replacement for it: a service with no boot
trigger does not auto-start in Safe mode whatever its SafeMode or
ErrorControl says, and remains demand-only. Within the boot-triggered
set, two categories are eligible:
- Critical services (
ErrorControl=Critical) start. If one fails, the ordinary Critical failure path applies — restart budget, reboot, counter increment, eventually recovery. SafeMode=1services are attempted best-effort. If one fails, Safe mode continues without it.
ErrorControl=Critical implies SafeMode, so a Critical service does
not have to declare both.
2.6.2.1 What caused the downgrade #
The rebuild discards the Full-mode graph, so the services that forced
Safe mode are never entered into the blocked set and are never marked
Failed. That is deliberate: Safe mode was never going to start them, and
a Failed state would say something about their own health that is not
true. status should keep meaning "this service is broken".
The reason is therefore recorded at boot level rather than per
service. Every finding that forced the downgrade — each critical cycle,
each critical boot conflict — is written to the console and emitted as a
boot.safe_mode_downgrade KMES event naming the services involved.
All of them are reported, not just the first. A machine can be downgraded by a cycle and a conflict at once, and an operator who fixed only the one they were shown would reboot straight back into Safe mode.
peinit rebuilds the dependency graph from scratch using only the eligible services. Dependencies on excluded services are dropped: if A depends on non-Critical B and B is excluded, A's dependency on B does not exist in the Safe mode graph. This is what makes Safe mode useful — it is a graph in which the broken parts of the configuration are simply not present, rather than a graph in which they are present and failing.
A successful Safe boot resets the boot attempt counter.
2.6.2.2 Entry #
- A cycle involving a Critical service at boot. Graph validation detects it and peinit downgrades in place, without rebooting — the cycle is a configuration error, and rebooting would find it again.
- An unresolvable conflict involving a Critical service at boot. Same reasoning.
peios.safemode=1on the kernel command line.
Safe mode is not entered because a Critical service crashed at runtime. That follows the ordinary path: restart budget, reboot, counter increment, recovery.
2.6.3 Console output #
peios.quiet=N bounds what peinit writes to the console:
| Value | Behaviour |
|---|---|
0 | Write unconditionally. |
1 | Do not write to a terminal held as the controlling terminal of a running service, except to announce loss of the system. This is the default. |
2 | Additionally drop ordinary progress everywhere, while still emitting errors. |
The two rules are independent, and an error is never less visible at 2
than at 1. A terminal is matched by device rather than by path, since
/dev/console and /dev/ttyS<n> can name the same device; where the
device cannot be determined, peinit falls back conservatively and treats
the terminal as held. Suppressed messages are discarded rather than
buffered.
The autorun step (§2.3) bypasses the policy: a script that ran that early and went wrong is worth interrupting a login prompt for.
2.6.4 Kernel command line #
| Parameter | Effect |
|---|---|
peios.safemode=1 | Force Safe mode. |
peios.recovery=1 | Force recovery mode regardless of the counter. |
peios.bootattempts=N | Set the recovery threshold; 0 disables the check. |
peios.quiet=N | Console verbosity, as above. |
peios.notifysocket=PATH | Override the notification socket path. |
A malformed value is ignored in favour of the default rather than failing the boot. Nothing exists this early to report a diagnostic to.
2.7 The Boot Attempt Counter
Peios / Advanced Peios / peinit / Boot
The counter is what turns a repeated failure into an escalation. peinit
keeps it at /.peinit/boot-attempts, as a plain decimal integer in a
file on the root filesystem — deliberately not in the registry, because
the registry may be the reason the boot is failing.
2.7.1 The cycle #
peinit reads the counter at startup, before selecting a boot mode. The recovery threshold is evaluated against this pre-increment value, so a default threshold of 3 admits exactly three boot attempts before recovery.
- Absent file: treated as 0.
- Unreadable, empty, non-decimal, carrying trailing non-whitespace data, or overflowing the counter representation: recovery. A counter that cannot be read cannot be trusted to escalate.
peinit increments once per boot, after the root is known writable and before Phase 2 begins. Incrementing before the root is known writable would silently lose the increment on a read-only root and defeat escalation entirely.
The increment happens after the mount, seed, machine ID and clock steps
rather than immediately after the writability probe, because reading the
kernel command line requires /proc. One consequence is that a recovery
entered from a mount failure, a machine ID failure, or an unreadable
command line does not advance the counter.
A write failure — a full disk, say — is treated as a counter of 0 and the boot continues. A failure to record an attempt is not itself a reason to escalate.
The counter resets to 0 on a successful Full or Safe boot, after the grace period.
2.7.2 Recovery threshold #
The threshold is peios.bootattempts=N on the kernel command line,
defaulting to 3. It is a command-line value rather than a registry one
because the check runs in Phase 1, before registryd is serving.
peios.bootattempts=0 disables the check entirely — the escape hatch
for a system whose counter is itself the fault.
peios.recovery=1 forces recovery without consulting the counter, but
does not suppress the increment.
2.8 Recovery Mode
Peios / Advanced Peios / peinit / Boot
Recovery mode is the last resort. There is no TCB guarantee and no degraded boot to speak of — it is a maintenance environment that hands the administrator an unrestricted SYSTEM shell on the console.
2.8.1 Entry #
- The boot attempt counter reaching the threshold (§2.7).
peios.recovery=1on the kernel command line.- Any of the Phase 1 failures listed in §2.3, most importantly a registryd that will not start or will not serve.
- A Phase 2 registry read that fails or times out, or invalid boot configuration.
- A required provisioned path that cannot be created or secured.
2.8.2 What peinit does #
peinit records the reason as a KMES audit event, then:
- Completes Phase 1 steps 1–5 if they have not been reached yet.
- Ensures the base registry structure exists, so the shell sees a normal layout even on a system that has never been provisioned.
- Attempts to start registryd. A failure here is ignored — recovery delivers a shell whatever registryd's state.
- Skips all Phase 2 services.
- Starts a shell on
/dev/consolefrom a compiled-in definition, with no registry dependency:/bin/recshif it is present and executable, otherwise/bin/sh, running as SYSTEM with a fixed environment ofPATH=/sbin:/bin,TERM=linuxandHOME=/. peinit does not care where either binary comes from. - Logs the failure reason to the console.
If the shell exits, peinit respawns it. Recovery never exits to an unmanaged PID 1.
If neither /bin/recsh nor /bin/sh can be exec'd, peinit cannot
deliver a shell at all. It logs the reason to the console, syncs, and
halts — PID 1 exiting would panic the kernel. A missing shell is a
binary-integrity failure and sits outside the boot-attempt machinery's
remit.
The shell receives /dev/console duplicated onto its standard streams,
but peinit does not call setsid() or acquire a controlling terminal
for it. The shell is not a session leader, so job control is not
available in the recovery shell.
Whether the earlier steps are re-run depends on where the failure came from. A recovery entered from a Phase 2 or runtime failure has already completed Phase 1 and has registryd running. A recovery entered from a Phase 1 failure — a mount that would not mount, an RTC that would not read, a control socket that would not bind — skips steps 1 and 3 above: it neither completes the remaining Phase 1 steps nor attempts registryd.
2.8.3 Offline registry access #
If registryd is what caused the recovery, the administrator needs tools that work without it. Three paths exist:
| Path | What it does |
|---|---|
loregd --inspector | Reads the storage database directly, bypassing LCS, for diagnosis. |
loregd --recover-from-backup | Restores from the automatic backup taken on every registryd startup. |
loregd --dangerously-clear-database | Wipes the registry. Role definitions are the source of truth for service configuration, so a cleared registry is recoverable. |
These name loregd rather than registryd because in recovery the administrator is interacting with the storage implementation, not with the registry abstraction. It is the one context where that distinction is visible.
2.8.4 Remote recovery #
Recovery requires console access: physical, IPMI or serial. Two post-v1 features address headless servers — rolling the registry back to a last-known-good state from the recovery shell, and an emergency sshd started without registry involvement.
3.1 Services
Peios / Advanced Peios / peinit / The Service Model
A service is the primary unit of management: a definition in the
registry, a runtime state, a Security Descriptor, and at most one
running main process. Definitions live under
Machine\System\Services\<name>, where the key name is the service
name. peinit reads them at Phase 2 boot and on an explicit
reload-config.
3.1.1 Names #
A service name is 1 to 128 bytes drawn from [A-Za-z0-9._-]. Any other
byte makes the name invalid.
Two exclusions are deliberate. / is out because names map directly
onto cgroup identifiers (§5.1) and onto registry key names, and a name
containing a separator would mean something different in each. : is
reserved for peinit's own synthetic naming.
3.1.2 The two types #
3.1.2.1 Simple #
A long-running daemon. peinit forks, installs a token, and execs the binary; the process is the service. When it exits, the service has stopped. This is the default and covers nearly everything — registryd, authd, sshd, application services.
Readiness comes from the Readiness field. Notify, the default,
waits for READY=1. Alive treats the process as ready the moment it
exists.
The service goes Active on readiness and stays Active until the process exits or something stops it.
3.1.2.2 Oneshot #
A run-to-completion task: database initialisation, a schema migration, a directory that has to exist. peinit forks, installs a token, execs, and waits for the exit.
Readiness is ignored. A Oneshot's readiness is always "it exited
successfully", because READY=1 is meaningless from a process whose job
is to finish. Success means exit code 0, or any code listed in
SuccessExitCodes.
The differences from Simple are:
- A successful exit goes to Completed. With
RemainAfterExit=1it stays there; without, it passes through Completed to release dependents and then goes Inactive. - A non-zero exit goes to Failed.
ExecStartPostruns after the successful exit rather than after a readiness signal, and does not run at all if the Oneshot failed.StartTimeoutcovers the entire execution, from the first pre-hook to the process exiting.
RemainAfterExit matters when the Completed state itself is the useful
information — so a status query shows a migration as finished rather
than as inactive.
3.1.3 Forking daemons #
peinit does not support them. A service that double-forks to daemonise
itself is working around a problem that does not exist when the service
manager tracks the process it spawned, and peinit tracks its child
through a pidfd obtained at fork. There is no MAINPID=, and no way to
point supervision at a different process.
A legacy binary that insists on double-forking is wrapped by whoever
packages it — a script with a --no-daemon flag, typically. That is a
packaging concern.
3.2 The Definition Schema
Peios / Advanced Peios / peinit / The Service Model
Every field a service definition can carry, with its registry type and its default. The semantics of each are in the section named alongside.
Value names are matched case-insensitively, so ImagePath and
imagepath are the same field — and therefore a definition carrying
both is a duplicate, not two fields.
| Field | Type | Default | Meaning |
|---|---|---|---|
| ImagePath | string | required | Absolute path to the service binary. |
| Arguments | multi_string | — | Arguments passed to the binary. |
| Type | dword | 0 (Simple) | 0 Simple, 1 Oneshot. §3.1 |
| Triggers | multi_string | — | When the service starts automatically. §3.4 |
| Disabled | dword | 0 | If 1, no trigger activates the service. |
| SafeMode | dword | 0 | If 1, attempt this service in Safe mode. Implied by ErrorControl=Critical. §2.6 |
| Identity | string | LocalService | Principal for the service token. §4.1 |
| RequiredPrivileges | multi_string | — | Privileges to keep; all others are removed. §4.5 |
| Requires | multi_string | — | Hard dependencies. §7.1 |
| Wants | multi_string | — | Soft dependencies. §7.1 |
| BindsTo | multi_string | — | Runtime coupling. §7.1 |
| Conflicts | multi_string | — | Mutual exclusion. §7.1 |
| OnFailure | string | — | Service to start when this one fails. §6.3 |
| ErrorControl | dword | 0 (Normal) | 0 Normal, 1 Critical. |
| RemainAfterExit | dword | 0 | Oneshot only: stay Completed after a successful exit. |
| SuccessExitCodes | multi_string | — | Non-zero exit codes treated as success. |
| ExecStartPre | multi_string | — | Commands run before the main binary, sequentially. §5.3 |
| ExecStartPost | multi_string | — | Commands run after readiness or successful exit. §5.3 |
| HookIdentity | string | — | Principal for the hook processes. Falls back to Identity. §4.1 |
| ExecReload | string | — | Reload command, or signal:<NAME>. Absent means SIGHUP. §6.5 |
| PreStartCheckTimeout | dword | 5 | Seconds before a filesystem check helper is killed. §3.5 |
| StartTimeout | dword | 30 | Seconds for the entire start sequence. §5.3 |
| StopTimeout | dword | 10 | Seconds after SIGTERM before SIGKILL. |
| WatchdogTimeout | dword | 0 | Seconds between expected WATCHDOG=1 pings; 0 disables. §6.6 |
| HealthCheck | string | — | Command run periodically. Exit 0 is healthy. §5.6 |
| HealthCheckInterval | dword | 30 | Seconds between health checks. |
| HealthCheckTimeout | dword | 5 | Seconds before a health check is killed and counted failed. |
| HealthCheckRetries | dword | 3 | Consecutive failures before the service is unhealthy. |
| RestartPolicy | dword | 1 (OnFailure) | 0 Never, 1 OnFailure, 2 Always. §6.4 |
| RestartMaxRetries | dword | 5 | Consecutive restarts before Failed. §6.4 |
| RestartWindow | dword | 120 | Seconds of sustained health that reset the restart counter. |
| RestartDelay | dword | 1 | Seconds before a restart; doubles each consecutive failure, capped at 60. |
| Readiness | dword | 0 (Notify) | 0 Notify, 1 Alive. Ignored for Oneshot. |
| NotifyAccess | dword | 0 (Main) | Who may send notifications. Main is the only mode. §10.5 |
| FdStoreMax | dword | 0 | Maximum descriptors held for the service; 0 disables the store. §10.6 |
| TimerPersistent | dword | 1 | Catch up a missed timer run after a reboot. §9.3 |
| TimerJitter | dword | 0 | Maximum random delay added to each firing. §9.4 |
| Environment | multi_string | — | KEY=VALUE pairs added to the environment. §5.5 |
| WorkingDirectory | string | / | Working directory for the process. |
| TTYPath | string | — | Terminal to attach as the standard streams and controlling terminal. §5.4 |
| RuntimeDirectories | multi_string | — | Private directories under /run, created before the main process. |
| LimitNOFILE | dword | — | RLIMIT_NOFILE. |
| LimitCORE | dword | — | RLIMIT_CORE, in bytes. |
| Conditions | multi_string | — | Start-time conditions; failure skips the service. §3.5 |
| Asserts | multi_string | — | Start-time assertions; failure fails the service. §3.5 |
| DisplayName | string | — | Human-readable name for status display. |
| Description | string | — | What the service does. |
| ServiceSecurity | binary | inherit | Descriptor controlling runtime operations on the service. §4.6 |
3.2.1 Registry types #
| Schema type | Registry type |
|---|---|
| string | REG_SZ, UTF-8 |
| multi_string | REG_MULTI_SZ, an ordered list |
| dword | REG_DWORD, 32-bit unsigned |
| binary | REG_BINARY |
A value whose registry type does not match the field's is a decode error, as is a dword carrying a value outside an enumerated field's range.
3.2.2 Schema version and forward compatibility #
Machine\System\Services\SchemaVersion is a dword, currently 1. peinit
creates it if it is absent (§2.3).
Unknown values on a service key are ignored, which is what lets the schema grow additively: a definition written for a newer peinit still loads on an older one, minus the fields it does not understand. A newer schema version does not prevent boot.
Known fields are the opposite. A known field appearing more than once in
a collected definition is a decode error rather than a last-one-wins,
because a definition that says two different things about the same field
has no defensible reading. Since names match case-insensitively, this
catches ImagePath and imagepath in the same key.
3.2.3 What a decode failure costs #
A definition that fails to decode fails that one service, and the answer differs by caller.
At boot the key is marked Failed with cause ValidationError and the
boot proceeds with every other definition. Anything that depended on the
failed service fails in turn through the ordinary dependency propagation
(§7.4), so the cost is bounded by what actually needed it.
On reload-config the whole read is rejected and the previous generation stays in place (§10.4). That is not an inconsistency: a reload is atomic and has a working configuration to fall back to, where a boot has none. Refusing everything is the safe answer only when there is something to keep.
3.3 Field Formats
Peios / Advanced Peios / peinit / The Service Model
Rules that apply to how a field's value is written, as distinct from what it means.
3.3.1 Strings #
A string field that is present is non-empty, unless this section says otherwise for that field. Three fields treat the empty string as absence:
Identity— empty is the same as absent, and defaults toLocalService.HookIdentity— empty is the same as absent, and falls back to the service'sIdentity.DisplayNameandDescription— empty is the same as absent.
WorkingDirectory, when present, is a non-empty absolute path. Whether
it exists, is a directory, and is reachable is checked when the service
starts, not when the definition is read.
TTYPath is checked for emptiness before absoluteness: an empty value
means no terminal, and a non-empty relative path is rejected.
3.3.2 Identity and HookIdentity #
Either a well-known principal name — SYSTEM, LocalService,
NetworkService — matched case-insensitively and canonicalised, or a
literal SID string such as S-1-5-18. Anything else is passed to authd
verbatim to resolve (§4.3).
3.3.3 RequiredPrivileges #
Privilege names, matched case-sensitively against the published
privilege table. A name that does not match exactly fails token
materialisation and therefore the service start, so SeTCBPrivilege
does not start a service that SeTcbPrivilege would.
3.3.4 RuntimeDirectories #
Each entry names one directory directly under /run. Entries are
non-empty relative names; an entry equal to . or .. is rejected, as
is any entry containing /, \, a NUL, or a control character. A dot
inside a name is fine — app.sock.d is a valid entry.
For an entry foo, peinit creates /run/foo immediately before
launching the service's main process, with a descriptor granting full
access to SYSTEM, Administrators, and the service's own SID. Hook
processes inherit the service's environment and identity rules but do
not cause provisioning — the directories belong to the main start.
If creation or descriptor assignment fails, the start fails with
ParentSetupFailure.
peinit does not remove runtime directories when a service stops. /run
is a boot-scoped tmpfs and the next boot clears it.
3.3.5 Environment #
Each entry is KEY=VALUE, with a non-empty key containing no NUL. An
entry that does not split into that shape is a decode error.
3.3.6 SuccessExitCodes #
Each entry is a decimal integer from 0 to 255 — a process exit code. Signal names and ranges are not accepted. Code 0 is always success and does not need listing. Duplicates are collapsed.
3.3.7 Dependency and handler names #
Entries in Requires, Wants, BindsTo and Conflicts, and the
value of OnFailure, are validated as service names when the definition
is read. A dependency naming something outside [A-Za-z0-9._-] is a
decode error rather than an unresolved dependency discovered later —
the difference being that a typo containing an illegal character is
caught immediately, while a typo that is still a legal name is caught at
graph validation as a missing target.
3.3.8 Timeouts and intervals #
Every timeout and interval in the schema is in whole seconds unless the
field says otherwise. The two sd_notify fields that carry durations,
WATCHDOG_USEC and EXTEND_TIMEOUT_USEC, are in microseconds because
that is what the protocol specifies.
3.3.9 Identifiers peinit generates #
Job identifiers, operation identifiers, and every other GUID peinit mints are UUIDv7. UUIDv7 is time-ordered, so identifiers sort by creation time — which is what keeps eventd's time-range and recency queries over jobs and operations cheap.
3.4 Triggers
Peios / Advanced Peios / peinit / The Service Model
A trigger says when a service starts by itself. Triggers are independent of service type: a Simple service can have a timer, and a Oneshot can start at boot.
Triggers is a multi_string, each entry either type or
type:argument.
| Trigger | Form | Meaning |
|---|---|---|
| Boot | boot | Start during the Phase 2 boot sequence. |
| Deferred boot | boot:settled | Start once the boot set has settled, or the deadline expires. §2.5 |
| Timer | timer:<schedule> | Start on a schedule. §9.1 |
A service with no triggers is demand-only: it starts only when something
asks for it, whether an administrator, a dependency, or an OnFailure
handler.
Multiple triggers of the same type are allowed. A service with
["timer:*-*-* 02:00:00", "timer:*-*-* 14:00:00"] runs at 2am and 2pm,
and each trigger is independent — its own timerfd, its own next-firing
computation, its own history.
3.4.1 Arity is enforced #
boot takes no argument. boot:<something> accepts only the
sub-triggers in the table above, and any other value is malformed rather
than an unknown trigger type to be ignored. timer with no schedule is
malformed, as is timer: with an empty one.
That strictness is the point. Unknown values on a service key are
ignored for forward compatibility, and if unknown trigger types were
ignored too, boot:setled would be silently accepted and the service
would simply never start. Instead it fails to decode, loudly.
3.4.2 Disabled #
Disabled=1 suppresses automatic activation and nothing else. No
trigger fires: not boot, not boot:settled, not a timer, and not any
trigger type added later. A disabled service's timers are neither armed
nor serviced.
The definition is still loaded into the in-memory model, and an explicit
start still works. To stop a service being started at all, deny
SERVICE_START in its ServiceSecurity descriptor — that is an access
control question, not a trigger question, and answering it with the
Disabled flag would mean a flag that anyone who can write the key can
clear.
Enabling and disabling are registry writes performed by administrative tools, not peinit commands. peinit picks the change up through a registry change notification, or on the next reload-config.
3.4.3 Extensibility #
The type:argument shape is designed to grow. Path, device and event
triggers slot into the same array with no schema change, because a
trigger is a string in a list rather than a field of its own.
3.5 Conditions and Asserts
Peios / Advanced Peios / peinit / The Service Model
Both are start-time checks in the same type:argument form, over the
same four check types. What differs is the consequence of a failure.
| Check | Form | Passes when |
|---|---|---|
| path | path:<path> | The path exists, of any type. |
| file | file:<path> | A regular file exists there. |
| directory | directory:<path> | A directory exists there. |
| registry | registry:<key> | The registry key exists. |
Conditions describe when a service applies. A failed condition skips the service: it transitions to Skipped, which satisfies its dependents, because a service that does not apply has succeeded by not needing to run.
Asserts describe what a service needs. A failed assert fails the
service, with cause AssertionError — the service was expected to run
and a precondition it depends on is missing.
All entries of a kind are AND'd. Conditions are evaluated first; asserts only if every condition passed. Both are evaluated before dependency resolution and before any pre-exec hook, and an entry with an empty argument or an unrecognised type is a decode error.
3.5.1 Evaluation without blocking #
peinit is single-threaded PID 1 and its event loop cannot block while a check runs. Two constraints follow, and they are why the check types behave differently from one another.
3.5.1.1 Registry checks are cache-only #
A registry: check is evaluated against the in-memory model, never by a
live registry read. It can therefore only name a key peinit already
caches — under Machine\System\Services\ or Machine\System\Init\.
Naming any other key is a decode error, caught at load rather than at
start.
Within that, resolution is narrower than the load-time check suggests.
A registry:Machine\System\Services\<name> check is true when that
service exists in the model, which makes it a subkey-existence test
rather than a general key-existence test.
3.5.1.2 Filesystem checks run in a helper #
stat() can block uninterruptibly on hung I/O — a dead NFS mount, a
failing disk controller — so peinit does not call it from the event
loop. It forks a short-lived helper into a dedicated checks/ cgroup
under the service's tree, using the same clone3 path as any other
child. The helper stats the paths and reports over a non-blocking pipe;
peinit waits on the pipe and the helper's pidfd through epoll, and never
blocks.
PreStartCheckTimeout, default 5 seconds, bounds the helper. A check
that does not report in time is treated as not satisfied — the
fail-safe direction, so a condition skips the service and an assert
fails it. peinit then SIGKILLs the helper's cgroup and unregisters the
result descriptor, and the event loop is never held up by a hung check.
3.5.2 When results are computed #
Checks are evaluated once, before the service's dependencies start, and the result is cached for the rest of that activation. A service that waits a long time for a dependency starts on the answer that was true when the wait began, not on a fresh one.
3.6 Command Strings
Peios / Advanced Peios / peinit / The Service Model
Four fields hold executable commands: ExecStartPre, ExecStartPost,
the command form of ExecReload, and HealthCheck. All four are parsed
the same way.
3.6.1 Parsing #
The string is split on whitespace into an argv, with double quotes grouping. There is no shell — no expansion, no substitution, no globbing, and peinit never invokes one. A command that needs shell features is wrapped in a script.
Whitespace means exactly six characters: space, horizontal tab, line feed, carriage return, form feed and vertical tab. Every other Unicode whitespace code point is an ordinary argument character, which keeps the split independent of the Unicode version peinit was built against.
Double quotes group text into one argv element and are not retained in
the result. Grouping may happen inside an argument, so
--name="hello world" becomes the single entry --name=hello world.
An empty quoted string is preserved as an empty argv entry.
Backslash has no escape semantics and is copied literally. A single quote is an ordinary character.
An empty or whitespace-only command is invalid, and so is an unclosed double quote.
3.6.2 The executable #
For all four fields, argv[0] is the executable path and begins with /.
Relative names, empty names and PATH-searched execution are decode
errors. Everything after argv[0] is an opaque string and is not path
validated.
3.6.3 ExecReload signals #
ExecReload may instead name a signal, as signal:<NAME>. The name is
an exact canonical Linux signal name. Numeric values, realtime signal
expressions, aliases, lowercase spellings and names with surrounding
whitespace are all invalid.
SIGKILL and SIGSTOP are invalid for reload, because a service cannot
handle either as a request to re-read its configuration.
The accepted set is the standard non-realtime Linux signals other than those two:
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
SIGBUS, SIGFPE, SIGUSR1, SIGSEGV, SIGUSR2, SIGPIPE,
SIGALRM, SIGTERM, SIGSTKFLT, SIGCHLD, SIGCONT, SIGTSTP,
SIGTTIN, SIGTTOU, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM,
SIGPROF, SIGWINCH, SIGIO, SIGPWR, SIGSYS.
Names are validated when the definition is read and again before
delivery, and mapped to host signal numbers at that point. ExecReload
absent means SIGHUP.
3.7 Configuration Generations
Peios / Advanced Peios / peinit / The Service Model
peinit operates on snapshots, not on a live view of the registry. Two generation concepts govern when a registry change takes effect.
3.7.1 The in-memory model #
peinit maintains a full in-memory model of every service definition. The registry is read synchronously exactly twice: during Phase 2 boot, before meaningful supervision has started, and during a reload-config, which an administrator initiated and which is bounded. At all other times peinit works from the model.
Change notifications arrive as events on a pollable descriptor. peinit
subscribes to Machine\System\Services\ and Machine\System\Init\ at
boot, using the LCS watch mechanism — a persistent subscription that
delivers change events on a key descriptor, with an OVERFLOW event when
the kernel-side queue is exceeded (Peios Kernel TRM §5).
Any drained watch event triggers a full reload of the configuration, not a targeted re-read of the changed key. That covers the OVERFLOW case by construction, and it is why an administrator writing one value causes every definition to be re-read.
3.7.2 The boot generation #
At the start of Phase 2, peinit reads all definitions, builds the dependency graph and validates it. The plan and the graph are fixed at that point, and the boot executes against them.
The watches are armed as the event loop starts, which is after the plan is fixed but while boot-plan services are still starting. A registry write during that window — from an install script, a post-hook, a package transaction — triggers a reload like any other. Services that are already running keep their pinned definition; a boot-plan service that has not started yet picks up the new one.
3.7.3 The activation generation #
When peinit starts a service, it snapshots that service's definition. The snapshot governs the whole start lifecycle: pre-exec hooks, the token request, the readiness timeout, the initial health checks. A field changed while the service is Starting does not take effect until the next start.
A service in Inactive or Failed has no activation snapshot, so starting it uses the current model. That gives the expected behaviour for the common edits:
- A new service entry is available once the change notification is processed.
- A timer change on an inactive service takes effect at the next trigger evaluation.
- A dependency change takes effect on the next start for an inactive service, and on the next restart for an active one.
3.7.4 Field mutability #
Which class a field falls into depends on when its value is consumed.
3.7.4.1 Pinned to the running definition #
A change takes effect only when the service is restarted:
ImagePath, Type, Identity, RequiredPrivileges, ErrorControl,
RemainAfterExit, Triggers, Disabled.
Triggers and Disabled are pinned only while the service is running.
On a service that is not running, both take effect as soon as the
notification is processed — which is what arms a timer added to an
inactive service.
3.7.4.2 Applied on the next start #
A change takes effect at the next start or explicit graph reload, not while services are running:
Requires, Wants, BindsTo, Conflicts, OnFailure, Conditions,
Asserts.
3.7.4.3 Reloaded at runtime #
A change takes effect at the next relevant event, with no restart:
Arguments, SuccessExitCodes, every timeout and retry value
(StartTimeout, StopTimeout, WatchdogTimeout, RestartDelay,
RestartMaxRetries, RestartWindow, PreStartCheckTimeout),
HealthCheck and its three parameters, RestartPolicy, Environment,
WorkingDirectory, ExecStartPre, ExecStartPost, ExecReload,
HookIdentity, Readiness, NotifyAccess, LimitNOFILE,
LimitCORE, FdStoreMax, TTYPath, RuntimeDirectories,
TimerPersistent, TimerJitter, SafeMode, DisplayName,
Description, and ServiceSecurity.
ServiceSecurity is the one whose reload is immediately observable:
a change takes effect on the very next control request against that
service (§4.6).
3.8 Service Removal
Peios / Advanced Peios / peinit / The Service Model
When a definition disappears from Machine\System\Services\, peinit
learns of it through the ordinary change-notification path. What happens
next depends on whether anything is running.
3.8.1 Not running #
An entry in Inactive, Failed, Completed, Skipped or Abandoned is discarded immediately. There is no process to consider.
3.8.2 Running #
An entry in Active, Starting, Reloading, Backoff or Stopping is not killed. The running process is a job, and a job's lifecycle is independent of the definition that produced it — removing a definition stops future management, it does not terminate work in progress.
peinit marks the entry definition-removed and keeps the cached
definition, solely to go on supervising the instance it already has.
When that instance exits it is not restarted — RestartPolicy is moot,
because there is nothing left to restart from — and peinit then discards
the entry.
While an entry is definition-removed:
- it keeps satisfying its dependents for as long as the instance is alive, because it is still running;
stopis accepted, so an administrator can drain it cleanly, using the cachedStopTimeout;start,restartandreloadare rejected withUNKNOWN_SERVICE— there is no definition to work from;statusreports the current runtime state withdefinition_removedset, so the draining instance is visible rather than silent.
definition-removed is a flag on the existing runtime state, not a
state of its own. The state machine (§6.1) and the command × state
matrix (§10.3) are unchanged by it.
Once the instance exits or is stopped, the entry — including anything in
its fd store (§10.6) — is discarded. A dependent that Requires the
removed service keeps being satisfied while the instance runs; after the
entry is discarded, the dependent's next start sees an unresolved
dependency and takes the ordinary validation path.
4.1 Token Materialisation
Peios / Advanced Peios / peinit / Service Identity
Every service process runs with a KACS token that determines its
identity and its access rights. peinit obtains or creates that token and
installs it on the child before exec. It never shares its own token —
even an Identity=SYSTEM service receives a separately materialised
token of its own.
Which route the token comes from depends on the Identity field:
| Identity | Source | Mechanism |
|---|---|---|
SYSTEM | Minted by peinit from its own identity | kacs_create_token. §4.2 |
| Anything else | authd | The token request flow. §4.3 |
| Absent or empty | authd | Defaults to LocalService. |
peinit reads its own token with kacs_open_self_token requesting the
real token rather than any impersonation, and opens it query-only: it is
a template to copy from, never a thing to hand out.
4.1.1 Where a token is materialised #
Materialisation happens at the point of use, per launched process, not once per service. A service that runs a pre-exec hook, a main process, and then a health check materialises three tokens.
| Context | Identity used |
|---|---|
| Main process | Identity |
ExecStartPre / ExecStartPost | HookIdentity if set, otherwise Identity |
| Health checks | Identity, always |
ExecReload external command | Identity, always |
| Ad-hoc jobs | The token JFS captured from the submitter |
Health checks and reload commands deliberately do not honour
HookIdentity. A health check reports on the service's own health and
should see what the service sees; a reload command acts on the running
service. HookIdentity exists for setup work — creating directories in
privileged locations, running a migration — which is a different job
from either.
If materialisation fails at any point — authd unreachable, an identity
that cannot be resolved, a KACS error — no child exists yet, and the
start fails with ParentSetupFailure for the main process or
PreHookFailure for a hook.
4.2 The SYSTEM Path
Peios / Advanced Peios / peinit / Service Identity
For Identity=SYSTEM, peinit mints a token itself. This is what breaks
the bootstrap circle: registryd, lpsd, authd and eventd all need tokens,
and authd — the thing that mints tokens — is one of them.
4.2.1 Minting #
peinit reads its own token as a template and builds a new primary
token carrying the same identity: user SID S-1-5-18, the same group
list, the same privilege set, the same integrity level. The mint
requires SeCreateTokenPrivilege, which the boot SYSTEM token carries;
the kernel refuses the call with EPERM otherwise.
Two details of the copy matter.
The logon session comes from the token's statistics. peinit takes
the auth_id from the source token's TokenStatistics — not from the
independent interactivity_scope field, and not from a hard-coded
well-known SYSTEM LUID. The minted token therefore stays associated
with the real SYSTEM logon session peinit was given at boot, while
carrying its own interactivity scope, which is zero for a platform
service. Substituting either of the other two values would associate
platform services with a session that does not exist.
The logon SID group is dropped from the copy. The kernel re-appends the session's logon SID when it creates the token, and rejects a create whose group list already contains it. So peinit filters that group out of the template before building.
peinit also asserts that its own token is a primary token and that its
user SID really is S-1-5-18 before minting, and fails the start with a
message naming what it found otherwise. PID 1 minting from something
that is not the boot SYSTEM token is not a situation to proceed from.
The minted token is fully independent. The privilege restriction that follows (§4.5) operates on it alone and cannot affect peinit's own.
4.3 The authd Path
Peios / Advanced Peios / peinit / Service Identity
For any identity other than SYSTEM, the token comes from authd. peinit
does not resolve identities, does not know whether a principal is local
or from a domain, and does not want to: routing is authd's whole
purpose.
What peinit requires of authd is:
- peinit sends the
Identityvalue verbatim. - authd routes it to an identity source — a built-in for the well-known principals, lpsd for local accounts, a connector for domain accounts.
- The source returns the principal's user SID and group SIDs.
- authd mints a KACS token, creates a logon session, adds the per-service SID to the group list, and returns the token descriptor to peinit.
Every non-SYSTEM service start depends on this, and every one of them fails if authd is unavailable when the token is needed. Platform services are unaffected, because they never take this route.
4.3.1 The interface is authd's #
The steps above describe what peinit needs, not how it asks. authd owns the request and response schema, the socket path, and the descriptor passing mechanism. No authd specification exists yet — authd's design was deliberately deferred until KACS and the registry had settled — so this path is not implementable from this manual alone.
4.3.2 The current implementation #
peinit's authd client is a placeholder. It ignores the requested identity and returns a freshly minted SYSTEM token, taking the §4.2 path for every service.
The consequence is that every service currently runs with user SID
S-1-5-18 and peinit's full privilege set, whatever its definition
says. RequiredPrivileges still applies, and is currently the only
thing that reduces what a service can do. A service that does not set it
runs fully privileged.
Two things follow that are worth stating plainly, because both are easy to reason wrongly about:
- Status output reports the declared identity, not the effective
one. The job's resolved identity string is what appears in a status
query and in a
job.createdevent, and it saysLocalServicefor a service running on a SYSTEM token. - The per-service SID is still correct. peinit computes it from the service name and adds it to the minted token (§4.4), so per-service ACLs behave as designed even while the user SID does not.
The placeholder also interacts with socket protection in a way worth knowing about. peinit's sockets are reachable only by SYSTEM (§13.3), so a service on a correctly resolved non-SYSTEM token could not reach the notification socket to report readiness. Today every service holds a SYSTEM token, so the question does not arise — which means the two have to be resolved together rather than one at a time.
4.4 Per-Service SIDs
Peios / Advanced Peios / peinit / Service Identity
Every service token carries a SID derived from the service's name, in its group list, alongside whatever the principal's own identity brings.
The derivation is the one KACS defines (Peios Kernel TRM §3.2). The
authority is S-1-5-80. The service name is uppercased and encoded as
UTF-16LE, SHA-1 is taken over those bytes, and the 20-byte digest is
split into five little-endian 32-bit sub-authorities:
S-1-5-80-<sub1>-<sub2>-<sub3>-<sub4>-<sub5>
peinit computes this itself, from the name alone, with no involvement from anything else. authd computes the same value independently when it mints a token, and the two implementations are pinned against each other by a shared test vector.
4.4.1 Why they exist #
Per-service SIDs are what make access control useful when services share
a principal. Every platform daemon runs as SYSTEM, and every service
with no Identity runs as LocalService; without something to tell
them apart, an ACL could grant a right to "LocalService" and thereby
grant it to a dozen unrelated services.
With a per-service SID, an ACE can name one specific service. It costs nothing — no account, no registry entry, no allocation — because it is a hash of a name that already has to be unique.
They are load-bearing in more than access checks. peinit uses the
service SID directly when it provisions a service's runtime directories,
stamping /run/<name> with a descriptor that grants full access to
SYSTEM, Administrators, and that service's SID and nothing else (§3.3).
4.4.2 The uppercasing rule #
The name is uppercased before encoding, using full Unicode case mapping
— the one that expands ß to SS and fi to FI rather than mapping
each code unit in place. peinit uppercases in the code-point domain,
before the UTF-16 encoding, so any expansion happens first.
For an ASCII service name, which is every real service, the choice is invisible. It becomes visible only for a name containing a character whose uppercase form is longer than itself.
4.5 Privilege Restriction
Peios / Advanced Peios / peinit / Service Identity
RequiredPrivileges is a list of the privileges a service needs.
Everything else is removed from its token before exec.
4.5.1 Subtractive only #
peinit removes privileges. It never adds one, on either path — there is
no code that constructs anything but a removal. A service cannot acquire
a privilege by naming it in RequiredPrivileges; if the token it was
given does not have it, listing it changes nothing.
The removal targets the token's present bitmask, through
KACS_IOC_ADJUST_PRIVS on the token descriptor — one kacs_priv_entry
per removed privilege, carrying the privilege-removed attribute. The
right required on the descriptor is KACS_TOKEN_ADJUST_PRIVS, which a
freshly minted token always has.
peinit does not use KACS_IOC_RESTRICT. That builds a restricted-SID
token — a different mechanism with different semantics — and does not
touch the privilege bitmask at all. It is available in the bindings and
would be a plausible-looking mistake.
Removing a privilege clears its present, enabled and enabled-by-default bits together, and irreversibly. The privileges that survive keep the enable state their source gave them: peinit does not enable, disable or re-order anything. Enable policy belongs to whoever minted the token.
peinit iterates all sixty-four privilege bits rather than only the ones it has names for, so a privilege this build does not know about is stripped along with the rest. The safe direction is to remove what was not asked for, including what cannot be named.
If RequiredPrivileges is absent, peinit does not query or adjust the
token at all, and the source's default privilege set stands unchanged.
4.5.2 Names #
Privilege names are matched case-sensitively against the published privilege table. A name that does not match exactly is not silently ignored: it fails token materialisation, and therefore fails the service start.
Two privileges KACS enforces are absent from the published table —
SeTakeOwnershipPrivilege and SeRelabelPrivilege — and so cannot be
named in RequiredPrivileges at all. A service that needs either
declares nothing and takes the source token's defaults, or fails to
start if it tries to name one.
4.6 Service Security Descriptors
Peios / Advanced Peios / peinit / Service Identity
A service carries two independent descriptors, and they answer different questions.
The registry key descriptor on Machine\System\Services\<name>
controls who may read and write the service's definition. It is
enforced by LCS at key-open time and is not peinit's concern — peinit
reads definitions as SYSTEM.
The ServiceSecurity descriptor controls who may perform runtime
operations on the service through the control interface. It is stored
as a binary ServiceSecurity value on the same registry key, but
enforced by peinit rather than by LCS.
The two are genuinely independent. An administrator might be able to query a service's status without being able to read its configuration, or the reverse. Runtime control and configuration access are separate concerns and there is no reason for one to imply the other.
4.6.1 Access rights #
| Right | Bit | Grants |
|---|---|---|
SERVICE_QUERY_STATUS | 0x0001 | Query state, PID, cause, health, warnings. |
SERVICE_START | 0x0002 | Start the service. |
SERVICE_STOP | 0x0004 | Stop the service. |
SERVICE_INTERROGATE | 0x0008 | Reload the service. |
SERVICE_ALL_ACCESS | 0x000F | The union of the four. |
Restart requires SERVICE_START and SERVICE_STOP together. Reset
requires SERVICE_STOP, because clearing a Failed or Abandoned state is
the tail of stopping something rather than the head of starting it.
The generic mapping peinit passes to AccessCheck:
| Generic right | Maps to |
|---|---|
GENERIC_READ | SERVICE_QUERY_STATUS |
GENERIC_WRITE | SERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE |
GENERIC_EXECUTE | SERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE |
GENERIC_ALL | SERVICE_ALL_ACCESS |
4.6.2 Inheritance and the default #
A service whose definition carries no ServiceSecurity value takes the
one on Machine\System\Services itself. The lookup is a single step to
that key, not a walk up the hierarchy, which is exact for the flat
layout definitions actually use.
If that key has no ServiceSecurity either, peinit applies a built-in
default:
O:SY G:SY D:(A;;GA;;;SY)(A;;0x0005;;;BA)
SYSTEM gets full access. Administrators get SERVICE_QUERY_STATUS and
SERVICE_STOP — query and stop, but not start and not reload. The
asymmetry is deliberate: stopping something that is misbehaving is a
containment action, and starting something is a change.
4.6.3 The check #
When a control command arrives, peinit:
- Takes the caller's token, captured when the connection was accepted.
- Resolves the target service and its ServiceSecurity descriptor. A
command naming no definition and no addressable definition-removed
entry returns
UNKNOWN_SERVICE; peinit does not invent a descriptor to check against. - Calls AccessCheck with the caller's token, that descriptor, the generic mapping above, and the right the command needs.
- On denial, returns
ACCESS_DENIEDand records the attempt as anaccess.deniedevent carrying the caller's SID, the target, the requested right by name, the requested access bits and the granted bits. - On grant, proceeds.
4.6.4 Hot reload #
ServiceSecurity changes take effect on the next control request, with
no restart. A registry change notification triggers a configuration
reload, and the reload re-reads every descriptor. There is no cached
decision to invalidate — the check runs against the current descriptor
every time.
4.6.5 Filtering, not denying #
list returns only the services the caller has SERVICE_QUERY_STATUS
on. Services the caller cannot query are omitted, not denied: a
caller with no query rights anywhere receives an empty list and a
successful response. The denials are recorded as audit events rather
than surfaced to the caller, because reporting them would answer the
question the filtering exists to avoid answering.
4.7 The Control Descriptor
Peios / Advanced Peios / peinit / Service Identity
Two operations are not about any one service: shutting the system down,
and re-reading the configuration. They are checked against peinit's own
descriptor, stored at Machine\System\Init\ControlSecurity as a binary
value.
4.7.1 Access rights #
| Right | Bit | Grants |
|---|---|---|
SYSTEM_SHUTDOWN | 0x0001 | Initiate poweroff, reboot or halt. |
SYSTEM_RELOAD_CONFIG | 0x0002 | Re-read all definitions from the registry. |
The generic mapping:
| Generic right | Maps to |
|---|---|
GENERIC_READ | nothing |
GENERIC_WRITE | SYSTEM_RELOAD_CONFIG |
GENERIC_EXECUTE | SYSTEM_SHUTDOWN |
GENERIC_ALL | both |
GENERIC_READ maps to nothing because there is nothing to read: the
control descriptor governs two actions and no queries. A grant of
GENERIC_READ on it is not an error, it simply conveys no access.
4.7.2 The default #
Absent a value in the registry, peinit applies:
O:SY G:BA D:(A;;0x0003;;;SY)(A;;0x0003;;;BA)
SYSTEM and Administrators both get shutdown and reload-config. Unlike the ServiceSecurity default, this one is symmetric — an administrator who can stop services one at a time can already stop the system, so withholding shutdown would be theatre.
4.7.3 Loading #
peinit loads the descriptor during Phase 2 boot and hot-reloads it on registry change notification, on the same path as the service descriptors. Until it is loaded — during Phase 1 and the early part of Phase 2 — the built-in default applies, which matters because the control socket exists from Phase 1 infrastructure setup onwards.
5.1 The Cgroup Tree
Peios / Advanced Peios / peinit / Starting a Service
peinit uses cgroups v2 for exactly two things: knowing which processes belong to a service, and killing all of them at once. It does no resource accounting and sets no limits.
Every service gets a tree:
/sys/fs/cgroup/peinit/<cgroup-id>/ service root
/sys/fs/cgroup/peinit/<cgroup-id>/main/ the main process
/sys/fs/cgroup/peinit/<cgroup-id>/hooks/ hooks and reload commands
/sys/fs/cgroup/peinit/<cgroup-id>/health/ health check invocations
/sys/fs/cgroup/peinit/<cgroup-id>/checks/ pre-start filesystem check helpers
The sub-cgroups satisfy cgroups v2's "no internal processes" rule, which applies whenever controllers are enabled, and give hooks and probes containment of their own so that killing one does not touch the service.
They are not all created at once. The root and hooks/ are created when
the first thing needs them — the first pre-exec hook, if there is one —
and main/ and health/ when the main process launches.
5.1.1 The cgroup id #
<cgroup-id> is the service name with every byte outside
[A-Za-z0-9._-] percent-encoded as % plus two uppercase hex digits.
Service names are already restricted to that set (§3.1), so in practice
the id equals the name.
The encoding is a defensive guarantee that distinct names always map to
distinct, cgroup-safe ids. % is not itself in the safe set, so it
escapes to %25 and the encoding is prefix-free. That is what makes it
injective, unlike a plain substitution of / for -, under which a/b
and a-b would collide.
The id is internal. The name a user sees is unchanged.
5.1.2 Generations #
A cgroup whose processes survived SIGKILL cannot be removed — rmdir on
it fails with EBUSY. When peinit detects that a service's tree still
has live processes after the post-kill deadline, it records the leak and
increments the service's cgroup generation. The next start uses a fresh tree:
/sys/fs/cgroup/peinit/<cgroup-id>.gen<N>/
Old leaked trees persist until the next reboot.
The generation counter advances once per recorded leak rather than once
per restart, and leaks are deduplicated by path and kind. A single
failed start can record two — one for hooks/ and one for the service
tree — so the number can advance by more than one at a time.
Because . is a legal character in a service name and the generational
suffix uses one, the tree path is not injective the way the id is: a
service literally named app.gen1 and generation 1 of a service named
app resolve to the same directory.
5.2 Pre-Start Evaluation
Peios / Advanced Peios / peinit / Starting a Service
Before anything is forked, peinit evaluates the service's conditions and asserts (§3.5). The definition comes from the in-memory cache, so this never touches the registry.
- Conditions are evaluated. Any failure transitions the service to Skipped and abandons the start. Skipped satisfies dependents.
- If every condition passed and the service has asserts, they are
evaluated. Any failure transitions the service to Failed with cause
AssertionErrorand abandons the start.
Only when both pass does the pre-exec sequence continue.
5.2.1 Where the transition happens #
For a fresh start from Inactive, the evaluation gates the transition: the service becomes Starting only after the checks pass, so a skipped service goes straight Inactive → Skipped.
For an activation that is already Starting — the start leg of a restart, for instance — the transition has already happened, and the evaluation gates further progress instead. A restart whose conditions no longer hold therefore passes through Starting on its way to Skipped, where a fresh start would not.
5.2.2 Two check kinds, two mechanisms #
registry: checks resolve against the in-memory model. Since a
non-cacheable key is rejected when the definition is read, this
evaluation never needs a live read.
Filesystem checks — path:, file:, directory: — run in a forked
helper. peinit clones it with CLONE_PIDFD | CLONE_INTO_CGROUP into the
service's checks/ sub-cgroup, exactly as it launches anything else.
The helper stats the paths and writes the results to a non-blocking
pipe; peinit watches the pipe and the helper's pidfd through epoll.
PreStartCheckTimeout bounds the helper, at 5 seconds by default. On
expiry every check still outstanding is marked not satisfied — the
fail-safe direction — and re-evaluated on that basis, so a condition
skips the service and an assert fails it. peinit then kills the helper's
cgroup and unregisters the result descriptor.
A helper that survives the kill, stuck in uninterruptible sleep, has its cgroup abandoned and recorded exactly as a leaked hook or health cgroup is (§5.7).
5.2.3 Caching #
The results are computed once, before the service's dependencies are started, and reused for the rest of that activation. A service that waits a long time on a dependency starts on the answer that was true when the wait began.
Filesystem checks are gathered in one helper run, from the conditions and the asserts together. There is only ever one run — the completion path evaluates both lists against the results it gets back, and there is no mechanism to ask for a second — so both lists' paths have to be stat'd before it. A path named in both is stat'd once.
A check with no result counts as not satisfied, which is the fail-safe direction for a timeout and is why the gather has to be complete: an omitted path is indistinguishable from a path that is not there.
5.3 The Pre-Exec Sequence
Peios / Advanced Peios / peinit / Starting a Service
Everything between "peinit decides to start service X" and "X's binary is running". The service is in Starting throughout.
5.3.1 Step 1: Arm the start timeout #
StartTimeout covers the entire remaining sequence — pre-hooks, fork,
exec, and the readiness wait. A single deadline, measured from the
operation's creation rather than from this point, bounds all of it.
Expiry aborts the start and kills the service's cgroup tree. The cause
recorded depends on where the deadline landed: a timeout during
pre-hooks is PreHookFailure; one during the readiness wait or the main
process's execution is ReadinessTimeout.
5.3.2 Step 2: Provision runtime directories #
If the definition lists RuntimeDirectories, peinit creates each one
under /run and applies its descriptor (§3.3) before anything else in
the launch. Failure classifies as ParentSetupFailure.
5.3.3 Step 3: Run pre-exec hooks #
Each ExecStartPre command runs in sequence, forked into hooks/. A
token is materialised for each at the point of use, from HookIdentity
if set and Identity otherwise (§4.1); a failure there fails the hook
and the service with PreHookFailure.
Any hook exiting non-zero kills the entire service cgroup tree —
cleaning up whatever grandchildren the hook left — and fails the service
with PreHookFailure.
When every hook has succeeded, peinit kills the hooks/ sub-cgroup
before the main process starts, so a hook that forked something into the
background does not become part of the service.
5.3.4 Step 4: Materialise the service token #
The main process's token, per §4.1. A failure means no child exists and
the service fails with ParentSetupFailure.
If a later parent-side step fails before the fork, peinit closes the token descriptor exactly once. A failure to close is retained as cleanup evidence and does not change how the start is classified — the classification describes what went wrong with the start, and a failed close is a separate fact about the same failure.
5.3.5 Step 5: Create the cgroups and the error pipe #
The main/ and health/ sub-cgroups are created, and a
pipe2(O_CLOEXEC) error pipe. The parent keeps the read end,
non-blocking; the child will hold the write end.
The pipe is how the child reports a failure it hits after fork but
before exec. If exec succeeds, the write end closes automatically —
that is what O_CLOEXEC is for — and the parent reads EOF, which means
setup succeeded. If setup fails, the child writes a structured error
first.
The payload is exactly eight bytes, written with one write(2):
| Bytes | Content |
|---|---|
| 0–3 | little-endian u32, the child setup step identifier |
| 4–7 | little-endian i32, a positive Linux errno |
The child writes at most one payload — the first reportable failure —
and then exits. The parent treats a non-EOF payload that is not exactly
eight bytes, or carries an unknown step identifier, or an errno of zero
or less, as malformed evidence and fails closed with PreExecFailure.
The step identifiers:
| Id | Step |
|---|---|
| 1 | Close the read end of the error pipe |
| 2 | Set the standard streams |
| 3 | Reset the signal environment |
| 4 | Install the service token |
| 5 | Set the resource limits |
| 6 | Set oom_score_adj |
| 7 | Set the working directory |
| 8 | reserved |
| 9 | Confirm NOTIFY_SOCKET |
| 10 | Inject stored descriptors |
| 11 | Exec the binary |
| 12 | Create a session |
| 13 | Acquire the controlling terminal |
Identifier 8 is reserved and never emitted: the environment is built in
the parent and applied by execve, so there is no step in the child
that could fail. Identifiers 12 and 13 are the two terminal steps, which
occur only for a service with a TTYPath.
If pipe2 fails, no child exists and the service fails with
ParentSetupFailure.
5.3.6 Step 6: Fork #
clone3(CLONE_PIDFD | CLONE_INTO_CGROUP), targeting main/. This does
two things atomically: it returns a pidfd for the child, and it places
the child directly into main/ at creation. There is no window in which
the child exists without a pidfd, and none in which it runs or execs in
peinit's own cgroup.
Placing the child at creation also avoids the alternative, which would
be having the child write its own PID into cgroup.procs — something
its post-installation token could not do.
A clone3 failure means no child exists: ParentSetupFailure, with
EMFILE/ENFILE, EAGAIN and ENOMEM the usual causes.
Immediately after clone3 returns, in the parent and on either outcome,
peinit closes the token descriptor and the main/ cgroup descriptor,
exactly once each. The child relies on close-on-exec and its own exit
instead. A failure to close is cleanup evidence and does not change how
the start is classified.
5.3.7 Step 7: The parent after the fork #
- Close the write end of the error pipe.
- Register the read end with the event loop. peinit does not block on it — signals, control traffic, timers and log pipes stay serviceable while the child's setup is pending.
- When the read end becomes readable or hangs up, read it
non-blocking:
- Would block: leave the source registered, the job stays in pending setup.
- EOF: exec succeeded. Record the pidfd on the job, emit
job.started, and only then apply readiness side effects such as Simple/Alive activation. - Data: setup failed. Parse the step and errno, log the specific
failure, fail the service with
PreExecFailure.
While setup is pending the job is not Running, and no dependent that waits on this service's readiness is released.
5.3.8 Steps 8 and 9 #
The child's own path is §5.4. What follows exec is the readiness wait and the post-readiness work:
- Simple with
Readiness=Notify: wait forREADY=1, then Active. - Simple with
Readiness=Alive: Active as soon as exec succeeds. - Oneshot: wait for the exit. Success is Completed; without
RemainAfterExitit then goes Inactive once dependents are released. A non-zero exit is Failed.
On readiness or successful exit, peinit runs ExecStartPost in sequence
into hooks/, releases the service's dependents, kills hooks/, and —
for a Simple service only — arms the watchdog if WatchdogTimeout is
non-zero and the health check timer if HealthCheck is set.
A post-hook that fails is logged and does not fail the service. The state transition to Active or Completed happens before the post-hooks run, so a service is already Active while its post-hooks are executing.
5.3.9 Failures before the fork #
Steps 2, 4, 5 and 6 can all fail with no child in existence. peinit
handles all four entirely in the parent: clean up whatever cgroups were
created, fail the service with ParentSetupFailure, and return the
error to the caller. These are system-level resource problems — file
descriptor limits, PID limits, memory, cgroup filesystem errors — rather
than anything about the service.
5.4 The Child Path
Peios / Advanced Peios / peinit / Starting a Service
Between clone3 returning in the child and execve, peinit runs a
straight line of setup. The path is kept minimal — no logging, no
complex library calls — because it runs after a fork, where very little
is safe to do.
Each step reports through the error pipe with the identifier from §5.3
if it fails, then exits: _exit(126) for a setup failure, _exit(127)
for a failed exec.
| # | Step | Id |
|---|---|---|
| 1 | Close the read end of the error pipe | 1 |
| 2 | setsid() — only with a TTYPath | 12 |
| 3 | Set the standard streams | 2 |
| 4 | ioctl(TIOCSCTTY) — only with a TTYPath | 13 |
| 5 | Reset the signal environment | 3 |
| 6 | Install the KACS token, then close its descriptor | 4 |
| 7 | Set RLIMIT_NOFILE and RLIMIT_CORE | 5 |
| 8 | Set oom_score_adj | 6 |
| 9 | Change the working directory | 7 |
| 10 | Confirm NOTIFY_SOCKET is present in the environment | 9 |
| 11 | Inject stored descriptors from fd 3 upward | 10 |
| 12 | execve | 11 |
5.4.1 The terminal steps #
setsid() comes first, and its position is load-bearing in two
directions. It has to precede the stream setup, because setsid() drops
any controlling terminal the child inherited — doing it afterwards would
throw away the terminal just attached. And it has to precede
TIOCSCTTY, which requires a session leader that does not already own a
controlling terminal.
TIOCSCTTY is passed a literal zero argument, which is what makes it
unable to steal a terminal already owned by another session.
Without a TTYPath neither step runs and the service stays in peinit's
session.
5.4.2 The standard streams #
Without a TTYPath: stdin from /dev/null, stdout and stderr onto the
write ends of the service's output pipes, and every inherited pipe end
that is no longer needed closed.
With a TTYPath: all three streams onto the opened terminal, and the
/dev/null descriptor and both pipe pairs closed. A terminal-attached
service's output is therefore not captured for logging — it goes to
the terminal, which is the point of asking for one.
5.4.3 The signal environment #
peinit blocks every signal for its signalfd (§12.3) and the child
inherits that mask across the fork. Step 5 empties the mask and resets
every resettable disposition to SIG_DFL. A service starting with
signals blocked, or with PID 1's handling in place, is one of the
classic ways for a daemon to behave inexplicably.
5.4.4 oom_score_adj #
-1000 — OOM-immune — for an ErrorControl=Critical service, and 0
for everything else. A Critical service is one whose loss reboots the
machine, so letting the OOM killer choose it would convert memory
pressure into a reboot.
5.4.5 The environment #
There is no step that sets the environment, which is why identifier 8 is
reserved and never emitted. peinit builds the environment in the parent
(§5.5) and hands it to execve as envp, so it arrives with the exec
rather than being installed beforehand. Step 10 is a check rather than a
set: it confirms NOTIFY_SOCKET is present in the prebuilt environment
and fails with a synthetic EINVAL if it is not.
5.4.6 What the child does not inherit #
A service inherits only what peinit hands it: its standard streams and any descriptors injected from the fd store. Everything else peinit holds is created close-on-exec — the control socket and every accepted connection, the notification socket, the epoll instance, the signalfd, every timerfd, both pidfds, every pipe, and every stored descriptor until it is deliberately un-marked at injection.
The signal reset and the close-on-exec discipline together are what make a service start from a clean context rather than from PID 1's privileged one.
The exception is a descriptor opened through the Peios native file interface, which returns without close-on-exec set. peinit repairs that where it opens input devices for the power button; the JFS device descriptor and each service's own cgroup directory descriptor are not repaired, and are inherited across exec.
5.5 The Base Environment
Peios / Advanced Peios / peinit / Starting a Service
peinit constructs every service and hook process's environment from
scratch, in four layers, lowest precedence first. Nothing is inherited:
peinit's own startup environment holds TERM and nothing else (§2.1),
and none of it is passed through.
5.5.1 Layer 1: the compiled-in base #
One variable:
| Variable | Value |
|---|---|
PATH | /sbin:/bin |
Executables are addressed through the root-level StrataFS runtime views.
Package storage paths under /usr are deliberately not on the default
search path.
5.5.2 Layer 2: global environment variables #
Each value under Machine\System\Init\EnvVars\ becomes a variable: the
value name is the variable name, the REG_SZ data is the value. An
EnvVars\PATH overrides the compiled-in PATH; every other name adds.
A malformed entry — an empty name, or a name containing = — fails the
whole layer, which at boot means recovery mode.
registryd does not receive this layer. The exemption is a trust
rule, not an availability one. Write access to EnvVars\ is equivalent
to compromising every service peinit starts: LD_PRELOAD,
LD_LIBRARY_PATH and their relatives are not filtered, because the
key's Security Descriptor is meant to be the control boundary. A key
that could inject into the daemon that enforces who may write it would
make that boundary self-referential.
The exemption is narrow, and matches on two things at once: the job's
resolved identity is SYSTEM and its service name is registryd. A
non-platform service that happens to be called registryd receives the
ordinary layering. It matches on the job, so a hook of registryd's
running under a non-SYSTEM HookIdentity would receive the layer; and
it matches the resolved identity string, so a definition naming
S-1-5-18 literally rather than SYSTEM would not be exempt.
registryd is launched in Phase 1, before EnvVars has been read at all,
so the exemption is only observable on a restart.
5.5.3 Layer 3: the service's own Environment #
The definition's Environment entries, overriding both layers below.
5.5.4 Layer 4: protocol variables #
NOTIFY_SOCKET, always. LISTEN_FDS and LISTEN_FDNAMES, only when
descriptors are being injected from the fd store.
These have the highest precedence and are inserted after both
configurable layers, so a service cannot override NOTIFY_SOCKET and
break its own notification protocol. The guard is insertion order rather
than a reserved-name check, which means the two fd-store variables are
protected only when they are actually being set — with no descriptors to
inject they are not inserted, and a value from either configurable layer
reaches the child unchanged.
LISTEN_PID, which a conforming sd_listen_fds implementation checks
against its own PID before trusting LISTEN_FDS, is not set.
5.5.5 What peinit does not set #
Not HOME, USER, LOGNAME, SHELL or TERM. Peios identity is a
KACS token — a SID — rather than a passwd entry, so there is no
canonical home directory or login shell to populate. A service that
needs one supplies it through EnvVars\ or its own Environment.
5.5.6 Hooks and probes #
Hooks, health checks and reload commands are built through the same
path, so they receive the identical environment: the same layers, the
same NOTIFY_SOCKET, and the service's WorkingDirectory,
LimitNOFILE, LimitCORE and RequiredPrivileges. They never receive
LISTEN_FDS — stored descriptors go to the main process only.
5.5.7 When changes apply #
The global layer is a snapshot refreshed at boot and on reload-config,
and both it and the per-service Environment take effect at a service's
next start. Neither is applied to a running process.
5.6 Health Checks
Peios / Advanced Peios / peinit / Starting a Service
A watchdog tells peinit that a service is still ticking. A health check tells it that the service still works. They address different failures: a process can be alive and responsive to its own event loop while having lost its database connection, wedged in a bad state, or started returning errors to everyone.
5.6.1 Execution #
The health check command runs with the service's own token — never
HookIdentity — so it checks the service's health from the service's
own vantage point.
Each invocation runs in an ephemeral health/ sub-cgroup under the
service's tree, as a child of peinit rather than of the service. When
the check completes or times out, peinit kills the whole sub-cgroup,
which cleans up anything the check spawned.
5.6.2 Overlap #
If the previous check is still running when the next interval fires, the
new one is skipped and nothing is counted. A check exceeding
HealthCheckTimeout has its sub-cgroup killed and is counted as a
failure.
A launch failure — a token that could not be materialised, a fork that failed — is also counted as a failure and escalates immediately.
5.6.3 Failure #
HealthCheckRetries consecutive failures mark the service unhealthy.
An unhealthy service is restarted through the ordinary restart policy:
RestartPolicy, exponential backoff and throttling all apply, exactly
as for a crash. The failure count resets the moment a check succeeds.
Escalation kills the service's root cgroup rather than just main/,
so it takes hooks and probes with it.
5.6.4 The flap constraint #
Restart throttling is what stops a service flapping — failing checks,
restarting, passing initial checks, failing again. But it only works if
the failure cycle is shorter than RestartWindow, because otherwise the
service stays healthy long enough between failures to reset the restart
counter, RestartMaxRetries is never reached, and it restarts forever.
So this relationship has to hold:
HealthCheckRetries × HealthCheckInterval < RestartWindow
It is enforced as an error rather than a warning, in both places a
definition can arrive. At boot, a violating service is blocked with
cause ValidationError and never started. On reload-config, it is a
validation finding, and a finding rejects the entire reload.
The constraint is checked for any service that declares a HealthCheck,
including a Oneshot — even though health checks are scheduled only for
Simple services, so the check being constrained would never run.
5.7 Leaked Sub-Cgroups
Peios / Advanced Peios / peinit / Starting a Service
A process in uninterruptible kernel sleep — D-state, typically a hung NFS mount or a failing disk controller — does not die when it is SIGKILLed. Its cgroup cannot be removed while it is there.
peinit detects this through cgroup.events: after sending the kill it
arms a post-kill deadline, 5 seconds by default, and checks whether
populated is still 1 when the deadline fires.
5.7.1 What happens depends on which cgroup it is #
For the main process, a survivor is fatal to supervision: the
service transitions to Abandoned with cause ProcessUnkillable and
peinit stops supervising it (§6.1).
For a health check or a hook, it is not. Those are diagnostic and setup processes; they hold no service resources — no ports, no file locks, no database connections — so a stuck one does not make the service unmanageable. peinit orphans the sub-cgroup instead:
- Marks it leaked, recording the path, the kind and the time.
- Increments the service's cgroup generation, so the next start builds a fresh tree (§5.1).
- Carries on supervising the service normally.
A leaked pre-start check helper is treated the same way: its
checks/ sub-cgroup is recorded and the generation bumped, which matters
because otherwise the next start would build into a tree that still
contains the unkillable process.
The leaked cgroup stays in the hierarchy until the next reboot.
5.7.2 Visibility #
Leaks are not silent. peinit both pushes one when it is detected and keeps it queryable afterwards.
The push happens once, on first detection — recording is idempotent, so a leak that is re-examined on a later cleanup pass is not announced again:
-
A
cgroup.leakedevent on the event stream, carrying the service, the sub-cgroup path, its kind, and the detection time in monotonic nanoseconds. -
A console line naming the same service, kind and path.
The pull side survives the moment of detection, for anyone who was not watching:
-
A status query includes a
warningsarray, one entry per leak, each an object with the sub-cgroup path, its kind, and the time of detection: -
A start command on a service with leaks returns a warning in its acknowledgement, saying the service has leaked sub-cgroups from a previous generation and that this indicates an I/O problem needing investigation.
The type is the same vocabulary everywhere: health for a leaked
health/ sub-cgroup, hooks for a leaked hooks/ one, helper for a
pre-start check helper's checks/, and service_tree for a leaked
service root — the last being the most serious, since it means the whole
tree including main/ could not be reclaimed.
Whichever way it reaches you, what the leak means is the same: something underneath the service is not responding to the kernel, and no amount of restarting the service will fix it.
6.1 States
Peios / Advanced Peios / peinit / The State Machine
Every service is in exactly one state. There are ten.
| State | Process? | Satisfies dependents? | Meaning |
|---|---|---|---|
| Inactive | No | No | Not started, or stopped cleanly and not set to restart. |
| Starting | Maybe | No | Activation in progress: checks, hooks, fork, or the readiness wait. The process may not exist yet. |
| Active | Yes | Yes | Running and ready. |
| Reloading | Yes | Yes | Re-reading configuration. Still satisfies dependents. |
| Stopping | Briefly | No | SIGTERM sent, awaiting exit or SIGKILL escalation. |
| Completed | No | Yes | Oneshot only. Exited successfully. |
| Backoff | No | No | A restart is pending; waiting out the backoff delay. |
| Failed | No | No | Exited abnormally with the restart policy exhausted or absent. |
| Abandoned | Yes, unkillably | No | SIGKILL sent and processes survived. Supervision has stopped, the cgroup is leaked. |
| Skipped | No | Yes | Conditions were not met. The service does not apply. |
6.1.1 Dependent satisfaction #
Exactly three states satisfy dependents: Active, Completed and
Skipped. Nothing else does, and a dependent blocked on a Requires
target in any other state does not start.
Completed satisfies regardless of RemainAfterExit — a Oneshot without
it passes through Completed to release its dependents on the way to
Inactive, rather than skipping the state.
Skipped satisfies because a service whose conditions do not hold has succeeded by not needing to run. Treating it as a failure would make every conditional service a hazard to everything that depends on it.
Reloading satisfies because the process is still there and still serving; a reload is a service telling itself to re-read a file, not an outage.
Backoff does not, and the distinction from Failed matters: a service in Backoff is going to start again, and its dependents wait rather than failing. It is the state that makes a restart something other than a transit through Failed.
6.1.2 Invariants #
- A service is in exactly one state at any moment. There is one state field and exactly one place in the implementation that assigns to it.
- Only the transitions in §6.2 are performed. The assignment is gated by a central whitelist, so an unlisted transition is not merely avoided by convention — it cannot be written.
- Only peinit transitions a service. The control socket produces operations; nothing outside peinit writes state.
- A service object is securable independently of its process token. The ServiceSecurity descriptor governs who may manage the service; the process token governs what the service may reach. Neither implies anything about the other.
- Readiness is per start generation. The generation increments on every
transition into Starting, and a
READY=1carrying a stale generation is rejected — so a notification from a previous incarnation can never be mistaken for this one's.
6.2 Transitions
Peios / Advanced Peios / peinit / The State Machine
Every transition peinit performs. Anything not listed here is not performed.
| From | To | Trigger |
|---|---|---|
| Inactive | Starting | Start command, dependency resolution, or a timer trigger. |
| Inactive | Skipped | A condition check failed. |
| Inactive | Failed | Validation, assertion, cycle or dependency failure — before any process existed. |
| Starting | Active | Readiness. Simple only: READY=1 received, or the process exists under Readiness=Alive. |
| Starting | Completed | Oneshot exited successfully. Without RemainAfterExit it passes through, releasing dependents, then goes Inactive. |
| Starting | Skipped | A condition failed after the activation had already entered Starting. |
| Starting | Failed | An assert failed after entering Starting; or a timeout, hook failure, setup failure, or an exit before readiness. |
| Starting | Backoff | A startup failure with the restart policy allowing a retry and budget remaining. |
| Starting | Stopping | An explicit stop cancelling an in-progress start. A restart on a Starting service is queued rather than cancelling. |
| Starting | Failed | The shutdown wave SIGKILLed a starting service. |
| Active | Reloading | ExecReload issued, or SIGHUP delivered. |
| Active | Stopping | Explicit stop, conflict eviction, a bound dependency stopping, or shutdown. |
| Active | Backoff | A crash, a watchdog timeout, or health check failure, with a restart allowed and budget remaining. |
| Active | Failed | The same three, with RestartPolicy=Never or the budget exhausted. |
| Active | Inactive | A Simple service exited successfully and RestartPolicy is not Always. Cause CleanExit. |
| Active | Backoff | A Simple service exited successfully and RestartPolicy=Always. Cause CleanExitRestart. |
| Reloading | Active | Reload resolved: READY=1, the detection window expiring, the extended wait expiring, or the reload command exiting — success or failure. |
| Reloading | Stopping | The same triggers as Active to Stopping. The reload is cancelled. |
| Reloading | Backoff | The main process exited during the reload, with a restart allowed. |
| Reloading | Failed | The same, with no restart available. |
| Stopping | Inactive | The process exited after an explicit stop or a shutdown. |
| Stopping | Failed | The process exited after a conflict eviction or a bound dependency stopping. |
| Stopping | Abandoned | SIGKILL sent and main/ still populated after the post-kill deadline. |
| Backoff | Starting | The backoff delay elapsed. Cause RestartPolicy. |
| Backoff | Inactive | An explicit stop cancelled the pending restart. |
| Completed | Inactive | RemainAfterExit=0 and dependents released; or an explicit stop; or shutdown clearing it. |
| Completed | Starting | A start command or timer trigger re-running the Oneshot. |
| Failed | Starting | An explicit start, a bound dependency recovering, or a timer trigger. |
| Failed | Inactive | A reset command clearing the state. |
| Failed | Abandoned | A service SIGKILLed by the shutdown wave whose cgroup stayed populated past the post-kill deadline. |
| Abandoned | Inactive | A reset command. |
| Skipped | Inactive | A reset command, or an explicit start clearing Skipped before it re-evaluates the conditions. |
6.2.1 Things the table settles #
A restart never passes through Failed. A restart-eligible failure
goes to Backoff, waits, and goes to Starting. Failed is reached only
when there will be no retry: RestartPolicy=Never, an invalid policy
for the cause, or an exhausted budget. This is why OnFailure (§6.3),
which fires on entry to Failed, does not fire on each retry — only when
the service finally fails out.
A clean exit is not a crash. A Simple service exiting zero goes to
Inactive under cause CleanExit, consulting neither the restart policy
nor the budget. It goes to Backoff only under RestartPolicy=Always,
and then with the distinct cause CleanExitRestart, so status and
events say plainly that the process succeeded and was restarted by
policy rather than that anything went wrong.
A forced stop remembers why. Stopping to Failed carries the cause
from the transition that started the stop — ConflictEviction or
BindsToPropagation — rather than a generic failure. A service that
lost a conflict and a service whose binding target went away are
distinguishable afterwards, which is what makes bound-dependency
recovery (§7.1) possible at all.
A restart detours through Inactive. The stop leg of an administrator's restart ends in Inactive, and the start leg begins from there, so a restarting service is briefly observable as Inactive.
A crash before readiness may be retried. A Simple process that exits
before signalling readiness is a ProcessCrash from Starting, and is
restart-eligible like any other, rather than a terminal startup failure.
6.2.2 Reset from Abandoned #
Resetting an Abandoned service re-checks its main/ sub-cgroup. If it
has finally emptied, peinit cleans up the whole service tree — main/,
hooks/, health/, then the root — and transitions to Inactive.
If it is still populated, peinit leaves the cgroup leaked, transitions to Inactive anyway, and returns this warning in the operation acknowledgement:
abandoned main cgroup for service <service> is still populated after
reset -- cgroup remains leaked; underlying D-state process requires
investigation
The warning is also written to the console, so a reset issued without reading the response still leaves a trace of the still-leaked cgroup.
The re-check targets main/. Note that the two paths into Abandoned
probe different cgroups: an explicit stop checks main/, while the
shutdown wave checks the service root.
6.3 Transition Causes
Peios / Advanced Peios / peinit / The State Machine
Every transition carries a cause recording why it happened. peinit
tracks both the current state and the cause of the most recent
transition, and the cause determines restart eligibility, OnFailure
behaviour, and what an administrator is told.
6.3.1 The taxonomy #
| Cause | Leads to | Meaning |
|---|---|---|
ExplicitStart | Starting | An administrator, an OnFailure handler, or a boot plan started it. |
ExplicitStart | Inactive | An explicit start on a Skipped service, clearing it so the conditions are re-evaluated. |
DependencyStart | Starting | Started to satisfy another service's dependency. |
RestartPolicy | Starting | An automatic restart after a backoff delay. |
BindsToRecovery | Starting | A bound dependency returned to Active. |
Timer | Starting | A timer trigger fired. |
ExplicitStop | Stopping | An administrator requested a stop. |
ExplicitReload | Reloading, Active | A reload was issued and resolved. |
ExplicitReset | Inactive | An administrator cleared Failed, Abandoned or Skipped. |
ConflictEviction | Stopping | A conflicting service started; this one lost. |
BindsToPropagation | Stopping | A bound dependency stopped. |
ShutdownWave | Stopping, Failed | The system is shutting down. Active and Reloading services go to Stopping; Starting services go straight to Failed. |
ProcessCrash | Failed, Backoff | The main process exited unexpectedly. |
CleanExit | Inactive | A Simple process exited successfully with a policy other than Always. |
CleanExitRestart | Backoff | A Simple process exited successfully under RestartPolicy=Always. |
ReadinessTimeout | Failed, Backoff | StartTimeout expired before readiness. |
WatchdogTimeout | Failed, Backoff | A keepalive did not arrive in time. |
HealthCheckFailure | Failed, Backoff | HealthCheckRetries consecutive failures. |
PreHookFailure | Failed, Backoff | An ExecStartPre hook exited non-zero, or its token failed, or the start timed out during hooks. |
ParentSetupFailure | Failed, Backoff | A parent-side failure before the fork. No child was created. |
PreExecFailure | Failed, Backoff | Post-fork setup failed before exec, reported through the error pipe. |
DependencyFailure | Failed | A Requires dependency entered Failed. |
RestartBudgetExhausted | Failed | RestartMaxRetries reached. |
CycleDetected | Failed | The service is part of a dependency cycle. |
ValidationError | Failed | The definition failed graph validation. |
AssertionError | Failed | A start-time assert failed. |
ConditionSkipped | Skipped | A start-time condition failed. |
ProcessUnkillable | Abandoned | Processes survived SIGKILL. |
6.3.2 Restart eligibility #
Causes fall into four classes, and the class decides whether the restart policy is consulted at all.
Restart-eligible. peinit consults RestartPolicy and the budget. If
a restart is allowed and the budget holds, the service goes to Backoff
and then to Starting; otherwise Failed.
ProcessCrash, WatchdogTimeout, HealthCheckFailure,
ReadinessTimeout, PreHookFailure, PreExecFailure,
ParentSetupFailure.
Always-only. CleanExitRestart applies when a Simple service exits
successfully and the policy is Always. It uses the same backoff and the
same budget as a failure, which is what stops a daemon that exits
cleanly in a tight loop from bypassing throttling entirely. It is never
treated as a ProcessCrash, and both status and events make clear the
process succeeded and was restarted only because the policy says so.
CleanExit is its non-restarting counterpart: straight to Inactive,
consulting neither policy nor budget.
Budget-exempt. BindsToRecovery takes a service from Failed to
Starting when its binding target returns. It is not subject to the
policy or the budget, because the service did not fail on its own — it
was stopped because its dependency went away.
Never-restart. peinit does not consult the policy at all:
ExplicitStop, ExplicitReset, ShutdownWave, ConflictEviction,
BindsToPropagation, ProcessUnkillable, RestartBudgetExhausted,
ValidationError, CycleDetected, DependencyFailure,
AssertionError, ConditionSkipped. Retrying cannot help with any of
them.
6.3.3 OnFailure #
When a service enters Failed and its definition names an OnFailure
service, peinit starts that service — with the exceptions below.
OnFailure fires on entry to Failed, and Failed to Failed is not a
transition, so it fires at most once per failure.
It does not fire for:
ShutdownWave— no new service starts during shutdown, so a fallback would be both impossible and pointless.ValidationError,CycleDetected,DependencyFailure,AssertionError— these are definition or graph breakage rather than runtime degradation. A broken definition cannot meaningfully trigger a fallback, and the fallback would probably sit in the same broken graph.
It fires for everything else, including ProcessCrash,
WatchdogTimeout, HealthCheckFailure, the startup failures, and
RestartBudgetExhausted on a non-Critical service.
For a Critical service exhausting its budget, the reboot takes
precedence and no fallback is started. The suppression keys on the cause
and the service's ErrorControl rather than on whether a reboot was
actually scheduled.
OnFailure is for graceful degradation — the main web interface fails,
so start a minimal emergency endpoint. It is not for monitoring or
alerting, which is eventd's job.
6.3.3.1 The loop guard #
An OnFailure handler can fail and carry its own OnFailure, so a
misconfiguration where A's handler is B and B's is A could run forever.
peinit bounds the chain originating from one failure two ways: it tracks
the set of services already started as handlers for that failure and
will not start one already in the set, and it will not follow the chain
past a fixed depth of 16. When either trips, peinit records an
on_failure.loop_suppressed event naming which, and stops.
The chain is cleared when a handler reaches Completed, Inactive, Skipped or Abandoned. A handler that starts and stays running keeps its entry, so it continues to occupy a slot of that originating failure's budget.
6.3.4 The logging contract #
Every state transition produces a record covering four things: what failed, why it failed, what peinit did about it, and what the administrator should do. Cryptic failure messages are a defect. A reboot loop caused by a configuration error with an opaque message is the worst outcome the system has, and the cause taxonomy exists so that the "why" is never a guess.
6.4 Restart
Peios / Advanced Peios / peinit / The State Machine
When a restart-eligible cause occurs, or a Simple clean exit produces
CleanExitRestart, peinit evaluates the restart policy. The outcome is
either Failed, or Backoff followed by Starting.
evaluate_restart(service, cause):
// 1. Policy.
if cause is never-restart:
return STAY_FAILED
if cause == CleanExitRestart and policy != Always:
return INVALID_CAUSE_FOR_POLICY
if policy == Never:
return STAY_FAILED
if policy == OnFailure:
// A termination counts as success only when the cause is a
// process exit whose code is in SuccessExitCodes. Only
// ProcessCrash and CleanExitRestart carry an exit code, and
// CleanExitRestart was handled above. Every other eligible
// cause has no exit code and is always a failure here.
if cause == ProcessCrash and exit_code in success_exit_codes:
return STAY_FAILED
// Always falls through unconditionally.
// 2. Budget.
if consecutive_failures >= restart_max_retries:
cause = RestartBudgetExhausted
if error_control == Critical:
sync and reboot
return STAY_FAILED
// 3. Delay.
delay = min(restart_delay << consecutive_failures, 60)
// 4. Schedule.
return RESTART_AFTER(delay)
RESTART_AFTER puts the service in Backoff for the delay; when it
elapses the service transitions to Starting and the ordinary activation
sequence begins, with its own fresh StartTimeout.
The exit code is available only when peinit observed a process exit. For
a Simple service that exits before signalling readiness the code is not
carried into the evaluation, so the SuccessExitCodes branch of the
OnFailure policy cannot apply and such a service is always restarted.
6.4.1 The policies #
| Policy | Value | Behaviour |
|---|---|---|
| Never | 0 | Never restart. The service stays Failed. |
| OnFailure | 1 | Restart on a non-zero exit or a runtime failure. An exit matching SuccessExitCodes is not restarted. |
| Always | 2 | Restart on any failure regardless of exit code, and for a Simple service also on a successful clean exit. |
For a Oneshot with RestartPolicy=Always, a successful exit is not
restart-eligible. RestartPolicy governs the response to failures; a
Oneshot that succeeds has done its job. It goes to Completed — and then
Inactive without RemainAfterExit — whatever the policy says, and only
a non-zero exit reaches the restart evaluation at all. Timer triggers
are the mechanism for re-running a Oneshot on a schedule.
6.4.2 Backoff #
The delay doubles on each consecutive failure, starting from
RestartDelay and capped at 60 seconds. The arithmetic is
overflow-safe, so a large RestartDelay or a long failure run saturates
at the cap rather than wrapping.
While a service is in Backoff it is down and does not satisfy
dependents. An explicit start creates or merges into a deferred
start operation, which honours the remaining delay rather than
short-circuiting it; a stop cancels the pending restart and takes the
service to Inactive.
6.4.3 The budget, and when it resets #
consecutive_failures counts consecutive restart-eligible failures. It
resets to zero only after the service has stayed Active for
RestartWindow seconds. It is not a count of restarts within a
trailing window, and the difference is what the mechanism turns on.
peinit stamps the moment a service becomes dependent-satisfying, and
clears that stamp on any transition to a non-satisfying state — so a
crash restarts the clock. The reset fires when the stamp plus
RestartWindow is reached with the service still Active.
A service that recovers and stays Active longer than RestartWindow
between crashes therefore never exhausts its budget: each crash starts
from a counter of zero. Only failures recurring faster than the service
can sustain a window of health accumulate.
Two other events also zero the counter, both of which mean the service is no longer in a failure run: a clean exit to Inactive, and an administrative reset. An explicit stop while in Backoff does not — the accrued failures are preserved.
Because the reset requires the service to be Active, a service that happens to be Reloading when its window boundary passes misses that reset and gets it on returning to Active.
6.4.4 Exhaustion #
Once RestartMaxRetries restarts have happened without the service
sustaining a window of health, the next failure is not restarted: Failed
with cause RestartBudgetExhausted. peinit then applies ErrorControl:
- Normal — the service stays Failed.
- Critical — peinit syncs the filesystems and reboots immediately.
The reboot takes precedence over
OnFailure.
The reboot is driven from the paths that observe a terminal outcome for
a running service: the main job ending, a health check failing or timing
out, and the watchdog expiring. A budget exhausted purely by startup
failures — repeated ReadinessTimeout, repeated PreHookFailure — does
not reach one of those paths, so a Critical service that can never get
as far as running settles in Failed rather than rebooting, and its
OnFailure handler is suppressed as well.
6.5 Reload
Peios / Advanced Peios / peinit / The State Machine
Reload tells a service to re-read its configuration without restarting. peinit issues the reload, moves the service to Reloading, and resolves it one of three ways.
A failed reload never takes a running service out of Active. And reload never gets stuck: every path has a timeout.
6.5.1 Choosing a path #
ExecReload absent means SIGHUP to the main process. A signal:<NAME>
value means that signal instead. Anything else is a command, forked into
the service's hooks/ sub-cgroup under the service's own identity —
never peinit's token, and HookIdentity does not apply.
6.5.2 The signal path #
There is no command exit to observe, so completion is inferred from the main process's own notifications.
start the detection window (2 seconds)
on READY=1, at any time:
-> Active, "confirmed"
on RELOADING=1 within the window:
cancel the window
start the extended wait (StartTimeout)
on the window expiring with no RELOADING=1:
-> Active, "advisory"
on the extended wait expiring with no READY=1:
-> Active, "advisory"
The two-second window is a constant and is not configurable through the
registry. It is bounded from above by the operation's own deadline, so a
service whose StartTimeout is under two seconds gets the shorter of
the two.
The extended-wait expiry means the service announced a reload and never
finished one. The outcome is carried in the operation's result, which a
wait=true caller receives; a reload issued without waiting — the
default — resolves silently.
6.5.3 The command path #
The command's exit gates failure; the main process's READY=1 gates
confirmation.
on the command exiting non-zero:
-> Active, "failed"
on the command exceeding StartTimeout:
SIGKILL the hooks/ sub-cgroup, taking its descendants
-> Active, "failed"
on the command exiting zero:
-> Active, "confirmed" if the main process sent READY=1 during the
reload, otherwise "advisory"
6.5.4 Auto-detection #
The protocol needs no per-service configuration. A service that
implements the notification handshake — RELOADING=1 then READY=1 —
gets real lifecycle tracking. One that does not gets a brief Reloading
state that resolves itself when the detection window expires. Neither
has to declare which it is.
6.5.5 Interruptions #
The main process crashes while Reloading. That is a ProcessCrash,
and the restart policy is consulted: Reloading to Backoff if a restart
is allowed and the budget holds, Reloading to Failed otherwise. Both
reload timers are cancelled and any in-flight reload command is killed.
This is a different event from an external reload command exiting non-zero, which is the "failed" outcome above and leaves the main process — and the service — running.
A stop arrives while Reloading. peinit cancels the reload immediately: it drops the reload deadlines, kills any reload command's cgroup, and sends SIGTERM in the same turn, without waiting out the window or the extended wait. The service goes to Stopping and the reload operation is aborted.
6.6 The Watchdog and Timeout Extension
Peios / Advanced Peios / peinit / The State Machine
Two notification fields let a running service adjust the deadlines it is held to. Both are authenticated exactly as any other notification (§10.5), and both carry microseconds.
6.6.1 The watchdog #
WatchdogTimeout sets the interval peinit expects WATCHDOG=1 pings
at. Zero, the default, disables it. Missing a ping is a
WatchdogTimeout cause and takes the ordinary restart path.
A service may change the interval at runtime by sending
WATCHDOG_USEC=<value>:
- A value greater than zero updates the interval and re-arms immediately — the current timer is cancelled and a fresh one starts from the moment the message was received, rather than the new interval applying only from the next ping.
- A value of zero disables the watchdog entirely, equivalent to
WatchdogTimeout=0.
The runtime value does not persist. On a restart the interval reverts to
the definition's WatchdogTimeout converted to microseconds, and if
that is zero the watchdog starts disabled whatever the previous
incarnation had set.
WATCHDOG_USEC is honoured only while the service is Active. A service
that sends it while still Starting — before its own READY=1 — is
ignored and gets the definition's value.
6.6.2 Timeout extension #
A service may ask for more time during a start, stop or reload by
sending EXTEND_TIMEOUT_USEC=<value>.
peinit sets the current phase's deadline to expire that many microseconds from now. The extension replaces the deadline rather than adding to it — each message sets an absolute deadline computed from its own arrival — and may be sent repeatedly.
Because it replaces, a small value shortens the remaining time rather than being ignored, and a value of zero sets the deadline to now.
6.6.2.1 The caps #
The extended deadline cannot exceed four times the phase's base timeout:
| Phase | Base | Ceiling |
|---|---|---|
| Starting | StartTimeout | StartTimeout × 4 |
| Stopping | StopTimeout | StopTimeout × 4 |
| Reloading | StartTimeout | StartTimeout × 4 |
A value beyond the cap is clamped, not rejected — the message succeeds and the deadline becomes the maximum permitted. Because the cap is anchored to when the operation started rather than to the previous deadline, repeated messages cannot creep past it.
During shutdown an additional cap applies: the deadline cannot exceed
the time remaining in the global ShutdownTimeout, and where both caps
apply the stricter wins.
6.6.2.2 Where it does not apply #
A message arriving while the service is in a non-transitional state — Active, Completed, Failed — is ignored. There is no deadline to extend.
During shutdown the extension applies only to a service whose stop wave has already begun. A service in a later wave, or one still winding down a start or a reload when shutdown was requested, has no shutdown deadline recorded yet and its extension request has no effect.
7.1 Relationships
Peios / Advanced Peios / peinit / Dependencies
Four relationship types. Each says something different about how two services interact at start, at stop, and on failure.
7.1.1 Requires #
A hard dependency. If A Requires B:
- Start. B has to reach a dependent-satisfying state before A
starts.
If B is not running, peinit starts it with cause
DependencyStart. If B enters Failed, A goes to Failed with causeDependencyFailurewithout attempting to start. - Stop. Stopping B does not stop A. This is a start-ordering constraint, not a runtime coupling.
- Runtime failure. If B crashes while A is Active, A is unaffected and keeps running. B's own restart policy handles B.
- Missing target. A goes to Failed with
DependencyFailure, detected at graph validation.
7.1.2 Wants #
A soft dependency. If A Wants B, peinit starts B before A — with
cause DependencyStart — provided B exists and is not disabled. If B
fails to start, or does not exist at all, A starts anyway. There is no
stop or failure effect in either direction.
The waiting rule is where the difference from Requires actually lives:
a dependent blocked on a Requires target waits for it to reach a
satisfying state, while a dependent blocked on a Wants target waits
only for it to reach any terminal state, satisfying or not. That is
what makes Wants ordering rather than dependency — "start this first
if you can, but I will work without it".
7.1.3 BindsTo #
A runtime coupling. If A BindsTo B:
- Start. Identical to
Requires. - Stop. If B stops for any reason — explicit stop, conflict,
crash, shutdown — A stops too, transitioning to Stopping with cause
BindsToPropagation. - Recovery. When B returns to Active, peinit automatically restarts
anything sitting in Failed with cause
BindsToPropagationfrom B's stop. This is reactive rather than polled: peinit watches for the transition into Active from a non-satisfying state, and reacts to it. These restarts do not consume the restart budget — the dependent never failed on its own, it was stopped because its binding target went away.
BindsTo implies Requires. A definition may list both for clarity,
and if it does, the BindsTo semantics apply.
7.1.4 Conflicts #
Mutual exclusion. Starting A while B is Active creates a stop operation
for B — source ConflictResolution, cause ConflictEviction — and A
does not start until B has left every state in which it could still be
running.
Conflicts are symmetric. If A declares Conflicts = ["B"], starting
either one stops the other; B does not have to declare it reciprocally,
and peinit scans both a starting service's own conflicts and everything
that declares a conflict against it.
If both A and B are boot-triggered and they conflict, graph validation
detects an unresolvable conflict and fails both with ValidationError.
A missing conflict target is silently dropped — there is nothing to conflict with.
7.1.5 Ordering and self-reference #
Dependencies imply start ordering, and — except for BindsTo — imply
nothing about stopping. peinit starts dependencies before dependents;
during shutdown it reverses the same graph and stops dependents before
dependencies (§12.2), derived from the one graph rather than from a
separate stop-ordering configuration.
A service may not name itself in any dependency field. peinit rejects a
self-reference at graph validation as a CycleDetected failure, which
is what it is — a cycle of length one.
7.2 Graph Validation
Peios / Advanced Peios / peinit / Dependencies
peinit validates the dependency graph before executing it. Validation runs once per graph build — at boot, on an on-demand start's transitive closure, and on a reload-config — and is never incremental.
7.2.1 Cycles #
peinit topologically sorts the graph; if the sort fails, a cycle exists. Detection returns all cycles rather than stopping at the first: each detected cycle's members are removed and the search re-run until the graph is clean.
Every service in a cycle is failed with cause CycleDetected, and the
cycle path is logged so an administrator can see what to break.
If any service in a cycle is Critical, peinit downgrades to Safe mode (§2.6) rather than rebooting. The cycle is a configuration error, and rebooting would find it again.
7.2.2 Missing targets #
| Relationship | A missing target means |
|---|---|
Requires | The dependent is failed with DependencyFailure. |
BindsTo | The same — treated as a missing Requires. |
Wants | Silently dropped. |
Conflicts | Silently dropped. |
Detection is a Full-mode behaviour. In Safe mode a hard-dependency target that is missing, disabled, or not Safe-mode-eligible does not block the dependent, which is started with the dependency unmet.
7.2.3 Validation errors #
A service that hits one is failed with cause ValidationError and never
started:
- The flap constraint,
HealthCheckRetries × HealthCheckInterval < RestartWindow(§5.6). - An invalid timer calendar expression (§9.1). This one is checked across every definition, not only those in the graph.
- Two boot-triggered services that conflict with each other. Both are failed. Safe mode applies if either is Critical.
7.2.4 Validation warnings #
Logged, and do not prevent boot:
- A service using
Readiness=Alivethat something else depends on hard.Alivereadiness means the process exists, which is no guarantee it is functional, so anything waiting on it is waiting on the wrong thing.
7.2.5 Multiple findings #
A service can attract more than one finding in one pass — being both in
a cycle and missing a Requires target, say. The runtime state records
a single primary cause, chosen by precedence:
CycleDetectedValidationErrorDependencyFailure
The precedence affects only which cause is stored. Every other finding for the service is retained beside it, in discovery order, and the operation's failure message enumerates all of them:
CycleDetected: dependency cycle a -> b -> a (also: ValidationError:
health check interval exceeds the restart window) [2 findings]
The primary comes first and unqualified, so a service with one finding reads exactly as it always did. A higher-precedence finding arriving later demotes the previous primary rather than deleting it — breaking the cycle should not be what it takes to discover the second fault.
Every finding is also emitted as its own graph.validation_error KMES
event, carrying phase: "boot". The console lines are for whoever is
watching the boot; the events are the account that survives it.
HardDependencyBlocked — blocked because a dependency is blocked —
gets its own finding value rather than being reported as a missing
dependency, which would claim the target does not exist when it does.
It has no reload-path equivalent, because reload rejects wholesale
instead of propagating a block.
The reload path behaves differently, because its consequence is
different. Validation there accumulates every finding, encodes each as
its own event under phase: "reload_config", and then rejects the
entire reload — the previous generation stays live and the findings
return to the caller (§10.4). Boot marks individual services and
continues; reload reports everything and changes nothing.
Both use the same event type deliberately: one consumer filter catches
validation problems in either regime, and phase says which.
7.3 Graph Execution
Peios / Advanced Peios / peinit / Dependencies
Executing a graph means starting the services whose dependencies are all satisfied, and doing it again each time something becomes satisfying, until nothing is left.
execute_graph(graph, max_parallel):
ready = services with no unsatisfied dependencies
in_flight = 0
while ready is not empty or in_flight > 0:
while ready is not empty and in_flight < max_parallel:
begin_start(ready.dequeue())
in_flight += 1
match wait_for_event():
ServiceSatisfied(s):
in_flight -= 1
for each dependent of s:
if all its dependencies are satisfied:
ready.enqueue(dependent)
ServiceFailed(s):
in_flight -= 1
propagate_failure(s)
MaxParallelStarts bounds the concurrency, counted per context as the
members currently running.
7.3.1 Execution contexts #
A graph execution is a retained object, not a transient loop. peinit holds a context carrying its members, their dependencies and their status, and there are two kinds: one boot context built from the boot plan, and an on-demand context per explicit start, built from that service's validated transitive closure.
Both use the same scheduler, the same satisfaction rules, the same failure propagation and the same parallelism, but they are distinct runtime objects — which matters because they can overlap.
An operation is associated with every context that created or adopted it. One operation can belong to more than one active on-demand context: two administrators starting different services that share a dependency both end up merged into the same already-starting operation for it, and both contexts need to hear how it turns out.
So when a pre-start outcome is terminal for an operation, peinit dispatches the corresponding graph event once per associated context. An operation with no associated context completes or fails normally, and its waiters are notified, but no graph event is dispatched.
A member whose dependent resolves without ever needing it is pruned — a dormant sub-tree is cancelled rather than started, so an on-demand start that turns out not to need half its closure does not start that half.
Contexts are not retired. A context and its operation associations persist for the lifetime of the process.
7.3.2 Failure propagation #
When a service enters Failed during graph execution:
- Everything that
RequiresorBindsToit transitions to Failed with causeDependencyFailure. - Everything that
Wantsit is unaffected and starts normally. - Propagation is transitive: if A requires B and B requires C, and C fails, then B fails and then A fails.
7.3.3 On-demand start #
Starting a service explicitly resolves its dependencies first:
- Collect the transitive
RequiresandBindsToclosure. - Collect the transitive
Wantsclosure, best-effort. - Validate the sub-graph — cycles, missing targets.
- Resolve conflicts, stopping whatever has to stop.
- Start the sub-graph with the same parallel algorithm. Dependencies
started this way carry cause
DependencyStartand operation sourceDependencyPropagation.
A service already in a satisfying state — Active, Completed or Skipped — is not restarted. Its dependency is already met.
The on-demand path treats a disabled hard-dependency target differently from the boot path: where boot blocks the dependent, an on-demand start includes the disabled target and starts it.
7.3.4 Shutdown ordering #
Shutdown reverses the graph: services with no dependents stop first, services that everything depends on stop last, derived by reverse topological sort from the same edges.
Only hard dependencies — Requires and BindsTo — contribute to the
ordering. A Wants dependent may therefore be stopped after its target.
The ordering is entirely emergent from what the definitions declare. There is no floor and no pinning, so where the TCB services end up in the wave order depends on their declared dependencies being right.
7.4 The Boot and On-Demand Paths
Peios / Advanced Peios / peinit / Dependencies
The two ways a graph gets built differ in more than scope, and the differences are worth having in one place.
| Boot | On-demand | |
|---|---|---|
| Members | Every boot-triggered root plus its closure | One requested service plus its closure |
| A missing hard-dependency target | Blocks the dependent, in Full mode only | Blocks the dependent |
| A disabled hard-dependency target | Blocks the dependent | Starts it |
| A validation finding | The service is failed, others continue | The start fails |
| Multiple findings on one service | Only the highest-precedence one is reported | — |
| Conflicts | Two boot-triggered conflicting services fail both | The conflicting service is evicted |
| Context | One boot context | One context per explicit start |
| Failure of the whole build | Recovery mode | An error to the caller |
7.4.1 Where a definition error lands #
The three places a bad definition can be caught behave differently, and which one catches it depends on what kind of wrong it is:
- Decoding. A definition that does not parse — an invalid name, a
malformed trigger, an unclosed quote, a
registry:check naming an uncacheable key, a duplicate field — is caught when the registry is read. At boot it fails that service withValidationErrorand the rest continue; on a reload it rejects the whole reload (§3.2). - Graph validation. A definition that parses but does not fit — a cycle, a missing target, a flap-constraint violation, an invalid calendar expression — is caught here, and fails that service at boot or the whole reload on a reload-config (§7.2).
- Start. A definition that parses and fits but whose preconditions do not hold — a failed assert, an unresolvable identity, a missing binary — is caught when the service actually starts, and fails that activation (§5.2, §5.3).
The dividing line between the first two is whether the problem is visible in one definition on its own. Decoding sees one key at a time; validation is the first place that can see two definitions together.
8.1 Jobs
Peios / Advanced Peios / peinit / Jobs and Operations
A job is one supervised process execution. Every fork peinit performs is a job: a service's main binary, a pre-exec hook, a post-exec hook, a reload command, a health check invocation, an ad-hoc submission.
Jobs are the observable unit of what actually ran. Services are definitions carrying identity, policy and configuration; jobs are instances. A restart creates a new job.
8.1.1 Lifecycle #
Created --> Running --> Completed
| |
| +------> Failed
| |
| +------> Abandoned
+------------------> Failed
| State | Meaning |
|---|---|
| Created | The job object exists but exec has not succeeded. The process may not have been forked, or it may be in pending post-fork setup with the error pipe unresolved. |
| Running | Exec succeeded and the process is alive. |
| Completed | The process exited successfully — code 0, or one in SuccessExitCodes. |
| Failed | The process failed, or peinit classified the job failed before the fork because parent setup failed. |
| Abandoned | The process survived SIGKILL. |
Job states are simpler than service states because they describe a process rather than a policy. A service has Starting, Reloading and Backoff because those are decisions; a job is running, or it finished, or it is stuck.
8.1.2 Fields #
Job {
id: GUID // UUIDv7
service: string? // null for ad-hoc
job_type: enum // ServiceMain, PreExecHook, PostExecHook,
// ReloadHook, HealthCheck, AdHoc
state: enum
pid: u32?
pidfd: fd?
resolved_identity: string // the resolved service, hook or submitter identity
token_summary: object // the resulting SID, groups, privileges
image_path: string
arguments: string[]
created_at_ns: u64
started_at_ns: u64?
ended_at_ns: u64?
exit_code: i32?
exit_signal: i32?
failure_cause: string?
cgroup_id: string
cgroup_generation: u32
activation_generation: u32
operation_id: GUID? // null for ad-hoc
hook_index: u32?
}
The rules that govern when the nullable fields are populated are what make a job record trustworthy:
idis assigned before the fork, so a job that never forks still has an identity.pidandpidfdland on the record only once exec success is confirmed by EOF on the error pipe. Until then they are held in pending setup state and the job is Created.- A setup failure takes the job straight from Created to Failed.
ended_at_nsrecords the classification time,failure_causerecords what went wrong, andpid,pidfd,started_at_ns,exit_codeandexit_signalall stay null. There was no process to have a PID or an exit status. exit_codeandexit_signalare populated only when peinit observed an exit — never both, since a process either exits or is killed.- For an Abandoned job,
ended_at_nsrecords when peinit stopped supervising, and the exit fields stay null. Nothing exited.
resolved_identity is the identity string — SYSTEM,
LocalService, a SID — that was resolved for the execution.
token_summary is what the resulting token actually contains. They are
separate because they can differ, and the identity field exposed in
status views and job events is the former.
8.1.3 Retention #
peinit tracks active jobs in memory. When a job reaches a terminal state it emits a structured event carrying the full record and then drops the job. There is no job history in peinit, and no structure that could hold one.
eventd is the historian. It consumes those events from the KMES kernel ring buffer, and a query for a service's past jobs is a query to eventd.
8.1.4 Ownership #
| Concern | Owner |
|---|---|
Restart policy, dependencies, health check schedule, ErrorControl | Service |
| Current state — Active, Failed, … | Service |
| PID, pidfd, exit code, exit signal | Job |
| Execution timestamps | Job |
| Identity and token | Job |
| cgroup assignment | Job |
| Log correlation | Job |
A service tracks its current main job's identifier, and a status query returns it.
8.2 Operations
Peios / Advanced Peios / peinit / Jobs and Operations
An operation is a requested state machine action on a service, as a first-class object. Control commands do not mutate state directly: every one creates an operation that is validated, queued, resolved against whatever else is in flight, and executed by the event loop.
Operations exist because peinit serves concurrent callers — administrative tools, automated triggers, other services. Without them, two commands arriving together collide with whatever behaviour falls out; with them, the resolution is explicit and observable.
8.2.1 Lifecycle #
Pending --> Running --> Completed
| |
+-> Merged +-------> Failed
| |
+-> Cancelled +-----> Aborted
|
+-> Failed
| State | Meaning |
|---|---|
| Pending | Validated and queued, waiting on a precondition. |
| Running | Executing. |
| Completed | The goal was reached. Start: Active for Simple, Completed or Inactive for Oneshot. Stop: the service is no longer running. Reload: the reload resolved. |
| Failed | The goal was not reached — or the operation's maximum lifetime expired while it was still Pending. |
| Merged | Merged into an existing identical operation, whose identifier is recorded. |
| Cancelled | Terminated while Pending. It never executed. |
| Aborted | Terminated while Running. |
Cancelled and Aborted are the same idea at different points: never ran versus was running. Why it happened is a property of the event, not of the state.
8.2.2 Fields #
Operation {
id: GUID
operation_type: enum // Start, Stop, Restart, Reload, Reset
service: string
state: enum
created_at_ns: u64
started_at_ns: u64?
completed_at_ns: u64?
source: enum
caller: token_summary? // admin-initiated only
result: string?
merged_into: GUID?
}
8.2.3 Sources #
Why peinit created the operation:
| Source | Meaning |
|---|---|
Admin | A control client asked for it. |
Boot | The Phase 2 boot plan. |
Shutdown | The shutdown lifecycle. |
DependencyPropagation | A start operation created one for an unsatisfied dependency. |
RestartPolicy | A restart policy generated a start. |
Timer | A timer trigger fired. |
BindsToRecovery | A bound target returned to Active. |
BindsToPropagation | A bound target stopped. |
ConflictResolution | A conflict evicted the running service. |
OnFailure | A failed service's fallback handler. |
Shutdown is declared and labelled but not currently produced: shutdown
transitions services and signals them directly, without creating
operations for the stops (§12.2).
8.2.4 The types #
Start creates a job for the target. Unsatisfied dependencies produce
their own start operations with source DependencyPropagation. It
completes when the service reaches Active, Completed or Inactive as
appropriate, or Skipped when pre-start conditions do not hold.
Stop sends SIGTERM, arms StopTimeout, escalates to SIGKILL. It
completes when the service reaches Inactive, or Failed after a conflict
eviction or bound-dependency propagation.
Restart is a stop then a start, tracked under one identifier across
both phases, and the type stays Restart throughout for observability.
Reload issues the reload command or signal (§6.5) and completes when the reload resolves. Unlike the other lifecycle commands it defaults to not waiting — the caller gets the identifier immediately.
Reset clears Failed, Abandoned or Skipped, taking the service to Inactive. It is synchronous.
8.2.5 Timeouts #
A start, reload or reset inherits the target's StartTimeout as its
maximum lifetime; a stop inherits StopTimeout. A restart has two legs,
each enforced against its own timeout, with the sum as the overall
lifetime.
The clock starts at creation, including queue time. From the
caller's point of view they have been waiting since they sent the
command, not since peinit got round to it. A start that sits Pending
behind a stop for longer than StartTimeout fails without ever running.
8.2.6 Retention #
Pending and Running operations are held in memory. A terminal operation is emitted as an event and dropped after a grace period of 60 seconds — long enough for a polling client to collect the result.
peinit keeps no operation history, for the same reason it keeps no job history. eventd is the historian.
8.3 Conflict Resolution
Peios / Advanced Peios / peinit / Jobs and Operations
When a new operation is requested and one for the same service is already Pending or Running, peinit resolves the two.
8.3.1 Merging #
An operation of the same type merges. The new caller receives the existing operation's identifier, and from their point of view their request is in progress — they neither know nor need to know that it merged.
| Existing | New | Resolution |
|---|---|---|
| Start | Start | Merge |
| Stop | Stop | Merge |
| Reload | Reload | Merge |
| Restart | Start | Merge — a restart already includes a start |
Restart is not mergeable with itself. A second restart while one is in progress is queued.
8.3.2 Cross-type #
| Existing | New | Resolution |
|---|---|---|
| Start (Pending) | Stop | Cancel the start, create the stop |
| Start (Running) | Stop | Abort the start, create the stop |
| Start (Pending) | Restart | Cancel the start, queue the restart |
| Start (Running) | Restart | Queue the restart |
| Stop (either) | Start | Queue the start |
| Stop (either) | Restart | Queue the restart |
| Restart (Pending) | Stop | Cancel the restart, create the stop |
| Restart (Running) | Stop | Abort the restart, create the stop |
| Restart (either) | Restart | Queue |
| Reload (Pending) | Stop | Cancel the reload, create the stop |
| Reload (Running) | Stop | Abort the reload, create the stop |
| Reload (Pending) | Restart | Cancel the reload, create the restart |
| Reload (Running) | Restart | Abort the reload, create the restart |
| Anything (either) | Reset | Reject |
Combinations outside this table are rejected: a new Reload while a Start, Stop or Restart is active, and a new Start while a Reload is active.
8.3.3 The principles #
- Stop wins over start. An explicit stop always takes priority. The administrator said stop, so stop; a queued start can follow.
- Later supersedes earlier. Start then immediately stop means the stop wins and the start is cancelled, recorded as superseded.
- Merging is transparent. The merged caller gets the original identifier and blocks on the original operation's outcome.
Reset is rejected outright while anything is in flight, because reset means "clear a terminal state" and nothing in flight has one.
8.3.4 Dependency propagation #
When a start executes against a service with unsatisfied dependencies, peinit creates start operations for them:
Requires— sourceDependencyPropagation. If one fails, the parent operation fails withDependencyFailure.Wants— sourceDependencyPropagation. If one fails, the parent continues.BindsTorecovery — sourceBindsToRecovery, created when a bound target returns to Active, for dependents sitting in Failed with causeBindsToPropagation.
Dependency-created operations follow the same resolution rules as administrator-created ones. If a dependency is already starting because something else also depends on it, the operations merge — and both graph execution contexts are then associated with the one operation (§7.3).
BindsToRecovery restarts are not subject to the restart budget. They
are created because a dependency returned, not because anything failed.
8.3.5 Restart policy and timers #
A restart-eligible failure creates a start operation with source
RestartPolicy once the backoff delay elapses. It goes through the
ordinary validation and resolution: if an administrator has already sent
a stop, or the budget is exhausted, it is rejected.
A timer firing creates an operation based on the service's current state:
| Type | State | Action |
|---|---|---|
| Oneshot | Inactive, Completed, Failed | Create a start, source Timer. |
| Oneshot | Active, Starting | Set the pending flag. One catch-up run, no operation. |
| Simple | Inactive, Failed | Create a start, source Timer. |
| Simple | Active, Starting | No-op. The firing is recorded. |
The Oneshot catch-up creates its start when the current run completes. Multiple missed firings collapse into one pending run.
8.3.6 Boot and shutdown are not operations #
Boot and shutdown are modes peinit enters, which then generate
per-service operations. There is no "shutdown operation" to observe or
cancel. Boot-generated starts use source Boot.
8.4 Event Emission
Peios / Advanced Peios / peinit / Jobs and Operations
peinit emits a structured event at every job and operation lifecycle
transition, and for its own audit records. All of them go into the KMES
kernel ring buffer through kmes_emit and kmes_emit_batch, encoded as
msgpack per the KMES event-record format (Peios Kernel TRM §2).
There is no event socket. Structured events are not sent to eventd over any connection. eventd consumes them from the ring buffer, which is why they survive eventd being down, being restarted, or not existing yet. The only thing peinit sends eventd over a socket is service output (§11.4), which is a different path with different guarantees.
8.4.1 Job events #
job.created — the job object exists. Carries the job identifier,
service name, type, image path, identity and operation identifier.
job.started — exec succeeded. Carries the job identifier, PID and
cgroup path.
job.ended — the process exited or was killed. Carries the job
identifier, final state, exit code or signal, duration and failure
cause.
The event type is the dotted string; the fields form the msgpack
payload. The payloads are supersets of the summaries above — job.ended
in particular carries the whole record.
8.4.2 Operation events #
Every operation event carries: operation_id, type, service,
source, caller — null for a lifecycle-generated operation — and
state after the transition.
| Event | Adds |
|---|---|
operation.requested | — |
operation.started | — |
operation.completed | duration_ns, result |
operation.failed | duration_ns, failure_reason |
operation.cancelled | reason |
operation.merged | merged_into |
operation.aborted | duration_ns, reason |
duration_ns is measured from creation, not from the start of
execution — the same reasoning as the operation timeout. What a caller
waited is what matters, and queue time is part of it.
The same field appears under three names across the two surfaces:
failure_reason on operation.failed, reason on
operation.cancelled and operation.aborted, and error in the
control interface's operation view (PSPU §4).
8.4.3 Ordering #
When one runtime step produces several lifecycle events, peinit emits them in causal order before committing the retained state for that step. For a terminal pre-start graph dispatch, the terminal event for the operation whose outcome satisfied or failed the graph input precedes the events for the operations that dispatch releases, which preserve graph dispatch order.
Every operation in a graph context is requested when the context is
built rather than when its turn comes, so what a release emits is
operation.started.
8.4.4 Audit and graph events #
peinit's own audit records go through the same path: access.denied for
a refused control command, with the caller's SID, the target, the
requested right by name and the access bits requested and granted;
on_failure.loop_suppressed when the fallback chain guard trips;
graph.validation_error and graph.validation_warning for validation
findings; notify.rejected for an unauthenticated notification;
fd_store.rejected for a refused descriptor; notify.status,
notify.errno and notify.exit_status for the three event-emitting
notification fields; cgroup.leaked the first time a sub-cgroup is found
still populated after its post-kill deadline (§5.7); and
graph.operation_terminal for a graph member's terminal outcome.
Audit records are events rather than logs, and that distinction is the point: the ring buffer persists from the moment PKM loads, so an access denial during Phase 1 is captured before the registry exists, let alone eventd.
8.5 Ad-Hoc Jobs
Peios / Advanced Peios / peinit / Jobs and Operations
An ad-hoc job is an arbitrary supervised process submitted by a service on behalf of its own client. It has no persistent definition: it runs once, reports, and is cleaned up.
8.5.1 The delegation problem #
A service — a privileged action broker, say — wants peinit to run a process as one of its users. It has impersonated that user's token, but KACS will not let it forward that token over IPC without Delegation-level impersonation. Fork inheritance works, because the kernel copies the token naturally, but then the service has to supervise the process itself, which defeats the point of having a service manager.
JFS — the Job Forwarding Subsystem — is the kernel's answer. It captures
the caller's effective token and delivers it, with a job definition, to
whatever holds /dev/jfs open. peinit is the consumer, and JFS is a
generic primitive rather than something built for peinit.
peinit opens /dev/jfs during Phase 1 infrastructure setup and adds the
descriptor to its event loop. If nothing has the device open, a
submitter's syscall returns ENODEV.
8.5.2 The shape of a request #
handle_jfs_request(request):
(job_definition, token_fd) = read from /dev/jfs
validate the image path, arguments, working directory
job = Job { id: new_guid(), service: null, type: AdHoc,
state: Created, token_summary: summarise(token_fd), ... }
write job.id back to /dev/jfs // unblocks the caller
fork, install token_fd on the child, exec
emit job.created, job.started, job.ended
8.5.3 The definition #
A subset of the service definition's fields, arriving as structured data rather than as registry values:
| Field | Required | Meaning |
|---|---|---|
| ImagePath | yes | The binary to execute. |
| Arguments | no | Its arguments. |
| Environment | no | Additional variables, as a map rather than KEY=VALUE strings. |
| Timeout | no | Maximum runtime in seconds. 0 means no limit. |
| WorkingDirectory | no | Defaults to /. |
| Description | no | For logs. |
The service-level fields deliberately absent are the policy ones:
RestartPolicy and its parameters, the four dependency fields,
HealthCheck, WatchdogTimeout, ErrorControl, SafeMode and
Triggers. Those belong to a persistent definition; an ad-hoc job runs
once.
8.5.4 Identity #
The job runs with the token JFS captured — the caller's effective identity at the moment of the syscall. An impersonating caller produces a job running as the impersonated user; a caller using its own primary token produces a job running as itself.
There is no identity field. A submitter cannot name an arbitrary identity, only pass through the one it already holds. That is what stops the mechanism being an escalation: a service cannot create jobs as principals it could not already act as.
8.5.5 Lifecycle #
- Forked with the JFS-provided token.
- Runs in its own cgroup under
/sys/fs/cgroup/peinit/, the id derived from the job's GUID rather than from a service name. - Output routed to eventd, tagged with the job's GUID.
- On exit: emit
job.ended, clean up the cgroup, drop the job. - No restart, no dependencies, no health checks.
Exceeding Timeout sends SIGTERM, waits the schema default
StopTimeout of 10 seconds, then SIGKILL — the same escalation as a
service stop.
Ad-hoc jobs bypass the operations model entirely. A JFS request creates a job directly, because there is no service to start and therefore no state machine action to represent:
Admin --> start command --> Operation --> peinit forks --> Job
Broker --> JFS request --> peinit forks --> Job
Timer --> timerfd fires --> Operation --> peinit forks --> Job
8.5.6 The current state of JFS #
JFS does not exist in the kernel. There is no /dev/jfs device, no
request encoding, and no mechanism for delivering a captured token
descriptor across a device read.
peinit's side is built up to that boundary and stops there. Phase 1
opens /dev/jfs if it is present, and a failure to open is a warning
that does not affect the boot. The descriptor is registered with the
event loop, and on the first readable event peinit reads nothing,
unregisters the descriptor and permanently disables the source.
The job machinery for ad-hoc jobs exists as far as the record: the job
type, the GUID-derived cgroup id, and a constructor. There is no launch
path — an ad-hoc job cannot currently be started — and the definition
fields above have no decoder to arrive through, so Timeout,
Environment and WorkingDirectory have nowhere to come from.
The byte-level /dev/jfs interface belongs to JFS rather than to
peinit, and this chapter describes the shape of the consumption
protocol rather than its encoding.
8.6 What a Caller Sees
Peios / Advanced Peios / peinit / Jobs and Operations
Operations are how a caller observes something taking effect. This article covers what the model looks like from the outside; the wire contract itself is PSPU §4.
8.6.1 Every lifecycle command returns an identifier #
A command that creates, merges into, queues, cancels, clears or executes an operation returns that operation's identifier. A caller can then poll it, or block on it.
Two cases return no identifier, because no operation exists: a command whose target is already in the state it asks for, and one that has no effect at all — a stop on an Inactive service. Both return the service's status instead of an acknowledgement, which is the honest answer.
Commands that never create an operation — status, list,
operation-status — have their own shapes.
8.6.2 Waiting #
Lifecycle commands block by default until the operation is terminal. The
exception is reload, which returns immediately unless asked otherwise,
because a reload's outcome is often advisory and a caller usually wants
the identifier rather than the wait.
A waiting connection is not idle and is never closed by the idle timeout, however long the operation runs. It is bounded by the operation's own lifetime instead.
8.6.3 Merging is invisible #
A caller whose command merged receives the surviving operation's
identifier and blocks on that operation's outcome. Nothing tells them
they merged, because there is nothing they could usefully do about it.
The consequence worth knowing is that the identifier a caller gets back
may be older than their request, and its created_at will be earlier
than when they sent it — which is exactly right, because that is when
the work they are waiting on actually began.
8.6.4 What an operation's result carries #
A completed operation carries the resulting service state. A failed one carries the failure reason. A merged one carries the survivor's identifier. A cancelled or aborted one carries why.
For a reload, the result also determines the reload's mode — whether
the service confirmed the reload with a READY=1, whether peinit is
merely assuming it happened, or whether it outright failed.
9.1 Calendar Expressions
Peios / Advanced Peios / peinit / Timers
A timer schedule is a calendar expression in systemd's OnCalendar
format. The grammar below is what peinit parses.
DayOfWeek Year-Month-Day Hour:Minute:Second Timezone
Every field is optional, and which fields are present is worked out positionally from their shape.
| Field | Form | Default if omitted |
|---|---|---|
| DayOfWeek | weekday names | any day |
| Date | Year-Month-Day | *-*-* |
| Time | Hour:Minute:Second | 00:00:00 |
| Second | the :Second of Time | :00 |
| Timezone | an IANA name | system-local |
A time-only expression such as 02:00:00 implies the date *-*-*.
Hour:Minute alone defaults the seconds to 00.
Years range 0–9999, months 1–12, days 1–31.
9.1.1 Component syntax #
Every numeric component, and the weekday, accepts:
- Wildcard —
*matches anything. - List — comma-separated values:
1,15. - Range — two values around
.., inclusive:Mon..Fri,8..17. A reversed range is a parse error rather than a wrap-around. - Repetition — a value or range suffixed with
/and a step.value/stepmatches the value and every multiple of the step above it, so0/15in the minute field is 0, 15, 30 and 45.start..end/stepwalks from start to end inclusive.
Weekdays are English names, case-insensitive, abbreviated or full, and
accept lists and ranges. Mon and Monday are the same day, as are
Tue, Tues and Tuesday.
9.1.2 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. *-*~01 is the last day of every month; *-02~03 is the
third-to-last day of February.
Repetition combines with it, and steps in the day-of-month direction —
which means it walks the offset downwards, towards the end of the
month. Mon *-05~07/1 covers the last seven days of May, and combined
with Mon resolves to exactly one day: the last Monday in May.
Wildcards, ranges and lists are also accepted after ~, so ~* matches
every day and ~03..07 the third- to seventh-to-last.
9.1.3 Named shortcuts #
| Shortcut | Equivalent |
|---|---|
minutely | *-*-* *:*:00 |
hourly | *-*-* *:00:00 |
daily | *-*-* 00:00:00 |
weekly | Mon *-*-* 00:00:00 |
monthly | *-*-01 00:00:00 |
quarterly | *-01,04,07,10-01 00:00:00 |
semiannually | *-01,07-01 00:00:00 |
yearly, annually | *-01-01 00:00:00 |
Shortcut names are case-insensitive and accept a trailing timezone, so
daily UTC is valid.
9.1.4 Timezones #
Timezone specifiers are IANA database names — Europe/London,
US/Eastern, UTC. An expression with no timezone is interpreted in
system-local time.
A name is validated when the expression is parsed and resolved again at evaluation. An unrecognised zone is a hard parse error, not a silent fallback to UTC.
9.1.5 Daylight saving #
- Spring forward, where the clock skips an hour: a scheduled time
falling inside the skipped interval does not fire. peinit moves on to
the next scheduled second, then the next date. A schedule of
*-03-31 01:30 Europe/Londonskips 2024 entirely. - Fall back, where an hour repeats: a scheduled time inside the repeated interval fires exactly once, on the first occurrence. On re-arming, the same civil time resolves to the same instant, which is not later than the firing that just happened, so the timer advances to the next day rather than firing again.
9.1.6 Precision #
Second-level. Unlike systemd, peinit does not accept fractional seconds — service scheduling has no use for finer granularity, and dropping it keeps the parser simpler. A fraction anywhere in the time component is a parse error.
9.1.7 Examples #
| Expression | Meaning |
|---|---|
*-*-* 02:00:00 | Every day at 2am, system-local. |
Mon *-*-* 00:00:00 | Every Monday at midnight. |
*-*-1,15 12:00:00 | The 1st and 15th, at noon. |
*-*~01 00:00:00 | The last day of each month, at midnight. |
Mon..Fri *-*-* 09:00:00 | Weekdays at 9am. |
*-*-* *:00/15:00 | Every fifteen minutes. |
*-*-* 02:00:00 Europe/London | Every day at 2am London time. |
9.2 Evaluation and Arming
Peios / Advanced Peios / peinit / Timers
A timer is a trigger, not a service type. A service with a
timer:<schedule> trigger is an ordinary Simple or Oneshot service that
peinit starts on a schedule.
9.2.1 Arming #
At boot, once the service graph is loaded, and whenever timer configuration changes, peinit computes the next firing time for every active trigger and arms a timerfd for it. Each trigger gets its own descriptor and its own computation.
A disabled service gets neither a registration nor a firing.
A schedule that fails to parse, or whose next occurrence cannot be computed, fails that trigger. Every other timer arms normally, and what did not arm is reported to the console. This matches how graph validation already treats an invalid schedule (§7.4), so the outcome no longer depends on which of the two caught it.
The next-occurrence search looks ten years ahead and then gives up. A
schedule can parse and still match nothing — *-02-30, or a fixed year
already past — and the horizon turns that into a prompt error against
the one service rather than a very long walk. Ten years clears the
sparsest schedule that is genuinely meaningful: *-02-29 skips a
century year not divisible by 400, so it can run eight years dry.
9.2.2 Firing #
handle_timer(service, trigger):
// 1. Decide what the firing means, from the service's state.
match (service.type, service.state):
(Oneshot, Active | Starting):
service.pending_timer = true // at most one
(Simple, Active | Starting):
record the firing; no action
(_, Inactive | Completed | Failed):
create_operation(Start, service, source = Timer)
// 2. Record when it fired.
write the last-run timestamp to the registry // asynchronously
// 3. Re-arm.
next = next_occurrence(trigger.schedule, now) + random(0, TimerJitter)
arm an absolute CLOCK_REALTIME timerfd for next
Every other state — Backoff, Stopping, Reloading, Abandoned, Skipped — records the firing and does nothing.
The last-run write happens in a forked child so that the event loop never waits on the registry. The parent returns immediately, and the child is reaped as an untracked orphan; a failed write is visible only as that child's exit status.
9.2.3 Oneshot pending runs #
A Oneshot that fires while it is already running sets a flag rather than queueing an operation. When it next reaches Inactive or Completed, peinit immediately creates a start operation and clears the flag.
Multiple firings during one run collapse into a single pending run. There is no queue, and the flag is per service rather than per trigger — a service with three timers that all fire during one long run still gets exactly one catch-up.
9.2.4 Multiple triggers #
Triggers on one service are independent: each has its own timerfd, its own next-firing computation, and its own last-run history. Only the Oneshot pending flag is shared.
9.3 Persistence
Peios / Advanced Peios / peinit / Timers
TimerPersistent, on by default, controls whether a run missed across a
reboot is caught up.
9.3.1 Where history lives #
Last-run timestamps are REG_QWORD values in the registry, written
after a firing.
A service with a single timer trigger stores its timestamp as
LastTimerRun on the service's own key:
Machine\System\Services\<name>\LastTimerRun
A service with multiple triggers stores one per trigger under a subkey, named by the schedule string:
Machine\System\Services\<name>\TimerState\<encoded-schedule>
A schedule contains characters — spaces, :, * — that are not valid
LCS value names, so the name is the schedule with every character
outside [A-Za-z0-9._-] percent-encoded, with uppercase hex digits.
This is the same encoding used for cgroup ids (§5.1). The schedule
*-*-* 02:00:00 is stored as:
%2A-%2A-%2A%2002%3A00%3A00
Two identical schedule strings on one service encode to the same name and therefore share one timestamp.
Timer firings are infrequent, so the write cost is negligible.
9.3.2 Catching up #
On boot, for each persistent trigger:
- Read the last-run timestamp.
- Compute the next scheduled firing after it.
- If that time has already passed, at least one run was missed: fire once, immediately.
- Compute the next future occurrence normally.
Catch-up is always a single run however many were missed. A daily timer that missed five days fires once on the next boot, not five times.
A trigger with no history at all is treated the same way, so its first boot produces one catch-up firing.
TimerPersistent=0 ignores history entirely — peinit does not even read
the registry for that trigger, and computes the next occurrence from
now.
9.3.3 When the timestamp is written #
The timestamp is written after the timer fires and the start is initiated, not after the service finishes. A service that crashes mid-run is not re-triggered on the next boot: the run was attempted, not missed.
A configuration reload re-arms every timer from the current time with no
catch-up, whatever TimerPersistent says. History is consulted at boot
only.
9.4 Jitter and Clocks
Peios / Advanced Peios / peinit / Timers
9.4.1 Jitter #
TimerJitter, zero by default, adds a random delay to each firing.
peinit draws a uniformly random whole number of seconds from zero to
TimerJitter inclusive, from the kernel's random source, and adds it to
the computed occurrence.
The delay is recomputed on every firing, so a daily timer with
TimerJitter=900 fires at a different moment between 00:00 and 00:15
each day. With TimerJitter=0 no randomness is consulted at all.
Jitter is applied after the calendar expression is evaluated and is only ever added, so a timer never fires early — only late.
The boot catch-up firing is not jittered. It fires immediately, and jitter applies from the next armed occurrence onward.
9.4.2 Which clock #
The split is the point of this section, and it is not cosmetic.
Calendar timers are wall-clock schedules, so they are armed as
absolute CLOCK_REALTIME timers: timerfd_settime with
TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET. CANCEL_ON_SET makes the
descriptor's read return ECANCELED whenever the realtime clock is
discontinuously changed — an NTP step, a manual set. peinit recomputes
the next occurrence against the new wall clock and re-arms, which is
what keeps *-*-* 02:00:00 anchored to 02:00 across clock corrections.
Interval timers are genuine relative durations and use
CLOCK_MONOTONIC: the watchdog, health check intervals and timeouts,
restart backoff, and the Start, Stop and Reload phase timeouts. "Wait
thirty seconds" means thirty elapsed seconds regardless of what happens
to the wall clock. They are not armed with CANCEL_ON_SET, correctly —
a monotonic timer has no reason to be cancelled by a realtime set.
These are not separate descriptors. Every interval deadline is aggregated onto one monotonic timerfd armed to the earliest of them.
Last-run timestamps are recorded on CLOCK_REALTIME, since they
record when a timer actually fired in wall-clock terms. The same firing
passes a monotonic timestamp into the operation machinery, because
operation timing is elapsed time.
9.4.3 Clock events #
- A realtime step at runtime. The armed timer is cancelled; peinit recomputes against the new wall clock and re-arms. A backward step pushes the next firing later; a forward step that crosses an occurrence fires it once. If the step lands inside a jitter window, the firing happens at the un-jittered scheduled time — later than the schedule, never earlier.
- Suspend and resume. An absolute deadline that elapsed while suspended fires once on resume. The expiration count is ignored, so a long suspend produces one firing, not one per occurrence.
- A missed occurrence within one uptime. Fire once, then compute the next future occurrence. peinit never replays every occurrence that elapsed during a gap — the same rule as the cross-reboot catch-up.
- A backward jump across a boot. If the last-run timestamp is in the future relative to the current wall clock at boot, peinit treats the history as unknown and fires the catch-up immediately. This check is boot-time only; there is no runtime equivalent.
- A wrong clock at boot. A system that boots with a badly wrong
clock and has NTP correct it later may fire a persistent catch-up
spuriously or not at all. The runtime half is covered by
CANCEL_ON_SET— once NTP corrects the clock, armed calendar timers are cancelled and recomputed — but the boot-time catch-up decision has already been made by then. Short of NTP-aware rescheduling, this remains an edge.
10.1 The Control Socket
Peios / Advanced Peios / peinit / The Control Interface
peinit serves every runtime command on a Unix stream socket at
/run/services/peinit/control.sock, created during Phase 1
infrastructure setup and existing for the lifetime of the system. Its
wire protocol is specified in PSPU §4; this chapter is how peinit
implements its side.
10.1.1 Creation and protection #
The socket is created with SOCK_CLOEXEC | SOCK_NONBLOCK and a listen
backlog of 32, and unlinked when peinit drops it. Accepted connections
come from accept4 with both flags, so no connection descriptor is ever
inherited by a service.
peinit sets no POSIX mode bits on the socket, on the notification socket, or on anything else it creates. Under KACS, mode bits are not what governs access — a Security Descriptor is — so setting them would be inert.
What governs access is inheritance. /run is a tmpfs peinit mounts
itself in Phase 1, and a fresh tmpfs carries no descriptor at all, which
under DENY_MISSING would leave every inode on it unreachable to
everything. So peinit stamps the mount root with an inheritable
descriptor as soon as it mounts it (§2.3):
O:SY G:SY D:(A;OICI;GA;;;SY)
Every inode created underneath inherits from it, the two sockets
included. The parent directory /run/services/peinit/ is created
plainly, with no descriptor of its own, so it inherits too.
The effect is that both sockets are reachable by SYSTEM and by nothing
else. The single inheritable entry grants GENERIC_ALL to S-1-5-18
and names no other principal, and connecting to a pathname socket is
checked against the socket inode's descriptor before any peer identity
is established.
10.1.2 Connections #
peinit accepts a connection, obtains the peer's token, and only then admits it against the connection limit:
| Key | Default | Meaning |
|---|---|---|
Machine\System\Init\MaxControlConnections | 32 | Concurrent connections. |
Machine\System\Init\MaxRequestSize | 65536 | Maximum request size, in bytes. |
Machine\System\Init\ConnectionTimeout | 30 | Seconds before an idle connection is closed. |
A connection over the limit is closed at the socket level, before any request is read and without a response — there is no error code for it, because there is no protocol state in which to deliver one. A peer whose token cannot be obtained is closed the same way.
10.1.3 The peer token #
The token is captured once, when the connection is accepted, using
kacs_open_peer_token. It is the peer thread's effective token at
that moment, so a peer that was impersonating is captured as the
impersonated identity — which is what makes access decisions reflect the
identity a client is actually operating under rather than its underlying
service identity.
Because it is captured once, a peer that changes identity mid-connection is still evaluated against the identity it connected with.
10.1.4 Idle and waiting #
A connection is idle only when it has nothing in flight. One blocked on
a wait=true operation, or with output still buffered, is never idle
and is never closed by ConnectionTimeout — it stays open until the
operation resolves, bounded by the operation's own timeout rather than
the connection's.
peinit handles one frame per readiness turn, and reads no further frames from a connection while a wait is pending on it. Pipelined requests are therefore serialised behind a wait.
10.1.5 Timestamps #
Every timestamp peinit puts on the wire is derived by projecting a monotonic event stamp through the current offset between the realtime and monotonic clocks. Elapsed-time decisions stay monotonic; only the presentation is wall-clock.
10.2 Dispatch and Authorisation
Peios / Advanced Peios / peinit / The Control Interface
A parsed command runs a fixed sequence before it does anything.
- The shutdown gate. If peinit is shutting down, everything except
status,listandoperation-statusis rejected. The gate runs before the access check, so during shutdown a caller who would have been denied is told the command is invalid for the current state rather than that they lack the right. - Resolve the target. A command naming no definition, and no
addressable definition-removed entry, returns
UNKNOWN_SERVICE. peinit does not synthesise a descriptor for something that does not exist. - AccessCheck. The caller's token, the target's descriptor, the generic mapping, and the right the command requires (§4.6, §4.7).
- On denial, return
ACCESS_DENIEDand record anaccess.deniedevent carrying the caller's SID, the target, the requested right by name, and the access bits requested and granted. Silent denial is not acceptable; a denial an administrator cannot see is indistinguishable from a bug. - On grant, classify the command against the service's state (§10.3) and act.
10.2.1 Rights #
| Command | Right |
|---|---|
start | SERVICE_START |
stop | SERVICE_STOP |
restart | SERVICE_START and SERVICE_STOP |
reload | SERVICE_INTERROGATE |
reset | SERVICE_STOP |
status | SERVICE_QUERY_STATUS |
list | Filtered per service by SERVICE_QUERY_STATUS |
operation-status | SERVICE_QUERY_STATUS on the target service |
shutdown | SYSTEM_SHUTDOWN |
reload-config | SYSTEM_RELOAD_CONFIG |
operation-status resolves the operation before it checks the right, so
an unknown identifier is reported as unknown regardless of who asked.
10.2.2 Filtering #
list checks every service and partitions the result. Services the
caller can query are returned; services it cannot are omitted, and the
denials become audit events rather than anything the caller sees. A
caller with no query rights anywhere gets an empty list and a successful
response, not a denial — the filtering exists to avoid answering the
question "does this service exist", and reporting the denials would
answer it.
Definition-removed services are listed, and the list entry does not say
so. A status query on one does.
10.3 The Command × State Matrix
Peios / Advanced Peios / peinit / The Control Interface
A command sent to a service in an unexpected state gets an answer, not a silent no-op. What the answer is depends on the pair.
| Inactive | Starting | Active | Reloading | Stopping | Completed | Backoff | Failed | Abandoned | Skipped | |
|---|---|---|---|---|---|---|---|---|---|---|
| start | Start | MERGE | ALREADY | ALREADY | QUEUE | Start | DEFER | Start | ERROR | Start |
| stop | NOOP | Cancel+Stop | Stop | Stop | MERGE | Clear | Cancel | NOOP | ERROR | NOOP |
| restart | Start | QUEUE | Restart | Restart | QUEUE | Start | Restart | Start | ERROR | Start |
| reload | ERROR | ERROR | Reload | MERGE | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR |
| reset | NOOP | ERROR | ERROR | ERROR | ERROR | ERROR | ERROR | Clear | Clear | Clear |
| status | OK | OK | OK | OK | OK | OK | OK | OK | OK | OK |
ALREADY — the service is already where the command would take it and no operation of that type is in flight. peinit returns the current status rather than an error.
MERGE — an operation of that type is already running. The command merges into it; the caller receives that operation's identifier and, if waiting, blocks on its outcome.
DEFER — create a Pending start operation but do not execute it until the existing backoff deadline expires. A deferred start already present is merged into.
QUEUE — the operation is queued Pending and executes after the current one completes.
NOOP — the command has no effect. peinit returns the status.
ERROR — the command is invalid for the state.
Clear — reset to Inactive.
Cancel — abort the current operation, then proceed.
10.3.1 The Backoff column #
Backoff is the interesting one, because the service is down with an automatic restart already pending.
startcreates or merges into a deferred start operation and honours the remaining delay. It does not short-circuit the backoff. If the automatic restart later becomes due, it merges into the administrator's operation, so the identifier the caller holds is the one that executes.stopcancels both the pending restart and any deferred start, and the service goes Inactive. A subsequent automatic restart is refused, because the service is no longer in Backoff.restartcancels the automatic restart and queues an administrator-initiated one.reloadandresetare invalid: there is no process to reload, and no terminal state to clear.
10.3.2 The Skipped column #
start and restart clear Skipped before they run. A Skipped service
is not in a state a start can proceed from — the state machine permits
Skipped -> Inactive and nothing else — so the activation performs that
transition first, then re-evaluates the conditions from scratch. Both
outcomes are possible: the precondition that was missing at boot may now
hold, in which case the service starts; or it may still not, in which
case the service is skipped again, for whatever reason applies now.
The clear is reported like any other transition, so a console watching the service sees it leave Skipped rather than appearing to jump.
reset also clears Skipped, and differs only in stopping there.
10.3.3 The Abandoned column #
Every lifecycle command is invalid on an Abandoned service except
reset, which clears it (§6.2). Nothing else is meaningful while
processes that ignored SIGKILL are still in the cgroup.
10.3.4 Definition-removed services #
Independently of state, a service whose definition has been removed
(§3.8) rejects start, restart and reload with UNKNOWN_SERVICE,
accepts stop, and reports its state on status.
10.4 reload-config
Peios / Advanced Peios / peinit / The Control Interface
reload-config takes a fresh snapshot of the configuration from the
registry. It does not live-update anything.
It is also the path a registry change notification takes: any drained watch event triggers the same full reload, rather than a targeted re-read of whatever changed.
10.4.1 Atomicity #
peinit reads everything first. Every registry read happens before any mutation, so a read failure returns an error with nothing touched. It then builds and validates a complete new graph in memory, and only swaps it in if validation succeeds.
If validation fails, the previous generation stays live and the findings are returned to the caller. This is where the reload path differs sharply from boot: boot marks individual services Failed and carries on, because it has to produce a running system; reload rejects the whole thing, because it has a running system already and a half-applied configuration would be worse than the one in place.
10.4.2 What changes #
- Every service definition is re-read.
- A new dependency graph is built and validated.
- Running services are unaffected and continue on their activation generation.
- New definitions take effect at the next start, restart, or trigger.
- New services become available for
startimmediately. - Timer triggers are re-evaluated and every calendar timer is re-armed, from the current time and with no catch-up.
A reload also refreshes things that are not service definitions: the control descriptor, the three control socket limits, the log configuration, the shutdown settings, the global environment layer, and the eventd log socket path. It also prunes the fd stores of services that no longer exist.
10.4.3 Removals and the compiled-in service #
A definition that has disappeared is handled by §3.8 — discarded if nothing is running, marked definition-removed if something is.
registryd is exempt. Its compiled-in definition survives a reload that does not mention it, and its provenance survives a registry entry that shadows it. peinit started registryd before the registry existed, and a reload finding no definition for it cannot conclude that it should stop being managed.
10.5 The Notification Socket
Peios / Advanced Peios / peinit / The Control Interface
peinit binds one Unix datagram socket for service notifications, by
default at /run/services/peinit/notify.sock. Its path is an
implementation detail: services receive it through NOTIFY_SOCKET and
nothing hardcodes it. The kernel command line can override it with
peios.notifysocket=.
There is one socket, not one per service, and the bind unlinks any stale path first. It carries the same inherited descriptor as the control socket (§10.1).
The protocol itself is PSPU §4. What follows is how peinit decides whether to believe a datagram.
10.5.1 Authenticating a sender #
SO_PASSCRED is enabled on the socket, so every datagram arrives with a
kernel-attested SCM_CREDENTIALS control message. A datagram without one
is rejected outright. Descriptors for the fd store arrive alongside, as
SCM_RIGHTS.
Authentication then runs five steps, and each closes a hole the previous one leaves:
- Find the sender. Scan for the current service-main job whose
PID equals the sender's. Only a main job is ever a candidate, which
is what makes
NotifyAccess=Mainthe only mode there is — a hook or a health check cannot notify on a service's behalf. - The job is Running. A job still in pending setup has not exec'd.
- The job has a pidfd.
- The pidfd still refers to that PID. This is the step that matters: PID matching alone is racy, because a PID can be recycled between the sender writing and peinit reading. The pidfd was obtained atomically at fork, so verifying the PID against it is what makes the match sound rather than probable.
- The generation matches. A job whose activation generation is
not the service's current one is a previous incarnation, and its
notifications are rejected. This is invariant 5 of §6.1 in force: a
READY=1from the process that just crashed cannot mark its replacement ready.
Anything that fails is dropped and recorded as a notify.rejected event
carrying the sender's PID and the reason.
The UID and GID in the credentials are parsed and never used. They are not policy inputs, and peinit does not consult them for anything — identity on Peios is a token, and the token here is established by which job the sender is, not by what UID it claims.
10.5.2 Applying a datagram #
A datagram may carry several newline-separated lines, and peinit applies every one. Parsing happens before application and is all-or-nothing: if any line is malformed, nothing from that datagram is applied, and any descriptors it carried are dropped and closed. Partial application of an ambiguous service-control message is structurally impossible rather than merely avoided.
A rejection is recorded after authentication, so the event can name the service where one could be established.
10.5.3 What the fields do #
Most are handled elsewhere: READY=1 and RELOADING=1 in §6.5,
WATCHDOG=1 and WATCHDOG_USEC and EXTEND_TIMEOUT_USEC in §6.6,
STOPPING=1 in §12.2, and the fd store fields in §10.6.
Three are event-emitting. STATUS=, ERRNO= and EXIT_STATUS= are
authenticated and then emitted as KMES events — notify.status,
notify.errno, notify.exit_status — whose payloads carry the service
name, the job identifier, the operation identifier and the activation
generation, alongside the value. They take the same path as job and
operation events, not a forward to eventd.
STATUS= is additionally stored on the service's runtime state and
exposed as status_text in a status query. It is cleared to null at the
start of every activation generation, in the same step that increments
the generation, so a status string cannot survive a restart and describe
a process that no longer exists.
ERRNO= and EXIT_STATUS= are not stored. They are emitted and
otherwise not retained.
10.5.4 Bounds #
A datagram is read into a fixed 64 KiB buffer, and the control message
buffer is sized for 64 descriptors. Neither MSG_TRUNC nor MSG_CTRUNC
is inspected, so a larger datagram is truncated silently and descriptors
beyond the sixty-fourth are dropped by the kernel before peinit sees
them.
10.6 The Fd Store
Peios / Advanced Peios / peinit / The Control Interface
The fd store lets a service keep file descriptors across its own restart. It pushes them to peinit, peinit holds them, and the new process gets them back. That is what lets a stateful daemon — a web server holding a listening socket, say — restart without dropping connections it has already accepted.
FdStoreMax in the definition sets the maximum number of descriptors
peinit will hold. It defaults to 0, which disables the store: most
services do not need it, and holding descriptors on behalf of a service
that will never ask for them back is pure cost.
10.6.1 Storing #
When an authenticated datagram carries FDSTORE=1 with descriptors
attached:
- If
FdStoreMaxis 0, peinit logs the rejection and closes the descriptor. - If the store already holds
FdStoreMaxentries, peinit logs the rejection and closes the descriptor. The existing store is not modified — a full store does not evict. FDNAME=<name>names it; an absent or empty name meansstored.FDPOLL=0marks the descriptor exempt from poll monitoring.
Either rejection emits an fd_store.rejected event carrying the outcome
and the reason, so a service whose descriptors are being silently
dropped can find out why.
Several descriptors may share a name. One FDSTORE=1 carrying N
descriptors creates N entries under the one name, each independently
subject to the limit — so the first few fit and the overflow is
rejected and closed.
peinit does not monitor stored descriptors. The FDPOLL flag is
recorded and nothing reads it, so no stored descriptor is evicted for
becoming invalid.
10.6.2 Removing #
FDSTOREREMOVE=1 with FDNAME=<name> removes every descriptor of that
name and closes them. A name matching nothing is a no-op rather than an
error.
FDSTOREREMOVE=1 without a name aborts the whole fd-store step for
that datagram — so a datagram carrying both an unnamed remove and an
FDSTORE=1 performs neither, and the attached descriptors are dropped
and closed.
10.6.3 Injecting #
When a service restarts, peinit injects the stored descriptors during the child's pre-exec path (§5.4):
- They are placed consecutively from
SD_LISTEN_FDS_START— descriptor 3 — upward, with close-on-exec cleared: the only sanctioned exception to the close-on-exec discipline. LISTEN_FDSis set to the count.LISTEN_FDNAMESis set to a colon-separated list of names in the same order as the descriptor numbers.- The store is cleared. peinit no longer holds them.
Both variables are omitted entirely when the store is empty.
LISTEN_PID, which a conforming client checks against its own PID
before trusting LISTEN_FDS, is not set.
Injection happens for the main process only. Hooks and health checks never receive stored descriptors.
The store is cleared on a successful injection, at the top of the started-launch handling. A launch that fails does not clear it, so the descriptors survive a failed attempt and are available to the next one.
10.6.4 Clearing #
The store is cleared, and its descriptors closed, when:
- The service is stopped explicitly — by an administrator, or by shutdown. The distinction peinit draws is the operation's type and source together: an administrator's stop clears, and a restart-policy-sourced stop does not. The service is not coming back from an explicit stop, so the descriptors are no longer useful.
- The definition is removed and its entry finally discarded (§3.8), immediately if nothing was running and on the instance's exit otherwise.
It survives an automatic restart — crash, restart policy, new start — which is the entire point. The descriptors persist through exactly the restart the service did not choose and cannot prepare for.
11.1 Wiring
Peios / Advanced Peios / peinit / Output Handling
peinit is not a logging system — eventd stores, indexes and queries logs. But peinit holds the pipes at birth. It decides where a service's output goes, and it has to cover the window before eventd exists.
11.1.1 The pipes #
Before exec, peinit creates a pipe for stdout and one for stderr with
pipe2(O_CLOEXEC). The child's streams are redirected onto the write
ends; peinit keeps the read ends and watches them with epoll.
The blocking discipline is asymmetric, deliberately:
- The parent's read ends are non-blocking. PID 1 cannot afford to block on a read.
- The child's write ends keep ordinary blocking semantics.
That second half is what makes backpressure work. When a service
produces faster than peinit consumes, the kernel pipe buffer fills and
the service's own write() blocks, so the service slows down. Making
the write ends non-blocking would turn that into EAGAIN failures in
the service, converting a flow-control mechanism into an error the
service has to handle.
The child's stdin is redirected to /dev/null. peinit provides no
interactive input channel; a service needing input obtains it explicitly
— through a socket, or a stored descriptor — never through inherited
stdin.
The epoll instance itself is close-on-exec and is never inherited.
11.1.2 Tagging #
peinit reads output line by line and tags each line with:
- the origin,
- the stream — stdout or stderr,
- a
CLOCK_REALTIMEtimestamp, - the job's identifier.
The origin is the service name for a main process. For a hook it is the
service name, the hook kind and its index — jellyfin/ExecStartPre[0].
Reload commands are <service>/ExecReload and health checks
<service>/HealthCheck.
Health check output is captured, which is usually the only way to find out why a health check failed.
11.1.3 Terminal-attached services #
A service with a TTYPath has all three streams on its terminal, and
both pipe pairs are closed in the child (§5.4). Its output is not
captured at all — it goes to the terminal, which is what asking for one
means.
11.2 The Pre-Eventd Buffer
Peios / Advanced Peios / peinit / Output Handling
Before eventd starts there is nowhere to send service output: eventd's log socket does not exist until eventd binds it. peinit buffers in memory until then, in a bounded buffer that drops the oldest entries when it fills.
| Key | Default | Minimum | Meaning |
|---|---|---|---|
Machine\System\Init\PreEventdBuffer | 1048576 | 4096 | Bytes of output retained before eventd exists. |
A value below the minimum keeps the default and logs a warning, rather than being honoured or failing the boot (§11.3). Zero is the case that matters: a zero-capacity buffer rejects every record, so honouring it would silently discard the whole pre-eventd window.
The value is read from the registry at boot and refreshed on reload, and the buffer adopts the new capacity each time. Lowering it takes effect immediately, dropping the oldest entries until the contents fit — the same end of the buffer a steady-state overrun drops.
Dropping the oldest rather than the newest is the right way round for this buffer: the point of the window is the boot that is happening now, and the most recent output is what explains where it got to.
11.2.1 Audit records are not logs #
peinit's own audit records — access denials, critical failures, recovery mode entry, graph errors, security-relevant transitions — are events rather than logs. They go into the KMES kernel ring buffer, which persists from the moment PKM loads: before eventd, before the registry, before Phase 2.
So there is no pre-eventd buffer for them, and no window in which they could be lost. eventd picks them up from the ring buffer when it attaches, wherever in the boot they were emitted.
That distinction is the reason the two paths exist at all. Logs are best-effort and lossy by design; audit events are neither.
11.3 Flood Protection
Peios / Advanced Peios / peinit / Output Handling
A noisy service cannot be allowed to starve the event loop. Three bounds apply.
| Key | Default | Minimum | Meaning |
|---|---|---|---|
Machine\System\Init\MaxLogLineLength | 8192 | 256 | Bytes per line before truncation. |
Machine\System\Init\MaxLogBufferPerService | 65536 | 4096 | Bytes buffered per service pipe before backpressure. |
Machine\System\Init\LogReadBytesPerEvent | 16384 | 512 | Bytes drained from one pipe per readable event. |
A value below the minimum is not honoured and does not fail the
boot: the compiled-in default is used and a warning is logged naming the
key, the configured value and what was used instead. The same rule
covers PreEventdBuffer (§11.2), and it is the rule peinit already
applies to the equivalent kernel command-line knobs — a typo in a
logging knob must not decide how the machine boots, and must not be
silent either.
MaxLogBufferPerService's minimum is the one with an external cause:
F_SETPIPE_SZ will not go below one page and rounds up regardless, so a
smaller number in the registry would describe a pipe that does not
exist. The other minimums are engineering judgement — the point below
which the mechanism stops working rather than working differently.
A line exceeding MaxLogLineLength is truncated and marked
[truncated], with the content trimmed so that content and marker
together come to exactly the limit. Everything up to the next newline is
then suppressed rather than emitted as a second line.
MaxLogBufferPerService is applied as the pipe's own capacity through
F_SETPIPE_SZ, which is a literal reading of "bytes buffered per
service pipe before backpressure" — the kernel does the buffering and
the bound is where it belongs.
LogReadBytesPerEvent bounds one readable event rather than one loop
iteration. A turn with several ready pipes reads up to the budget from
each, bounded overall by how many events one epoll wait returns.
11.3.1 Where loss is allowed and where it is not #
At the pipe stage, peinit does not drop. It keeps reading within its
budget and appends every complete line; only EAGAIN, end of file, or
an error stops the read. Backpressure through the pipe is the
flow-control mechanism between a service and peinit, and dropping there
would replace it with silence.
Downstream of the pipe, delivery is loss-tolerant by design. The pre-eventd buffer drops its oldest entries when full, and eventd's datagram socket may drop records under load. The no-silent-drop guarantee covers reading the pipe, not delivery to eventd.
Audit events are exempt from all of it. They go through KMES.
11.3.2 Event loop fairness #
No source may starve the loop. Signals are handled at the highest priority — SIGCHLD reaping and shutdown handling take precedence over everything else in every iteration — with the shutdown deadline timer next and every other source below that, ties broken by arrival order.
The power button shares the top priority with signals, on the reasoning that someone physically pressing it is asking for the same class of attention.
11.4 The eventd Handoff
Peios / Advanced Peios / peinit / Output Handling
When eventd reaches Active, peinit switches from buffering to forwarding.
- Start sending to eventd's log datagram socket, at the path from
Machine\System\eventd\LogSocketPath. - Replay the pre-eventd buffer, oldest first, preserving each line's timestamp and metadata. The replay is best-effort: these are datagrams on a loss-tolerant socket, so some may be dropped, and peinit does not block waiting to deliver them.
- Switch to real-time forwarding — new output sent as it arrives.
- Clear the buffer.
From there peinit is a relay: read from the pipes, tag each line, forward. Audit events continue to flow as KMES events, entirely separately.
11.4.1 The record #
Each record is a msgpack map:
| Key | Type | Content |
|---|---|---|
origin | string | The service name, or the hook identifier. |
is_error | bool | True for stderr. |
message | string | The line. |
timestamp | uint | Nanoseconds, wall clock. |
job_id | bin | The job's 16-byte GUID. Omitted when absent. |
The map has four or five entries depending on whether a job identifier applies.
11.4.2 Lossy delivery #
eventd's log socket is a non-blocking Unix datagram socket. If eventd
cannot drain it fast enough its SO_RCVBUF fills and the kernel drops
further datagrams silently — log ingestion deliberately exerts no
backpressure on senders.
peinit therefore keeps no outbound write buffer. It sends each record as a datagram and accepts that some may be dropped. It never blocks on a send, and never lets pending records grow without bound.
Each send opens a datagram socket, sends, and closes it. Records go one at a time; there is no batching of several records into one datagram.
A send that fails — the receive buffer full — takes peinit out of forwarding for the remainder of that turn: the failing record and the rest of its batch go back into the pre-eventd buffer. The end of the turn re-establishes forwarding by replaying them.
11.4.3 When eventd goes away #
peinit supervises eventd like any other service, so it sees the exit directly. It re-enables the pre-eventd buffer, and when eventd restarts and reaches Active the handoff repeats.
There is a log gap between eventd crashing and restarting, bounded by the buffer size. Events are unaffected: they land in the KMES ring buffer regardless of eventd's state, and eventd resumes consuming from the last persisted sequence when it comes back.
11.5 Console Output
Peios / Advanced Peios / peinit / Output Handling
peinit writes its own operational messages to /dev/console:
- Phase 1 progress — mount results, registryd starting.
- Phase 2 progress — services starting and failing, dependency errors.
- Shutdown progress.
- Recovery mode entry.
- Critical service failures.
Service output is never echoed to the console. The console is for
peinit's own messages; a service that wants a terminal asks for one with
TTYPath.
11.5.1 Severity and quiet #
Each message carries a severity, and peios.quiet (§2.6) decides what
that means:
- At
0, everything is written. - At
1, the default, peinit stays out of a terminal held as the controlling terminal of a running service, except to announce loss of the system. Terminals are matched by device rather than by path, since/dev/consoleand/dev/ttyS<n>can be the same device; where the device cannot be determined peinit assumes the terminal is held. - At
2, ordinary progress is dropped everywhere while errors still get through.
Suppressed messages are discarded rather than buffered for later.
Shutdown progress carries ordinary status severity, so peios.quiet=2
suppresses it along with every other kind of progress.
The autorun step in Phase 1 (§2.3) bypasses the policy entirely, on the grounds that a script running that early and going wrong is worth interrupting anything for.
12.1 Triggers
Peios / Advanced Peios / peinit / Shutdown
Four paths initiate a shutdown.
12.1.1 The control socket #
A shutdown command naming a type, gated on SYSTEM_SHUTDOWN against
peinit's control descriptor (§4.7):
| Type | Effect |
|---|---|
poweroff | Stop everything, unmount, power off. |
reboot | Stop everything, unmount, reboot. |
halt | Stop everything, unmount, halt — the CPU stops, the system stays powered. |
12.1.2 Signals #
| Signal | Meaning |
|---|---|
| SIGINT | Reboot. The kernel sends it on Ctrl+Alt+Del. |
| SIGTERM | Poweroff. PID 1 cannot be killed by it but may choose to act on it. |
| SIGPWR | Poweroff. The compatibility path for environments that surface power failure or a power-button policy as a signal. |
Three SIGINTs within five seconds force an immediate shutdown: no graceful stop, no ordering, SIGKILL every service cgroup, sync, reboot. The window is a sliding five seconds and the press is recorded before the already-shutting-down check, so three presses still force even after a graceful reboot has begun. That is the point — someone pressing it three times has decided the graceful path is not working.
12.1.3 The power button #
An EV_KEY / KEY_POWER press from a readable /dev/input/event*
device is a graceful poweroff. Only a press — value 1 — initiates.
Releases, key repeats, other keys and other event types are ignored.
The path is fail-soft throughout: a missing /dev/input, a device that
cannot be opened or registered, and a registered descriptor that later
fails to read are all survivable, and a failing descriptor is removed
from the event loop so repeated failures cannot spin PID 1. Losing it
degrades only direct power-button handling; the socket and signal paths
remain.
It is deliberately minimal. It is not a power-management policy engine and does not replace a future daemon that would translate richer policy into control socket commands.
12.1.4 Critical service failure #
A Critical service entering Failed with its restart budget exhausted means peinit syncs the filesystems and reboots immediately. This is not a graceful shutdown: there is no stop ordering, no seed save, and no unmount. The system is in an undefined state and the fastest path to a defined one is a reboot.
12.2 The Graceful Sequence
Peios / Advanced Peios / peinit / Shutdown
12.2.1 Step 1: Enter the shutdown state #
peinit sets an internal flag. While it is set, no new service starts,
and control commands other than status, list and operation-status
are rejected as invalid for the current state.
Timer triggers are not disarmed. A calendar timer that fires during the shutdown window is still classified and acted on: for a service in Inactive, Completed or Failed — precisely the states step 3 classifies as not participating — that means creating a start operation and starting the service. The shutdown plan was fixed when the shutdown began, so a service started this way is in no wave, is not waited for, and is not reached by the global-timeout sweep.
12.2.2 Step 2: Suspend Critical failure semantics #
A Critical service failing during shutdown is recorded but does not trigger a reboot. The system is already going down, and rebooting from here would loop.
12.2.3 Step 3: Classify #
Completed services — Oneshots with RemainAfterExit — have no process
and are transitioned to Inactive, releasing the dependency
relationships they were holding so their dependents can be stopped
cleanly.
The rest are classified for stop eligibility:
- Active and Reloading are graceful-stop eligible and enter the waves.
- Stopping services are already on a stop path. They join the waves for ordering and timeout purposes, but do not receive another SIGTERM.
- Starting services are not eligible. peinit cancels the startup,
SIGKILLs the service cgroup if one exists, and transitions them to
Failed with cause
ShutdownWavewhile post-kill verification is pending. A cgroup still populated after the post-kill timeout takes the service to Abandoned with causeProcessUnkillableand the cgroup is leaked. A Starting service whose job never forked skips the check and goes straight to Failed. - Inactive, Failed, Skipped, Backoff and Abandoned do not participate. Abandoned cgroups stay leaked and shutdown continues.
12.2.4 Step 4: Stop in reverse dependency order #
peinit builds the reverse dependency graph over hard dependencies and stops in waves:
- Each eligible service receives SIGTERM. Already-stopping ones do not.
- Each has
StopTimeoutto exit. - On expiry, SIGKILL to the service's entire cgroup.
- No service is stopped until everything depending on it has stopped.
A service that sent STOPPING=1 does not receive a SIGTERM at all: it
has already said it is shutting down, and peinit goes straight to the
stop deadline.
12.2.4.1 Timing an already-stopping service #
peinit does not reset an already-Stopping service's clock, either when shutdown begins or when its wave becomes eligible. It uses the timing evidence retained from the stop path that put the service in Stopping:
- If a Stop operation, or a Restart executing its stop leg, is in flight, that operation's retained timing governs.
- Otherwise peinit uses service-level evidence, which carries the cause
that initiated the transition —
ExplicitStop,ShutdownWave,ConflictEvictionorBindsToPropagation.
peinit requires the evidence to be present, to belong to a service actually in Stopping, to carry a cause matching the service's current cause, to name one of those four causes, and not to describe a deadline earlier than its own start. Evidence that is missing, stale or ambiguous fails closed rather than being guessed at — and it fails the whole shutdown, not just that participant, so a shutdown command with one such service is refused, and one discovered on a later wave ends the runtime loop.
An operation whose lifetime expires while it is still Pending — a later-wave stop waiting for its dependencies — fails that operation and its waiters. It does not authorise signalling the service before its wave is eligible. Shutdown owns the signalling; the operation object is an observation of it.
12.2.5 Step 5: Global timeout #
| Key | Default | Meaning |
|---|---|---|
Machine\System\Boot\ShutdownTimeout | 90 | Seconds for the whole sequence. |
Machine\System\Boot\PostKillTimeout | 5 | Seconds for a cgroup to drain after SIGKILL. |
On expiry, every remaining participant's cgroup is killed, anything that does not drain within the post-kill timeout is marked Abandoned with its cgroup leaked, and shutdown continues regardless.
12.2.6 Step 6: Save the random seed #
After every service has stopped and before any unmount, peinit writes a
fresh seed to /var/state/peinit/random-seed for the next boot: 512
bytes from the kernel CSPRNG on this machine, never copied from an
image, protected so that only SYSTEM-equivalent authority can read or
replace it.
The write is crash-conscious: a temporary file on the same filesystem, written, flushed, atomically renamed over the old seed, and the containing directory flushed. A failure is recorded and shutdown continues.
The forced and Critical-reboot paths skip it, along with the unmount step, and go directly to sync and the final action.
12.2.7 Step 7: Unmount #
- Snapshot the mount table first, from
/proc/self/mountinfo. - Attempt to unmount every remaining non-root mount in the namespace — not only what peinit mounted, so the Phase 1 set is covered by construction.
- Process in descending path depth, so children go before parents.
- A mount point already gone is a successful no-op.
- On failure, attempt a read-only remount. If that fails too, record it and continue.
- The root is never unmounted, but is remounted read-only at the end. A failure there is recorded and does not stop step 8.
12.2.8 Step 8: Sync and the final action #
This step is irreversible. Cleanup failures retained from steps 6 and 7 affect diagnostics only and never block it.
sync(). Called on all three paths — graceful, forced and Critical reboot.reboot(2)withRB_POWER_OFF,RB_AUTOBOOTorRB_HALT_SYSTEM.- If
reboot(2)returns, the final action failed. peinit enters a minimal failed-shutdown state, keeps PID 1 alive, records the failure, and retries the same action no more than once a second. It does not restart services and does not enter recovery mode — finalisation has begun and there is nothing to go back to.
12.2.9 Shutdown during boot #
A shutdown requested while Phase 2 is still running takes effect immediately, through the same classification: Starting services are SIGKILLed, services that reached Active are stopped gracefully, and the boot is abandoned.
12.3 Signals
Peios / Advanced Peios / peinit / Shutdown
PID 1 handles every signal through a signalfd. All signals are blocked and read from the event loop, so there are no signal handlers and no async-signal-safety concerns anywhere in peinit.
12.3.1 Setup #
peinit builds a mask containing every blockable signal in the supported
range — SIGKILL and SIGSTOP are not blockable and are never delivered
through a signalfd — and installs it with
rt_sigprocmask(SIG_BLOCK, ...) before entering the main event
loop. It then creates the descriptor with signalfd4(-1, mask, ...),
using the same mask, with SFD_CLOEXEC | SFD_NONBLOCK.
If any part of that fails — blocking, creating the descriptor, retaining it, registering it with the event loop — peinit fails closed. There is no fallback to asynchronous handlers, because a PID 1 with handlers installed where it expected a signalfd is a PID 1 whose assumptions about what can interrupt it are wrong.
The mask is inherited across fork, which is why every child resets it before exec (§5.4).
12.3.2 The signals #
| Signal | Behaviour |
|---|---|
| SIGCHLD | Reap children with waitpid. Match them to tracked jobs; also reap orphans belonging to nothing. |
| SIGINT | Reboot. Three within five seconds forces one. |
| SIGTERM | Poweroff. |
| SIGPWR | Poweroff. |
| SIGHUP | Ignored. PID 1 has no controlling terminal. |
| SIGPIPE | Ignored. A broken pipe on the control socket cannot be allowed to kill PID 1. |
Everything else is ignored. The kernel protects PID 1 from fatal signals, so no signal can kill it.
12.3.3 Reaping #
peinit reaps with waitpid(-1, ..., WNOHANG) in a drain loop, and
normalises the wait status before any service or job policy sees it:
- An exited child carries its exact exit code, 0–255.
- A signalled child carries the terminating signal number and whether the core-dump bit was set.
- A stopped or continued status is invalid on this path, because peinit never asks for them. Observing one fails closed rather than being interpreted.
That last rule matters more than it looks. A stopped child reported as an exit would be read as a service that had terminated, and peinit would act on a process that is merely paused.
As PID 1, peinit also reaps processes nobody is tracking — orphans reparented to it — and reports them as untracked rather than trying to attribute them to a service.
12.4 Finalisation
Peios / Advanced Peios / peinit / Shutdown
Three shutdown paths reach the kernel, and they do different amounts of work on the way.
| Path | Stop waves | Seed save | Unmount | Sync | Final action |
|---|---|---|---|---|---|
| Graceful | Yes | Yes | Yes | Yes | Yes |
| Forced — three SIGINTs | No, SIGKILL everything | No | No | Yes | Yes |
| Critical service failure | No | No | No | Yes | Yes |
The two abrupt paths are required to reach sync() and the final kernel
action with minimal additional work, and skipping the seed and the
unmounts is what "minimal" means. A machine that is rebooting because
its audit daemon died has nothing to gain from a tidy unmount and
something to lose from the time it takes.
12.4.1 What survives a failure #
Steps 6 and 7 — the seed and the unmounts — retain their failures as evidence rather than acting on them. Every one is recorded, none of them blocks step 8, and none of them enters recovery mode. By this point there is nothing to recover to: services have stopped and the filesystems are on their way down.
12.4.2 If the final action returns #
reboot(2) does not return on success. If it does, the final action
failed, and peinit enters a minimal failed-shutdown state:
- PID 1 stays alive. It has to — PID 1 exiting panics the kernel.
- The failure is recorded.
- The same action is retried, no more than once a second.
- Services are not restarted, and recovery mode is not entered.
There is no way back from here. Finalisation has begun, the services are gone and the filesystems are read-only; the only correct behaviour is to keep trying the one thing that would end it.
RB_HALT_SYSTEM does not return either, so this state is only reachable
for a genuine failure rather than for the halt case.
12.4.3 A note on the mount table #
The unmount step re-reads /proc/self/mountinfo when checking whether a
mount point that returned ENOENT or EINVAL is really gone. Depth
ordering puts the depth-one mounts last, alphabetically — /dev,
/proc, /run, /sys — so /proc is unmounted before /run and
/sys are attempted, and the check for those two cannot read the file
it needs. The result is a recorded cleanup failure and a pointless
read-only remount attempt at the tail of every graceful shutdown.
13.1 Trust Boundaries
Peios / Advanced Peios / peinit / Security
peinit sits at two.
13.1.1 Kernel to peinit #
peinit is the first userspace process, and the kernel gives it a SYSTEM
token — S-1-5-18, every privilege. This is the root of trust for all
userspace identity on the system.
peinit does not drop that token and does not authenticate to anything. Its identity is axiomatic: there is no authority above it in userspace that could vouch for it, and the kernel handing it the boot token is the vouching.
13.1.2 peinit to services #
peinit creates service processes with specific identities and reduced privileges. The trust runs one way. peinit trusts the kernel because it has no alternative; services trust peinit because peinit gave them their identity. Services do not trust each other — KACS mediates every access between them, and nothing peinit does creates a relationship between two services beyond the ordering their definitions asked for.
13.1.3 The TCB #
peinit is part of the Trusted Computing Base, alongside the kernel, KACS, LCS, KMES, registryd, authd, lpsd and eventd. A compromise of any of them compromises the system.
That list is not decoration. It is why registryd is exempt from the global environment layer (§5.5) — a component in the TCB cannot be configurable by a mechanism it is itself the enforcement point for — and it is why eventd is Critical, since a TCB whose audit trail can be silently stopped is not one.
13.1.4 Filesystem enforcement #
KACS enforces Security Descriptors on filesystem access. peinit's Phase
1 descriptor seeding (§2.3) exists precisely because of it: a freshly
mounted tmpfs carries no descriptor, and under DENY_MISSING every
inode on it would be unreachable to everything — including peinit —
until something stamps a descriptor it can inherit from.
That enforcement has no bypass. There is no owner exemption, no privilege that overrides a missing descriptor, and no root escape, which is what makes the seeding step fatal on failure rather than advisory.
FACS extends the model further, and until it lands the filesystem layer still relies partly on conventional trust — correct packaging, controlled binary paths — for the objects nothing has stamped.
13.2 peinit's Privileges
Peios / Advanced Peios / peinit / Security
peinit runs as SYSTEM with every privilege for the lifetime of the system. What it actually exercises is narrower.
| Privilege or capability | Used for |
|---|---|
SeCreateTokenPrivilege | Minting SYSTEM tokens for platform services during bootstrap, before authd exists (§4.2). |
SeTcbPrivilege | Requesting tokens from authd on a service's behalf, and installing a primary token on a child whose identity differs from peinit's own. |
| Process creation | Fork and exec, inherent to PID 1. |
| cgroup management | Creating and destroying trees under /sys/fs/cgroup/peinit/. |
| Signal delivery | SIGTERM and SIGKILL to managed processes. |
| Mount operations | The Phase 1 virtual filesystems. |
peinit does not verify at startup that it holds any of these. A missing
SeCreateTokenPrivilege surfaces as an EPERM from the first token
mint, which is the first service start.
SeImpersonatePrivilege is not used. peinit passes the peer's token
descriptor to AccessCheck directly rather than impersonating the caller
and evaluating as them, so the privilege that would be needed to
impersonate is not needed at all.
For non-platform services peinit creates no tokens. It installs the ones authd minted. It mints only for the SYSTEM platform services it starts during bootstrap, before authd is available.
13.3 The Attack Surface
Peios / Advanced Peios / peinit / Security
| Surface | Reachable by | Controls | Protected by |
|---|---|---|---|
| Control socket | Anything that can connect | Service lifecycle, shutdown | The socket inode's descriptor, then the peer token and AccessCheck against the target's descriptor |
| Notification socket | Anything that can connect | Service readiness, watchdog, stored descriptors | The socket inode's descriptor, then PID matching verified through a pidfd, plus the start generation |
| Registry keys | Anything with registry access | Definitions, triggers, configuration | Registry key descriptors, enforced by LCS |
Machine\System\Init\EnvVars\ | Anything with registry access | The environment of every service | That key's descriptor, and nothing else — variable names are not filtered |
| Service cgroups | peinit | Process tracking and clean kill | Ownership of the hierarchy |
| Phase 1 mounts | peinit | Virtual filesystem availability | Hardcoded, no external input |
/lcl/policy/autorun.d | Whatever can write it | Arbitrary code as SYSTEM in early boot | The directory's descriptor |
| Boot attempt counter | Whatever can write /.peinit/ | Recovery mode entry | That file's descriptor |
| JFS device | Whatever holds the submission privilege | Ad-hoc job submission | A KACS privilege check, kernel-enforced |
13.3.1 What the socket descriptors mean in practice #
Both sockets inherit the single-entry descriptor peinit stamps on /run
in Phase 1 (§2.3), which grants GENERIC_ALL to SYSTEM and names no
other principal.
So both are reachable by SYSTEM alone. A connection from a
non-SYSTEM principal is refused at connect(), by the filesystem, before
peinit ever obtains a peer token — which means the ACCESS_DENIED path,
the audit event, and the Administrators entries in both default
descriptors (§4.6, §4.7) are unreachable for such a caller.
Since every service currently receives a SYSTEM token (§4.3), nothing observes this today: every notifier and every client is SYSTEM. The two have to move together, because a service correctly resolved to a non-SYSTEM identity could not reach the notification socket to report that it had started.
13.3.2 The autorun directory #
The Phase 1 autorun step (§2.3) executes every file in
/lcl/policy/autorun.d as SYSTEM, before path provisioning and before
any service. It is fail-open by design, so nothing about a script going
wrong stops the boot.
Its only protection is the descriptor on that directory. Anything that can write there executes as SYSTEM at the earliest point in userspace that exists.
13.4 Security Invariants
Peios / Advanced Peios / peinit / Security
Properties peinit does not violate.
1. peinit does not grant privileges it was not asked to grant.
RequiredPrivileges is subtractive. peinit removes privileges and never
adds one — there is no code path that constructs anything but a removal
(§4.5).
2. peinit does not bypass AccessCheck for control operations. Every control command reaches a check against the appropriate descriptor. There is no backdoor, no override flag, and no "trust localhost".
3. peinit does not expose one service's state to another without
access control. list returns only what the caller may query, and
status is checked per service (§10.2).
4. peinit records every access denial. A failed AccessCheck produces
an access.denied event carrying the caller's SID, the target, the
requested right by name, and the access bits requested and granted.
Silent denial is not acceptable.
5. The control descriptor and the ServiceSecurity descriptors are the only policy inputs for runtime access control. peinit consults no configuration file, no environment variable and no hardcoded principal list. The only inputs to AccessCheck are those two descriptors, sourced from the registry with a compiled-in default.
6. peinit does not share its SYSTEM token. It opens its own token
query-only, as a template, and never installs it on a child. Even an
Identity=SYSTEM service gets a separately minted token.
7. peinit does not drop its SYSTEM identity. PID 1 runs as SYSTEM for the lifetime of the system. Only the forked child installs a token; peinit never installs one on itself.
8. Identity is deterministic, and the dangerous case is never
implicit. Every service runs with a known identity. SYSTEM has to be
declared explicitly; an absent or empty Identity means LocalService.
The declaration logic upholds the eighth: an empty value resolves to
LocalService, and SYSTEM is reached only by naming it. What a
service actually receives depends on materialisation, and while the
authd client returns a minted SYSTEM token for every identity (§4.3), a
service that declared nothing runs on one.
14.1 Losing the Registry
Peios / Advanced Peios / peinit / Failure Modes
peinit depends on the registry once, hard, and then never again in the same way. The difference between those two situations is most of what this section is about.
14.1.1 During Phase 1 #
registryd failing to start, failing readiness, or failing the schema-version probe sends peinit to recovery mode immediately. There is no Phase 2 without a registry and nothing useful to degrade to. The counter is incremented, so a persistently broken registryd burns boot attempts even though every one of them fails the same way.
The recovery shell reached from a Phase 1 failure does not have registryd running, and the offline tools (§2.8) are what an administrator has.
14.1.2 During Phase 2 #
A registry read that fails or times out during the definition read sends peinit to recovery, for the same reason: there is no graph to boot.
14.1.3 At runtime #
This is where the design pays off. peinit holds a complete in-memory model and does not read the registry during normal supervision, so registryd going away does not stop peinit supervising anything. Services keep running, restarts keep working, the control socket keeps answering, and timers keep firing.
What stops working is anything that needs new configuration: reload-config fails, change notifications stop arriving, and a timer's last-run timestamp cannot be written — so a persistent timer may produce a spurious catch-up on the next boot.
registryd itself is a Critical service, so its failure takes the ordinary Critical path: restart budget, then sync and reboot. peinit does not have to handle a permanently absent registryd at runtime, because the system reboots first.
14.1.4 A definition that will not decode #
One definition that fails to decode fails that definition (§3.2). At
boot the key is marked Failed with cause ValidationError and every
other service starts normally; on a reload the whole reload is rejected
and the previous generation is left in place.
Both are the safe answer for their caller. A reload is atomic and has a working configuration behind it, so refusing the change costs nothing. A boot has no previous generation to fall back to, so refusing everything would mean not booting at all over a single malformed key — which is what it used to do.
14.2 Losing a Dependency
Peios / Advanced Peios / peinit / Failure Modes
14.2.1 authd #
Without authd, no non-SYSTEM service can obtain a token, so every such
start fails with ParentSetupFailure. Platform services are unaffected,
since they never take that path.
authd is Critical, so its own failure eventually reboots the system rather than leaving it in a state where no user-facing service can start.
14.2.2 eventd #
peinit supervises eventd like anything else, and eventd is Critical. While it is down, peinit re-enables the pre-eventd buffer (§11.2) and keeps collecting output; when eventd returns the handoff repeats.
There is a log gap bounded by the buffer size. There is no event gap — audit events land in the KMES ring buffer regardless of eventd's state, and eventd resumes from the last persisted sequence.
14.2.3 JFS #
The device may not exist. Phase 1 treats a failure to open /dev/jfs as
a warning and continues, and the boot is unaffected. Nothing else in
peinit depends on it.
14.2.4 A bound dependency #
A service that BindsTo something is stopped when that something stops,
with cause BindsToPropagation, and restarted when it returns, with
cause BindsToRecovery and no charge against the restart budget
(§7.1). This is the one dependency relationship that recovers by itself.
A Requires dependency crashing does not affect a running dependent at
all. The dependent was ordered after it, not coupled to it.
14.2.5 A dependency that never becomes satisfying #
A dependent blocked on a hard dependency waits until its own operation
lifetime expires, and then fails. The timeout is measured from when the
operation was created rather than from when it started running, so a
service queued behind a slow dependency can exhaust its StartTimeout
without ever having attempted to start — which is the honest answer, in
that the caller really has been waiting that long.
14.3 Unkillable Processes
Peios / Advanced Peios / peinit / Failure Modes
A process in uninterruptible kernel sleep does not respond to SIGKILL. Nothing peinit can do will make it exit, and the design consequence is that peinit stops trying rather than blocking on it.
Detection is uniform: after sending the kill, arm a post-kill deadline (5 seconds by default) and check whether the cgroup still reports as populated when it fires.
What follows depends on which cgroup it was:
| Cgroup | Consequence |
|---|---|
main/ | The service goes to Abandoned with cause ProcessUnkillable. Supervision stops; the cgroup is leaked. |
health/ or hooks/ | The sub-cgroup is orphaned and recorded as a leak. The service carries on normally. |
checks/ | The sub-cgroup is dropped with no record. |
The distinction is about what the stuck process is holding. A main process holds the service's ports, locks and connections, so a service whose main process cannot be killed cannot be restarted into a working state. A health check or a hook holds nothing, so a stuck one is a nuisance rather than a blocker.
14.3.1 What Abandoned means #
peinit has given up. The service is not restarted, does not satisfy
dependents, and every lifecycle command against it is invalid except
reset (§10.3).
A reset re-checks main/. If it has finally emptied — the I/O that
was hung completed, or the device came back — peinit cleans up the whole
tree and the service returns to Inactive. If it is still populated, the
service returns to Inactive anyway, the cgroup stays leaked, and both the
acknowledgement and the console carry a warning saying so.
14.3.2 The generational escape #
A leaked cgroup cannot be removed, so the next start would collide with
it. Recording a leak increments the service's cgroup generation, and the
next start builds a fresh tree at .gen<N> (§5.1). Old trees persist
until reboot.
That is what lets a service recover from a leak at all: peinit cannot clean up the old tree, so it stops trying to and uses a new one.
14.3.3 What it actually means #
Every path here has the same underlying cause. Something below the service — a hung mount, a failing controller, a device that stopped answering — is not responding to the kernel, and no amount of restarting the service will change that. The leak record exists to say so, because the alternative is a service that mysteriously will not restart.
14.4 Resource Exhaustion
Peios / Advanced Peios / peinit / Failure Modes
peinit is single-threaded PID 1, so most resource pressure reaches it as a failure at a syscall rather than as slowness.
14.4.1 Descriptors #
peinit holds a descriptor per supervised process (a pidfd), two per service for output pipes, one per armed timer, one per control connection, plus the sockets, the epoll instance, the signalfd and the JFS device.
EMFILE or ENFILE from pipe2 or clone3 fails the start with
ParentSetupFailure — a restart-eligible cause, so a service that
failed because the system was momentarily out of descriptors gets
another go.
Two paths leak descriptors slowly: the pre-start check helper's result descriptor and pidfd are unregistered from the event loop but not closed, so a service using filesystem conditions leaks two per start.
14.4.2 Processes #
EAGAIN from clone3 — the PID limit — is also ParentSetupFailure
and restart-eligible.
The one path that leaks processes is the timer last-run write, which forks a child per firing (§9.2). The children are short-lived and reaped by the ordinary PID 1 reaper, but the fork is real and happens on every firing of every persistent timer.
14.4.3 Memory #
ENOMEM from clone3 behaves like the others.
peinit's own memory is bounded by design in the places that could otherwise grow without limit: the pre-eventd buffer has a fixed size and drops its oldest, there is no outbound queue for log delivery, terminal jobs and operations are dropped rather than retained, and neither has a history structure.
Two things do accumulate. Graph execution contexts and their operation
associations are never retired, so each boot and each on-demand start
adds one for the life of the process. And an OnFailure chain entry for
a handler that starts and stays running is never cleared, so it
permanently occupies a slot of that failure's depth budget.
14.4.4 Disk #
A full root filesystem shows up in three places. The boot attempt counter cannot be written, which peinit treats as a counter of zero and continues — a failure to record an attempt is not itself worth escalating. The random seed cannot be saved at shutdown, which is recorded and does not block the shutdown. And registryd cannot write, which is registryd's problem and reaches peinit as a Critical service failing.
14.4.5 The OOM killer #
An ErrorControl=Critical service is marked OOM-immune, with
oom_score_adj at -1000; everything else is left at the default
(§5.4). A Critical service is one whose loss reboots the machine, so
letting the OOM killer pick it would turn memory pressure into a reboot.
peinit itself is PID 1 and the kernel will not choose it.
14.5 Power Loss and Corruption
Peios / Advanced Peios / peinit / Failure Modes
14.5.1 What peinit writes #
Three things, and all three are written to survive an unexpected loss of power:
| What | Where | How |
|---|---|---|
| Boot attempt counter | /.peinit/boot-attempts | A plain integer, rewritten each boot. |
| Local machine ID | /lcl/etc/machine-id | Temporary file, flush, rename. |
| Random seed | /var/state/peinit/random-seed | Temporary file on the same filesystem, flush, atomic rename, directory flush. |
The seed and the machine ID are atomic in the sense that matters: a reader sees either the old value or the new one, never a partial write.
14.5.2 Reading a damaged file #
The three behave differently on finding something they cannot use, and the differences track how bad each situation is.
The counter is the strictest. Absent means zero. But unreadable, empty, non-decimal, carrying trailing data, or overflowing all send peinit to recovery mode. A counter that cannot be trusted cannot escalate, and the whole point of it is escalation — so failing to read it is treated as though it had already escalated.
The machine ID is regenerated. Absent, empty, all zeroes, the wrong length, not hexadecimal, or missing its trailing newline all produce a fresh identifier, recorded as a warning. It is an opaque install identifier, not a security principal, and a new one is a smaller problem than no boot. An I/O failure while reading, generating, or writing does send peinit to recovery.
The seed is entirely fail-soft. Absent, empty, oversized, or unrestorable all continue the boot silently. A system with no entropy cache still boots; it starts with less entropy, which is the image builder's problem to solve with a hardware or virtio RNG.
14.5.3 The registry #
peinit does not write service state to the registry, apart from timer last-run timestamps. Everything else about a service's runtime state lives in memory and is rebuilt from the definitions on the next boot, which means a power loss cannot leave peinit's own state inconsistent — there is no state on disk to be inconsistent with.
Registry consistency across power loss is loregd's concern, and its recovery paths are what the recovery shell offers (§2.8).
14.5.4 Timers across a loss #
A persistent timer's last-run timestamp is written after the firing and after the start is initiated, not after the service completes (§9.3). A power loss mid-run therefore does not re-trigger on the next boot — the run was attempted, not missed.
A power loss between the firing and the write does re-trigger, which is the right way round: one extra run is better than a silently skipped one.
14.5.5 Shutdown that never finishes #
If power is lost during the graceful sequence, the effect depends on how far it got. Before step 7, the filesystems are still mounted read-write and the next boot behaves like any unclean shutdown. After step 7, the root has been remounted read-only and everything else unmounted, so there is nothing outstanding to lose.
The counter was incremented at the start of the boot that is now ending, and is reset only by a successful boot — so a power loss during a shutdown leaves the counter advanced. Enough of them in a row reach the recovery threshold, which is the intended behaviour: a machine that keeps losing power mid-shutdown is a machine an administrator should be looking at.
Appendix A Registry Key Reference
Peios / Advanced Peios / peinit
Every registry key peinit reads or writes. Semantics are in the sections referenced.
A.1 Service definitions #
| Key | Purpose | Defined in |
|---|---|---|
Machine\System\Services\ | Parent key. Each child key is one service. | §3.2 |
Machine\System\Services\SchemaVersion | Schema version guard. REG_DWORD, currently 1. Created by peinit if absent. | §2.3, §3.2 |
Machine\System\Services\ServiceSecurity | The descriptor inherited by definitions that carry none. REG_BINARY. | §4.6 |
Machine\System\Services\<name> | One service definition. | §3.2 |
Machine\System\Services\<name>\LastTimerRun | Last-run timestamp for a single-trigger persistent timer. REG_QWORD, written by peinit. | §9.3 |
Machine\System\Services\<name>\TimerState\ | Per-trigger timestamps for a multi-trigger service. Each value is named by the percent-encoded schedule and holds a REG_QWORD. | §9.3 |
A.2 Boot configuration #
| Key | Type | Default | Purpose | Defined in |
|---|---|---|---|---|
Machine\System\Boot\MaxParallelStarts | dword, > 0 | 10 | Services starting concurrently during boot. Zero, a type mismatch, or a malformed payload is invalid and enters recovery. | §2.5 |
Machine\System\Boot\BootSuccessGrace | dword | 30 | Seconds every Critical service has to hold a dependent-satisfying state before the boot counts as successful. | §2.5 |
Machine\System\Boot\SettleTimeout | dword | 5 | Seconds to wait for the boot set to settle before starting boot:settled services regardless. Zero is legal. | §2.5 |
Machine\System\Boot\ShutdownTimeout | dword | 90 | Seconds for the entire graceful shutdown. | §12.2 |
Machine\System\Boot\PostKillTimeout | dword | 5 | Seconds for a cgroup to drain after SIGKILL before it is treated as stuck. Bounds one service's final stop, where ShutdownTimeout bounds the sequence. | §12.2 |
A.3 Operational parameters #
| Key | Type | Default | Purpose | Defined in |
|---|---|---|---|---|
Machine\System\Init\ControlSecurity | binary | SYSTEM and Administrators, both rights | The descriptor for system-level control operations. | §4.7 |
Machine\System\Init\MaxControlConnections | dword | 32 | Concurrent control socket connections. | §10.1 |
Machine\System\Init\MaxRequestSize | dword | 65536 | Maximum control request size, in bytes. | §10.1 |
Machine\System\Init\ConnectionTimeout | dword | 30 | Seconds before an idle control connection is closed. | §10.1 |
Machine\System\Init\MaxLogLineLength | dword | 8192 | Bytes per output line before truncation. Minimum 256; below that the default is used and a warning logged. | §11.3 |
Machine\System\Init\MaxLogBufferPerService | dword | 65536 | Pipe capacity per service, applied with F_SETPIPE_SZ. Minimum 4096 (one page, the kernel's own floor). | §11.3 |
Machine\System\Init\LogReadBytesPerEvent | dword | 16384 | Bytes drained from one output pipe per readable event. Minimum 512. | §11.3 |
Machine\System\Init\PreEventdBuffer | dword | 1048576 | Bytes of output retained before eventd is available. Applied at boot and on reload. Minimum 4096. | §11.2 |
Machine\System\Init\EnvVars\ | parent key | empty | Variables injected into every service but registryd. Value name is the variable name; REG_SZ data is the value. Its descriptor is security-critical. | §5.5 |
Machine\System\Init\ProvisionedPaths\ | parent key | empty | Boot-time path provisioning entries. | §2.4 |
Machine\System\Init\ProvisionedPaths\<name>\Kind | string | — | directory or file. Required. | §2.4 |
Machine\System\Init\ProvisionedPaths\<name>\Path | string | — | The absolute path. Required. | §2.4 |
Machine\System\Init\ProvisionedPaths\<name>\Security | binary | built-in | The descriptor to apply. | §2.4 |
Machine\System\Init\ProvisionedPaths\<name>\Required | dword | 0 | If 1, a failure enters recovery before Phase 2. | §2.4 |
A.4 Other subsystems #
| Key | Type | Purpose | Defined in |
|---|---|---|---|
Machine\System\eventd\LogSocketPath | string | Where peinit forwards service output. | §11.4 |
A.5 Watches #
peinit subscribes to Machine\System\Services\ and
Machine\System\Init\ at boot. Any drained event triggers a full
reload-config, which also covers the OVERFLOW case (§3.7).
Appendix B Constants and Paths
Peios / Advanced Peios / peinit
Values compiled into peinit, which no registry key changes.
B.1 Filesystem paths #
| Path | Purpose |
|---|---|
/usr/bin/peinit2 | Where peinit is installed in package storage. |
/bin/peinit2 | The runtime path the kernel init= names. |
/sbin/registryd | The compiled-in registryd image path. |
/var/state/loregd/Machine.hive | The machine hive, passed to registryd. |
/var/state/loregd/Users.hive | The users hive, passed to registryd. |
/.peinit/ | peinit's own state directory on the root filesystem. |
/.peinit/boot-attempts | The boot attempt counter. |
/lcl/etc/machine-id | The local machine identifier. |
/var/state/peinit/random-seed | The persisted entropy seed. |
/lcl/policy/autorun.d | Phase 1 autorun scripts. |
/run/services/peinit/control.sock | The control socket. |
/run/services/peinit/notify.sock | The notification socket, by default. |
/sys/fs/cgroup/peinit/ | The root of every service cgroup tree. |
/dev/jfs | The job forwarding device. |
/dev/rtc, /dev/rtc0 | The hardware clock, in that order of preference. |
/dev/console | Where peinit writes its own messages. |
/bin/recsh, /bin/sh | The recovery shell, in that order of preference. |
B.2 Timeouts and limits #
| Constant | Value | Meaning |
|---|---|---|
| registryd setup timeout | 30 s | Process setup, driven synchronously in Phase 1. |
| registryd readiness timeout | 30 s | Waiting for READY=1 in Phase 1. |
| Reload detection window | 2 s | Waiting for RELOADING=1 after a reload signal. Bounded above by the operation deadline. |
| Restart delay cap | 60 s | The ceiling on exponential backoff. |
| Timeout extension cap | ×4 | The multiple of a phase's base timeout an extension may reach. |
| Operation retention | 60 s | How long a terminal operation is kept before being dropped. |
| Boot attempt threshold | 3 | The default, overridden by peios.bootattempts=. |
OnFailure chain depth | 16 | The maximum handler chain from one originating failure. |
| Pre-eventd buffer | 1 MiB | The compiled-in buffer capacity. |
| Notification datagram | 64 KiB | The receive buffer size. |
| Descriptors per datagram | 64 | The control message buffer capacity. |
| Random seed | 512 bytes | Written at shutdown. Restoring accepts 1–4096 bytes. |
| Control listen backlog | 32 | |
| First injected descriptor | 3 | Where stored descriptors are placed. |
| Final action retry | 1 s | The minimum interval between reboot(2) retries. |
B.3 Environment #
| Variable | Value |
|---|---|
PATH | /sbin:/bin — the compiled-in base for every service. |
The recovery shell additionally receives TERM=linux and HOME=/.
B.4 Kernel command line #
| Parameter | Effect |
|---|---|
peios.safemode=1 | Force Safe mode. |
peios.recovery=1 | Force recovery mode. |
peios.bootattempts=N | The recovery threshold; 0 disables the check. |
peios.quiet=N | Console verbosity: 0, 1 or 2. |
peios.notifysocket=PATH | Override the notification socket path. |
B.5 Descriptors peinit applies #
| Where | SDDL |
|---|---|
/dev/shm, /run, /sys/fs/cgroup after mounting | O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA) |
/dev/null, /dev/zero, /dev/full, /dev/random, /dev/urandom, /dev/tty, /dev/ptmx (DACL only) | D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FRFW;;;WD) |
A provisioned path with no Security | O:SYG:SYD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FR;;;BU) |
A service's /run/<name> runtime directory | O:SYG:SYD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;<service SID>) |
| The random seed file | O:SYG:SYD:(A;;GA;;;SY) |
| The default ServiceSecurity | O:SYG:SYD:(A;;GA;;;SY)(A;;0x0005;;;BA) |
| The default ControlSecurity | O:SYG:BAD:(A;;0x0003;;;SY)(A;;0x0003;;;BA) |
B.6 Access rights #
| Right | Bit |
|---|---|
SERVICE_QUERY_STATUS | 0x0001 |
SERVICE_START | 0x0002 |
SERVICE_STOP | 0x0004 |
SERVICE_INTERROGATE | 0x0008 |
SERVICE_ALL_ACCESS | 0x000F |
SYSTEM_SHUTDOWN | 0x0001 |
SYSTEM_RELOAD_CONFIG | 0x0002 |
B.7 Identifiers #
Every GUID peinit generates — jobs, operations — is UUIDv7, so identifiers sort by creation time.