What this project has had to understand about Linux, beyond its manual: the points that changed a design or caused a failure. It is not a guide to Linux.
The sandbox
- Sessions run under
bwrapwith their own namespaces: the system read only, the workspace writable, the network shared. Its one call site is replaceable (precept I6 inPLAN.md). - What is not bound in does not exist. The served repository file is outside every sandbox, so a session with a file remote could sync only between turns (0160971667).
- A login directory is bound at
~/plus its name, read and write: ahomesentry of/var/lib/ocx/release/stagedappears as~/staged(e91c8acbcd). A session whose definition drops such an entry loses that login, as Codex did with/root/.codex(5ad49ff621). /etc/hostsand/etc/resolv.confare bound in, so names resolve inside the sandbox as on the host./tmpis the session's own and is emptied between turns. A file kept there for the next turn is gone; keep it in the workspace (877097e6b9).
Files and links
- A symbolic link can redirect any check made before a file is opened. A report path checked as inside the tree was swapped for a link to a file outside before it was read (877097e6b9).
- A link to nothing is written through.
editon a link to a file not there yet made that file where the link leads, outside the tree, and a path under a link that leads back to itself cannot be looked at (too many levels of symbolic links); OCX refuses a path any part of which, not there, is a link, andfindpasses over links (898527c82d). /proc/self/fd/Nnames the file a handle actually opened, so a check made through it cannot be redirected (8ef3cd9765).
Processes
- An open file description's flags are shared by every process holding a
descriptor to it, including
O_NONBLOCK. Crystal's runtime making ocx2's inherited stdin non-blocking made the parent's pipe non-blocking too; a program reading deliveries took the empty read as end of input. Opening/proc/self/fd/Ngives a process its own description of the same pipe (tools/stdio.cr), which a socket does not allow. - Each session runs in its own process group, started by
setsid; the watcher stops a session by signalling the whole group, so nothing it started is left behind (72ce3dca69). A daemon that dies leaves its watchers and their sessions running. setsidruns its program in its own place when it leads no process group, as a process just started does, and forks only when it leads one: the host's, BusyBox's, did so (tried 2026-10-21), as util-linux's manual says its own does. So the numberProcess.newgives forsetsid PROGRAMis the program's, which a detached project's directory names (HISTORY.md decision 120).- A window set on a terminal is told only to its session's foreground.
Setting a pseudo-terminal's size (
TIOCSWINSZ) sends SIGWINCH to the foreground process group of the session whose controlling terminal it is, and to no one when it is none's, or when the size is unchanged; a program merely reading and writing it never learns its window changed. bubblewrap's--new-sessionputs its command in a session with no controlling terminal, so a run's process on a terminal starts undersetsid -cinstead, whose sessiontimeoutand bubblewrap stay in (HISTORY.md decision 85). - What a program on a pseudo-terminal has yet to read is asked of the far
end.
TIOCOUTQon the near end says 0 whatever waits there. The far end, opened through the near end (TIOCGPTPEER, withO_NOCTTY, so it becomes no one's controlling terminal) and closed at once, says by poll(2) whether it holds what its program has not read, Linux first giving it what the near end took; a canonical terminal shows only ended lines, and its mode is the far end's (tcgetattr). Each read of the program tells the near end's writers there is room, anEPOLLOUTedge though there was room before, and the far end's last close tells them it has hung up. Opening and closing the far end while the program holds it ends nothing (tried 2026-10-22 on Linux 6.1, for HISTORY.md decision 106). - A session started by another parent is left alone. A watcher ends only
what its run's lock names: what an earlier watcher of that run left, as it
answers the run
lost. - A subreaper is given what its children leave running. A process that
sets
prctl(PR_SET_CHILD_SUBREAPER)becomes the parent of a process its child leaves behind. bubblewrap 0.11 exits once it has its command's exit status, leaving its sandbox's init, which it does not wait for, to that subreaper; the kernel ends every process of a pid namespace before its init's end completes, so waiting for that init waits for the whole sandbox. A process's children are listed in/proc/self/task/TID/children, and one in a pid namespace of its own has two pids on itsNSpid:status line (tried 2026-10-19, for HISTORY.md decision 80). That init waits until none of its own children is left, and only--die-with-parentends it with bubblewrap's outer process: without it, a TERM to that process, ortimeout's KILL, ends it alone, and the init and its program run on (tried 2026-10-21 and 22: a run ofsleep 17stopped at 2 s ended at 17 s; HISTORY.md decision 118). With--new-sessionthat init leads a session, and so a group, of its own, its program in it, in no group of the process that started bubblewrap: ending that process's groups ends bubblewrap's outer process alone (tried 2026-10-23 with bubblewrap 0.11.0, for HISTORY.md decision 124). - A process group outlives its leader, and its number is held while any
process is in it. Linux gives no other process or group a number in use
as a process's, a group's or a session's, and a child that has ended
keeps its number until it is waited for; so a group signalled while a
child not yet waited for is in it is that group, never one that took its
number since. A process's group and session are the 5th and 6th fields
of
/proc/PID/stat, and its start, in clock ticks after the boot, the 22nd, which tells it from one that took its number later (HISTORY.md decision 97), within one boot: Linux names each boot anew in/proc/sys/kernel/random/boot_id, and after one, numbers and starts begin again from the lowest. Within a boot it gives each new process the next number free after the last it gave, up to/proc/sys/kernel/pid_max, 4,194,304 on the server's host, then the lowest free again, so a number freed is given again only once Linux has gone round every number free up to that bound (HISTORY.md decision 124). - A process group stays in its session.
setpgidmoves a process only among the groups of its own session, so a job that a shell's job control (set -m, or any shell at a terminal), Python'sos.setpgrpor GNUtimeout, by its source, puts in a group of its own stays in the session its program leads; onlysetsidleaves it. A session's leader ending hangs up only its terminal's foreground group, so such a job runs on (tried 2026-10-21:set -m; sleep 17 & exit 3at a terminal ended its run at 17 s; HISTORY.md decision 118). The last close of a terminal's near end, as when the process holding it is killed, hangs it up too: the session's leader and its foreground group are sent SIGHUP, which ends a leader that does not catch it, and such a job runs on in its session, which outlives its leader (tried 2026-10-23, for HISTORY.md decision 124). timeoutkeeps a wall on its program alone. BusyBox's, the host's, becomes the program and leaves a watchdog in a session of its own that sends the signal to the program's number; GNU's, by its source, signals its whole group, but only while its child runs, and exits with it. Neither ends what the program left running once it has exited, which may hold its output for ever:sleep 40 & exit 3ended its run only at 40 s (HISTORY.md decision 97).- A rename wakes every inotify reader of its directory. The daemon
writes completion numbers by rename; each watcher opens its own
nonblocking
inotify_init1(IN_CLOEXEC | IN_NONBLOCK)descriptor and watchesIN_MOVED_TOoncompleted/, matching the clone's server-code. Crystal yields its fiber while the descriptor waits; closing it releases the reader (completion specs).