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

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 svctl command 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 #

ConcernComponent
Authentication and token mintingauthd
The local identity databaselpsd
Log storage, indexing and querieseventd
Registry storageregistryd, over LCS
Packaging and installationpeipkg and the role system
Device managementeudev
Network configurationa dedicated service
File access control enforcementFACS

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, /sys and /dev are mounted and have been moved into the real root;
  • the environment holds TERM and 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.

Note

The initramfs itself is assembled by mkirf from hook scripts that packages contribute; mkirf resolves the ordering and bakes the sequence its PID 1 runs. What peinit depends on is the handoff contract above, not how the initramfs arranged to satisfy it.

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:

ServiceWhy
registrydStarts before authd exists at all.
lpsdMust be running before authd can resolve a local identity.
authdNeeds SeTcbPrivilege and SeCreateTokenPrivilege; it is the minter for everything else.
eventdStarts 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.

Note

This is the same arrangement Windows uses: the SCM, LSASS and the core platform services all run as LocalSystem. The trust anchor is that peinit is SYSTEM, and the kernel guarantees that by handing PID 1 the boot token.

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 pointFilesystemFlagsProvided by
/procprocnosuid, nodev, noexecinitramfs
/syssysfsnosuid, nodev, noexecinitramfs
/devdevtmpfsnosuidinitramfs
/dev/ptsdevptsnosuid, noexecpeinit
/dev/shmtmpfsnosuid, nodevpeinit
/runtmpfsnosuid, nodevpeinit
/sys/fs/cgroupcgroup2nosuid, nodev, noexecpeinit

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:

