5.4 The Child Path
Between clone3 returning in the child and execve, peinit runs a
straight line of setup. The path is kept minimal — no logging, no
complex library calls — because it runs after a fork, where very little
is safe to do.
Each step reports through the error pipe with the identifier from §5.3
if it fails, then exits: _exit(126) for a setup failure, _exit(127)
for a failed exec.
| # | Step | Id |
|---|---|---|
| 1 | Close the read end of the error pipe | 1 |
| 2 | setsid() — only with a TTYPath | 12 |
| 3 | Set the standard streams | 2 |
| 4 | ioctl(TIOCSCTTY) — only with a TTYPath | 13 |
| 5 | Reset the signal environment | 3 |
| 6 | Install the KACS token, then close its descriptor | 4 |
| 7 | Set RLIMIT_NOFILE and RLIMIT_CORE | 5 |
| 8 | Set oom_score_adj | 6 |
| 9 | Change the working directory | 7 |
| 10 | Confirm NOTIFY_SOCKET is present in the environment | 9 |
| 11 | Inject stored descriptors from fd 3 upward | 10 |
| 12 | execve | 11 |
5.4.1 The terminal steps #
setsid() comes first, and its position is load-bearing in two
directions. It has to precede the stream setup, because setsid() drops
any controlling terminal the child inherited — doing it afterwards would
throw away the terminal just attached. And it has to precede
TIOCSCTTY, which requires a session leader that does not already own a
controlling terminal.
TIOCSCTTY is passed a literal zero argument, which is what makes it
unable to steal a terminal already owned by another session.
Without a TTYPath neither step runs and the service stays in peinit's
session.
5.4.2 The standard streams #
Without a TTYPath: stdin from /dev/null, stdout and stderr onto the
write ends of the service's output pipes, and every inherited pipe end
that is no longer needed closed.
With a TTYPath: all three streams onto the opened terminal, and the
/dev/null descriptor and both pipe pairs closed. A terminal-attached
service's output is therefore not captured for logging — it goes to
the terminal, which is the point of asking for one.
5.4.3 The signal environment #
peinit blocks every signal for its signalfd (§12.3) and the child
inherits that mask across the fork. Step 5 empties the mask and resets
every resettable disposition to SIG_DFL. A service starting with
signals blocked, or with PID 1's handling in place, is one of the
classic ways for a daemon to behave inexplicably.
5.4.4 oom_score_adj #
-1000 — OOM-immune — for an ErrorControl=Critical service, and 0
for everything else. A Critical service is one whose loss reboots the
machine, so letting the OOM killer choose it would convert memory
pressure into a reboot.
5.4.5 The environment #
There is no step that sets the environment, which is why identifier 8 is
reserved and never emitted. peinit builds the environment in the parent
(§5.5) and hands it to execve as envp, so it arrives with the exec
rather than being installed beforehand. Step 10 is a check rather than a
set: it confirms NOTIFY_SOCKET is present in the prebuilt environment
and fails with a synthetic EINVAL if it is not.
5.4.6 What the child does not inherit #
A service inherits only what peinit hands it: its standard streams and any descriptors injected from the fd store. Everything else peinit holds is created close-on-exec — the control socket and every accepted connection, the notification socket, the epoll instance, the signalfd, every timerfd, both pidfds, every pipe, and every stored descriptor until it is deliberately un-marked at injection.
The signal reset and the close-on-exec discipline together are what make a service start from a clean context rather than from PID 1's privileged one.
The exception is a descriptor opened through the Peios native file interface, which returns without close-on-exec set. peinit repairs that where it opens input devices for the power button; the JFS device descriptor and each service's own cgroup directory descriptor are not repaired, and are inherited across exec.