From ebc0ef62e6d368da9db6b5751de708ace79dd14c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:14 +0200 Subject: [PATCH] libink/finit: properties, and org.finit on the system bus Runlevel and version are state, not actions, so they belong behind org.freedesktop.DBus.Properties rather than another method each. Finit also claims org.finit on the system bus when it finds one, so ordinary D-Bus clients can reach it without knowing about /run/finit/bus. Opportunistic on purpose: no dbus-daemon is a normal state for the systems Finit runs on, not an error to report. Signed-off-by: Joachim Wiberg --- Makefile.am | 2 +- configure.ac | 1 + dbus-1/.gitignore | 2 + dbus-1/Makefile.am | 6 + dbus-1/org.finit.conf | 63 ++++++ doc/ChangeLog.md | 49 +++++ doc/dbus.md | 294 ++++++++++++++++++++++++++ libink/builtin.c | 196 ++++++++++++++++- libink/client.c | 58 +++++- libink/dispatch.c | 4 + libink/link.h | 70 ++++++- libink/marshal.c | 61 ++++++ libink/marshal.h | 4 + libink/server.c | 42 ++++ mkdocs.yml | 1 + src/dbus.c | 406 ++++++++++++++++++++++++++++++------ src/initctl.c | 240 +++++++++++++++++---- src/private.h | 1 + src/sm.c | 3 + test/dbus-initctl.sh | 28 +++ test/dbus-manager.sh | 66 ++++++ test/src/dbus-auth-client.c | 31 +-- 22 files changed, 1474 insertions(+), 154 deletions(-) create mode 100644 dbus-1/.gitignore create mode 100644 dbus-1/Makefile.am create mode 100644 dbus-1/org.finit.conf create mode 100644 doc/dbus.md diff --git a/Makefile.am b/Makefile.am index bc3b885d..b3afffea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,7 +8,7 @@ ACLOCAL_AMFLAGS = -I m4 # after src is fine. SUBDIRS = man plugins if DBUS -SUBDIRS += libink +SUBDIRS += libink dbus-1 endif SUBDIRS += src system tmpfiles.d dist_doc_DATA = README.md LICENSE contrib/finit.conf diff --git a/configure.ac b/configure.ac index 10182af5..dcd02548 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,7 @@ 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 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 5b25a99b..b6c21350 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -20,6 +20,55 @@ All relevant changes are documented in this file. same service, `command = { "/lib/systemd/systemd-udevd", "-udevd" }`, and Finit starts the first one it finds. The line-based format could only express this by repeating the whole stanza per candidate +- 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/builtin.c b/libink/builtin.c index 715157db..0d7a677b 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -104,24 +104,53 @@ static void xprintf(struct xbuf *x, const char *fmt, ...) x->off += (size_t)n; } +/* + * Advance past one single complete type in a D-Bus signature: + * a basic type code, 'a' + element type, or a bracketed group. + * Signatures come from our own vtables, so trust them; an + * unterminated group just stops at NUL. + */ +static const char *sig_next(const char *p) +{ + while (*p == 'a') /* array prefixes, then element type */ + p++; + if (*p == '(' || *p == '{') { + char close = *p == '(' ? ')' : '}'; + + for (p++; *p && *p != close; p = sig_next(p)) + ; + } + return *p ? p + 1 : p; /* NUL: unterminated group, stop here */ +} + +static void emit_args(struct xbuf *x, const char *sig, const char *dir) +{ + const char *p, *e; + + for (p = sig; p && *p; p = e) { + e = sig_next(p); + xprintf(x, " \n", + (int)(e - p), p, dir); + } +} + /* 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); + emit_args(x, m->in_sig, "in"); + emit_args(x, m->out_sig, "out"); xprintf(x, " \n"); } -/* 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 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"); +} static const char STANDARD_INTERFACES_XML[] = " \n" @@ -134,6 +163,17 @@ static const char STANDARD_INTERFACES_XML[] = " \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 @@ -197,11 +237,16 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) 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"); } } @@ -231,6 +276,129 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) 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) @@ -310,5 +478,11 @@ int __handle_builtin(link_connection_t *conn, const struct link_msg *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 index a5c245a1..fbfbe70f 100644 --- a/libink/client.c +++ b/libink/client.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,7 @@ struct link_client { size_t rxlen; }; -link_client_t *link_client_open(const char *path) +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; @@ -48,6 +49,20 @@ link_client_t *link_client_open(const char *path) 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; @@ -67,6 +82,11 @@ link_client_t *link_client_open(const char *path) 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) @@ -76,6 +96,18 @@ void link_client_close(link_client_t *c) 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)) @@ -229,6 +261,30 @@ const link_reply_t *link_client_reply(link_client_t *c) 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. */ diff --git a/libink/dispatch.c b/libink/dispatch.c index 8b65c4b2..9d3d322b 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -298,6 +298,7 @@ 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); } @@ -311,6 +312,9 @@ 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; } diff --git a/libink/link.h b/libink/link.h index 9a59c41e..01a61c11 100644 --- a/libink/link.h +++ b/libink/link.h @@ -96,6 +96,18 @@ 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); @@ -116,9 +128,22 @@ typedef struct { 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 *interface; /* e.g. "org.finit.Manager1" */ - const link_method_t *methods; /* terminated by {NULL, ...} */ + 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 @@ -189,8 +214,23 @@ int link_connection_emit_signal(link_connection_t *conn, * 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 */ @@ -234,6 +274,14 @@ int link_client_call_v(link_client_t *c, 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(). @@ -264,6 +312,7 @@ 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); @@ -280,12 +329,21 @@ 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); -/* Byte offset of the next read inside the original body buffer. Used - * to detect end-of-array when walking "a" payloads: read the array - * byte-length prefix with link_r_u32 first, record (pos+length) as the - * end, then loop while link_r_pos < end. */ +/* 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 diff --git a/libink/marshal.c b/libink/marshal.c index d22c7c8c..ae215893 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -102,6 +102,14 @@ void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 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) { @@ -254,7 +262,60 @@ static int read_string_like(struct link_reader *r, const char **out) 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 index b7717813..313205b2 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -23,6 +23,7 @@ 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. */ @@ -43,6 +44,9 @@ 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/server.c b/libink/server.c index ef6325ae..f5a82431 100644 --- a/libink/server.c +++ b/libink/server.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -160,3 +161,44 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) *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 2ccce539..674a554a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,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/dbus.c b/src/dbus.c index 94dc575a..109c8a5e 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -31,9 +31,10 @@ #ifdef HAVE_DBUS #include +#include #include #include -#include +#include #include #include @@ -50,6 +51,7 @@ #include "sig.h" #include "sm.h" #include "svc.h" +#include "util.h" #define DBUS_MAX_PEERS 64 @@ -88,6 +90,39 @@ static void peer_cb(uev_t *w, void *arg, int events) 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; @@ -99,7 +134,6 @@ static void accept_cb(uev_t *w, void *arg, int events) for (;;) { link_connection_t *conn = NULL; - struct peer *p; if (link_server_accept(server, &conn) < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) @@ -107,29 +141,8 @@ static void accept_cb(uev_t *w, void *arg, int events) break; } - if (peer_count >= DBUS_MAX_PEERS) { - logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", - peer_count); - link_connection_close(conn); - continue; - } - - p = calloc(1, sizeof(*p)); - if (!p) { - link_connection_close(conn); - err(1, "Out of memory accepting D-Bus client"); - break; - } - - p->conn = conn; - TAILQ_INSERT_TAIL(&peers, p, link); - peer_count++; - - if (uev_io_init(w->ctx, &p->watcher, peer_cb, p, - link_connection_get_fd(conn), UEV_READ)) { - err(1, "Failed registering D-Bus peer watcher"); - peer_drop(p); - } + if (!peer_register(w->ctx, conn)) + continue; /* logged inside */ } } @@ -240,6 +253,7 @@ static int dbus_apply_restart(svc_t *svc, void *user_data) struct dispatch_ctx { int (*action)(svc_t *, void *); + void *udata; int matched; }; @@ -248,7 +262,7 @@ static int dispatch_found(svc_t *svc, void *udata) struct dispatch_ctx *ctx = udata; ctx->matched++; - return ctx->action(svc, NULL); + return ctx->action(svc, ctx->udata); } static int dispatch_missing(char *job, char *id, void *udata) @@ -257,14 +271,15 @@ static int dispatch_missing(char *job, char *id, void *udata) return 0; /* don't penalise the return; we'll check ->matched */ } -/* Apply `action` 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). */ +/* 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 *)) + int (*action)(svc_t *, void *), void *udata) { char buf[MAX_IDENT_LEN]; - struct dispatch_ctx ctx = { .action = action }; + struct dispatch_ctx ctx = { .action = action, .udata = udata }; int rc; if (!ident || !*ident || strlen(ident) >= sizeof(buf)) @@ -281,14 +296,21 @@ static int manager_take_string_method(link_call_t *call, int (*action)(svc_t *, void *)) { const char *ident; + int rc; 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) != 0) + + rc = dispatch_action(ident, action, NULL); + if (rc < 0) return link_call_reply_error(call, "org.finit.Error.NoSuchService", ident); + if (rc) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "failed on matched service(s)"); (void)link_call_reply(call); /* empty reply */ return 0; @@ -351,6 +373,117 @@ static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shut 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, rc; + + (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; + rc = dispatch_action(ident, signal_one, &sig); + if (rc < 0) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + if (rc) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "failed signalling matched service(s)"); + + (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 }, @@ -372,12 +505,19 @@ static const link_method_t manager_methods[] = { .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, + .interface = "org.finit.Manager1", + .methods = manager_methods, + .properties = manager_properties, }; /* ---------- org.finit.Service1 (one object per service) ---------- @@ -485,6 +625,38 @@ void dbus_unregister_service(svc_t *svc) (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, *tmp; + + if (!server || TAILQ_EMPTY(&peers)) + return; + TAILQ_FOREACH_SAFE(p, &peers, link, tmp) { + if (link_connection_emit_signal(p->conn, + path, interface, member, + signature, body, body_len) < 0) { + /* nothing hit the wire, and same for every peer */ + if (errno == EMSGSIZE || errno == EINVAL) + break; + logit(LOG_WARNING, "D-Bus peer fd %d write failed: " + "%s, dropping", + link_connection_get_fd(p->conn), + strerror(errno)); + peer_drop(p); + } + } +} + /* ---------- signal emission: ServiceStateChanged ---------- */ /* @@ -518,36 +690,51 @@ static const char *state_name(svc_state_t s) void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) { - uint8_t body[256]; - link_writer_t w; - struct peer *p; - char ident[MAX_IDENT_LEN]; - ssize_t blen; - svc_state_t o = (svc_state_t)old_state; - svc_state_t n = (svc_state_t)new_state; + uint8_t body[256]; + link_writer_t w; + char ident[MAX_IDENT_LEN]; + ssize_t blen; - if (!server || !svc) + if (!svc) return; - if (TAILQ_EMPTY(&peers)) - return; /* nobody could possibly be listening */ - svc_ident(svc, ident, sizeof(ident)); link_writer_init(&w, body, sizeof(body)); link_w_string(&w, ident); - link_w_string(&w, state_name(o)); - link_w_string(&w, state_name(n)); + 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; - TAILQ_FOREACH(p, &peers, link) - (void)link_connection_emit_signal(p->conn, - "/org/finit/manager", - "org.finit.Manager1", - "ServiceStateChanged", - "sss", - body, (size_t)blen); + 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 ---------- */ @@ -766,14 +953,11 @@ static const link_vtable_t cond_vtable = { void dbus_notify_condition_change(const char *name, const char *state) { - uint8_t body[256]; - link_writer_t w; - struct peer *p; - ssize_t blen; + uint8_t body[256]; + link_writer_t w; + ssize_t blen; - if (!server || !name || !state) - return; - if (TAILQ_EMPTY(&peers)) + if (!name || !state) return; link_writer_init(&w, body, sizeof(body)); @@ -783,13 +967,97 @@ void dbus_notify_condition_change(const char *name, const char *state) if (blen < 0) return; - TAILQ_FOREACH(p, &peers, link) - (void)link_connection_emit_signal(p->conn, - COND_PATH_OBJECT, - COND_INTERFACE, - "ConditionChanged", - "ss", - body, (size_t)blen); + 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 ---------- */ @@ -839,6 +1107,8 @@ int dbus_init(uev_ctx_t *ctx) dbus_register_service(svc); } + try_attach_system_bus(ctx); + return 0; } diff --git a/src/initctl.c b/src/initctl.c index 71f14263..6379bc9a 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -41,6 +41,10 @@ #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; @@ -148,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 = { @@ -155,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)); } @@ -196,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 = { @@ -208,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: @@ -272,22 +367,46 @@ static int do_startstop(int cmd, char *arg) } #ifdef HAVE_DBUS -#include "link.h" + +/* 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 replied with an error -- callers should error out * -1 D-Bus not reachable -- callers should fall back to the * legacy INIT_SOCKET transport - * - * On D-Bus error replies the function maps the org.* error name to - * the same exit code initctl historically printed for that case - * (e.g. NoSuchService -> 69 with the legacy message). */ + * 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; - const char *err; int rc; c = link_client_open(FINIT_BUS_SOCKET); @@ -306,29 +425,37 @@ static int try_dbus_manager(const char *method, const char *arg_sig, else rc = LINK_CALL_FAIL; - if (rc == LINK_CALL_ERROR) { - const link_reply_t *r = link_client_reply(c); - - err = (r && r->error_name) ? r->error_name : ""; - /* Exact match on the fully-qualified error name; substring - * matching would misfire on a future name that contains - * one of these as a substring. */ - if (!strcmp(err, "org.finit.Error.NoSuchService")) { - link_client_close(c); - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", - arg ? arg : ""); - } - if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) { - link_client_close(c); - ERRX(1, "permission denied: %s requires root", method); - } - link_client_close(c); - ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); - } + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, arg); /* exits */ link_client_close(c); - if (rc == LINK_CALL_OK) - return 0; - return -1; /* LINK_CALL_FAIL or anything else: fall back */ + 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 @@ -357,14 +484,9 @@ static int cond_dbus_call(link_client_t **bus, condop_t op, "s", arg); if (rc == LINK_CALL_OK) { if (op == COND_GET) { - const link_reply_t *r = link_client_reply(*bus); - link_reader_t reader; - const char *state = NULL; + const char *state = NULL; - if (r && r->body) { - link_reader_init(&reader, r->body, r->body_len); - link_r_string(&reader, &state); - } + link_reply_get_string(link_client_reply(*bus), &state); if (verbose && state) puts(state); *out_exit = (state && !strcmp(state, "on")) ? 0 @@ -493,6 +615,12 @@ static int do_reload (char *arg) 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); } @@ -530,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; @@ -543,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; @@ -926,8 +1073,17 @@ int do_poweroff(char *arg) return do_cmd(INIT_CMD_POWEROFF); } -/* Suspend has no Manager1 equivalent yet; uses the legacy IPC. */ -int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); } +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) diff --git a/src/private.h b/src/private.h index 3b03f510..ae501161 100644 --- a/src/private.h +++ b/src/private.h @@ -53,6 +53,7 @@ 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); 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 @@ restart: 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/test/dbus-initctl.sh b/test/dbus-initctl.sh index c51c1a3e..2a1b47c1 100755 --- a/test/dbus-initctl.sh +++ b/test/dbus-initctl.sh @@ -66,3 +66,31 @@ case "$(cat /tmp/dbus-initctl-cond.out)" in *) 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 index e26b4c11..c3bdce33 100755 --- a/test/dbus-manager.sh +++ b/test/dbus-manager.sh @@ -67,3 +67,69 @@ case "$result" in *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/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 53f85223..fd0e3bb2 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -175,8 +175,6 @@ static int drop_uid(const char *uid_arg, const char *progname) static int mode_hello(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *name; int rc; @@ -188,9 +186,7 @@ static int mode_hello(int argc, char *argv[]) "org.freedesktop.DBus", "Hello", NULL); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - if (link_r_string(&reader, &name) == 0) + if (link_reply_get_string(link_client_reply(c), &name) == 0) printf("%s\n", name); else rc = 2; @@ -202,8 +198,6 @@ static int mode_hello(int argc, char *argv[]) static int mode_introspect(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *xml; int rc; @@ -216,9 +210,7 @@ static int mode_introspect(int argc, char *argv[]) "Introspect", NULL); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - if (link_r_string(&reader, &xml) == 0) + if (link_reply_get_string(link_client_reply(c), &xml) == 0) printf("%s\n", xml); else rc = 2; @@ -227,23 +219,17 @@ static int mode_introspect(int argc, char *argv[]) return rc; } -/* Decode body with signature "as" -- u32 array byte-len, then "s" strings. - * libink's public reader doesn't yet have an array helper, so we walk - * the wire form with link_r_u32 + link_r_string + link_r_pos. */ +/* Decode body with signature "as", print one string per line. */ static int print_string_array(const link_reply_t *r) { link_reader_t reader; - uint32_t array_len; 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_u32(&reader, &array_len) < 0) - return -1; - end = link_r_pos(&reader) + array_len; - if (end > r->body_len) + if (link_r_array_begin(&reader, &end) < 0) return -1; while (link_r_pos(&reader) < end) { @@ -330,8 +316,6 @@ static int mode_call_void_as_uid(int argc, char *argv[]) static int mode_get_service(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *path; int rc; @@ -344,11 +328,8 @@ static int mode_get_service(int argc, char *argv[]) "s", argv[3]); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - /* Reply signature is "o" but link_r_path / link_r_string - * have the same wire form. */ - if (link_r_path(&reader, &path) == 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;