NodeDACL
/dev/null, /dev/zero, /dev/full, /dev/random, /dev/urandom, /dev/tty, /dev/ptmxD:(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.

Note

NTP corrects the clock properly later in the boot. This step only gets it into the right decade, so that early timestamps are not absurd and the timer subsystem has a wall clock to anchor to.

2.3.6 Step 6: Start registryd #

peinit holds a compiled-in definition for registryd — the only compiled-in service definition there is:

FieldValue
ImagePath/sbin/registryd
ArgumentsMachine=/var/state/loregd/Machine.hive, Users=/var/state/loregd/Users.hive
IdentitySYSTEM
ReadinessNotify
ErrorControlCritical

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:

  1. The control socket at /run/services/peinit/control.sock, which serves every runtime command for the lifetime of the system.
  2. The JFS device: peinit opens /dev/jfs and adds the descriptor to its event loop, enabling ad-hoc job submission once Phase 2 runs.
  3. Loopback: peinit brings up lo over netlink, because services that bind 127.0.0.1 need 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 #

FailureResponse
Root writability probe failsRecovery
A mount point cannot be createdRecovery
A peinit-owned filesystem fails to mountRecovery
A mounted filesystem cannot be stamped with its descriptorRecovery
/proc/self/mountinfo unreadable or unparseableRecovery
/proc, /sys or /dev already mounted, or EBUSYTolerated as success
A device node in the policy list cannot be stampedWarning; node keeps the inherited default
A device node in the policy list does not existNoted; boot continues
Random seed absent, oversized, empty, or unrestorableWarning; boot continues
Machine ID read, generation, or write failsRecovery
Machine ID absent, empty, or malformedRegenerated; boot continues
Any RTC or clock failureRecovery
registryd fails to start, or setup times outRecovery
registryd readiness times outRecovery
Base registry provisioning failsRecovery
Schema-version probe returns a wrong type or lengthRecovery
An autorun script is missing, unspawnable, or exits non-zeroWarning; boot continues
A provisioning entry is malformedWarning; entry skipped
An optional provisioned path failsWarning; boot continues
A required provisioned path failsRecovery
Control socket creation failsRecovery
JFS open or registration failsWarning; boot continues
Loopback bring-up failsWarning; 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.

ValueTypeRequiredDefaultMeaning
Kindstringyes—directory or file.
Pathstringyes—Absolute path to create or verify.
Securitybinarynobuilt-inThe Peios file Security Descriptor to apply.
Requireddwordno0If 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 Requires target fails the dependent.
  • Warnings — a Readiness=Alive service 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:

KeyDefaultMeaning
Machine\System\Boot\MaxParallelStarts10Services 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.

  1. eudev — device management. SYSTEM, with RequiredPrivileges stripped to the minimum it needs. Starts before authd exists.
  2. lpsd — the local identity database. Depends only on registryd.
  3. authd — the identity authority. Depends on registryd and lpsd. Once it is ready, token minting is available.
  4. eventd — logging and audit. Services started before it log into peinit's pre-eventd buffer.
  5. Networking — the first service to receive a token from authd.
  6. Application services, in dependency order.
  7. Login services, last, so the system is operational before it accepts a session.

2.5.3.2 The bootstrap matrix #

ServicePhaseIdentityToken fromReadinessErrorControl
registryd1SYSTEMminted by peinitNotifyCritical
eudev2SYSTEMminted by peinit, privileges strippedAliveNormal
lpsd2SYSTEMminted by peinitNotifyCritical
authd2SYSTEMminted by peinitNotifyCritical
eventd2SYSTEMminted by peinitNotifyCritical
networking2service tokenauthdNotifyNormal
sshd2service tokenauthdAliveNormal
application services2service tokenauthdper-serviceNormal

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:

KeyDefaultMeaning
Machine\System\Boot\SettleTimeout5Seconds 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.

Note

The motivating case is a console login prompt being scribbled over by peinit's own progress messages. That is a scheduling preference, not a dependency — the prompt does not require anything to have started, it just wants the console to itself. Expressing it as a dependency would mean naming every service that might print, which changes every time the system does.

2.5.5 Boot success #

A boot is successful once every Critical service has held a dependent-satisfying state continuously for a grace period:

KeyDefaultMeaning
Machine\System\Boot\BootSuccessGrace30Seconds 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 #

FailureResponse
A service definition fails to decodeRecovery
A registry read times outRecovery
Invalid MaxParallelStarts or SettleTimeoutRecovery
A dependency cycleAll services in it Failed; Safe mode if any is Critical (see below)
An unresolvable conflictBoth Failed; Safe mode if either is Critical (see below)
A missing Requires targetThe dependent Failed
A Critical service fails during bootRestart budget, then reboot
A non-Critical service failsFailed; its Requires dependents fail; the rest continue
authd unavailable when a service needs a tokenThat 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=1 services 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.

Note

Safe mode is purely a boot-sequencing concern. Once booted, an administrator starts anything by hand exactly as in Full mode. It is for a system whose configuration is broken but whose TCB is healthy.

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=1 on 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:

ValueBehaviour
0Write unconditionally.
1Do 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.
2Additionally 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 #

ParameterEffect
peios.safemode=1Force Safe mode.
peios.recovery=1Force recovery mode regardless of the counter.
peios.bootattempts=NSet the recovery threshold; 0 disables the check.
peios.quiet=NConsole verbosity, as above.
peios.notifysocket=PATHOverride 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.

Note

The counter catches boots where peinit runs but the system never reaches health — a crash-looping Critical service being the usual case. It deliberately does not try to catch a peinit too broken to reach its own increment. Recovery mode is itself a peinit mode, so a peinit that cannot start cannot deliver recovery either, and a higher count could not help. That failure belongs to binary integrity, not to the counter.

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=1 on 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:

  1. Completes Phase 1 steps 1–5 if they have not been reached yet.
  2. Ensures the base registry structure exists, so the shell sees a normal layout even on a system that has never been provisioned.
  3. Attempts to start registryd. A failure here is ignored — recovery delivers a shell whatever registryd's state.
  4. Skips all Phase 2 services.
  5. Starts a shell on /dev/console from a compiled-in definition, with no registry dependency: /bin/recsh if it is present and executable, otherwise /bin/sh, running as SYSTEM with a fixed environment of PATH=/sbin:/bin, TERM=linux and HOME=/. peinit does not care where either binary comes from.
  6. 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.

Note

Recovery mode is not a degraded boot; it is a maintenance environment. There are no security protections beyond what the kernel provides. The administrator has a SYSTEM shell and the corresponding responsibility.

2.8.3 Offline registry access #

If registryd is what caused the recovery, the administrator needs tools that work without it. Three paths exist:

PathWhat it does
loregd --inspectorReads the storage database directly, bypassing LCS, for diagnosis.
loregd --recover-from-backupRestores from the automatic backup taken on every registryd startup.
loregd --dangerously-clear-databaseWipes 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.

Note

/bin/recsh exists so a system can ship a purpose-built recovery shell — one that bundles the offline registry tools, presents guidance, or curates a command set — without making it mandatory. /bin/sh is the floor where it is absent. peinit treats both as opaque executables.

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=1 it stays there; without, it passes through Completed to release dependents and then goes Inactive.
  • A non-zero exit goes to Failed.
  • ExecStartPost runs after the successful exit rather than after a readiness signal, and does not run at all if the Oneshot failed.
  • StartTimeout covers 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.

FieldTypeDefaultMeaning
ImagePathstringrequiredAbsolute path to the service binary.
Argumentsmulti_string—Arguments passed to the binary.
Typedword0 (Simple)0 Simple, 1 Oneshot. §3.1
Triggersmulti_string—When the service starts automatically. §3.4
Disableddword0If 1, no trigger activates the service.
SafeModedword0If 1, attempt this service in Safe mode. Implied by ErrorControl=Critical. §2.6
IdentitystringLocalServicePrincipal for the service token. §4.1
RequiredPrivilegesmulti_string—Privileges to keep; all others are removed. §4.5
Requiresmulti_string—Hard dependencies. §7.1
Wantsmulti_string—Soft dependencies. §7.1
BindsTomulti_string—Runtime coupling. §7.1
Conflictsmulti_string—Mutual exclusion. §7.1
OnFailurestring—Service to start when this one fails. §6.3
ErrorControldword0 (Normal)0 Normal, 1 Critical.
RemainAfterExitdword0Oneshot only: stay Completed after a successful exit.
SuccessExitCodesmulti_string—Non-zero exit codes treated as success.
ExecStartPremulti_string—Commands run before the main binary, sequentially. §5.3
ExecStartPostmulti_string—Commands run after readiness or successful exit. §5.3
HookIdentitystring—Principal for the hook processes. Falls back to Identity. §4.1
ExecReloadstring—Reload command, or signal:<NAME>. Absent means SIGHUP. §6.5
PreStartCheckTimeoutdword5Seconds before a filesystem check helper is killed. §3.5
StartTimeoutdword30Seconds for the entire start sequence. §5.3
StopTimeoutdword10Seconds after SIGTERM before SIGKILL.
WatchdogTimeoutdword0Seconds between expected WATCHDOG=1 pings; 0 disables. §6.6
HealthCheckstring—Command run periodically. Exit 0 is healthy. §5.6
HealthCheckIntervaldword30Seconds between health checks.
HealthCheckTimeoutdword5Seconds before a health check is killed and counted failed.
HealthCheckRetriesdword3Consecutive failures before the service is unhealthy.
RestartPolicydword1 (OnFailure)0 Never, 1 OnFailure, 2 Always. §6.4
RestartMaxRetriesdword5Consecutive restarts before Failed. §6.4
RestartWindowdword120Seconds of sustained health that reset the restart counter.
RestartDelaydword1Seconds before a restart; doubles each consecutive failure, capped at 60.
Readinessdword0 (Notify)0 Notify, 1 Alive. Ignored for Oneshot.
NotifyAccessdword0 (Main)Who may send notifications. Main is the only mode. §10.5
FdStoreMaxdword0Maximum descriptors held for the service; 0 disables the store. §10.6
TimerPersistentdword1Catch up a missed timer run after a reboot. §9.3
TimerJitterdword0Maximum random delay added to each firing. §9.4
Environmentmulti_string—KEY=VALUE pairs added to the environment. §5.5
WorkingDirectorystring/Working directory for the process.
TTYPathstring—Terminal to attach as the standard streams and controlling terminal. §5.4
RuntimeDirectoriesmulti_string—Private directories under /run, created before the main process.
LimitNOFILEdword—RLIMIT_NOFILE.
LimitCOREdword—RLIMIT_CORE, in bytes.
Conditionsmulti_string—Start-time conditions; failure skips the service. §3.5
Assertsmulti_string—Start-time assertions; failure fails the service. §3.5
DisplayNamestring—Human-readable name for status display.
Descriptionstring—What the service does.
ServiceSecuritybinaryinheritDescriptor controlling runtime operations on the service. §4.6

3.2.1 Registry types #

Schema typeRegistry type
stringREG_SZ, UTF-8
multi_stringREG_MULTI_SZ, an ordered list
dwordREG_DWORD, 32-bit unsigned
binaryREG_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 to LocalService.
  • HookIdentity — empty is the same as absent, and falls back to the service's Identity.
  • DisplayName and Description — 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.

TriggerFormMeaning
BootbootStart during the Phase 2 boot sequence.
Deferred bootboot:settledStart once the boot set has settled, or the deadline expires. §2.5
Timertimer:<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.

CheckFormPasses when
pathpath:<path>The path exists, of any type.
filefile:<path>A regular file exists there.
directorydirectory:<path>A directory exists there.
registryregistry:<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.

Note

The model exists because the event loop cannot block on a userspace service during normal supervision. In single-threaded PID 1 a blocking syscall blocks everything — child reaping, watchdog expiry, shutdown signals, every other event stops while the syscall is stuck. Reading the registry means waiting on registryd, and registryd is a service peinit supervises.

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;
  • stop is accepted, so an administrator can drain it cleanly, using the cached StopTimeout;
  • start, restart and reload are rejected with UNKNOWN_SERVICE — there is no definition to work from;
  • status reports the current runtime state with definition_removed set, 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:

IdentitySourceMechanism
SYSTEMMinted by peinit from its own identitykacs_create_token. §4.2
Anything elseauthdThe token request flow. §4.3
Absent or emptyauthdDefaults 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.

ContextIdentity used
Main processIdentity
ExecStartPre / ExecStartPostHookIdentity if set, otherwise Identity
Health checksIdentity, always
ExecReload external commandIdentity, always
Ad-hoc jobsThe 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.

Note

HookIdentity typically grants more than the service itself. peinit does not validate filesystem permissions on hook binaries, so where it grants elevated privileges the administrator is responsible for the hook binary and its parent directories not being writable by anything lower-privileged.

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.

Note

Minting is an interim mechanism. The intended model is for peinit to derive the token from its own handle, through a KACS duplicate-with-additions operation — kernel-attested as a descendant of peinit's real token, and requiring no token-minting privilege at all. That operation does not exist in KACS yet, so peinit mints.

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:

  1. peinit sends the Identity value verbatim.
  2. authd routes it to an identity source — a built-in for the well-known principals, lpsd for local accounts, a connector for domain accounts.
  3. The source returns the principal's user SID and group SIDs.
  4. 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.created event, and it says LocalService for 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 #

RightBitGrants
SERVICE_QUERY_STATUS0x0001Query state, PID, cause, health, warnings.
SERVICE_START0x0002Start the service.
SERVICE_STOP0x0004Stop the service.
SERVICE_INTERROGATE0x0008Reload the service.
SERVICE_ALL_ACCESS0x000FThe 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 rightMaps to
GENERIC_READSERVICE_QUERY_STATUS
GENERIC_WRITESERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE
GENERIC_EXECUTESERVICE_START | SERVICE_STOP | SERVICE_INTERROGATE
GENERIC_ALLSERVICE_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:

  1. Takes the caller's token, captured when the connection was accepted.
  2. 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.
  3. Calls AccessCheck with the caller's token, that descriptor, the generic mapping above, and the right the command needs.
  4. On denial, returns ACCESS_DENIED and records the attempt as an access.denied event carrying the caller's SID, the target, the requested right by name, the requested access bits and the granted bits.
  5. 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 #

RightBitGrants
SYSTEM_SHUTDOWN0x0001Initiate poweroff, reboot or halt.
SYSTEM_RELOAD_CONFIG0x0002Re-read all definitions from the registry.

The generic mapping:

Generic rightMaps to
GENERIC_READnothing
GENERIC_WRITESYSTEM_RELOAD_CONFIG
GENERIC_EXECUTESYSTEM_SHUTDOWN
GENERIC_ALLboth

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.

  1. Conditions are evaluated. Any failure transitions the service to Skipped and abandons the start. Skipped satisfies dependents.
  2. If every condition passed and the service has asserts, they are evaluated. Any failure transitions the service to Failed with cause AssertionError and 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):

