Commit Graph
100 Commits
Author SHA1 Message Date
Joachim Wiberg b743d15e35 libink: declare signals in introspection XML
All three org.finit signals were invisible to generated proxies, and
org.freedesktop.DBus was missing from the standard interfaces even
though Hello, AddMatch, and RemoveMatch are answered.

Add a link_signal_t table to the vtable, emitted like methods and
properties, declare the Manager1 and Cond1 signals, and complete the
static XML with PropertiesChanged and org.freedesktop.DBus.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:39 +02:00
Joachim Wiberg 9d4cc8e933 service: one owner for user-requested start/stop/restart
The D-Bus methods carried byte-for-byte copies of api.c's static
start/stop/restart helpers.  Promote them to service.c alongside
service_reload(), which already serves both callers, and reduce both
sides to svc_parse_jobstr-style adapters.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:39 +02:00
Joachim Wiberg 3efa9d6f41 test: cover Manager1 edge semantics against the legacy API
Regression tests for the recent handler fixes: bogus SetRunlevel is
InvalidArgs, Signal on a stopped service is Failed, and the reboot
family declares the timeout argument.  Reboot cannot be invoked
without taking down the sandbox, so the latter is asserted via
introspection.  New call-u and call-su modes in dbus-auth-client.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:39 +02:00
Joachim Wiberg 62b55d5b43 dbus: honor the shutdown timeout in the reboot family
initctl -t N reboot arms an emergency bypass timer over the legacy
socket, but the bus methods took no argument, so the timeout was
silently dropped whenever D-Bus was up.

Reboot, Halt, and Poweroff now take a timeout in seconds, 0 for
none, armed via the same shutdown_bypass() the legacy path uses.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:38 +02:00
Joachim Wiberg 8b61aaea1a dbus: report action and condition failures instead of empty success
Service1 Start/Stop/Restart discarded the action return value, their
Manager1 twins map it to org.finit.Error.Failed.  Cond1 Set/Clear
replied success even when the condition symlink operation failed,
where legacy initctl exits 73.  Verify the resulting condition state
with cond_get() rather than the noupdate return values, which report
no-change, not failure, and would reject an idempotent re-set.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:38 +02:00
Joachim Wiberg 2af83ea632 dbus: signalling a stopped service is an error
Manager1.Signal silently skipped stopped services, so the same
command gave different exit codes depending on transport: the legacy
INIT_CMD_SIGNAL path fails when the service is not running.  Mirror
the legacy behavior.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:37 +02:00
Joachim Wiberg b53c7e6879 dbus: mirror legacy runlevel edge cases in Manager1.SetRunlevel
The bus method called sm_runlevel() unconditionally.  In runlevel 0
and 6 that aborts an in-flight shutdown, which INIT_CMD_RUNLVL
refuses with a warning, and during bootstrap it switches immediately
where the legacy path defers via cfglevel to the end of runlevel S.

Port both.  A bad runlevel argument still returns InvalidArgs, where
the legacy protocol acks silently: a typed interface rejects garbage.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:37 +02:00
Joachim Wiberg 8114508bbb dbus: refuse Manager1.Suspend in bootstrap and shutdown
The legacy INIT_CMD_SUSPEND is refused in runlevel S, 0, and 6, the
bus method suspended unconditionally, even mid-shutdown.  Add the
same guard, replying WrongRunlevel like the reboot family.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:36 +02:00
Joachim Wiberg 8138b402a0 test: don't slay a service Finit has given up on
Once it gives up it forks the post:script and reports that PID as the
service's, so a slay still waiting for the service to come back killed
the script instead, and crashing.sh lost the /tmp/post it checks for.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:36 +02:00
Joachim Wiberg 864a13460c test: add keventd device manager test
The unified keventd has no automated coverage, only the devmon
fallback is exercised by the test suite.  Network interfaces are
the one device class an unprivileged test can hotplug: the sandbox
has its own network namespace, so 'ip link add' makes the kernel
emit genuine uevents.

Verify keventd readiness, <class/net/IFNAME> driving a service --
and <dev/IFNAME> NOT asserted, interfaces are not device nodes --
libudev-compatible n<ifindex> keying in /run/udev/data, conditions
surviving initctl reload, and cleanup on interface remove.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:36 +02:00
Joachim Wiberg b3a206a28c devmon: bridge keventd conditions to the service engine
A service with a <class/net/eth0>, or any other keventd-provided,
condition is never started when the device appears.  keventd asserts
the condition file, but devmon only watches /dev, so no cond_update()
ever reaches affected services.  Reload made it worse:
devmon_reconf() clears any registered dev/ condition without a /dev
node behind it.

Watch the dev/, class/, and driver/ condition directories, like the
sys and usr plugins do for their namespaces, and treat an existing
condition file as device presence in devmon_reconf().

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:35 +02:00
Joachim Wiberg 81cb90804e test: include bundled helpers' libraries in the sysroot
keventd links libblkid, which finit itself does not, but sysroot.mk
only copied the libraries finit links.  Inside the sysroot keventd
then fails to start:

    Service keventd[18] died (exit status: 127)

Collect libraries from finit and everything installed under
libexec/finit/ instead.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:35 +02:00
Joachim Wiberg 5a3972714a keventd: defer pidfile until coldplug queue is drained
When started with -c (the default when keventd is the device manager),
gate the pidfile on the kernel's uevent_seqnum having been stable for
200ms.  Up to now ready signaling with the pidfile was done right after
coldplug() triggered the kernel to re-emit events, but before uev_run()
had drained any of them, so <pid/keventd> really only meant "listening
on netlink".

With the gate, services that depend on <pid/keventd> can now assume /dev
is populated and persistent symlinks are live.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:34 +02:00
Joachim Wiberg c1c68054ea keventd: add -S settle mode polling /sys/kernel/uevent_seqnum
Stop-gap "settle" equivalent of udevadm settle for migration scenarios.
Polls /sys/kernel/uevent_seqnum every 50ms and exits zero when the
sequence number has been stable for 200ms (or non-zero after -t SECONDS
timeout, default 30s).

This is racy by design -- a slow probe firing after we return still
races -- so the doc steers users toward dev/, class/, and bind/
conditions for any service they control.  Settle is for legacy boot
scripts and init transitions where condition wiring isn't feasible.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:34 +02:00
Joachim Wiberg f3461ce2ba keventd: add class/<subsystem>/<name> and driver/<name> conditions
dev/<X> only fires when a device gets a /dev node, which leaves a lot of
embedded-relevant hardware uncoverable: DSA switch cores, IIO sensors,
LEDs, backlight, PHYs, regulators -- all live purely under sysfs.

Two new condition namespaces:

  class/<subsystem>/<sysname>  asserted on every sysfs class device add
                                (e.g. <class/leds/blue>)
  driver/<name>                 asserted while the driver is bound to at
                                least one device (e.g. <driver/mt7530>)

A driver can bind to several devices, so driver/ conditions are
refcounted: asserted on first bind, cleared when the last device is
unbound.

