Where 66 stands among init systems¶
An honest placement of 66 next to runit, the s6 stack, OpenRC, finit, dinit and systemd: what it is alone in doing, what it deliberately does not do, and what the remaining gaps would actually cost to close.
The grid that matters¶
Most init comparisons are shouting matches about size. Two axes are enough to place everyone without arguing about taste.
Axis A, the supervision model. One supervisor process per service (PID 1 stays tiny, supervision survives a crash of the manager) versus one monolithic manager that watches every service itself.
Axis B, the richness of the service model. Raw shell scripts, versus a complete declarative model, versus a full system layer that also owns login, network, DNS, logging and an IPC bus.
Crossing them places everyone. Axis B runs down the rows, widest scope at the top and poorest model at the bottom; axis A splits the columns.
| Axis B ↓ / Axis A → | one supervisor per service | one monolithic manager |
|---|---|---|
| System platform: also owns login, network, DNS, logging, an IPC bus | empty | systemd |
| Complete declarative model: dependencies, instances, groups as objects, readiness, user scope | 66 | dinit · finit |
| Declarative, narrower: a dependency graph over a script-shaped model | s6 stack | — |
| Scripts: no native model | runit | OpenRC (no native supervision at all) |
Two things fall out of it.
66 holds a cell nobody else occupies. Everything else in its column, runit and the s6 stack, has the supervision model but a much poorer service model. Everything else on its row, dinit and finit, has a comparable service model but a manager that supervises every service itself, so a bug in the manager takes the whole supervision tree down with it. Nobody else has both.
The row above is a different job, not a higher grade. systemd is one row up because it owns login, the network, name resolution and an IPC bus: scope, not refinement. Reading that row as "more advanced" is what turns the comparison into a shouting match. And the top-left cell is empty. Nobody has built a system platform on a one-supervisor-per-service architecture, and 66 is not trying to; see Placement.
What it actually looks like¶
Before the argument, the object. Everything below is the real syntax; a reader who has never seen 66 can judge the rest against it.
A service is one file. Here is a supervised daemon that also re-reads its configuration
whenever your configuration management tool rewrites /etc/ssh/sshd_config:
[Main]
Type = classic
Description = "OpenSSH daemon"
Depends = ( network )
[Start]
Execute = ( /usr/bin/sshd -D )
[Event]
EventType = inotify
From = ( sshd-config-watch )
Do = reload
Everything about sshd is in the file called sshd, including how it reacts. There is no companion unit beside it.
The [Event] block names a source, and a source is itself a service, of a type that runs
no process and only emits:
[Main]
Type = event
Description = "watch /etc/ssh/sshd_config"
EventType = inotify
Watch = /etc/ssh/sshd_config
On = ( IN_CLOSE_WRITE )
Execute is an execline script by default, but a shebang on its first line switches
language: the same daemon in bash or Python is the same file with
Execute = (#!/usr/bin/bash …). Bring it up with 66 start sshd; there is no separate
parse step, start does it. A logger is attached by default, so 66 status sshd reports
the pid, the uptime, the tree it landed in, and the last lines it printed.
That is the shape of the whole system: one INI file per service, groups of services as trees, and a service's reaction written inside the service it concerns.
Who this is for¶
The answer differs depending on which chair you are sitting in, and conflating the two is how these comparisons usually go wrong.
As a user of a distribution that ships 66, there is nothing to take on. Services are installed by packages, they start at boot, and the init system is as invisible as any other. Nothing above is homework: a frontend with no tree named lands in the default tree on its own. You write a file only when you want a service nobody packaged, and then you write the ten lines shown above, once.
Good fit if you administer machines you actually reason about: a workstation you tune, a fleet you want reproducible, a container image you want deterministic. Especially if you have ever wanted to know why a machine came up the way it did and found that the only answer available was to run an analyser after the fact. The two things 66 gives you that nothing else here does are a graph frozen before boot and an ecosystem you can move between machines as one artefact.
Good fit if you build a distribution or an appliance and want the boot sequence to be a reviewable object rather than an emergent one.
On packaging, the honest position is narrower than it is usually stated. Upstream
projects do not ship 66 frontends, but they do not ship runit, OpenRC, dinit or s6-rc
descriptions either; upstream ships systemd units and nothing else, which is a property of
systemd's reach rather than of anyone's model. What matters is whether a catalogue exists,
and one does. 66-service carries about seventy-five
ready frontends (sshd, nginx, postgresql, dockerd, NetworkManager, wpa_supplicant,
pipewire, bluetooth, cups, four display managers, libvirtd, samba and so on) written to be
distribution-agnostic rather than Obarun-specific, alongside the boot@ module that has
been proven on several distributions.
Adoption is not Obarun-only either: antiX 26, built on Debian trixie, ships 66 as one of five selectable init systems. The residual cost is real but bounded. What the catalogue does not cover, you write, and that is the file shown above.
Bad fit if you depend on the systemd platform rather than on systemd the service
manager: logind integration over D-Bus, socket activation, systemd-networkd, resolved,
or software that links libsystemd and expects the rest to be there. Parts of that have
answers in the 66 suite and parts do not; pretending otherwise wastes your week.
Feature comparison¶
Native capability, not what can be bolted on with glue. The s6 stack column covers
s6 + s6-rc + s6-linux-init, driven by the s6 command of s6-frontend 0.1.0.0 — the
frontend is a CLI over that stack, so the capabilities are the stack's.
| runit | s6 stack | OpenRC | finit | dinit | systemd | 66 | |
|---|---|---|---|---|---|---|---|
| Runs as PID 1 | yes | yes (s6-svscan, via s6-linux-init) |
no (service manager) | yes | yes | yes | yes |
| One supervisor per service | ✔ | ✔ | ✖ (delegates) | ✖ | ✖ | ✖ (cgroup-tracked) | ✔ |
| Dependencies | none | DAG (services only) | depend() in shell |
conditions | typed DAG | DAG (units only) | two DAGs — services (+ Conflict / Provide / OptsDepends) and trees |
| Service description | scripts | source directories, compiled into one database for all services | shell | INI-like | key=value | INI units | INI frontend compiled to one CDB per service (+ addons) |
| Graph computed | n/a (no dependencies) | at compile time | at boot (shell) | at boot | at load time | at boot, after generators run | at parse time, frozen in CDB |
| Readiness protocol | ✖ (./check polls, no notification) |
✔ (fd) | partial | ✔ | ✔ (fd) | ✔ (sd_notify) |
✔ (Notify) |
| Templated instances | ✖ | ✔ — s6 dynamic instances: created at run time in a nested supervision tree, outside the compiled database | ✖ | ✖ | ✖ | ✔ | ✔ (@) — materialised at parse time as ordinary services, inside the graph |
| Reified service groups | ✖ | bundles (aliases) + offline sets | runlevels | runlevels | ✖ | targets (ordering only) | trees, with dependencies of their own + encapsulated modules |
| First-class user scope | ✖ | ✔ (s6 -u) |
✖ | ✖ | ✔ | ✔ | ✔ |
| Logging | per-service svlogd | per-service s6-log | syslog | syslog | file / buffer | central binary journal | per-service chained logger + 66 log |
| Process containment | ✖ | ✖ (planned package) | ✖ | cgroups | ✖ | cgroups | PID namespace (66-ns) |
| Resource control | ✖ | ✖ (planned package) | cgroups | cgroups (v2) | ✖ | cgroups | ✖ (rlimits only — on the roadmap) |
| Socket activation | ✖ | ✖ | ✖ | inetd | ✖ | ✔ | ✖ |
| D-Bus activation | ✖ | ✖ | ✖ | ✖ | ✖ | ✔ | 66-dbus-launch — bus service files translated to frontends, activation request → 66 start |
| Sandboxing | ✖ | ✖ (planned package) | ✖ | limited | limited | namespaces + seccomp | caps, rlimits, no-new-privs + namespaces (66-ns) |
| Seats & sessions | external | external | external | external | external | logind (D-Bus) | 66-userd + seatd, no D-Bus |
| Events / timers | ✖ | ✖ (ftrig is a primitive, not a rule engine) | ✖ | conditions | ✖ | timer / path units |
[Event]: service, signal, inotify, schedule, user, timer |
| Definition layering | ✖ | stores — list read from s6.conf, + masking |
✖ | ✖ | ✖ | unit directory search path + mask (unloadable) |
admin > packaged, first match wins (three levels in user scope); paths fixed at build, explicit per-command override |
| Snapshot & restore | ✖ | offline sets — a selection, not the definitions | ✖ | ✖ | ✖ | ✖ | the whole ecosystem, transferable and exactly restorable |
| Typical adoption | Void, Artix | Artix, Adélie | Gentoo, Alpine | embedded | Chimera, Artix | nearly everything | Obarun; antiX 26 (one of five selectable inits) |
What 66 is alone in doing¶
Trees, and a second dependency graph over them. The service space is partitioned into
named groups, enabled, disabled, started and stopped as a block, several of them live at
once. The part that is easy to miss is that a tree is not merely ordered against its
neighbours: it carries depends and requiredby of its own, resolved through its own
DAG, by the same machinery that resolves services, with the same resolved fields on disk
and the same two-directional traversal. 66 runs two dependency graphs at two levels:
services inside a tree, and trees among themselves.
The two graphs are not peers. The service graph is global and authoritative: it crosses tree boundaries and always wins. If a service in one tree depends on a service held by another tree that is scheduled to come up later, the dependency is still started first and the tree ordering yields. Trees arrange blocks; they never reorder a dependency. That answers the obvious objection to reified groups, which is that putting services into ordered containers should let an administrator write a grouping that contradicts the dependency graph and then either deadlock or start something too late. The contradiction simply resolves: the coarse graph never overrides the fine one, so a tree layout can be inconvenient but not incorrect.
That is what makes the group a container rather than a label, and it is where the usual comparisons miss the point. systemd targets do have dependencies, but a target is not a container, so those dependencies are simply more unit-level edges in the one flat graph. There is no graph over the groups because there are no groups, only units that happen to pull others in. Runlevels, at the other extreme, are ordered by integer and cannot express anything else.
And s6-frontend's sets, which look closest of anything in this comparison, are a different object: a set holds every service the stores know about, each carrying a boot disposition (masked, usable, active, essential); it is edited offline, checked for consistency, compiled, then installed, and exactly one is live at a time. A set answers "what should this machine run at boot". A tree answers "which live block does this service belong to". Both deserve their name; neither substitutes for the other.
Boot and runtime are separate objects, not separate conventions. Bringing the machine
up and running services on it are different jobs, and in 66 they live in different trees.
The shipped seeds say so outright: contributions/seed/boot declares groups = boot,
contributions/seed/session declares groups = admin and depends = global. The
partition is the default the project ships, not a discipline each administrator has to
invent. Every operation that could cascade (stop, free, disable a block) names a tree, and
a tree does not enumerate what is not in it.
Neither of the two comparable systems partitions anything, and both need a fence instead.
s6-rc compiles one database holding every service, boot infrastructure included, in one
flat graph, and s6-rc -d propagates upward through reverse dependencies. The fence is
flag-essential, a file dropped in a service definition directory: with it present,
"s6-rc will refuse to stop that service". Only -D overrides, and their own documentation
says that option "should only be used when the machine is going to be stopped". That is a
per-service veto compensating for the absence of a boundary. The graph cannot express
"this belongs to the machine's foundations", so each foundation service has to carry a flag
saying "not me".
systemd is flat too. Its nearest equivalent is DefaultDependencies=, true by default
(unit.c:108), which silently injects After= / Requires=sysinit.target and Before= /
Conflicts=shutdown.target into every service (service.c:764,785); early-boot units opt
out with DefaultDependencies=no. So the boot/runtime distinction exists, but as a
per-unit opt-out from edges the manager adds behind the author's back. That is the same
objection as the generators in a different guise: the graph is not what the unit files say.
The cost on the 66 side is that a partition has to be assigned. Somebody, the distribution's seeds or the administrator, decides which tree a service belongs to, and a wrong assignment is a real mistake to make. A flat graph never asks the question. It also never lets you answer it.
Modules. A group of services handled as one encapsulated unit, with instance substitution inside it. Everywhere else a group is a label on a set of independent units; in 66 it is an object with a boundary.
Live version migration. The manager can be upgraded and its own persistent state converted in place, under a running system: 0.8.x resolve files become 0.9.0.0 ones without a reboot. No other project here attempts that.
Snapshots, which are not merely named configurations. A snapshot carries the ecosystem itself: the frontend definitions at all three levels (system, administrator, user), their configuration and environment files, the scripts, the seeds that describe trees, and the resolved system state. One artefact, the whole thing. That buys two properties a prescription structurally cannot have:
- Transfer. Dropped on another machine, a snapshot reproduces the same ecosystem there, with the guarantee that services run against strictly the same definitions rather than against whatever the local packages happen to provide. Reproducibility across a fleet is a property of the artefact, not of the discipline of whoever populated the machines.
- Exact restore. Coming back means coming back to a working state, byte for byte, definitions and configuration and environment included, not to a selection that will resolve against whatever is installed today.
Which is where a set, described above, differs in kind rather than in degree: it is a
selection over definitions that live outside it. Copy one to another machine and it means
something only if that machine's stores already hold the same definitions; the set does not
carry them, and cannot guarantee them. A snapshot is the definitions. One is a choice
about an ecosystem, the other is the ecosystem. s6-rc-update, similarly, swaps a live
service database while services keep running, which is a different operation again.
Event reactors. One [Event] block, written inside the service that cares about it.
EventType picks the source (service, signal, inotify, schedule, user, timer),
On and OnAll give the condition, and Do gives the action: start, stop, restart,
reload, reconfigure or free. A separate Emit key lets a rule publish an event of its
own, so reactors chain. It is fed by internal milestones (boot.done, shutdown.begin) as
much as by 66 emit from userland.
systemd covers part of that ground with two extra unit types, path and timer, and the
gap is wider than the missing sources. A service there cannot react on itself. A path or
timer is a separate unit, and the only action it can take is to activate the unit it is
paired with. It cannot restart, reload or reconfigure it, and there is no way to write,
inside a service, a rule about that service. In 66 the rule lives in the service it
concerns and acts on it: Do = reload on a SIGHUP is the service responding to itself,
in its own frontend file.
A PID 1 that does almost nothing, which is not 66's alone. It is shared with runit and above all with the s6 stack, from which the model is inherited and which the table credits with it too. Kept through the native rewrite: the scanner spawns one supervisor per service, and the supervisors are what keep services alive. The CLI, the graph resolver and the parser are ordinary programs that can crash without taking supervision down.
Compiled ahead of time, not parsed at boot¶
This is a property of the pipeline rather than a feature, and it decides what the other guarantees are worth.
In 66 the parser runs when the administrator says so, at 66 parse or at 66 enable, and
its output is a CDB: a static, compiled description of the service. Boot does not parse, it
reads compiled artefacts.
That guarantee comes from an ordering property, and it should be stated exactly rather than
too broadly. 66 start can parse: hand it a service that has never been parsed and it
will do the work on the spot, which is the right behaviour on the interactive path. It
never happens at boot, because a service only comes up at boot if it was enabled, and
enabling is what runs the parse. So the boot path cannot reach a service whose CDB does not
already exist. The parser is not absent from the binary; it is unreachable from the path
where reproducibility matters.
Three things follow, and they are consequences rather than promises:
- A given start is always the same start. Not "should be", not "as long as nothing drifted". There is no code path in which a frontend file is re-interpreted while the machine is coming up, so there is nothing to drift.
- The dependency graph is frozen at parse time. It is a thing on disk that can be inspected, diffed and shipped, not a computation whose result depends on when you ran it.
- It is what makes a snapshot mean anything. A snapshot carries the compiled state, so "the same ecosystem on another machine" means the same resolved graph, not the same inputs re-resolved locally and hoped to agree.
systemd is built the other way round, by construction rather than by oversight. Units are
loaded from a search path at runtime, and before they are loaded, generators run, at
every boot and at every daemon-reload. They are ordinary executables that synthesise,
alter and mask units. Eleven ship in the tree, and what they read is machine state:
debug-generator takes systemd.mask= and systemd.wants= straight off the kernel
command line (debug-generator.c:42,55), gpt-auto-generator probes the partition table
with blkid, fstab-generator reads /etc/fstab. Third-party generators are supported and
common.
So the unit set, and therefore the graph, is a function of the machine's state at the
moment of boot, recomputed every time. Two boots of a machine nobody touched can differ
because the kernel command line, a partition table or a dropped-in .conf differed. There
is no compiled artefact to review; systemd-analyze dump reports what the graph became,
after the fact. That is the difference between a guarantee and an observation.
s6-rc compiles too: s6-rc-compile produces a service database, and s6 set commit
compiles a set into one. Compile-ahead is a design school with two members here rather than
a 66 exclusive, which makes it a stronger point and not a weaker one. But the granularity is
opposite, and it decides everything downstream. s6-rc compiles one database for all
services. 66 compiles one CDB per service, finer still, with separate addons per concern
beside it: .execute, .dependencies, .environ, .io, .limit, .regex.
The consequences all run the same way:
- Unit of change. Touching one service in 66 rewrites that service's artefact. In s6-rc it means recompiling the whole database and swapping the live one.
- Blast radius. One malformed definition fails one parse and leaves everything else intact. A monolithic compile either succeeds whole or yields nothing.
- Ceremony. This is why the s6 stack needs sets,
commit,installandapplyat all; eights6-rc-set-*binaries exist to manage those selections. When the artefact is the entire machine, changing one line requires an offline staging area and an install step. When the artefact is the service,66 enable foois just enabling foo. - Composability. A per-service collection of CDBs can be added to and taken from, which is what lets a snapshot be an ecosystem rather than a blob.
The other side has something for it. Compiling the whole database buys whole-system consistency by construction: the live database cannot contain a dangling dependency, because it was validated as a unit. 66 has to obtain that from graph resolution instead. That is where the cost of per-service granularity is paid.
The division of labour¶
Several things systemd absorbed into PID 1 exist in the 66 world as separate programs of the suite, and reading the core alone makes them look missing:
- Confinement is
66-ns: mount, PID, user, network, UTS, IPC and cgroup namespaces, declared as typed elements or rule files and composed through the service'sExecuteline. - Sessions are
66-userd: a PAM module reports login and logout, the daemon tracks who is logged in and from where, owns each user's runtime directory, applies the shutdown policy, and brings that user's 66 services up on the first session and down on the last, without D-Bus. - Seats are
seatd: the hardware context, devices and VTs. - D-Bus activation is
66-dbus-launch: it launches and supervisesdbus-brokeras its controller, translates every D-Bus service file it finds into a 66 frontend placed in thedbustree, and turns an activation request from the bus into a plain66 start. ASIGHUP, or66 reload dbus, resynchronises: new bus files are translated and activated, removed ones are66 removed and deactivated, changed ones re-parsed. It handlesUpdateActivationEnvironment()andReloadConfig(), and runs per-user as readily as for root, dropping privileges before exec'ing the broker.
So the answer to "where is logind" is: split between 66-userd and seatd, along the
boundary of what each piece actually owns, and the same reflex accounts for the other two
entries above. That is the supervision model applied to the suite itself: no single process
has to be trusted with all of it.
The activation case repays a second look, because it shows the reflex paying off. There is
no activation unit type in 66 and no special case in the core: a bus-activated service
becomes an ordinary frontend, parsed to CDB like any other, started by the ordinary start
path, supervised by the ordinary supervisor. The translation step is admittedly a cousin of
a systemd generator, since it derives service definitions from foreign files, but it runs at
a moment the administrator chooses, and what it emits is readable frontend files on disk
rather than units materialised in a manager's memory. Two limitations come from its own
documentation: dbus-broker's policy API is not yet stable, so access is broad, and D-Bus's
own system.conf / session.conf are not interpreted.
Readiness and events: who is allowed to speak¶
The feature table gives 66 and systemd the same tick for "readiness protocol". The tick
hides the most instructive disagreement in the whole comparison. Both projects answer the
same question, when is a daemon actually ready?, and the difference is not in the answer
but in which direction the information travels. 66 is not alone on its side: the descriptor
protocol is s6's design, which 66 kept through the rewrite, and dinit arrived at the same
shape independently. Both are credited ✔ (fd) in the same table. systemd is the outlier
here, not 66.
The readiness contract¶
Both agree that exec succeeding proves nothing and that Type=forking is a poor proxy.
The daemon has to say so itself.
- 66 passes a pre-opened descriptor to the service (
Notify). The daemon writes one\non it. Its supervisor reads it and the service is up. - systemd creates an
AF_UNIXdatagram socket, exports its path asNOTIFY_SOCKET, and the daemon sendsREADY=1to it. The same socket also carriesSTATUS=,MAINPID=,RELOADING=1,STOPPING=1,WATCHDOG=1,EXTEND_TIMEOUT_USEC=,FDSTORE=1withSCM_RIGHTS,ERRNO=andBARRIER=1.
The socket buys one real thing an inherited descriptor cannot: ambient reachability. An
environment variable crosses fork and exec for free and survives the classic
closefrom(3) in a daemonizing routine, which silently destroys a notification
descriptor. Any descendant, at any depth, can still notify. That is a genuine advantage
and it should be conceded plainly.
Everything else systemd carries on that channel is the invoice for it:
| Because | It needs |
|---|---|
| anyone can write to the socket | peer authentication: SO_PEERCRED, NotifyAccess= |
| the sender may not be the daemon | MAINPID=, which lets a service redefine which process the manager tracks |
| the sender may exit before the manager reads | BARRIER=1 plus a descriptor passed by SCM_RIGHTS |
And none of it closes the hole. The man page for systemd-notify(1) ends up recommending
that you call sd_notify() in-process instead, which means giving up ambient reachability,
the only thing the socket was selling. The design refutes itself in its own documentation.
The clearest evidence is external to 66. s6 ships two bridges between the two protocols,
and their asymmetry is the whole argument. s6-notify-fd-from-socket converts systemd to
fd: it listens for one thing, READY=1, discards every other key, and writes a single
byte. s6-notify-socket-from-fd converts fd to systemd: it must synthesise a MAINPID=
the source protocol never needed to name, then add a BARRIER=1 round trip to compensate
for an authentication problem the source protocol never had. One direction is a filter that
throws away. The other is a filter that manufactures. The poorer protocol is the semantic
superset.
One more property is rarely mentioned: a descriptor gives EOF for free. If the daemon dies
during initialisation, the read end sees EOF immediately, so "not ready yet" and "dead" are
distinguishable with no timeout and no knowledge of any pid. On a stateless datagram socket,
silence means nothing, so the manager needs a second, out-of-band source of truth: SIGCHLD
on the main pid. Which requires knowing the main pid. Which is why MAINPID= exists.
systemd only escapes the circle because it is PID 1 and reaps every orphan.
The event bus is a separate channel¶
Where systemd runs every one of those semantics down a single pipe, 66 keeps two
mechanisms apart. Readiness is the descriptor, and nothing else. Events are 66-eventd,
which never participates in readiness: a service does not become ready by emitting. The
bus reports what happened, not what a process claims to be.
That separation is enforced by who is allowed to produce. Lifecycle events are emitted by
the supervisor (66-supervise.c lines 339, 1021, 1290 and 1344) from what it knows
first-hand: its own waitpid, the notification descriptor it holds, the signal it sent.
The supervised service has no way to speak about itself on the bus.
This inverts the systemd flow exactly. There, the process declares its state and the
manager decides whether to believe it. Here the state is observed by the one party that
holds it. That is why there is no equivalent of MAINPID=: the supervisor knows the pid,
nobody needs to tell it, and crucially nobody is permitted to redefine it. Across the
entire lifecycle path there is simply nothing to authenticate, because the question never
arises.
Transport chosen per problem¶
66 does use a socket, eventd/s, for the one job a descriptor structurally cannot do:
accepting events from arbitrary clients not known at start-up (66 emit). N unknown
producers require a named rendezvous. But the transport is a connected stream, and the
check happens at accept time (66-eventd.c lines 1295–1314): socketunix_getucred on a
live peer, rejection unless cred.uid matches the daemon's owner. No race against a
sender that has already exited, no recycled pid, and no barrier to invent. The stream is
the barrier.
| Need | Producers | Transport | Authentication |
|---|---|---|---|
| readiness | one, known, direct child | inherited fd | none needed, structural |
| lifecycle → bus | one, the supervisor | fifo directory | none needed, the producer is the trusted party |
| external injection | N, unknown | connected stream | at accept, on a live peer |
systemd has one row for all three needs. Everything else follows from that.
The sharpest consequence is where each channel sits relative to a privilege boundary.
66's socket is same-uid to same-uid: a process able to reach it can already run
66 start, so 66 emit grants no authority the CLI did not already grant, and a coarse
uid check is exactly the right granularity. systemd's channel runs service to PID 1, and
the writing process is often one the unit deliberately weakened with dropped privileges, a
sandbox, DynamicUser= or a namespace, yet it retains a path to the most privileged
process on the machine, with MAINPID= and FDSTORE=1 on it. The channel crosses the very
boundary the unit just erected.
Two caveats. First, 66-eventd does more than sd_notify, not less: a reactor turns an
event into an action, which is a far larger surface than a readiness protocol ever had. The
claim here is not "simpler" but "the transport was chosen per problem instead of once for
all of them". Second, neither model helps a daemon that has no notion of readiness at all
and accepts no descriptor; that case needs external probing on both sides, s6-notifyoncheck
in one world and a blocking ExecStartPost in the other. The merit of the descriptor is in
the contract, not in the coverage.
What 66 does not do¶
- No cgroup resource control yet. There is no memory ceiling, CPU weight, task cap
or per-service accounting; rlimits are per-process, and
RLIMIT_NPROCcounts per user, not per service. This is a control gap, and it is on the roadmap. Why it is not also a tracking gap, and what closing it costs, is worked out in What cgroups and seccomp would cost 66. - No socket activation. The fd holder keeps descriptors across restarts; that is not lazy activation. D-Bus activation, by contrast, is covered; see the division of labour below.
- No seccomp, in the core or in
66-ns. Hardening stops at capabilities, rlimits,no_new_privsand namespaces. - Linux only. Not by architecture, but for one missing event-loop backend; see Portability below.
- Ecosystem reach. No upstream project ships a 66 frontend, as none ships runit,
OpenRC, dinit or s6-rc descriptions either, so the catalogue is maintained downstream.
66-servicecovers roughly seventy-five of the common ones; beyond that, the distribution writes them. - Maintenance surface. lib66 is around 256
.cfiles, and the native rewrite of the supervision chain (scanner, supervisor, logger, svctl) adds surface that systemd amortises over hundreds of contributors. That is a project risk, not a technical one, and it should be stated plainly.
Portability¶
66 runs on Linux only, and the reason is not the one people assume. 66 is not architecturally tied to Linux; it is tied to one event loop that has only ever had a Linux backend.
66's own non-portable surface is tiny. The complete inventory of OS-specific headers
under src/:
| Header | Uses | Where | Half |
|---|---|---|---|
linux/capability.h |
4 | lib66/linux/caps.c, enum_parser.c, headers |
manager |
sys/prctl.h |
2 | 66-execute.c (no-new-privs), caps.c |
manager |
sys/mount.h |
2 | ssexec_boot.c, shutdown/umountall.c |
init |
sys/reboot.h |
2 | 66-hpr, shutdown |
init |
linux/kd.h |
1 | ssexec_boot.c (console / VT) |
init |
sys/inotify.h |
1 | 66-eventd's inotify source | manager |
sys/signalfd.h |
1 | 66-log.c |
manager |
Plus three /proc paths: cmdline and mounts, both in the init half (kenv and
getmntinfo on FreeBSD), and /proc/self/fd, which exists as /dev/fd everywhere.
Notably, epoll_create, timerfd_create and eventfd appear nowhere in 66. Not one
direct call.
The actual blocker is one layer down. The whole runtime (66-scandir, 66-supervise,
66-log, 66-eventd, 66-fdholderd, svc_launch, tree_launch) runs on oblibs' SSE loop,
which is built on the five Linux mechanisms: epoll, signalfd, timerfd, eventfd and
inotify. That is what pins 66 to Linux, and it lives in oblibs, not here.
The shape of that debt is favourable. oblibs' event/ directory is 12 files and about
2300 lines, and all seven epoll_* calls in the project sit in a single file, sse.c.
The demultiplexer is already behind an abstraction; the watchers above it are fd sources.
And this is the direction in which kqueue is simpler than epoll, not harder:
EVFILT_SIGNAL, EVFILT_TIMER, EVFILT_USER and EVFILT_VNODE replace the four
Linux fd types natively, with no descriptor at all. Four fd-based watchers become four
filters. epoll is the late, partial imitation of kqueue; porting this way runs with the
grain.
It is also the price of independence. skalibs was the layer that carried these abstractions onto the BSDs; 0.9.0.0 removed it in exchange for control, and portability left with it.
And "portable" means less than it sounds, for everyone. dinit on macOS is not PID 1; it is a session service manager there and nothing more. On FreeBSD, being PID 1 obliges it to reimplement the init role locally, exactly as 66 would. The split is the same on both sides:
- The manager half (parser, DAG, trees, resolve/CDB, modules, instances, logger, snapshots) is plain POSIX. Nothing to port. That is most of lib66.
- The init half (
ssexec_boot,umountall,66-hpr, the console) is platform-specific for everyone, dinit included. That is not portability, it is a per-platform rewrite by nature. - Capabilities are a Linux concept. FreeBSD's Capsicum is a different model, not an
equivalent; macOS has nothing.
CapsBound/CapsAmbientwould be compile-time gated.
| Target | Work | Cost |
|---|---|---|
| kqueue backend in oblibs | one file beside sse.c + four watchers on native filters |
1–2 weeks |
| 66 as a service manager on FreeBSD/macOS | gate capabilities, gate the init half | a few days |
| 66 as a FreeBSD init | ssexec_boot, umountall, hpr natively (getmntinfo, kenv, reboot(2)) |
a project of its own |
So: 66 as a service manager on FreeBSD or macOS is roughly two to three weeks, and most of it happens in oblibs rather than in 66. 66 as a FreeBSD init is a separate undertaking, the same one dinit had to carry out.
Placement¶
66 is the only service manager that offers a systemd-grade declarative model on an s6-grade supervision architecture. Two projects sit closest, for opposite reasons, and neither is systemd.
s6-frontend is the nearest architecturally. The s6 stack now has its own unified s6
command, with the same offline/live split, the same enable/disable vocabulary, a per-user
mode and the same 0/100/111 exit convention, over the same one-supervisor-per-service
model.
The chronology runs the opposite way from what that sentence suggests. 66 has existed
since 2018, and its single unified 66 command dates from 0.8.0.0, tagged October 2024;
s6-frontend's first release is January 2026 and its 0.1.0.0, the one that adds user mode
and apply, is July 2026. So 66 reached that shape about fifteen months earlier. Instances
are the older case and the wider gap: 66 shipped @I in 0.2.1.0, September 2019; s6
implemented instances in 2.11.2.0, January 2023, more than three years later.
Two datapoints are not a law, and neither project is copying the other. Both are solving the same problems from the same supervision model, which is exactly why they converge. But the direction of travel should be stated plainly, because the reading order of this document invites the opposite assumption: where the two have arrived at comparable features, 66 arrived first.
The distance that matters is therefore not in the CLI, and s6-frontend's own overview says so plainly: it "does not come with any innovating concepts — it's just a series of user-friendly wrappers around various commands in the s6 ecosystem". That is an accurate description of a frontend, and it is also the measure. The two projects converged on how a service manager should be driven, and diverge entirely on what it should model.
What separates them is the service model, not the supervision model: s6-rc source directories compiled into one database for the whole machine against an INI frontend compiled per service, bundles that alias a group against modules that encapsulate one, no live partition of the service space, no event engine, no snapshot that carries the definitions. Plus the dependency stance, which now points in opposite directions. 66 0.9.0.0 depends on exactly one library, oblibs, which is Obarun's own and is itself free of skalibs and execline; s6-frontend requires five external packages: skalibs, execline, s6, s6-rc and s6-linux-init. The difference is not "none versus five", it is a single library the project controls against a stack it consumes.
Two things it has that 66 does not, and they are real: whole-database consistency by construction, discussed above, and portability, inherited from skalibs.
Stores are the third candidate, and they do not survive inspection. Layering itself is not the difference. 66 already searches administrator directories before packaged ones, and user directories before both, first match winning; a packaged definition and a local override coexist there exactly as they do in the s6 stack. What differs is two details, and neither points the way it first appears.
The store list is configuration in s6-frontend: /etc/s6.conf carries a storelist key
read at run time. In 66 the paths are fixed at build, and an absolute path handed to
66 parse overrides them for that invocation. By this document's own argument that is the
more consistent choice rather than a lesser one, since a resolution that depends on what a
file said at the moment the command ran is the same class of thing the compiled-ahead
section objects to in generators. 66 keeps the escape hatch explicit and per-command
instead of ambient. The trade is real: no way to reconfigure the search set without
rebuilding.
Instances deserve a separate note, because the two projects mean different things by the
word. s6 has had them since 2.11.2.0 in January 2023, 66 since 0.2.1.0 in September 2019,
and s6's are dynamic: s6-instance-maker turns a template into a service directory that
is itself a nested supervision tree, and s6-instance-create spawns a copy at run time.
Instances come and go while the machine runs, without recompiling anything, which is
precisely the escape hatch a monolithic compiled database needs and the honest counterpart
to the ceremony criticised above. The cost is that individual instances live outside that
database: nothing can declare a dependency on one, because it did not exist when the graph
was compiled. 66's @ is the other choice. An instance is materialised at parse time into
its own CDB and is an ordinary service in the graph, so it can be depended on, put in a tree
and enabled like any other, at the price of existing before it is needed. Neither is a
subset of the other.
Masking is the more interesting one, because it answers different questions on each
side. s6-frontend's own documentation gives its reason: masking is for when "the stores
provide services that come from random installed packages, the user never wants to run
these services, and it's just better not to see them". That is legibility, and it is needed
because the compiled database holds everything the stores hold: a consequence of one
database for all services rather than a capability. systemd's mask does something else and
harder. The unit is symlinked to /dev/null and becomes unloadable, so it cannot be started
even when another unit depends on it. That is a veto against activation the administrator
does not control: socket units, D-Bus activation, a Wants= shipped by a third-party
package, a generator.
66 has neither problem. One CDB per service means there is no crowded database to hide
things from. And activation stays enumerable: there is no socket activation and there are
no generators, while D-Bus activation reaches only the services 66-dbus-launch translated
into the dbus tree, each of which exists as a frontend you can read and a CDB you can
inspect, and each of which stops being activatable when its bus service file goes away.
66 disable and the service does not come up. For the one residual case, a packaged
definition that someone else's dependency drags in and you do not want, the layering
already answers it: shadow the frontend in /etc/66/service, which is explicit and
reviewable where a symlink to /dev/null is neither. The veto is unnecessary because the
set of things that can start a service is small and named, not because nothing can.
dinit is the nearest as a competitor, and the split is clean:
- dinit wins on portability (Linux, FreeBSD, macOS, though see above: the gap is one missing kqueue backend in oblibs, not an architectural tie to Linux), on conceptual simplicity, and on adoption;
- 66 wins on administrative power (trees, modules, instances, snapshots, live migration, events) and on the robustness of the supervision model.
Put another way: runit is a tool, OpenRC is a script scheduler, finit is an embedded init, the s6 stack is a supervision toolkit that has grown a friendly face, dinit is a good general-purpose manager, systemd is a system platform, and 66 is a complete service manager that refuses to become a platform. That is a coherent position, not a compromise.
Which leaves one real question, and it is not "66 versus systemd", nor even "cgroups or not". It is whether a service manager owes its administrator resource ceilings, a memory cap and a CPU weight per service, the way it already owes them rlimits.
66 answers yes, and it is on the roadmap. Not as a change of position on cgroups as a
tracking substrate; that refusal stands, and the PID namespace already covers the job. But
a ceiling is an attribute of a service, in the same family as LimitAS, and a manager
that grants rlimits owes the ceiling too. It costs far less than it looks, because
declining cgroups for tracking is exactly what makes them cheap to adopt for resource
control. The work is priced out in
What cgroups and seccomp would cost 66, along
with seccomp, the other gap regularly raised here.