BytesContent
0–3little-endian u32, the child setup step identifier
4–7little-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:

IdStep
1Close the read end of the error pipe
2Set the standard streams
3Reset the signal environment
4Install the service token
5Set the resource limits
6Set oom_score_adj
7Set the working directory
8reserved
9Confirm NOTIFY_SOCKET
10Inject stored descriptors
11Exec the binary
12Create a session
13Acquire 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 #

  1. Close the write end of the error pipe.
  2. 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.
  3. 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 for READY=1, then Active.
  • Simple with Readiness=Alive: Active as soon as exec succeeds.
  • Oneshot: wait for the exit. Success is Completed; without RemainAfterExit it 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.

#StepId
1Close the read end of the error pipe1
2setsid() — only with a TTYPath12
3Set the standard streams2
4ioctl(TIOCSCTTY) — only with a TTYPath13
5Reset the signal environment3
6Install the KACS token, then close its descriptor4
7Set RLIMIT_NOFILE and RLIMIT_CORE5
8Set oom_score_adj6
9Change the working directory7
10Confirm NOTIFY_SOCKET is present in the environment9
11Inject stored descriptors from fd 3 upward10
12execve11

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:

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

Note

Write access to Machine\System\Init\EnvVars\ is equivalent to compromising every service on the system. peinit does not filter variable names, consistent with the registry's write-authority threat model — the key's descriptor is the boundary. The recommended default is SYSTEM full control and Administrators read-only.

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.

Note

