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.

The steps, in the order the child runs them:

#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
6Set RLIMIT_NOFILE and RLIMIT_CORE5
7Set oom_score_adj6
8Install the KACS token, then close its descriptor4
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 Resources before the token #

The resource limits and oom_score_adj are set while the child still holds peinit's credentials, and only then is the service token installed. Both are peinit's policy rather than authority the service holds: raising a hard limit or lowering oom_score_adj needs CAP_SYS_RESOURCE, which KACS projects from SeIncreaseQuotaPrivilege. Setting them after the install would make every Critical service, and every service with a raised limit, depend on that privilege. A RequiredPrivileges list that leaves it out would then stop the service from starting at all.

The step identifiers are unchanged by this order. They name steps, not positions.

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

A descriptor opened through the Peios native file interface returns without close-on-exec set, so it does not get the discipline for free. peinit sets the flag by hand on each of those — the input devices it opens for the power button, the main/ cgroup directory descriptor, /dev/null, and the service and recovery consoles — immediately after opening them. The child duplicates its intended streams onto descriptors 0, 1 and 2 without close-on-exec, then explicitly closes the original descriptors. The streams survive exec; the original handles do not.

Edit this page