dev_cond() is generalized into a static cond_emit(prefix, rel, set) so
class_cond() and driver_cond() share the same mkpath + symlink/erase
code.  The name avoids colliding with src/cond.h's public cond_path()
helper (unrelated function that returns a condition's filesystem path).

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:34 +02:00
Joachim Wiberg e4d9193f06 keventd: skip futile fork for missing /lib/udev/<helper>
When a rule references an absolute helper path (e.g. /lib/udev/fido_id,
/lib/udev/scsi_id) that the system does not ship and we have no matching
builtin either, return 1 from try_builtin_fallback() so the caller does
not fork /bin/sh on a binary that's known to be missing.  Previously
each such uevent left a zombie 127 child for keventd's sigchld_cb to
reap -- harmless but noisy and wasteful at coldplug.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:33 +02:00
Joachim Wiberg 9d5e646e46 keventd: drop SIGCHLD save/restore dance, libuev reaps for us
modprobe_load() and run_program() used to temporarily restore the
default SIGCHLD handler around fork+waitpid because the main loop set
SIGCHLD=SIG_IGN.  With the libuev conversion, sigchld_cb in keventd.c
handles reaping via signalfd -- and signalfd does not interfere with
synchronous waitpid(pid, ...) -- so the dance is dead code.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:33 +02:00
Joachim Wiberg 0111dc2178 keventd: convert main loop to libuev
Replace the raw poll() + global running/reload_rules flag dance with
libuev.  All four signals (SIGUSR1, SIGTERM, SIGHUP, SIGCHLD) are now
handled via uev_signal_init() -- libuev uses signalfd internally so the
handlers run in normal main-loop context, not async signal context.

The SIGCHLD handler waitpid(-1, WNOHANG) reaps any subprocess (modprobe
loads, rule RUN+= helpers) instead of the old SIG_IGN auto-reap trick.
Synchronous waitpid() in modprobe_load() and run_program() still works
because signalfd queues the signal -- it only reaches sigchld_cb on the
next main-loop iteration.

The netlink receive loop becomes uevent_cb() registered via uev_io_init(),
with the receive buffer passed via the watcher's arg slot (avoids
file-static state).  ENOBUFS and EINTR/EAGAIN are still tolerated, any
other recv() error still panic()s.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:32 +02:00
Joachim Wiberg 153a1de043 keventd: record the libblkid build requirement
keventd is the only thing in the tree that links libblkid, so a tree
that used to build now stops in configure with no hint of which package
to install.  Say so where people look for dependencies, and give CI the
package it now needs.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:32 +02:00
Joachim Wiberg 4e914dd8a9 keventd: ship curated udev rules derived from eudev 3.2.14
Add 27 stock rules installed to /lib/udev/rules.d/.  Rules invoking
/lib/udev/<helper> fall back to keventd builtins (path_id, usb_id,
blkid, hwdb, kmod, net_id, input_id) when the helper binary is absent;
user-supplied helpers in /lib/udev/ still take precedence.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:31 +02:00
Joachim Wiberg 67d61a8282 keventd: add udev rules engine
Transform keventd from a power-supply monitor + basic hotplug handler into
a full udev-compatible device manager.

Rules engine (rules.c):
- Full .rules file parser covering all udev key types: ACTION, KERNEL,
  SUBSYSTEM, DEVPATH, ENV, ATTR, SYSCTL, TAG, RESULT, PROGRAM, TEST,
  parent-chain KERNELS/SUBSYSTEMS/ATTRS/DRIVERS, and more
- Pattern matching: plain string, fnmatch glob, and pipe-separated alternatives
- Operators: ==, !=, =, +=, -=, :=
- Assignments: NAME=, MODE=, OWNER=, GROUP=, SYMLINK+=, ENV{k}=, TAG+=, RUN+=
- IMPORT{program|file|builtin|parent|cmdline|db}=
- PROGRAM= with stdout capture for subsequent RESULT== matching
- GOTO=/LABEL= flow control
- Loads *.rules from /lib/udev/rules.d, /run/udev/rules.d, /etc/udev/rules.d
  and an optional extra directory (-r DIR); reloads on SIGHUP

Builtin framework (builtin.c):
- kmod:     load module by MODALIAS or explicit alias
- hwdb:     match device against *.hwdb text files in udev hwdb dirs; builds
	    correct lookup key per subsystem — evdev:input:b*v*p*e* for input,
	    usb:v*p* for USB, raw modalias for PCI/platform
- path_id:  build stable ID_PATH / ID_PATH_TAG from sysfs topology (PCI, USB,
	    ATA, NVMe, platform, ACPI, virtio)
- usb_id:   read idVendor/idProduct/bcdDevice/serial from sysfs; look up
	    ID_VENDOR_FROM_DATABASE and ID_MODEL_FROM_DATABASE from usb.ids
	    (hwdata package) when available; silent fallback when absent
- input_id: classify input devices (keyboard, mouse, joystick, touchscreen,
	    touchpad) from evdev capability bitmasks in sysfs
- net_id:   generate predictable names — MAC-based enx<mac> and PCI-slot-based
	    enp<bus>s<dev>[f<func>]
- blkid:    probe filesystem type, UUID, and label via libblkid; sets ID_FS_*
	    and ID_PART_TABLE_* properties

Network interface renaming (uevent.c):
- netdev_add() renames interfaces via SIOCSIFNAME when a NAME= rule matched,
  then sets the Finit dev/ condition on the final name; and any setup using
  persistent interface naming via udev rules

Device node and symlink improvements (uevent.c):
- NAME=, MODE=, OWNER=, GROUP= overrides from matched rules applied at
  mknod/chown time, falling back to the built-in permission table
- SYMLINK+= links from rules applied alongside built-in by-id/by-path links

Device property database (udevdb.c):
- Persist per-device E:/S:/I: records to /run/udev/data/ on ADD/CHANGE,
  delete on REMOVE; IMPORT{db}= restores saved properties into event env

Build:
- libblkid (util-linux) is now required for keventd

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:31 +02:00
Joachim Wiberg dda0d5c763 Relocate keventd from src/ to keventd/
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:31 +02:00
Joachim Wiberg 48f677c3a0 keventd: signal readiness via pidfile after coldplug
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:30 +02:00
Joachim Wiberg db5567b5d1 keventd: add passive mode (-p) for coexistence with hotplug plugin
This is a backwards compatible mode for users upgrading and not noticing
that keventd is now build by default.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 22:03:30 +02:00
Joachim Wiberg 8e91bb4297 keventd: disable legacy kernel uevent helper at startup
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:28 +02:00
Joachim Wiberg 71628471bc keventd: minor, constify + coding style
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:28 +02:00
Joachim Wiberg 425256ff3d doc: update keventd docs with new rebrodcast feature
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:28 +02:00
Joachim Wiberg 155d21cee7 keventd: add netlink uevent rebroadcast for libudev-zero
After keventd processes a uevent (creating device nodes, loading
modules, etc.), rebroadcast the original event to netlink group
0x4 so that libudev-zero consumers -- graphical applications,
Wayland/X11 compositors, libinput, and anything else using libudev
to monitor device hotplug -- can receive device events.

Rebroadcast is enabled by default.  Use -g to override the target
netlink group mask, or -G to disable rebroadcast entirely.  Bit 0
(kernel group) is always masked out to prevent feedback loops.

Ref: https://github.com/finit-project/finit/issues/451#issuecomment-3817233886
See: https://github.com/illiliti/libudev-zero

Suggested-by: Aaron Andersen <aaron@fosslib.net>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:27 +02:00
Joachim Wiberg 120c8e6002 man: add fine manual for keventd(8)
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:27 +02:00
Joachim Wiberg 8cf98a7bfd doc: rewrite keventd documentation for unified device manager
Rewrite doc/keventd.md from a 14-line stub into comprehensive
documentation covering all features of the new unified keventd:
device node creation, persistent symlinks, firmware loading,
module loading, coldplug, conditions, and command-line usage.