Active health checks on ErrorControl=Critical services deserve caution. A false positive eventually exhausts the restart budget and reboots the machine. Critical services — registryd, authd, lpsd, eventd — are usually better served by a passive watchdog with a generous timeout: the service knows whether it is healthy and can stop sending keepalives when it decides it is not. peinit does not prevent health checks on Critical services and applies identical semantics to them, but the escalation path ends at a reboot.

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:

  1. Marks it leaked, recording the path, the kind and the time.
  2. Increments the service's cgroup generation, so the next start builds a fresh tree (§5.1).
  3. 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.leaked event 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 warnings array, one entry per leak, each an object with the sub-cgroup path, its kind, and the time of detection:

    {"path": "/sys/fs/cgroup/peinit/jellyfin/health", "type": "health", "detected_at": "2026-06-01T12:34:56.123456789Z"}
    
  • 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.

StateProcess?Satisfies dependents?Meaning
InactiveNoNoNot started, or stopped cleanly and not set to restart.
StartingMaybeNoActivation in progress: checks, hooks, fork, or the readiness wait. The process may not exist yet.
ActiveYesYesRunning and ready.
ReloadingYesYesRe-reading configuration. Still satisfies dependents.
StoppingBrieflyNoSIGTERM sent, awaiting exit or SIGKILL escalation.
CompletedNoYesOneshot only. Exited successfully.
BackoffNoNoA restart is pending; waiting out the backoff delay.
FailedNoNoExited abnormally with the restart policy exhausted or absent.
AbandonedYes, unkillablyNoSIGKILL sent and processes survived. Supervision has stopped, the cgroup is leaked.
SkippedNoYesConditions 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 #

  1. 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.
  2. 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.
  3. Only peinit transitions a service. The control socket produces operations; nothing outside peinit writes state.
  4. 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.
  5. Readiness is per start generation. The generation increments on every transition into Starting, and a READY=1 carrying 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.

FromToTrigger
InactiveStartingStart command, dependency resolution, or a timer trigger.
InactiveSkippedA condition check failed.
InactiveFailedValidation, assertion, cycle or dependency failure — before any process existed.
StartingActiveReadiness. Simple only: READY=1 received, or the process exists under Readiness=Alive.
StartingCompletedOneshot exited successfully. Without RemainAfterExit it passes through, releasing dependents, then goes Inactive.
StartingSkippedA condition failed after the activation had already entered Starting.
StartingFailedAn assert failed after entering Starting; or a timeout, hook failure, setup failure, or an exit before readiness.
StartingBackoffA startup failure with the restart policy allowing a retry and budget remaining.
StartingStoppingAn explicit stop cancelling an in-progress start. A restart on a Starting service is queued rather than cancelling.
StartingFailedThe shutdown wave SIGKILLed a starting service.
ActiveReloadingExecReload issued, or SIGHUP delivered.
ActiveStoppingExplicit stop, conflict eviction, a bound dependency stopping, or shutdown.
ActiveBackoffA crash, a watchdog timeout, or health check failure, with a restart allowed and budget remaining.
ActiveFailedThe same three, with RestartPolicy=Never or the budget exhausted.
ActiveInactiveA Simple service exited successfully and RestartPolicy is not Always. Cause CleanExit.
ActiveBackoffA Simple service exited successfully and RestartPolicy=Always. Cause CleanExitRestart.
ReloadingActiveReload resolved: READY=1, the detection window expiring, the extended wait expiring, or the reload command exiting — success or failure.
ReloadingStoppingThe same triggers as Active to Stopping. The reload is cancelled.
ReloadingBackoffThe main process exited during the reload, with a restart allowed.
ReloadingFailedThe same, with no restart available.
StoppingInactiveThe process exited after an explicit stop or a shutdown.
StoppingFailedThe process exited after a conflict eviction or a bound dependency stopping.
StoppingAbandonedSIGKILL sent and main/ still populated after the post-kill deadline.
BackoffStartingThe backoff delay elapsed. Cause RestartPolicy.
BackoffInactiveAn explicit stop cancelled the pending restart.
CompletedInactiveRemainAfterExit=0 and dependents released; or an explicit stop; or shutdown clearing it.
CompletedStartingA start command or timer trigger re-running the Oneshot.
FailedStartingAn explicit start, a bound dependency recovering, or a timer trigger.
FailedInactiveA reset command clearing the state.
FailedAbandonedA service SIGKILLed by the shutdown wave whose cgroup stayed populated past the post-kill deadline.
AbandonedInactiveA reset command.
SkippedInactiveA 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 #

CauseLeads toMeaning
ExplicitStartStartingAn administrator, an OnFailure handler, or a boot plan started it.
ExplicitStartInactiveAn explicit start on a Skipped service, clearing it so the conditions are re-evaluated.
DependencyStartStartingStarted to satisfy another service's dependency.
RestartPolicyStartingAn automatic restart after a backoff delay.
BindsToRecoveryStartingA bound dependency returned to Active.
TimerStartingA timer trigger fired.
ExplicitStopStoppingAn administrator requested a stop.
ExplicitReloadReloading, ActiveA reload was issued and resolved.
ExplicitResetInactiveAn administrator cleared Failed, Abandoned or Skipped.
ConflictEvictionStoppingA conflicting service started; this one lost.
BindsToPropagationStoppingA bound dependency stopped.
ShutdownWaveStopping, FailedThe system is shutting down. Active and Reloading services go to Stopping; Starting services go straight to Failed.
ProcessCrashFailed, BackoffThe main process exited unexpectedly.
CleanExitInactiveA Simple process exited successfully with a policy other than Always.
CleanExitRestartBackoffA Simple process exited successfully under RestartPolicy=Always.
ReadinessTimeoutFailed, BackoffStartTimeout expired before readiness.
WatchdogTimeoutFailed, BackoffA keepalive did not arrive in time.
HealthCheckFailureFailed, BackoffHealthCheckRetries consecutive failures.
PreHookFailureFailed, BackoffAn ExecStartPre hook exited non-zero, or its token failed, or the start timed out during hooks.
ParentSetupFailureFailed, BackoffA parent-side failure before the fork. No child was created.
PreExecFailureFailed, BackoffPost-fork setup failed before exec, reported through the error pipe.
DependencyFailureFailedA Requires dependency entered Failed.
RestartBudgetExhaustedFailedRestartMaxRetries reached.
CycleDetectedFailedThe service is part of a dependency cycle.
ValidationErrorFailedThe definition failed graph validation.
AssertionErrorFailedA start-time assert failed.
ConditionSkippedSkippedA start-time condition failed.
ProcessUnkillableAbandonedProcesses 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.

