diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 460d378a..aa6d0c50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,16 +63,16 @@ jobs: tree /tmp || true - name: Check dependencies run: | - ldd /tmp/sbin/finit + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/finit size /tmp/sbin/finit - ldd /tmp/sbin/initctl + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/initctl size /tmp/sbin/initctl - ldd /tmp/sbin/reboot + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/reboot size /tmp/sbin/reboot - name: Verify starting and showing usage text run: | - sudo /tmp/sbin/finit -h - sudo /tmp/sbin/initctl -h + sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/finit -h + sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/initctl -h - name: Enable unprivileged userns (unshare) run: | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 diff --git a/Makefile.am b/Makefile.am index a5333be6..b3afffea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,16 @@ ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = man plugins src system tmpfiles.d + +# libink must precede src in SUBDIRS because finit links against +# libink at build time. Automake recurses subdirs strictly in +# declaration order, so the typical "explicit dependency" pattern +# (foo: bar) doesn't help here — ordering is the only thing that +# does. libsystemd is consumed by test/serv only, so its position +# after src is fine. +SUBDIRS = man plugins +if DBUS +SUBDIRS += libink dbus-1 +endif +SUBDIRS += src system tmpfiles.d dist_doc_DATA = README.md LICENSE contrib/finit.conf if CONTRIB diff --git a/configure.ac b/configure.ac index 7b0943ac..8f4349f3 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,8 @@ AC_CONFIG_FILES([Makefile contrib/debian/Makefile contrib/debian/finit.d/Makefile contrib/debian/finit.d/available/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 libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile plugins/Makefile @@ -88,6 +90,10 @@ AC_ARG_ENABLE(logrotate, AS_HELP_STRING([--disable-logrotate], [Disable built-in rotation of /var/log/wtmp]),,[ enable_logrotate=yes]) +AC_ARG_ENABLE(dbus, + AS_HELP_STRING([--disable-dbus], [Disable D-Bus support (libink + Finit object tree)]),,[ + enable_dbus=yes]) + AC_ARG_ENABLE(doc, AS_HELP_STRING([--disable-doc], [Disable build and install of doc/ section]),,[ enable_doc=yes]) @@ -237,6 +243,10 @@ AS_IF([test "x$enable_rescue" != "xno"], [ AM_CONDITIONAL(LOGROTATE, [test "x$enable_logrotate" = "xyes"]) +AS_IF([test "x$enable_dbus" = "xyes"], [ + AC_DEFINE(HAVE_DBUS, 1, [Build D-Bus support via libink])]) +AM_CONDITIONAL(DBUS, [test "x$enable_dbus" = "xyes"]) + ### With features ############################################################################## AS_IF([test "x$bash_dir" = "xyes"], [ PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0], @@ -432,6 +442,7 @@ Optional features: Built-in sulogin......: $with_sulogin $sulogin Built-in watchdogd....: $with_watchdog $watchdog Built-in logrotate....: $enable_logrotate + D-Bus support (libink): $enable_dbus Replacement libsystemd: $with_libsystemd Use cgroup v2.........: $enable_cgroup Use libcap............: $enable_libcap diff --git a/dbus-1/.gitignore b/dbus-1/.gitignore new file mode 100644 index 00000000..b336cc7c --- /dev/null +++ b/dbus-1/.gitignore @@ -0,0 +1,2 @@ +/Makefile +/Makefile.in diff --git a/dbus-1/Makefile.am b/dbus-1/Makefile.am new file mode 100644 index 00000000..dec4a333 --- /dev/null +++ b/dbus-1/Makefile.am @@ -0,0 +1,6 @@ +EXTRA_DIST = org.finit.conf + +if DBUS +dbuspolicydir = $(sysconfdir)/dbus-1/system.d +dist_dbuspolicy_DATA = org.finit.conf +endif diff --git a/dbus-1/org.finit.conf b/dbus-1/org.finit.conf new file mode 100644 index 00000000..ea730e9b --- /dev/null +++ b/dbus-1/org.finit.conf @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 35c61552..3597609b 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -8,6 +8,55 @@ All relevant changes are documented in this file. ### Changes +- Finit now ships with a built-in brokerless D-Bus implementation, + **libink**, exposing the running init system as a peer on its own + private bus at `/run/finit/bus`, and -- opportunistically -- + registering `org.finit` on the standard system bus when a + `dbus-daemon` is reachable. No external `libdbus`/`sd-bus`/`GIO` + dependency. + + The bus implements the stock `org.freedesktop.DBus`, + `org.freedesktop.DBus.Peer`, `org.freedesktop.DBus.Introspectable`, + and `org.freedesktop.DBus.Properties` interfaces, plus three + Finit-specific ones: + + * `org.finit.Manager1` at `/org/finit/manager` -- + `ListServices`, `GetService`, `Start`/`Stop`/`Restart`/`Reload`, + `SetRunlevel`, `SetDebug`, `Signal`, `Suspend`, and the + `Reboot`/`Halt`/`Poweroff` triplet. Read-only properties + `Runlevel`, `PrevRunlevel`, `Version`. Signals + `ServiceStateChanged (sss)` and `RunlevelChanged (ss)`. + + * `org.finit.Service1` at `/org/finit/service/` -- one + object per loaded service, with `Start`/`Stop`/`Restart`/`Reload` + for working off an object handle rather than passing the + identity string around. + + * `org.finit.Cond1` at `/org/finit/cond` -- `Get`, `Set`, `Clear`, + `List`, `Dump` for [user-defined conditions](conditions.md), + with a `ConditionChanged (ss)` signal. + + Privileged methods reject non-root callers based on the kernel- + authenticated peer uid (`SO_PEERCRED`); read-only methods are open. + See [D-Bus Integration](dbus.md) for the full surface, build flag, + and `dbus-send`/`dbus-monitor` examples. + +- `initctl` now transparently routes through D-Bus when the bus is + reachable, with the legacy `INIT_SOCKET` transport as a fallback: + `start`, `stop`, `restart`, `reload`, `reload `, `reboot`, + `halt`, `poweroff`, `suspend`, `debug`, `signal`, `runlevel`, and + `cond {get,set,clr}` all use the new path. Two new subcommands + show up that have no legacy equivalent: + + * `initctl monitor` -- streams every signal on the bus to the + terminal, one line per delivery (`HH:MM:SS iface.member(args)`), + until interrupted. Same idea as `dbus-monitor`, but scoped to + Finit and with no address plumbing required. + + * Issuing `initctl cond set/clr` over D-Bus also fires the + `Cond1.ConditionChanged` signal, so observers see user-driven + state changes the same way they see service-driven ones. + - Restart log now spells out the signal name and flags core dumps, e.g. `killed by SIGKILL` or `killed by SIGSEGV, core dumped`, in place of the bare numeric `by signal: N`. Gives operators a much diff --git a/doc/dbus.md b/doc/dbus.md new file mode 100644 index 00000000..3b035909 --- /dev/null +++ b/doc/dbus.md @@ -0,0 +1,294 @@ +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. + +> [!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. + +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` | + +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: + +```sh +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. + +Object tree +----------- + +``` +/ +├── org/ +│ └── finit/ +│ ├── manager Manager1 +│ ├── cond Cond1 +│ └── service/ +│ ├── keventd Service1 (one per service) +│ ├── sshd +│ └── … +└── org/freedesktop/DBus Standard well-known interfaces +``` + +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) | + +`org.finit.Manager1` +-------------------- + +Lives at **`/org/finit/manager`**. Owns the global init operations +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. | + +### 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`). | + +### 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. | + +State names emitted by `ServiceStateChanged` are stable wire strings: +`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, +`paused`, `waiting`, `starting`, `running`. + +`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. + +| 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). | + +The per-service surface lets generic tooling supply an object handle +once and then invoke methods on it, instead of repeatedly passing the +identity string. + +`org.finit.Cond1` +----------------- + +Lives at **`/org/finit/cond`**. Exposes Finit's +[condition system](conditions.md) to bus clients. + +### 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. | + +### Signals + +| 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. + +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"`). + +`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: + +| 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}` | + +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 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. + +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. + +When `org.finit` is registered on the system bus you can replace +`--address=unix:path=/run/finit/bus` with `--system` in any example +below. + +List the running services: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.finit.Manager1.ListServices +``` + +Read the current runlevel via the Properties interface: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.freedesktop.DBus.Properties.Get \ + string:org.finit.Manager1 string:Runlevel +``` + +Subscribe to every state change on the manager object: + +```sh +dbus-monitor --address=unix:path=/run/finit/bus \ + "type='signal',interface='org.finit.Manager1'" +``` + +Or use `initctl monitor`, which does the same without any address +plumbing. + +Restart a service by its object path: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/service/sshd \ + org.finit.Service1.Restart +``` + +Trigger a `usr/`-condition assertion that wakes any dependent service: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/cond \ + org.finit.Cond1.Set string:"data-ready" +``` + +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. + +[D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/libink/.gitignore b/libink/.gitignore new file mode 100644 index 00000000..8f7eb2a5 --- /dev/null +++ b/libink/.gitignore @@ -0,0 +1,8 @@ +.deps/* +.libs/* +.dirstamp +*.lo +libink.* +!libink.pc.in +Makefile +Makefile.in diff --git a/libink/Makefile.am b/libink/Makefile.am new file mode 100644 index 00000000..7f908a9b --- /dev/null +++ b/libink/Makefile.am @@ -0,0 +1,22 @@ +# libink — brokerless D-Bus library (server + client), born inside Finit +lib_LTLIBRARIES = libink.la +libink_la_SOURCES = server.c auth.c connection.c \ + proto.c proto.h \ + marshal.c marshal.h \ + dispatch.c builtin.c \ + match.c \ + path.c \ + client.c io.c \ + 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/auth.c b/libink/auth.c new file mode 100644 index 00000000..76baee13 --- /dev/null +++ b/libink/auth.c @@ -0,0 +1,300 @@ +/* libink — D-Bus AUTH EXTERNAL handshake + * + * Implements the line-based SASL-style exchange described in the + * D-Bus specification, section "Authentication Protocol". Only the + * AUTH EXTERNAL mechanism is offered; everything else is rejected. + * + * The exchange: + * + * client --> [nul byte] + * client --> "AUTH EXTERNAL \r\n" + * server <-- "OK \r\n" + * client --> "NEGOTIATE_UNIX_FD\r\n" [optional] + * server <-- "ERROR \r\n" (no fd-passing yet) + * client --> "BEGIN\r\n" + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +static const char rejected_ext[] = "REJECTED EXTERNAL\r\n"; + +#define write_all(fd, buf, len) __io_write_all((fd), (buf), (len)) + +/* Shared by __auth_generate_guid (server) and __auth_client + * (client) for hex-encoding GUIDs and uid claims. */ +static const char hex_digits[] = "0123456789abcdef"; + +static int reply(int fd, const char *line) +{ + return write_all(fd, line, strlen(line)); +} + +static int reject(link_connection_t *conn) +{ + return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1); +} + +void __auth_generate_guid(char out[33]) +{ + uint8_t raw[16]; + size_t i; + + if (getrandom(raw, sizeof(raw), 0) != (ssize_t)sizeof(raw)) { + /* Extraordinarily unlikely; GUID is informational, not a + * security primitive — fall back to something deterministic + * rather than uninitialized memory. */ + for (i = 0; i < sizeof(raw); i++) + raw[i] = (uint8_t)(i ^ 0xa5); + } + + for (i = 0; i < sizeof(raw); i++) { + out[i * 2] = hex_digits[raw[i] >> 4]; + out[i * 2 + 1] = hex_digits[raw[i] & 0xf]; + } + out[32] = '\0'; +} + +static int hexval(int c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* Parse "AUTH EXTERNAL " payload into a uid. The argument is + * an even-length hex string whose decoded form is a decimal uid in + * ASCII. Returns 0 on success, -1 on malformed input. */ +static int parse_external_uid(const char *arg, size_t arglen, uid_t *out) +{ + char decoded[24]; + char *ep = NULL; + unsigned long v; + size_t i, dlen; + + if (arglen == 0 || (arglen & 1) || arglen / 2 >= sizeof(decoded)) + return -1; + + dlen = arglen / 2; + for (i = 0; i < dlen; i++) { + int hi = hexval((unsigned char)arg[i * 2]); + int lo = hexval((unsigned char)arg[i * 2 + 1]); + + if (hi < 0 || lo < 0) + return -1; + decoded[i] = (char)((hi << 4) | lo); + } + decoded[dlen] = '\0'; + + errno = 0; + v = strtoul(decoded, &ep, 10); + if (errno || !ep || *ep != '\0' || v > (unsigned long)((uid_t)-1)) + return -1; + + *out = (uid_t)v; + return 0; +} + +static int handle_line(link_connection_t *conn, const char *line, size_t len) +{ + if (len >= 14 && memcmp(line, "AUTH EXTERNAL ", 14) == 0) { + uid_t claimed; + char ok[64]; + + if (parse_external_uid(line + 14, len - 14, &claimed) < 0) + return reject(conn); + if (conn->peer_uid == (uid_t)-1 || claimed != conn->peer_uid) + return reject(conn); + + snprintf(ok, sizeof(ok), "OK %s\r\n", conn->guid); + return reply(conn->fd, ok); + } + + if (len == 4 && memcmp(line, "AUTH", 4) == 0) + return reject(conn); + + if (len == 17 && memcmp(line, "NEGOTIATE_UNIX_FD", 17) == 0) + return reply(conn->fd, "ERROR fd-passing not supported\r\n"); + + if (len == 5 && memcmp(line, "BEGIN", 5) == 0) { + conn->auth = LINK_AUTH_DONE; + return 0; + } + + if (len == 6 && memcmp(line, "CANCEL", 6) == 0) + return reject(conn); + + if (len >= 5 && memcmp(line, "ERROR", 5) == 0) + return reject(conn); + + return reply(conn->fd, "ERROR Unknown command\r\n"); +} + +/* Pull one CR+LF-terminated line out of conn->linebuf. Returns the + * line length (without the CR+LF), or 0 if no complete line is + * present yet. Consumes the line on success. */ +static size_t take_line(link_connection_t *conn, char *out, size_t outsz) +{ + size_t i; + + for (i = 0; i + 1 < conn->linelen; i++) { + if (conn->linebuf[i] == '\r' && conn->linebuf[i + 1] == '\n') { + size_t linelen = i; + size_t consumed = i + 2; + + if (linelen >= outsz) + linelen = outsz - 1; + + memcpy(out, conn->linebuf, linelen); + out[linelen] = '\0'; + + memmove(conn->linebuf, conn->linebuf + consumed, + conn->linelen - consumed); + conn->linelen -= consumed; + return linelen; + } + } + return 0; +} + +int __auth_process(link_connection_t *conn) +{ + uint8_t buf[256]; + ssize_t n; + size_t off = 0; + + n = read(conn->fd, buf, sizeof(buf)); + if (n == 0) + return -1; /* peer closed */ + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return 0; + return -1; + } + + if (conn->auth == LINK_AUTH_NUL) { + if (buf[0] != 0x00) { + conn->auth = LINK_AUTH_FAILED; + return -1; + } + off = 1; + conn->auth = LINK_AUTH_LINE; + } + + if (conn->auth == LINK_AUTH_LINE) { + size_t take = (size_t)n - off; + char line[LINK_AUTH_LINEBUF_SIZE]; + size_t linelen; + + if (conn->linelen + take > sizeof(conn->linebuf)) { + conn->auth = LINK_AUTH_FAILED; + return -1; + } + memcpy(conn->linebuf + conn->linelen, buf + off, take); + conn->linelen += take; + + while ((linelen = take_line(conn, line, sizeof(line))) > 0) { + if (handle_line(conn, line, linelen) < 0) + return -1; + if (conn->auth != LINK_AUTH_LINE) + break; + } + + /* If BEGIN flipped us to DONE, any remaining linebuf bytes + * are the first bytes of the binary D-Bus stream — move + * them to rxbuf so the dispatcher can pick them up on the + * next process() call. */ + if (conn->auth == LINK_AUTH_DONE && conn->linelen > 0) { + if (conn->linelen > sizeof(conn->rxbuf)) + return -1; + memcpy(conn->rxbuf, conn->linebuf, conn->linelen); + conn->rxlen = conn->linelen; + conn->linelen = 0; + } + } + + return 0; +} + +/* ---- client-side SASL composer ---- */ + +/* Read a single CR+LF (or just LF) terminated line from fd into buf. + * Returns the line length (without the terminator), or -1 on EOF or + * buffer overflow. Blocks until a complete line arrives. + * + * Used only by __auth_client; the server-side parser does its + * own line extraction out of conn->linebuf. */ +static ssize_t client_read_line(int fd, char *buf, size_t bufsz) +{ + size_t off = 0; + + while (off + 1 < bufsz) { + ssize_t n = read(fd, buf + off, 1); + + if (n == 0) + return -1; + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + if (buf[off] == '\n') { + buf[off] = '\0'; + if (off > 0 && buf[off - 1] == '\r') + buf[--off] = '\0'; + return (ssize_t)off; + } + off++; + } + return -1; +} + +int __auth_client(int fd, uid_t uid) +{ + + char uidstr[16]; + char hexuid[32]; + char line[64]; + char reply_line[256]; + size_t i, n; + int rc; + + if (write_all(fd, "\0", 1) < 0) + return -1; + + n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)uid); + if (n * 2 >= sizeof(hexuid)) + return -1; + for (i = 0; i < n; i++) { + unsigned c = (unsigned char)uidstr[i]; + + hexuid[i * 2] = hex_digits[c >> 4]; + hexuid[i * 2 + 1] = hex_digits[c & 0xf]; + } + hexuid[n * 2] = '\0'; + + rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); + if (rc < 0 || (size_t)rc >= sizeof(line)) + return -1; + if (write_all(fd, line, (size_t)rc) < 0) + return -1; + + if (client_read_line(fd, reply_line, sizeof(reply_line)) < 0) + return -1; + if (strncmp(reply_line, "OK ", 3) != 0) + return -1; + + if (write_all(fd, "BEGIN\r\n", 7) < 0) + return -1; + return 0; +} diff --git a/libink/builtin.c b/libink/builtin.c new file mode 100644 index 00000000..5a5a2ae5 --- /dev/null +++ b/libink/builtin.c @@ -0,0 +1,468 @@ +/* libink — built-in implementations of the well-known + * org.freedesktop.DBus.* interfaces (Hello, Peer, Introspectable). + * + * These run before object-tree lookup in the dispatcher; returning + * 0 means "handled, reply sent"; <0 means "not a built-in, fall + * through to user-registered handlers". + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +#include "internal.h" + +/* ---------- helpers ---------- */ + +static int member_is(const struct link_msg *m, const char *iface, const char *member) +{ + if (!m->member || strcmp(m->member, member) != 0) + return 0; + if (m->interface && strcmp(m->interface, iface) != 0) + return 0; + return 1; +} + +static int send_string_reply(link_connection_t *conn, const struct link_msg *req, + const char *s) +{ + struct link_writer w; + ssize_t blen; + + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_string(&w, s); + blen = __w_finish(&w); + if (blen < 0) { + errno = EMSGSIZE; + return -1; + } + return __send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); +} + +/* ---------- Hello ---------- */ + +static int handle_hello(link_connection_t *conn, const struct link_msg *m) +{ + if (!conn->unique_name[0]) { + uint32_t n = ++conn->server->next_unique_id; + + snprintf(conn->unique_name, sizeof(conn->unique_name), + ":1.%u", n); + } + return send_string_reply(conn, m, conn->unique_name); +} + +/* ---------- Ping / GetMachineId ---------- */ + +static int handle_ping(link_connection_t *conn, const struct link_msg *m) +{ + return __send_method_return(conn, m, NULL, NULL, 0); +} + +static int handle_get_machine_id(link_connection_t *conn, const struct link_msg *m) +{ + /* D-Bus mandates a 32-char hex machine-id. Use the per-server + * GUID-style identifier we already generate for each connection, + * promoted to a per-server constant on first call. Good enough + * for the brokerless case where clients use this only as a + * sanity hint. */ + static char machine_id[33]; + + if (!machine_id[0]) + __auth_generate_guid(machine_id); + return send_string_reply(conn, m, machine_id); +} + +/* ---------- Introspect ---------- */ + +struct xbuf { + char *buf; + size_t cap; + size_t off; + int err; +}; + +static void xprintf(struct xbuf *x, const char *fmt, ...) +{ + va_list ap; + int n; + + if (x->err) + return; + va_start(ap, fmt); + n = vsnprintf(x->buf + x->off, x->cap - x->off, fmt, ap); + va_end(ap); + if (n < 0 || (size_t)n >= x->cap - x->off) { + x->err = 1; + return; + } + x->off += (size_t)n; +} + +/* Emit a single stanza for one method definition. */ +static void emit_method(struct xbuf *x, const link_method_t *m) +{ + const char *p; + + xprintf(x, " \n", m->name); + for (p = m->in_sig ? m->in_sig : ""; *p; p++) + xprintf(x, " \n", *p); + for (p = m->out_sig ? m->out_sig : ""; *p; p++) + xprintf(x, " \n", *p); + xprintf(x, " \n"); +} + +static void emit_property(struct xbuf *x, const link_property_t *p) +{ + /* Setters are not implemented, so every property advertises + * access="read" today. When Properties.Set lands, switch on + * a writable flag. */ + xprintf(x, " \n", + p->name, p->sig ? p->sig : "s"); +} + +/* Introspection limitation: emit_method prints one per + * character of the signature, which is wrong for compound types + * (an "a(ss)" arg appears as four args). Good enough for the + * "s", "u", "as" signatures we expose today; replace with a + * signature parser when the first compound argument lands. */ + +static const char STANDARD_INTERFACES_XML[] = + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n"; + +/* Is `child` a path under `parent`? If so, write the first segment + * of the relative remainder into out (max outsz) and return 1. */ +static int child_segment(const char *parent, const char *child, + char *out, size_t outsz) +{ + size_t plen = strlen(parent); + const char *rest, *slash; + size_t seglen; + + if (strncmp(parent, child, plen) != 0) + return 0; + /* Special case for "/" */ + if (plen == 1 && parent[0] == '/') + rest = child + 1; + else if (child[plen] != '/') + return 0; + else + rest = child + plen + 1; + if (!*rest) + return 0; + + slash = strchr(rest, '/'); + seglen = slash ? (size_t)(slash - rest) : strlen(rest); + if (seglen + 1 > outsz) + return 0; + memcpy(out, rest, seglen); + out[seglen] = '\0'; + return 1; +} + +static int handle_introspect(link_connection_t *conn, const struct link_msg *m) +{ + static char xml[8192]; /* static keeps the stack small in PID 1 */ + struct xbuf x = { .buf = xml, .cap = sizeof(xml) }; + struct link_object *o; + const char *path = m->path; + + xprintf(&x, + "\n" + "\n"); + + xprintf(&x, "%s", STANDARD_INTERFACES_XML); + + o = NULL; + { + struct link_object *p; + + TAILQ_FOREACH(p, &conn->server->objects, link) { + if (strcmp(p->path, path) == 0) { + o = p; + break; + } + } + } + + if (o) { + struct link_vtable_entry *e; + const link_method_t *meth; + + TAILQ_FOREACH(e, &o->vtables, link) { + const link_property_t *prop; + + xprintf(&x, " \n", + e->vt->interface); + if (e->vt->methods) + for (meth = e->vt->methods; meth->name; meth++) + emit_method(&x, meth); + if (e->vt->properties) + for (prop = e->vt->properties; prop->name; prop++) + emit_property(&x, prop); + xprintf(&x, " \n"); + } + } + + { + struct link_object *p; + char prev_seg[LINK_PATH_MAX] = { 0 }; + char seg [LINK_PATH_MAX]; + + TAILQ_FOREACH(p, &conn->server->objects, link) { + if (!child_segment(path, p->path, seg, sizeof(seg))) + continue; + if (strcmp(prev_seg, seg) == 0) + continue; + xprintf(&x, " \n", seg); + memcpy(prev_seg, seg, sizeof(prev_seg)); + } + } + + xprintf(&x, "\n"); + + if (x.err) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Introspection XML overflow"); + + return send_string_reply(conn, m, xml); +} + +/* ---------- Properties.Get / GetAll ---------- */ + +/* Find the (object, vtable-entry) pair matching `path` and `interface`. + * Returns NULL if the path is unknown or the interface isn't exposed + * on it. */ +static struct link_vtable_entry * +find_vtable(link_connection_t *conn, const char *path, const char *interface) +{ + struct link_object *o; + + if (!path || !interface) + return NULL; + TAILQ_FOREACH(o, &conn->server->objects, link) { + struct link_vtable_entry *e; + + if (strcmp(o->path, path) != 0) + continue; + TAILQ_FOREACH(e, &o->vtables, link) { + if (strcmp(e->vt->interface, interface) == 0) + return e; + } + } + return NULL; +} + +static int handle_properties_get(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface, *prop_name; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "ss") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.Get takes (interface, property)"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0 || __r_string(&r, &prop_name) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e || !e->vt->properties) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + for (p = e->vt->properties; p->name; p++) { + if (strcmp(p->name, prop_name) != 0) + continue; + if (!p->getter) + break; + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + return __send_method_return(conn, m, "v", + conn->txbuf, (size_t)blen); + } + + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownProperty", + "No such property on this interface"); +} + +static int handle_properties_get_all(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.GetAll takes one string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_array_begin(&w, '{'); + if (e->vt->properties) { + for (p = e->vt->properties; p->name; p++) { + if (!p->getter) + continue; + __w_struct_begin(&w); + __w_string(&w, p->name); + if (p->getter(&w, e->userdata) != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + __w_struct_end(&w); + } + } + __w_array_end(&w); + + blen = __w_finish(&w); + if (blen < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply too large"); + + return __send_method_return(conn, m, "a{sv}", + conn->txbuf, (size_t)blen); +} + +/* ---------- AddMatch / RemoveMatch ---------- */ + +static int handle_add_match(link_connection_t *conn, const struct link_msg *m) +{ + const char *rule; + struct link_reader r; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "AddMatch takes a single string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &rule) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + if (__match_add(conn, rule) < 0) { + if (errno == ENOSPC) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.LimitsExceeded", + "Too many active match rules"); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.MatchRuleInvalid", + "Unrecognised key or malformed rule"); + } + return __send_method_return(conn, m, NULL, NULL, 0); +} + +static int handle_remove_match(link_connection_t *conn, const struct link_msg *m) +{ + const char *rule; + struct link_reader r; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "RemoveMatch takes a single string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &rule) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + if (__match_remove(conn, rule) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.MatchRuleNotFound", + "No such match rule on this connection"); + + return __send_method_return(conn, m, NULL, NULL, 0); +} + +/* ---------- entry point ---------- */ + +int __handle_builtin(link_connection_t *conn, const struct link_msg *m) +{ + if (member_is(m, "org.freedesktop.DBus", "Hello") && + m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) + return handle_hello(conn, m); + + if (member_is(m, "org.freedesktop.DBus", "AddMatch") && + m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) + return handle_add_match(conn, m); + + if (member_is(m, "org.freedesktop.DBus", "RemoveMatch") && + m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) + return handle_remove_match(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Peer", "Ping")) + return handle_ping(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Peer", "GetMachineId")) + return handle_get_machine_id(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Introspectable", "Introspect")) + return handle_introspect(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Properties", "Get")) + return handle_properties_get(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Properties", "GetAll")) + return handle_properties_get_all(conn, m); + + return -1; /* not a built-in */ +} diff --git a/libink/client.c b/libink/client.c new file mode 100644 index 00000000..fbfbe70f --- /dev/null +++ b/libink/client.c @@ -0,0 +1,351 @@ +/* libink — synchronous client-side D-Bus calls. + * + * Pairs with server.c / connection.c on the receiving end. The + * intent is for short-lived CLI tools (initctl) and tests to use + * libink as their D-Bus client rather than reimplementing the + * wire format. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +struct link_client { + int fd; + uint32_t next_serial; + 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 + * "have we ever produced a reply?" flag. */ + int have_reply; + /* Re-use the server-side rx buffer size for incoming replies. + * Replies to our methods are bounded by the same per-message + * sanity cap as everything else. */ + uint8_t rxbuf[LINK_RX_BUF_SIZE]; + size_t rxlen; +}; + +link_client_t *link_client_open_timeout(const char *path, int timeout_ms) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + link_client_t *c; + int fd; + + if (!path || strlen(path) >= sizeof(sun.sun_path)) + return NULL; + memcpy(sun.sun_path, path, strlen(path) + 1); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return NULL; + + if (timeout_ms > 0) { + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + /* Cover both directions so the AUTH write and the + * subsequent read both honour the budget. setsockopt + * failure is non-fatal -- the bus may still respond + * quickly enough; we just lose the safety net. */ + (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { + close(fd); + return NULL; + } + if (__auth_client(fd, geteuid()) < 0) { + close(fd); + return NULL; + } + + c = calloc(1, sizeof(*c)); + if (!c) { + close(fd); + return NULL; + } + c->fd = fd; + c->next_serial = 1; + return c; +} + +link_client_t *link_client_open(const char *path) +{ + return link_client_open_timeout(path, 0); +} + +void link_client_close(link_client_t *c) +{ + if (!c) + return; + if (c->fd >= 0) + close(c->fd); + free(c); +} + +int link_client_steal_fd(link_client_t *c) +{ + int fd; + + if (!c) + return -1; + fd = c->fd; + c->fd = -1; + free(c); + return fd; +} + +/* read_full / send_all live in libink/io.c. */ +#define read_full(fd, buf, len) __io_read_full ((fd), (buf), (len)) +#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) + +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +/* Read one complete D-Bus message: the 16-byte fixed header tells + * us fields_len + body_len, so we then issue exactly one more read + * for the remainder. Both lengths are bounded against rxbuf before + * arithmetic so a malformed wire u32 can't wrap into a near-4-GiB + * read. */ +static int read_one(link_client_t *c, struct link_msg *msg) +{ + uint32_t body_len, fields_len, body_off, total; + ssize_t consumed; + + memset(msg, 0, sizeof(*msg)); + + if (read_full(c->fd, c->rxbuf, 16) < 0) + return -1; + if (c->rxbuf[0] != 'l') + return -1; + + body_len = (uint32_t)c->rxbuf[4] + | ((uint32_t)c->rxbuf[5] << 8) + | ((uint32_t)c->rxbuf[6] << 16) + | ((uint32_t)c->rxbuf[7] << 24); + fields_len = (uint32_t)c->rxbuf[12] + | ((uint32_t)c->rxbuf[13] << 8) + | ((uint32_t)c->rxbuf[14] << 16) + | ((uint32_t)c->rxbuf[15] << 24); + + /* Bound the wire-supplied lengths before any arithmetic on + * them. Without this, fields_len = 0xFFFFFFF0 would wrap + * 16u + fields_len to near zero, bypass the total < rxbuf + * check, and trigger an out-of-bounds read. */ + if (fields_len > sizeof(c->rxbuf) || body_len > sizeof(c->rxbuf)) + return -1; + + body_off = (uint32_t)ALIGN_UP(16u + fields_len, 8u); + total = body_off + body_len; + if (total > sizeof(c->rxbuf) || total < 16) + return -1; + + if (read_full(c->fd, c->rxbuf + 16, total - 16) < 0) + return -1; + c->rxlen = total; + + consumed = __msg_parse(c->rxbuf, c->rxlen, msg); + if (consumed <= 0) + return -1; + return 0; +} + +static void publish_reply(link_client_t *c, const struct link_msg *m) +{ + c->reply.type = m->type; + c->reply.signature = m->signature; + c->reply.error_name = m->error_name; + c->reply.path = m->path; + c->reply.interface = m->interface; + c->reply.member = m->member; + c->reply.body = m->body_avail ? m->body : NULL; + c->reply.body_len = m->body_avail; + c->have_reply = 1; +} + +/* The reply view in c->reply points into c->rxbuf and is invalidated + * the moment we touch that buffer again -- clear it at every entry, + * even on the bad-args path, so link_client_reply() cannot return + * stale dangling pointers from a previous call. */ +static void clear_reply(link_client_t *c) +{ + if (!c) + return; + memset(&c->reply, 0, sizeof(c->reply)); + c->have_reply = 0; +} + +/* 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) +{ + struct link_msg msg; + + if (timeout_ms >= 0) { + struct pollfd pfd = { .fd = c->fd, .events = POLLIN }; + int rc; + + do { + rc = poll(&pfd, 1, timeout_ms); + } while (rc < 0 && errno == EINTR); + if (rc < 0) + return -1; + if (rc == 0) + return 1; + } + + if (read_one(c, &msg) < 0) + return -1; + publish_reply(c, &msg); + return 0; +} + +int link_client_call(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + /* Generous: Manager1 headers fit in ~150 B, but the buffer is + * shared with whatever future callers throw at us, and an + * overflow only manifests as a silent LINK_CALL_FAIL via + * __msg_build_method_call returning -1. 1 KiB on stack + * is cheap insurance. */ + uint8_t hdr[1024]; + ssize_t hlen; + uint32_t serial; + + clear_reply(c); + if (!c || c->fd < 0 || !obj_path || !member) + return LINK_CALL_FAIL; + + serial = c->next_serial++; + hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, + obj_path, interface, member, + signature, (uint32_t)body_len); + if (hlen < 0) + return LINK_CALL_FAIL; + + if (send_all(c->fd, hdr, (size_t)hlen) < 0) + return LINK_CALL_FAIL; + if (body_len > 0 && send_all(c->fd, body, body_len) < 0) + return LINK_CALL_FAIL; + + if (read_and_publish(c, -1) != 0) + return LINK_CALL_FAIL; + + if (c->reply.type == LINK_MSG_METHOD_RETURN) + return LINK_CALL_OK; + if (c->reply.type == LINK_MSG_ERROR) + return LINK_CALL_ERROR; + return LINK_CALL_FAIL; +} + +const link_reply_t *link_client_reply(link_client_t *c) +{ + if (!c || !c->have_reply) + return NULL; + return &c->reply; +} + +int link_reply_get_string(const link_reply_t *r, const char **out) +{ + link_reader_t reader; + + if (out) + *out = NULL; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_string(&reader, out); +} + +int link_reply_get_u32(const link_reply_t *r, uint32_t *out) +{ + link_reader_t reader; + + if (out) + *out = 0; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_u32(&reader, out); +} + +/* Marshal varargs into `body` (capacity `cap`) according to `sig`. + * Returns the marshalled length on success, -1 on overflow or + * unsupported type code. */ +static ssize_t marshal_va(uint8_t *body, size_t cap, + const char *sig, va_list ap) +{ + link_writer_t w; + const char *s; + + link_writer_init(&w, body, cap); + for (s = sig; *s; s++) { + switch (*s) { + case 'y': + link_w_byte(&w, (uint8_t)va_arg(ap, int)); + break; + case 'b': + link_w_bool(&w, va_arg(ap, int)); + break; + case 'u': + link_w_u32(&w, va_arg(ap, uint32_t)); + break; + case 's': + link_w_string(&w, va_arg(ap, const char *)); + break; + case 'o': + link_w_path(&w, va_arg(ap, const char *)); + break; + default: + return -1; + } + } + return link_writer_finish(&w); +} + +int link_client_call_v(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, ...) +{ + uint8_t body[1024]; + ssize_t body_len = 0; + + if (signature && *signature) { + va_list ap; + + va_start(ap, signature); + body_len = marshal_va(body, sizeof(body), signature, ap); + va_end(ap); + if (body_len < 0) + return LINK_CALL_FAIL; + } + + return link_client_call(c, obj_path, interface, member, + signature, body, (size_t)body_len); +} + +int link_client_wait(link_client_t *c, int timeout_ms) +{ + clear_reply(c); + if (!c || c->fd < 0) + return -1; + return read_and_publish(c, timeout_ms); +} diff --git a/libink/connection.c b/libink/connection.c new file mode 100644 index 00000000..f88afda8 --- /dev/null +++ b/libink/connection.c @@ -0,0 +1,119 @@ +/* libink — per-connection lifecycle and dispatch entry point + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include "internal.h" + +int link_connection_get_fd(const link_connection_t *conn) +{ + return conn ? conn->fd : -1; +} + +uid_t link_connection_get_uid(const link_connection_t *conn) +{ + return conn ? conn->peer_uid : (uid_t)-1; +} + +void link_connection_close(link_connection_t *conn) +{ + size_t i; + + if (!conn) + return; + + for (i = 0; i < conn->matches_count; i++) + __match_free(conn->matches[i]); + + if (conn->fd >= 0) + close(conn->fd); + free(conn); +} + +/* Process buffered binary D-Bus messages, dispatching each complete + * message and shifting consumed bytes out of rxbuf. Returns -1 if + * we should drop the connection (peer closed, protocol error, + * downstream send failure). */ +static int process_binary(link_connection_t *conn) +{ + while (conn->rxlen > 0) { + struct link_msg msg; + ssize_t consumed; + + consumed = __msg_parse(conn->rxbuf, conn->rxlen, &msg); + if (consumed == 0) + break; /* incomplete; wait for more bytes */ + if (consumed < 0) + return -1; + + if (__dispatch_message(conn, &msg) < 0) + return -1; + + memmove(conn->rxbuf, conn->rxbuf + consumed, + conn->rxlen - (size_t)consumed); + conn->rxlen -= (size_t)consumed; + } + return 0; +} + +int link_connection_process(link_connection_t *conn) +{ + if (!conn) { + errno = EINVAL; + return -1; + } + + if (conn->auth == LINK_AUTH_FAILED) + return -1; + + if (conn->auth != LINK_AUTH_DONE) { + if (__auth_process(conn) < 0) + return -1; + + /* Still in SASL phase — wait for more bytes. */ + if (conn->auth != LINK_AUTH_DONE) + return 0; + + /* Fall through: BEGIN may have arrived in the same read + * as the first binary message. auth_process moved those + * bytes into rxbuf; they must be dispatched now, because + * no further wake-up is guaranteed (the kernel has + * already delivered everything that was readable). */ + if (process_binary(conn) < 0) + return -1; + } + + /* Read additional bytes and dispatch any complete messages. + * process_binary is called inside the loop after every + * successful read; no second call after EAGAIN because the + * buffer hasn't changed. */ + for (;;) { + ssize_t n; + size_t room = sizeof(conn->rxbuf) - conn->rxlen; + + if (room == 0) { + errno = E2BIG; + return -1; + } + + n = read(conn->fd, conn->rxbuf + conn->rxlen, room); + if (n == 0) + return -1; /* peer closed */ + if (n < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return 0; + return -1; + } + conn->rxlen += (size_t)n; + if (process_binary(conn) < 0) + return -1; + } +} diff --git a/libink/dispatch.c b/libink/dispatch.c new file mode 100644 index 00000000..7e9536fa --- /dev/null +++ b/libink/dispatch.c @@ -0,0 +1,421 @@ +/* libink — object tree, vtable registration, and method dispatch. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +#include "internal.h" + +/* ---------- object/vtable registration ---------- */ + +static struct link_object *find_object(link_server_t *srv, const char *path) +{ + struct link_object *o; + + TAILQ_FOREACH(o, &srv->objects, link) + if (strcmp(o->path, path) == 0) + return o; + return NULL; +} + +int link_server_remove_object(link_server_t *srv, const char *path) +{ + struct link_object *o; + struct link_vtable_entry *e; + + if (!srv || !path) { + errno = EINVAL; + return -1; + } + + o = find_object(srv, path); + if (!o) { + errno = ENOENT; + return -1; + } + + while ((e = TAILQ_FIRST(&o->vtables))) { + TAILQ_REMOVE(&o->vtables, e, link); + free(e); + } + TAILQ_REMOVE(&srv->objects, o, link); + free(o); + return 0; +} + +int link_server_add_object(link_server_t *srv, const char *path, + const link_vtable_t *vt, void *userdata) +{ + struct link_object *o; + struct link_vtable_entry *e; + size_t plen; + + if (!srv || !path || !*path || !vt || !vt->interface) { + errno = EINVAL; + return -1; + } + plen = strlen(path); + if (plen >= LINK_PATH_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + o = find_object(srv, path); + if (!o) { + o = calloc(1, sizeof(*o)); + if (!o) + return -1; + memcpy(o->path, path, plen + 1); + TAILQ_INIT(&o->vtables); + TAILQ_INSERT_TAIL(&srv->objects, o, link); + } + + e = calloc(1, sizeof(*e)); + if (!e) + return -1; + e->vt = vt; + e->userdata = userdata; + TAILQ_INSERT_TAIL(&o->vtables, e, link); + return 0; +} + +/* ---------- lookup ---------- */ + +static const link_method_t *find_method(const link_vtable_t *vt, const char *name) +{ + const link_method_t *m; + + if (!vt->methods) + return NULL; + for (m = vt->methods; m->name; m++) + if (strcmp(m->name, name) == 0) + return m; + return NULL; +} + +/* If incoming.interface is NULL, search every interface on the + * object for a member with this name. Returns the matching method + * and writes back its vtable_entry in *out_e. */ +static const link_method_t *resolve(struct link_object *o, + const char *iface, const char *member, + struct link_vtable_entry **out_e) +{ + struct link_vtable_entry *e; + const link_method_t *m; + + if (iface) { + TAILQ_FOREACH(e, &o->vtables, link) { + if (strcmp(e->vt->interface, iface) != 0) + continue; + m = find_method(e->vt, member); + if (m) { + *out_e = e; + return m; + } + return NULL; + } + return NULL; + } + + TAILQ_FOREACH(e, &o->vtables, link) { + m = find_method(e->vt, member); + if (m) { + *out_e = e; + return m; + } + } + return NULL; +} + +/* ---------- send helpers ---------- */ + +/* send_all is blocking. A slow or paused peer that lets its + * socket buffer fill will stall PID 1 here. The 64-peer cap + + * kernel buffer (~256 KB) hide the symptom for now; for production + * hardening, make the fd non-blocking and drop the peer on EAGAIN. + * The shared implementation lives in libink/io.c so the eventual + * fix lands in one place. */ +#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) + +int __send_method_return(link_connection_t *conn, const struct link_msg *req, + const char *out_sig, + const uint8_t *body, size_t body_len) +{ + uint8_t hdr[512]; + ssize_t hlen; + uint32_t serial = ++conn->next_serial; + + hlen = __msg_build_return(hdr, sizeof(hdr), serial, + req->serial, + req->sender, + out_sig, (uint32_t)body_len); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (send_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (body_len > 0 && send_all(conn->fd, body, body_len) < 0) + return -1; + return 0; +} + +int link_connection_emit_signal(link_connection_t *conn, + const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + uint8_t hdr[512]; + ssize_t hlen; + uint32_t serial; + size_t i; + int matched = 0; + + if (!conn || !path || !interface || !member) { + errno = EINVAL; + return -1; + } + if (conn->auth != LINK_AUTH_DONE) + return 0; /* peer hasn't finished the SASL phase */ + + for (i = 0; i < conn->matches_count; i++) { + if (__match_matches(conn->matches[i], path, + interface, member)) { + matched = 1; + break; + } + } + if (!matched) + return 0; /* peer didn't subscribe — nothing to do */ + + serial = ++conn->next_serial; + hlen = __msg_build_signal(hdr, sizeof(hdr), serial, + path, interface, member, + signature, (uint32_t)body_len); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (send_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (body_len > 0 && send_all(conn->fd, body, body_len) < 0) + return -1; + return 0; +} + +int __send_error(link_connection_t *conn, const struct link_msg *req, + const char *error_name, const char *text) +{ + uint8_t hdr[512]; + uint8_t body[256]; + ssize_t hlen; + size_t blen = 0; + uint32_t serial = ++conn->next_serial; + const char *sig = NULL; + + if (text && *text) { + struct link_writer w; + ssize_t n; + + __w_init(&w, body, sizeof(body)); + __w_string(&w, text); + n = __w_finish(&w); + if (n < 0) { + errno = EMSGSIZE; + return -1; + } + blen = (size_t)n; + sig = "s"; + } + + hlen = __msg_build_error(hdr, sizeof(hdr), serial, + req->serial, req->sender, + error_name, sig, (uint32_t)blen); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (send_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (blen > 0 && send_all(conn->fd, body, blen) < 0) + return -1; + return 0; +} + +/* ---------- link_call public surface ---------- */ + +const char *link_call_path (const link_call_t *c) { return c ? c->incoming.path : NULL; } +const char *link_call_interface(const link_call_t *c) { return c ? c->incoming.interface : NULL; } +const char *link_call_member (const link_call_t *c) { return c ? c->incoming.member : NULL; } +uid_t link_call_uid (const link_call_t *c) { return c ? c->conn->peer_uid : (uid_t)-1; } + +link_writer_t *link_call_reply(link_call_t *call) +{ + if (!call || call->reply_consumed || call->error_sent) + return NULL; + call->reply_consumed = 1; + __w_init(&call->reply_writer, + call->conn->txbuf, sizeof(call->conn->txbuf)); + return &call->reply_writer; +} + +int link_call_reply_error(link_call_t *call, const char *name, const char *message) +{ + if (!call || call->error_sent) { + errno = EINVAL; + return -1; + } + call->error_sent = 1; + return __send_error(call->conn, &call->incoming, name, message); +} + +/* ---------- public reader wrappers ---------- */ + +int link_call_read_byte (link_call_t *c, uint8_t *o) { return __r_byte (&c->read_cursor, o); } +int link_call_read_bool (link_call_t *c, int *o) { return __r_bool (&c->read_cursor, o); } +int link_call_read_u32 (link_call_t *c, uint32_t *o) { return __r_u32 (&c->read_cursor, o); } +int link_call_read_string(link_call_t *c, const char **o) { return __r_string(&c->read_cursor, o); } +int link_call_read_path (link_call_t *c, const char **o) { return __r_path (&c->read_cursor, o); } + +/* ---------- public writer wrappers ---------- */ + +void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap) { __w_init(w, buf, cap); } +ssize_t link_writer_finish(link_writer_t *w) { return __w_finish(w); } + +void link_w_byte (link_writer_t *w, uint8_t v) { __w_byte(w, v); } +void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); } +void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); } +void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); } +void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); } +void link_w_variant_string(link_writer_t *w, const char *s) { __w_variant_string(w, s); } +void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); } +void link_w_array_end (link_writer_t *w) { __w_array_end(w); } +void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); } +void link_w_struct_end (link_writer_t *w) { __w_struct_end(w); } + +/* ---------- public reader wrappers ---------- */ + +void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len) { __r_init(r, body, len); } +int link_r_byte (link_reader_t *r, uint8_t *o) { return __r_byte (r, o); } +int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); } +int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); } +int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); } +int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); } +int link_r_variant_string(link_reader_t *r, const char **o) { return __r_variant_string(r, o); } +int link_r_align (link_reader_t *r, size_t n) { return __r_align (r, n); } +int link_r_array_begin(link_reader_t *r, size_t *e) { return __r_array_begin(r, e); } +int link_r_done (const link_reader_t *r) { return __r_done (r); } +size_t link_r_pos (const link_reader_t *r) { return r->off; } + +/* ---------- dispatch entry point ---------- */ + +int __dispatch_message(link_connection_t *conn, const struct link_msg *m) +{ + struct link_object *o; + struct link_vtable_entry *e = NULL; + const link_method_t *meth; + struct link_call call; + ssize_t blen; + int rc; + + if (m->type != LINK_MSG_METHOD_CALL) { + /* Signals and replies from a client to PID 1 are nonsense; + * silently drop. */ + return 0; + } + + if (!m->path || !m->member) { + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Method call without path or member"); + } + + /* Built-in DBus interfaces (Hello, Ping, Introspect, Properties) + * are handled here before object-tree lookup, which means they + * also run before the LINK_METHOD_PRIVILEGED authz gate further + * down. The current set is read-only; do NOT introduce a + * state-changing built-in without first adding equivalent + * authorisation inside __handle_builtin. */ + rc = __handle_builtin(conn, m); + if (rc >= 0) + return rc; /* 0 = handled OK, 1 = built-in but failed; <0 = not a built-in */ + + o = find_object(conn->server, m->path); + if (!o) { + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownObject", + "No such object"); + } + + meth = resolve(o, m->interface, m->member, &e); + if (!meth) { + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownMethod", + "No such method on this object"); + } + + /* Validate signature: client must match the declared in_sig. */ + { + const char *got = m->signature ? m->signature : ""; + const char *want = meth->in_sig ? meth->in_sig : ""; + + if (strcmp(got, want) != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Argument signature mismatch"); + } + + /* Per-method authorization. PRIVILEGED methods require uid 0; + * the peer's uid was captured via SO_PEERCRED at accept time + * and verified against the AUTH EXTERNAL claim, so we can trust + * conn->peer_uid here. */ + if ((meth->flags & LINK_METHOD_PRIVILEGED) && conn->peer_uid != 0) { + return __send_error(conn, m, + "org.freedesktop.DBus.Error.AccessDenied", + "Method requires root privileges"); + } + + memset(&call, 0, sizeof(call)); + call.conn = conn; + call.incoming = *m; + __r_init(&call.read_cursor, m->body, m->body_avail); + + rc = meth->handler(&call, e->userdata); + if (rc < 0 && !call.reply_consumed && !call.error_sent) { + /* Handler returned an error without sending one. */ + __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Handler failed"); + return 0; + } + + if (!call.reply_consumed && !call.error_sent) { + /* Handler returned 0 but never produced a reply; treat as + * empty reply with out_sig "". */ + __send_method_return(conn, m, NULL, NULL, 0); + return 0; + } + + if (call.reply_consumed && !call.error_sent) { + blen = __w_finish(&call.reply_writer); + if (blen < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply marshalling overflow"); + return __send_method_return(conn, m, meth->out_sig, + conn->txbuf, (size_t)blen); + } + + return 0; +} diff --git a/libink/internal.h b/libink/internal.h new file mode 100644 index 00000000..ac5b7153 --- /dev/null +++ b/libink/internal.h @@ -0,0 +1,142 @@ +/* libink internal types — not for external consumers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_INTERNAL_H_ +#define LIBINK_INTERNAL_H_ + +#include +#include + +#include "link.h" +#include "marshal.h" +#include "proto.h" + +typedef enum { + LINK_AUTH_NUL = 0, + LINK_AUTH_LINE, + LINK_AUTH_DONE, + LINK_AUTH_FAILED, +} link_auth_state_t; + +#define LINK_PATH_MAX 108 +#define LINK_AUTH_LINEBUF_SIZE 256 +#define LINK_RX_BUF_SIZE (64 * 1024) +#define LINK_TX_BUF_SIZE (16 * 1024) +#define LINK_UNIQUE_NAME_LEN 16 +#define LINK_MATCH_RULE_MAX 256 /* per-peer match rule cap */ +#define LINK_MATCH_PEER_CAP 16 /* max active match rules per peer */ + +/* Per-vtable record attached to an object's interface list. */ +struct link_vtable_entry { + const link_vtable_t *vt; + void *userdata; + TAILQ_ENTRY(link_vtable_entry) link; +}; + +TAILQ_HEAD(link_vtable_list, link_vtable_entry); + +/* An object exposed at one path. */ +struct link_object { + char path[LINK_PATH_MAX]; + struct link_vtable_list vtables; + TAILQ_ENTRY(link_object) link; +}; + +TAILQ_HEAD(link_object_list, link_object); + +struct link_server { + int fd; + char path[LINK_PATH_MAX]; + struct link_object_list objects; + uint32_t next_unique_id; /* for ":1.N" names */ +}; + +/* The reply being assembled inside a method handler. + * + * The reply body lives in conn->txbuf, not on this struct, so a + * stack-allocated link_call (in dispatch) stays small. Sharing the + * connection's txbuf is safe: the event loop is single-threaded and + * a connection only ever has one in-flight method call at a time. */ +struct link_call { + link_connection_t *conn; + struct link_msg incoming; + struct link_reader read_cursor; + struct link_writer reply_writer; /* writes into conn->txbuf */ + int reply_consumed; + int error_sent; +}; + +/* A parsed AddMatch rule. Fields are NULL when the rule omits the + * key, meaning "match anything"; non-NULL means "must equal". */ +struct link_match { + char *raw; /* original string, for RemoveMatch */ + char *type; /* "signal", or NULL */ + char *interface; + char *member; + char *path; +}; + +struct link_connection { + int fd; + uid_t peer_uid; + + char guid[33]; + char unique_name[LINK_UNIQUE_NAME_LEN]; /* ":1.N" */ + + link_auth_state_t auth; + char linebuf[LINK_AUTH_LINEBUF_SIZE]; + size_t linelen; + + /* Match rules registered via org.freedesktop.DBus.AddMatch. + * Bounded for PID 1 hygiene; a peer that exceeds the cap gets + * a LimitsExceeded error reply. */ + struct link_match *matches[LINK_MATCH_PEER_CAP]; + size_t matches_count; + + uint8_t rxbuf[LINK_RX_BUF_SIZE]; + size_t rxlen; + + /* Scratch for outgoing reply bodies. Shared by the dispatch + * path (writes through call.reply_writer) and built-in handlers + * (send_string_reply). Lifetime ends with each send_method_* + * call. */ + uint8_t txbuf[LINK_TX_BUF_SIZE]; + + uint32_t next_serial; + + struct link_server *server; /* back-pointer for dispatch */ +}; + +/* io.c — shared blocking I/O loops. See the TODO by send_all + * callers about future non-blocking + drop-on-EAGAIN hardening. */ +int __io_write_all(int fd, const void *buf, size_t len); +int __io_read_full(int fd, void *buf, size_t len); + +/* auth.c */ +int __auth_process(link_connection_t *conn); +void __auth_generate_guid(char out[33]); +int __auth_client(int fd, uid_t uid); + +/* dispatch.c */ +int __dispatch_message(link_connection_t *conn, const struct link_msg *m); +int __send_error(link_connection_t *conn, const struct link_msg *req, + const char *error_name, const char *text); +int __send_method_return(link_connection_t *conn, const struct link_msg *req, + const char *out_sig, + const uint8_t *body, size_t body_len); + +/* builtin.c */ +int __handle_builtin(link_connection_t *conn, const struct link_msg *m); + +/* match.c */ +struct link_match *__match_parse (const char *rule); +void __match_free (struct link_match *m); +int __match_matches(const struct link_match *m, + const char *path, const char *iface, + const char *member); +int __match_add (link_connection_t *conn, const char *rule); +int __match_remove (link_connection_t *conn, const char *rule); + +#endif /* LIBINK_INTERNAL_H_ */ diff --git a/libink/io.c b/libink/io.c new file mode 100644 index 00000000..cf29e651 --- /dev/null +++ b/libink/io.c @@ -0,0 +1,54 @@ +/* libink — shared blocking I/O helpers. + * + * Server send paths (libink/dispatch.c, libink/auth.c) and the + * client (libink/client.c) all need EINTR-resilient write_all / + * read_full. Consolidate them here so a future hardening change + * (e.g. non-blocking sends with drop-peer-on-EAGAIN, see the TODO + * by send_all callers) only has to land in one place. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "internal.h" + +int __io_write_all(int fd, const void *buf, size_t len) +{ + const char *p = buf; + + while (len > 0) { + ssize_t n = write(fd, p, len); + + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += n; + len -= (size_t)n; + } + return 0; +} + +int __io_read_full(int fd, void *buf, size_t len) +{ + char *p = buf; + + while (len > 0) { + ssize_t n = read(fd, p, len); + + if (n == 0) + return -1; + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += n; + len -= (size_t)n; + } + return 0; +} diff --git a/libink/libink.pc.in b/libink/libink.pc.in new file mode 100644 index 00000000..f84248de --- /dev/null +++ b/libink/libink.pc.in @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..11e4a2c4 --- /dev/null +++ b/libink/link.h @@ -0,0 +1,350 @@ +/* libink — brokerless D-Bus server library, born inside Finit + * + * Copyright (c) 2026 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef LIBINK_LINK_H_ +#define LIBINK_LINK_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct link_server link_server_t; +typedef struct link_connection link_connection_t; +typedef struct link_call link_call_t; +typedef struct link_client link_client_t; + +/* D-Bus message type codes -- see link_reply_t.type. */ +#define LINK_MSG_INVALID 0 +#define LINK_MSG_METHOD_CALL 1 +#define LINK_MSG_METHOD_RETURN 2 +#define LINK_MSG_ERROR 3 +#define LINK_MSG_SIGNAL 4 + +/* Writer is exposed so callers can stack-allocate one for marshalling + * signal/reply bodies. Treat the fields as opaque; use link_writer_init + * + the link_w_* helpers + link_writer_finish. Sized for typical D-Bus + * messages -- the array stack supports up to 8 levels of nesting. */ +#define LINK_WRITER_MAX_NESTING 8 +typedef struct link_writer { + uint8_t *buf; + size_t cap; + size_t off; + int err; + struct { + size_t lenpos; + size_t elemstart; + } arrays[LINK_WRITER_MAX_NESTING]; + size_t array_depth; +} link_writer_t; + +/* Reader is exposed so callers can stack-allocate one for decoding + * reply or signal bodies received from a peer. Treat fields as + * opaque; use link_reader_init + the link_r_* helpers. */ +typedef struct link_reader { + const uint8_t *base; + size_t off; + size_t cap; + int err; /* sticky */ +} link_reader_t; + +/* View of an inbound message (method-return, error, or signal), + * populated by link_client_call(_v) and link_client_wait(), and + * returned by link_client_reply(). All pointers reference internal + * client storage and are invalidated by the next call or wait on the + * same client, or by link_client_close(). `body` is NULL iff + * body_len==0; `error_name` is non-NULL only when type == LINK_MSG_ERROR; + * `path`/`interface`/`member` are non-NULL on signals. */ +typedef struct { + uint8_t type; /* LINK_MSG_METHOD_RETURN, _ERROR, or _SIGNAL */ + const char *signature; + const char *error_name; + const char *path; + const char *interface; + const char *member; + const uint8_t *body; + size_t body_len; +} link_reply_t; + +/* ---------- server / connection lifecycle ---------- */ + +int link_server_new (link_server_t **server, const char *path); +void link_server_free (link_server_t *server); +int link_server_get_fd(const link_server_t *server); + +int link_server_accept(link_server_t *server, link_connection_t **conn); + +/* Insert an externally-authenticated fd into the server's connection + * set. Used to integrate an outbound peer (e.g. a client-side + * handshake against an external dbus-daemon) so the same dispatch + + * signal-fan-out machinery covers it. `peer_uid` becomes what + * privileged-method checks see; pass (uid_t)-1 to make all + * LINK_METHOD_PRIVILEGED methods reject by default. + * + * On success the connection takes ownership of `fd`. On any failure + * `fd` is closed before the function returns NULL, so callers never + * have to track partial state. */ +link_connection_t *link_server_attach(link_server_t *server, int fd, uid_t peer_uid); + +int link_connection_get_fd (const link_connection_t *conn); +uid_t link_connection_get_uid (const link_connection_t *conn); +int link_connection_process (link_connection_t *conn); +void link_connection_close (link_connection_t *conn); + +/* ---------- object registration ---------- */ + +typedef int (*link_method_fn)(link_call_t *call, void *userdata); + +/* Method flags for link_method_t.flags */ +#define LINK_METHOD_PRIVILEGED (1u << 0) /* peer must be uid 0 (root) */ + +typedef struct { + const char *name; /* member name */ + const char *in_sig; /* input signature (D-Bus, e.g. "" or "s") */ + const char *out_sig; /* output signature */ + unsigned flags; /* OR of LINK_METHOD_* */ + link_method_fn handler; +} link_method_t; + +/* A read-only property descriptor. Set via the Properties.Set side + * is not yet implemented; only Get and GetAll are. The getter writes + * the property's value as a D-Bus variant (use link_w_variant_string + * for "s"-typed properties) into the provided writer. */ +typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); + +typedef struct { + const char *name; /* property name */ + const char *sig; /* D-Bus signature, e.g. "s" */ + link_property_getter_fn getter; +} link_property_t; + +typedef struct { + const char *interface; /* e.g. "org.finit.Manager1" */ + const link_method_t *methods; /* terminated by {NULL, ...}, or NULL */ + const link_property_t *properties; /* terminated by {NULL, ...}, or NULL */ +} link_vtable_t; + +/* Register one (interface, methods) at `path`. Calling repeatedly + * with the same path and different vtables adds more interfaces at + * that object. The vtable pointer must outlive the server (typically + * a static table). */ +int link_server_add_object(link_server_t *server, const char *path, + const link_vtable_t *vt, void *userdata); + +/* Remove every vtable registered at `path` and free the object. + * Returns 0 if the object existed, -1 (errno=ENOENT) otherwise. */ +int link_server_remove_object(link_server_t *server, const char *path); + +/* ---------- call accessors ---------- */ + +const char *link_call_path (const link_call_t *call); +const char *link_call_interface(const link_call_t *call); +const char *link_call_member (const link_call_t *call); +uid_t link_call_uid (const link_call_t *call); + +/* ---------- reading method-call arguments ---------- + * + * Cursor starts at the beginning of the request body. Each + * function returns 0 on success and advances the cursor; on + * failure it returns -1 and leaves the cursor in an error state + * (subsequent reads also fail). Strings reference the + * connection's rx buffer and are valid for the duration of the + * method handler. */ + +int link_call_read_byte (link_call_t *call, uint8_t *out); +int link_call_read_bool (link_call_t *call, int *out); +int link_call_read_u32 (link_call_t *call, uint32_t *out); +int link_call_read_string(link_call_t *call, const char **out); /* "s" */ +int link_call_read_path (link_call_t *call, const char **out); /* "o" */ + +/* ---------- reply construction ---------- */ + +/* Get the writer for the reply body, write args into it, return 0 + * from the handler. Dispatch finalizes and sends the reply with + * the out_sig declared on the vtable. May be called once per + * call. */ +link_writer_t *link_call_reply(link_call_t *call); + +/* Send a D-Bus error reply. `name` must be a valid D-Bus error + * name (e.g. "org.freedesktop.DBus.Error.UnknownMethod"); `message` + * may be NULL. */ +int link_call_reply_error(link_call_t *call, const char *name, const char *message); + +/* ---------- signal emission ---------- + * + * Send a signal to a single peer if its AddMatch rules accept it. + * Callers marshal the body separately and pass the resulting bytes. + * Returns 0 on success (or "filtered out, nothing sent"), -1 on + * underlying transport failure. */ +int link_connection_emit_signal(link_connection_t *conn, + const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len); + +/* ---------- client (outgoing method calls) ---------- + * + * Connect, authenticate as the current effective uid, send BEGIN. + * Returns NULL on any failure (caller can fall back to another + * transport if it has one). */ +link_client_t *link_client_open(const char *path); + +/* As link_client_open but applies SO_SNDTIMEO + SO_RCVTIMEO before + * the connect/AUTH handshake. After link_server_attach flips the fd + * to non-blocking the timeout is silently inert; it only protects + * the synchronous open path against a hung peer. timeout_ms == 0 + * disables the budget (same behaviour as link_client_open). */ +link_client_t *link_client_open_timeout(const char *path, int timeout_ms); + +void link_client_close(link_client_t *c); + +/* 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 + * integration) that want to promote an outbound client connection + * into a server-attached peer via link_server_attach(). */ +int link_client_steal_fd(link_client_t *c); + +/* Status codes returned by link_client_call(_v). */ +#define LINK_CALL_OK 0 /* method-return received */ +#define LINK_CALL_ERROR 1 /* server replied with an error */ +#define LINK_CALL_FAIL (-1) /* transport, parse, or invalid-arg failure */ + +/* Send a METHOD_CALL and read the reply synchronously. + * + * `signature` and `body`/`body_len` describe the outgoing body -- + * marshal it yourself with link_writer_init + the link_w_* helpers + * + link_writer_finish. Pass signature=NULL and body=NULL for + * methods that take no arguments. + * + * After the call, inspect the reply via link_client_reply() -- it + * exposes the body bytes (for callers that want to decode them with + * link_reader_init + link_r_*) and the error name on LINK_CALL_ERROR. + * The reply view is invalidated by the next call on the same client + * or by link_client_close(). */ +int link_client_call(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len); + +/* Convenience wrapper that marshals the outgoing body from varargs + * matching `signature`. Supported type codes (one per arg): + * 'y' -> int (promoted uint8_t) + * 'b' -> int (0/non-zero) + * 'u' -> uint32_t + * 's' -> const char * + * 'o' -> const char * (object path) + * + * Pass signature=NULL or "" for void calls. Return value matches + * link_client_call; an unsupported type code returns LINK_CALL_FAIL + * with no message sent. */ +int link_client_call_v(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, ...); + +const link_reply_t *link_client_reply(link_client_t *c); + +/* Convenience accessors for the common case where a reply carries + * exactly one string ("s" or "o") or one u32 ("u"). They wrap the + * link_reader_init + link_r_* pattern; on success return 0 and + * populate *out, on parse failure or missing body return -1. Use + * link_client_reply + link_reader_init directly for richer payloads. */ +int link_reply_get_string(const link_reply_t *r, const char **out); +int link_reply_get_u32 (const link_reply_t *r, uint32_t *out); + +/* Wait up to `timeout_ms` milliseconds for the next inbound message + * (typically a SIGNAL delivered after an AddMatch subscription), and + * populate the same view returned by link_client_reply(). + * timeout_ms < 0 : block forever + * timeout_ms == 0 : non-blocking (returns 1 immediately if no data) + * timeout_ms > 0 : wait that long + * Returns 0 on success, 1 on timeout, -1 on transport/parse error. + * + * Note: the timeout gates only the wait for the first byte of the + * next frame. Once data starts arriving the rest of the message is + * read blockingly; callers that need a hard upper bound should pass + * a positive timeout AND have a watchdog at a higher level. */ +int link_client_wait(link_client_t *c, int timeout_ms); + +/* ---------- standalone writer ---------- + * + * For marshalling bodies outside a method-call handler (signals, + * pre-computed replies). Initialise on a caller-owned buffer, + * write args via link_w_*, then call link_writer_finish which + * returns the body length or -1 on overflow. */ +void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap); +ssize_t link_writer_finish(link_writer_t *w); + +/* ---------- writer (mirrors the internal marshaller) ---------- */ + +void link_w_byte (link_writer_t *w, uint8_t v); +void link_w_bool (link_writer_t *w, int v); +void link_w_u32 (link_writer_t *w, uint32_t v); +void link_w_string (link_writer_t *w, const char *s); /* "s" */ +void link_w_path (link_writer_t *w, const char *s); /* "o" */ +void link_w_variant_string(link_writer_t *w, const char *s); /* "v" containing "s" */ +void link_w_array_begin (link_writer_t *w, char element_sig); +void link_w_array_end (link_writer_t *w); +void link_w_struct_begin(link_writer_t *w); +void link_w_struct_end (link_writer_t *w); + +/* ---------- standalone reader ---------- + * + * For decoding bodies received off the wire (reply or signal). + * Initialise on the body pointer + length, read via link_r_*, + * check link_r_done() to confirm everything was consumed. */ +void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len); +int link_r_byte (link_reader_t *r, uint8_t *out); +int link_r_bool (link_reader_t *r, int *out); +int link_r_u32 (link_reader_t *r, uint32_t *out); +int link_r_string (link_reader_t *r, const char **out); /* "s" */ +int link_r_path (link_reader_t *r, const char **out); /* "o" */ +int link_r_variant_string(link_reader_t *r, const char **out); /* "v" containing "s" */ +int link_r_align (link_reader_t *r, size_t n); /* skip to next n-byte boundary */ +int link_r_done (const link_reader_t *r); + +/* Begin reading an "a" array. On success returns 0 and sets + * *out_end to the absolute reader offset at which the array ends; + * caller loops while link_r_pos < *out_end. For dict-entry arrays + * ("a{T}") call link_r_align(r, 8) at the top of each iteration -- + * the element-alignment skip from the array prefix only covers the + * first entry. */ +int link_r_array_begin(link_reader_t *r, size_t *out_end); + +/* Byte offset of the next read inside the original body buffer. + * Use together with the *out_end returned by link_r_array_begin to + * walk the elements of an "a" payload. */ +size_t link_r_pos (const link_reader_t *r); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBINK_LINK_H_ */ diff --git a/libink/marshal.c b/libink/marshal.c new file mode 100644 index 00000000..ae215893 --- /dev/null +++ b/libink/marshal.c @@ -0,0 +1,321 @@ +/* libink — D-Bus body marshalling (writer side). + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "marshal.h" + +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +void __w_init(struct link_writer *w, uint8_t *buf, size_t cap) +{ + w->buf = buf; + w->cap = cap; + w->off = 0; + w->err = 0; + w->array_depth = 0; +} + +ssize_t __w_finish(struct link_writer *w) +{ + if (w->err || w->array_depth != 0) + return -1; + return (ssize_t)w->off; +} + +static int reserve(struct link_writer *w, size_t align, size_t bytes) +{ + size_t pad; + + if (w->err) + return -1; + + pad = ALIGN_UP(w->off, align) - w->off; + if (w->off + pad + bytes > w->cap) { + w->err = 1; + return -1; + } + while (pad-- > 0) + w->buf[w->off++] = 0; + return 0; +} + +static void put_u32_at(struct link_writer *w, size_t pos, uint32_t v) +{ + w->buf[pos] = (uint8_t)(v & 0xff); + w->buf[pos + 1] = (uint8_t)((v >> 8) & 0xff); + w->buf[pos + 2] = (uint8_t)((v >> 16) & 0xff); + w->buf[pos + 3] = (uint8_t)((v >> 24) & 0xff); +} + +static void put_u32(struct link_writer *w, uint32_t v) +{ + put_u32_at(w, w->off, v); + w->off += 4; +} + +void __w_byte(struct link_writer *w, uint8_t v) +{ + if (reserve(w, 1, 1) < 0) + return; + w->buf[w->off++] = v; +} + +void __w_bool(struct link_writer *w, int v) +{ + if (reserve(w, 4, 4) < 0) + return; + put_u32(w, v ? 1u : 0u); +} + +void __w_u32(struct link_writer *w, uint32_t v) +{ + if (reserve(w, 4, 4) < 0) + return; + put_u32(w, v); +} + +static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_len) +{ + size_t len = s ? strlen(s) : 0; + + if (onebyte_len) { + if (reserve(w, 1, 1 + len + 1) < 0) + return; + w->buf[w->off++] = (uint8_t)len; + } else { + if (reserve(w, 4, 4 + len + 1) < 0) + return; + put_u32(w, (uint32_t)len); + } + if (s && len) + memcpy(w->buf + w->off, s, len); + w->off += len; + w->buf[w->off++] = 0; +} + +void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void __w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void __w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); } + +/* Variant "v" containing a string. Wire form: + * 1-byte sig length (1), 's', NUL, then the string per __w_string. */ +void __w_variant_string(struct link_writer *w, const char *s) +{ + __w_sig (w, "s"); + __w_string(w, s); +} + +static size_t element_align(char c) +{ + switch (c) { + case 'y': case 'g': case 'v': return 1; + case 'n': case 'q': return 2; + case 'b': case 'i': case 'u': + case 's': case 'o': case 'h': case 'a': return 4; + case 'x': case 't': case 'd': + case '(': case '{': return 8; + default: return 1; + } +} + +void __w_array_begin(struct link_writer *w, char element_sig_first_char) +{ + size_t lenpos; + + if (w->err) + return; + if (w->array_depth >= LINK_WRITER_MAX_NESTING) { + w->err = 1; + return; + } + + if (reserve(w, 4, 4) < 0) + return; + lenpos = w->off; + put_u32(w, 0); /* placeholder */ + + /* Pad to the element's alignment. These pad bytes are NOT + * counted in the array length per the D-Bus spec. */ + if (reserve(w, element_align(element_sig_first_char), 0) < 0) + return; + + w->arrays[w->array_depth].lenpos = lenpos; + w->arrays[w->array_depth].elemstart = w->off; + w->array_depth++; +} + +void __w_array_end(struct link_writer *w) +{ + size_t elemstart, lenpos; + uint32_t actual; + + if (w->err || w->array_depth == 0) { + w->err = 1; + return; + } + w->array_depth--; + lenpos = w->arrays[w->array_depth].lenpos; + elemstart = w->arrays[w->array_depth].elemstart; + actual = (uint32_t)(w->off - elemstart); + put_u32_at(w, lenpos, actual); +} + +void __w_struct_begin(struct link_writer *w) +{ + reserve(w, 8, 0); +} + +void __w_struct_end(struct link_writer *w) +{ + (void)w; +} + +/* ---- reader ---- */ + +void __r_init(struct link_reader *r, const uint8_t *body, size_t len) +{ + r->base = body; + r->off = 0; + r->cap = len; + r->err = 0; +} + +static int r_skip_align(struct link_reader *r, size_t align) +{ + size_t pad; + + if (r->err) + return -1; + pad = ALIGN_UP(r->off, align) - r->off; + if (r->off + pad > r->cap) { + r->err = 1; + return -1; + } + r->off += pad; + return 0; +} + +static uint32_t rd_u32(const uint8_t *p) +{ + return (uint32_t)p[0] + | ((uint32_t)p[1] << 8) + | ((uint32_t)p[2] << 16) + | ((uint32_t)p[3] << 24); +} + +int __r_byte(struct link_reader *r, uint8_t *out) +{ + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) { + r->err = 1; + return -1; + } + *out = r->base[r->off++]; + return 0; +} + +int __r_u32(struct link_reader *r, uint32_t *out) +{ + if (r_skip_align(r, 4) < 0 || r->off + 4 > r->cap) { + r->err = 1; + return -1; + } + *out = rd_u32(r->base + r->off); + r->off += 4; + return 0; +} + +int __r_bool(struct link_reader *r, int *out) +{ + uint32_t v; + + if (__r_u32(r, &v) < 0) + return -1; + *out = v ? 1 : 0; + return 0; +} + +static int read_string_like(struct link_reader *r, const char **out) +{ + uint32_t len; + + if (__r_u32(r, &len) < 0) + return -1; + if (r->off + (size_t)len + 1 > r->cap) { + r->err = 1; + return -1; + } + /* Spec requires nul terminator at base[off + len]. */ + if (r->base[r->off + len] != 0) { + r->err = 1; + return -1; + } + *out = (const char *)(r->base + r->off); + r->off += (size_t)len + 1; + return 0; +} + +int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } +int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } + +/* Read a variant "v" expected to contain a string. Fails if the + * inner signature is anything other than "s" (returns -1, *out set + * to NULL). */ +int __r_variant_string(struct link_reader *r, const char **out) +{ + uint8_t sig_len; + uint32_t slen; + + *out = NULL; + + /* signature is "g" wire form: 1-byte length, bytes, NUL */ + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) { r->err = 1; return -1; } + sig_len = r->base[r->off++]; + if (sig_len != 1 || r->off + 2 > r->cap) { r->err = 1; return -1; } + if (r->base[r->off] != 's' || r->base[r->off + 1] != 0) { r->err = 1; return -1; } + r->off += 2; + + /* now a normal string */ + if (__r_u32(r, &slen) < 0) return -1; + if (r->off + (size_t)slen + 1 > r->cap || r->base[r->off + slen] != 0) { + r->err = 1; + return -1; + } + *out = (const char *)(r->base + r->off); + r->off += (size_t)slen + 1; + return 0; +} + +int __r_done(const struct link_reader *r) +{ + return !r->err && r->off == r->cap; +} + +int __r_align(struct link_reader *r, size_t n) +{ + return r_skip_align(r, n); +} + +/* Begin reading an "a" array. Reads the u32 byte-length prefix + * and sets *out_end to the absolute reader offset at which the array + * ends. Caller loops while r->off < *out_end. Returns -1 on a + * truncated or oversized array length. */ +int __r_array_begin(struct link_reader *r, size_t *out_end) +{ + uint32_t array_bytes; + size_t end; + + if (__r_u32(r, &array_bytes) < 0) + return -1; + end = r->off + (size_t)array_bytes; + if (end > r->cap) { + r->err = 1; + return -1; + } + *out_end = end; + return 0; +} diff --git a/libink/marshal.h b/libink/marshal.h new file mode 100644 index 00000000..313205b2 --- /dev/null +++ b/libink/marshal.h @@ -0,0 +1,52 @@ +/* libink — D-Bus body marshalling (writer side). + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_MARSHAL_H_ +#define LIBINK_MARSHAL_H_ + +#include +#include + +/* struct link_writer is defined in ink.h (public). Field layout is + * "opaque" per the public contract; this file's helpers manipulate + * the fields directly. */ +#include "link.h" + +void __w_init (struct link_writer *w, uint8_t *buf, size_t cap); +ssize_t __w_finish(struct link_writer *w); + +void __w_byte (struct link_writer *w, uint8_t v); +void __w_bool (struct link_writer *w, int v); +void __w_u32 (struct link_writer *w, uint32_t v); +void __w_string (struct link_writer *w, const char *s); /* "s" */ +void __w_path (struct link_writer *w, const char *s); /* "o" */ +void __w_sig (struct link_writer *w, const char *s); /* "g" */ +void __w_variant_string(struct link_writer *w, const char *s); /* "v" containing "s" */ + +/* element_sig_first_char drives the alignment padding inserted + * between the array length prefix and the first element. */ +void __w_array_begin (struct link_writer *w, char element_sig_first_char); +void __w_array_end (struct link_writer *w); + +void __w_struct_begin(struct link_writer *w); +void __w_struct_end (struct link_writer *w); + +/* ---- reader ---- + * + * Reads from a message body pointer + length, advancing a cursor. + * struct link_reader is defined in link.h (public, opaque); the + * helpers here manipulate the fields directly. */ +void __r_init (struct link_reader *r, const uint8_t *body, size_t len); +int __r_byte (struct link_reader *r, uint8_t *out); +int __r_bool (struct link_reader *r, int *out); +int __r_u32 (struct link_reader *r, uint32_t *out); +int __r_string(struct link_reader *r, const char **out); /* "s" */ +int __r_path (struct link_reader *r, const char **out); /* "o" */ +int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ +int __r_align (struct link_reader *r, size_t n); /* skip to n-byte boundary */ +int __r_array_begin(struct link_reader *r, size_t *out_end); +int __r_done (const struct link_reader *r); + +#endif /* LIBINK_MARSHAL_H_ */ diff --git a/libink/match.c b/libink/match.c new file mode 100644 index 00000000..add721b4 --- /dev/null +++ b/libink/match.c @@ -0,0 +1,199 @@ +/* libink — D-Bus AddMatch / RemoveMatch rule parsing and matching. + * + * Subset of the spec: type, interface, member, path. Each entry is + * a key='value' pair with single-quoted value, separated by commas. + * Backslash escapes inside values (\\ and \') are not interpreted — + * a peer needing them will get unexpected literal content. Unknown + * keys cause the whole rule to be rejected so a peer learns its + * filter didn't take, rather than silently receiving everything. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include "internal.h" + +static char *dup_range(const char *p, size_t n) +{ + char *s = malloc(n + 1); + + if (!s) + return NULL; + memcpy(s, p, n); + s[n] = '\0'; + return s; +} + +/* Parse one key='value' entry starting at *p. On success advances + * *p past the trailing quote and any comma, returns 0. On malformed + * input, returns -1. */ +static int parse_kv(const char **p, char **out_key, char **out_value) +{ + const char *q = *p; + const char *key_start, *val_start; + + while (*q == ' ' || *q == '\t') + q++; + key_start = q; + while ((*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z') || *q == '_') + q++; + if (q == key_start || *q != '=') + return -1; + *out_key = dup_range(key_start, (size_t)(q - key_start)); + if (!*out_key) + return -1; + q++; + + if (*q != '\'') { + free(*out_key); + return -1; + } + q++; + val_start = q; + while (*q && *q != '\'') + q++; + if (*q != '\'') { + free(*out_key); + return -1; + } + *out_value = dup_range(val_start, (size_t)(q - val_start)); + if (!*out_value) { + free(*out_key); + return -1; + } + q++; + + while (*q == ' ' || *q == '\t' || *q == ',') + q++; + *p = q; + return 0; +} + +struct link_match *__match_parse(const char *rule) +{ + struct link_match *m; + const char *p; + + if (!rule || strlen(rule) >= LINK_MATCH_RULE_MAX) { + errno = EINVAL; + return NULL; + } + + m = calloc(1, sizeof(*m)); + if (!m) + return NULL; + m->raw = strdup(rule); + if (!m->raw) { + free(m); + return NULL; + } + + for (p = rule; *p; ) { + char *key = NULL, *value = NULL; + char **slot = NULL; + + if (parse_kv(&p, &key, &value) < 0) + goto bad; + + if (!strcmp(key, "type")) slot = &m->type; + else if (!strcmp(key, "interface")) slot = &m->interface; + else if (!strcmp(key, "member")) slot = &m->member; + else if (!strcmp(key, "path")) slot = &m->path; + else { + free(key); + free(value); + goto bad; + } + + if (*slot) { + /* Duplicate key. */ + free(key); + free(value); + goto bad; + } + *slot = value; + free(key); + } + + /* No need to default m->type: when it's NULL the matcher below + * treats it as "match any", and the only thing libink emits via + * the match table is signals, so the effective filter is + * already "signal" without the explicit assignment. */ + return m; + +bad: + __match_free(m); + errno = EINVAL; + return NULL; +} + +void __match_free(struct link_match *m) +{ + if (!m) + return; + free(m->raw); + free(m->type); + free(m->interface); + free(m->member); + free(m->path); + free(m); +} + +static int field_matches(const char *want, const char *got) +{ + if (!want) + return 1; /* no filter on this field */ + if (!got) + return 0; + return strcmp(want, got) == 0; +} + +int __match_matches(const struct link_match *m, + const char *path, const char *iface, + const char *member) +{ + /* Type filter: only signals get delivered through this path. */ + if (m->type && strcmp(m->type, "signal") != 0) + return 0; + + return field_matches(m->path, path) + && field_matches(m->interface, iface) + && field_matches(m->member, member); +} + +int __match_add(link_connection_t *conn, const char *rule) +{ + struct link_match *m; + + if (conn->matches_count >= LINK_MATCH_PEER_CAP) { + errno = ENOSPC; + return -1; + } + + m = __match_parse(rule); + if (!m) + return -1; + + conn->matches[conn->matches_count++] = m; + return 0; +} + +int __match_remove(link_connection_t *conn, const char *rule) +{ + size_t i; + + for (i = 0; i < conn->matches_count; i++) { + if (strcmp(conn->matches[i]->raw, rule) == 0) { + __match_free(conn->matches[i]); + conn->matches[i] = conn->matches[conn->matches_count - 1]; + conn->matches_count--; + return 0; + } + } + errno = ENOENT; + return -1; +} diff --git a/libink/path.c b/libink/path.c new file mode 100644 index 00000000..80f97963 --- /dev/null +++ b/libink/path.c @@ -0,0 +1,43 @@ +/* libink — D-Bus object-path encoding for arbitrary identifiers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include + +#include "path.h" + +static int is_safe(unsigned char c) +{ + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9'); +} + +int link_path_encode(const char *in, char *out, size_t outsz) +{ + static const char hex[] = "0123456789abcdef"; + size_t off = 0; + + if (!in || !out || outsz == 0) + return -1; + + for (; *in; in++) { + unsigned char c = (unsigned char)*in; + + if (is_safe(c)) { + if (off + 1 >= outsz) + return -1; + out[off++] = (char)c; + } else { + if (off + 3 >= outsz) + return -1; + out[off++] = '_'; + out[off++] = hex[c >> 4]; + out[off++] = hex[c & 0xf]; + } + } + out[off] = '\0'; + return (int)off; +} diff --git a/libink/path.h b/libink/path.h new file mode 100644 index 00000000..784c634e --- /dev/null +++ b/libink/path.h @@ -0,0 +1,21 @@ +/* libink — D-Bus object-path encoding for arbitrary identifiers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_PATH_H_ +#define LIBINK_PATH_H_ + +#include + +/* Encode `in` as a D-Bus path segment. Bytes in [A-Za-z0-9] pass + * through unchanged; everything else (including '_' itself) becomes + * "_HH" with lowercase hex, mirroring systemd's escape_path. + * + * Returns the encoded length (excluding the terminating nul), or -1 + * if the output buffer cannot fit the result. `outsz` must + * accommodate the encoded bytes plus a trailing nul; the worst-case + * size for an N-byte input is 3*N + 1. */ +int link_path_encode(const char *in, char *out, size_t outsz); + +#endif /* LIBINK_PATH_H_ */ diff --git a/libink/proto.c b/libink/proto.c new file mode 100644 index 00000000..d9bf712e --- /dev/null +++ b/libink/proto.c @@ -0,0 +1,387 @@ +/* libink — D-Bus wire protocol: message header parsing and building. + * + * Implements the binary message header format described in the + * D-Bus specification, sections "Message Format" and "Header Fields". + * Bodies are deliberately not parsed here — that's the marshaller's + * job (marshal.c). + * + * Native byte order is assumed to be little-endian; messages with the + * 'B' endianness flag are rejected for now (every conforming client + * on the platforms Finit targets sends 'l'). + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include "proto.h" + +#define HDR_FIXED_SIZE 16 +#define MAX_MSG_SIZE (128 * 1024) /* sanity limit for PID 1 */ +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +static inline uint32_t rd_u32(const uint8_t *p) +{ + return (uint32_t)p[0] + | ((uint32_t)p[1] << 8) + | ((uint32_t)p[2] << 16) + | ((uint32_t)p[3] << 24); +} + +static inline void wr_u32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v & 0xff); + p[1] = (uint8_t)((v >> 8) & 0xff); + p[2] = (uint8_t)((v >> 16) & 0xff); + p[3] = (uint8_t)((v >> 24) & 0xff); +} + +/* Parse a (length-prefixed, nul-terminated) DBus STRING or PATH from + * the header field array. Returns a pointer into buf or NULL on + * malformed input. *consumed receives the bytes used including the + * nul. */ +static const char *parse_string(const uint8_t *buf, size_t avail, size_t *consumed) +{ + uint32_t len; + + if (avail < 4) + return NULL; + len = rd_u32(buf); + if (len >= avail - 4) /* need room for len bytes + nul */ + return NULL; + if (buf[4 + len] != 0) + return NULL; + *consumed = 4 + len + 1; + return (const char *)(buf + 4); +} + +/* Parse a SIGNATURE (1-byte length, nul-terminated). */ +static const char *parse_signature(const uint8_t *buf, size_t avail, size_t *consumed) +{ + uint32_t len; + + if (avail < 1) + return NULL; + len = buf[0]; + if (len + 2 > avail) + return NULL; + if (buf[1 + len] != 0) + return NULL; + *consumed = 1 + len + 1; + return (const char *)(buf + 1); +} + +ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out) +{ + uint32_t fields_len, total_hdr, body_off, total; + const uint8_t *fp, *fend; + + memset(out, 0, sizeof(*out)); + + if (len < HDR_FIXED_SIZE) + return 0; + + if (buf[0] != 'l') { + errno = EPROTO; + return -1; + } + if (buf[3] != LINK_PROTOCOL_VERSION) { + errno = EPROTONOSUPPORT; + return -1; + } + out->endian = buf[0]; + out->type = buf[1]; + out->flags = buf[2]; + out->body_len = rd_u32(buf + 4); + out->serial = rd_u32(buf + 8); + fields_len = rd_u32(buf + 12); + + if (fields_len > MAX_MSG_SIZE || out->body_len > MAX_MSG_SIZE) { + errno = E2BIG; + return -1; + } + + total_hdr = HDR_FIXED_SIZE + fields_len; + body_off = (uint32_t)ALIGN_UP(total_hdr, 8); + total = body_off + out->body_len; + + if (len < total) + return 0; /* need more bytes */ + + /* Walk the array of (byte field-code, variant). */ + fp = buf + HDR_FIXED_SIZE; + fend = fp + fields_len; + while (fp < fend) { + uint8_t code; + const char *vsig; + size_t used; + + fp = buf + ALIGN_UP((size_t)(fp - buf), 8); + if (fp >= fend) + break; + + code = *fp++; + vsig = parse_signature(fp, (size_t)(fend - fp), &used); + if (!vsig) { + errno = EPROTO; + return -1; + } + fp += used; + + if (vsig[0] == 's' || vsig[0] == 'o') { + fp = buf + ALIGN_UP((size_t)(fp - buf), 4); + if (fp >= fend) { errno = EPROTO; return -1; } + const char *s = parse_string(fp, (size_t)(fend - fp), &used); + if (!s) { errno = EPROTO; return -1; } + switch (code) { + case LINK_HDR_PATH: out->path = s; break; + case LINK_HDR_INTERFACE: out->interface = s; break; + case LINK_HDR_MEMBER: out->member = s; break; + case LINK_HDR_ERROR_NAME: out->error_name = s; break; + case LINK_HDR_DESTINATION: out->destination = s; break; + case LINK_HDR_SENDER: out->sender = s; break; + } + fp += used; + } else if (vsig[0] == 'g') { + const char *s = parse_signature(fp, (size_t)(fend - fp), &used); + if (!s) { errno = EPROTO; return -1; } + if (code == LINK_HDR_SIGNATURE) + out->signature = s; + fp += used; + } else if (vsig[0] == 'u') { + fp = buf + ALIGN_UP((size_t)(fp - buf), 4); + if (fp + 4 > fend) { errno = EPROTO; return -1; } + uint32_t v = rd_u32(fp); + if (code == LINK_HDR_REPLY_SERIAL) + out->reply_serial = v; + fp += 4; + } else { + /* Unknown field type — skip whole message. */ + errno = EPROTO; + return -1; + } + } + + out->body = buf + body_off; + out->body_avail = out->body_len; + return (ssize_t)total; +} + +/* ---------- builders ---------- */ + +/* Append a (byte field-code, variant) entry to a header-fields array, + * with the entry pre-aligned to 8 bytes. */ +static int put_field_string(uint8_t *buf, size_t cap, size_t *off, + uint8_t code, char vsig_char, + const char *value) +{ + size_t o = *off; + size_t pad = ALIGN_UP(o, 8) - o; + size_t len = strlen(value); + + /* Padding for struct alignment */ + while (pad-- > 0) { + if (o >= cap) return -1; + buf[o++] = 0; + } + + /* code, variant signature (1B len + 1B char + 1B nul) */ + if (o + 4 > cap) return -1; + buf[o++] = code; + buf[o++] = 1; + buf[o++] = (uint8_t)vsig_char; + buf[o++] = 0; + + if (vsig_char == 's' || vsig_char == 'o') { + /* 4-byte align for u32 length */ + while (o & 3) { + if (o >= cap) return -1; + buf[o++] = 0; + } + if (o + 4 + len + 1 > cap) return -1; + wr_u32(buf + o, (uint32_t)len); + o += 4; + memcpy(buf + o, value, len); + o += len; + buf[o++] = 0; + } else if (vsig_char == 'g') { + if (o + 1 + len + 1 > cap) return -1; + buf[o++] = (uint8_t)len; + memcpy(buf + o, value, len); + o += len; + buf[o++] = 0; + } else { + return -1; + } + + *off = o; + return 0; +} + +static int put_field_u32(uint8_t *buf, size_t cap, size_t *off, + uint8_t code, uint32_t value) +{ + size_t o = *off; + size_t pad = ALIGN_UP(o, 8) - o; + + while (pad-- > 0) { + if (o >= cap) return -1; + buf[o++] = 0; + } + if (o + 8 > cap) return -1; + buf[o++] = code; + buf[o++] = 1; + buf[o++] = 'u'; + buf[o++] = 0; + while (o & 3) { + if (o >= cap) return -1; + buf[o++] = 0; + } + if (o + 4 > cap) return -1; + wr_u32(buf + o, value); + o += 4; + *off = o; + return 0; +} + +static ssize_t finalize_header(uint8_t *buf, size_t cap, + uint8_t type, uint8_t flags, + uint32_t body_len, uint32_t serial, + size_t fields_end) +{ + size_t hdr_end = fields_end; + size_t pad = ALIGN_UP(hdr_end, 8) - hdr_end; + + buf[0] = 'l'; + buf[1] = type; + buf[2] = flags; + buf[3] = LINK_PROTOCOL_VERSION; + wr_u32(buf + 4, body_len); + wr_u32(buf + 8, serial); + wr_u32(buf + 12, (uint32_t)(hdr_end - HDR_FIXED_SIZE)); + + while (pad-- > 0) { + if (hdr_end >= cap) return -1; + buf[hdr_end++] = 0; + } + return (ssize_t)hdr_end; +} + +ssize_t __msg_build_return(uint8_t *buf, size_t cap, + uint32_t serial, uint32_t reply_serial, + const char *destination, + const char *signature, uint32_t body_len) +{ + size_t off = HDR_FIXED_SIZE; + + if (cap < HDR_FIXED_SIZE) + return -1; + + if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 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; + + return finalize_header(buf, cap, LINK_MSG_METHOD_RETURN, + LINK_FLAG_NO_REPLY_EXPECTED, + body_len, serial, off); +} + +ssize_t __msg_build_error(uint8_t *buf, size_t cap, + uint32_t serial, uint32_t reply_serial, + const char *destination, + const char *error_name, + const char *signature, uint32_t body_len) +{ + size_t off = HDR_FIXED_SIZE; + + if (cap < HDR_FIXED_SIZE || !error_name) + return -1; + + if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0) + return -1; + if (put_field_string(buf, cap, &off, LINK_HDR_ERROR_NAME, 's', error_name) < 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; + + return finalize_header(buf, cap, LINK_MSG_ERROR, + LINK_FLAG_NO_REPLY_EXPECTED, + body_len, serial, off); +} + +ssize_t __msg_build_signal(uint8_t *buf, size_t cap, + uint32_t serial, + const char *path, + const char *interface, + const char *member, + const char *signature, uint32_t body_len) +{ + size_t off = HDR_FIXED_SIZE; + + if (cap < HDR_FIXED_SIZE || !path || !interface || !member) + return -1; + + if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0) + return -1; + if (put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0) + return -1; + if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0) + return -1; + if (signature && *signature && + put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) + return -1; + + return finalize_header(buf, cap, LINK_MSG_SIGNAL, + LINK_FLAG_NO_REPLY_EXPECTED, + body_len, serial, off); +} + +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 *signature, + uint32_t body_len) +{ + size_t off = HDR_FIXED_SIZE; + + if (cap < HDR_FIXED_SIZE || !path || !member) + return -1; + + if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0) + return -1; + if (interface && + put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0) + return -1; + if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0) + return -1; + if (signature && *signature && + put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) + return -1; + + return finalize_header(buf, cap, LINK_MSG_METHOD_CALL, + /* flags=0: we expect a reply */ + 0, + body_len, serial, off); +} + +size_t __msg_header_size(const struct link_msg *m) +{ + (void)m; + /* Generous upper bound used by callers to size send buffers. */ + return 512; +} diff --git a/libink/proto.h b/libink/proto.h new file mode 100644 index 00000000..f7ef0866 --- /dev/null +++ b/libink/proto.h @@ -0,0 +1,101 @@ +/* libink — D-Bus wire protocol: message header parsing and building. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_PROTO_H_ +#define LIBINK_PROTO_H_ + +#include +#include +#include + +#include "link.h" /* LINK_MSG_* type codes */ + +/* Message flags. */ +#define LINK_FLAG_NO_REPLY_EXPECTED 0x01 +#define LINK_FLAG_NO_AUTO_START 0x02 +#define LINK_FLAG_ALLOW_INTERACTIVE_AUTHORIZATION 0x04 + +/* Header field codes. */ +#define LINK_HDR_PATH 1 +#define LINK_HDR_INTERFACE 2 +#define LINK_HDR_MEMBER 3 +#define LINK_HDR_ERROR_NAME 4 +#define LINK_HDR_REPLY_SERIAL 5 +#define LINK_HDR_DESTINATION 6 +#define LINK_HDR_SENDER 7 +#define LINK_HDR_SIGNATURE 8 +#define LINK_HDR_UNIX_FDS 9 + +#define LINK_PROTOCOL_VERSION 1 + +/* Parsed view of an incoming message. Pointers reference bytes + * inside the receiver's own rx buffer; treat as borrowed and short- + * lived (until the next read of the same connection). */ +struct link_msg { + uint8_t type; + uint8_t flags; + uint8_t endian; /* 'l' or 'B' */ + uint32_t body_len; + uint32_t serial; + uint32_t reply_serial; + + const char *path; /* object path, or NULL */ + const char *interface; /* may be NULL on method calls */ + const char *member; + const char *error_name; + const char *destination; + const char *sender; + const char *signature; /* may be NULL if body is empty */ + + /* Pointer into the rx buffer and length, after header padding. */ + const uint8_t *body; + uint32_t body_avail; +}; + +/* Parse a complete D-Bus message from `buf` of size `len`. On + * success returns the total number of bytes consumed (header + + * padding + body) and fills *out. Returns 0 if more bytes are + * needed, -1 on malformed input. */ +ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out); + +/* Compute the on-wire size of a future message header given the + * fields we'd populate. Used to size send buffers. */ +size_t __msg_header_size(const struct link_msg *m); + +/* Build a method-return header into `buf` (capacity `cap`). + * `reply_serial`/`destination` come from the call being replied to. + * `signature` is the body signature ("" if no args). `body_len` + * is the length of the body that will follow the header padding. + * Returns the number of bytes written, or -1 on overflow. */ +ssize_t __msg_build_return(uint8_t *buf, size_t cap, + uint32_t serial, uint32_t reply_serial, + const char *destination, + const char *signature, uint32_t body_len); + +/* Build an error reply header. */ +ssize_t __msg_build_error(uint8_t *buf, size_t cap, + uint32_t serial, uint32_t reply_serial, + const char *destination, + const char *error_name, + const char *signature, uint32_t body_len); + +/* Build a signal header (no reply expected, no destination). */ +ssize_t __msg_build_signal(uint8_t *buf, size_t cap, + uint32_t serial, + const char *path, + const char *interface, + const char *member, + const char *signature, uint32_t body_len); + +/* Build a method-call header (client side). */ +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 *signature, + uint32_t body_len); + +#endif /* LIBINK_PROTO_H_ */ diff --git a/libink/server.c b/libink/server.c new file mode 100644 index 00000000..f5a82431 --- /dev/null +++ b/libink/server.c @@ -0,0 +1,204 @@ +/* libink — listening socket, accept, peer-credential capture + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +static void close_save_errno(int fd) +{ + int saved = errno; + close(fd); + errno = saved; +} + +int link_server_new(link_server_t **out, const char *path) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + link_server_t *srv; + size_t plen; + int fd; + + if (!out || !path || !*path) { + errno = EINVAL; + return -1; + } + + plen = strlen(path); + if (plen >= sizeof(sun.sun_path) || plen >= LINK_PATH_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + srv = calloc(1, sizeof(*srv)); + if (!srv) + return -1; + TAILQ_INIT(&srv->objects); + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) + goto err_free; + + memcpy(sun.sun_path, path, plen + 1); + + (void)unlink(path); + + /* fchmod() on a Unix-domain socket fd is a silent no-op on Linux: + * the file mode is fixed at bind() time as (0777 & ~umask). Set + * umask around the bind() so the socket appears with mode 0666 + * atomically, no race window. World-accessible by design; + * per-method authorization happens later in dispatch via + * SO_PEERCRED. */ + { + mode_t oldmask = umask(0111); + int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun)); + int saved = errno; + + umask(oldmask); + if (rc < 0) { + errno = saved; + goto err_close; + } + } + + if (listen(fd, 16) < 0) + goto err_unlink; + + srv->fd = fd; + memcpy(srv->path, path, plen + 1); + *out = srv; + return 0; + +err_unlink: + (void)unlink(path); +err_close: + close_save_errno(fd); +err_free: + free(srv); + return -1; +} + +void link_server_free(link_server_t *srv) +{ + struct link_object *o; + + if (!srv) + return; + + o = TAILQ_FIRST(&srv->objects); + while (o) { + struct link_object *next_o = TAILQ_NEXT(o, link); + struct link_vtable_entry *e = TAILQ_FIRST(&o->vtables); + + while (e) { + struct link_vtable_entry *next_e = TAILQ_NEXT(e, link); + + free(e); + e = next_e; + } + free(o); + o = next_o; + } + + if (srv->fd >= 0) + close(srv->fd); + if (srv->path[0]) + (void)unlink(srv->path); + free(srv); +} + +int link_server_get_fd(const link_server_t *srv) +{ + if (!srv) + return -1; + + return srv->fd; +} + +int link_server_accept(link_server_t *srv, link_connection_t **out) +{ + struct ucred cred = { 0 }; + socklen_t credlen = sizeof(cred); + link_connection_t *conn; + int cfd; + + if (!srv || !out) { + errno = EINVAL; + return -1; + } + + cfd = accept4(srv->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (cfd < 0) + return -1; + + conn = calloc(1, sizeof(*conn)); + if (!conn) { + close_save_errno(cfd); + return -1; + } + + conn->fd = cfd; + conn->auth = LINK_AUTH_NUL; + conn->server = srv; + + if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0) + conn->peer_uid = cred.uid; + else + conn->peer_uid = (uid_t)-1; + + __auth_generate_guid(conn->guid); + + *out = conn; + return 0; +} + +link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid) +{ + link_connection_t *conn; + int flags; + + /* On entry we always own `fd` -- close it on every failure path + * so callers don't have to track whether we touched fcntl state. */ + if (!srv || fd < 0) { + if (fd >= 0) + close_save_errno(fd); + errno = EINVAL; + return NULL; + } + + /* Match server_accept's fd setup: CLOEXEC first (so a fork-and- + * exec between the two calls cannot leak the fd), then NONBLOCK + * so process_binary's read loop can drain without hanging. */ + flags = fcntl(fd, F_GETFD, 0); + if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + goto err_close; + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + goto err_close; + + conn = calloc(1, sizeof(*conn)); + if (!conn) + goto err_close; + + conn->fd = fd; + conn->auth = LINK_AUTH_DONE; /* caller already handshook */ + conn->server = srv; + conn->peer_uid = peer_uid; + __auth_generate_guid(conn->guid); + + return conn; + +err_close: + close_save_errno(fd); + return NULL; +} diff --git a/mkdocs.yml b/mkdocs.yml index 8d02f735..d07fdb9a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,6 +90,7 @@ nav: - Plugins: plugins.md - Watchdog: watchdog.md - keventd: keventd.md + - D-Bus Integration: dbus.md - Service State Machine: state-machine.md - Distributions: distro.md - Requirements: requirements.md diff --git a/src/Makefile.am b/src/Makefile.am index bd126600..73125001 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -80,6 +80,9 @@ finit_SOURCES = api.c cgroup.c cgroup.h \ if LOGROTATE finit_SOURCES += logrotate.c endif +if DBUS +finit_SOURCES += dbus.c +endif pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \ plugin.h svc.h service.h @@ -93,6 +96,10 @@ finit_LDADD += ../plugins/libplug.la else finit_LDADD += -ldl endif +if DBUS +finit_CPPFLAGS += -I$(top_srcdir)/libink +finit_LDADD += $(top_builddir)/libink/libink.la +endif initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \ client.c client.h cond.c cond.h reboot.c \ @@ -100,6 +107,10 @@ initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \ initctl_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 initctl_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS) initctl_LDADD = $(lite_LIBS) $(uev_LIBS) +if DBUS +initctl_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/libink +initctl_LDADD += $(top_builddir)/libink/libink.la +endif INIT_LNKS = init telinit REBOOT_LNKS = reboot shutdown halt poweroff suspend diff --git a/src/api.c b/src/api.c index a0af4542..be14e215 100644 --- a/src/api.c +++ b/src/api.c @@ -112,41 +112,8 @@ static int restart(svc_t *svc, void *user_data) static int reload(svc_t *svc, void *user_data) { - char cond[MAX_COND_LEN]; - (void)user_data; - - if (!svc) - return 1; - - if (svc_is_blocked(svc)) - svc_start(svc); - else - service_timeout_cancel(svc); - - /* - * Clear conditions before reload to ensure dependent services - * are properly updated. Only needed when the service does NOT - * support SIGHUP (noreload), because then it will be stopped - * and restarted, so conditions genuinely go away. When the - * service handles SIGHUP, its PID and pidfile persist, so the - * condition stays valid and dependents should not be disrupted. - * - * Note: only clear 'ready' for services where the pidfile - * inotify handler reasserts it (pid/none). For s6/systemd - * services readiness relies on their respective notification - * mechanism which may not re-trigger on SIGHUP. - */ - if (svc_is_noreload(svc)) { - cond_clear(mkcond(svc, cond, sizeof(cond))); - if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) - service_ready(svc, 0); - } - - svc_mark_dirty(svc); - service_step(svc); - - return 0; + return service_reload(svc); } static int do_stop (char *buf, size_t len) { return call(stop, buf, len); } diff --git a/src/cond-w.c b/src/cond-w.c index 4ec119d2..d4489494 100644 --- a/src/cond-w.c +++ b/src/cond-w.c @@ -37,6 +37,13 @@ #include "service.h" #include "sm.h" +#ifdef HAVE_DBUS +/* Forward-declared locally to keep cond-w.c independent of the + * daemon's private.h (which pulls in svc/plugin headers). The full + * prototype lives in private.h for callers in finit's main loop. */ +void dbus_notify_condition_change(const char *name, const char *state); +#endif + struct cond_boot { TAILQ_ENTRY(cond_boot) link; char *name; @@ -328,6 +335,9 @@ void cond_set(const char *name) if (cond_set_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "on"); +#endif cond_update(name); } @@ -358,6 +368,9 @@ void cond_set_oneshot(const char *name) if (cond_set_oneshot_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "on"); +#endif cond_update(name); } @@ -379,6 +392,9 @@ void cond_clear(const char *name) if (cond_clear_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "off"); +#endif cond_update(name); } diff --git a/src/dbus.c b/src/dbus.c new file mode 100644 index 00000000..acea312f --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,1113 @@ +/* Finit-side glue between the event loop and libink. + * + * Owns the libink server, accepts new peers, drives each peer's + * state machine, and registers the Finit-specific D-Bus object + * tree (org.finit.Manager1 et al). Nothing in libink/ depends on + * finit-internal types: the boundary lives in this file, by design. + * + * Copyright (c) 2026 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#ifdef HAVE_DBUS + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "link.h" +#include "path.h" + +#include "finit.h" +#include "cond.h" +#include "conf.h" +#include "log.h" +#include "private.h" +#include "service.h" +#include "sig.h" +#include "sm.h" +#include "svc.h" +#include "util.h" + +#define DBUS_MAX_PEERS 64 + +struct peer { + uev_t watcher; + link_connection_t *conn; + TAILQ_ENTRY(peer) link; +}; + +static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); +static link_server_t *server; +static uev_t accept_watcher; +static size_t peer_count; + +static void peer_drop(struct peer *p) +{ + uev_io_stop(&p->watcher); + link_connection_close(p->conn); + TAILQ_REMOVE(&peers, p, link); + peer_count--; + free(p); +} + +static void peer_cb(uev_t *w, void *arg, int events) +{ + struct peer *p = arg; + + (void)w; + + if (UEV_ERROR == events) { + peer_drop(p); + return; + } + + if (link_connection_process(p->conn) < 0) + peer_drop(p); +} + +/* Wrap an authenticated connection in a struct peer, insert into the + * peer list, and register an event-loop watcher. Enforces + * DBUS_MAX_PEERS. Closes the connection and returns NULL on failure. + * Used by both the accept path and the system-bus attach path. */ +static struct peer *peer_register(uev_ctx_t *ctx, link_connection_t *conn) +{ + struct peer *p; + + if (peer_count >= DBUS_MAX_PEERS) { + logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", + peer_count); + link_connection_close(conn); + return NULL; + } + + p = calloc(1, sizeof(*p)); + if (!p) { + link_connection_close(conn); + return NULL; + } + + p->conn = conn; + TAILQ_INSERT_TAIL(&peers, p, link); + peer_count++; + + if (uev_io_init(ctx, &p->watcher, peer_cb, p, + link_connection_get_fd(conn), UEV_READ)) { + peer_drop(p); + return NULL; + } + return p; +} + +static void accept_cb(uev_t *w, void *arg, int events) +{ + (void)arg; + + if (UEV_ERROR == events) { + err(1, "D-Bus accept watcher error"); + return; + } + + for (;;) { + link_connection_t *conn = NULL; + + if (link_server_accept(server, &conn) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + err(1, "Failed accepting D-Bus client"); + break; + } + + if (!peer_register(w->ctx, conn)) + continue; /* logged inside */ + } +} + +/* ---------- org.finit.Manager1 ---------- */ + +/* Forward decl + buffer-size constant — both consumed by Manager1 + * handlers below, defined in the Service1 block further down. */ +#define SERVICE_PATH_PREFIX "/org/finit/service/" +#define SERVICE_PATH_PREFIX_LEN (sizeof(SERVICE_PATH_PREFIX) - 1) +#define FINIT_SVC_PATH_MAX 512 +static int service_path_for(svc_t *svc, char *buf, size_t bufsz); + +static int manager_list_services(link_call_t *call, void *userdata) +{ + link_writer_t *w; + svc_t *iter = NULL; + svc_t *svc; + + (void)userdata; + + w = link_call_reply(call); + if (!w) + return -1; + + link_w_array_begin(w, 's'); + for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { + char ident[64]; + + svc_ident(svc, ident, sizeof(ident)); + link_w_string(w, ident); + } + link_w_array_end(w); + return 0; +} + +static int manager_get_service(link_call_t *call, void *userdata) +{ + const char *ident; + svc_t *svc; + char path[FINIT_SVC_PATH_MAX]; + link_writer_t *w; + + (void)userdata; + + if (link_call_read_string(call, &ident) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + svc = svc_find_by_str(ident); + if (!svc) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + if (service_path_for(svc, path, sizeof(path)) < 0) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "Path encoding overflow"); + + w = link_call_reply(call); + if (!w) + return -1; + link_w_path(w, path); + return 0; +} + +/* Service-control helpers used by Start/Stop/Restart/Reload. These + * mirror the static helpers in api.c — kept private here so api.c + * stays untouched in this increment. */ + +static int dbus_apply_stop(svc_t *svc, void *user_data) +{ + (void)user_data; + if (!svc) + return 1; + service_timeout_cancel(svc); + svc_stop(svc); + service_step(svc); + if (!IS_RESERVED_RUNLEVEL(runlevel)) + service_step_all(SVC_TYPE_ANY); + return 0; +} + +static int dbus_apply_start(svc_t *svc, void *user_data) +{ + (void)user_data; + if (!svc) + return 1; + service_timeout_cancel(svc); + svc_start(svc); + service_step(svc); + if (!IS_RESERVED_RUNLEVEL(runlevel)) + service_step_all(SVC_TYPE_ANY); + return 0; +} + +static int dbus_apply_restart(svc_t *svc, void *user_data) +{ + if (!svc) + return 1; + if (!svc_is_running(svc)) + return dbus_apply_start(svc, user_data); + service_timeout_cancel(svc); + service_stop(svc); + service_step(svc); + return 0; +} + +struct dispatch_ctx { + int (*action)(svc_t *, void *); + void *udata; + int matched; +}; + +static int dispatch_found(svc_t *svc, void *udata) +{ + struct dispatch_ctx *ctx = udata; + + ctx->matched++; + return ctx->action(svc, ctx->udata); +} + +static int dispatch_missing(char *job, char *id, void *udata) +{ + (void)job; (void)id; (void)udata; + return 0; /* don't penalise the return; we'll check ->matched */ +} + +/* Apply `action(svc, udata)` to every service matched by `ident`. + * Returns 0 if at least one service matched and the action succeeded + * on all; -1 if no service matched the identity (caller sends + * NoSuchService). */ +static int dispatch_action(const char *ident, + int (*action)(svc_t *, void *), void *udata) +{ + char buf[128]; + struct dispatch_ctx ctx = { .action = action, .udata = udata }; + int rc; + + if (!ident || !*ident || strlen(ident) >= sizeof(buf)) + return -1; + memcpy(buf, ident, strlen(ident) + 1); + rc = svc_parse_jobstr(buf, sizeof(buf), &ctx, + dispatch_found, dispatch_missing); + if (ctx.matched == 0) + return -1; + return rc; +} + +static int manager_take_string_method(link_call_t *call, + int (*action)(svc_t *, void *)) +{ + const char *ident; + + if (link_call_read_string(call, &ident) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + if (dispatch_action(ident, action, NULL) != 0) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + (void)link_call_reply(call); /* empty reply */ + return 0; +} + +static int manager_start (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); } +static int manager_stop (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); } +static int manager_restart(link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); } + +static int manager_reload(link_call_t *call, void *userdata) +{ + (void)userdata; + if (IS_RESERVED_RUNLEVEL(runlevel)) + return link_call_reply_error(call, + "org.finit.Error.WrongRunlevel", + "Reload not allowed in runlevel S or 0/6"); + sm_reload(); + (void)link_call_reply(call); + return 0; +} + +static int manager_set_runlevel(link_call_t *call, void *userdata) +{ + uint32_t lvl; + + (void)userdata; + if (link_call_read_u32(call, &lvl) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (u)"); + if (lvl > 9 || lvl == INIT_LEVEL) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "runlevel must be 0-9 (excluding internal levels)"); + + if (lvl == 0) halt = SHUT_OFF; + if (lvl == 6) halt = SHUT_REBOOT; + sm_runlevel((int)lvl); + + (void)link_call_reply(call); + return 0; +} + +static int dbus_shutdown(link_call_t *call, shutop_t target, int level) +{ + if (IS_RESERVED_RUNLEVEL(runlevel)) + return link_call_reply_error(call, + "org.finit.Error.WrongRunlevel", + "Already in shutdown"); + halt = target; + sm_runlevel(level); + (void)link_call_reply(call); + return 0; +} + +static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); } +static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); } +static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); } + +static int manager_set_debug(link_call_t *call, void *u) +{ + (void)u; + log_debug(); + (void)link_call_reply(call); + return 0; +} + +static int signal_one(svc_t *svc, void *udata) +{ + int signo = *(int *)udata; + + /* Silently skip stopped services -- a multi-match ident + * (e.g. "sshd:*") should not fail the whole call just because + * one of the matches happens to be in a halted state. */ + if (!svc_is_running(svc)) + return 0; + return !!kill(svc->pid, signo); +} + +static int manager_signal(link_call_t *call, void *u) +{ + const char *ident; + uint32_t signo; + int sig; + + (void)u; + if (link_call_read_string(call, &ident) < 0 || + link_call_read_u32 (call, &signo) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s, u)"); + /* Match the upper bound `initctl signal` allows (1..31). RT + * signals are a future story; keep both sides in lockstep so + * users see the same range regardless of transport. */ + if (signo == 0 || signo > 31) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "signal out of range (1..31)"); + + sig = (int)signo; + if (dispatch_action(ident, signal_one, &sig) != 0) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + (void)link_call_reply(call); + return 0; +} + +static int manager_suspend(link_call_t *call, void *u) +{ + (void)u; + sync(); + if (suspend() < 0) { + const char *msg = (errno == EINVAL) + ? "Kernel does not support suspend to RAM" + : strerror(errno); + return link_call_reply_error(call, + "org.finit.Error.Failed", msg); + } + (void)link_call_reply(call); + return 0; +} + +/* ---------- Manager1 properties ---------- + * + * Read-only string properties exposed via the standard + * 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. */ +static int prop_runlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + snprintf(buf, sizeof(buf), "%d", runlevel); + link_w_variant_string(w, buf); + return 0; +} + +static int prop_prevrunlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + snprintf(buf, sizeof(buf), "%d", prevlevel); + link_w_variant_string(w, buf); + return 0; +} + +static int prop_version(link_writer_t *w, void *u) +{ + (void)u; + link_w_variant_string(w, PACKAGE_VERSION); + return 0; +} + +static const link_property_t manager_properties[] = { + { .name = "Runlevel", .sig = "s", .getter = prop_runlevel }, + { .name = "PrevRunlevel", .sig = "s", .getter = prop_prevrunlevel }, + { .name = "Version", .sig = "s", .getter = prop_version }, + { NULL, NULL, NULL } +}; + +static const link_method_t manager_methods[] = { + { .name = "ListServices", .in_sig = "", .out_sig = "as", + .handler = manager_list_services }, + { .name = "GetService", .in_sig = "s", .out_sig = "o", + .handler = manager_get_service }, + { .name = "Start", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_start }, + { .name = "Stop", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_stop }, + { .name = "Restart", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload }, + { .name = "SetRunlevel", .in_sig = "u", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel }, + { .name = "Reboot", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot }, + { .name = "Poweroff", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff }, + { .name = "Halt", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt }, + { .name = "Suspend", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_suspend }, + { .name = "SetDebug", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_debug }, + { .name = "Signal", .in_sig = "su", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_signal }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_vtable_t manager_vtable = { + .interface = "org.finit.Manager1", + .methods = manager_methods, + .properties = manager_properties, +}; + +/* ---------- org.finit.Service1 (one object per service) ---------- + * + * Per-service object at /org/finit/service/. + * The vtable's `userdata` is the svc_t * for the specific service. + * Registration is driven dynamically from svc_new()/svc_del() via + * dbus_register_service() / dbus_unregister_service() below. */ + +/* SERVICE_PATH_PREFIX / SERVICE_PATH_PREFIX_LEN / FINIT_SVC_PATH_MAX + * defined near the top of the file so Manager1.GetService can refer + * to them. */ + +static int service_action_method(link_call_t *call, void *userdata, + int (*action)(svc_t *, void *)) +{ + svc_t *svc = userdata; + + if (!svc) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", + "Service object no longer valid"); + + action(svc, NULL); + (void)link_call_reply(call); + return 0; +} + +static int service1_start (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); } +static int service1_stop (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); } +static int service1_restart(link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); } + +static int service1_reload(link_call_t *call, void *userdata) +{ + svc_t *svc = userdata; + + if (!svc) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", + "Service object no longer valid"); + + service_reload(svc); + (void)link_call_reply(call); + return 0; +} + +static const link_method_t service_methods[] = { + { .name = "Start", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_start }, + { .name = "Stop", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_stop }, + { .name = "Restart", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_reload }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_vtable_t service_vtable = { + .interface = "org.finit.Service1", + .methods = service_methods, +}; + +/* Build the canonical object path for a service. Identity is + * "name" for single-instance services, "name:id" otherwise. */ +static int service_path_for(svc_t *svc, char *buf, size_t bufsz) +{ + char ident[MAX_IDENT_LEN]; + size_t plen = SERVICE_PATH_PREFIX_LEN; + int enc; + + if (bufsz <= plen) + return -1; + memcpy(buf, SERVICE_PATH_PREFIX, plen); + + svc_ident(svc, ident, sizeof(ident)); + enc = link_path_encode(ident, buf + plen, bufsz - plen); + if (enc < 0) + return -1; + return (int)plen + enc; +} + +void dbus_register_service(svc_t *svc) +{ + char path[FINIT_SVC_PATH_MAX]; + + if (!server || !svc) + return; + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + if (link_server_add_object(server, path, &service_vtable, svc) < 0) + logit(LOG_WARNING, "dbus: failed registering %s", path); +} + +void dbus_unregister_service(svc_t *svc) +{ + char path[FINIT_SVC_PATH_MAX]; + + if (!server || !svc) + return; + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + (void)link_server_remove_object(server, path); +} + +/* ---------- signal fan-out helper ---------- + * + * Fan out a pre-marshalled signal body to every connected peer, + * letting each connection apply its AddMatch filter. Short-circuits + * when no peers are connected so dbus_notify_* callers don't have + * to inspect that state themselves. */ +static void dbus_emit_signal(const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + struct peer *p; + + if (!server || TAILQ_EMPTY(&peers)) + return; + TAILQ_FOREACH(p, &peers, link) + (void)link_connection_emit_signal(p->conn, + path, interface, member, + signature, body, body_len); +} + +/* ---------- signal emission: ServiceStateChanged ---------- */ + +/* Coarse svc_state_t -> string. svc_status() in svc.h returns a + * richer string that also considers svc->block, but emitting just + * the state-machine state is enough for clients to track lifecycle + * transitions. Keep the strings stable -- they're a wire-API + * commitment once shipped. + * + * No `default:` on purpose: a new SVC_*_STATE added to svc.h must + * also pick a wire name here, and -Wswitch-enum makes the missing + * case a build error. */ +static const char *state_name(svc_state_t s) +{ + switch (s) { + case SVC_HALTED_STATE: return "halted"; + case SVC_DONE_STATE: return "done"; + case SVC_DEAD_STATE: return "dead"; + case SVC_CLEANUP_STATE: return "cleanup"; + case SVC_TEARDOWN_STATE: return "teardown"; + case SVC_STOPPING_STATE: return "stopping"; + case SVC_SETUP_STATE: return "setup"; + case SVC_PAUSED_STATE: return "paused"; + case SVC_WAITING_STATE: return "waiting"; + case SVC_STARTING_STATE: return "starting"; + case SVC_RUNNING_STATE: return "running"; + } + return "unknown"; +} + +void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) +{ + uint8_t body[256]; + link_writer_t w; + char ident[MAX_IDENT_LEN]; + ssize_t blen; + + if (!svc) + return; + svc_ident(svc, ident, sizeof(ident)); + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, ident); + link_w_string(&w, state_name((svc_state_t)old_state)); + link_w_string(&w, state_name((svc_state_t)new_state)); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "ServiceStateChanged", "sss", body, (size_t)blen); +} + +/* ---------- 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. */ +void dbus_notify_runlevel_change(int old_level, int new_level) +{ + uint8_t body[64]; + link_writer_t w; + 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); + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, old_s); + link_w_string(&w, new_s); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "RunlevelChanged", "ss", body, (size_t)blen); +} + +/* ---------- org.finit.Cond1 ---------- */ + +#define COND_PATH_OBJECT "/org/finit/cond" +#define COND_INTERFACE "org.finit.Cond1" + +/* Cond1.Set/Clear refuse anything that isn't a usr/ condition -- + * pid/, sys/, hook/ are owned by Finit's state machine and giving + * clients write access there would let them corrupt service state. + * Bare names ("foo") are normalised to "usr/foo" the same way + * initctl does. The returned pointer is valid for the duration + * of the caller's stack frame (`buf` must be at least 128 bytes). */ +static const char *normalise_usr_cond(const char *name, char *buf, size_t bufsz) +{ + const char *tail; + + if (!name || !*name) + return NULL; + if (strchr(name, '.')) + return NULL; + + if (strchr(name, '/')) { + if (strncmp(name, "usr/", 4) != 0) + return NULL; + tail = name + 4; + /* Match initctl's policy: no further slashes in the tail, + * and no empty tail ("usr/" alone). */ + if (!*tail || strchr(tail, '/')) + return NULL; + if (strlen(name) >= bufsz) + return NULL; + memcpy(buf, name, strlen(name) + 1); + return buf; + } + + if ((size_t)snprintf(buf, bufsz, "usr/%s", name) >= bufsz) + return NULL; + return buf; +} + +/* Reject names that would escape /run/finit/cond/. cond_get(name) + * boils down to fopen(_PATH_COND + name), so without this check any + * caller can make PID 1 open arbitrary files -- a path traversal + * primitive that also stalls PID 1 if pointed at a FIFO or a slow + * device. Legal cond names look like "usr/foo", "pid/sshd", + * "service/keventd/ready"; no leading slash, no ".." segment. */ +static int cond_name_valid(const char *name) +{ + const char *p; + + if (!name || !*name || *name == '/') + return 0; + for (p = name; *p; p++) { + if (*p == '.' && p[1] == '.' && + (p[2] == '\0' || p[2] == '/')) + return 0; + } + return 1; +} + +static int cond1_get(link_call_t *call, void *userdata) +{ + const char *name; + link_writer_t *w; + + (void)userdata; + + if (link_call_read_string(call, &name) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + if (!cond_name_valid(name)) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "invalid condition name"); + + w = link_call_reply(call); + if (!w) + return -1; + link_w_string(w, condstr(cond_get(name))); + return 0; +} + +static int cond1_set_or_clear(link_call_t *call, int do_set) +{ + const char *name; + char buf[128]; + const char *full; + + if (link_call_read_string(call, &name) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + full = normalise_usr_cond(name, buf, sizeof(buf)); + if (!full) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "Set/Clear is restricted to usr/* conditions"); + + if (do_set) + /* cond_set_oneshot, not cond_set: a user-asserted condition + * is a symlink to _PATH_RECONF, so it tracks the reconf + * generation automatically and stays "on" across reloads + * and runlevel switches. cond_set() writes a fixed + * generation that goes "flux" on the next reload -- wrong + * semantics for user conditions, and what initctl cond set + * has done forever via the filesystem path. */ + cond_set_oneshot(full); + else + cond_clear(full); + + (void)link_call_reply(call); + return 0; +} + +static int cond1_set (link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); } +static int cond1_clear(link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 0); } + +/* nftw() can't pass user data so a single static handle ferries the + * writer into the callback. Safe because dispatch is single-threaded. */ +static link_writer_t *cond_walk_writer; +static int cond_walk_dump; + +static int cond_walk_cb(const char *fpath, const struct stat *sb, + int tflag, struct FTW *ftwbuf) +{ + const char *name; + const char *state; + size_t prefix_len; + + (void)sb; + (void)ftwbuf; + + if (tflag != FTW_F) + return 0; + if (!strcmp(fpath, _PATH_RECONF)) + return 0; + + prefix_len = strlen(_PATH_COND); + if (strlen(fpath) <= prefix_len) + return 0; + name = fpath + prefix_len; + + if (cond_walk_dump) { + state = condstr(cond_get_path(fpath)); + link_w_struct_begin(cond_walk_writer); + link_w_string(cond_walk_writer, name); + link_w_string(cond_walk_writer, state); + link_w_struct_end(cond_walk_writer); + } else { + link_w_string(cond_walk_writer, name); + } + return 0; +} + +static int cond1_list(link_call_t *call, void *userdata) +{ + link_writer_t *w; + + (void)userdata; + + w = link_call_reply(call); + if (!w) + return -1; + + link_w_array_begin(w, 's'); + cond_walk_writer = w; + cond_walk_dump = 0; + (void)nftw(_PATH_COND, cond_walk_cb, 20, 0); + cond_walk_writer = NULL; + link_w_array_end(w); + return 0; +} + +static int cond1_dump(link_call_t *call, void *userdata) +{ + link_writer_t *w; + + (void)userdata; + + w = link_call_reply(call); + if (!w) + return -1; + + link_w_array_begin(w, '('); + cond_walk_writer = w; + cond_walk_dump = 1; + (void)nftw(_PATH_COND, cond_walk_cb, 20, 0); + cond_walk_writer = NULL; + link_w_array_end(w); + return 0; +} + +static const link_method_t cond_methods[] = { + { .name = "Get", .in_sig = "s", .out_sig = "s", + .handler = cond1_get }, + { .name = "Set", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = cond1_set }, + { .name = "Clear", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = cond1_clear }, + { .name = "List", .in_sig = "", .out_sig = "as", + .handler = cond1_list }, + { .name = "Dump", .in_sig = "", .out_sig = "a(ss)", + .handler = cond1_dump }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_vtable_t cond_vtable = { + .interface = COND_INTERFACE, + .methods = cond_methods, +}; + +/* ---------- signal emission: ConditionChanged ---------- */ + +void dbus_notify_condition_change(const char *name, const char *state) +{ + uint8_t body[256]; + link_writer_t w; + ssize_t blen; + + if (!name || !state) + return; + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, name); + link_w_string(&w, state); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal(COND_PATH_OBJECT, COND_INTERFACE, + "ConditionChanged", "ss", body, (size_t)blen); +} + +/* ---------- system-bus attach (opportunistic) ---------- + * + * If /var/run/dbus/system_bus_socket is reachable, libink connects to + * the system bus as a regular client, claims org.finit as a well-known + * name, then promotes the authenticated fd into a server-attached + * peer so the same vtables serve incoming method calls and outgoing + * signal fan-out reaches the system bus. + * + * peer_uid is set to (uid_t)-1 so LINK_METHOD_PRIVILEGED methods + * reject by default -- per-request sender uid lookup via + * GetConnectionUnixUser is a follow-up. Read-only methods + * (ListServices, Properties.Get, Introspect, ...) work as expected. + * + * A bounded SO_SNDTIMEO/SO_RCVTIMEO budget is applied via + * link_client_open_timeout so a hung dbus-daemon can't stall boot; + * once the connection is attached and flipped to non-blocking, those + * timeouts are silently inert. */ + +#define SYSTEM_BUS_PATH "/var/run/dbus/system_bus_socket" +#define FINIT_BUS_NAME "org.finit" +/* Budget for the synchronous AUTH + Hello + RequestName round-trips. + * If the system bus is alive but the daemon is wedged we'd rather + * give up after a couple of seconds than stall the rest of dbus_init + * (and through it, boot). */ +#define SYSTEM_BUS_TIMEOUT_MS 2000 +/* DBUS_NAME_FLAG_DO_NOT_QUEUE: fail fast if the name is taken + * (something else owns org.finit -- shouldn't happen and we'd + * rather log than silently sit in the queue). */ +#define DBUS_NAME_FLAG_DO_NOT_QUEUE 0x04 + +static int sysbus_request_name(link_client_t *c) +{ + uint32_t result = 0; + int rc; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "RequestName", + "su", FINIT_BUS_NAME, + (uint32_t)DBUS_NAME_FLAG_DO_NOT_QUEUE); + if (rc != LINK_CALL_OK) + return -1; + if (link_reply_get_u32(link_client_reply(c), &result) < 0) + return -1; + /* 1 = primary owner; 2/3/4 mean we didn't get the name */ + return (result == 1) ? 0 : -1; +} + +static void try_attach_system_bus(uev_ctx_t *ctx) +{ + link_client_t *c; + link_connection_t *conn; + int rc; + + c = link_client_open_timeout(SYSTEM_BUS_PATH, SYSTEM_BUS_TIMEOUT_MS); + if (!c) { + dbg("System bus unavailable at %s; skipping registration", + SYSTEM_BUS_PATH); + return; + } + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + if (rc != LINK_CALL_OK) { + dbg("System-bus Hello failed (rc=%d); skipping", rc); + link_client_close(c); + return; + } + + if (sysbus_request_name(c) < 0) { + logit(LOG_WARNING, "Failed to claim %s on system bus", FINIT_BUS_NAME); + link_client_close(c); + return; + } + + /* link_server_attach owns the fd from this point on whether it + * succeeds or fails, so the steal-then-attach pair has no leak + * window. */ + conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1); + if (!conn) + return; + + if (!peer_register(ctx, conn)) { + logit(LOG_WARNING, "Failed registering system-bus peer"); + return; + } + + logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); +} + +/* ---------- init / exit ---------- */ + +int dbus_init(uev_ctx_t *ctx) +{ + dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET); + + if (link_server_new(&server, FINIT_BUS_SOCKET) < 0) { + err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET); + return 1; + } + + if (link_server_add_object(server, "/org/finit/manager", + &manager_vtable, NULL) < 0) { + err(1, "Failed registering Manager1 object"); + link_server_free(server); + server = NULL; + return 1; + } + + if (link_server_add_object(server, COND_PATH_OBJECT, + &cond_vtable, NULL) < 0) { + err(1, "Failed registering Cond1 object"); + link_server_free(server); + server = NULL; + return 1; + } + + if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, + link_server_get_fd(server), UEV_READ)) { + err(1, "Failed registering D-Bus accept watcher"); + link_server_free(server); + server = NULL; + return 1; + } + + /* Register Service1 objects for every service already loaded. + * Subsequent svc_new()/svc_del() calls into + * dbus_register_service()/dbus_unregister_service(). */ + { + svc_t *iter = NULL; + svc_t *svc; + + for (svc = svc_iterator(&iter, 1); svc; + svc = svc_iterator(&iter, 0)) + dbus_register_service(svc); + } + + try_attach_system_bus(ctx); + + return 0; +} + +int dbus_exit(void) +{ + struct peer *p; + + uev_io_stop(&accept_watcher); + + while ((p = TAILQ_FIRST(&peers))) + peer_drop(p); + + if (server) { + link_server_free(server); + server = NULL; + } + + return 0; +} + +#endif /* HAVE_DBUS */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/src/finit.c b/src/finit.c index 63df48a1..24e081c5 100644 --- a/src/finit.c +++ b/src/finit.c @@ -761,6 +761,11 @@ int main(int argc, char *argv[]) dbg("Starting initctl API responder ..."); api_init(&loop); +#ifdef HAVE_DBUS + dbg("Starting D-Bus listener ..."); + dbus_init(&loop); +#endif + dbg("Starting service interval monitor ..."); service_init(&loop); diff --git a/src/finit.h b/src/finit.h index 5bef212b..30657537 100644 --- a/src/finit.h +++ b/src/finit.h @@ -71,6 +71,7 @@ #define BUF_SIZE 4096 #define INIT_SOCKET _PATH_VARRUN "finit/socket" +#define FINIT_BUS_SOCKET _PATH_VARRUN "finit/bus" #define INIT_MAGIC 0x03091969 #define INIT_LEVEL 10 diff --git a/src/initctl.c b/src/initctl.c index 435e2da9..2febc596 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -41,6 +41,13 @@ #include "service.h" #include "cgutil.h" #include "utmp-api.h" +#ifdef HAVE_DBUS +#include "link.h" +#include "path.h" +#endif + +/* Used by both do_cond_act and (with HAVE_DBUS) cond_dbus_call. */ +typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; struct cmd { char *cmd; @@ -145,6 +152,11 @@ static int runlevel_get(int *prevlevel) return rc; } +#ifdef HAVE_DBUS +static int try_dbus_manager(const char *method, const char *arg_sig, + const char *arg); +#endif + static int toggle_debug(char *arg) { struct init_request rq = { @@ -152,6 +164,13 @@ static int toggle_debug(char *arg) .cmd = INIT_CMD_DEBUG, }; + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("SetDebug", "", NULL); + if (rc >= 0) return rc; + } +#endif return client_send(&rq, sizeof(rq)); } @@ -193,6 +212,68 @@ static int show_log(char *arg) return do_log(svc, ""); } +#ifdef HAVE_DBUS +/* Fetch all org.finit.Manager1 string properties in one Properties.GetAll + * round-trip, then pick out a subset. `wanted` is a NULL-terminated array + * of property names; `out` parallel-receives the values (each entry left + * untouched if its property wasn't returned). Returns 0 on transport + * success (even if some properties weren't present), -1 on transport or + * parse failure. */ +static int dbus_get_manager_props(const char *const *wanted, char **out, size_t out_sz) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + size_t end; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.freedesktop.DBus.Properties", "GetAll", + "s", "org.finit.Manager1"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + return -1; + } + + r = link_client_reply(c); + if (!r || !r->body) { + link_client_close(c); + return -1; + } + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) { + link_client_close(c); + return -1; + } + + while (link_r_pos(&reader) < end) { + const char *key = NULL; + const char *val = NULL; + size_t i; + + if (link_r_align(&reader, 8) < 0) break; + if (link_r_string(&reader, &key) < 0) break; + if (link_r_variant_string(&reader, &val) < 0) break; + if (!key || !val) break; + + for (i = 0; wanted[i]; i++) { + if (!strcmp(key, wanted[i])) { + strlcpy(out[i], val, out_sz); + break; + } + } + } + + link_client_close(c); + return 0; +} +#endif + static int do_runlevel(char *arg) { struct init_request rq = { @@ -205,6 +286,23 @@ static int do_runlevel(char *arg) int currlevel; char prev, curr; +#ifdef HAVE_DBUS + char curr_buf[16] = { 0 }, prev_buf[16] = { 0 }; + const char *const wanted[] = { "Runlevel", "PrevRunlevel", NULL }; + char *out[] = { curr_buf, prev_buf }; + + 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); + return 0; + } +#endif + currlevel = runlevel_get(&prevlevel); switch (currlevel) { case 255: @@ -268,19 +366,271 @@ static int do_startstop(int cmd, char *arg) return do_svc(cmd, arg); } -static int do_start (char *arg) { return do_startstop(INIT_CMD_START_SVC, arg); } -static int do_stop (char *arg) { return do_startstop(INIT_CMD_STOP_SVC, arg); } +#ifdef HAVE_DBUS + +/* Map a LINK_CALL_ERROR reply on `c` to the appropriate ERRX exit: + * org.finit.Error.NoSuchService -> exit 69 (legacy "no such svc") + * org.freedesktop.DBus.Error.AccessDenied -> exit 1 (permission denied) + * anything else -> exit 1 (method: err) + * `c` is closed before exit either way. Use exact-match on the + * fully-qualified error name; a substring match would misfire on a + * future name that contained one of these as a prefix. */ +static void map_dbus_err(link_client_t *c, const char *method, const char *ident) +{ + const link_reply_t *r = link_client_reply(c); + char err[128]; + + /* The reply view points into c->rxbuf; copy the error name out + * before link_client_close() frees the client. Otherwise the + * strcmps below read freed memory. */ + if (r && r->error_name) + strlcpy(err, r->error_name, sizeof(err)); + else + err[0] = '\0'; + link_client_close(c); + + if (!strcmp(err, "org.finit.Error.NoSuchService")) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", + ident ? ident : ""); + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: %s requires root", method); + ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); +} + +/* Try the D-Bus path for a Manager1 method. Returns: + * 0 succeeded via D-Bus + * -1 D-Bus not reachable -- callers should fall back to the + * legacy INIT_SOCKET transport + * LINK_CALL_ERROR is handled internally via map_dbus_err (does not + * return). */ +static int try_dbus_manager(const char *method, const char *arg_sig, + const char *arg) +{ + link_client_t *c; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + /* "s" methods take the service identity (arg may be NULL -> + * empty string); void methods pass no body. */ + if (arg_sig && !strcmp(arg_sig, "s")) + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", method, + "s", arg ? arg : ""); + else if (!arg_sig || !*arg_sig) + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", method, NULL); + else + rc = LINK_CALL_FAIL; + + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, arg); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; +} + +/* Call a void-arg method on Service1 at /org/finit/service/. + * Same return convention as try_dbus_manager. */ +static int try_dbus_service(const char *method, const char *ident) +{ + char path[256]; + const char *prefix = "/org/finit/service/"; + size_t plen = strlen(prefix); + link_client_t *c; + int rc; + + if (!ident || !*ident) + return -1; + memcpy(path, prefix, plen); + if (link_path_encode(ident, path + plen, sizeof(path) - plen) < 0) + return -1; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, path, "org.finit.Service1", method, NULL); + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, ident); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; +} + +/* Try one Cond1.{Get,Set,Clear} call. On COND_GET success the helper + * fills *out_exit with the exit code (0 = on, 1 = off, 255 = flux). + * Outcomes: + * 1 call succeeded; for GET the result is in *out_exit, for + * SET/CLR the caller loops to the next arg + * 0 bus not reachable, or LINK_CALL_FAIL -- *bus is closed/NULLed + * and the caller should drop to the legacy filesystem path + * (LINK_CALL_ERROR exits via ERRX inside the helper) + * + * `*bus` is borrowed; the helper closes it (and sets NULL) on every + * exit path that leaves the bus unusable. */ +static int cond_dbus_call(link_client_t **bus, condop_t op, + const char *arg, int *out_exit) +{ + const char *method = (op == COND_GET) ? "Get" + : (op == COND_SET) ? "Set" : "Clear"; + int rc; + + if (!*bus) + return 0; + + rc = link_client_call_v(*bus, "/org/finit/cond", + "org.finit.Cond1", method, + "s", arg); + if (rc == LINK_CALL_OK) { + if (op == COND_GET) { + const char *state = NULL; + + link_reply_get_string(link_client_reply(*bus), &state); + if (verbose && state) + puts(state); + *out_exit = (state && !strcmp(state, "on")) ? 0 + : (state && !strcmp(state, "off")) ? 1 : 255; + } + return 1; + } + if (rc == LINK_CALL_ERROR) { + const link_reply_t *r = link_client_reply(*bus); + const char *err = (r && r->error_name) ? r->error_name : ""; + + link_client_close(*bus); + *bus = NULL; + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: cond %s requires root", method); + ERRX(73, "Failed %s condition <%s>: %s", + op == COND_SET ? "asserting" : "deasserting", + arg, *err ? err : "D-Bus error"); + } + /* LINK_CALL_FAIL */ + link_client_close(*bus); + *bus = NULL; + return 0; +} + +/* Subscribe to every signal on the bus and print one line per + * incoming message: + * HH:MM:SS interface.member(arg1, arg2, ...) + * + * Only string-typed leading args are decoded (matches what our two + * current signals -- ServiceStateChanged (sss) and ConditionChanged + * (ss) -- emit). Non-string args are silently skipped. Loops until + * the connection drops or the user hits ^C. */ +static int do_monitor(char *arg) +{ + link_client_t *c; + int rc; + + (void)arg; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + ERRX(1, "monitor requires the D-Bus socket at %s", FINIT_BUS_SOCKET); + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", "type='signal'"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + ERRX(1, "AddMatch failed (rc=%d)", rc); + } + + for (;;) { + const link_reply_t *r; + link_reader_t reader; + char ts[16]; + time_t now; + struct tm tm; + + rc = link_client_wait(c, -1); + if (rc < 0) { + link_client_close(c); + ERRX(1, "bus connection lost"); + } + if (rc > 0) /* impossible with timeout=-1, but harmless */ + continue; + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) + continue; + + now = time(NULL); + localtime_r(&now, &tm); + strftime(ts, sizeof(ts), "%H:%M:%S", &tm); + printf("%s %s.%s(", ts, + r->interface ? r->interface : "?", + r->member ? r->member : "?"); + + link_reader_init(&reader, r->body, r->body_len); + if (r->signature) { + const char *p; + int first = 1; + + for (p = r->signature; *p == 's'; p++) { + const char *s; + + if (link_r_string(&reader, &s) < 0) + break; + printf("%s%s", first ? "" : ", ", s); + first = 0; + } + } + printf(")\n"); + fflush(stdout); + } + + link_client_close(c); + return 0; +} +#endif /* HAVE_DBUS */ + +static int do_start (char *arg) +{ +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Start", "s", arg); + if (rc >= 0) return rc; +#endif + return do_startstop(INIT_CMD_START_SVC, arg); +} + +static int do_stop (char *arg) +{ +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Stop", "s", arg); + if (rc >= 0) return rc; +#endif + return do_startstop(INIT_CMD_STOP_SVC, arg); +} static int do_reload (char *arg) { - if (!arg || !arg[0]) + if (!arg || !arg[0]) { +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Reload", "", NULL); + if (rc >= 0) return rc; +#endif return do_svc(INIT_CMD_RELOAD, NULL); + } +#ifdef HAVE_DBUS + { + int rc = try_dbus_service("Reload", arg); + if (rc >= 0) return rc; + } +#endif return do_startstop(INIT_CMD_RELOAD_SVC, arg); } static int do_restart(char *arg) { +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Restart", "s", arg); + if (rc == 0) return 0; + if (rc == 1) ERRX(noerr ? 0 : 7, "failed restarting %s", arg); +#endif if (do_startstop(INIT_CMD_RESTART_SVC, arg)) ERRX(noerr ? 0 : 7, "failed restarting %s", arg); @@ -308,10 +658,6 @@ int do_signal(int argc, char *argv[]) if (argc != 2) ERRX(2, "invalid number of arguments to signal"); - strlcpy(rq.data, argv[0], sizeof(rq.data)); - if (client_send(&rq, sizeof(rq))) - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); - signo = str2sig(argv[1]); if (signo == -1) { const char *errstr = NULL; @@ -321,6 +667,29 @@ int do_signal(int argc, char *argv[]) ERRX(65, "%s signal: %s", errstr, argv[1]); } +#ifdef HAVE_DBUS + { + link_client_t *c = link_client_open(FINIT_BUS_SOCKET); + + if (c) { + int rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "Signal", + "su", argv[0], (uint32_t)signo); + + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, "Signal", argv[0]); /* exits */ + link_client_close(c); + if (rc == LINK_CALL_OK) + return 0; + /* LINK_CALL_FAIL: drop to legacy */ + } + } +#endif + + strlcpy(rq.data, argv[0], sizeof(rq.data)); + if (client_send(&rq, sizeof(rq))) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); + /* Reuse runlevel for signal number. */ rq.magic = INIT_MAGIC; rq.cmd = INIT_CMD_SIGNAL; @@ -408,8 +777,6 @@ static int do_cond_dump(char *arg) return 0; } -typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; - static cond_state_t cond_read(char *path) { int now, gen; @@ -439,6 +806,9 @@ static int do_cond_act(char *args, condop_t op) cond_state_t cstate; char path[256]; char *arg; +#ifdef HAVE_DBUS + link_client_t *bus = link_client_open(FINIT_BUS_SOCKET); +#endif if (!args || !args[0]) ERRX(2, "Invalid condition (empty)"); @@ -458,6 +828,22 @@ static int do_cond_act(char *args, condop_t op) ERRX(2, "Invalid condition (periods)"); } +#ifdef HAVE_DBUS + { + int exit_code; + + if (cond_dbus_call(&bus, op, arg, &exit_code)) { + if (op == COND_GET) { + link_client_close(bus); + return exit_code; + } + arg = strtok(NULL, " \t"); + continue; + } + /* bus is NULL now -- drop through to legacy */ + } +#endif + if (strchr(arg, '/')) snprintf(path, sizeof(path), _PATH_COND "%s", arg); else @@ -494,6 +880,10 @@ static int do_cond_act(char *args, condop_t op) arg = strtok(NULL, " \t"); } +#ifdef HAVE_DBUS + if (bus) + link_client_close(bus); +#endif return 0; } @@ -641,10 +1031,57 @@ static int do_cmd(int cmd) return 0; } -int do_reboot (char *arg) { return do_cmd(INIT_CMD_REBOOT); } -int do_halt (char *arg) { return do_cmd(INIT_CMD_HALT); } -int do_poweroff(char *arg) { return do_cmd(INIT_CMD_POWEROFF); } -int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); } +#ifdef HAVE_DBUS +static int do_reboot_dbus(const char *method) +{ + int rc = try_dbus_manager(method, "", NULL); + + if (rc == 0) { + sleep(5); /* match legacy: wait for finit to shut down */ + return 0; + } + return rc; /* 1 = error, -1 = fall back */ +} +#endif + +int do_reboot (char *arg) +{ +#ifdef HAVE_DBUS + int rc = do_reboot_dbus("Reboot"); + if (rc >= 0) return rc; +#endif + return do_cmd(INIT_CMD_REBOOT); +} + +int do_halt (char *arg) +{ +#ifdef HAVE_DBUS + int rc = do_reboot_dbus("Halt"); + if (rc >= 0) return rc; +#endif + return do_cmd(INIT_CMD_HALT); +} + +int do_poweroff(char *arg) +{ +#ifdef HAVE_DBUS + int rc = do_reboot_dbus("Poweroff"); + if (rc >= 0) return rc; +#endif + return do_cmd(INIT_CMD_POWEROFF); +} + +int do_suspend(char *arg) +{ + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("Suspend", "", NULL); + if (rc >= 0) return rc; + } +#endif + return do_cmd(INIT_CMD_SUSPEND); +} /** * do_switch_root - Switch to a new root filesystem (initramfs only) @@ -1570,6 +2007,9 @@ static int usage(int rc) " Note: Finit .conf file(s) are *not* reloaded!\n" " restart [:ID] Restart (stop/start) service by name\n" " kill [:ID] Send signal S to service by name, with optional ID\n" +#ifdef HAVE_DBUS + " monitor Stream D-Bus signals (service state, conditions) until ^C\n" +#endif " ident [NAME] Show matching identities for NAME, or all\n" " status [:ID] Show service status, by name\n" " status Show status of services, default command\n"); @@ -1754,6 +2194,9 @@ int main(int argc, char *argv[]) { "start", NULL, do_start, NULL, NULL }, { "stop", NULL, do_stop, NULL, NULL }, { "restart", NULL, do_restart, NULL, NULL }, +#ifdef HAVE_DBUS + { "monitor", NULL, do_monitor, NULL, NULL }, +#endif { "signal", NULL, NULL, NULL, do_signal }, { "kill", NULL, NULL, NULL, do_signal }, /* alias */ diff --git a/src/private.h b/src/private.h index 1612803b..ae501161 100644 --- a/src/private.h +++ b/src/private.h @@ -45,6 +45,16 @@ extern uev_ctx_t *ctx; int api_init (uev_ctx_t *ctx); int api_exit (void); + +#ifdef HAVE_DBUS +int dbus_init (uev_ctx_t *ctx); +int dbus_exit (void); +void dbus_register_service (svc_t *svc); +void dbus_unregister_service (svc_t *svc); +void dbus_notify_service_state (svc_t *svc, int old_state, int new_state); +void dbus_notify_condition_change(const char *name, const char *state); +void dbus_notify_runlevel_change(int old_level, int new_level); +#endif void conf_flush_events(void); void service_monitor (pid_t lost, int status); diff --git a/src/service.c b/src/service.c index 127e0099..ecabbc77 100644 --- a/src/service.c +++ b/src/service.c @@ -1328,17 +1328,67 @@ int service_stop(svc_t *svc) } /** - * service_reload - Reload a service + * service_reload - Request reload of a service, driven by the state machine * @svc: Service to reload * - * This function does some basic checks of the runtime state of Finit - * and a sanity check of the @svc before sending %SIGHUP or calling - * the reload:script command. + * Mark a service dirty so the state machine will (re-)apply its + * configuration, then advance the state machine. For services that + * don't handle SIGHUP this also clears the readiness condition and + * the pidfile/none-notify readiness flag so dependents are properly + * notified once the service comes back. + * + * Returns: + * POSIX OK(0) on success, non-zero if @svc is NULL. + */ +int service_reload(svc_t *svc) +{ + char cond[MAX_COND_LEN]; + + if (!svc) + return 1; + + if (svc_is_blocked(svc)) + svc_start(svc); + else + service_timeout_cancel(svc); + + /* + * Clear conditions before reload to ensure dependent services + * are properly updated. Only needed when the service does NOT + * support SIGHUP (noreload), because then it will be stopped + * and restarted, so conditions genuinely go away. When the + * service handles SIGHUP, its PID and pidfile persist, so the + * condition stays valid and dependents should not be disrupted. + * + * Note: only clear 'ready' for services where the pidfile + * inotify handler reasserts it (pid/none). For s6/systemd + * services readiness relies on their respective notification + * mechanism which may not re-trigger on SIGHUP. + */ + if (svc_is_noreload(svc)) { + cond_clear(mkcond(svc, cond, sizeof(cond))); + if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) + service_ready(svc, 0); + } + + svc_mark_dirty(svc); + service_step(svc); + + return 0; +} + +/** + * service_reload_apply - Perform the actual reload of a running service + * @svc: Service to reload + * + * Low-level reload step: sends %SIGHUP or runs the reload:script + * command. Called by the state machine when a service marked + * reload-pending by service_reload() reaches the right state. * * Returns: * POSIX OK(0) or non-zero on error. */ -static int service_reload(svc_t *svc) +static int service_reload_apply(svc_t *svc) { const char *id = svc_ident(svc, NULL, 0); int do_progress = 1; @@ -2864,6 +2914,10 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state) return; *state = new_state; +#ifdef HAVE_DBUS + dbus_notify_service_state(svc, old_state, new_state); +#endif + if (svc_is_runtask(svc)) { char success[MAX_COND_LEN], failure[MAX_COND_LEN]; @@ -3225,7 +3279,7 @@ int service_step(svc_t *svc) if (sm_in_reload()) break; - service_reload(svc); + service_reload_apply(svc); } svc_mark_clean(svc); @@ -3263,7 +3317,7 @@ int service_step(svc_t *svc) if (svc_is_noreload(svc)) service_stop(svc); else - service_reload(svc); + service_reload_apply(svc); break; } diff --git a/src/service.h b/src/service.h index 5efc2354..9c50de9e 100644 --- a/src/service.h +++ b/src/service.h @@ -43,6 +43,7 @@ void service_forked (svc_t *svc); void service_ready (svc_t *svc, int ready); int service_stop (svc_t *svc); +int service_reload (svc_t *svc); int service_step (svc_t *svc); void service_step_all (int types); void service_worker (void *unused); diff --git a/src/sm.c b/src/sm.c index ab3abdb9..44d0972d 100644 --- a/src/sm.c +++ b/src/sm.c @@ -388,6 +388,9 @@ void sm_step(void) prevlevel = runlevel; runlevel = sm.newlevel; sm.newlevel = -1; +#ifdef HAVE_DBUS + dbus_notify_runlevel_change(prevlevel, runlevel); +#endif /* Restore terse mode and run hooks before shutdown */ if (runlevel == 0 || runlevel == 6) { diff --git a/src/svc.c b/src/svc.c index 272c48f1..c756697d 100644 --- a/src/svc.c +++ b/src/svc.c @@ -38,6 +38,7 @@ #include "finit.h" #include "conf.h" +#include "private.h" #include "svc.h" #include "helpers.h" #include "pid.h" @@ -156,6 +157,10 @@ svc_t *svc_new(char *cmd, char *name, char *id, int type) TAILQ_INSERT_TAIL(&svc_list, svc, link); +#ifdef HAVE_DBUS + dbus_register_service(svc); +#endif + return svc; } @@ -173,6 +178,10 @@ static struct wq work = { */ int svc_del(svc_t *svc) { +#ifdef HAVE_DBUS + dbus_unregister_service(svc); +#endif + TAILQ_REMOVE(&svc_list, svc, link); TAILQ_INSERT_TAIL(&gc_list, svc, link); diff --git a/test/Makefile.am b/test/Makefile.am index da6eb905..be9b0947 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -62,6 +62,12 @@ EXTRA_DIST += start-stop-serv.sh EXTRA_DIST += signal-service.sh EXTRA_DIST += testserv.sh EXTRA_DIST += unexpected-restart.sh +EXTRA_DIST += dbus-auth.sh +EXTRA_DIST += dbus-bus.sh +EXTRA_DIST += dbus-manager.sh +EXTRA_DIST += dbus-service.sh +EXTRA_DIST += dbus-cond.sh +EXTRA_DIST += dbus-initctl.sh AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -108,6 +114,14 @@ if TESTSERV TESTS += testserv.sh endif TESTS += unexpected-restart.sh +if DBUS +TESTS += dbus-auth.sh +TESTS += dbus-bus.sh +TESTS += dbus-manager.sh +TESTS += dbus-service.sh +TESTS += dbus-cond.sh +TESTS += dbus-initctl.sh +endif check-recursive: setup-chroot diff --git a/test/check.sh b/test/check.sh index 3c847f56..c12a2614 100755 --- a/test/check.sh +++ b/test/check.sh @@ -25,8 +25,8 @@ if [ "$run_make" -eq 1 ]; then fi ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \ - --enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \ - --with-keventd --with-libsystemd \ + --enable-dbus --enable-x11-common-plugin --enable-testserv-plugin \ + --with-watchdog --with-keventd --with-libsystemd \ CFLAGS='-fsanitize=address -ggdb' if [ "$run_make" -eq 1 ]; then diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh new file mode 100755 index 00000000..1e9535d3 --- /dev/null +++ b/test/dbus-auth.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# libink: D-Bus AUTH EXTERNAL handshake. +# +# Verifies the SASL handshake itself in isolation -- everything else +# the bus does (built-in DBus interface, vtables, signals, initctl +# routing) lives in the other dbus-*.sh tests. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Socket mode is 0666" +mode=$(texec stat -c %a "$BUS") +assert "Socket mode is 666 (got $mode)" "$mode" = "666" + +say "AUTH EXTERNAL: claim correct UID (root = 0)" +reply=$(texec "$CLIENT" auth "$BUS" 0) +assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK" + +guid=${reply#OK } +assert "GUID is 32 hex chars (got: $guid)" \ + "$(printf '%s' "$guid" | tr -d '0-9a-f' | wc -c)" -eq 0 +assert "GUID length is 32 (got: ${#guid})" "${#guid}" -eq 32 + +say "AUTH EXTERNAL: wrong UID is rejected" +set +e +wrong_reply=$(texec "$CLIENT" auth "$BUS" 1) +wrong_rc=$? +set -e +assert "Wrong UID rejected (rc=$wrong_rc, reply: $wrong_reply)" \ + "$wrong_rc" -eq 1 + +say "Two sequential AUTH connections get different GUIDs" +r1=$(texec "$CLIENT" auth "$BUS" 0) +r2=$(texec "$CLIENT" auth "$BUS" 0) +g1=${r1#OK } +g2=${r2#OK } +assert "Per-connection GUIDs differ ($g1 vs $g2)" "$g1" != "$g2" diff --git a/test/dbus-bus.sh b/test/dbus-bus.sh new file mode 100755 index 00000000..7bd53704 --- /dev/null +++ b/test/dbus-bus.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# libink: org.freedesktop.DBus built-in interface. +# +# Covers the stock D-Bus interface every conforming bus implements: +# Hello (peer name allocation), Introspect (XML), and AddMatch's +# error-path rule parser. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Hello() returns a unique name beginning with ':1.'" +name=$(texec "$CLIENT" hello "$BUS") +case "$name" in + :1.*) assert "Hello returned a :1.N name (got $name)" 0 -eq 0 ;; + *) fail "Hello returned unexpected name: $name" ;; +esac + +say "Two Hello() calls produce different unique names" +n1=$(texec "$CLIENT" hello "$BUS") +n2=$(texec "$CLIENT" hello "$BUS") +assert "Unique names increment ($n1 vs $n2)" "$n1" != "$n2" + +say "Introspect on root path returns valid XML" +xml=$(texec "$CLIENT" introspect "$BUS" /) +case "$xml" in + *' root (good)" 0 -eq 0 ;; + *) fail "Root introspect missing : $xml" ;; +esac + +say "Introspect on /org/finit/manager exposes Manager1.ListServices" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) +case "$xml" in + *'org.finit.Manager1'*'ListServices'*) + assert "Manager1 and ListServices visible in XML" 0 -eq 0 ;; + *) + fail "Manager1 XML missing; got: $xml" ;; +esac + +say "AddMatch with a bogus key is rejected" +set +e +texec "$CLIENT" call-s "$BUS" /org/freedesktop/DBus \ + org.freedesktop.DBus AddMatch "bogus='whatever'" >/tmp/dbus-match.out 2>&1 +am_rc=$? +set -e +assert "Bad rule rejected (rc=$am_rc)" "$am_rc" -eq 1 +case "$(cat /tmp/dbus-match.out)" in + *MatchRuleInvalid*) assert "Error is MatchRuleInvalid" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-match.out)" ;; +esac + +say "Unknown method on a Finit interface gets an org.freedesktop.DBus.Error.* reply" +set +e +texec "$CLIENT" unknown "$BUS" +unknown_rc=$? +set -e +assert "Unknown method returned an error (rc=$unknown_rc)" "$unknown_rc" -eq 0 diff --git a/test/dbus-cond.sh b/test/dbus-cond.sh new file mode 100755 index 00000000..f0e55da1 --- /dev/null +++ b/test/dbus-cond.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# libink: org.finit.Cond1 vtable + ConditionChanged signal. +# +# Covers user-condition manipulation: Get, Set (with signal fan-out), +# the usr/* policy guard (non-usr conditions are rejected), and the +# non-root authorization gate. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Cond1.Get returns 'off' for an unset condition" +result=$(texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Get "no-such-cond") +case "$result" in + OK*) : ;; # ok, the cond reports a state, fall through + *) fail "Cond1.Get failed: $result" ;; +esac + +say "Cond1.Set fires Cond1.ConditionChanged and Get reflects the change" +rm -f /tmp/dbus-cond.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-cond.out 2>&1 ) & +cond_mon_pid=$! +sleep 0.5 +texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "dbus-test-cond" >/dev/null \ + || fail "Cond1.Set returned non-zero" +set +e +wait "$cond_mon_pid" +cond_mon_rc=$? +set -e +assert "Cond1 monitor saw a signal (rc=$cond_mon_rc)" "$cond_mon_rc" -eq 0 +case "$(cat /tmp/dbus-cond.out)" in + *"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/dbus-test-cond"*on*) + assert "ConditionChanged carries usr/dbus-test-cond and 'on'" 0 -eq 0 ;; + *) + fail "Unexpected Cond1 signal: $(cat /tmp/dbus-cond.out)" ;; +esac + +say "Cond1.Set/Clear on non-usr/* is rejected" +set +e +texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "pid/sshd" >/tmp/dbus-condrej.out 2>&1 +condrej_rc=$? +set -e +assert "pid/* rejected (rc=$condrej_rc)" "$condrej_rc" -eq 1 +case "$(cat /tmp/dbus-condrej.out)" in + *InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-condrej.out)" ;; +esac + +say "Cond1.Set from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "would-be-cond" >/tmp/dbus-condauthz.out 2>&1 +ca_rc=$? +set -e +assert "Non-root Cond1.Set rejected (rc=$ca_rc)" "$ca_rc" -eq 1 +case "$(cat /tmp/dbus-condauthz.out)" in + *AccessDenied*) assert "Cond1 authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-condauthz.out)" ;; +esac diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh new file mode 100755 index 00000000..2a1b47c1 --- /dev/null +++ b/test/dbus-initctl.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# initctl: confirms the legacy CLI now routes through D-Bus. +# +# Subscribes to ServiceStateChanged on a background monitor and then +# runs initctl -- if D-Bus is in use, the signal fires. If the legacy +# socket were still in use, the dbus subscriber would see nothing. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "initctl restart drives D-Bus (signal observed via dbus-auth-client)" +rm -f /tmp/dbus-initctl-sig.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \ + 5000 > /tmp/dbus-initctl-sig.out 2>&1 ) & +ic_pid=$! +sleep 0.5 +texec initctl restart keventd >/dev/null \ + || fail "initctl restart returned non-zero" +set +e +wait "$ic_pid" +ic_rc=$? +set -e +assert "ServiceStateChanged fired from initctl restart (rc=$ic_rc)" \ + "$ic_rc" -eq 0 +case "$(cat /tmp/dbus-initctl-sig.out)" in + *"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*) + assert "initctl restart routed through D-Bus" 0 -eq 0 ;; + *) + fail "initctl restart didn't produce expected signal: $(cat /tmp/dbus-initctl-sig.out)" ;; +esac + +say "initctl reload (no args) routes through Manager1.Reload" +texec initctl reload >/dev/null \ + || fail "initctl reload returned non-zero" +assert "initctl reload ok" 0 -eq 0 + +# A ConditionChanged signal can only originate from Cond1.Set going +# through finit (the legacy filesystem path doesn't emit signals). +# So if the monitor sees one, we know initctl cond set was routed +# via D-Bus. +say "initctl cond set drives Cond1.Set via D-Bus" +rm -f /tmp/dbus-initctl-cond.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-initctl-cond.out 2>&1 ) & +ic_cond_pid=$! +sleep 0.5 +texec initctl cond set "via-initctl" >/dev/null \ + || fail "initctl cond set returned non-zero" +set +e +wait "$ic_cond_pid" +ic_cond_rc=$? +set -e +assert "ConditionChanged fired from initctl cond set (rc=$ic_cond_rc)" \ + "$ic_cond_rc" -eq 0 +case "$(cat /tmp/dbus-initctl-cond.out)" in + *"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/via-initctl"*on*) + assert "initctl cond set routed through D-Bus" 0 -eq 0 ;; + *) + fail "initctl cond set didn't produce expected signal: $(cat /tmp/dbus-initctl-cond.out)" ;; +esac + +# ---------- arc B: signal + reload routing ---------- + +# initctl signal exits 0 iff the bus call succeeded. We send SIGCONT +# (no observable side-effect on a healthy daemon) so the test stays +# benign regardless of how keventd handles it. +say "initctl signal routes through Manager1.Signal" +texec initctl signal keventd CONT >/dev/null \ + || fail "initctl signal returned non-zero" +assert "initctl signal ok" 0 -eq 0 + +say "initctl signal on a bogus identity reports NoSuchService" +set +e +texec initctl signal no-such-svc-anywhere CONT >/tmp/dbus-sig-bad.out 2>&1 +sigbad_rc=$? +set -e +assert "Bogus signal target rejected (rc=$sigbad_rc)" "$sigbad_rc" -ne 0 +case "$(cat /tmp/dbus-sig-bad.out)" in + *"no such task or service"*) + assert "Error message mentions missing service" 0 -eq 0 ;; + *) + fail "Unexpected initctl signal output: $(cat /tmp/dbus-sig-bad.out)" ;; +esac + +say "initctl reload routes through Service1.Reload" +texec initctl reload keventd >/dev/null \ + || fail "initctl reload keventd returned non-zero" +assert "Per-service reload ok" 0 -eq 0 diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh new file mode 100755 index 00000000..c3bdce33 --- /dev/null +++ b/test/dbus-manager.sh @@ -0,0 +1,135 @@ +#!/bin/sh +# libink: org.finit.Manager1 vtable. +# +# Covers the Manager1 method surface: ListServices, Reload, Stop with +# bogus service, plus per-method authorization (Restart from non-root +# is rejected, ListServices remains reachable as non-root). + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Manager1.ListServices returns the running services" +list=$(texec "$CLIENT" liststrings "$BUS" /org/finit/manager \ + org.finit.Manager1 ListServices) +assert "ListServices returned at least one service" \ + "$(printf '%s' "$list" | wc -l | tr -d ' ')" -ge 1 +echo "$list" + +say "Manager1.Reload (void) succeeds" +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 Reload >/dev/null \ + || fail "Reload returned non-zero" +assert "Reload void method ok" 0 -eq 0 + +say "Manager1.Stop with bogus identity returns NoSuchService error" +set +e +texec "$CLIENT" call-s "$BUS" /org/finit/manager \ + org.finit.Manager1 Stop "no-such-service-here" >/tmp/dbus-stop.out 2>&1 +stop_rc=$? +set -e +assert "Bogus service rejected (rc=$stop_rc)" "$stop_rc" -eq 1 +case "$(cat /tmp/dbus-stop.out)" in + *NoSuchService*) assert "Error is NoSuchService" 0 -eq 0 ;; + *) fail "Unexpected error reply: $(cat /tmp/dbus-stop.out)" ;; +esac + +say "Manager1.Restart from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart "testserv" >/tmp/dbus-authz.out 2>&1 +authz_rc=$? +set -e +assert "Non-root Restart rejected (rc=$authz_rc)" "$authz_rc" -eq 1 +case "$(cat /tmp/dbus-authz.out)" in + *AccessDenied*) assert "Error is AccessDenied" 0 -eq 0 ;; + *) fail "Unexpected error: $(cat /tmp/dbus-authz.out)" ;; +esac + +# Send call-s-as-uid an "s" body where the server expects "" -- the +# server must reply with org.freedesktop.DBus.Error.InvalidArgs. +# Asserting that *positive* marker (not just "no AccessDenied") +# ensures we don't silently pass if setuid() failed or the client +# never reached the server (a transport error would print neither +# AccessDenied nor InvalidArgs). +say "Manager1.ListServices is reachable as non-root (not blocked by authz)" +set +e +result=$(texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 ListServices "" 2>&1) +set -e +case "$result" in + *AccessDenied*) fail "Non-root ListServices rejected by authz: $result" ;; + *InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;; + *) fail "Unexpected reply from non-root ListServices: $result" ;; +esac + +# ---------- Properties ---------- + +say "Introspect on /org/finit/manager advertises org.freedesktop.DBus.Properties" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) +case "$xml" in + *'org.freedesktop.DBus.Properties'*) assert "Properties interface in XML" 0 -eq 0 ;; + *) fail "Properties interface missing from XML" ;; +esac + +say "Manager1 declares Runlevel + Version as in introspection XML" +case "$xml" in + *'/dev/null \ + || fail "SetDebug returned non-zero" +# Toggle back so this test leaves debug in the same state we found it +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/dev/null || true +assert "SetDebug round-trip ok" 0 -eq 0 + +say "Manager1.SetDebug from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/tmp/dbus-setdbg.out 2>&1 +sdbg_rc=$? +set -e +assert "Non-root SetDebug rejected (rc=$sdbg_rc)" "$sdbg_rc" -eq 1 +case "$(cat /tmp/dbus-setdbg.out)" in + *AccessDenied*) assert "SetDebug authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-setdbg.out)" ;; +esac + +# Suspend would actually suspend the test sysroot if it succeeded -- so +# we only test the non-root rejection path, which fails before suspend() +# is called. +say "Manager1.Suspend from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 Suspend >/tmp/dbus-susp.out 2>&1 +susp_rc=$? +set -e +assert "Non-root Suspend rejected (rc=$susp_rc)" "$susp_rc" -eq 1 +case "$(cat /tmp/dbus-susp.out)" in + *AccessDenied*) assert "Suspend authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-susp.out)" ;; +esac + +say "initctl runlevel reads via Properties.Get when D-Bus available" +# runlevel output format is " ", e.g. "N 2". Just check +# we get a sensible two-token line. +rl=$(texec initctl runlevel) +case "$rl" in + [N0-9S]\ [0-9S]) + assert "initctl runlevel returned '$rl'" 0 -eq 0 ;; + *) + fail "Unexpected initctl runlevel output: $rl" ;; +esac diff --git a/test/dbus-service.sh b/test/dbus-service.sh new file mode 100755 index 00000000..9099a637 --- /dev/null +++ b/test/dbus-service.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# libink: org.finit.Service1 vtable + ServiceStateChanged signal. +# +# Covers per-service objects exposed at /org/finit/service/: +# GetService lookup, Introspect on a service object, Service1.Restart, +# authorization (non-root rejected), and the Manager1.ServiceStateChanged +# signal that Service1.Restart triggers. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Manager1.GetService(keventd) returns the encoded object path" +path=$(texec "$CLIENT" get-service "$BUS" keventd) +expected="/org/finit/service/keventd" +assert "GetService returned expected path (got: $path)" "$path" = "$expected" + +say "Introspect on the service object exposes Service1 methods" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/service/keventd) +case "$xml" in + *'org.finit.Service1'*'Restart'*) + assert "Service1.Restart visible in service-object XML" 0 -eq 0 ;; + *) + fail "Service1 not visible on /org/finit/service/keventd: $xml" ;; +esac + +say "Service1.Restart on /org/finit/service/keventd succeeds" +texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Restart >/dev/null \ + || fail "Service1.Restart returned non-zero" +assert "Per-service Restart ok" 0 -eq 0 + +say "Service1.Restart from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Restart >/tmp/dbus-svcauthz.out 2>&1 +svc_authz_rc=$? +set -e +assert "Non-root Service1.Restart rejected (rc=$svc_authz_rc)" \ + "$svc_authz_rc" -eq 1 +case "$(cat /tmp/dbus-svcauthz.out)" in + *AccessDenied*) assert "Service1 authz fires" 0 -eq 0 ;; + *) fail "Expected AccessDenied, got: $(cat /tmp/dbus-svcauthz.out)" ;; +esac + +say "Service1.Restart fires Manager1.ServiceStateChanged" +rm -f /tmp/dbus-sig.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \ + 5000 > /tmp/dbus-sig.out 2>&1 ) & +mon_pid=$! +sleep 0.5 +texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Restart >/dev/null \ + || fail "Restart trigger returned non-zero" +set +e +wait "$mon_pid" +mon_rc=$? +set -e +assert "monitor saw a signal (rc=$mon_rc)" "$mon_rc" -eq 0 +case "$(cat /tmp/dbus-sig.out)" in + *"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*) + assert "Signal payload contains the keventd identity" 0 -eq 0 ;; + *) + fail "Unexpected signal output: $(cat /tmp/dbus-sig.out)" ;; +esac diff --git a/test/lib/Makefile.am b/test/lib/Makefile.am index a281a920..ab85f6a4 100644 --- a/test/lib/Makefile.am +++ b/test/lib/Makefile.am @@ -1 +1 @@ -EXTRA_DIST = exec.sh setup.sh start.sh sysroot.mk +EXTRA_DIST = exec.sh setup.sh start.sh sysroot.mk dbus-setup.sh diff --git a/test/lib/dbus-setup.sh b/test/lib/dbus-setup.sh new file mode 100644 index 00000000..5b969da7 --- /dev/null +++ b/test/lib/dbus-setup.sh @@ -0,0 +1,20 @@ +# shellcheck shell=sh +# Shared preamble for the D-Bus smoke tests. Expects test/lib/setup.sh +# to have been sourced already (so texec, skip, retry, say, assert are +# available). Skips the test when the libink-driven client was not +# built, otherwise blocks until the bus socket appears. +# +# Exports: CLIENT, BUS. + +command -v texec >/dev/null \ + || { echo "dbus-setup.sh: source test/lib/setup.sh first" >&2; exit 99; } + +CLIENT=/sbin/dbus-auth-client +BUS=/run/finit/bus + +if ! texec test -x "$CLIENT"; then + skip "dbus-auth-client not built (configured with --disable-dbus?)" +fi + +say "Wait for $BUS to appear" +retry "texec test -S $BUS" diff --git a/test/setup-sysroot.sh b/test/setup-sysroot.sh index d6787f4d..f986125e 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -15,9 +15,28 @@ 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/" +fi # shellcheck disable=SC2154 -FINITBIN="$(pwd)/$top_builddir/src/finit" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk" +# 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" # 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 diff --git a/test/src/.gitignore b/test/src/.gitignore index fdb16e7f..14824ddc 100644 --- a/test/src/.gitignore +++ b/test/src/.gitignore @@ -3,3 +3,4 @@ /.libs/ /.deps/ /serv +/dbus-auth-client diff --git a/test/src/Makefile.am b/test/src/Makefile.am index 38b26490..87d6e362 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -7,3 +7,10 @@ serv_CPPFLAGS += -I$(top_srcdir)/libsystemd $(lite_CFLAGS) serv_SOURCES += $(top_srcdir)/libsystemd/sd-daemon.c serv_LDADD = $(lite_LIBS) endif + +if DBUS +noinst_PROGRAMS += dbus-auth-client +dbus_auth_client_SOURCES = dbus-auth-client.c +dbus_auth_client_CPPFLAGS = -D_GNU_SOURCE -I$(top_srcdir)/libink +dbus_auth_client_LDADD = $(top_builddir)/libink/libink.la +endif diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c new file mode 100644 index 00000000..fd0e3bb2 --- /dev/null +++ b/test/src/dbus-auth-client.c @@ -0,0 +1,448 @@ +/* Minimal D-Bus client used by the libink smoke tests. + * + * Modes: + * dbus-auth-client auth + * Send the SASL handshake claiming ; print server reply line. + * Exit 0 if reply begins "OK ", 1 if "REJECTED ", 2 otherwise. + * (Manual SASL: this mode exists *to* test AUTH itself.) + * + * dbus-auth-client hello + * Call org.freedesktop.DBus.Hello, print the assigned unique name. + * + * dbus-auth-client introspect + * Call org.freedesktop.DBus.Introspectable.Introspect, print XML. + * + * dbus-auth-client liststrings + * Call method expecting reply signature "as", print one per line. + * + * dbus-auth-client call-s + * dbus-auth-client call-void + * Issue method call with the given (or no) argument; "OK" or + * "ERROR: " on stderr. + * + * dbus-auth-client call-{s,void}-as-uid ... + * As above, but setuid() first so AUTH EXTERNAL claims . + * + * dbus-auth-client get-service + * Manager1.GetService(identity) -> print the encoded path. + * + * dbus-auth-client monitor-signal + * AddMatch + wait for one SIGNAL. Print "SIGNAL " + * then any string-typed body args. Exit 0 on signal, 1 on timeout. + * + * dbus-auth-client unknown + * Call a bogus method, exit 0 iff the server replies with an + * org.freedesktop.DBus.Error.* error. + * + * Exit codes for the non-auth modes: 0 on success, 1 on server-side + * error reply, 2 on transport / parse / arg error. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "link.h" + +/* ---------- manual SASL: only mode_auth uses this ---------- */ + +static const char hex[] = "0123456789abcdef"; + +static int write_all_fd(int fd, const void *buf, size_t len) +{ + const char *p = buf; + + while (len > 0) { + ssize_t n = write(fd, p, len); + if (n < 0) { + if (errno == EINTR) continue; + return -1; + } + p += n; + len -= (size_t)n; + } + return 0; +} + +static ssize_t read_line_fd(int fd, char *buf, size_t bufsz) +{ + size_t off = 0; + + while (off + 1 < bufsz) { + ssize_t n = read(fd, buf + off, 1); + if (n == 0) return -1; + if (n < 0) { + if (errno == EINTR) continue; + return -1; + } + if (buf[off] == '\n') { + buf[off] = '\0'; + if (off > 0 && buf[off - 1] == '\r') + buf[--off] = '\0'; + return (ssize_t)off; + } + off++; + } + return -1; +} + +static int mode_auth(int argc, char *argv[]) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + char hexuid[32], line[64], reply[256]; + const char *path, *claimed; + size_t i, claimed_len, plen; + int fd, rc; + + if (argc != 4) return 2; + path = argv[2]; + claimed = argv[3]; + + plen = strlen(path); + if (plen >= sizeof(sun.sun_path)) return 2; + claimed_len = strlen(claimed); + if (claimed_len * 2 >= sizeof(hexuid)) return 2; + for (i = 0; i < claimed_len; i++) { + unsigned c = (unsigned char)claimed[i]; + + hexuid[i * 2] = hex[c >> 4]; + hexuid[i * 2 + 1] = hex[c & 0xf]; + } + hexuid[claimed_len * 2] = '\0'; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { perror("socket"); return 2; } + memcpy(sun.sun_path, path, plen + 1); + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { + perror("connect"); close(fd); return 2; + } + if (write_all_fd(fd, "\0", 1) < 0) { close(fd); return 2; } + rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); + if (rc < 0 || (size_t)rc >= sizeof(line)) { close(fd); return 2; } + if (write_all_fd(fd, line, (size_t)rc) < 0) { close(fd); return 2; } + if (read_line_fd(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; } + printf("%s\n", reply); + close(fd); + if (strncmp(reply, "OK ", 3) == 0) return 0; + if (strncmp(reply, "REJECTED ", 9) == 0) return 1; + return 2; +} + +/* ---------- libink-driven modes ---------- */ + +/* Convert link_client_call rc to the test client's 0/1/2 convention, + * printing the error name on stderr for ERROR replies. */ +static int report_rc(link_client_t *c, int rc) +{ + if (rc == LINK_CALL_OK) + return 0; + if (rc == LINK_CALL_ERROR) { + const link_reply_t *r = link_client_reply(c); + fprintf(stderr, "ERROR: %s\n", + (r && r->error_name) ? r->error_name : ""); + return 1; + } + return 2; +} + +/* Drop effective uid to argv-supplied value (decimal). */ +static int drop_uid(const char *uid_arg, const char *progname) +{ + char *ep = NULL; + long v; + + errno = 0; + v = strtol(uid_arg, &ep, 10); + if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) { + fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg); + return 2; + } + if (setuid((uid_t)v) < 0) { + perror("setuid"); + return 2; + } + return 0; +} + +static int mode_hello(int argc, char *argv[]) +{ + link_client_t *c; + const char *name; + int rc; + + if (argc != 3) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + rc = report_rc(c, rc); + if (rc == 0) { + if (link_reply_get_string(link_client_reply(c), &name) == 0) + printf("%s\n", name); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +static int mode_introspect(int argc, char *argv[]) +{ + link_client_t *c; + const char *xml; + int rc; + + if (argc != 4) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], + "org.freedesktop.DBus.Introspectable", + "Introspect", NULL); + rc = report_rc(c, rc); + if (rc == 0) { + if (link_reply_get_string(link_client_reply(c), &xml) == 0) + printf("%s\n", xml); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +/* Decode body with signature "as", print one string per line. */ +static int print_string_array(const link_reply_t *r) +{ + link_reader_t reader; + size_t end; + + if (!r || !r->signature || strcmp(r->signature, "as") != 0) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) + return -1; + + while (link_r_pos(&reader) < end) { + const char *s; + + if (link_r_string(&reader, &s) < 0) + return -1; + printf("%s\n", s); + } + return 0; +} + +static int mode_liststrings(int argc, char *argv[]) +{ + link_client_t *c; + int rc; + + if (argc != 6) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], argv[4], argv[5], NULL); + rc = report_rc(c, rc); + if (rc == 0 && print_string_array(link_client_reply(c)) < 0) + rc = 2; + link_client_close(c); + return rc; +} + +/* Shared call helpers used by both the plain and the -as-uid modes. + * `arg` may be NULL (void method); when non-NULL the call signature + * is "s" with `arg` as the single string argument. */ +static int do_call(const char *sock, const char *obj, const char *iface, + const char *method, const char *arg) +{ + link_client_t *c; + int rc; + + c = link_client_open(sock); + if (!c) return 2; + + rc = arg + ? link_client_call_v(c, obj, iface, method, "s", arg) + : link_client_call_v(c, obj, iface, method, NULL); + rc = report_rc(c, rc); + if (rc == 0) + printf("OK\n"); + link_client_close(c); + return rc; +} + +static int mode_call_s(int argc, char *argv[]) +{ + if (argc != 7) return 2; + return do_call(argv[2], argv[3], argv[4], argv[5], argv[6]); +} + +static int mode_call_void(int argc, char *argv[]) +{ + if (argc != 6) return 2; + return do_call(argv[2], argv[3], argv[4], argv[5], NULL); +} + +static int mode_call_s_as_uid(int argc, char *argv[]) +{ + int rc; + + if (argc != 8) return 2; + if ((rc = drop_uid(argv[2], argv[0])) != 0) + return rc; + return do_call(argv[3], argv[4], argv[5], argv[6], argv[7]); +} + +static int mode_call_void_as_uid(int argc, char *argv[]) +{ + int rc; + + if (argc != 7) return 2; + if ((rc = drop_uid(argv[2], argv[0])) != 0) + return rc; + return do_call(argv[3], argv[4], argv[5], argv[6], NULL); +} + +static int mode_get_service(int argc, char *argv[]) +{ + link_client_t *c; + const char *path; + int rc; + + if (argc != 4) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "GetService", + "s", argv[3]); + rc = report_rc(c, rc); + if (rc == 0) { + /* Reply sig is "o", same wire form as "s". */ + if (link_reply_get_string(link_client_reply(c), &path) == 0) + printf("%s\n", path); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +static int mode_monitor_signal(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + char *ep = NULL; + long v; + int timeout_ms; + int rc; + + if (argc != 5) return 2; + + errno = 0; + v = strtol(argv[4], &ep, 10); + if (errno || !ep || *ep != '\0' || v <= 0 || v > 600000) { + fprintf(stderr, "%s: bad timeout: %s\n", argv[0], argv[4]); + return 2; + } + timeout_ms = (int)v; + + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", argv[3]); + if (rc != LINK_CALL_OK) { + rc = report_rc(c, rc); + link_client_close(c); + return rc; + } + + for (;;) { + rc = link_client_wait(c, timeout_ms); + if (rc != 0) { + link_client_close(c); + return 1; /* timeout or transport */ + } + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) + continue; + + printf("SIGNAL %s %s\n", + r->interface ? r->interface : "", + r->member ? r->member : ""); + /* Print any leading "s" args (other types are silently + * skipped -- callers test for the strings only). */ + link_reader_init(&reader, r->body, r->body_len); + if (r->signature) { + const char *s; + const char *p; + + for (p = r->signature; *p == 's'; p++) { + if (link_r_string(&reader, &s) < 0) + break; + printf("%s\n", s); + } + } + link_client_close(c); + return 0; + } +} + +static int mode_unknown(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + int rc; + + if (argc != 3) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "NotARealMethod", NULL); + if (rc != LINK_CALL_ERROR) { + link_client_close(c); + return rc == LINK_CALL_OK ? 1 : 2; + } + + r = link_client_reply(c); + { + static const char prefix[] = "org.freedesktop.DBus.Error."; + rc = (r && r->error_name && + strncmp(r->error_name, prefix, sizeof(prefix) - 1) == 0) + ? 0 : 1; + } + link_client_close(c); + return rc; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) return 2; + if (!strcmp(argv[1], "auth")) return mode_auth (argc, argv); + if (!strcmp(argv[1], "hello")) return mode_hello (argc, argv); + if (!strcmp(argv[1], "introspect")) return mode_introspect (argc, argv); + if (!strcmp(argv[1], "liststrings")) return mode_liststrings (argc, argv); + if (!strcmp(argv[1], "call-s")) return mode_call_s (argc, argv); + if (!strcmp(argv[1], "call-void")) return mode_call_void (argc, argv); + if (!strcmp(argv[1], "call-s-as-uid")) return mode_call_s_as_uid (argc, argv); + if (!strcmp(argv[1], "call-void-as-uid")) return mode_call_void_as_uid(argc, argv); + if (!strcmp(argv[1], "get-service")) return mode_get_service (argc, argv); + if (!strcmp(argv[1], "monitor-signal")) return mode_monitor_signal (argc, argv); + if (!strcmp(argv[1], "unknown")) return mode_unknown (argc, argv); + fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); + return 2; +}