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.

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

Edit this page