Note

The startup failures are restart-eligible because a transient problem during startup should get another go. A pre-hook that failed because a network mount was briefly unavailable deserves a retry rather than an immediate give-up.

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 #

PolicyValueBehaviour
Never0Never restart. The service stays Failed.
OnFailure1Restart on a non-zero exit or a runtime failure. An exit matching SuccessExitCodes is not restarted.
Always2Restart 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.

Note

Runtime watchdog updates suit a service whose phases have genuinely different latency. A database engine might want a tight five-second watchdog in normal operation and sixty seconds during a compaction pass. The service knows its own phases better than whoever wrote its definition does.

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:

PhaseBaseCeiling
StartingStartTimeoutStartTimeout × 4
StoppingStopTimeoutStopTimeout × 4
ReloadingStartTimeoutStartTimeout × 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.

Note

Timeout extension is for a service doing variable-duration work in a transition — replaying a write-ahead log on startup, say. It sends periodic messages to prove it is making progress; if it stops sending, the last deadline fires and peinit escalates as usual. The fourfold cap is what stops a buggy service extending forever.

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 cause DependencyFailure without 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 BindsToPropagation from 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.

Note

Conflicts is for real mutual exclusion: two services binding the same port, or two implementations of one role where exactly one must run. It is not a way to solve resource contention that has a better answer.

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 #

RelationshipA missing target means
RequiresThe dependent is failed with DependencyFailure.
BindsToThe same — treated as a missing Requires.
WantsSilently dropped.
ConflictsSilently 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=Alive that something else depends on hard. Alive readiness 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:

  1. CycleDetected
  2. ValidationError
  3. DependencyFailure

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:

  1. Everything that Requires or BindsTo it transitions to Failed with cause DependencyFailure.
  2. Everything that Wants it is unaffected and starts normally.
  3. Propagation is transitive: if A requires B and B requires C, and C fails, then B fails and then A fails.

7.3.3 On-demand start #

Starting a service explicitly resolves its dependencies first:

  1. Collect the transitive Requires and BindsTo closure.
  2. Collect the transitive Wants closure, best-effort.
  3. Validate the sub-graph — cycles, missing targets.
  4. Resolve conflicts, stopping whatever has to stop.
  5. Start the sub-graph with the same parallel algorithm. Dependencies started this way carry cause DependencyStart and operation source DependencyPropagation.

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.

BootOn-demand
MembersEvery boot-triggered root plus its closureOne requested service plus its closure
A missing hard-dependency targetBlocks the dependent, in Full mode onlyBlocks the dependent
A disabled hard-dependency targetBlocks the dependentStarts it
A validation findingThe service is failed, others continueThe start fails
Multiple findings on one serviceOnly the highest-precedence one is reported—
ConflictsTwo boot-triggered conflicting services fail bothThe conflicting service is evicted
ContextOne boot contextOne context per explicit start
Failure of the whole buildRecovery modeAn 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 with ValidationError and 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
StateMeaning
CreatedThe 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.
RunningExec succeeded and the process is alive.
CompletedThe process exited successfully — code 0, or one in SuccessExitCodes.
FailedThe process failed, or peinit classified the job failed before the fork because parent setup failed.
AbandonedThe 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:

  • id is assigned before the fork, so a job that never forks still has an identity.
  • pid and pidfd land 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_ns records the classification time, failure_cause records what went wrong, and pid, pidfd, started_at_ns, exit_code and exit_signal all stay null. There was no process to have a PID or an exit status.
  • exit_code and exit_signal are populated only when peinit observed an exit — never both, since a process either exits or is killed.
  • For an Abandoned job, ended_at_ns records 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 #

ConcernOwner
Restart policy, dependencies, health check schedule, ErrorControlService
Current state — Active, Failed, …Service
PID, pidfd, exit code, exit signalJob
Execution timestampsJob
Identity and tokenJob
cgroup assignmentJob
Log correlationJob

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
StateMeaning
PendingValidated and queued, waiting on a precondition.
RunningExecuting.
CompletedThe goal was reached. Start: Active for Simple, Completed or Inactive for Oneshot. Stop: the service is no longer running. Reload: the reload resolved.
FailedThe goal was not reached — or the operation's maximum lifetime expired while it was still Pending.
MergedMerged into an existing identical operation, whose identifier is recorded.
CancelledTerminated while Pending. It never executed.
AbortedTerminated 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:

SourceMeaning
AdminA control client asked for it.
BootThe Phase 2 boot plan.
ShutdownThe shutdown lifecycle.
DependencyPropagationA start operation created one for an unsatisfied dependency.
RestartPolicyA restart policy generated a start.
TimerA timer trigger fired.
BindsToRecoveryA bound target returned to Active.
BindsToPropagationA bound target stopped.
ConflictResolutionA conflict evicted the running service.
OnFailureA 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.

ExistingNewResolution
StartStartMerge
StopStopMerge
ReloadReloadMerge
RestartStartMerge — 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 #

ExistingNewResolution
Start (Pending)StopCancel the start, create the stop
Start (Running)StopAbort the start, create the stop
Start (Pending)RestartCancel the start, queue the restart
Start (Running)RestartQueue the restart
Stop (either)StartQueue the start
Stop (either)RestartQueue the restart
Restart (Pending)StopCancel the restart, create the stop
Restart (Running)StopAbort the restart, create the stop
Restart (either)RestartQueue
Reload (Pending)StopCancel the reload, create the stop
Reload (Running)StopAbort the reload, create the stop
Reload (Pending)RestartCancel the reload, create the restart
Reload (Running)RestartAbort the reload, create the restart
Anything (either)ResetReject

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 #

  1. Stop wins over start. An explicit stop always takes priority. The administrator said stop, so stop; a queued start can follow.
  2. Later supersedes earlier. Start then immediately stop means the stop wins and the start is cancelled, recorded as superseded.
  3. 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 — source DependencyPropagation. If one fails, the parent operation fails with DependencyFailure.
  • Wants — source DependencyPropagation. If one fails, the parent continues.
  • BindsTo recovery — source BindsToRecovery, created when a bound target returns to Active, for dependents sitting in Failed with cause BindsToPropagation.

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:

