# 5.3 The Pre-Exec Sequence

_Peios / Advanced Peios / peinit / Starting a Service_

> Everything between deciding to start a service and its binary running — timeouts, runtime directories, hooks and the fork.

Everything between "peinit decides to start service X" and "X's binary
is running". The service is in Starting throughout.

## 5.3.1 Step 1: Arm the start timeout

`StartTimeout` covers the entire remaining sequence — pre-hooks, fork,
exec, and the readiness wait. A single deadline, measured from the
operation's creation rather than from this point, bounds all of it.

Expiry aborts the start and kills the service's cgroup tree. The cause
recorded depends on where the deadline landed: a timeout during
pre-hooks is `PreHookFailure`; one during the readiness wait or the main
process's execution is `ReadinessTimeout`.

## 5.3.2 Step 2: Provision runtime directories

If the definition lists `RuntimeDirectories`, peinit creates each one
under `/run` and applies its descriptor (§3.3) before anything else in
the launch. Failure classifies as `ParentSetupFailure`.

## 5.3.3 Step 3: Run pre-exec hooks

Each `ExecStartPre` command runs in sequence, forked into `hooks/`. A
token is materialised for each at the point of use, from `HookIdentity`
if set and `Identity` otherwise (§4.1); a failure there fails the hook
and the service with `PreHookFailure`.

Any hook exiting non-zero kills the entire service cgroup tree —
cleaning up whatever grandchildren the hook left — and fails the service
with `PreHookFailure`.

When every hook has succeeded, peinit kills the `hooks/` sub-cgroup
before the main process starts, so a hook that forked something into the
background does not become part of the service.

## 5.3.4 Step 4: Materialise the service token

The main process's token, per §4.1. A failure means no child exists and
the service fails with `ParentSetupFailure`.

If a later parent-side step fails before the fork, peinit closes the
token descriptor exactly once. A failure to close is retained as cleanup
evidence and does not change how the start is classified — the
classification describes what went wrong with the start, and a failed
close is a separate fact about the same failure.

## 5.3.5 Step 5: Create the cgroups and the error pipe

The `main/` and `health/` sub-cgroups are created, and a
`pipe2(O_CLOEXEC)` error pipe. The parent keeps the read end,
non-blocking; the child will hold the write end.

The pipe is how the child reports a failure it hits after fork but
before exec. If exec succeeds, the write end closes automatically —
that is what `O_CLOEXEC` is for — and the parent reads EOF, which means
setup succeeded. If setup fails, the child writes a structured error
first.

**The payload is exactly eight bytes, written with one `write(2)`:**

| Bytes | Content |
|---|---|
| 0–3 | little-endian `u32`, the child setup step identifier |
| 4–7 | little-endian `i32`, a positive Linux errno |

The child writes at most one payload — the first reportable failure —
and then exits. The parent treats a non-EOF payload that is not exactly
eight bytes, or carries an unknown step identifier, or an errno of zero
or less, as malformed evidence and fails closed with `PreExecFailure`.

The step identifiers:

| Id | Step |
|---|---|
| 1 | Close the read end of the error pipe |
| 2 | Set the standard streams |
| 3 | Reset the signal environment |
| 4 | Install the service token |
| 5 | Set the resource limits |
| 6 | Set `oom_score_adj` |
| 7 | Set the working directory |
| 8 | *reserved* |
| 9 | Confirm `NOTIFY_SOCKET` |
| 10 | Inject stored descriptors |
| 11 | Exec the binary |
| 12 | Create a session |
| 13 | Acquire the controlling terminal |

Identifier 8 is reserved and never emitted: the environment is built in
the parent and applied by `execve`, so there is no step in the child
that could fail. Identifiers 12 and 13 are the two terminal steps, which
occur only for a service with a `TTYPath`.

If `pipe2` fails, no child exists and the service fails with
`ParentSetupFailure`.

## 5.3.6 Step 6: Fork

`clone3(CLONE_PIDFD | CLONE_INTO_CGROUP)`, targeting `main/`. This does
two things atomically: it returns a pidfd for the child, and it places
the child directly into `main/` at creation. There is no window in which
the child exists without a pidfd, and none in which it runs or execs in
peinit's own cgroup.

Placing the child at creation also avoids the alternative, which would
be having the child write its own PID into `cgroup.procs` — something
its post-installation token could not do.

A `clone3` failure means no child exists: `ParentSetupFailure`, with
`EMFILE`/`ENFILE`, `EAGAIN` and `ENOMEM` the usual causes.

Immediately after `clone3` returns, in the parent and on either outcome,
peinit closes the token descriptor and the `main/` cgroup descriptor,
exactly once each. The child relies on close-on-exec and its own exit
instead. A failure to close is cleanup evidence and does not change how
the start is classified.

## 5.3.7 Step 7: The parent after the fork

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.