Update doc/conditions.md to list keventd as the primary provider
of dev/* and sys/pwr/* conditions, with devmon as fallback when
an external device manager is used instead.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:26 +02:00
Joachim Wiberg 8ea299f39d keventd: expand scope to become a unified device manager
Evolve keventd from a power_supply-only monitor into a full device
manager capable of replacing mdev/mdevd on embedded systems.  This
is the first step towards Finit v5.0 where keventd absorbs devmon.

New capabilities:

 - Parse all uevent actions (add, remove, change, bind, unbind)
 - Create and remove /dev nodes with subsystem-aware permissions
 - Create persistent symlinks in /dev/disk/by-{id,path} and
   /dev/input/by-{id,path}, tracked for cleanup on device removal
 - Load firmware from /lib/firmware/ via the sysfs loading protocol
 - Spawn modprobe for MODALIAS events (async, non-blocking)
 - Coldplug support via -c flag (walks /sys/devices to replay events)
 - Set dev/* conditions for Finit's service dependency system

The original power_supply monitoring and sys/pwr/ac condition are
preserved.

New files: keventd.h (structures/API), uevent.c (all device logic).
The receive buffer is increased to 8K with a 1MB socket buffer to
reduce event loss during coldplug bursts.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:26 +02:00
Joachim Wiberg f3b013bc0a runparts: improve usage text
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 21:43:26 +02:00
Joachim Wiberg 8e2492fc87 Merge pull request #490 from finit-project/next
Finit 5.x D-Bus Support

Fixes #396

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 09:23:53 +02:00
Joachim Wiberg 3231ade38a dbus: fixes from a code review of the branch
A pass over the whole branch before merge, mostly in libink since
that is the new code and the part exposed to the wire.  Grouped here
rather than scattered so the review is easy to read in one place.

libink parser and dispatch:

 - Bound reader lengths so a 32-bit size_t can't wrap a wire length
   past the guard and read out of bounds.  Reachable pre-auth on any
   bus, so it matters on the 32-bit targets Finit runs on.
 - Drop a peer when a reply send fails instead of limping on with a
   half-written frame; a built-in whose send failed used to fall
   through and put a second frame on the wire.

initctl:

 - Copy a D-Bus error name out of the reply before closing the client;
   the reply points into memory the close frees.  Both error paths now
   share one helper so this can't creep back.

Authorization:

 - Take the caller's groups from the kernel (SO_PEERCRED plus
   SO_PEERGROUPS) rather than getpwuid()/getgrouplist(), which go
   through NSS and can block PID 1 on a slow LDAP or SSSD backend.
   The check is now a lookup against the group resolved once at init,
   with no NSS and no 256 KiB array on the stack.  A caller reaching
   us through a broker carries no group set, so system-bus privileged
   methods are root-only; the local bus keeps group support.  See
   libink/README.md for the note on lifting that.

Shutdown:

 - Call dbus_exit() from the shutdown path so the server, its peers,
   and the socket are let go cleanly.  The teardown existed but nobody
   called it.

Tests, CI, docs:

 - A fuzz target for the message parser, run as a quick sweep in the
   suite and properly under libFuzzer in CI, with the corpus carried
   between runs.  The -as-uid tests drop groups the way a login does
   so SO_PEERGROUPS sees the right set, and widen the test socket to
   reach the per-method check behind the 0660 gate.  Bring the GitHub
   actions up to versions that run on Node 24, and tidy a few small
   things a /simplify pass turned up.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 08:57:38 +02:00
Joachim Wiberg e62b463852 .github: bump actions to node24
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg c28acf21a1 test: fuzz target for the message parser
__msg_parse() turns bytes off a socket into pointers, before anything
has vouched for the peer, and it is the only place in libink that
does.  It had no test of its own beyond whatever the other tests
happened to send it, all of it well-formed.

The target checks the parser's contract, not merely that it survived.
A header field must point into the header field array, and terminate
inside it, and the parse must never claim more bytes than it was
handed.  Crash-only would pass a parser that walked into the body and
returned fields from there, since those bytes were handed over too.
The expected bounds are derived from the raw header rather than from
the parser, so the two have to agree independently.

Every input is copied into an allocation sized to it first.  Reading
past the end of a roomy buffer stays inside the allocation and the
sanitizer never sees it; against an exact one the same read is a
fault, which is where the sharpest findings come from.

Under libFuzzer it is an ordinary fuzz target and named files replay,
which is how a find gets reproduced.  With no arguments it runs a
fixed sweep -- every truncation, every single-byte corruption, every
value of the length that decides where the header ends, and seeded
garbage -- so the suite covers the same contract on every build,
without clang or a corpus in the tree.  It takes 40 ms.

CI fuzzes it properly on every pull request, keeps the crashers, and
carries the corpus between runs so it reaches deeper over time than
any single run can.  Note that clang links the fuzzer runtime against
the newest GCC tree it finds, so the libstdc++ headers have to match
that one and not the default compiler, which is worth saying since
installing the obvious package leaves you exactly where you started.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg 5cc41b3ba4 libink: take the match keys clients send without asking
A rule carrying sender, destination, or eavesdrop was refused whole,
and a peer whose AddMatch fails gets no signals at all.  That is a
poor trade for keys clients attach as a matter of course: better a
filter wider than asked for than a subscription that never happened.

They are accepted and ignored rather than honoured.  Widening costs
nothing here since Finit is the only sender on this bus, and what it
emits through the match table is state any peer that got this far may
already read.

argN and argNpath still take the whole rule down.  Honouring them
means parsing message bodies, and nothing asks for them yet.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg f3c73f1192 doc: Properties.Set is absent by design, not pending
"Set not yet implemented" reads as a promise.  Finit exposes no
writable property and has no use for one: everything a caller might
want to change is a Manager1 method, where the authorization lives.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg bb43b5670c libink: mark the big-endian gap where it is decided
Only the file header said we refuse 'B' messages, and nobody reads a
header comment when they are looking at why a parse failed.  Put it at
the check, with an XXX so it turns up in a grep for known gaps.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg 853e226812 libink/dbus: give parked and outstanding calls a deadline
A call is parked until the resolver says who sent it, and an outbound
call sits in a pending slot until its reply lands.  Neither had a way
to give up.  A broker that answers GetConnectionUnixUser slowly, or
not at all, leaves the caller waiting forever and keeps the slot; four
of those and every later privileged call is refused with
LimitsExceeded until Finit restarts.

libink cannot time itself out, it has no event loop, so the deadline
is the embedder's to keep.  One sweep per connection covers both, and
the ordering between them stays in the library rather than in each
embedder: calls first, because one timing out usually resolves the
park it was made for, and AccessDenied tells that caller more than a
bare timeout.

The sweep is armed when a resolve is deferred and stops rearming as
soon as nothing is outstanding, so a system that never meets a broker
never wakes up for it.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg a1969efac1 libink: broker state belongs to the connection that has a broker
Parked calls and outbound calls awaiting a reply only ever happen on a
connection talking to a broker, but the parked array sat on the server
and the pending array on every connection.  A server with no broker
carried 4 KiB of slots it could never fill, and both were reachable
from code paths that have no business in them.

Move both behind one struct, allocated on the first park or call and
freed with the connection.  link_server_t goes from 4400 to 168 bytes;
link_connection_t barely moves, its buffers dominate, but an ordinary
peer no longer carries reply-tracking it never uses.

Tokens are now per bus rather than per server, so link_uid_resolved()
takes the connection the answer is about.  Every resolver already has
it: it is the first argument to both the resolver and the reply
callback.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg 44e7da6d56 dbus: policy comment predates the per-sender uid lookup
It still described treating every system-bus caller as unprivileged as
the state of things, which stopped being true when Finit learned to ask
the broker who sent a call.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg 127049d925 test: Finit against a real dbus-daemon
The other dbus-*.sh tests drive libink's own client, so the wire format
was only ever checked against the implementation that wrote it, and the
broker path had no coverage at all.  Every bug found in it so far was
found by hand on a target.

Let the dbus plugin bring up a real dbus-daemon, wait for Finit to
claim org.finit, then talk to Finit with dbus-send, which shares no
code with us.  The privileged call is the interesting one: it can only
be answered by parking the call and asking the broker who sent it.
The bus reads the policy Finit installs, so a malformed org.finit.conf
fails here rather than on a target.

Tests no longer build --with-libsystemd.  Our replacement carries the
real soname but only the sd_notify() symbols, so in the test root it
shadowed the libsystemd the host's libdbus-1 wants and dbus-daemon
died on a missing sd_is_socket.  Nothing under test needs the shared
library: serv is the only consumer and it compiles sd-daemon.c
straight in, which it now does regardless of the flag so notify.sh
keeps testing notify:systemd either way.

Staged from the host by lib/sysroot.mk like any other binary, and
skipped when the host has neither program.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg 2e0b1d6f8b plugin: start a system bus by default
Finit speaks D-Bus itself now and claims org.finit on the system bus
when it finds one, but nothing in a default build ever brings that bus
up.  The plugin that does was opt-in, so the built-in support sat idle
unless the integrator knew to ask for both halves.

Defaulting it on is only reasonable if the result stays the admin's to
change, and a service registered from C through conf_save_service() is
not: it lands in the run path where it cannot be overridden or emptied
out.  So the daemon moves to 20-dbus.conf and its directories to
tmpfiles.d/dbus.conf, the same way hotplug and every other daemon we
ship them for.  The plugin keeps only what has to look at the running
system, the stale pidfile and the machine UUID.

Those directories are no longer chowned to messagebus.  tmpfiles.d
skips a line whose user does not exist rather than falling back, so
the plugin's messagebus/dbus/root ladder has no equivalent there, and
dbus-daemon binds its socket before dropping privileges anyway.

The plugin already bows out where there is no dbus-daemon installed,
so systems that never wanted a bus are unaffected, and
--disable-dbus-plugin is there for those that have one and would still
rather init left it alone.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00
Joachim Wiberg 9875e76419 dbus: let a dropped peer outlive its own read loop
A peer can be dropped from inside its own read loop: a handler emits a
signal, the write to that very peer fails, and the drop lands while
link_connection_process() still holds the connection and will touch
its rx buffer on the way out.  Freeing there pulls the ground out from
under it.  Unlink the peer and let the event loop free it once the
stack has unwound.

The work has to be scheduled with a non-zero delay.  A uev timer armed
with zero is a disarmed timer, so the queue would never run and the
connections would leak instead.

Losing a peer is also not a warning.  It is what shutdown looks like
from here, and every reboot said so on the console.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:06 +02:00
Joachim Wiberg 1e508f9168 libink: trace connections, calls, and authorization decisions
The legacy socket logs a line per command under initctl debug; the bus
logged nothing, so the transport that now carries most of initctl was
the one you could not watch.

libink gets a logger hook rather than a dependency on Finit's: it
passes the emitting function and a formatted message, and dbus.c hands
both to logit() so the two sources read alike.  Trace points cover the
connection lifecycle, every inbound call, and why a call was refused.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:06 +02:00
Joachim Wiberg 63aabaa6df libink/dbus: identify the caller behind a broker
On the local bus SO_PEERCRED says who is calling and the kernel is the
one saying it.  Behind a broker one connection carries every caller,
so that credential describes dbus-daemon and nothing else, and every
privileged method was refused there, root included.

Ask the bus driver instead.  libink parks the call and hands us the
sender; we ask GetConnectionUnixUser and answer when the reply lands,
through the same event loop as everything else.  Nothing blocks:
blocking in PID 1 is why libuEv exists.  That needs calls libink can
make on a connection it already has, so it gained those too.

Answers are cached, since a bus never reuses a unique name while it
runs.  Not across a restart though: a new dbus-daemon numbers from
scratch and :1.7 becomes somebody else, so the cache goes when the
broker does.  A sender name too long to key on is refused rather than
truncated, two callers sharing a truncated key would share an
identity.

Privilege is no longer uid 0 alone.  The socket is already owned by
the --with-group group, so refusing its members every method that
changes anything left a wheel user able to open the bus and unable to
reboot.  Both gates now say the same thing.

Group membership needs NSS, which the C library loads with dlopen(),
so the lookup is compiled out where Finit is built to link statically.
That leaves such a build root-only, which is worth saying out loud
rather than leaving to be discovered.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:06 +02:00
Joachim Wiberg d710a23513 dbus: gate the bus socket like INIT_SOCKET
The D-Bus socket was bound world read/write, on the reasoning that
SO_PEERCRED authorizes each method anyway.  That leaves the read-only
surface open to every local user, and it quietly ignores --with-group:
a system that restricts initctl to the wheel group still handed the
same service state to anyone who asked over the bus.

Bind it 0660 and chown it to the configured group, the same gate the
fallback socket has always had.  libink takes the mode as an argument
rather than assuming one, since who may connect is the embedder's
policy, not the library's.

The mode is applied at bind(), so there is no window where the socket
is more permissive than intended.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:14:48 +02:00
Joachim Wiberg c71ccce742 libink: a broker peer is not an ordinary client
libink was written peer to peer, where one connection is one client
and one principal.  Attaching to a message bus breaks both halves of
that, and two things followed from it.

Signals never reached the system bus.  Fan-out is gated on the peer
having sent AddMatch, but a broker subscribes for its own clients and
never sends us one, so every ServiceStateChanged was dropped on the
floor.  A connection attached with LINK_ATTACH_BROKER gets them all.

Hello, AddMatch and RemoveMatch write per-connection state.  Shared by
every caller behind a broker, that lets one sender exhaust the match
cap or drop another's rule, so we leave all three to the bus, whose
job they are.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:14:48 +02:00
Joachim Wiberg abab0e0fd4 test: depserv waits for the transition to settle
The last assertion sampled bar's state one step after asking Finit to
stop foo, but bar passes through stopped on its way to waiting, so the
test failed roughly one run in eight.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:14:48 +02:00
Joachim Wiberg 3499a5eb76 dbus: say why the system-bus attach failed
The attach is best effort and its diagnostics were tuned for the case
where no broker exists, so a broker that answers but refuses us was
reported as a bare rc=1 at debug level.  Chasing that meant reading
the header builder to find out what the number meant.

Failures now quote the error name the broker sent, and the one for a
name we could not claim says which of the three ways it went wrong.

Repeats stay quiet.  The probe runs on every service and condition
change, and before syslog is up each line is an open, write and close
on /dev/kmsg, so a broker that keeps refusing would otherwise flood
the console during boot.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:14:48 +02:00
Joachim Wiberg b55dada80b libink: a message bus is not a peer
libink was written against the only bus it had, its own, where the
peer on the other end is the client.  A broker is not: it routes for
senders it names itself, expects a DESTINATION on anything addressed
through it, and answers on its own schedule rather than next.

Runlevels go on the wire as S and N rather than the digits Finit
keeps internally, since that is what a caller outside Finit means by
one.

The library stays a convenience library, linked into finit and
initctl and installed nowhere: the ABI promise waits until libink is
its own project.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:26 +02:00
Joachim Wiberg 6310d9e760 initctl: the status views over D-Bus
The summary table, the per-service detail, JSON and the quiet and
ident forms all read state Finit already publishes, so they read it
from the bus like everything else rather than through a second path
that has to be kept in step.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:26 +02:00
Joachim Wiberg ebc0ef62e6 libink/finit: properties, and org.finit on the system bus
Runlevel and version are state, not actions, so they belong behind
org.freedesktop.DBus.Properties rather than another method each.

Finit also claims org.finit on the system bus when it finds one, so
ordinary D-Bus clients can reach it without knowing about
/run/finit/bus.  Opportunistic on purpose: no dbus-daemon is a normal
state for the systems Finit runs on, not an error to report.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:14 +02:00
Joachim Wiberg dd1390a6c2 initctl: monitor and condition control over the bus
The bus can already answer questions and change services, so give
initctl the two things it still did another way: watching signals as
they happen, and getting or setting user conditions.

The dbus tests move with it, split by area rather than one file that
grew every time the library did.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:14 +02:00
Joachim Wiberg 0a269f3298 libink: a brokerless D-Bus implementation for Finit
Finit had no way to answer the question every service manager gets
asked: what is running, and change it.  D-Bus is how the rest of
userspace asks, but linking libdbus, sd-bus or GIO into PID 1 buys a
dependency, an allocator and a main loop we do not control.

So libink: the wire format, an object tree, and a bus of Finit's own
at /run/finit/bus, gated like INIT_SOCKET.  It speaks the standard
org.freedesktop.DBus, .Peer, .Introspectable interfaces, and Finit's
own Manager1, Service1 and Cond1 on top.  Methods that change
something are marked privileged and answered only for a caller the
kernel vouched for, via SO_PEERCRED.

Server and client both, since initctl is the first thing that needs
to talk to it, and its Start/Stop/Restart/Reload now go over the bus
rather than the legacy socket.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:14 +02:00
Joachim Wiberg ae9a24f44f Merge pull request #498 from finit-project/misc-cgroup
Misc cgroup fixes

Fixes #497
2026-08-13 08:15:59 +02:00
Joachim Wiberg 1843c24cda cgroup: move PID 1 out of the init/ group
A cgroup holding processes cannot enable controllers for its children,
so init/ had to stay a leaf.  The hotplug helpers 10-hotplug.conf.in
places there ended up in groups where cpu.weight and friends could
never be set.

Keeping PID 1 in the root cgroup makes init/ a domain like the others.
It also unbreaks lxc-based runtimes: liblxc bases the container tree on
PID 1's cgroup and only special-cases systemd's init.scope/, so under
Finit it landed containers in init/, with no controllers available.

Issue #497

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-12 07:18:40 +02:00
Joachim Wiberg a35ed8f72e cgroups: non-root service cannot create child cgroups
The kernel delegation docs require write access on the directory so
the delegatee can mkdir() children.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-12 06:53:14 +02:00
Joachim Wiberg d2781ef655 Merge pull request #495 from aanderse/cond/bufferlen 2026-08-06 07:39:33 +02:00
Joachim Wiberg 6b77a16f8b conf: add missing passenv to tty blocks
The line-based format has had the flag since v4.4 (issue #286), where
it prepends -p to the built-in getty, which turns it into login -p and
passes the environment on.  The block format was written from the three
documented tty variants and the flags listed in the tty documentation,
and passenv was in neither, so it was left out.  Converting a tty line
that used it therefore lost it, with nothing said.

It only reaches the built-in getty.  An external getty is handed its
arguments through command, so there is nowhere to put a -p, and the
setting is refused with a warning rather than quietly ignored.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:59:11 +02:00
Joachim Wiberg 96c5b4b031 doc: document the provides setting
The migration guide told anyone holding the repeated-stanza idiom for a
per-platform service to split the variants across files or stay on the
line-based format, because a block title is an identity and the
variants have to share one barrier.  provides is the answer, so the
guide converts that shape now instead of routing around it, and the
header of 10-hotplug.conf.in no longer points at the workaround.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:59:09 +02:00
Joachim Wiberg ffb1db7d2d conf: add provides, additional conditions a service supplies
A block title is a service identity, so two variants of one service
gated differently per platform cannot share a title.  They do need to
share the barrier condition downstream services wait for, which until
now was spelled by the identity alone and so could not be shared:

    service syslogd:udev {
        if         = "udevd"
        conditions = { "run/udevadm:5/success" }
        provides   = "pid/syslogd"
        command    = "-syslogd -F"
    }

Any namespace is allowed, since the point is publishing a name that
existing configurations already wait on.  A claim on a condition that
is already owned is dropped with a warning naming the owner, and the
service still registers: the overlap is a configuration bug, and an
init system is more useful degraded than refusing to boot.  A real
identity outranks a claim, pid/<ident> is how Finit tracks its own
services, so it is not up for grabs.

Claims are dropped before each reload re-reads the .conf files.  Doing
it per service as it re-registers is not enough, since services are
read in file order and one re-registering would lose to a claim
another had not dropped yet, flipping the owner on every reload.

initctl cond dump asked who owned a condition only for the pid/
namespace and printed 'static' for usr/, which now hides a provider.
It asks first and falls back to what the namespace implies.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:58:43 +02:00
Joachim Wiberg 22fbb408eb conf: route service conditions through one owner primitive
A service's condition was computed with mkcond() at each of the six
sites that assert or clear it, and svc_find_by_cond() reimplemented
the reverse lookup a seventh time.  maybe_clear_cond() had its own
scan for another service supplying the same condition.

svc_cond_owner() answers who owns a condition, svc_cond_nth() walks
the conditions a service owns, and svc_cond_set()/svc_cond_clear()
apply to all of them.  svc_find_by_cond() becomes a wrapper, and
maybe_clear_cond() keeps its rule per condition rather than for the
one it used to compute.

The provides[] storage lands here unused, since svc_cond_nth() reads
num_provides.  Nothing sets it yet, so a service still owns exactly
its own pid/<ident> and there is no functional change.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:57:57 +02:00
Joachim Wiberg 4299ce8839 doc: document command candidates and the duplicate title rule
The migration guide covered a stanza at a time, which is the wrong
shape for the two idioms that repeated a whole stanza.  One of them,
several candidate binaries for one service, is now a command list.
The other, one service gated differently per platform, has no block
equivalent: those blocks share an identity because they share the
barrier condition downstream services wait for, so they cannot be
given separate titles.  For that one the guide says to split the
variants across files, or leave that file in the line-based format,
which Finit still reads.

The udevd example in services.md taught the merge-broken form, and
system/10-hotplug.conf.in pointed readers at it for their syslogd.

Also lists libConfuse among the build dependencies.  It has been
mandatory since the new .conf format landed, and build.md still said
two libraries.  And corrects the note on variable expansion: it is
${VAR} that libconfuse expands when the file is read, with
${VAR:-default} supported.  A plain $VAR reaches the service, which is
what makes `command = "syslogd -F $SYSLOGD_ARGS"` work with envfile.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:57:56 +02:00
Joachim Wiberg cfae4a0e10 conf: support list of candidate commands for services
A daemon known under more than one name had to be declared once per
name, each stanza carrying nowarn so the ones that were not installed
were skipped quietly.  That leaned on the line-based format having no
titles.  With a title as the service identity the repetition is no
longer available, so the candidates move inside the block:

    command = { "/lib/systemd/systemd-udevd", "-udevd" }

Finit starts the first one whose binary resolves.  A candidate that is
not installed is expected here, so nothing is logged for the ones that
lose, and only the winner is looked up again by service_register().
The leading '-' keeps its meaning and is read from the candidate that
wins, or from the last one when none resolve.

libconfuse accepts a bare string for a list option, so the common
`command = "prog args"` is unchanged, and whichp() already skips any
arguments to the command, so the candidate goes to it as written.

tty blocks take the same list.  Their command is the getty to run, and
it has the same reason to name alternatives.  Both block types now
share svc_command(), which also puts the leading '-' rule back in one
place: tty_translate() had its own copy.  A tty needs no command at
all, it may name a device or notty instead, so svc_command() returns
NULL for an empty section and each caller decides whether that is an
error.

system/10-hotplug.conf.in returns to one udevd block, which is what it
meant all along.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:57:27 +02:00
Joachim Wiberg 516ee41494 conf: reject .conf files that declare duplicate block titles
The block title is the service identity, and libconfuse merges two
sections that share one, without a word.  Two blocks titled the same
in one file therefore loaded as a single service holding a mix of both
declarations, with scalars taken from the last block and lists reset
by it.

system/10-hotplug.conf.in is written this way: two udevd blocks, one
per candidate binary, the way the line-based format spelled a
fallback.  Only the second survived the merge, so a system that has
/lib/systemd/systemd-udevd but no udevd got no udevd service at all,
and the whole `if = "udevd"` chain behind it went with it.

CFGF_NO_TITLE_DUPES turns the merge into a parse error naming the file
and the title, and the file is then rejected as a whole.  The same
title in another file is untouched, that is how an administrator
overrides a system .conf.

Format detection had to stop agreeing with it.  is_new_format() probes
by parsing, so a duplicate title made it answer "not block format" and
conf_parse_file() handed the file to the legacy parser, whose errors
buried the real message.  The probe now clears the flag on its own
copy of the option array, keeping the verdict syntactic.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:56:40 +02:00
Joachim Wiberg c3c41f113e conf: include signal.h for str2sig() and SIGHUP
The reload-signal translation calls str2sig() and compares against
SIGHUP, but conf.c never included signal.h.  glibc pulls it in
transitively, so the omission went unnoticed until a cross-compile
against uClibc-ng in Buildroot failed to build.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-04 10:04:20 +02:00
Joachim Wiberg 1099d663ab conf: escape runtime strings in generated .conf files
The runparts directory and the dbus pidfile and daemon paths are
embedded in double-quoted values of the generated block files.  A
literal quote in either ends the value early and libconfuse rejects
the whole file, and a backslash is read as an escape sequence,
silently mangling the path.  The legacy one-liners had no quoting, so
neither failure existed before the block conversion.

conf_escape() doubles backslashes and escapes quotes; verified by
round-tripping hostile paths through cfg_parse_buf().

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-01 11:25:40 +02:00
Joachim Wiberg 2c6547635a conf: drop stale draft of the pidfile translation comment
An earlier revision of the comment survived right above its final
form.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-01 11:25:40 +02:00
Joachim Wiberg 9360c8e9b9 conf: generate runtime .conf files in the block format
Finit's own generated services -- watchdogd, keventd, runparts, and
the dbus plugin -- still went through conf_save_service() as legacy
one-liners, so `initctl show keventd` taught the old format on a
system otherwise converted to the new one.

conf_save_service() now takes the block title and a printf-style body
and writes the file itself:

    # Generated by finit:conf_save_service()
    service keventd {
            description = "Finit kernel event daemon"
            runlevel    = "S12345789"
            notify      = "none"
            cgroup init {}
            command     = "/libexec/finit/keventd"
    }

vfprintf() into the file also removes the fixed-size staging buffers
in the callers, where a long dbus pidfile path could truncate inside
a quoted string and take the whole generated file with it.

Semantics preserved: watch-only pid:! maps to pidfile without
pidfile-create, the watchdog keeps its watchdog:finit identity, and
log:console becomes log { file = "/dev/console" }.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-01 11:25:39 +02:00
Joachim Wiberg 5aa7daf708 doc: fix mkdocs build warnings
The README.md symlinks exist for GitHub browsing and collide with
index.md when mkdocs renders both; exclude them like TODO.md.

Two links pointed at anchors that never existed: features.md has bold
captions rather than headings, so "Automatic Reload" gets an explicit
attr_list anchor for the link from the front page, and the TTY link
now spells the actual heading, controlling-tty-for-services.

mkdocs build is silent after this.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-01 11:25:39 +02:00
Joachim Wiberg 057ded451f doc: add a syntax migration guide
The new-format reference describes the block format on its own terms,
which is the wrong lookup direction for someone holding a legacy
one-liner.  Aaron migrated Finix OS from the PR description, proving
the need for a token-in, key-out mapping in the user guide.

One table per part of a stanza, worked conversions for the shapes
that changed structurally -- cgroup selection and the three tty
variants -- and the dropped tokens listed with their replacements.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-01 11:25:38 +02:00
Joachim Wiberg 3062f49370 Merge pull request #493 from finit-project/file-format
New file format
2026-08-01 10:08:19 +02:00
Joachim Wiberg 301992b8d8 doc: bare-ID services have no block equivalent
The line-based format accepts `service :80 ...`, deriving the name
from the command basename.  The block format has no counterpart, the
title carries both name and ID.  Implied by the format description,
but anyone converting such a line deserves to find it written down.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-31 11:55:38 +02:00
Joachim Wiberg cdf8ec932a .github: fix another stale link and refresh
Fix stale link to kernel coding style and refresh the contributing guidelines.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-31 11:39:12 +02:00
Joachim Wiberg c6802b5ab3 Merge pull request #494 from henziger/patch-1
doc: Update link in CONTRIBUTING.md
2026-07-31 11:32:44 +02:00
Joachim Wiberg 153f719e33 .github: install libconfuse-dev, mandatory since the new .conf format
The workflows install libuev and libite from source and everything
else from apt, but never libconfuse, so every build job on this
branch dies in configure:

    checking for libconfuse >= 3.3... no

Ubuntu ships libconfuse 3.3 with the static library included, which
covers both the static and regular builds.  Staying on 3.3 in CI is
deliberate: it exercises the fallback paths marked
"XXX: Workaround for libConfuse <3.4" that a from-source 3.4 would
leave untested.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:31:46 +02:00
Joachim Wiberg 6b03a1ec8f conf: adopt the systemd semantics for per-service directories
Aaron Andersen points out in the #492 discussion that the *Directory
settings carry more contract than create-and-chown: per-directory
modes, specific ownership rules, and cleanup toggles.  Without them
config-dir was chowned to the service user, which systemd never does,
an existing directory with drifted ownership was left wrong, and the
runtime directory could not survive a restart.

Now matching systemd.exec(5), and where the man page is vague, the
code in setup_exec_directory():

  - each directory takes a matching -mode key, octal with the leading
    zero, default 0755.  The mode of the named directory is locked
    down again on every start, also when it already exists
  - config-dir is created but never chowned
  - the contents of an existing directory are left alone as long as
    the owner is right; on drift everything under it is chowned back
  - runtime-dir-preserve = no | restart | yes maps
    RuntimeDirectoryPreserve=.  A service still qualified to run when
    the runtime directory would be removed is restarting, not
    stopping, which is what svc_enabled() answers

The dir mechanics move to mksubsysd(), taking resolved ids, with
mksubsys() reduced to a name-resolving wrapper for the dbus plugin.
The child resolves uid/gid once for both directory setup and
privilege drop.

The symlink form, RuntimeDirectory=foo:bar, is not adopted.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:38 +02:00
Joachim Wiberg f0d7257374 Fix #492: add per-service directories, systemd RuntimeDirectory style
A service that drops privileges cannot create its own PID file in
/run, root owns it.  Finit can create the file with pidfile-create,
but the daemon still cannot touch it to confirm a SIGHUP.

Five new settings, block format only: runtime-dir, state-dir,
cache-dir, logs-dir, and config-dir.  The value is a directory name,
resolved under /run, /var/lib, /var/cache, /var/log, and /etc,
respectively.  The directory is created before the service starts,
mode 0755 owned by user/group, and the full path is exported to the
process as RUNTIME_DIRECTORY, STATE_DIRECTORY, CACHE_DIRECTORY,
LOGS_DIRECTORY, and CONFIGURATION_DIRECTORY.  Mode and ownership are
asserted at creation only, a daemon may tighten them afterwards.

The runtime directory is removed when the unit stops, after any
exec-stop-post script, like systemd with RuntimeDirectoryPreserve=no.
A completed run/task counts as stopped unless remain-after-exit keeps
it up.  The other four persist across restarts.

These are the first settings with no legacy token: they are validated
by service_set_dir() and stored on the svc that service_register()
now returns.  systemd accepts a list of directories per setting; this
is a single name for now, widening later is compatible since
libconfuse accepts a bare value for a list option.

The test sysroot gains libnss_files.so.2, which ldd cannot see, glibc
dlopen()s it.  Without it getpwnam() fails inside the chroot, so
user/group settings never resolved and directory ownership could not
be tested.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:37 +02:00
Joachim Wiberg 7f8a64dd39 util: promote rmrf() from tmpfiles.c, fix silent mksubsys() skip
rmrf() is needed outside tmpfiles.c.  The move also deduplicates the
nftw callback: the contents-only removal used by tmpfiles 'D' entries
is now rmcontents(), sharing the callback with rmrf().

mksubsys() did nothing at all when the user could not be resolved, no
directory and no message, and callers had no way to tell.  Now the
directory is always created, ownership is best effort, and an unknown
user is warned about.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:37 +02:00
Joachim Wiberg 6615a248ee service: return the svc from service_register()
Settings that exist only in the block format have nowhere to go: the
legacy line cannot carry them, and service_register() returned an errno
that no caller ever read, so conf.c had no handle on the service it just
created.  Return the svc instead, NULL with errno set on failure, errno
zero when a block is skipped on purpose.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:36 +02:00
Joachim Wiberg be28436c2d conf: drop the angle brackets from if, infer from the value
The block format spells conditions as bare strings everywhere else, so
requiring `if = "<usr/foo>"` left one sigil behind, carried over from
the line-based `if:` token.  A namespace separator already tells the two
apart: a value with a '/' is a condition, anything else is a service
name.

svc_ifthen() picks its mode from the start of the statement and applies
it to the whole, so a statement naming both kinds cannot be evaluated.
That is now an error, as are the old angle brackets, and either one
skips the block:

    /etc/finit.conf: mixed: if: cannot mix a service name with a
    condition in 'anchor,usr/enable-me', a statement must be all of
    one kind, skipping

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:36 +02:00
Joachim Wiberg c584795202 doc: fix back-references and stale claims left by the conversion
Reference sections kept pointing at the line-based format they no longer
document.  `sysv` and `task` sent the reader to Services for "<COND>",
the cgroups chapter opened by listing three legacy directives and then
explained further down that only two of them exist here, and the logging
chapter still gave "log:prio:facility.level,tag:ident" as the full
syntax.

Some claims were wrong independent of the format:

  - a sysv is a supervised daemon, grouped with service in
    SVC_TYPE_DAEMON, not a variation on task
  - restart-max has no upper bound of 255, or any other
  - the built-in rescue fallback runs in 12345789, not 12345
  - conditional loading quotes system/10-hotplug.conf, not
    system/hotplug.conf
  - the key spells conflicts, not conflict
  - the built-in getty no longer wants TERM last, it is a key

`if` takes either a service name or, in angle brackets, a condition,
decided in svc_ifthen().  Only the examples showed this, so it is now
said.

Terminology follows the split index.md already draws: a block is the new
format, a stanza the line-based one.

src/rescue.conf was still line-based, missed because it sits in src/
rather than system/ or contrib/.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:35 +02:00
Joachim Wiberg ddda905487 service: warn when capabilities cannot take effect
An ambient capability only reaches the effective set when euid is
non-zero, so a service that pairs `capabilities = { "^cap_..." }` with a
root user gets none of the restriction it asks for, and keeps the full
root set instead.  Finit read the list, applied it, and said nothing.  A
build without libcap dropped the list on the floor just as quietly.

Both now warn, naming the service:

    nginx: ambient capabilities ('^') have no effect as root, use a
    non-root user, or '%' and '!' entries

The ambient entries are read back from the parsed IAB value rather than
matched in the text, so inheritable ('%') and bounding ('!') entries stay
silent -- those work fine as root.

The warning repeats when the .conf files are re-read on runlevel change,
as parse warnings here already do.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:35 +02:00
Joachim Wiberg 00794d41bc doc: convert the remaining legacy syntax
The block conversion changed the bodies of the reference sections but
left every "**Syntax:**" header spelling the line-based format, so each
page opened by teaching the format it then stopped using.  Six files
were missed entirely: runparts, files, capabilities, requirements,
runlevels, and switchroot.

runparts had no block spelling written down anywhere, though the parser
has read `runparts`, `runparts-progress`, and `runparts-sysv` all along.

tty gains a table per variant.  Its three syntax lines carried nine
positional fields between them, which no longer describes anything the
parser accepts.

Fixes #148

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:23:13 +02:00
Joachim Wiberg b44a5ff2f8 contrib, system: convert the shipped .conf files to the block format
The examples people copy from were still written in the line-based
format, so the block format was documented but nowhere demonstrated.

Two names in contrib were accidents of the old format, where the
service name falls out of the command basename: the Alpine and Void
keymap task was called zcat, and Debian's console/keyboard setup tasks
carried a .sh suffix.  They now carry the name their file implies.
Nothing referenced the old names.

The mdevd coldplug path keeps the name it has always had.  Its legacy
line spelled the name inside the cgroup argument, where it names the
cgroup leaf and not the service, so the barrier condition really is
<run/mdevd-coldplug/success> and not the <run/coldplug/success> the
comment above it promises.  Converted as-is so boot ordering does not
change; the discrepancy is now written down where it happens.

A list may not contain comments, the lexer sees the entries after the
'#' regardless:

    modules = {
    #	"fbcon",
    	"softdog"
    }

so the commented-out module candidates sit above the list instead.

setup-sysroot.sh removes 10-hotplug.conf from the test sysroot, so that
file is covered by parsing only, not by make check.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:31 +02:00
Joachim Wiberg b9ad9bcb21 doc: convert the documentation to the block format
The syntax overview no longer describes a line-based format, since that
is not what the rest of the documentation shows.  It now covers the
grammar, the two naming conventions, the nine aliases, and the leading
'-' on a path, and it says plainly that both formats are still read and
told apart per file by content.  Without that, a reader with an
existing configuration is left wondering what happened to it.

service-opts.md was a list of modifiers to place between a directive
and its command, so it needed rewriting rather than translating: there
are no positions left to describe.  It is now grouped by what the
settings do.

conditions.md needed correcting.  It presented '!' as a condition
prefix alongside '~'.  It is neither a condition nor a negation, it is
a flag on the block that means one thing on a service and another on a
run or task, so it is spelled reload-signal and required here, and the
page maps the old form to both.

Two things the pages claimed are not true.  The kill delay range is
1-300, not 1-60, and stop and reload scripts are no longer run without
a timeout.

ChangeLog.md keeps its line-based examples.  Those sit in historical
release entries, and rewriting them in a syntax that did not exist at
the time would misdate the format.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:31 +02:00
Joachim Wiberg ecac1e58e3 conf: make cgroup delegate and leaf name first-class keys
parse_cgroup() takes two arguments that are not cgroupfs files: the
leaf directory to place the service in, and whether to hand the subtree
over to it.  The block format could express neither.  'name' happened
to work, because a free-form key is emitted as name:VALUE and that is
what the parser looks for, but 'delegate' came out as delegate:true and
was filed as a cgroup setting, so it silently did nothing.

Declare both, and emit delegate as the bare flag the parser expects.
Neither means anything on a top-level group definition, so say so there
rather than emitting something that would be written to cgroupfs.

    service podman {
        cgroup containers { name = "podman"  delegate = true }
        ...
    }

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:30 +02:00
Joachim Wiberg ddaef0600f doc: rewrite sample.conf in the block format
This is what 'initctl create' and 'initctl edit -c' put in front of a
user writing their first .conf file, so it is also the whole of the
"initctl emits the new format" work: neither command generates syntax,
they copy this file and open an editor on it.

The ASCII diagram naming eight positional fields goes with it.  A
block has no positions to explain.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:30 +02:00
Joachim Wiberg 13d58ace9f conf: rename remain and manual for what they actually do
Both keys prompt the question they should be answering.

'remain' decides whether a finished run or task keeps existing: without
it the entry is pruned, so the work re-runs on every runlevel entry,
initctl cannot see it, and its post script never fires.  With it the
entry stays, is not re-run, and gets a teardown when stopped or when it
leaves its runlevels.  That is systemd's RemainAfterExit, and 'remain'
is that name with the informative half cut off.

'manual' says how a service is started but not that it is about
starting at all.

    remain -> remain-after-exit
    manual -> manual-start

Both keep their old spelling as an alias, which they qualify for twice
over, as abbreviations of the canonical name and as the legacy
spellings.

While here, give sec_getbool() the alias argument its string and list
counterparts already take.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:29 +02:00
Joachim Wiberg f48f8aacec conf: replace the '!' condition prefix with reload-signal and required
A condition list could be led by '!', which is not a condition and not
a negation.  It is a flag on the block, and it means two unrelated
things depending on which block it sits in: a service or sysv does not
handle SIGHUP and must be restarted to reload, while a run or task
must not hold up bootstrap.  Writing '<!>' with no condition at all is
legal, which gives away that it was never an operator.

Give each meaning its own key, valid only where it applies:

    service foo { reload-signal = "none" }   # restart to reload
    task    bar { required      = false  }   # do not hold up bootstrap

Using either on a block type it does not apply to warns, as does a '!'
left in a conditions list.  Both still translate to that same '!',
which is all a legacy line can carry, so reload-signal takes SIGHUP or
none for now; str2sig() already accepts any case and an optional SIG
prefix.

This also clears the way for the conditions list to grow real
operators, '+' and '-' for asserted and deasserted, without '!'
sitting among them meaning something else entirely.

The '~' prefix stays.  It belongs to the list: it marks a dependency
whose reload should propagate here.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:29 +02:00
Joachim Wiberg 8a541acd36 conf: settle the block format's key names and value shapes
The initial implementation was done using a naive translator of the
legacy one-liner format key by key, so it inherited encodings that the
block format exists to remove: a timeout packed into a script path, a
small comma-and-colon language inside the log string, sigils standing in
for booleans, and a log key (services) carrying three (!) types.

Settled naming against systemd, OpenRC, FreeBSD rc.subr, s6 and SMF.
Match systemd's semantics, not its naming.

    pid          -> pidfile, plus pidfile-create for the rare case
                    where Finit writes the file rather than the daemon
    environment  -> envfile, since it names a file to source, and the
                    top-level environment {} block sets variables
    pre/post/... -> exec-start-pre, exec-start-ready, exec-stop,
                    exec-stop-post, exec-reload, exec-cleanup, each
                    with its own -timeout instead of "SEC,script"
    halt, kill   -> stop-signal, stop-timeout
    restart      -> restart for the policy, restart-max for the count
    log          -> a block with file, priority and identity, where
                    /dev/null and /dev/console are spelled as paths
    group        -> group and extra-groups, no longer positional
    nowarn       -> a leading - on command, as on envfile

List-valued keys take plural names.  Aliases are desc, cond, mod,
caps, env, halt and kill; an alias may abbreviate the canonical name
or preserve a legacy spelling, nothing else.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:28 +02:00
Joachim Wiberg 8a3d55b416 test: refuse to run against a stale sysroot binary
'make check' refreshes the sysroot through the setup-chroot rule, but
running a test script by hand does not, so the test exercises whichever
finit was installed last and reports on code that is no longer there.
Both a passing and a failing run are then meaningless, and nothing says
so.

Compare the built binary against the installed one at startup and fail
with the command that fixes it.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:28 +02:00
Joachim Wiberg 53cb2b7bdb service: give stop and reload scripts their own timeout
Both were bounded by killdelay, the delay between the stop signal and
SIGKILL, because service_run_script() had nothing else to reach for.
That conflates two things: how long the daemon may take to die, and
how long its stop script may run.

Give each hook a timeout of its own, defaulting to killdelay when
unset, so the existing behaviour is what you get until you ask for
something else.  parse_script() already falls back that way for the
hooks that had one.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:27 +02:00
Joachim Wiberg 2a5190ecff service: do not let a script timeout take PID 1 with it
A stop: or reload: script written with a timeout killed Finit at
config load:

    service stop:5,/bin/true service.sh -- Boom

parse_script() takes the timeout as a pointer and the caller decides
whether it wants one.  However, both stop: and reload: scripts so far
have no timeout, i.e., NULL.  Guard the branch that reads a leading
number.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:27 +02:00
Joachim Wiberg 0721b0fec2 util: one whole-file reader for all tools
Three private ones had grown: fnread() in util.c, flen() behind
pid_cmdline()/pid_cgroup() in cgutil.c, and conf_read_template() in
conf.c.  Two of them were also wrong in ways the others were not.

fnread() formatted the path into a char[256] and stat()ed it before
opening, so a longer path was silently truncated and then read from
whichever file the truncation happened to name, and the size could
change between the look and the read.  flen() existed because neither
of those approaches works on procfs at all, where stat() reports zero
and the only way to learn the size is to read to EOF.

Add fslurp() to util.[ch], which every tool already links.  It opens
first and sizes the fd it holds, treats st_size as a hint, and reads
until EOF, so procfs and regular files take the same path.  Paths are
formatted by libite's vfopenf(), which allocates to fit.  Callers that
need the byte count, /proc/PID/cmdline embeds NUL, ask for it.

fnread() keeps its signature and becomes a bounded copy out of the
result, so its one caller is unaffected.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:27 +02:00
Joachim Wiberg f1393ff8a1 conf: instantiate %i templates for the block format
A template in the block format registered garbage.  conf_parse_file()
routed every file with an '@' in its name straight to the legacy
parser, which read the block line by line: the section header became a
service whose command was the section title, and each key = value line
below it became an environment variable.

    service serv:%i { ... }   ->  service 'serv:eth0' with argument '{'

Substitute %i over the whole file before parsing instead, so format
detection and both parsers see finished text.  A bare name@.conf is
still skipped, it is the template rather than an instance of one.

The legacy parser no longer opens the file or substitutes per line, it
is handed the instantiated buffer, so the template convention now has
one implementation instead of two.  conf_is_template() applies
basenm(), a directory with an '@' in its name is not a template.

libconfuse cannot name a buffer it parses before 3.4, so a typo in a
template would be reported against "[buf]".  Parse through fmemopen()
with the file name preset until the floor moves.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:26 +02:00
Joachim Wiberg 47a18140c1 conf: cleanup and relocation of common parsing functions and helpers
- legacy.[ch]: strictly legacy .conf parser boilerplate only
 - conf.[ch]: .conf parser and generic configuration functionality

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:26 +02:00
Joachim Wiberg c20d64587c conf: relocate global state from legacy parser
Relocate process-wide global variables from legacy parser that ended up
there because it used to be conf.c, but which is now now frozen at the
4.x feature set.  Each variable is moved to their respective "owner".

Give cgroup_current[] and cgroup_settings_current[] named bounds.  Their
extern declarations were unsized, so sizeof() on them stopped compiling
once the definitions moved to another translation unit.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:21:25 +02:00