TypeStateAction
OneshotInactive, Completed, FailedCreate a start, source Timer.
OneshotActive, StartingSet the pending flag. One catch-up run, no operation.
SimpleInactive, FailedCreate a start, source Timer.
SimpleActive, StartingNo-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.

EventAdds
operation.requested—
operation.started—
operation.completedduration_ns, result
operation.failedduration_ns, failure_reason
operation.cancelledreason
operation.mergedmerged_into
operation.abortedduration_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:

FieldRequiredMeaning
ImagePathyesThe binary to execute.
ArgumentsnoIts arguments.
EnvironmentnoAdditional variables, as a map rather than KEY=VALUE strings.
TimeoutnoMaximum runtime in seconds. 0 means no limit.
WorkingDirectorynoDefaults to /.
DescriptionnoFor 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 #

  1. Forked with the JFS-provided token.
  2. Runs in its own cgroup under /sys/fs/cgroup/peinit/, the id derived from the job's GUID rather than from a service name.
  3. Output routed to eventd, tagged with the job's GUID.
  4. On exit: emit job.ended, clean up the cgroup, drop the job.
  5. 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.

FieldFormDefault if omitted
DayOfWeekweekday namesany day
DateYear-Month-Day*-*-*
TimeHour:Minute:Second00:00:00
Secondthe :Second of Time:00
Timezonean IANA namesystem-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/step matches the value and every multiple of the step above it, so 0/15 in the minute field is 0, 15, 30 and 45. start..end/step walks 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 #

ShortcutEquivalent
minutely*-*-* *:*:00
hourly*-*-* *:00:00
daily*-*-* 00:00:00
weeklyMon *-*-* 00:00:00
monthly*-*-01 00:00:00
quarterly*-01,04,07,10-01 00:00:00
semiannually*-01,07-01 00:00:00
yearly, annually*-01-01 00:00:00

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/London skips 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 #

ExpressionMeaning
*-*-* 02:00:00Every day at 2am, system-local.
Mon *-*-* 00:00:00Every Monday at midnight.
*-*-1,15 12:00:00The 1st and 15th, at noon.
*-*~01 00:00:00The last day of each month, at midnight.
Mon..Fri *-*-* 09:00:00Weekdays at 9am.
*-*-* *:00/15:00Every fifteen minutes.
*-*-* 02:00:00 Europe/LondonEvery day at 2am London time.

Note

An expression that parses but can never match — *-02-30, or a fixed year already past — is not rejected at parse time. Computing its next occurrence walks forward a day at a time to the year 9999 before concluding there isn't one.

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:

  1. Read the last-run timestamp.
  2. Compute the next scheduled firing after it.
  3. If that time has already passed, at least one run was missed: fire once, immediately.
  4. 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.

Note

Timestamps for a multi-trigger service are keyed by schedule string, so changing a schedule orphans its history and the new schedule produces one spurious catch-up run. A stable trigger identifier would fix it; schedule changes are rare and one extra run is the whole cost. A single-trigger service is unaffected, since its timestamp lives at a fixed name.

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.

Note

The calendar parser deserves heavy testing. Time parsing is a rich source of edge cases: month boundaries, leap years, last-day-of-month arithmetic, DST transitions and timezone database updates are all fertile ground for subtle errors, and every one of them is a bug that only appears on a particular day of a particular year.

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:

KeyDefaultMeaning
Machine\System\Init\MaxControlConnections32Concurrent connections.
Machine\System\Init\MaxRequestSize65536Maximum request size, in bytes.
Machine\System\Init\ConnectionTimeout30Seconds 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.

  1. The shutdown gate. If peinit is shutting down, everything except status, list and operation-status is 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.
  2. 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.
  3. AccessCheck. The caller's token, the target's descriptor, the generic mapping, and the right the command requires (§4.6, §4.7).
  4. On denial, return ACCESS_DENIED and record 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; a denial an administrator cannot see is indistinguishable from a bug.
  5. On grant, classify the command against the service's state (§10.3) and act.

10.2.1 Rights #

CommandRight
startSERVICE_START
stopSERVICE_STOP
restartSERVICE_START and SERVICE_STOP
reloadSERVICE_INTERROGATE
resetSERVICE_STOP
statusSERVICE_QUERY_STATUS
listFiltered per service by SERVICE_QUERY_STATUS
operation-statusSERVICE_QUERY_STATUS on the target service
shutdownSYSTEM_SHUTDOWN
reload-configSYSTEM_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.

InactiveStartingActiveReloadingStoppingCompletedBackoffFailedAbandonedSkipped
startStartMERGEALREADYALREADYQUEUEStartDEFERStartERRORStart
stopNOOPCancel+StopStopStopMERGEClearCancelNOOPERRORNOOP
restartStartQUEUERestartRestartQUEUEStartRestartStartERRORStart
reloadERRORERRORReloadMERGEERRORERRORERRORERRORERRORERROR
resetNOOPERRORERRORERRORERRORERRORERRORClearClearClear
statusOKOKOKOKOKOKOKOKOKOK

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.

  • start creates 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.
  • stop cancels 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.
  • restart cancels the automatic restart and queues an administrator-initiated one.
  • reload and reset are 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 start immediately.
  • 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:

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

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

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

10.5.2 Applying a datagram #

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

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

10.5.3 What the fields do #

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

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

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

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

10.5.4 Bounds #

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

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:

  1. If FdStoreMax is 0, peinit logs the rejection and closes the descriptor.
  2. If the store already holds FdStoreMax entries, peinit logs the rejection and closes the descriptor. The existing store is not modified — a full store does not evict.
  3. FDNAME=<name> names it; an absent or empty name means stored.
  4. FDPOLL=0 marks 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):

  1. 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.
  2. LISTEN_FDS is set to the count.
  3. LISTEN_FDNAMES is set to a colon-separated list of names in the same order as the descriptor numbers.
  4. 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.

Note

