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>
This commit is contained in:
Joachim Wiberg committed 2026-08-13 09:28:26 +02:00
1 parent 6310d9e760
commit b55dada80b
16 files changed
+340 -250

No files matched your search

+5 -5
View File
@@ -62,16 +62,16 @@ jobs:
tree /tmp || true tree /tmp || true
- name: Check dependencies - name: Check dependencies
run: | run: |
LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/finit ldd /tmp/sbin/finit
size /tmp/sbin/finit size /tmp/sbin/finit
LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/initctl ldd /tmp/sbin/initctl
size /tmp/sbin/initctl size /tmp/sbin/initctl
LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/reboot ldd /tmp/sbin/reboot
size /tmp/sbin/reboot size /tmp/sbin/reboot
- name: Verify starting and showing usage text - name: Verify starting and showing usage text
run: | run: |
sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/finit -h sudo /tmp/sbin/finit -h
sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/initctl -h sudo /tmp/sbin/initctl -h
- name: Enable unprivileged userns (unshare) - name: Enable unprivileged userns (unshare)
run: | run: |
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
+1 -1
View File
@@ -13,7 +13,7 @@ AC_CONFIG_FILES([Makefile
contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile
doc/Makefile doc/config/Makefile doc/Makefile doc/config/Makefile
dbus-1/Makefile dbus-1/Makefile
libink/Makefile libink/libink.pc libink/Makefile
libsystemd/Makefile libsystemd/libsystemd.pc libsystemd/Makefile libsystemd/libsystemd.pc
man/Makefile man/Makefile
plugins/Makefile plugins/Makefile
+8 -1
View File
@@ -50,9 +50,16 @@ Below are a few of the main switches to configure:
`/proc/cmdline`, this is *not recommended* since Finit may be running as the `/proc/cmdline`, this is *not recommended* since Finit may be running as the
init for container apps that can see the host's `/proc` filesystem init for container apps that can see the host's `/proc` filesystem
* `--disable-dbus`: Opt out of Finit's built-in D-Bus support, enabled by
default. See [D-Bus Integration](dbus.md) for what it provides. Not to
be confused with `--enable-dbus-plugin` below, which only starts an
external `dbus-daemon`
* `--enable-alsa-utils-plugin`: Enable the optional `alsa-utils.so` sound plugin. * `--enable-alsa-utils-plugin`: Enable the optional `alsa-utils.so` sound plugin.
* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin. * `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin, which
launches `dbus-daemon` at boot. Unrelated to the built-in bus, see
`--disable-dbus` above.
* `--enable-resolvconf-plugin`: Enable the `resolvconf.so` optional plugin. * `--enable-resolvconf-plugin`: Enable the `resolvconf.so` optional plugin.
+152 -166
View File
@@ -1,45 +1,45 @@
D-Bus Integration D-Bus Integration
================= =================
Finit ships with a built-in, brokerless [D-Bus][] implementation, Finit ships with a built-in, brokerless [D-Bus][] implementation, **libink**
**libink**, that exposes the running init system as a peer on its own (`-link`), that exposes the running init system as a peer on its own private
private bus, and optionally on the system bus when `dbus-daemon` is bus, and optionally on the system bus when `dbus-daemon` is available.
available. Everything `initctl` does is also reachable from any Everything `initctl` does is also reachable from any generic D-Bus tooling —
generic D-Bus tooling — `dbus-send`, `dbus-monitor`, `gdbus`, `dbus-send`, `dbus-monitor`, `gdbus`, language bindings, dashboards,
language bindings, dashboards, monitoring agents, etc. monitoring agents, etc.
> [!NOTE] > [!NOTE]
> D-Bus support is enabled at build time with `--enable-dbus`. See > D-Bus support is enabled by default, opt out at build time with
> [Building](build.md) for details. When disabled, `initctl` keeps > `--disable-dbus`, see [Building](build.md) for details. When disabled,
> using the legacy `INIT_SOCKET` transport and Finit exposes no bus. > `initctl` keeps using the legacy `INIT_SOCKET` transport and Finit
> exposes no bus.
Bus address Bus address
----------- -----------
| Bus | Address | | Bus | Address |
| --- | --- | |------------------------|---------------------------------------------|
| Local (always) | `unix:path=/run/finit/bus` | | Local (always) | `unix:path=/run/finit/bus` |
| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket`, well-known name `org.finit` | | System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket` |
The **local** bus is brokerless: clients connect straight to Finit The **local** bus is brokerless: clients connect straight to Finit over a
over a Unix-domain socket using the standard D-Bus SASL EXTERNAL Unix-domain socket using the standard D-Bus SASL EXTERNAL handshake. No
handshake. No `dbus-daemon` is required, which makes it suitable for `dbus-daemon` is required, which makes it suitable for embedded systems that
embedded systems that don't ship one. don't ship one.
The **system** bus is best-effort: at start-up Finit probes for a The **system** bus is best-effort: Finit probes for a running `dbus-daemon`
running `dbus-daemon` and, if reachable, registers `org.finit` so that and, when reachable, claims the well-known name `org.finit` so that standard
standard tooling sees Finit just like any other system service: tooling sees Finit just like any other system service:
```sh ```sh
dbus-send --system --print-reply --dest=org.finit \ dbus-send --system --print-reply --dest=org.finit /org/finit/manager \
/org/finit/manager \
org.finit.Manager1.ListServices org.finit.Manager1.ListServices
dbus-monitor --system "sender='org.finit'" dbus-monitor --system "sender='org.finit'"
``` ```
If no system bus is present (the common case on embedded targets), If no system bus is present (the common case on embedded targets), this step
this step is silently skipped. is silently skipped.
Object tree Object tree
----------- -----------
@@ -59,12 +59,15 @@ Object tree
Every node implements the usual stock interfaces: Every node implements the usual stock interfaces:
| Interface | Purpose | | Interface | Purpose |
| ------------------------------------ | ------- | |---------------------------------------|-------------------------------------------|
| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` (on `/org/freedesktop/DBus`) | | `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` |
| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | | `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` |
| `org.freedesktop.DBus.Introspectable`| `Introspect()` — XML description | | `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description |
| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | | `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) |
Note: `Hello`, `AddMatch`, and `RemoveMatch` are answered on the canonical
`/org/freedesktop/DBus` object only, as per the D-Bus specification.
`org.finit.Manager1` `org.finit.Manager1`
-------------------- --------------------
@@ -74,88 +77,89 @@ and the service registry.
### Methods ### Methods
| Method | In sig | Out sig | Privileged | Notes | | Method | In sig | Out sig | Priv. | Notes |
| ----------------------- | ------ | ------- | ---------- | ----- | |------------------------------|--------|---------|-------|-----------------------------------------------------------|
| `ListServices` | — | `as` | no | Returns the identities (`name`, `name:id`) of every loaded service. | | `ListServices` | — | `as` | no | Returns identities (`name`, `name:id`) of loaded services |
| `GetService` | `s` | `o` | no | Resolves a service identity to its `Service1` object path. | | `GetService` | `s` | `o` | no | Resolves an identity to its `Service1` object path |
| `Start` | `s` | — | yes | Start the service(s) matching the identity. | | `Start` | `s` | — | yes | Start the service(s) matching the identity |
| `Stop` | `s` | — | yes | Stop the service(s) matching the identity. | | `Stop` | `s` | — | yes | Stop the service(s) matching the identity |
| `Restart` | `s` | — | yes | Restart (stop + start) the service(s). | | `Restart` | `s` | — | yes | Restart (stop + start) the service(s) |
| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes (same as `initctl reload`). | | `Reload` | — | — | yes | Re-read all `*.conf` and apply changes |
| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6). | | `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6) |
| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag. | | `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag |
| `Signal` | `su` | — | yes | Send signal number `u` (1–31) to every running service matching identity `s`. Halted matches are silently skipped. | | `Signal` | `su` | — | yes | Send signal `u` (1–31) to services matching identity `s` |
| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM. | | `Suspend` | — | — | yes | `sync()` + suspend-to-RAM |
| `Reboot` / `Halt` / `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence. | | `Reboot`, `Halt`, `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence |
### Properties ### Properties
All read-only strings; observable via `Properties.Get` and All read-only strings; observable via `Properties.Get` and
`Properties.GetAll`. `Properties.GetAll`.
| Property | Type | Returns | | Property | Type | Returns |
| -------------- | ---- | ------- | |----------------|------|--------------------------------------------------------|
| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"`. | | `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"` |
| `PrevRunlevel` | `s` | Previous runlevel, same encoding. | | `PrevRunlevel` | `s` | Previous runlevel, same encoding, `"N"` when none |
| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`). | | `Version` | `s` | Finit's version string (`PACKAGE_VERSION`) |
### Signals ### Signals
| Signal | Body | Fires when | | Signal | Body | Fires when |
| ----------------------- | ---- | ---------- | |-----------------------|----------------------------------------|------------------------|
| `ServiceStateChanged` | `(sss)` — identity, old state, new state | A service transitions between supervisor states. | | `ServiceStateChanged` | `sss` — identity, old state, new state | Service transitions |
| `RunlevelChanged` | `(ss)` — old level, new level | The system enters a new runlevel. | | `RunlevelChanged` | `ss` — old level, new level | System runlevel change |
State names emitted by `ServiceStateChanged` are stable wire strings: State names emitted by `ServiceStateChanged` are stable wire strings:
`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, `halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`,
`paused`, `waiting`, `starting`, `running`. `paused`, `waiting`, `starting`, `running`. `RunlevelChanged` levels
use the same encoding as the `Runlevel` property: digits, `"S"`, `"N"`.
`org.finit.Service1` (per-service objects) `org.finit.Service1` (per-service objects)
------------------------------------------ ------------------------------------------
Lives at **`/org/finit/service/<encoded>`**, one object per loaded Lives at **`/org/finit/service/<encoded>`**, one object per loaded service.
service. `<encoded>` is the service identity (name, or `name:id` for `<encoded>` is the service identity (name, or `name:id` for templated
templated services) put through systemd-style `_HH` hex escaping — services) put through systemd-style `_HH` hex escaping — ASCII alphanumerics
ASCII alphanumerics and `_` pass through, anything else becomes `_HH` and `_` pass through, anything else becomes `_HH` where `HH` is the hex byte.
where `HH` is the hex byte. Use `Manager1.GetService(identity)` to Use `Manager1.GetService(identity)` to look up the exact path rather than
look up the exact path rather than constructing it by hand. constructing it by hand.
| Method | In sig | Out sig | Privileged | Notes | | Method | In sig | Out sig | Priv. | Notes |
| --------- | ------ | ------- | ---------- | ----- | |-----------|--------|---------|-------|-------------------------------------------------------------|
| `Start` | — | — | yes | Equivalent to `Manager1.Start(<identity>)` for this service. | | `Start` | — | — | yes | Equivalent to `Manager1.Start(<identity>)` for this service |
| `Stop` | — | — | yes | … | | `Stop` | — | — | yes | … |
| `Restart` | — | — | yes | … | | `Restart` | — | — | yes | … |
| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | | `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart) |
### Properties ### Properties
All read-only; observable via `Properties.Get` and `Properties.GetAll`. All read-only; observable via `Properties.Get` and `Properties.GetAll`.
| Property | Type | Returns | | Property | Type | Returns |
| -------------- | ---- | ------- | |-----------------|------|----------------------------------------------------------------|
| `Identity` | `s` | Service identity, `name` or `name:id`. | | `Identity` | `s` | Service identity, `name` or `name:id` |
| `Name` | `s` | Program name (basename of the command). | | `Name` | `s` | Program name (basename of the command) |
| `State` | `s` | Current status, same vocabulary as `initctl status` (richer than the coarse `ServiceStateChanged` strings). | | `State` | `s` | Current status, same vocabulary as `initctl status` |
| `Pid` | `u` | Current PID, 0 when not running. | | `Pid` | `u` | Current PID, 0 when not running |
| `RestartCount` | `u` | Restarts since the last stable run. | | `RestartCount` | `u` | Restarts since the last stable run |
| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S. | | `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S |
| `Description` | `s` | The service's `description` string. | | `Description` | `s` | The service's `description` string |
| `Command` | `s` | Full command line, arguments included. | | `Command` | `s` | Full command line, arguments included |
| `Conditions` | `s` | Declared conditions, raw `.conf` form. | | `Conditions` | `s` | Declared conditions, raw `.conf` form |
| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free`. | | `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free` |
| `Origin` | `s` | Source `.conf` file, empty for built-ins. | | `Origin` | `s` | Source `.conf` file, empty for built-ins |
| `Environment` | `s` | The service's `env` setting, raw. | | `Environment` | `s` | The service's `env` setting, raw |
| `PidFile` | `s` | Declared PID file, raw (`!` prefix included). | | `PidFile` | `s` | Declared PID file, raw (`!` prefix included) |
| `User` | `s` | User the service runs as. | | `User` | `s` | User the service runs as |
| `Group` | `s` | Group the service runs as. | | `Group` | `s` | Group the service runs as |
| `Uptime` | `u` | Seconds since start, 0 when not running. | | `Uptime` | `u` | Seconds since start, 0 when not running |
| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit. | | `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit |
| `RestartsTotal`| `u` | Restarts over the service's lifetime. | | `RestartsTotal` | `u` | Restarts over the service's lifetime |
| `RestartMax` | `u` | Restart limit before the service is blocked. | | `RestartMax` | `u` | Restart limit before the service is blocked |
| `Starts` | `u` | Times started, for `manual-start` units. | | `Starts` | `u` | Times started, for `manual-start` units |
| `ManualStart` | `b` | `manual-start` set in the `.conf`. | | `ManualStart` | `b` | `manual-start` set in the `.conf` |
| `Forking` | `b` | Daemon forks to background. | | `Forking` | `b` | Daemon forks to background |
| `Started` | `b` | Run/task completed successfully. | | `Started` | `b` | Run/task completed successfully |
On every state transition the object also emits the standard On every state transition the object also emits the standard
`org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State` `org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State`
@@ -174,89 +178,87 @@ Lives at **`/org/finit/cond`**. Exposes Finit's
### Methods ### Methods
| Method | In sig | Out sig | Privileged | Notes | | Method | In sig | Out sig | Priv. | Notes |
| -------- | ------ | ------- | ---------- | ----- | |---------|--------|---------|-------|--------------------------------------------------------------|
| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition. | | `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition |
| `Set` | `s` | — | yes | Assert a `usr/<name>` condition. Non-`usr/*` paths are rejected with `InvalidArgs` (system conditions belong to Finit's state machine). | | `Set` | `s` | — | yes | Assert a `usr/<name>` condition |
| `Clear` | `s` | — | yes | Deassert a `usr/<name>` condition. | | `Clear` | `s` | — | yes | Deassert a `usr/<name>` condition |
| `List` | — | `as` | no | Names of all known conditions. | | `List` | — | `as` | no | Names of all known conditions |
| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns. | | `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns |
Note: non-`usr/*` paths are rejected with `InvalidArgs` -- system
conditions belong to Finit's state machine.
### Signals ### Signals
| Signal | Body | Fires when | | Signal | Body | Fires when |
| ------------------- | ---- | ---------- | |--------------------|------------------------|---------------------------------------|
| `ConditionChanged` | `(ss)` — name, new state | A condition is asserted or deasserted. | | `ConditionChanged` | `ss` — name, new state | A condition is asserted or deasserted |
Authorization Authorization
------------- -------------
Privileged methods reject any caller whose peer `uid` isn't 0. Privileged methods reject any caller whose peer `uid` isn't 0. On the
On the **local** bus the kernel's `SO_PEERCRED` socket option tells **local** bus the kernel's `SO_PEERCRED` socket option tells Finit exactly
Finit exactly who's calling, so privilege escalation through the bus who's calling, so privilege escalation through the bus is impossible.
is impossible.
On the **system** bus, all incoming traffic is treated as On the **system** bus, all incoming traffic is treated as unprivileged: it
unprivileged: it arrives through `dbus-daemon` (typically running as arrives through `dbus-daemon` (typically running as root) and Finit cannot yet
root) and Finit cannot yet ask the daemon for the real requester's ask the daemon for the real requester's uid via `GetConnectionUnixUser`. This
uid via `GetConnectionUnixUser`. This means external tooling can means external tooling can freely `Get`/`Introspect`/`ListServices`, but every
freely `Get`/`Introspect`/`ListServices`, but every state-changing state-changing method returns `org.freedesktop.DBus.Error.AccessDenied`.
method returns `org.freedesktop.DBus.Error.AccessDenied`. Per-sender Per-sender uid lookup is on the roadmap.
uid lookup is on the roadmap.
When a privileged method is rejected the error name is exactly When a privileged method is rejected the error name is exactly
`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a `org.freedesktop.DBus.Error.AccessDenied`, and the body carries a short reason
short reason string (e.g. `"permission denied: Start requires root"`). string (e.g. `"permission denied: Start requires root"`).
`initctl` integration `initctl` integration
--------------------- ---------------------
`initctl` transparently routes through D-Bus when the bus socket is `initctl` transparently routes through D-Bus when the bus socket is present,
present, and falls back to the legacy `INIT_SOCKET` transport and falls back to the legacy `INIT_SOCKET` transport otherwise. Concretely,
otherwise. Concretely, the following subcommands use the bus first: the following subcommands use the bus first:
| Subcommand | Method | | Subcommand | Method |
| ------------------- | ------------------------------- | |----------------------------|--------------------------------------------------|
| `initctl start` | `Manager1.Start(svc)` | | `initctl start` | `Manager1.Start(svc)` |
| `initctl stop` | `Manager1.Stop(svc)` | | `initctl stop` | `Manager1.Stop(svc)` |
| `initctl restart` | `Manager1.Restart(svc)` | | `initctl restart` | `Manager1.Restart(svc)` |
| `initctl reload` | `Manager1.Reload()` | | `initctl reload` | `Manager1.Reload()` |
| `initctl reload S` | `Service1.Reload()` (per-svc) | | `initctl reload S` | `Service1.Reload()` (per-svc) |
| `initctl reboot` | `Manager1.Reboot()` | | `initctl reboot` | `Manager1.Reboot()` |
| `initctl halt` | `Manager1.Halt()` | | `initctl halt` | `Manager1.Halt()` |
| `initctl poweroff` | `Manager1.Poweroff()` | | `initctl poweroff` | `Manager1.Poweroff()` |
| `initctl suspend` | `Manager1.Suspend()` | | `initctl suspend` | `Manager1.Suspend()` |
| `initctl debug` | `Manager1.SetDebug()` | | `initctl debug` | `Manager1.SetDebug()` |
| `initctl signal` | `Manager1.Signal(svc, signo)` | | `initctl signal` | `Manager1.Signal(svc, signo)` |
| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` | | `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` |
| `initctl cond set/get/clr` | `Cond1.{Set,Get,Clear}` | | `initctl cond set/get/clear` | `Cond1.{Set,Get,Clear}`, `clr` is an alias |
Two `initctl` subcommands are pure D-Bus features without legacy Two `initctl` subcommands are pure D-Bus features without legacy equivalents:
equivalents:
* `initctl monitor` — subscribes to every signal on the local bus and * `initctl monitor` — subscribes to every signal on the local bus and prints
prints one line per delivery (with timestamp, interface and one line per delivery (with timestamp, interface and member). Same idea as
member). Same idea as `dbus-monitor`, but scoped to Finit and with `dbus-monitor`, but scoped to Finit and with no need to pass `--address`.
no need to pass `--address`.
* `initctl cond` (when D-Bus is reachable) emits the standard * `initctl cond` emits the standard `Cond1.ConditionChanged` signal as a side
`Cond1.ConditionChanged` signal as a side effect, so subscribers effect, so subscribers observe user-driven state changes the same way they
observe user-driven state changes the same way they observe observe service-driven ones.
service-driven ones.
Examples Examples
-------- --------
The examples below use `dbus-send` and `dbus-monitor`, which ship as The examples below use `dbus-send` and `dbus-monitor`, which ship as part of
part of the [dbus][] reference implementation; they're widely the [dbus][] reference implementation; they're widely packaged and don't pull
packaged and don't pull in any extra runtime. Any tool that speaks in any extra runtime. Any tool that speaks D-Bus over an AF_UNIX socket works
D-Bus over an AF_UNIX socket works equally well — `gdbus`, Python's equally well — `gdbus`, Python's `jeepney`/`dasbus`, etc. — substitute their
`jeepney`/`dasbus`, etc. — substitute their syntax for setting the syntax for setting the bus address. The wire protocol is the compatibility
bus address. surface: Finit's own **libink** is an internal implementation detail, external
clients should use any standard D-Bus library.
When `org.finit` is registered on the system bus you can replace When `org.finit` is registered on the system bus you can replace
`--address=unix:path=/run/finit/bus` with `--system` in any example `--address=unix:path=/run/finit/bus` with `--system` in any example below.
below.
List the running services: List the running services:
@@ -309,21 +311,5 @@ The `--dest=org.finit` argument is informational on the local
brokerless bus — Finit accepts any destination because there's no brokerless bus — Finit accepts any destination because there's no
broker to route by name — but `dbus-send` requires it syntactically. broker to route by name — but `dbus-send` requires it syntactically.
[dbus]: https://gitlab.freedesktop.org/dbus/dbus [dbus]: https://gitlab.freedesktop.org/dbus/dbus
Implementation notes
--------------------
The D-Bus server library lives in `libink/`. It speaks the binary
D-Bus 1.0 wire format directly, has no `libdbus`/`sd-bus`/`GIO`
dependency, and is tiny — a few thousand lines of C. The Finit-side
glue in `src/dbus.c` registers vtables for Manager1/Service1/Cond1,
emits the four signals from the appropriate hook points (state
transitions, runlevel transitions, condition flips), and bridges the
event loop to the libink server.
The `initctl` client uses the same library — `link_client_open`,
`link_client_call_v`, `link_client_reply`, `link_reader_*` — so the
single wire-format implementation serves both ends.
[D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html [D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html
-1
View File
@@ -3,6 +3,5 @@
.dirstamp .dirstamp
*.lo *.lo
libink.* libink.*
!libink.pc.in
Makefile Makefile
Makefile.in Makefile.in
+7 -11
View File
@@ -1,5 +1,10 @@
# libink — brokerless D-Bus library (server + client), born inside Finit # libink — brokerless D-Bus library (server + client), born inside Finit
lib_LTLIBRARIES = libink.la #
# Internal convenience library: linked statically into finit and
# initctl, nothing installed. Public install (and the ABI promise
# that comes with it) is deferred until libink is extracted into its
# own project.
noinst_LTLIBRARIES = libink.la
libink_la_SOURCES = server.c auth.c connection.c \ libink_la_SOURCES = server.c auth.c connection.c \
proto.c proto.h \ proto.c proto.h \
marshal.c marshal.h \ marshal.c marshal.h \
@@ -7,16 +12,7 @@ libink_la_SOURCES = server.c auth.c connection.c \
match.c \ match.c \
path.c \ path.c \
client.c io.c \ client.c io.c \
internal.h link.h path.h internal.h
libink_la_LDFLAGS = -version-info 0:0:0
libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE
libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
# pkg-config support
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libink.pc
# Public headers install to $(includedir)/ink/
inkdir = $(includedir)/ink
ink_HEADERS = link.h path.h
+64
View File
@@ -0,0 +1,64 @@
libink — brokerless D-Bus for Finit
===================================
libink is a small C library implementing the [D-Bus wire protocol][spec],
both the server and the client side, without a broker and without any
dependency on `libdbus`, `sd-bus`, or GIO. It was born inside Finit to
let PID 1 be a bus of its own: clients connect straight to the listening
socket, authenticate with the standard SASL EXTERNAL handshake, and get
kernel-authenticated credentials via `SO_PEERCRED`.
For what the bus exposes and how to talk to it, see the User Guide,
[D-Bus Integration](../doc/dbus.md). This file covers the library
itself.
Status
------
libink is an internal implementation detail of Finit: built as a libtool
convenience library, linked statically into `finit` and `initctl`,
nothing installed. There is deliberately no ABI promise yet — that
comes if/when libink is extracted into a project of its own. External
D-Bus clients need none of this; the wire protocol is the compatibility
surface, any standard D-Bus library works.
Layout
------
| File | Contents |
|-----------------|-------------------------------------------------------|
| `server.c` | Listening socket, accept, peer credential capture |
| `auth.c` | SASL EXTERNAL handshake, uid verification |
| `connection.c` | Per-peer state machine, message framing |
| `proto.c` | Wire header parse/build |
| `marshal.c` | Body (de)marshalling: basic types, arrays, variants |
| `dispatch.c` | Object tree, vtable registration, method dispatch |
| `builtin.c` | `org.freedesktop.DBus.*` stock interfaces |
| `match.c` | AddMatch/RemoveMatch rule parsing and signal filter |
| `path.c` | systemd-style `_HH` object path encoding |
| `client.c` | Outgoing connections, method calls, reply/signal wait |
| `io.c` | Shared EINTR-resilient read/write loops |
Public API symbols carry the `link_*` prefix (`link.h`), internal ones
`__*` (`internal.h`). Method handlers are registered as vtables of
`link_method_t`/`link_property_t`; the framework emits variant
signatures from the property table so the declared type is the single
source of truth.
The boundary to Finit is deliberate: nothing under `libink/` includes a
Finit header. All glue lives in `src/dbus.c` — object registration,
signal emission from the service/condition/runlevel hook points, and
the uev event loop bridge. `initctl` uses the client half of the same
library, so one wire-format implementation serves both ends. If libink
is ever spun out, that file is the cut line.
Testing
-------
The `test/dbus-*.sh` suite exercises the library end to end against a
live Finit in a namespace, driven by `test/src/dbus-auth-client.c`.
Wire-format conformance against third-party tools (`dbus-send`,
`dbus-monitor`) and fuzzing of the parsers are tracked as pre-merge
work — this is PID 1's attack surface.
[spec]: https://dbus.freedesktop.org/doc/dbus-specification.html
+44 -1
View File
@@ -24,6 +24,7 @@
struct link_client { struct link_client {
int fd; int fd;
uint32_t next_serial; uint32_t next_serial;
const char *destination; /* not owned; NULL when brokerless */
link_reply_t reply; /* most recent reply view (points into rxbuf) */ link_reply_t reply; /* most recent reply view (points into rxbuf) */
/* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which /* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which
* is a wire-valid (if malformed) type, so we need an out-of-band * is a wire-valid (if malformed) type, so we need an out-of-band
@@ -87,6 +88,12 @@ link_client_t *link_client_open(const char *path)
return link_client_open_timeout(path, 0); return link_client_open_timeout(path, 0);
} }
void link_client_set_destination(link_client_t *c, const char *destination)
{
if (c)
c->destination = destination;
}
void link_client_close(link_client_t *c) void link_client_close(link_client_t *c)
{ {
if (!c) if (!c)
@@ -187,6 +194,41 @@ static void clear_reply(link_client_t *c)
c->have_reply = 0; c->have_reply = 0;
} }
/* A broker interleaves traffic of its own with our replies: claiming a
* name makes it emit NameAcquired, and it arrives before the reply to
* the call that caused it. Read past anything that is not the reply
* we are waiting for. On a brokerless link nothing is interleaved and
* the first message read is always the one we want.
*
* Bounded so a chatty or hostile broker cannot stall PID 1 here; each
* read is bounded in turn by SO_RCVTIMEO when the caller asked for a
* timeout at open. */
#define LINK_CALL_MAX_SKIP 16
static int read_reply(link_client_t *c, uint32_t serial)
{
int i;
for (i = 0; i < LINK_CALL_MAX_SKIP; i++) {
struct link_msg msg;
if (read_one(c, &msg) < 0)
return -1;
/* Not a reply at all, or a reply to something else. */
if (msg.type != LINK_MSG_METHOD_RETURN && msg.type != LINK_MSG_ERROR)
continue;
if (msg.reply_serial != serial)
continue;
publish_reply(c, &msg);
return 0;
}
errno = EPROTO;
return -1;
}
/* Wait up to timeout_ms (-1 = forever) for one full inbound frame /* Wait up to timeout_ms (-1 = forever) for one full inbound frame
* and publish it. Returns 0 on success, 1 on timeout, -1 on error. */ * and publish it. Returns 0 on success, 1 on timeout, -1 on error. */
static int read_and_publish(link_client_t *c, int timeout_ms) static int read_and_publish(link_client_t *c, int timeout_ms)
@@ -235,6 +277,7 @@ int link_client_call(link_client_t *c,
serial = c->next_serial++; serial = c->next_serial++;
hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, hlen = __msg_build_method_call(hdr, sizeof(hdr), serial,
obj_path, interface, member, obj_path, interface, member,
c->destination,
signature, (uint32_t)body_len); signature, (uint32_t)body_len);
if (hlen < 0) if (hlen < 0)
return LINK_CALL_FAIL; return LINK_CALL_FAIL;
@@ -244,7 +287,7 @@ int link_client_call(link_client_t *c,
if (body_len > 0 && send_all(c->fd, body, body_len) < 0) if (body_len > 0 && send_all(c->fd, body, body_len) < 0)
return LINK_CALL_FAIL; return LINK_CALL_FAIL;
if (read_and_publish(c, -1) != 0) if (read_reply(c, serial) < 0)
return LINK_CALL_FAIL; return LINK_CALL_FAIL;
if (c->reply.type == LINK_MSG_METHOD_RETURN) if (c->reply.type == LINK_MSG_METHOD_RETURN)
-10
View File
@@ -1,10 +0,0 @@
prefix=@prefix@
exec_prefix=@exec_prefix@
libdir=@libdir@
includedir=@includedir@
Name: libink
Description: Brokerless D-Bus server library, born inside Finit
Version: @PACKAGE_VERSION@
Libs: -L${libdir} -link
Cflags: -I${includedir}
+7
View File
@@ -226,6 +226,13 @@ link_client_t *link_client_open_timeout(const char *path, int timeout_ms);
void link_client_close(link_client_t *c); void link_client_close(link_client_t *c);
/* Address subsequent calls on `c` to a well-known name. Needed when
* a broker routes the message, e.g. "org.freedesktop.DBus" to reach
* the bus driver itself; a brokerless link has a single peer and
* needs no destination, which is the default. `destination` is not
* copied, so it must outlive the client. */
void link_client_set_destination(link_client_t *c, const char *destination);
/* Detach the authenticated socket from the client and return the raw /* Detach the authenticated socket from the client and return the raw
* fd; subsequent link_client_close on `c` is invalid because the * fd; subsequent link_client_close on `c` is invalid because the
* structure has already been freed. Used by callers (e.g. system-bus * structure has already been freed. Used by callers (e.g. system-bus
+4
View File
@@ -354,6 +354,7 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
const char *path, const char *path,
const char *interface, const char *interface,
const char *member, const char *member,
const char *destination,
const char *signature, const char *signature,
uint32_t body_len) uint32_t body_len)
{ {
@@ -369,6 +370,9 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
return -1; return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0) if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
return -1; return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature && if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1; return -1;
+3 -1
View File
@@ -89,12 +89,14 @@ ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
const char *member, const char *member,
const char *signature, uint32_t body_len); const char *signature, uint32_t body_len);
/* Build a method-call header (client side). */ /* Build a method-call header (client side). `destination` is NULL
* when no broker routes the message. */
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap, ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t serial,
const char *path, const char *path,
const char *interface, const char *interface,
const char *member, const char *member,
const char *destination,
const char *signature, const char *signature,
uint32_t body_len); uint32_t body_len);
+28 -9
View File
@@ -457,15 +457,31 @@ static int manager_suspend(link_call_t *call, void *u)
* org.freedesktop.DBus.Properties interface. Getters write a * org.freedesktop.DBus.Properties interface. Getters write a
* variant containing a single string. */ * variant containing a single string. */
/* Two distinct getters because the property table is static const -- /*
* we can't bind &runlevel/&prevlevel through userdata. */ * Two distinct getters because the property table is static const --
* we can't bind &runlevel/&prevlevel through userdata. The values
* use the same encoding as the runlevel(8) command and `initctl
* runlevel`: "S" for single-user, "N" for no previous runlevel --
* the internal digit is not a wire format.
*/
static const char *runlevel_encode(int level, char *buf, size_t len)
{
if (level == INIT_LEVEL)
strlcpy(buf, "S", len);
else if (level >= 0 && level <= 9)
snprintf(buf, len, "%d", level);
else
strlcpy(buf, "N", len);
return buf;
}
static int prop_runlevel(link_writer_t *w, void *u) static int prop_runlevel(link_writer_t *w, void *u)
{ {
char buf[8]; char buf[8];
(void)u; (void)u;
snprintf(buf, sizeof(buf), "%d", runlevel); link_w_string(w, runlevel_encode(runlevel, buf, sizeof(buf)));
link_w_string(w, buf);
return 0; return 0;
} }
@@ -474,7 +490,10 @@ static int prop_prevrunlevel(link_writer_t *w, void *u)
char buf[8]; char buf[8];
(void)u; (void)u;
snprintf(buf, sizeof(buf), "%d", prevlevel); if (prevlevel <= 0 || prevlevel > 9)
strlcpy(buf, "N", sizeof(buf));
else
snprintf(buf, sizeof(buf), "%d", prevlevel);
link_w_string(w, buf); link_w_string(w, buf);
return 0; return 0;
} }
@@ -893,8 +912,8 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state)
/* ---------- signal emission: RunlevelChanged ---------- /* ---------- signal emission: RunlevelChanged ----------
* *
* Fired by sm.c right after the runlevel global flips. Body is * Fired by sm.c right after the runlevel global flips. Body is
* (old, new) as one-digit strings, matching the format that * (old, new) in the same runlevel(8) encoding as the Manager1
* Manager1.Runlevel (the property) returns. */ * Runlevel property: digits, "S", or "N". */
void dbus_notify_runlevel_change(int old_level, int new_level) void dbus_notify_runlevel_change(int old_level, int new_level)
{ {
uint8_t body[64]; uint8_t body[64];
@@ -902,8 +921,8 @@ void dbus_notify_runlevel_change(int old_level, int new_level)
char old_s[8], new_s[8]; char old_s[8], new_s[8];
ssize_t blen; ssize_t blen;
snprintf(old_s, sizeof(old_s), "%d", old_level); runlevel_encode(old_level, old_s, sizeof(old_s));
snprintf(new_s, sizeof(new_s), "%d", new_level); runlevel_encode(new_level, new_s, sizeof(new_s));
link_writer_init(&w, body, sizeof(body)); link_writer_init(&w, body, sizeof(body));
link_w_string(&w, old_s); link_w_string(&w, old_s);
+13 -8
View File
@@ -308,12 +308,8 @@ static int do_runlevel(char *arg)
if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 && if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 &&
curr_buf[0] && prev_buf[0]) { curr_buf[0] && prev_buf[0]) {
int cl = atoi(curr_buf); /* already in runlevel(8) encoding: digits, S, N */
int pl = atoi(prev_buf); printf("%s %s\n", prev_buf, curr_buf);
curr = (cl == INIT_LEVEL) ? 'S' : (char)(cl + '0');
prev = (pl > 0 && pl <= 9) ? (char)(pl + '0') : 'N';
printf("%c %c\n", prev, curr);
return 0; return 0;
} }
#endif #endif
@@ -2181,6 +2177,15 @@ fail:
return -1; return -1;
} }
/* wire encoding is runlevel(8) style: digits, S, N */
static int runlevel_from_str(const char *s)
{
if (!strcmp(s, "S"))
return INIT_LEVEL;
return atoi(s);
}
/* /*
* All show_status() views over D-Bus, self-contained (the current * All show_status() views over D-Bus, self-contained (the current
* runlevel comes from Manager1, not the legacy socket). Returns 0 * runlevel comes from Manager1, not the legacy socket). Returns 0
@@ -2216,7 +2221,7 @@ static int dbus_show_status(char *arg, int *retval)
/* runlevel feeds the detail view's Runlevels line */ /* runlevel feeds the detail view's Runlevels line */
if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0)
goto fail; goto fail;
runlevel = atoi(curr); runlevel = runlevel_from_str(curr);
if (json) { if (json) {
*retval = json_status_one(stdout, &rows[0], "", 0); *retval = json_status_one(stdout, &rows[0], "", 0);
puts(""); puts("");
@@ -2230,7 +2235,7 @@ static int dbus_show_status(char *arg, int *retval)
if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0)
goto fail; goto fail;
runlevel = atoi(curr); runlevel = runlevel_from_str(curr);
filter = (arg && arg[0]) ? arg : NULL; filter = (arg && arg[0]) ? arg : NULL;
if (json) { if (json) {
-1
View File
@@ -330,7 +330,6 @@ export SYSROOT
top_builddir="${top_builddir:-$TEST_DIR/..}" top_builddir="${top_builddir:-$TEST_DIR/..}"
sysroot_finit="$SYSROOT/sbin/finit" sysroot_finit="$SYSROOT/sbin/finit"
built_finit="$top_builddir/src/finit" built_finit="$top_builddir/src/finit"
[ -x "$top_builddir/src/.libs/finit" ] && built_finit="$top_builddir/src/.libs/finit"
if [ -x "$built_finit" ] && [ -e "$sysroot_finit" ] && ! cmp -s "$built_finit" "$sysroot_finit"; then if [ -x "$built_finit" ] && [ -e "$sysroot_finit" ] && ! cmp -s "$built_finit" "$sysroot_finit"; then
fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'" fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'"
+4 -35
View File
@@ -2,41 +2,17 @@
set -eu set -eu
echo "=== Finit Test Sysroot Setup ==="
echo "Date: $(date)"
echo "SYSROOT: $SYSROOT"
echo "top_builddir: $top_builddir"
echo "srcdir: $srcdir"
echo "================================"
echo
# shellcheck disable=SC2154 # shellcheck disable=SC2154
make -C "$top_builddir" DESTDIR="$SYSROOT" install make -C "$top_builddir" DESTDIR="$SYSROOT" install
mkdir -p "$SYSROOT/sbin/" mkdir -p "$SYSROOT/sbin/"
cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/"
# Prefer the real ELF in .libs/ over the libtool wrapper script -- if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then
# since the test client links libink.la, libtool wraps the top-level cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/"
# dbus-auth-client as a shell script that re-execs the real binary
# via its own RPATH, which falls apart inside the test namespace.
if [ -x "$top_builddir/test/src/.libs/dbus-auth-client" ]; then
cp "$top_builddir/test/src/.libs/dbus-auth-client" "$SYSROOT/sbin/"
elif [ -x "$top_builddir/test/src/dbus-auth-client" ]; then
cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/"
fi fi
# shellcheck disable=SC2154 # shellcheck disable=SC2154
# Prefer the real ELF in .libs/ over the libtool wrapper script at FINITBIN="$(pwd)/$top_builddir/src/finit" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk"
# $top_builddir/src/finit. Libtool generates a shell wrapper when
# the binary depends on an in-tree convenience library (e.g. libink),
# and `ldd <wrapper>` returns "not a dynamic executable", which
# silently makes sysroot.mk copy zero host libs into the sysroot.
if [ -f "$top_builddir/src/.libs/finit" ]; then
finitbin_for_ldd="$(pwd)/$top_builddir/src/.libs/finit"
else
finitbin_for_ldd="$(pwd)/$top_builddir/src/finit"
fi
FINITBIN="$finitbin_for_ldd" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk"
# Drop plugins we don't need in test, only causes confusing FAIL in logs. # Drop plugins we don't need in test, only causes confusing FAIL in logs.
for plugin in tty.so urandom.so rtc.so modprobe.so; do for plugin in tty.so urandom.so rtc.so modprobe.so; do
@@ -50,11 +26,4 @@ for conf in 10-hotplug.conf; do
done done
# Update dynamic linker cache for /usr/local/lib libraries # Update dynamic linker cache for /usr/local/lib libraries
echo "Running ldconfig in sysroot: $SYSROOT" ldconfig -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?"
echo "Contents of $SYSROOT/etc/ld.so.conf:"
cat "$SYSROOT/etc/ld.so.conf" || echo "Warning: ld.so.conf not found"
echo "Libraries in $SYSROOT/usr/local/lib:"
ls -la "$SYSROOT/usr/local/lib/" 2>/dev/null || echo "Warning: /usr/local/lib not found in sysroot"
ldconfig -v -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?"
echo "Verifying ldconfig cache was created:"
ls -la "$SYSROOT/etc/ld.so.cache" || echo "Warning: ld.so.cache not created"