From b55dada80bba1b8f114f997a137d1fcc339fdc66 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:26 +0200 Subject: [PATCH] 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 --- .github/workflows/build.yml | 10 +- configure.ac | 2 +- doc/build.md | 9 +- doc/dbus.md | 318 +++++++++++++++++------------------- libink/.gitignore | 1 - libink/Makefile.am | 18 +- libink/README.md | 64 ++++++++ libink/client.c | 45 ++++- libink/libink.pc.in | 10 -- libink/link.h | 7 + libink/proto.c | 4 + libink/proto.h | 4 +- src/dbus.c | 37 ++++- src/initctl.c | 21 ++- test/lib/setup.sh | 1 - test/setup-sysroot.sh | 39 +---- 16 files changed, 340 insertions(+), 250 deletions(-) create mode 100644 libink/README.md delete mode 100644 libink/libink.pc.in diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84e7cae3..cec7aeab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,16 +62,16 @@ jobs: tree /tmp || true - name: Check dependencies run: | - LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/finit + ldd /tmp/sbin/finit size /tmp/sbin/finit - LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/initctl + ldd /tmp/sbin/initctl size /tmp/sbin/initctl - LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/reboot + ldd /tmp/sbin/reboot size /tmp/sbin/reboot - name: Verify starting and showing usage text run: | - sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/finit -h - sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/initctl -h + sudo /tmp/sbin/finit -h + sudo /tmp/sbin/initctl -h - name: Enable unprivileged userns (unshare) run: | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 diff --git a/configure.ac b/configure.ac index dcd02548..e13316b7 100644 --- a/configure.ac +++ b/configure.ac @@ -13,7 +13,7 @@ AC_CONFIG_FILES([Makefile contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile doc/Makefile doc/config/Makefile dbus-1/Makefile - libink/Makefile libink/libink.pc + libink/Makefile libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile plugins/Makefile diff --git a/doc/build.md b/doc/build.md index 65bf5365..298437b6 100644 --- a/doc/build.md +++ b/doc/build.md @@ -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 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-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. diff --git a/doc/dbus.md b/doc/dbus.md index 60efff2d..2c89a842 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -1,45 +1,45 @@ D-Bus Integration ================= -Finit ships with a built-in, brokerless [D-Bus][] implementation, -**libink**, that exposes the running init system as a peer on its own -private bus, and optionally on the system bus when `dbus-daemon` is -available. Everything `initctl` does is also reachable from any -generic D-Bus tooling — `dbus-send`, `dbus-monitor`, `gdbus`, -language bindings, dashboards, monitoring agents, etc. +Finit ships with a built-in, brokerless [D-Bus][] implementation, **libink** +(`-link`), that exposes the running init system as a peer on its own private +bus, and optionally on the system bus when `dbus-daemon` is available. +Everything `initctl` does is also reachable from any generic D-Bus tooling — +`dbus-send`, `dbus-monitor`, `gdbus`, language bindings, dashboards, +monitoring agents, etc. > [!NOTE] -> D-Bus support is enabled at build time with `--enable-dbus`. See -> [Building](build.md) for details. When disabled, `initctl` keeps -> using the legacy `INIT_SOCKET` transport and Finit exposes no bus. +> D-Bus support is enabled by default, opt out at build time with +> `--disable-dbus`, see [Building](build.md) for details. When disabled, +> `initctl` keeps using the legacy `INIT_SOCKET` transport and Finit +> exposes no bus. Bus address ----------- -| Bus | Address | -| --- | --- | -| Local (always) | `unix:path=/run/finit/bus` | -| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket`, well-known name `org.finit` | +| Bus | Address | +|------------------------|---------------------------------------------| +| Local (always) | `unix:path=/run/finit/bus` | +| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket` | -The **local** bus is brokerless: clients connect straight to Finit -over a Unix-domain socket using the standard D-Bus SASL EXTERNAL -handshake. No `dbus-daemon` is required, which makes it suitable for -embedded systems that don't ship one. +The **local** bus is brokerless: clients connect straight to Finit over a +Unix-domain socket using the standard D-Bus SASL EXTERNAL handshake. No +`dbus-daemon` is required, which makes it suitable for embedded systems that +don't ship one. -The **system** bus is best-effort: at start-up Finit probes for a -running `dbus-daemon` and, if reachable, registers `org.finit` so that -standard tooling sees Finit just like any other system service: +The **system** bus is best-effort: Finit probes for a running `dbus-daemon` +and, when reachable, claims the well-known name `org.finit` so that standard +tooling sees Finit just like any other system service: ```sh -dbus-send --system --print-reply --dest=org.finit \ - /org/finit/manager \ +dbus-send --system --print-reply --dest=org.finit /org/finit/manager \ org.finit.Manager1.ListServices dbus-monitor --system "sender='org.finit'" ``` -If no system bus is present (the common case on embedded targets), -this step is silently skipped. +If no system bus is present (the common case on embedded targets), this step +is silently skipped. Object tree ----------- @@ -59,12 +59,15 @@ Object tree Every node implements the usual stock interfaces: -| Interface | Purpose | -| ------------------------------------ | ------- | -| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` (on `/org/freedesktop/DBus`) | -| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | -| `org.freedesktop.DBus.Introspectable`| `Introspect()` — XML description | -| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | +| Interface | Purpose | +|---------------------------------------|-------------------------------------------| +| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` | +| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | +| `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description | +| `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` -------------------- @@ -74,88 +77,89 @@ and the service registry. ### Methods -| Method | In sig | Out sig | Privileged | Notes | -| ----------------------- | ------ | ------- | ---------- | ----- | -| `ListServices` | — | `as` | no | Returns the identities (`name`, `name:id`) of every loaded service. | -| `GetService` | `s` | `o` | no | Resolves a service identity to its `Service1` object path. | -| `Start` | `s` | — | yes | Start 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). | -| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes (same as `initctl reload`). | -| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6). | -| `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. | -| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM. | -| `Reboot` / `Halt` / `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence. | +| Method | In sig | Out sig | Priv. | Notes | +|------------------------------|--------|---------|-------|-----------------------------------------------------------| +| `ListServices` | — | `as` | no | Returns identities (`name`, `name:id`) of loaded services | +| `GetService` | `s` | `o` | no | Resolves an identity to its `Service1` object path | +| `Start` | `s` | — | yes | Start 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) | +| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes | +| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6) | +| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag | +| `Signal` | `su` | — | yes | Send signal `u` (1–31) to services matching identity `s` | +| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM | +| `Reboot`, `Halt`, `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence | ### Properties All read-only strings; observable via `Properties.Get` and `Properties.GetAll`. -| Property | Type | Returns | -| -------------- | ---- | ------- | -| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"`. | -| `PrevRunlevel` | `s` | Previous runlevel, same encoding. | -| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`). | +| Property | Type | Returns | +|----------------|------|--------------------------------------------------------| +| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"` | +| `PrevRunlevel` | `s` | Previous runlevel, same encoding, `"N"` when none | +| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`) | ### Signals -| Signal | Body | Fires when | -| ----------------------- | ---- | ---------- | -| `ServiceStateChanged` | `(sss)` — identity, old state, new state | A service transitions between supervisor states. | -| `RunlevelChanged` | `(ss)` — old level, new level | The system enters a new runlevel. | +| Signal | Body | Fires when | +|-----------------------|----------------------------------------|------------------------| +| `ServiceStateChanged` | `sss` — identity, old state, new state | Service transitions | +| `RunlevelChanged` | `ss` — old level, new level | System runlevel change | State names emitted by `ServiceStateChanged` are stable wire strings: `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) ------------------------------------------ -Lives at **`/org/finit/service/`**, one object per loaded -service. `` is the service identity (name, or `name:id` for -templated services) put through systemd-style `_HH` hex escaping — -ASCII alphanumerics and `_` pass through, anything else becomes `_HH` -where `HH` is the hex byte. Use `Manager1.GetService(identity)` to -look up the exact path rather than constructing it by hand. +Lives at **`/org/finit/service/`**, one object per loaded service. +`` is the service identity (name, or `name:id` for templated +services) put through systemd-style `_HH` hex escaping — ASCII alphanumerics +and `_` pass through, anything else becomes `_HH` where `HH` is the hex byte. +Use `Manager1.GetService(identity)` to look up the exact path rather than +constructing it by hand. -| Method | In sig | Out sig | Privileged | Notes | -| --------- | ------ | ------- | ---------- | ----- | -| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service. | -| `Stop` | — | — | yes | … | -| `Restart` | — | — | yes | … | -| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | +| Method | In sig | Out sig | Priv. | Notes | +|-----------|--------|---------|-------|-------------------------------------------------------------| +| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service | +| `Stop` | — | — | yes | … | +| `Restart` | — | — | yes | … | +| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart) | ### Properties All read-only; observable via `Properties.Get` and `Properties.GetAll`. -| Property | Type | Returns | -| -------------- | ---- | ------- | -| `Identity` | `s` | Service identity, `name` or `name:id`. | -| `Name` | `s` | Program name (basename of the command). | -| `State` | `s` | Current status, same vocabulary as `initctl status` (richer than the coarse `ServiceStateChanged` strings). | -| `Pid` | `u` | Current PID, 0 when not running. | -| `RestartCount` | `u` | Restarts since the last stable run. | -| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S. | -| `Description` | `s` | The service's `description` string. | -| `Command` | `s` | Full command line, arguments included. | -| `Conditions` | `s` | Declared conditions, raw `.conf` form. | -| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free`. | -| `Origin` | `s` | Source `.conf` file, empty for built-ins. | -| `Environment` | `s` | The service's `env` setting, raw. | -| `PidFile` | `s` | Declared PID file, raw (`!` prefix included). | -| `User` | `s` | User the service runs as. | -| `Group` | `s` | Group the service runs as. | -| `Uptime` | `u` | Seconds since start, 0 when not running. | -| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit. | -| `RestartsTotal`| `u` | Restarts over the service's lifetime. | -| `RestartMax` | `u` | Restart limit before the service is blocked. | -| `Starts` | `u` | Times started, for `manual-start` units. | -| `ManualStart` | `b` | `manual-start` set in the `.conf`. | -| `Forking` | `b` | Daemon forks to background. | -| `Started` | `b` | Run/task completed successfully. | +| Property | Type | Returns | +|-----------------|------|----------------------------------------------------------------| +| `Identity` | `s` | Service identity, `name` or `name:id` | +| `Name` | `s` | Program name (basename of the command) | +| `State` | `s` | Current status, same vocabulary as `initctl status` | +| `Pid` | `u` | Current PID, 0 when not running | +| `RestartCount` | `u` | Restarts since the last stable run | +| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S | +| `Description` | `s` | The service's `description` string | +| `Command` | `s` | Full command line, arguments included | +| `Conditions` | `s` | Declared conditions, raw `.conf` form | +| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free` | +| `Origin` | `s` | Source `.conf` file, empty for built-ins | +| `Environment` | `s` | The service's `env` setting, raw | +| `PidFile` | `s` | Declared PID file, raw (`!` prefix included) | +| `User` | `s` | User the service runs as | +| `Group` | `s` | Group the service runs as | +| `Uptime` | `u` | Seconds since start, 0 when not running | +| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit | +| `RestartsTotal` | `u` | Restarts over the service's lifetime | +| `RestartMax` | `u` | Restart limit before the service is blocked | +| `Starts` | `u` | Times started, for `manual-start` units | +| `ManualStart` | `b` | `manual-start` set in the `.conf` | +| `Forking` | `b` | Daemon forks to background | +| `Started` | `b` | Run/task completed successfully | On every state transition the object also emits the standard `org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State` @@ -174,89 +178,87 @@ Lives at **`/org/finit/cond`**. Exposes Finit's ### Methods -| Method | In sig | Out sig | Privileged | Notes | -| -------- | ------ | ------- | ---------- | ----- | -| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition. | -| `Set` | `s` | — | yes | Assert a `usr/` condition. Non-`usr/*` paths are rejected with `InvalidArgs` (system conditions belong to Finit's state machine). | -| `Clear` | `s` | — | yes | Deassert a `usr/` condition. | -| `List` | — | `as` | no | Names of all known conditions. | -| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns. | +| Method | In sig | Out sig | Priv. | Notes | +|---------|--------|---------|-------|--------------------------------------------------------------| +| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition | +| `Set` | `s` | — | yes | Assert a `usr/` condition | +| `Clear` | `s` | — | yes | Deassert a `usr/` condition | +| `List` | — | `as` | no | Names of all known conditions | +| `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 -| Signal | Body | Fires when | -| ------------------- | ---- | ---------- | -| `ConditionChanged` | `(ss)` — name, new state | A condition is asserted or deasserted. | +| Signal | Body | Fires when | +|--------------------|------------------------|---------------------------------------| +| `ConditionChanged` | `ss` — name, new state | A condition is asserted or deasserted | Authorization ------------- -Privileged methods reject any caller whose peer `uid` isn't 0. -On the **local** bus the kernel's `SO_PEERCRED` socket option tells -Finit exactly who's calling, so privilege escalation through the bus -is impossible. +Privileged methods reject any caller whose peer `uid` isn't 0. On the +**local** bus the kernel's `SO_PEERCRED` socket option tells Finit exactly +who's calling, so privilege escalation through the bus is impossible. -On the **system** bus, all incoming traffic is treated as -unprivileged: it arrives through `dbus-daemon` (typically running as -root) and Finit cannot yet ask the daemon for the real requester's -uid via `GetConnectionUnixUser`. This means external tooling can -freely `Get`/`Introspect`/`ListServices`, but every state-changing -method returns `org.freedesktop.DBus.Error.AccessDenied`. Per-sender -uid lookup is on the roadmap. +On the **system** bus, all incoming traffic is treated as unprivileged: it +arrives through `dbus-daemon` (typically running as root) and Finit cannot yet +ask the daemon for the real requester's uid via `GetConnectionUnixUser`. This +means external tooling can freely `Get`/`Introspect`/`ListServices`, but every +state-changing method returns `org.freedesktop.DBus.Error.AccessDenied`. +Per-sender uid lookup is on the roadmap. When a privileged method is rejected the error name is exactly -`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a -short reason string (e.g. `"permission denied: Start requires root"`). +`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a short reason +string (e.g. `"permission denied: Start requires root"`). `initctl` integration --------------------- -`initctl` transparently routes through D-Bus when the bus socket is -present, and falls back to the legacy `INIT_SOCKET` transport -otherwise. Concretely, the following subcommands use the bus first: +`initctl` transparently routes through D-Bus when the bus socket is present, +and falls back to the legacy `INIT_SOCKET` transport otherwise. Concretely, +the following subcommands use the bus first: -| Subcommand | Method | -| ------------------- | ------------------------------- | -| `initctl start` | `Manager1.Start(svc)` | -| `initctl stop` | `Manager1.Stop(svc)` | -| `initctl restart` | `Manager1.Restart(svc)` | -| `initctl reload` | `Manager1.Reload()` | -| `initctl reload S` | `Service1.Reload()` (per-svc) | -| `initctl reboot` | `Manager1.Reboot()` | -| `initctl halt` | `Manager1.Halt()` | -| `initctl poweroff` | `Manager1.Poweroff()` | -| `initctl suspend` | `Manager1.Suspend()` | -| `initctl debug` | `Manager1.SetDebug()` | -| `initctl signal` | `Manager1.Signal(svc, signo)` | -| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` | -| `initctl cond set/get/clr` | `Cond1.{Set,Get,Clear}` | +| Subcommand | Method | +|----------------------------|--------------------------------------------------| +| `initctl start` | `Manager1.Start(svc)` | +| `initctl stop` | `Manager1.Stop(svc)` | +| `initctl restart` | `Manager1.Restart(svc)` | +| `initctl reload` | `Manager1.Reload()` | +| `initctl reload S` | `Service1.Reload()` (per-svc) | +| `initctl reboot` | `Manager1.Reboot()` | +| `initctl halt` | `Manager1.Halt()` | +| `initctl poweroff` | `Manager1.Poweroff()` | +| `initctl suspend` | `Manager1.Suspend()` | +| `initctl debug` | `Manager1.SetDebug()` | +| `initctl signal` | `Manager1.Signal(svc, signo)` | +| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` | +| `initctl cond set/get/clear` | `Cond1.{Set,Get,Clear}`, `clr` is an alias | -Two `initctl` subcommands are pure D-Bus features without legacy -equivalents: +Two `initctl` subcommands are pure D-Bus features without legacy equivalents: -* `initctl monitor` — subscribes to every signal on the local bus and - prints one line per delivery (with timestamp, interface and - member). Same idea as `dbus-monitor`, but scoped to Finit and with - no need to pass `--address`. +* `initctl monitor` — subscribes to every signal on the local bus and prints + one line per delivery (with timestamp, interface and member). Same idea as + `dbus-monitor`, but scoped to Finit and with no need to pass `--address`. -* `initctl cond` (when D-Bus is reachable) emits the standard - `Cond1.ConditionChanged` signal as a side effect, so subscribers - observe user-driven state changes the same way they observe - service-driven ones. +* `initctl cond` emits the standard `Cond1.ConditionChanged` signal as a side + effect, so subscribers observe user-driven state changes the same way they + observe service-driven ones. Examples -------- -The examples below use `dbus-send` and `dbus-monitor`, which ship as -part of the [dbus][] reference implementation; they're widely -packaged and don't pull in any extra runtime. Any tool that speaks -D-Bus over an AF_UNIX socket works equally well — `gdbus`, Python's -`jeepney`/`dasbus`, etc. — substitute their syntax for setting the -bus address. +The examples below use `dbus-send` and `dbus-monitor`, which ship as part of +the [dbus][] reference implementation; they're widely packaged and don't pull +in any extra runtime. Any tool that speaks D-Bus over an AF_UNIX socket works +equally well — `gdbus`, Python's `jeepney`/`dasbus`, etc. — substitute their +syntax for setting the bus address. The wire protocol is the compatibility +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 -`--address=unix:path=/run/finit/bus` with `--system` in any example -below. +`--address=unix:path=/run/finit/bus` with `--system` in any example below. 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 broker to route by name — but `dbus-send` requires it syntactically. -[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. - +[dbus]: https://gitlab.freedesktop.org/dbus/dbus [D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/libink/.gitignore b/libink/.gitignore index 8f7eb2a5..ffdcdbf7 100644 --- a/libink/.gitignore +++ b/libink/.gitignore @@ -3,6 +3,5 @@ .dirstamp *.lo libink.* -!libink.pc.in Makefile Makefile.in diff --git a/libink/Makefile.am b/libink/Makefile.am index 7f908a9b..e2f46e3a 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -1,5 +1,10 @@ # 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 \ proto.c proto.h \ marshal.c marshal.h \ @@ -7,16 +12,7 @@ libink_la_SOURCES = server.c auth.c connection.c \ match.c \ path.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_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 diff --git a/libink/README.md b/libink/README.md new file mode 100644 index 00000000..fbbf64c1 --- /dev/null +++ b/libink/README.md @@ -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 diff --git a/libink/client.c b/libink/client.c index fbfbe70f..15a1760d 100644 --- a/libink/client.c +++ b/libink/client.c @@ -24,6 +24,7 @@ struct link_client { int fd; uint32_t next_serial; + const char *destination; /* not owned; NULL when brokerless */ link_reply_t reply; /* most recent reply view (points into rxbuf) */ /* 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 @@ -87,6 +88,12 @@ link_client_t *link_client_open(const char *path) 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) { if (!c) @@ -187,6 +194,41 @@ static void clear_reply(link_client_t *c) 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 * 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) @@ -235,6 +277,7 @@ int link_client_call(link_client_t *c, serial = c->next_serial++; hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, obj_path, interface, member, + c->destination, signature, (uint32_t)body_len); if (hlen < 0) 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) return LINK_CALL_FAIL; - if (read_and_publish(c, -1) != 0) + if (read_reply(c, serial) < 0) return LINK_CALL_FAIL; if (c->reply.type == LINK_MSG_METHOD_RETURN) diff --git a/libink/libink.pc.in b/libink/libink.pc.in deleted file mode 100644 index f84248de..00000000 --- a/libink/libink.pc.in +++ /dev/null @@ -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} diff --git a/libink/link.h b/libink/link.h index 265905cb..5f1988a8 100644 --- a/libink/link.h +++ b/libink/link.h @@ -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); +/* 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 * fd; subsequent link_client_close on `c` is invalid because the * structure has already been freed. Used by callers (e.g. system-bus diff --git a/libink/proto.c b/libink/proto.c index d9bf712e..1d7f27c6 100644 --- a/libink/proto.c +++ b/libink/proto.c @@ -354,6 +354,7 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap, const char *path, const char *interface, const char *member, + const char *destination, const char *signature, uint32_t body_len) { @@ -369,6 +370,9 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap, return -1; if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0) return -1; + if (destination && + put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0) + return -1; if (signature && *signature && put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) return -1; diff --git a/libink/proto.h b/libink/proto.h index f7ef0866..ef28f4b7 100644 --- a/libink/proto.h +++ b/libink/proto.h @@ -89,12 +89,14 @@ ssize_t __msg_build_signal(uint8_t *buf, size_t cap, const char *member, 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, uint32_t serial, const char *path, const char *interface, const char *member, + const char *destination, const char *signature, uint32_t body_len); diff --git a/src/dbus.c b/src/dbus.c index 8f98b3d1..95c9af66 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -457,15 +457,31 @@ static int manager_suspend(link_call_t *call, void *u) * org.freedesktop.DBus.Properties interface. Getters write a * 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) { char buf[8]; (void)u; - snprintf(buf, sizeof(buf), "%d", runlevel); - link_w_string(w, buf); + link_w_string(w, runlevel_encode(runlevel, buf, sizeof(buf))); return 0; } @@ -474,7 +490,10 @@ static int prop_prevrunlevel(link_writer_t *w, void *u) char buf[8]; (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); return 0; } @@ -893,8 +912,8 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) /* ---------- signal emission: RunlevelChanged ---------- * * Fired by sm.c right after the runlevel global flips. Body is - * (old, new) as one-digit strings, matching the format that - * Manager1.Runlevel (the property) returns. */ + * (old, new) in the same runlevel(8) encoding as the Manager1 + * Runlevel property: digits, "S", or "N". */ void dbus_notify_runlevel_change(int old_level, int new_level) { 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]; ssize_t blen; - snprintf(old_s, sizeof(old_s), "%d", old_level); - snprintf(new_s, sizeof(new_s), "%d", new_level); + runlevel_encode(old_level, old_s, sizeof(old_s)); + runlevel_encode(new_level, new_s, sizeof(new_s)); link_writer_init(&w, body, sizeof(body)); link_w_string(&w, old_s); diff --git a/src/initctl.c b/src/initctl.c index f246a861..e5f0df70 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -308,12 +308,8 @@ static int do_runlevel(char *arg) if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 && curr_buf[0] && prev_buf[0]) { - int cl = atoi(curr_buf); - int pl = atoi(prev_buf); - - curr = (cl == INIT_LEVEL) ? 'S' : (char)(cl + '0'); - prev = (pl > 0 && pl <= 9) ? (char)(pl + '0') : 'N'; - printf("%c %c\n", prev, curr); + /* already in runlevel(8) encoding: digits, S, N */ + printf("%s %s\n", prev_buf, curr_buf); return 0; } #endif @@ -2181,6 +2177,15 @@ fail: 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 * 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 */ if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) goto fail; - runlevel = atoi(curr); + runlevel = runlevel_from_str(curr); if (json) { *retval = json_status_one(stdout, &rows[0], "", 0); puts(""); @@ -2230,7 +2235,7 @@ static int dbus_show_status(char *arg, int *retval) if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) goto fail; - runlevel = atoi(curr); + runlevel = runlevel_from_str(curr); filter = (arg && arg[0]) ? arg : NULL; if (json) { diff --git a/test/lib/setup.sh b/test/lib/setup.sh index 18b90138..41065998 100755 --- a/test/lib/setup.sh +++ b/test/lib/setup.sh @@ -330,7 +330,6 @@ export SYSROOT top_builddir="${top_builddir:-$TEST_DIR/..}" sysroot_finit="$SYSROOT/sbin/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 fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'" diff --git a/test/setup-sysroot.sh b/test/setup-sysroot.sh index f986125e..59af2272 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -2,41 +2,17 @@ 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 make -C "$top_builddir" DESTDIR="$SYSROOT" install mkdir -p "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" -# Prefer the real ELF in .libs/ over the libtool wrapper script -- -# since the test client links libink.la, libtool wraps the top-level -# 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/" +if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then + cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" fi # shellcheck disable=SC2154 -# Prefer the real ELF in .libs/ over the libtool wrapper script at -# $top_builddir/src/finit. Libtool generates a shell wrapper when -# the binary depends on an in-tree convenience library (e.g. libink), -# and `ldd ` 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" +FINITBIN="$(pwd)/$top_builddir/src/finit" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk" # 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 @@ -50,11 +26,4 @@ for conf in 10-hotplug.conf; do done # Update dynamic linker cache for /usr/local/lib libraries -echo "Running ldconfig in sysroot: $SYSROOT" -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" +ldconfig -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?"