The LISTEN_FDS and LISTEN_FDNAMES convention is systemd's, so software already written to receive descriptors that way works unmodified. peinit's own C client library exposes the notification helpers but no descriptor-passing helper, so reaching the store from that library means constructing the control message by hand.

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_REALTIME timestamp,
  • 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.

KeyDefaultMinimumMeaning
Machine\System\Init\PreEventdBuffer10485764096Bytes 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.

Note

In practice the only services that run before eventd are registryd, lpsd, authd and eudev. The first three are Peios-owned with controlled output. eudev is the real overrun risk, being verbose about device enumeration. A crash loop's output is bounded by the restart budget.

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.

KeyDefaultMinimumMeaning
Machine\System\Init\MaxLogLineLength8192256Bytes per line before truncation.
Machine\System\Init\MaxLogBufferPerService655364096Bytes buffered per service pipe before backpressure.
Machine\System\Init\LogReadBytesPerEvent16384512Bytes 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.

  1. Start sending to eventd's log datagram socket, at the path from Machine\System\eventd\LogSocketPath.
  2. 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.
  3. Switch to real-time forwarding — new output sent as it arrives.
  4. 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:

KeyTypeContent
originstringThe service name, or the hook identifier.
is_errorboolTrue for stderr.
messagestringThe line.
timestampuintNanoseconds, wall clock.
job_idbinThe 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/console and /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):

TypeEffect
poweroffStop everything, unmount, power off.
rebootStop everything, unmount, reboot.
haltStop everything, unmount, halt — the CPU stops, the system stays powered.

12.1.2 Signals #

SignalMeaning
SIGINTReboot. The kernel sends it on Ctrl+Alt+Del.
SIGTERMPoweroff. PID 1 cannot be killed by it but may choose to act on it.
SIGPWRPoweroff. 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 ShutdownWave while post-kill verification is pending. A cgroup still populated after the post-kill timeout takes the service to Abandoned with cause ProcessUnkillable and 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:

  1. Each eligible service receives SIGTERM. Already-stopping ones do not.
  2. Each has StopTimeout to exit.
  3. On expiry, SIGKILL to the service's entire cgroup.
  4. 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, ConflictEviction or BindsToPropagation.

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 #

KeyDefaultMeaning
Machine\System\Boot\ShutdownTimeout90Seconds for the whole sequence.
Machine\System\Boot\PostKillTimeout5Seconds 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 #

  1. Snapshot the mount table first, from /proc/self/mountinfo.
  2. 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.
  3. Process in descending path depth, so children go before parents.
  4. A mount point already gone is a successful no-op.
  5. On failure, attempt a read-only remount. If that fails too, record it and continue.
  6. 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.

  1. sync(). Called on all three paths — graceful, forced and Critical reboot.
  2. reboot(2) with RB_POWER_OFF, RB_AUTOBOOT or RB_HALT_SYSTEM.
  3. 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 #

SignalBehaviour
SIGCHLDReap children with waitpid. Match them to tracked jobs; also reap orphans belonging to nothing.
SIGINTReboot. Three within five seconds forces one.
SIGTERMPoweroff.
SIGPWRPoweroff.
SIGHUPIgnored. PID 1 has no controlling terminal.
SIGPIPEIgnored. 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.

PathStop wavesSeed saveUnmountSyncFinal action
GracefulYesYesYesYesYes
Forced — three SIGINTsNo, SIGKILL everythingNoNoYesYes
Critical service failureNoNoNoYesYes

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 capabilityUsed for
SeCreateTokenPrivilegeMinting SYSTEM tokens for platform services during bootstrap, before authd exists (§4.2).
SeTcbPrivilegeRequesting tokens from authd on a service's behalf, and installing a primary token on a child whose identity differs from peinit's own.
Process creationFork and exec, inherent to PID 1.
cgroup managementCreating and destroying trees under /sys/fs/cgroup/peinit/.
Signal deliverySIGTERM and SIGKILL to managed processes.
Mount operationsThe 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.

Note

Minting is interim. The intended model derives a service's token from peinit's own handle through a KACS duplicate-with-additions operation — kernel-attested as a descendant of peinit's real token, and needing no minting privilege. That would let peinit drop SeCreateTokenPrivilege entirely, which is the point: the privilege to mint arbitrary tokens is the strongest thing peinit holds and the one it has the least use for.

13.3 The Attack Surface

Peios / Advanced Peios / peinit / Security

SurfaceReachable byControlsProtected by
Control socketAnything that can connectService lifecycle, shutdownThe socket inode's descriptor, then the peer token and AccessCheck against the target's descriptor
Notification socketAnything that can connectService readiness, watchdog, stored descriptorsThe socket inode's descriptor, then PID matching verified through a pidfd, plus the start generation
Registry keysAnything with registry accessDefinitions, triggers, configurationRegistry key descriptors, enforced by LCS
Machine\System\Init\EnvVars\Anything with registry accessThe environment of every serviceThat key's descriptor, and nothing else — variable names are not filtered
Service cgroupspeinitProcess tracking and clean killOwnership of the hierarchy
Phase 1 mountspeinitVirtual filesystem availabilityHardcoded, no external input
/lcl/policy/autorun.dWhatever can write itArbitrary code as SYSTEM in early bootThe directory's descriptor
Boot attempt counterWhatever can write /.peinit/Recovery mode entryThat file's descriptor
JFS deviceWhatever holds the submission privilegeAd-hoc job submissionA 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:

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

WhatWhereHow
Boot attempt counter/.peinit/boot-attemptsA plain integer, rewritten each boot.
Local machine ID/lcl/etc/machine-idTemporary file, flush, rename.
Random seed/var/state/peinit/random-seedTemporary 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 #

KeyPurposeDefined in
Machine\System\Services\Parent key. Each child key is one service.§3.2
Machine\System\Services\SchemaVersionSchema version guard. REG_DWORD, currently 1. Created by peinit if absent.§2.3, §3.2
Machine\System\Services\ServiceSecurityThe descriptor inherited by definitions that carry none. REG_BINARY.§4.6
Machine\System\Services\<name>One service definition.§3.2
Machine\System\Services\<name>\LastTimerRunLast-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 #

KeyTypeDefaultPurposeDefined in
Machine\System\Boot\MaxParallelStartsdword, > 010Services starting concurrently during boot. Zero, a type mismatch, or a malformed payload is invalid and enters recovery.§2.5
Machine\System\Boot\BootSuccessGracedword30Seconds every Critical service has to hold a dependent-satisfying state before the boot counts as successful.§2.5
Machine\System\Boot\SettleTimeoutdword5Seconds to wait for the boot set to settle before starting boot:settled services regardless. Zero is legal.§2.5
Machine\System\Boot\ShutdownTimeoutdword90Seconds for the entire graceful shutdown.§12.2
Machine\System\Boot\PostKillTimeoutdword5Seconds 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 #

KeyTypeDefaultPurposeDefined in
Machine\System\Init\ControlSecuritybinarySYSTEM and Administrators, both rightsThe descriptor for system-level control operations.§4.7
Machine\System\Init\MaxControlConnectionsdword32Concurrent control socket connections.§10.1
Machine\System\Init\MaxRequestSizedword65536Maximum control request size, in bytes.§10.1
Machine\System\Init\ConnectionTimeoutdword30Seconds before an idle control connection is closed.§10.1
Machine\System\Init\MaxLogLineLengthdword8192Bytes per output line before truncation. Minimum 256; below that the default is used and a warning logged.§11.3
Machine\System\Init\MaxLogBufferPerServicedword65536Pipe capacity per service, applied with F_SETPIPE_SZ. Minimum 4096 (one page, the kernel's own floor).§11.3
Machine\System\Init\LogReadBytesPerEventdword16384Bytes drained from one output pipe per readable event. Minimum 512.§11.3
Machine\System\Init\PreEventdBufferdword1048576Bytes of output retained before eventd is available. Applied at boot and on reload. Minimum 4096.§11.2
Machine\System\Init\EnvVars\parent keyemptyVariables 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 keyemptyBoot-time path provisioning entries.§2.4
Machine\System\Init\ProvisionedPaths\<name>\Kindstring—directory or file. Required.§2.4
Machine\System\Init\ProvisionedPaths\<name>\Pathstring—The absolute path. Required.§2.4
Machine\System\Init\ProvisionedPaths\<name>\Securitybinarybuilt-inThe descriptor to apply.§2.4
Machine\System\Init\ProvisionedPaths\<name>\Requireddword0If 1, a failure enters recovery before Phase 2.§2.4

A.4 Other subsystems #

KeyTypePurposeDefined in
Machine\System\eventd\LogSocketPathstringWhere 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 #

PathPurpose
/usr/bin/peinit2Where peinit is installed in package storage.
/bin/peinit2The runtime path the kernel init= names.
/sbin/registrydThe compiled-in registryd image path.
/var/state/loregd/Machine.hiveThe machine hive, passed to registryd.
/var/state/loregd/Users.hiveThe users hive, passed to registryd.
/.peinit/peinit's own state directory on the root filesystem.
/.peinit/boot-attemptsThe boot attempt counter.
/lcl/etc/machine-idThe local machine identifier.
/var/state/peinit/random-seedThe persisted entropy seed.
/lcl/policy/autorun.dPhase 1 autorun scripts.
/run/services/peinit/control.sockThe control socket.
/run/services/peinit/notify.sockThe notification socket, by default.
/sys/fs/cgroup/peinit/The root of every service cgroup tree.
/dev/jfsThe job forwarding device.
/dev/rtc, /dev/rtc0The hardware clock, in that order of preference.
/dev/consoleWhere peinit writes its own messages.
/bin/recsh, /bin/shThe recovery shell, in that order of preference.

B.2 Timeouts and limits #

ConstantValueMeaning
registryd setup timeout30 sProcess setup, driven synchronously in Phase 1.
registryd readiness timeout30 sWaiting for READY=1 in Phase 1.
Reload detection window2 sWaiting for RELOADING=1 after a reload signal. Bounded above by the operation deadline.
Restart delay cap60 sThe ceiling on exponential backoff.
Timeout extension cap×4The multiple of a phase's base timeout an extension may reach.
Operation retention60 sHow long a terminal operation is kept before being dropped.
Boot attempt threshold3The default, overridden by peios.bootattempts=.
OnFailure chain depth16The maximum handler chain from one originating failure.
Pre-eventd buffer1 MiBThe compiled-in buffer capacity.
Notification datagram64 KiBThe receive buffer size.
Descriptors per datagram64The control message buffer capacity.
Random seed512 bytesWritten at shutdown. Restoring accepts 1–4096 bytes.
Control listen backlog32
First injected descriptor3Where stored descriptors are placed.
Final action retry1 sThe minimum interval between reboot(2) retries.

B.3 Environment #

VariableValue
PATH/sbin:/bin — the compiled-in base for every service.

The recovery shell additionally receives TERM=linux and HOME=/.

B.4 Kernel command line #

ParameterEffect
peios.safemode=1Force Safe mode.
peios.recovery=1Force recovery mode.
peios.bootattempts=NThe recovery threshold; 0 disables the check.
peios.quiet=NConsole verbosity: 0, 1 or 2.
peios.notifysocket=PATHOverride the notification socket path.

B.5 Descriptors peinit applies #

WhereSDDL
/dev/shm, /run, /sys/fs/cgroup after mountingO: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 SecurityO:SYG:SYD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;FR;;;BU)
A service's /run/<name> runtime directoryO:SYG:SYD:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;<service SID>)
The random seed fileO:SYG:SYD:(A;;GA;;;SY)
The default ServiceSecurityO:SYG:SYD:(A;;GA;;;SY)(A;;0x0005;;;BA)
The default ControlSecurityO:SYG:BAD:(A;;0x0003;;;SY)(A;;0x0003;;;BA)

B.6 Access rights #

RightBit
SERVICE_QUERY_STATUS0x0001
SERVICE_START0x0002
SERVICE_STOP0x0004
SERVICE_INTERROGATE0x0008
SERVICE_ALL_ACCESS0x000F
SYSTEM_SHUTDOWN0x0001
SYSTEM_RELOAD_CONFIG0x0002

B.7 Identifiers #

Every GUID peinit generates — jobs, operations — is UUIDv7, so identifiers sort by creation time.

Peios Learn — documentation for the Peios project.

Built with Trail.