From 0a269f329814741d631e8c78880726c6fc162e6f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:14 +0200 Subject: [PATCH 01/22] libink: a brokerless D-Bus implementation for Finit Finit had no way to answer the question every service manager gets asked: what is running, and change it. D-Bus is how the rest of userspace asks, but linking libdbus, sd-bus or GIO into PID 1 buys a dependency, an allocator and a main loop we do not control. So libink: the wire format, an object tree, and a bus of Finit's own at /run/finit/bus, gated like INIT_SOCKET. It speaks the standard org.freedesktop.DBus, .Peer, .Introspectable interfaces, and Finit's own Manager1, Service1 and Cond1 on top. Methods that change something are marked privileged and answered only for a caller the kernel vouched for, via SO_PEERCRED. Server and client both, since initctl is the first thing that needs to talk to it, and its Start/Stop/Restart/Reload now go over the bus rather than the legacy socket. Signed-off-by: Joachim Wiberg --- Makefile.am | 13 +- configure.ac | 10 + libink/.gitignore | 8 + libink/Makefile.am | 22 + libink/auth.c | 300 +++++++++++++ libink/builtin.c | 314 +++++++++++++ libink/client.c | 295 ++++++++++++ libink/connection.c | 119 +++++ libink/dispatch.c | 417 +++++++++++++++++ libink/internal.h | 141 ++++++ libink/io.c | 53 +++ libink/libink.pc.in | 10 + libink/link.h | 295 ++++++++++++ libink/marshal.c | 260 +++++++++++ libink/marshal.h | 48 ++ libink/match.c | 199 +++++++++ libink/path.c | 43 ++ libink/path.h | 21 + libink/proto.c | 387 ++++++++++++++++ libink/proto.h | 101 +++++ libink/server.c | 162 +++++++ src/Makefile.am | 11 + src/api.c | 33 +- src/cond-w.c | 16 + src/dbus.c | 869 ++++++++++++++++++++++++++++++++++++ src/finit.c | 5 + src/finit.h | 1 + src/initctl.c | 136 +++++- src/private.h | 9 + src/service.c | 66 ++- src/service.h | 1 + src/svc.c | 9 + test/Makefile.am | 4 + test/check.sh | 4 +- test/dbus-auth.sh | 315 +++++++++++++ test/setup-sysroot.sh | 15 +- test/src/.gitignore | 1 + test/src/Makefile.am | 5 + test/src/dbus-auth-client.c | 861 +++++++++++++++++++++++++++++++++++ 39 files changed, 5530 insertions(+), 49 deletions(-) create mode 100644 libink/.gitignore create mode 100644 libink/Makefile.am create mode 100644 libink/auth.c create mode 100644 libink/builtin.c create mode 100644 libink/client.c create mode 100644 libink/connection.c create mode 100644 libink/dispatch.c create mode 100644 libink/internal.h create mode 100644 libink/io.c create mode 100644 libink/libink.pc.in create mode 100644 libink/link.h create mode 100644 libink/marshal.c create mode 100644 libink/marshal.h create mode 100644 libink/match.c create mode 100644 libink/path.c create mode 100644 libink/path.h create mode 100644 libink/proto.c create mode 100644 libink/proto.h create mode 100644 libink/server.c create mode 100644 src/dbus.c create mode 100755 test/dbus-auth.sh create mode 100644 test/src/dbus-auth-client.c diff --git a/Makefile.am b/Makefile.am index a5333be6..bc3b885d 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 +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 0b213b53..10182af5 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 + libink/Makefile libink/libink.pc libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile plugins/Makefile @@ -94,6 +95,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]) @@ -243,6 +248,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], @@ -438,6 +447,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/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..715157db --- /dev/null +++ b/libink/builtin.c @@ -0,0 +1,314 @@ +/* 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"); +} + +/* 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"; + +/* 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) { + xprintf(&x, " \n", + e->vt->interface); + if (e->vt->methods) + for (meth = e->vt->methods; meth->name; meth++) + emit_method(&x, meth); + 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); +} + +/* ---------- 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); + + return -1; /* not a built-in */ +} diff --git a/libink/client.c b/libink/client.c new file mode 100644 index 00000000..a5c245a1 --- /dev/null +++ b/libink/client.c @@ -0,0 +1,295 @@ +/* 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 "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(const char *path) +{ + 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 (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; +} + +void link_client_close(link_client_t *c) +{ + if (!c) + return; + if (c->fd >= 0) + close(c->fd); + free(c); +} + +/* 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; +} + +/* 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..8b65c4b2 --- /dev/null +++ b/libink/dispatch.c @@ -0,0 +1,417 @@ +/* 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 ---------- */ + +/* + * Peer fds are non-blocking, so send_all may fail mid-frame (e.g. + * EAGAIN from a peer that stopped draining its socket). Any failure + * poisons the peer's stream: never retry on the same connection, + * drop the peer. + */ +#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_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_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..7528db5c --- /dev/null +++ b/libink/internal.h @@ -0,0 +1,141 @@ +/* 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 EINTR-resilient I/O loops. */ +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..7f95eafc --- /dev/null +++ b/libink/io.c @@ -0,0 +1,53 @@ +/* libink — shared 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. On any other error they return -1 with an unknown + * number of bytes already transferred. + * + * 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..9a59c41e --- /dev/null +++ b/libink/link.h @@ -0,0 +1,295 @@ +/* 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); + +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; + +typedef struct { + const char *interface; /* e.g. "org.finit.Manager1" */ + const link_method_t *methods; /* terminated by {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 with + * errno set on failure: EMSGSIZE and EINVAL mean nothing hit the + * wire and the connection is still usable; anything else is a + * transport failure that may have left a partial frame -- the + * caller must drop the peer. */ +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); +void link_client_close(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); + +/* 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_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_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. */ +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..d22c7c8c --- /dev/null +++ b/libink/marshal.c @@ -0,0 +1,260 @@ +/* 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); } + +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); } + +int __r_done(const struct link_reader *r) +{ + return !r->err && r->off == r->cap; +} diff --git a/libink/marshal.h b/libink/marshal.h new file mode 100644 index 00000000..b7717813 --- /dev/null +++ b/libink/marshal.h @@ -0,0 +1,48 @@ +/* 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" */ + +/* 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_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..ef6325ae --- /dev/null +++ b/libink/server.c @@ -0,0 +1,162 @@ +/* libink — listening socket, accept, peer-credential capture + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#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; +} diff --git a/src/Makefile.am b/src/Makefile.am index df8a8dfd..0c065e6a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -81,6 +81,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 @@ -94,6 +97,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 \ @@ -101,6 +108,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 b60a58d9..be14e215 100644 --- a/src/api.c +++ b/src/api.c @@ -113,38 +113,7 @@ static int restart(svc_t *svc, void *user_data) static int reload(svc_t *svc, void *user_data) { (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)) { - svc_cond_clear(svc); - 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 b3662d6e..fc3d2150 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; @@ -353,6 +360,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); } @@ -383,6 +393,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); } @@ -404,6 +417,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..94dc575a --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,869 @@ +/* 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 "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" + +#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); +} + +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; + struct peer *p; + + if (link_server_accept(server, &conn) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + err(1, "Failed accepting D-Bus client"); + 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); + } + } +} + +/* ---------- 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[MAX_IDENT_LEN]; + + 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 *); + int matched; +}; + +static int dispatch_found(svc_t *svc, void *udata) +{ + struct dispatch_ctx *ctx = udata; + + ctx->matched++; + return ctx->action(svc, NULL); +} + +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` 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 *)) +{ + char buf[MAX_IDENT_LEN]; + struct dispatch_ctx ctx = { .action = action }; + 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) != 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; + /* + * Same semantics as api.c: harmless no-op during bootstrap + * and shutdown, the client still sees success. + */ + if (IS_RESERVED_RUNLEVEL(runlevel)) + warnx("Ignoring reload in runlevel S and 6/0."); + else + 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 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 }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_vtable_t manager_vtable = { + .interface = "org.finit.Manager1", + .methods = manager_methods, +}; + +/* ---------- 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 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 -Wall (-Wswitch) flags the + * missing case. + */ +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; + 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; + + if (!server || !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)); + 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); +} + +/* ---------- 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; + struct peer *p; + ssize_t blen; + + if (!server || !name || !state) + return; + if (TAILQ_EMPTY(&peers)) + 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; + + TAILQ_FOREACH(p, &peers, link) + (void)link_connection_emit_signal(p->conn, + COND_PATH_OBJECT, + COND_INTERFACE, + "ConditionChanged", + "ss", + body, (size_t)blen); +} + +/* ---------- 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); + } + + 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 96061dd7..8c0e3dc8 100644 --- a/src/finit.c +++ b/src/finit.c @@ -780,6 +780,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 f0dbcf86..0fcb408f 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -268,19 +268,105 @@ 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 +#include "link.h" + +/* 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). */ +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); + 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) { + 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"); + } + link_client_close(c); + if (rc == LINK_CALL_OK) + return 0; + return -1; /* LINK_CALL_FAIL or anything else: fall back */ +} +#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); + } 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); @@ -643,9 +729,47 @@ 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); } +#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); +} + +/* Suspend has no Manager1 equivalent yet; uses the legacy IPC. */ int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); } /** diff --git a/src/private.h b/src/private.h index 1612803b..3b03f510 100644 --- a/src/private.h +++ b/src/private.h @@ -45,6 +45,15 @@ 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); +#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 55f3b858..abb1a00e 100644 --- a/src/service.c +++ b/src/service.c @@ -1443,17 +1443,65 @@ 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) +{ + 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)) { + svc_cond_clear(svc); + 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; @@ -3015,6 +3063,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 + /* * The unit has stopped: HALTED comes after any post:/cleanup: * script, DONE is a completed run/task, where remain-after-exit @@ -3384,7 +3436,7 @@ int service_step(svc_t *svc) if (sm_in_reload()) break; - service_reload(svc); + service_reload_apply(svc); } svc_mark_clean(svc); @@ -3422,7 +3474,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 f8b94fe5..4d6038a8 100644 --- a/src/service.h +++ b/src/service.h @@ -59,6 +59,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/svc.c b/src/svc.c index cbe29cb5..a1fe9c99 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" @@ -153,6 +154,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; } @@ -170,6 +175,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 a12c296b..f4b04ee5 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -71,6 +71,7 @@ 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 AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -126,6 +127,9 @@ if TESTSERV TESTS += testserv.sh endif TESTS += unexpected-restart.sh +if DBUS +TESTS += dbus-auth.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..27bf6bea --- /dev/null +++ b/test/dbus-auth.sh @@ -0,0 +1,315 @@ +#!/bin/sh +# End-to-end smoke test for libink: +# - AUTH EXTERNAL handshake (happy and wrong-uid paths) +# - org.freedesktop.DBus.Hello +# - org.freedesktop.DBus.Introspectable.Introspect (root, manager) +# - org.finit.Manager1.ListServices +# - Error reply for an unknown method. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +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" + +say "Socket mode is 0666" +mode=$(texec stat -c %a "$BUS") +assert "Socket mode is 666 (got $mode)" "$mode" = "666" + +# ---------- AUTH ---------- + +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" + +# ---------- Built-in interfaces ---------- + +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 referencing /manager" +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 + +# ---------- Real method call ---------- + +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" + +# ---------- Method with arguments ---------- + +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 + +# ---------- Authorization ---------- + +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 + +say "Manager1.ListServices is reachable as non-root (not blocked by authz)" +# call-s-as-uid sends an "s" body; ListServices expects "", so 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 (e.g. a transport error would print +# neither AccessDenied nor InvalidArgs). +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 + +# ---------- Per-service objects (Service1) ---------- + +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 + +# ---------- Signals ---------- + +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 + +# ---------- Cond1 ---------- + +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 + +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 + +# ---------- initctl port ---------- + +# initctl now talks to /run/finit/bus when available. Verify by +# subscribing to ServiceStateChanged on a background monitor and +# then running initctl restart -- if D-Bus is in use, the signal +# fires. If the legacy socket were still in use, the dbus subscriber +# would see nothing. + +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 + +# ---------- Error reply ---------- + +say "Unknown method 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/setup-sysroot.sh b/test/setup-sysroot.sh index d6787f4d..102cbad2 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -15,9 +15,22 @@ make -C "$top_builddir" DESTDIR="$SYSROOT" install mkdir -p "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" +if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then + cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" +fi # shellcheck disable=SC2154 -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..b8d607d1 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -7,3 +7,8 @@ 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 +endif diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c new file mode 100644 index 00000000..00e275a4 --- /dev/null +++ b/test/src/dbus-auth-client.c @@ -0,0 +1,861 @@ +/* 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. + * + * dbus-auth-client hello + * Auth as own uid; call org.freedesktop.DBus.Hello on + * /org/freedesktop/DBus. Print the assigned unique name. + * + * dbus-auth-client introspect + * Auth + org.freedesktop.DBus.Introspectable.Introspect. + * Print the XML reply. + * + * dbus-auth-client liststrings + * Auth + method call expecting reply signature "as"; print one + * string per line. + * + * dbus-auth-client unknown + * Auth + call a bogus method; exits 0 only if the server replies + * with an "org.freedesktop.DBus.Error.*" error. + * + * In every non-auth mode the program exits 0 on a successful method + * reply, 1 on a server-side error reply, 2 on transport / parse error. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char hex[] = "0123456789abcdef"; + +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +/* ---------- low level I/O ---------- */ + +static int 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; +} + +static int 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; +} + +static int read_with_timeout(int fd, void *buf, size_t len, int timeout_ms) +{ + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + int rc; + + for (;;) { + rc = poll(&pfd, 1, timeout_ms); + if (rc < 0) { + if (errno == EINTR) + continue; + return -1; + } + if (rc == 0) + return 0; /* timed out */ + break; + } + return (int)read(fd, buf, len); +} + +static ssize_t 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; +} + +/* ---------- connect + AUTH ---------- */ + +static int connect_and_auth(const char *path, uid_t claimed_uid) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + char uidstr[16]; + char hexuid[32]; + char line[64]; + char reply[256]; + size_t i, n; + int fd, rc; + + if (strlen(path) >= sizeof(sun.sun_path)) + return -1; + memcpy(sun.sun_path, path, strlen(path) + 1); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { + close(fd); + return -1; + } + + n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)claimed_uid); + for (i = 0; i < n; i++) { + unsigned c = (unsigned char)uidstr[i]; + + hexuid[i * 2] = hex[c >> 4]; + hexuid[i * 2 + 1] = hex[c & 0xf]; + } + hexuid[n * 2] = '\0'; + + if (write_all(fd, "\0", 1) < 0) goto io; + + rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); + if (rc < 0 || (size_t)rc >= sizeof(line)) goto io; + if (write_all(fd, line, (size_t)rc) < 0) goto io; + + if (read_line(fd, reply, sizeof(reply)) < 0) goto io; + if (strncmp(reply, "OK ", 3) != 0) { + fprintf(stderr, "auth failed: %s\n", reply); + close(fd); + return -1; + } + + if (write_all(fd, "BEGIN\r\n", 7) < 0) goto io; + return fd; + +io: + perror("auth handshake"); + close(fd); + return -1; +} + +/* ---------- D-Bus message build / parse ---------- */ + +struct buf { + uint8_t *p; + size_t cap; + size_t off; + int err; +}; + +static int b_reserve(struct buf *b, size_t align, size_t bytes) +{ + size_t pad = ALIGN_UP(b->off, align) - b->off; + + if (b->err || b->off + pad + bytes > b->cap) { + b->err = 1; + return -1; + } + while (pad--) b->p[b->off++] = 0; + return 0; +} + +static void b_put_u32(struct buf *b, uint32_t v) +{ + if (b_reserve(b, 4, 4) < 0) return; + b->p[b->off++] = (uint8_t)(v & 0xff); + b->p[b->off++] = (uint8_t)((v >> 8) & 0xff); + b->p[b->off++] = (uint8_t)((v >> 16) & 0xff); + b->p[b->off++] = (uint8_t)((v >> 24) & 0xff); +} + +static void b_put_byte(struct buf *b, uint8_t v) +{ + if (b_reserve(b, 1, 1) < 0) return; + b->p[b->off++] = v; +} + +static void b_put_string(struct buf *b, const char *s) +{ + size_t len = strlen(s); + + if (b_reserve(b, 4, 4 + len + 1) < 0) return; + b_put_u32(b, (uint32_t)len); + memcpy(b->p + b->off, s, len); + b->off += len; + b->p[b->off++] = 0; +} + +static void b_put_signature(struct buf *b, const char *s) +{ + size_t len = strlen(s); + + if (b_reserve(b, 1, 1 + len + 1) < 0) return; + b->p[b->off++] = (uint8_t)len; + memcpy(b->p + b->off, s, len); + b->off += len; + b->p[b->off++] = 0; +} + +/* Send a method call with an optional argument. + * arg_sig == NULL or "" -> no body + * arg_sig == "s" -> arg_string used + * arg_sig == "u" -> arg_u32 used + */ +static int send_method_call_with_arg(int fd, + const char *path, + const char *interface, + const char *member, + const char *arg_sig, + const char *arg_string, + uint32_t arg_u32); + +static int send_method_call(int fd, + const char *path, + const char *interface, + const char *member) +{ + return send_method_call_with_arg(fd, path, interface, member, + NULL, NULL, 0); +} + +static int send_method_call_with_arg(int fd, + const char *path, + const char *interface, + const char *member, + const char *arg_sig, + const char *arg_string, + uint32_t arg_u32) +{ + uint8_t hdr[2048]; + uint8_t body[1024]; + struct buf b = { .p = hdr, .cap = sizeof(hdr) }; + struct buf bb = { .p = body, .cap = sizeof(body) }; + size_t fields_start, fields_end, padded_end; + uint32_t body_len = 0; + + /* Build body first so its length and the signature are known + * before we write the header. */ + if (arg_sig && *arg_sig) { + if (strcmp(arg_sig, "s") == 0) { + b_put_string(&bb, arg_string ? arg_string : ""); + } else if (strcmp(arg_sig, "u") == 0) { + b_put_u32(&bb, arg_u32); + } else { + return -1; + } + if (bb.err) return -1; + body_len = (uint32_t)bb.off; + } + + /* Fixed header */ + memset(hdr, 0, 16); + hdr[0] = 'l'; + hdr[1] = 1; /* METHOD_CALL */ + hdr[2] = 0; /* flags */ + hdr[3] = 1; /* protocol */ + hdr[4] = (uint8_t)( body_len & 0xff); + hdr[5] = (uint8_t)((body_len >> 8) & 0xff); + hdr[6] = (uint8_t)((body_len >> 16) & 0xff); + hdr[7] = (uint8_t)((body_len >> 24) & 0xff); + hdr[8] = 1; /* serial */ + b.off = 16; + fields_start = b.off; + + /* PATH */ + b_reserve(&b, 8, 0); + b_put_byte(&b, 1); + b_put_signature(&b, "o"); + b_put_string(&b, path); + + if (interface) { + b_reserve(&b, 8, 0); + b_put_byte(&b, 2); + b_put_signature(&b, "s"); + b_put_string(&b, interface); + } + + b_reserve(&b, 8, 0); + b_put_byte(&b, 3); + b_put_signature(&b, "s"); + b_put_string(&b, member); + + if (arg_sig && *arg_sig) { + b_reserve(&b, 8, 0); + b_put_byte(&b, 8); + b_put_signature(&b, "g"); + /* SIGNATURE wire form: 1-byte len, bytes, nul */ + b_put_byte(&b, (uint8_t)strlen(arg_sig)); + if (b.off + strlen(arg_sig) + 1 > b.cap) return -1; + memcpy(b.p + b.off, arg_sig, strlen(arg_sig)); + b.off += strlen(arg_sig); + b.p[b.off++] = 0; + } + + fields_end = b.off; + { + uint32_t flen = (uint32_t)(fields_end - fields_start); + hdr[12] = (uint8_t)( flen & 0xff); + hdr[13] = (uint8_t)((flen >> 8) & 0xff); + hdr[14] = (uint8_t)((flen >> 16) & 0xff); + hdr[15] = (uint8_t)((flen >> 24) & 0xff); + } + + padded_end = ALIGN_UP(fields_end, 8); + while (b.off < padded_end) hdr[b.off++] = 0; + + if (b.err) return -1; + + if (write_all(fd, hdr, b.off) < 0) return -1; + if (body_len > 0 && write_all(fd, body, body_len) < 0) return -1; + return 0; +} + +/* Read one D-Bus message header + body into msg/body buffers. + * Returns 0 on success. Caller-supplied buffers must be large + * enough; we set them generously. */ +struct reply { + uint8_t type; + uint32_t serial; + uint32_t body_len; + char signature[64]; + char error_name[128]; + char interface[128]; + char member[128]; + uint8_t body[8192]; +}; + +/* Read one D-Bus message into *r. + * timeout_ms == 0 -> block forever waiting for the header byte + * timeout_ms > 0 -> wait that long for the header to start; once + * bytes arrive, the remainder of the frame is + * read without a timeout (it's "in flight"). + * Returns 0 on success, -1 on EOF / parse error / timeout. */ +static int read_reply(int fd, struct reply *r, int timeout_ms) +{ + uint8_t hdr_fixed[16]; + uint8_t hdr_fields[2048]; + uint32_t fields_len; + size_t body_off; + size_t pos; + size_t off = 0; + + memset(r, 0, sizeof(*r)); + + if (timeout_ms > 0) { + int n = read_with_timeout(fd, hdr_fixed, 1, timeout_ms); + if (n <= 0) return -1; + off = 1; + } + if (off < 16 && read_full(fd, hdr_fixed + off, 16 - off) < 0) + return -1; + + if (hdr_fixed[0] != 'l') return -1; + r->type = hdr_fixed[1]; + r->body_len = (uint32_t)hdr_fixed[4] + | ((uint32_t)hdr_fixed[5] << 8) + | ((uint32_t)hdr_fixed[6] << 16) + | ((uint32_t)hdr_fixed[7] << 24); + r->serial = (uint32_t)hdr_fixed[8] + | ((uint32_t)hdr_fixed[9] << 8) + | ((uint32_t)hdr_fixed[10] << 16) + | ((uint32_t)hdr_fixed[11] << 24); + fields_len = (uint32_t)hdr_fixed[12] + | ((uint32_t)hdr_fixed[13] << 8) + | ((uint32_t)hdr_fixed[14] << 16) + | ((uint32_t)hdr_fixed[15] << 24); + if (fields_len > sizeof(hdr_fields)) return -1; + if (read_full(fd, hdr_fields, fields_len) < 0) return -1; + + body_off = (size_t)ALIGN_UP(16 + fields_len, 8); + if (body_off > 16 + fields_len) { + uint8_t pad[8]; + if (read_full(fd, pad, body_off - 16 - fields_len) < 0) + return -1; + } + + pos = 0; + while (pos < fields_len) { + uint8_t code; + size_t vsig_len; + const char *vsig; + + pos = ALIGN_UP(pos, 8); + if (pos >= fields_len) break; + code = hdr_fields[pos++]; + vsig_len = hdr_fields[pos++]; + if (pos + vsig_len + 1 > fields_len) return -1; + vsig = (const char *)(hdr_fields + pos); + pos += vsig_len + 1; + + if (vsig[0] == 's' || vsig[0] == 'o') { + uint32_t slen; + char *dst = NULL; + size_t dst_sz = 0; + + pos = ALIGN_UP(pos, 4); + if (pos + 4 > fields_len) return -1; + slen = (uint32_t)hdr_fields[pos] + | ((uint32_t)hdr_fields[pos + 1] << 8) + | ((uint32_t)hdr_fields[pos + 2] << 16) + | ((uint32_t)hdr_fields[pos + 3] << 24); + pos += 4; + if (pos + slen + 1 > fields_len) return -1; + + switch (code) { + case 2: dst = r->interface; dst_sz = sizeof(r->interface); break; + case 3: dst = r->member; dst_sz = sizeof(r->member); break; + case 4: dst = r->error_name; dst_sz = sizeof(r->error_name); break; + default: break; + } + if (dst && slen < dst_sz) { + memcpy(dst, hdr_fields + pos, slen); + dst[slen] = '\0'; + } + pos += slen + 1; + } else if (vsig[0] == 'g') { + uint32_t slen = hdr_fields[pos++]; + if (pos + slen + 1 > fields_len) return -1; + if (code == 8 && slen < sizeof(r->signature)) { + memcpy(r->signature, hdr_fields + pos, slen); + r->signature[slen] = '\0'; + } + pos += slen + 1; + } else if (vsig[0] == 'u') { + pos = ALIGN_UP(pos, 4); + pos += 4; + } else { + return -1; + } + } + + if (r->body_len > sizeof(r->body)) return -1; + if (r->body_len > 0 && read_full(fd, r->body, r->body_len) < 0) + return -1; + return 0; +} + +/* Decode a body containing exactly one "s" or "o" -- the wire form + * is identical for both (u32 length + bytes + nul). */ +static int decode_string(struct reply *r, char *out, size_t outsz) +{ + uint32_t len; + + if (r->body_len < 5) + return -1; + if (strcmp(r->signature, "s") != 0 && strcmp(r->signature, "o") != 0) + return -1; + len = (uint32_t)r->body[0] + | ((uint32_t)r->body[1] << 8) + | ((uint32_t)r->body[2] << 16) + | ((uint32_t)r->body[3] << 24); + if (4 + len + 1 > r->body_len) return -1; + if (len + 1 > outsz) return -1; + memcpy(out, r->body + 4, len); + out[len] = '\0'; + return 0; +} + +/* Decode a body with signature "as", print one string per line. */ +static int decode_array_of_strings(struct reply *r) +{ + uint32_t array_len; + size_t pos; + + if (strcmp(r->signature, "as") != 0 || r->body_len < 4) + return -1; + array_len = (uint32_t)r->body[0] + | ((uint32_t)r->body[1] << 8) + | ((uint32_t)r->body[2] << 16) + | ((uint32_t)r->body[3] << 24); + pos = ALIGN_UP(4, 4); + if (pos + array_len > r->body_len) return -1; + + while (pos < 4 + array_len) { + uint32_t slen; + pos = ALIGN_UP(pos, 4); + if (pos + 4 > r->body_len) return -1; + slen = (uint32_t)r->body[pos] + | ((uint32_t)r->body[pos + 1] << 8) + | ((uint32_t)r->body[pos + 2] << 16) + | ((uint32_t)r->body[pos + 3] << 24); + pos += 4; + if (pos + slen + 1 > r->body_len) return -1; + printf("%.*s\n", (int)slen, r->body + pos); + pos += slen + 1; + } + return 0; +} + +/* ---------- modes ---------- */ + +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, "\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, line, (size_t)rc) < 0) { close(fd); return 2; } + if (read_line(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; +} + +static int do_call_arg(const char *path, const char *obj_path, + const char *iface, const char *method, + const char *arg_sig, const char *arg_string, + uint32_t arg_u32, struct reply *r) +{ + int fd = connect_and_auth(path, getuid()); + + if (fd < 0) return 2; + if (send_method_call_with_arg(fd, obj_path, iface, method, + arg_sig, arg_string, arg_u32) < 0) { + fprintf(stderr, "send: %s\n", strerror(errno)); + close(fd); + return 2; + } + if (read_reply(fd, r, 0) < 0) { + fprintf(stderr, "read_reply\n"); + close(fd); + return 2; + } + close(fd); + if (r->type == 3) { + fprintf(stderr, "ERROR: %s\n", r->error_name); + return 1; + } + return 0; +} + +static int do_call(const char *path, const char *obj_path, + const char *iface, const char *method, + struct reply *r) +{ + return do_call_arg(path, obj_path, iface, method, NULL, NULL, 0, r); +} + +static int mode_hello(int argc, char *argv[]) +{ + struct reply r; + char name[256]; + int rc; + + if (argc != 3) return 2; + rc = do_call(argv[2], "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", &r); + if (rc != 0) return rc; + if (decode_string(&r, name, sizeof(name)) < 0) return 2; + printf("%s\n", name); + return 0; +} + +static int mode_introspect(int argc, char *argv[]) +{ + struct reply r; + char xml[8192]; + int rc; + + if (argc != 4) return 2; + rc = do_call(argv[2], argv[3], + "org.freedesktop.DBus.Introspectable", "Introspect", &r); + if (rc != 0) return rc; + if (decode_string(&r, xml, sizeof(xml)) < 0) return 2; + printf("%s\n", xml); + return 0; +} + +static int mode_liststrings(int argc, char *argv[]) +{ + struct reply r; + int rc; + + if (argc != 6) return 2; + rc = do_call(argv[2], argv[3], argv[4], argv[5], &r); + if (rc != 0) return rc; + if (decode_array_of_strings(&r) < 0) return 2; + return 0; +} + +/* call-s: method taking one string arg, void/error reply. + * call-void: method taking no args, void/error reply. */ +static int mode_call_s(int argc, char *argv[]) +{ + struct reply r; + int rc; + + if (argc != 7) return 2; + rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], + "s", argv[6], 0, &r); + if (rc == 0) + printf("OK\n"); + return rc; +} + +static int mode_call_void(int argc, char *argv[]) +{ + struct reply r; + int rc; + + if (argc != 6) return 2; + rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], + NULL, NULL, 0, &r); + if (rc == 0) + printf("OK\n"); + return rc; +} + +/* get-service + * + * Calls Manager1.GetService(identity) and prints the returned + * object path. Exit 0 on success, 1 on server error, 2 transport. */ +static int mode_get_service(int argc, char *argv[]) +{ + struct reply r; + char path[256]; + int rc; + + if (argc != 4) return 2; + rc = do_call_arg(argv[2], "/org/finit/manager", + "org.finit.Manager1", "GetService", + "s", argv[3], 0, &r); + if (rc != 0) return rc; + /* decode_string accepts both "s" and "o" — wire form is + * identical; no need to pre-check the signature here. */ + if (decode_string(&r, path, sizeof(path)) < 0) + return 2; + printf("%s\n", path); + return 0; +} + +/* Drop effective uid to argv[2], parsed as decimal. Returns 0 on + * success, 2 (the program's "transport error" code) on failure. */ +static int drop_uid_from_arg(const char *uid_arg, const char *progname) +{ + uid_t drop_to; + 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; + } + drop_to = (uid_t)v; + + if (setuid(drop_to) < 0) { + perror("setuid"); + return 2; + } + return 0; +} + +/* monitor-signal + * + * Subscribes via org.freedesktop.DBus.AddMatch, then reads + * incoming messages until either a SIGNAL is received or the + * timeout elapses. On a signal: prints "SIGNAL " + * followed by any "s" args, one per line. Exit 0 on signal, 1 on + * timeout, 2 on transport error. */ +static int mode_monitor_signal(int argc, char *argv[]) +{ + int fd; + int timeout_ms; + struct reply r; + char *ep = NULL; + long v; + + 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; + + fd = connect_and_auth(argv[2], getuid()); + if (fd < 0) return 2; + + /* AddMatch on org.freedesktop.DBus */ + if (send_method_call_with_arg(fd, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", argv[3], 0) < 0) { + close(fd); return 2; + } + if (read_reply(fd, &r, 0) < 0) { close(fd); return 2; } + if (r.type == 3) { + fprintf(stderr, "AddMatch ERROR: %s\n", r.error_name); + close(fd); return 2; + } + + /* Now read messages until a signal or timeout. */ + for (;;) { + if (read_reply(fd, &r, timeout_ms) < 0) { + close(fd); + return 1; /* timeout / transport */ + } + if (r.type != 4) /* not a SIGNAL */ + continue; + printf("SIGNAL %s %s\n", r.interface, r.member); + /* Decode body as a sequence of strings; print one per line. */ + { + size_t pos = 0; + while (pos + 4 <= r.body_len) { + uint32_t slen; + pos = ALIGN_UP(pos, 4); + if (pos + 4 > r.body_len) break; + slen = (uint32_t)r.body[pos] + | ((uint32_t)r.body[pos + 1] << 8) + | ((uint32_t)r.body[pos + 2] << 16) + | ((uint32_t)r.body[pos + 3] << 24); + pos += 4; + if (pos + slen + 1 > r.body_len) break; + printf("%.*s\n", (int)slen, r.body + pos); + pos += slen + 1; + } + } + close(fd); + return 0; + } +} + +/* call-s-as-uid + * + * Drops effective uid to (must work inside the test + * namespace where additional uids are mapped) before connecting, + * so AUTH EXTERNAL captures as the peer's real identity. + * Used to verify per-method authorization gating. */ +static int mode_call_s_as_uid(int argc, char *argv[]) +{ + struct reply r; + int rc; + + if (argc != 8) return 2; + if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) + return rc; + rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], + "s", argv[7], 0, &r); + if (rc == 0) + printf("OK\n"); + return rc; +} + +/* call-void-as-uid */ +static int mode_call_void_as_uid(int argc, char *argv[]) +{ + struct reply r; + int rc; + + if (argc != 7) return 2; + if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) + return rc; + rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], + NULL, NULL, 0, &r); + if (rc == 0) + printf("OK\n"); + return rc; +} + +static int mode_unknown(int argc, char *argv[]) +{ + struct reply r; + int rc; + + if (argc != 3) return 2; + rc = do_call(argv[2], "/org/finit/manager", + "org.finit.Manager1", "NotARealMethod", &r); + if (rc == 1 && strstr(r.error_name, "org.freedesktop.DBus.Error.") == r.error_name) + return 0; + if (rc == 1) + return 1; + return 2; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) return 2; + if (strcmp(argv[1], "auth") == 0) return mode_auth(argc, argv); + if (strcmp(argv[1], "hello") == 0) return mode_hello(argc, argv); + if (strcmp(argv[1], "introspect") == 0) return mode_introspect(argc, argv); + if (strcmp(argv[1], "liststrings") == 0) return mode_liststrings(argc, argv); + if (strcmp(argv[1], "call-s") == 0) return mode_call_s(argc, argv); + if (strcmp(argv[1], "call-void") == 0) return mode_call_void(argc, argv); + if (strcmp(argv[1], "monitor-signal") == 0) return mode_monitor_signal(argc, argv); + if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); + if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv); + if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv); + if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv); + fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); + return 2; +} From dd1390a6c29fe59732ead4e54a2db9e7a95e9fb9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:14 +0200 Subject: [PATCH 02/22] initctl: monitor and condition control over the bus The bus can already answer questions and change services, so give initctl the two things it still did another way: watching signals as they happen, and getting or setting user conditions. The dbus tests move with it, split by area rather than one file that grew every time the library did. Signed-off-by: Joachim Wiberg --- src/initctl.c | 167 ++++++- test/Makefile.am | 10 + test/dbus-auth.sh | 286 +---------- test/dbus-bus.sh | 62 +++ test/dbus-cond.sh | 69 +++ test/dbus-initctl.sh | 68 +++ test/dbus-manager.sh | 69 +++ test/dbus-service.sh | 71 +++ test/lib/Makefile.am | 2 +- test/lib/dbus-setup.sh | 20 + test/setup-sysroot.sh | 8 +- test/src/Makefile.am | 6 +- test/src/dbus-auth-client.c | 928 +++++++++++------------------------- 13 files changed, 820 insertions(+), 946 deletions(-) create mode 100755 test/dbus-bus.sh create mode 100755 test/dbus-cond.sh create mode 100755 test/dbus-initctl.sh create mode 100755 test/dbus-manager.sh create mode 100755 test/dbus-service.sh create mode 100644 test/lib/dbus-setup.sh diff --git a/src/initctl.c b/src/initctl.c index 0fcb408f..71f14263 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -42,6 +42,9 @@ #include "cgutil.h" #include "utmp-api.h" +/* 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; struct cmd *ctx; @@ -327,6 +330,139 @@ static int try_dbus_manager(const char *method, const char *arg_sig, return 0; return -1; /* LINK_CALL_FAIL or anything else: fall back */ } + +/* 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 link_reply_t *r = link_client_reply(*bus); + link_reader_t reader; + const char *state = NULL; + + if (r && r->body) { + link_reader_init(&reader, r->body, r->body_len); + link_r_string(&reader, &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) @@ -496,8 +632,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; @@ -527,6 +661,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)"); @@ -546,6 +683,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 @@ -582,6 +735,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; } @@ -1696,6 +1853,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"); @@ -1880,6 +2040,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/test/Makefile.am b/test/Makefile.am index f4b04ee5..e5a3d5ac 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -72,6 +72,11 @@ 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; @@ -129,6 +134,11 @@ 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/dbus-auth.sh b/test/dbus-auth.sh index 27bf6bea..1e9535d3 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -1,10 +1,9 @@ #!/bin/sh -# End-to-end smoke test for libink: -# - AUTH EXTERNAL handshake (happy and wrong-uid paths) -# - org.freedesktop.DBus.Hello -# - org.freedesktop.DBus.Introspectable.Introspect (root, manager) -# - org.finit.Manager1.ListServices -# - Error reply for an unknown method. +# 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 @@ -12,23 +11,13 @@ TEST_DIR=$(dirname "$0") # shellcheck source=/dev/null . "$TEST_DIR/lib/setup.sh" - -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" +# 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" -# ---------- AUTH ---------- - say "AUTH EXTERNAL: claim correct UID (root = 0)" reply=$(texec "$CLIENT" auth "$BUS" 0) assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK" @@ -52,264 +41,3 @@ r2=$(texec "$CLIENT" auth "$BUS" 0) g1=${r1#OK } g2=${r2#OK } assert "Per-connection GUIDs differ ($g1 vs $g2)" "$g1" != "$g2" - -# ---------- Built-in interfaces ---------- - -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 referencing /manager" -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 - -# ---------- Real method call ---------- - -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" - -# ---------- Method with arguments ---------- - -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 - -# ---------- Authorization ---------- - -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 - -say "Manager1.ListServices is reachable as non-root (not blocked by authz)" -# call-s-as-uid sends an "s" body; ListServices expects "", so 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 (e.g. a transport error would print -# neither AccessDenied nor InvalidArgs). -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 - -# ---------- Per-service objects (Service1) ---------- - -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 - -# ---------- Signals ---------- - -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 - -# ---------- Cond1 ---------- - -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 - -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 - -# ---------- initctl port ---------- - -# initctl now talks to /run/finit/bus when available. Verify by -# subscribing to ServiceStateChanged on a background monitor and -# then running initctl restart -- if D-Bus is in use, the signal -# fires. If the legacy socket were still in use, the dbus subscriber -# would see nothing. - -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 - -# ---------- Error reply ---------- - -say "Unknown method 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-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..c51c1a3e --- /dev/null +++ b/test/dbus-initctl.sh @@ -0,0 +1,68 @@ +#!/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 diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh new file mode 100755 index 00000000..e26b4c11 --- /dev/null +++ b/test/dbus-manager.sh @@ -0,0 +1,69 @@ +#!/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 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 102cbad2..f986125e 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -15,7 +15,13 @@ make -C "$top_builddir" DESTDIR="$SYSROOT" install mkdir -p "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" -if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then +# 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 diff --git a/test/src/Makefile.am b/test/src/Makefile.am index b8d607d1..87d6e362 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -9,6 +9,8 @@ serv_LDADD = $(lite_LIBS) endif if DBUS -noinst_PROGRAMS += dbus-auth-client -dbus_auth_client_SOURCES = dbus-auth-client.c +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 index 00e275a4..53f85223 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -4,32 +4,44 @@ * 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 - * Auth as own uid; call org.freedesktop.DBus.Hello on - * /org/freedesktop/DBus. Print the assigned unique name. + * Call org.freedesktop.DBus.Hello, print the assigned unique name. * * dbus-auth-client introspect - * Auth + org.freedesktop.DBus.Introspectable.Introspect. - * Print the XML reply. + * Call org.freedesktop.DBus.Introspectable.Introspect, print XML. * * dbus-auth-client liststrings - * Auth + method call expecting reply signature "as"; print one - * string per line. + * 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 - * Auth + call a bogus method; exits 0 only if the server replies - * with an "org.freedesktop.DBus.Error.*" error. + * Call a bogus method, exit 0 iff the server replies with an + * org.freedesktop.DBus.Error.* error. * - * In every non-auth mode the program exits 0 on a successful method - * reply, 1 on a server-side error reply, 2 on transport / parse 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 @@ -37,76 +49,35 @@ #include #include #include -#include -#include -static const char hex[] = "0123456789abcdef"; +#include "link.h" -#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) +/* ---------- manual SASL: only mode_auth uses this ---------- */ -/* ---------- low level I/O ---------- */ +static const char hex[] = "0123456789abcdef"; -static int write_all(int fd, const void *buf, size_t len) +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 int 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; + p += n; len -= (size_t)n; } return 0; } -static int read_with_timeout(int fd, void *buf, size_t len, int timeout_ms) -{ - struct pollfd pfd = { .fd = fd, .events = POLLIN }; - int rc; - - for (;;) { - rc = poll(&pfd, 1, timeout_ms); - if (rc < 0) { - if (errno == EINTR) - continue; - return -1; - } - if (rc == 0) - return 0; /* timed out */ - break; - } - return (int)read(fd, buf, len); -} - -static ssize_t read_line(int fd, char *buf, size_t bufsz) +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; @@ -123,410 +94,6 @@ static ssize_t read_line(int fd, char *buf, size_t bufsz) return -1; } -/* ---------- connect + AUTH ---------- */ - -static int connect_and_auth(const char *path, uid_t claimed_uid) -{ - struct sockaddr_un sun = { .sun_family = AF_UNIX }; - char uidstr[16]; - char hexuid[32]; - char line[64]; - char reply[256]; - size_t i, n; - int fd, rc; - - if (strlen(path) >= sizeof(sun.sun_path)) - return -1; - memcpy(sun.sun_path, path, strlen(path) + 1); - - fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) return -1; - if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { - close(fd); - return -1; - } - - n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)claimed_uid); - for (i = 0; i < n; i++) { - unsigned c = (unsigned char)uidstr[i]; - - hexuid[i * 2] = hex[c >> 4]; - hexuid[i * 2 + 1] = hex[c & 0xf]; - } - hexuid[n * 2] = '\0'; - - if (write_all(fd, "\0", 1) < 0) goto io; - - rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); - if (rc < 0 || (size_t)rc >= sizeof(line)) goto io; - if (write_all(fd, line, (size_t)rc) < 0) goto io; - - if (read_line(fd, reply, sizeof(reply)) < 0) goto io; - if (strncmp(reply, "OK ", 3) != 0) { - fprintf(stderr, "auth failed: %s\n", reply); - close(fd); - return -1; - } - - if (write_all(fd, "BEGIN\r\n", 7) < 0) goto io; - return fd; - -io: - perror("auth handshake"); - close(fd); - return -1; -} - -/* ---------- D-Bus message build / parse ---------- */ - -struct buf { - uint8_t *p; - size_t cap; - size_t off; - int err; -}; - -static int b_reserve(struct buf *b, size_t align, size_t bytes) -{ - size_t pad = ALIGN_UP(b->off, align) - b->off; - - if (b->err || b->off + pad + bytes > b->cap) { - b->err = 1; - return -1; - } - while (pad--) b->p[b->off++] = 0; - return 0; -} - -static void b_put_u32(struct buf *b, uint32_t v) -{ - if (b_reserve(b, 4, 4) < 0) return; - b->p[b->off++] = (uint8_t)(v & 0xff); - b->p[b->off++] = (uint8_t)((v >> 8) & 0xff); - b->p[b->off++] = (uint8_t)((v >> 16) & 0xff); - b->p[b->off++] = (uint8_t)((v >> 24) & 0xff); -} - -static void b_put_byte(struct buf *b, uint8_t v) -{ - if (b_reserve(b, 1, 1) < 0) return; - b->p[b->off++] = v; -} - -static void b_put_string(struct buf *b, const char *s) -{ - size_t len = strlen(s); - - if (b_reserve(b, 4, 4 + len + 1) < 0) return; - b_put_u32(b, (uint32_t)len); - memcpy(b->p + b->off, s, len); - b->off += len; - b->p[b->off++] = 0; -} - -static void b_put_signature(struct buf *b, const char *s) -{ - size_t len = strlen(s); - - if (b_reserve(b, 1, 1 + len + 1) < 0) return; - b->p[b->off++] = (uint8_t)len; - memcpy(b->p + b->off, s, len); - b->off += len; - b->p[b->off++] = 0; -} - -/* Send a method call with an optional argument. - * arg_sig == NULL or "" -> no body - * arg_sig == "s" -> arg_string used - * arg_sig == "u" -> arg_u32 used - */ -static int send_method_call_with_arg(int fd, - const char *path, - const char *interface, - const char *member, - const char *arg_sig, - const char *arg_string, - uint32_t arg_u32); - -static int send_method_call(int fd, - const char *path, - const char *interface, - const char *member) -{ - return send_method_call_with_arg(fd, path, interface, member, - NULL, NULL, 0); -} - -static int send_method_call_with_arg(int fd, - const char *path, - const char *interface, - const char *member, - const char *arg_sig, - const char *arg_string, - uint32_t arg_u32) -{ - uint8_t hdr[2048]; - uint8_t body[1024]; - struct buf b = { .p = hdr, .cap = sizeof(hdr) }; - struct buf bb = { .p = body, .cap = sizeof(body) }; - size_t fields_start, fields_end, padded_end; - uint32_t body_len = 0; - - /* Build body first so its length and the signature are known - * before we write the header. */ - if (arg_sig && *arg_sig) { - if (strcmp(arg_sig, "s") == 0) { - b_put_string(&bb, arg_string ? arg_string : ""); - } else if (strcmp(arg_sig, "u") == 0) { - b_put_u32(&bb, arg_u32); - } else { - return -1; - } - if (bb.err) return -1; - body_len = (uint32_t)bb.off; - } - - /* Fixed header */ - memset(hdr, 0, 16); - hdr[0] = 'l'; - hdr[1] = 1; /* METHOD_CALL */ - hdr[2] = 0; /* flags */ - hdr[3] = 1; /* protocol */ - hdr[4] = (uint8_t)( body_len & 0xff); - hdr[5] = (uint8_t)((body_len >> 8) & 0xff); - hdr[6] = (uint8_t)((body_len >> 16) & 0xff); - hdr[7] = (uint8_t)((body_len >> 24) & 0xff); - hdr[8] = 1; /* serial */ - b.off = 16; - fields_start = b.off; - - /* PATH */ - b_reserve(&b, 8, 0); - b_put_byte(&b, 1); - b_put_signature(&b, "o"); - b_put_string(&b, path); - - if (interface) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 2); - b_put_signature(&b, "s"); - b_put_string(&b, interface); - } - - b_reserve(&b, 8, 0); - b_put_byte(&b, 3); - b_put_signature(&b, "s"); - b_put_string(&b, member); - - if (arg_sig && *arg_sig) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 8); - b_put_signature(&b, "g"); - /* SIGNATURE wire form: 1-byte len, bytes, nul */ - b_put_byte(&b, (uint8_t)strlen(arg_sig)); - if (b.off + strlen(arg_sig) + 1 > b.cap) return -1; - memcpy(b.p + b.off, arg_sig, strlen(arg_sig)); - b.off += strlen(arg_sig); - b.p[b.off++] = 0; - } - - fields_end = b.off; - { - uint32_t flen = (uint32_t)(fields_end - fields_start); - hdr[12] = (uint8_t)( flen & 0xff); - hdr[13] = (uint8_t)((flen >> 8) & 0xff); - hdr[14] = (uint8_t)((flen >> 16) & 0xff); - hdr[15] = (uint8_t)((flen >> 24) & 0xff); - } - - padded_end = ALIGN_UP(fields_end, 8); - while (b.off < padded_end) hdr[b.off++] = 0; - - if (b.err) return -1; - - if (write_all(fd, hdr, b.off) < 0) return -1; - if (body_len > 0 && write_all(fd, body, body_len) < 0) return -1; - return 0; -} - -/* Read one D-Bus message header + body into msg/body buffers. - * Returns 0 on success. Caller-supplied buffers must be large - * enough; we set them generously. */ -struct reply { - uint8_t type; - uint32_t serial; - uint32_t body_len; - char signature[64]; - char error_name[128]; - char interface[128]; - char member[128]; - uint8_t body[8192]; -}; - -/* Read one D-Bus message into *r. - * timeout_ms == 0 -> block forever waiting for the header byte - * timeout_ms > 0 -> wait that long for the header to start; once - * bytes arrive, the remainder of the frame is - * read without a timeout (it's "in flight"). - * Returns 0 on success, -1 on EOF / parse error / timeout. */ -static int read_reply(int fd, struct reply *r, int timeout_ms) -{ - uint8_t hdr_fixed[16]; - uint8_t hdr_fields[2048]; - uint32_t fields_len; - size_t body_off; - size_t pos; - size_t off = 0; - - memset(r, 0, sizeof(*r)); - - if (timeout_ms > 0) { - int n = read_with_timeout(fd, hdr_fixed, 1, timeout_ms); - if (n <= 0) return -1; - off = 1; - } - if (off < 16 && read_full(fd, hdr_fixed + off, 16 - off) < 0) - return -1; - - if (hdr_fixed[0] != 'l') return -1; - r->type = hdr_fixed[1]; - r->body_len = (uint32_t)hdr_fixed[4] - | ((uint32_t)hdr_fixed[5] << 8) - | ((uint32_t)hdr_fixed[6] << 16) - | ((uint32_t)hdr_fixed[7] << 24); - r->serial = (uint32_t)hdr_fixed[8] - | ((uint32_t)hdr_fixed[9] << 8) - | ((uint32_t)hdr_fixed[10] << 16) - | ((uint32_t)hdr_fixed[11] << 24); - fields_len = (uint32_t)hdr_fixed[12] - | ((uint32_t)hdr_fixed[13] << 8) - | ((uint32_t)hdr_fixed[14] << 16) - | ((uint32_t)hdr_fixed[15] << 24); - if (fields_len > sizeof(hdr_fields)) return -1; - if (read_full(fd, hdr_fields, fields_len) < 0) return -1; - - body_off = (size_t)ALIGN_UP(16 + fields_len, 8); - if (body_off > 16 + fields_len) { - uint8_t pad[8]; - if (read_full(fd, pad, body_off - 16 - fields_len) < 0) - return -1; - } - - pos = 0; - while (pos < fields_len) { - uint8_t code; - size_t vsig_len; - const char *vsig; - - pos = ALIGN_UP(pos, 8); - if (pos >= fields_len) break; - code = hdr_fields[pos++]; - vsig_len = hdr_fields[pos++]; - if (pos + vsig_len + 1 > fields_len) return -1; - vsig = (const char *)(hdr_fields + pos); - pos += vsig_len + 1; - - if (vsig[0] == 's' || vsig[0] == 'o') { - uint32_t slen; - char *dst = NULL; - size_t dst_sz = 0; - - pos = ALIGN_UP(pos, 4); - if (pos + 4 > fields_len) return -1; - slen = (uint32_t)hdr_fields[pos] - | ((uint32_t)hdr_fields[pos + 1] << 8) - | ((uint32_t)hdr_fields[pos + 2] << 16) - | ((uint32_t)hdr_fields[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > fields_len) return -1; - - switch (code) { - case 2: dst = r->interface; dst_sz = sizeof(r->interface); break; - case 3: dst = r->member; dst_sz = sizeof(r->member); break; - case 4: dst = r->error_name; dst_sz = sizeof(r->error_name); break; - default: break; - } - if (dst && slen < dst_sz) { - memcpy(dst, hdr_fields + pos, slen); - dst[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'g') { - uint32_t slen = hdr_fields[pos++]; - if (pos + slen + 1 > fields_len) return -1; - if (code == 8 && slen < sizeof(r->signature)) { - memcpy(r->signature, hdr_fields + pos, slen); - r->signature[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'u') { - pos = ALIGN_UP(pos, 4); - pos += 4; - } else { - return -1; - } - } - - if (r->body_len > sizeof(r->body)) return -1; - if (r->body_len > 0 && read_full(fd, r->body, r->body_len) < 0) - return -1; - return 0; -} - -/* Decode a body containing exactly one "s" or "o" -- the wire form - * is identical for both (u32 length + bytes + nul). */ -static int decode_string(struct reply *r, char *out, size_t outsz) -{ - uint32_t len; - - if (r->body_len < 5) - return -1; - if (strcmp(r->signature, "s") != 0 && strcmp(r->signature, "o") != 0) - return -1; - len = (uint32_t)r->body[0] - | ((uint32_t)r->body[1] << 8) - | ((uint32_t)r->body[2] << 16) - | ((uint32_t)r->body[3] << 24); - if (4 + len + 1 > r->body_len) return -1; - if (len + 1 > outsz) return -1; - memcpy(out, r->body + 4, len); - out[len] = '\0'; - return 0; -} - -/* Decode a body with signature "as", print one string per line. */ -static int decode_array_of_strings(struct reply *r) -{ - uint32_t array_len; - size_t pos; - - if (strcmp(r->signature, "as") != 0 || r->body_len < 4) - return -1; - array_len = (uint32_t)r->body[0] - | ((uint32_t)r->body[1] << 8) - | ((uint32_t)r->body[2] << 16) - | ((uint32_t)r->body[3] << 24); - pos = ALIGN_UP(4, 4); - if (pos + array_len > r->body_len) return -1; - - while (pos < 4 + array_len) { - uint32_t slen; - pos = ALIGN_UP(pos, 4); - if (pos + 4 > r->body_len) return -1; - slen = (uint32_t)r->body[pos] - | ((uint32_t)r->body[pos + 1] << 8) - | ((uint32_t)r->body[pos + 2] << 16) - | ((uint32_t)r->body[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > r->body_len) return -1; - printf("%.*s\n", (int)slen, r->body + pos); - pos += slen + 1; - } - return 0; -} - -/* ---------- modes ---------- */ - static int mode_auth(int argc, char *argv[]) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; @@ -557,11 +124,11 @@ static int mode_auth(int argc, char *argv[]) if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { perror("connect"); close(fd); return 2; } - if (write_all(fd, "\0", 1) < 0) { 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, line, (size_t)rc) < 0) { close(fd); return 2; } - if (read_line(fd, reply, sizeof(reply)) < 0) { 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; @@ -569,170 +136,236 @@ static int mode_auth(int argc, char *argv[]) return 2; } -static int do_call_arg(const char *path, const char *obj_path, - const char *iface, const char *method, - const char *arg_sig, const char *arg_string, - uint32_t arg_u32, struct reply *r) +/* ---------- 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) { - int fd = connect_and_auth(path, getuid()); + char *ep = NULL; + long v; - if (fd < 0) return 2; - if (send_method_call_with_arg(fd, obj_path, iface, method, - arg_sig, arg_string, arg_u32) < 0) { - fprintf(stderr, "send: %s\n", strerror(errno)); - close(fd); + 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 (read_reply(fd, r, 0) < 0) { - fprintf(stderr, "read_reply\n"); - close(fd); + if (setuid((uid_t)v) < 0) { + perror("setuid"); return 2; } - close(fd); - if (r->type == 3) { - fprintf(stderr, "ERROR: %s\n", r->error_name); - return 1; - } return 0; } -static int do_call(const char *path, const char *obj_path, - const char *iface, const char *method, - struct reply *r) -{ - return do_call_arg(path, obj_path, iface, method, NULL, NULL, 0, r); -} - static int mode_hello(int argc, char *argv[]) { - struct reply r; - char name[256]; + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *name; int rc; if (argc != 3) return 2; - rc = do_call(argv[2], "/org/freedesktop/DBus", - "org.freedesktop.DBus", "Hello", &r); - if (rc != 0) return rc; - if (decode_string(&r, name, sizeof(name)) < 0) return 2; - printf("%s\n", name); - return 0; + 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) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_string(&reader, &name) == 0) + printf("%s\n", name); + else + rc = 2; + } + link_client_close(c); + return rc; } static int mode_introspect(int argc, char *argv[]) { - struct reply r; - char xml[8192]; + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *xml; int rc; if (argc != 4) return 2; - rc = do_call(argv[2], argv[3], - "org.freedesktop.DBus.Introspectable", "Introspect", &r); - if (rc != 0) return rc; - if (decode_string(&r, xml, sizeof(xml)) < 0) return 2; - printf("%s\n", xml); + 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) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_string(&reader, &xml) == 0) + printf("%s\n", xml); + else + rc = 2; + } + link_client_close(c); + 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. */ +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) + 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[]) { - struct reply r; + link_client_t *c; int rc; if (argc != 6) return 2; - rc = do_call(argv[2], argv[3], argv[4], argv[5], &r); - if (rc != 0) return rc; - if (decode_array_of_strings(&r) < 0) return 2; - return 0; + 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; } -/* call-s: method taking one string arg, void/error reply. - * call-void: method taking no args, void/error reply. */ -static int mode_call_s(int argc, char *argv[]) +/* 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) { - struct reply r; + link_client_t *c; int rc; - if (argc != 7) return 2; - rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], - "s", argv[6], 0, &r); + 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_void(int argc, char *argv[]) +static int mode_call_s(int argc, char *argv[]) { - struct reply r; - int rc; + 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; - rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], - NULL, NULL, 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; + return do_call(argv[2], argv[3], argv[4], argv[5], NULL); } -/* get-service - * - * Calls Manager1.GetService(identity) and prints the returned - * object path. Exit 0 on success, 1 on server error, 2 transport. */ -static int mode_get_service(int argc, char *argv[]) +static int mode_call_s_as_uid(int argc, char *argv[]) { - struct reply r; - char path[256]; - int rc; + int rc; - if (argc != 4) return 2; - rc = do_call_arg(argv[2], "/org/finit/manager", - "org.finit.Manager1", "GetService", - "s", argv[3], 0, &r); - if (rc != 0) return rc; - /* decode_string accepts both "s" and "o" — wire form is - * identical; no need to pre-check the signature here. */ - if (decode_string(&r, path, sizeof(path)) < 0) - return 2; - printf("%s\n", path); - return 0; + 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]); } -/* Drop effective uid to argv[2], parsed as decimal. Returns 0 on - * success, 2 (the program's "transport error" code) on failure. */ -static int drop_uid_from_arg(const char *uid_arg, const char *progname) +static int mode_call_void_as_uid(int argc, char *argv[]) { - uid_t drop_to; - char *ep = NULL; - long v; + int rc; - 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; - } - drop_to = (uid_t)v; + 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); +} - if (setuid(drop_to) < 0) { - perror("setuid"); - return 2; +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; + + 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) { + 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) + printf("%s\n", path); + else + rc = 2; } - return 0; + link_client_close(c); + return rc; } -/* monitor-signal - * - * Subscribes via org.freedesktop.DBus.AddMatch, then reads - * incoming messages until either a SIGNAL is received or the - * timeout elapses. On a signal: prints "SIGNAL " - * followed by any "s" args, one per line. Exit 0 on signal, 1 on - * timeout, 2 on transport error. */ static int mode_monitor_signal(int argc, char *argv[]) { - int fd; - int timeout_ms; - struct reply r; + 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; @@ -744,118 +377,91 @@ static int mode_monitor_signal(int argc, char *argv[]) } timeout_ms = (int)v; - fd = connect_and_auth(argv[2], getuid()); - if (fd < 0) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; - /* AddMatch on org.freedesktop.DBus */ - if (send_method_call_with_arg(fd, "/org/freedesktop/DBus", - "org.freedesktop.DBus", "AddMatch", - "s", argv[3], 0) < 0) { - close(fd); return 2; - } - if (read_reply(fd, &r, 0) < 0) { close(fd); return 2; } - if (r.type == 3) { - fprintf(stderr, "AddMatch ERROR: %s\n", r.error_name); - close(fd); 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; } - /* Now read messages until a signal or timeout. */ for (;;) { - if (read_reply(fd, &r, timeout_ms) < 0) { - close(fd); - return 1; /* timeout / transport */ + rc = link_client_wait(c, timeout_ms); + if (rc != 0) { + link_client_close(c); + return 1; /* timeout or transport */ } - if (r.type != 4) /* not a SIGNAL */ + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) continue; - printf("SIGNAL %s %s\n", r.interface, r.member); - /* Decode body as a sequence of strings; print one per line. */ - { - size_t pos = 0; - while (pos + 4 <= r.body_len) { - uint32_t slen; - pos = ALIGN_UP(pos, 4); - if (pos + 4 > r.body_len) break; - slen = (uint32_t)r.body[pos] - | ((uint32_t)r.body[pos + 1] << 8) - | ((uint32_t)r.body[pos + 2] << 16) - | ((uint32_t)r.body[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > r.body_len) break; - printf("%.*s\n", (int)slen, r.body + pos); - pos += slen + 1; + + 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); } } - close(fd); + link_client_close(c); return 0; } } -/* call-s-as-uid - * - * Drops effective uid to (must work inside the test - * namespace where additional uids are mapped) before connecting, - * so AUTH EXTERNAL captures as the peer's real identity. - * Used to verify per-method authorization gating. */ -static int mode_call_s_as_uid(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 8) return 2; - if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) - return rc; - rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], - "s", argv[7], 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - -/* call-void-as-uid */ -static int mode_call_void_as_uid(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 7) return 2; - if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) - return rc; - rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], - NULL, NULL, 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - static int mode_unknown(int argc, char *argv[]) { - struct reply r; + link_client_t *c; + const link_reply_t *r; int rc; if (argc != 3) return 2; - rc = do_call(argv[2], "/org/finit/manager", - "org.finit.Manager1", "NotARealMethod", &r); - if (rc == 1 && strstr(r.error_name, "org.freedesktop.DBus.Error.") == r.error_name) - return 0; - if (rc == 1) - return 1; - 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") == 0) return mode_auth(argc, argv); - if (strcmp(argv[1], "hello") == 0) return mode_hello(argc, argv); - if (strcmp(argv[1], "introspect") == 0) return mode_introspect(argc, argv); - if (strcmp(argv[1], "liststrings") == 0) return mode_liststrings(argc, argv); - if (strcmp(argv[1], "call-s") == 0) return mode_call_s(argc, argv); - if (strcmp(argv[1], "call-void") == 0) return mode_call_void(argc, argv); - if (strcmp(argv[1], "monitor-signal") == 0) return mode_monitor_signal(argc, argv); - if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); - if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv); - if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv); - if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv); + 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; } From ebc0ef62e6d368da9db6b5751de708ace79dd14c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:14 +0200 Subject: [PATCH 03/22] 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 | 238 +++++++++++++++++---- 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, 1473 insertions(+), 153 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 *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, ...} */ + 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); + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, arg); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; +} - 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"); - } +/* 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); - if (rc == LINK_CALL_OK) - return 0; - return -1; /* LINK_CALL_FAIL or anything else: fall back */ + 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 @@ 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/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; From 6310d9e760495d51f9504058f31486c4dd719347 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:26 +0200 Subject: [PATCH 04/22] initctl: the status views over D-Bus The summary table, the per-service detail, JSON and the quiet and ident forms all read state Finit already publishes, so they read it from the bus like everything else rather than through a second path that has to be kept in step. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 10 +- doc/dbus.md | 35 ++ libink/builtin.c | 2 + libink/dispatch.c | 2 + libink/link.h | 10 +- libink/marshal.c | 85 ++- libink/marshal.h | 3 + src/dbus.c | 253 ++++++++- src/initctl.c | 1024 +++++++++++++++++++++++++---------- src/service.c | 2 +- src/service.h | 1 + test/Makefile.am | 2 + test/dbus-initctl.sh | 48 ++ test/dbus-introspect.sh | 31 ++ test/dbus-service.sh | 28 +- test/src/dbus-auth-client.c | 42 ++ 16 files changed, 1267 insertions(+), 311 deletions(-) create mode 100755 test/dbus-introspect.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec7aeab..84e7cae3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,16 +62,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/doc/dbus.md b/doc/dbus.md index 3b035909..60efff2d 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -127,6 +127,41 @@ look up the exact path rather than constructing it by hand. | `Restart` | — | — | yes | … | | `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | +### Properties + +All read-only; observable via `Properties.Get` and `Properties.GetAll`. + +| Property | Type | Returns | +| -------------- | ---- | ------- | +| `Identity` | `s` | Service identity, `name` or `name:id`. | +| `Name` | `s` | Program name (basename of the command). | +| `State` | `s` | Current status, same vocabulary as `initctl status` (richer than the coarse `ServiceStateChanged` strings). | +| `Pid` | `u` | Current PID, 0 when not running. | +| `RestartCount` | `u` | Restarts since the last stable run. | +| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S. | +| `Description` | `s` | The service's `description` string. | +| `Command` | `s` | Full command line, arguments included. | +| `Conditions` | `s` | Declared conditions, raw `.conf` form. | +| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free`. | +| `Origin` | `s` | Source `.conf` file, empty for built-ins. | +| `Environment` | `s` | The service's `env` setting, raw. | +| `PidFile` | `s` | Declared PID file, raw (`!` prefix included). | +| `User` | `s` | User the service runs as. | +| `Group` | `s` | Group the service runs as. | +| `Uptime` | `u` | Seconds since start, 0 when not running. | +| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit. | +| `RestartsTotal`| `u` | Restarts over the service's lifetime. | +| `RestartMax` | `u` | Restart limit before the service is blocked. | +| `Starts` | `u` | Times started, for `manual-start` units. | +| `ManualStart` | `b` | `manual-start` set in the `.conf`. | +| `Forking` | `b` | Daemon forks to background. | +| `Started` | `b` | Run/task completed successfully. | + +On every state transition the object also emits the standard +`org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State` +in the changed dictionary, `Pid` and `RestartCount` invalidated (call +`Get` for fresh values). + 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. diff --git a/libink/builtin.c b/libink/builtin.c index 0d7a677b..f1d5dd23 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -333,6 +333,7 @@ static int handle_properties_get(link_connection_t *conn, const struct link_msg if (!p->getter) break; __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_sig(&w, p->sig ? p->sig : "s"); if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0) return __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", @@ -380,6 +381,7 @@ static int handle_properties_get_all(link_connection_t *conn, const struct link_ continue; __w_struct_begin(&w); __w_string(&w, p->name); + __w_sig(&w, p->sig ? p->sig : "s"); if (p->getter(&w, e->userdata) != 0) return __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", diff --git a/libink/dispatch.c b/libink/dispatch.c index 9d3d322b..2899d8ff 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -312,6 +312,8 @@ 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_begin (link_reader_t *r, char *type) { return __r_variant_begin(r, type); } +int link_r_skip_basic (link_reader_t *r, char type) { return __r_skip_basic(r, type); } 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); } diff --git a/libink/link.h b/libink/link.h index 01a61c11..265905cb 100644 --- a/libink/link.h +++ b/libink/link.h @@ -129,9 +129,11 @@ typedef struct { } 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. */ + * is not yet implemented; only Get and GetAll are. The framework + * emits the variant signature from `sig`; the getter writes only the + * bare value into the provided writer (link_w_string for "s", + * link_w_u32 for "u", ...), so the declared type is the single + * source of truth. */ typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); typedef struct { @@ -329,6 +331,8 @@ 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_begin (link_reader_t *r, char *type); /* sig header, cursor at value */ +int link_r_skip_basic (link_reader_t *r, char type); /* skip one basic value */ 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); diff --git a/libink/marshal.c b/libink/marshal.c index ae215893..48f00363 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -110,6 +110,7 @@ void __w_variant_string(struct link_writer *w, const char *s) __w_string(w, s); } + static size_t element_align(char c) { switch (c) { @@ -262,32 +263,88 @@ 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); } +/* + * Parse a variant's signature header, i.e. "g" wire form: 1-byte + * length, bytes, NUL. Only single-character inner signatures are + * supported. On success the cursor sits at the value and the type + * code is returned in *type. + */ +int __r_variant_begin(struct link_reader *r, char *type) +{ + uint8_t sig_len; + + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) + goto fail; + sig_len = r->base[r->off++]; + if (sig_len != 1 || r->off + 2 > r->cap) + goto fail; + if (r->base[r->off + 1] != 0) + goto fail; + *type = (char)r->base[r->off]; + r->off += 2; + return 0; +fail: + r->err = 1; + return -1; +} + /* 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; + char type; *out = NULL; + if (__r_variant_begin(r, &type) < 0) + return -1; + if (type != 's') { + r->err = 1; + return -1; + } + return read_string_like(r, out); +} - /* 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; +/* + * Skip one basic value of the given type code, as returned by + * __r_variant_begin(). Lets a{sv} consumers tolerate value types + * they don't know. Returns -1 on non-basic types. + */ +int __r_skip_basic(struct link_reader *r, char type) +{ + const char *s; + uint32_t u; + uint8_t y; + + switch (type) { + case 's': + case 'o': + return read_string_like(r, &s); + case 'b': + case 'u': + case 'i': + return __r_u32(r, &u); + case 'y': + return __r_byte(r, &y); + default: + r->err = 1; + return -1; + } +} + +/* Read a variant "v" expected to contain a uint32. */ +int __r_variant_u32(struct link_reader *r, uint32_t *out) +{ + char type; - /* 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) { + if (__r_variant_begin(r, &type) < 0) + return -1; + if (type != 'u') { r->err = 1; return -1; } - *out = (const char *)(r->base + r->off); - r->off += (size_t)slen + 1; - return 0; + + return __r_u32(r, out); } int __r_done(const struct link_reader *r) diff --git a/libink/marshal.h b/libink/marshal.h index 313205b2..3246d3e7 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -44,7 +44,10 @@ 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_begin (struct link_reader *r, char *type); /* sig header, cursor at value */ +int __r_skip_basic (struct link_reader *r, char type); /* skip one basic value */ int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ +int __r_variant_u32 (struct link_reader *r, uint32_t *out); /* "v" containing "u" */ 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); diff --git a/src/dbus.c b/src/dbus.c index 109c8a5e..8f98b3d1 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -65,14 +65,23 @@ 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 struct peer *sysbus_peer; + +static void sysbus_probe(void); static void peer_drop(struct peer *p) { + int was_sysbus = p == sysbus_peer; + uev_io_stop(&p->watcher); link_connection_close(p->conn); TAILQ_REMOVE(&peers, p, link); peer_count--; free(p); + + /* broker gone; the notify paths probe for its return */ + if (was_sysbus) + sysbus_peer = NULL; } static void peer_cb(uev_t *w, void *arg, int events) @@ -456,7 +465,7 @@ static int prop_runlevel(link_writer_t *w, void *u) (void)u; snprintf(buf, sizeof(buf), "%d", runlevel); - link_w_variant_string(w, buf); + link_w_string(w, buf); return 0; } @@ -466,14 +475,14 @@ static int prop_prevrunlevel(link_writer_t *w, void *u) (void)u; snprintf(buf, sizeof(buf), "%d", prevlevel); - link_w_variant_string(w, buf); + link_w_string(w, buf); return 0; } static int prop_version(link_writer_t *w, void *u) { (void)u; - link_w_variant_string(w, PACKAGE_VERSION); + link_w_string(w, PACKAGE_VERSION); return 0; } @@ -576,9 +585,146 @@ static const link_method_t service_methods[] = { { NULL, NULL, NULL, 0, NULL } }; +/* + * Getters write the bare value; the framework emits the variant + * signature from the table below. `State` deliberately uses the + * initctl status vocabulary from svc_status(), not the coarser + * ServiceStateChanged strings -- a client that only tracks edges + * has the signal, a client that asks gets the full story. + */ +#define SVC_PROP_STR(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_string(w, ((svc_t *)arg)->field); \ + return 0; \ +} + +#define SVC_PROP_U32(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_u32(w, (uint32_t)((svc_t *)arg)->field); \ + return 0; \ +} + +#define SVC_PROP_BOOL(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_bool(w, ((svc_t *)arg)->field); \ + return 0; \ +} + +static int svc_prop_identity(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_ident(arg, NULL, 0)); + return 0; +} + +static int svc_prop_state(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_status(arg)); + return 0; +} + +static int svc_prop_type(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_typestr((svc_t *)arg)); + return 0; +} + +static int svc_prop_command(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + char buf[512]; + + compose_cmdline(svc, buf, sizeof(buf)); + if (svc_is_sysv(svc)) { + strlcat(buf, " ", sizeof(buf)); + strlcat(buf, svc->state == SVC_HALTED_STATE + ? "stop" : "start", sizeof(buf)); + } + + link_w_string(w, buf); + return 0; +} + +static int svc_prop_pid(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + + link_w_u32(w, svc->pid > 0 ? (uint32_t)svc->pid : 0); + return 0; +} + +static int svc_prop_restarts(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + + link_w_u32(w, svc->restart_cnt > 0 ? (uint32_t)svc->restart_cnt : 0); + return 0; +} + +static int svc_prop_uptime(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + long up = 0; + + if (svc->pid > 0) { + up = jiffies() - svc->start_time; + if (up < 0) + up = 0; + } + link_w_u32(w, (uint32_t)up); + return 0; +} + +SVC_PROP_STR (svc_prop_name, name) +SVC_PROP_STR (svc_prop_desc, desc) +SVC_PROP_STR (svc_prop_conditions, cond) +SVC_PROP_STR (svc_prop_origin, file) +SVC_PROP_STR (svc_prop_environ, env) +SVC_PROP_STR (svc_prop_pidfile, pidfile) +SVC_PROP_STR (svc_prop_user, username) +SVC_PROP_STR (svc_prop_group, group) +SVC_PROP_U32 (svc_prop_runlevels, runlevels) +SVC_PROP_U32 (svc_prop_exitstatus, status) +SVC_PROP_U32 (svc_prop_restarts_tot, restart_tot) +SVC_PROP_U32 (svc_prop_restart_max, restart_max) +SVC_PROP_U32 (svc_prop_starts, once) +SVC_PROP_BOOL(svc_prop_manual, manual) +SVC_PROP_BOOL(svc_prop_forking, forking) +SVC_PROP_BOOL(svc_prop_started, started) + +static const link_property_t service_properties[] = { + { .name = "Identity", .sig = "s", .getter = svc_prop_identity }, + { .name = "Name", .sig = "s", .getter = svc_prop_name }, + { .name = "State", .sig = "s", .getter = svc_prop_state }, + { .name = "Pid", .sig = "u", .getter = svc_prop_pid }, + { .name = "RestartCount", .sig = "u", .getter = svc_prop_restarts }, + { .name = "Runlevels", .sig = "u", .getter = svc_prop_runlevels }, + { .name = "Description", .sig = "s", .getter = svc_prop_desc }, + { .name = "Command", .sig = "s", .getter = svc_prop_command }, + { .name = "Conditions", .sig = "s", .getter = svc_prop_conditions }, + { .name = "Type", .sig = "s", .getter = svc_prop_type }, + { .name = "Origin", .sig = "s", .getter = svc_prop_origin }, + { .name = "Environment", .sig = "s", .getter = svc_prop_environ }, + { .name = "PidFile", .sig = "s", .getter = svc_prop_pidfile }, + { .name = "User", .sig = "s", .getter = svc_prop_user }, + { .name = "Group", .sig = "s", .getter = svc_prop_group }, + { .name = "Uptime", .sig = "u", .getter = svc_prop_uptime }, + { .name = "ExitStatus", .sig = "u", .getter = svc_prop_exitstatus }, + { .name = "RestartsTotal",.sig = "u", .getter = svc_prop_restarts_tot }, + { .name = "RestartMax", .sig = "u", .getter = svc_prop_restart_max }, + { .name = "Starts", .sig = "u", .getter = svc_prop_starts }, + { .name = "ManualStart", .sig = "b", .getter = svc_prop_manual }, + { .name = "Forking", .sig = "b", .getter = svc_prop_forking }, + { .name = "Started", .sig = "b", .getter = svc_prop_started }, + { NULL, NULL, NULL } +}; + static const link_vtable_t service_vtable = { - .interface = "org.finit.Service1", - .methods = service_methods, + .interface = "org.finit.Service1", + .methods = service_methods, + .properties = service_properties, }; /* Build the canonical object path for a service. Identity is @@ -693,10 +839,14 @@ 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]; + char path[FINIT_SVC_PATH_MAX]; ssize_t blen; if (!svc) return; + + sysbus_probe(); + svc_ident(svc, ident, sizeof(ident)); link_writer_init(&w, body, sizeof(body)); @@ -709,6 +859,35 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", "ServiceStateChanged", "sss", body, (size_t)blen); + + /* + * Dual emission: Properties-aware clients track one object via + * the standard PropertiesChanged instead of filtering the + * manager-wide signal. Volatile numerics are invalidated, not + * marshalled -- interested clients re-Get. + */ + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, "org.finit.Service1"); + link_w_array_begin(&w, '{'); + link_w_struct_begin(&w); + link_w_string(&w, "State"); + link_w_variant_string(&w, svc_status(svc)); + link_w_struct_end(&w); + link_w_array_end(&w); + link_w_array_begin(&w, 's'); + link_w_string(&w, "Pid"); + link_w_string(&w, "RestartCount"); + link_w_array_end(&w); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal(path, "org.freedesktop.DBus.Properties", + "PropertiesChanged", "sa{sv}as", + body, (size_t)blen); } /* ---------- signal emission: RunlevelChanged ---------- @@ -960,6 +1139,8 @@ void dbus_notify_condition_change(const char *name, const char *state) if (!name || !state) return; + sysbus_probe(); + link_writer_init(&w, body, sizeof(body)); link_w_string(&w, name); link_w_string(&w, state); @@ -1018,17 +1199,18 @@ static int sysbus_request_name(link_client_t *c) return (result == 1) ? 0 : -1; } -static void try_attach_system_bus(uev_ctx_t *ctx) +static int try_attach_system_bus(uev_ctx_t *ctx) { link_client_t *c; link_connection_t *conn; + struct peer *p; 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; + return -1; } rc = link_client_call_v(c, "/org/freedesktop/DBus", @@ -1036,13 +1218,13 @@ static void try_attach_system_bus(uev_ctx_t *ctx) if (rc != LINK_CALL_OK) { dbg("System-bus Hello failed (rc=%d); skipping", rc); link_client_close(c); - return; + return -1; } if (sysbus_request_name(c) < 0) { logit(LOG_WARNING, "Failed to claim %s on system bus", FINIT_BUS_NAME); link_client_close(c); - return; + return -1; } /* link_server_attach owns the fd from this point on whether it @@ -1050,14 +1232,55 @@ static void try_attach_system_bus(uev_ctx_t *ctx) * window. */ conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1); if (!conn) - return; + return -1; - if (!peer_register(ctx, conn)) { + p = peer_register(ctx, conn); + if (!p) { logit(LOG_WARNING, "Failed registering system-bus peer"); - return; + return -1; } + sysbus_peer = p; logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); + return 0; +} + +/* + * The broker is usually not up yet when dbus_init() runs -- it is + * typically a finit service itself -- and it may restart at any + * time. The service and condition notify paths call sysbus_probe() + * on every event: when org.finit is unclaimed and the broker's + * socket exists, one coalesced attach attempt is scheduled. This + * stays daemon-agnostic -- only the socket is probed, never a + * service name -- and the broker's own service transitions are what + * trigger it. + */ +static uev_t sysbus_tmr; +static int sysbus_tmr_up; + +static void sysbus_probe_cb(uev_t *w, void *arg, int events) +{ + (void)arg; + (void)events; + + if (!sysbus_peer) + (void)try_attach_system_bus(w->ctx); +} + +static void sysbus_probe(void) +{ + if (sysbus_peer || !server) + return; + if (access(SYSTEM_BUS_PATH, F_OK)) + return; + + /* coalesce bursts to a single probe */ + if (!sysbus_tmr_up) + sysbus_tmr_up = !uev_timer_init(ctx, &sysbus_tmr, + sysbus_probe_cb, NULL, + 200, 0); + else + uev_timer_set(&sysbus_tmr, 200, 0); } /* ---------- init / exit ---------- */ @@ -1107,7 +1330,7 @@ int dbus_init(uev_ctx_t *ctx) dbus_register_service(svc); } - try_attach_system_bus(ctx); + (void)try_attach_system_bus(ctx); return 0; } @@ -1116,6 +1339,10 @@ int dbus_exit(void) { struct peer *p; + if (sysbus_tmr_up) { + uev_timer_stop(&sysbus_tmr); + sysbus_tmr_up = 0; + } uev_io_stop(&accept_watcher); while ((p = TAILQ_FIRST(&peers))) diff --git a/src/initctl.c b/src/initctl.c index 6379bc9a..f246a861 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -79,7 +79,6 @@ int iw, pw; extern int reboot_main(int argc, char *argv[]); - /* figure ut width of IDENT and PID columns */ static void col_widths(void) { @@ -174,21 +173,12 @@ static int toggle_debug(char *arg) return client_send(&rq, sizeof(rq)); } -static int do_log(svc_t *svc, char *tail) +static int do_log_named(const char *nm, pid_t pid, char *tail) { const char *logfile = "/var/log/syslog"; - pid_t pid; - char *nm; - if (svc) { - nm = svc_ident(svc, NULL, 0); - pid = svc->pid; - if (!pid) - return 0; /* not running */ - } else { - nm = "finit"; - pid = 1; - } + if (!pid) + return 0; /* not running */ if (!fexist(logfile)) { logfile = "/var/log/messages"; @@ -199,6 +189,13 @@ static int do_log(svc_t *svc, char *tail) return systemf("cat %s | grep '\\[%d\\]\\|%s' %s", logfile, pid, nm, tail); } +static int do_log(svc_t *svc, char *tail) +{ + if (svc) + return do_log_named(svc_ident(svc, NULL, 0), svc->pid, tail); + return do_log_named("finit", 1, tail); +} + static int show_log(char *arg) { svc_t *svc = NULL; @@ -213,6 +210,19 @@ static int show_log(char *arg) } #ifdef HAVE_DBUS +/* + * Advance one a{sv} dict entry: key + variant type; the caller reads + * the value with the link_r_* matching `type`. + */ +static int dbus_dict_next(link_reader_t *r, const char **key, char *type) +{ + if (link_r_align(r, 8) < 0) + return -1; + if (link_r_string(r, key) < 0) + return -1; + return link_r_variant_begin(r, type); +} + /* 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 @@ -254,12 +264,13 @@ static int dbus_get_manager_props(const char *const *wanted, char **out, size_t while (link_r_pos(&reader) < end) { const char *key = NULL; const char *val = NULL; + char type; 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; + if (dbus_dict_next(&reader, &key, &type) < 0) + goto fail; + if (type != 's' || link_r_string(&reader, &val) < 0) + goto fail; for (i = 0; wanted[i]; i++) { if (!strcmp(key, wanted[i])) { @@ -271,7 +282,11 @@ static int dbus_get_manager_props(const char *const *wanted, char **out, size_t link_client_close(c); return 0; +fail: + link_client_close(c); + return -1; } + #endif static int do_runlevel(char *arg) @@ -431,20 +446,28 @@ static int try_dbus_manager(const char *method, const char *arg_sig, return (rc == LINK_CALL_OK) ? 0 : -1; } +/* Build the object path for a service identity, same encoding as the + * server side; use instead of a Manager1.GetService round-trip. */ +static int dbus_svc_path(const char *ident, char *path, size_t len) +{ + const char *prefix = "/org/finit/service/"; + size_t plen = strlen(prefix); + + if (!ident || !*ident || plen >= len) + return -1; + memcpy(path, prefix, plen); + return link_path_encode(ident, path + plen, len - plen); +} + /* 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) + if (dbus_svc_path(ident, path, sizeof(path)) < 0) return -1; c = link_client_open(FINIT_BUS_SOCKET); @@ -893,16 +916,16 @@ static int do_cond_get(char *arg) { return do_cond_act(arg, COND_GET); } static int do_cond_set(char *arg) { return do_cond_act(arg, COND_SET); } static int do_cond_clr(char *arg) { return do_cond_act(arg, COND_CLR); } -static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) +static char *cond_string(const char *condstr, char *buf, size_t len, int ansi) { char *cond, *conds; buf[0] = 0; - if (!svc->cond[0]) + if (!condstr || !condstr[0]) return buf; - conds = strdupa(svc->cond); + conds = strdupa(condstr); if (!conds) return buf; @@ -954,6 +977,11 @@ static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) return buf; } +static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) +{ + return cond_string(svc->cond, buf, len, ansi); +} + static int do_cond_show(char *arg) { enum cond_state cond; @@ -1267,7 +1295,6 @@ char *runlevel_string(int currlevel, int levels) strlcat(lvl, i == INIT_LEVEL ? "S" : l, sizeof(lvl)); - if (!plain && currlevel == i) strlcat(lvl, "\e[0m", sizeof(lvl)); } else { @@ -1281,7 +1308,6 @@ char *runlevel_string(int currlevel, int levels) i++; } while (i < INIT_LEVEL); - strlcat(lvl, "]", sizeof(lvl)); return lvl; @@ -1313,23 +1339,9 @@ char *runlevel_arr(int levels) return lvl; } -static int missing(svc_t *svc) -{ - if (svc->state == SVC_HALTED_STATE && svc_is_missing(svc)) - return 1; - - return 0; -} - -static char *svc_command(svc_t *svc, char *buf, size_t len, int ansi) +static char *svc_command(svc_t *svc, char *buf, size_t len) { - int bold = missing(svc) && ansi; - - if (whichp(svc->cmd)) - bold = 0; - - strlcpy(buf, bold ? "\e[1m" : "", len); - strlcat(buf, svc->cmd, len); + strlcpy(buf, svc->cmd, len); for (int i = 1; i < MAX_NUM_SVC_ARGS; i++) { if (!svc->args[i][0]) @@ -1346,40 +1358,24 @@ static char *svc_command(svc_t *svc, char *buf, size_t len, int ansi) strlcat(buf, cmd, len); } - strlcat(buf, bold ? "\e[0m" : "", len); - return buf; } -static char *svc_environ(svc_t *svc, char *buf, size_t len, int ansi) -{ - int bold = missing(svc); - - if (!ansi || svc_checkenv(svc)) - bold = 0; - - strlcpy(buf, bold ? "\e[1m" : "", len); - strlcat(buf, svc->env, len); - strlcat(buf, bold ? "\e[0m" : "", len); - - return buf; -} - -static char *exit_status(svc_t *svc, char *buf, size_t len) +static char *exit_status_raw(int status, int manual, char *buf, size_t len) { int rc, sig; char *str; - rc = WEXITSTATUS(svc->status); - sig = WTERMSIG(svc->status); + rc = WEXITSTATUS(status); + sig = WTERMSIG(status); - if (WIFEXITED(svc->status)) { + if (WIFEXITED(status)) { str = code2str(rc); snprintf(buf, len, " (code=exited, status=%d%s%s%s)", rc, str[0] ? "/" : "", str, - svc->manual ? ", manual=yes" : ""); + manual ? ", manual=yes" : ""); } - else if (WIFSIGNALED(svc->status)) { + else if (WIFSIGNALED(status)) { str = sig2str(sig); snprintf(buf, len, " (code=signal, status=%d%s%s)", sig, str[0] ? "/" : "", str); } @@ -1387,57 +1383,6 @@ static char *exit_status(svc_t *svc, char *buf, size_t len) return buf; } -static char *status(svc_t *svc, int full) -{ - static char buf[96]; - const char *color; - char ok[48] = {0}; - char *s; - - s = svc_status(svc); - switch (svc->state) { - case SVC_HALTED_STATE: - exit_status(svc, ok, sizeof(ok)); - color = "\e[1m"; - break; - - case SVC_RUNNING_STATE: - color = "\e[1;32m"; - break; - - case SVC_DONE_STATE: - exit_status(svc, ok, sizeof(ok)); - if (WIFEXITED(svc->status)) { - if (WEXITSTATUS(svc->status)) - color = "\e[1;31m"; - else - color = "\e[1;32m"; - } else { - if (full && WIFSIGNALED(svc->status)) - color = "\e[1;31m"; - else - color = "\e[1;33m"; - } - break; - - default: - exit_status(svc, ok, sizeof(ok)); - color = "\e[1;33m"; - break; - } - - if (!full || plain) - color = NULL; - - if (!full) - snprintf(buf, sizeof(buf), "%-8.8s", s); - else - snprintf(buf, sizeof(buf), "%s%s%s%s", - color ? color : "", s, ok, color ? "\e[0m" : ""); - - return buf; -} - static void show_cgroup_tree(char *group, char *pfx) { char path[256]; @@ -1475,6 +1420,176 @@ static int svc_compare(svc_t *svc, char *arg) return 0; } +/* + * All fields the status views need, transport-independent: filled + * from a raw svc_t on the legacy socket path or from Service1 + * properties on the D-Bus path, rendered by the same printers. + */ +struct svc_row { + char ident[MAX_IDENT_LEN]; + char state[16]; + char type[16]; + char desc[MAX_STR_LEN]; + char cmdline[512]; + char env[MAX_CMD_LEN]; + char cond[MAX_COND_LEN]; + char pidfile[MAX_CMD_LEN]; + char user[MAX_USER_LEN]; + char grp[MAX_USER_LEN]; + char origin[MAX_ARG_LEN]; + uint32_t pid; + uint32_t runlevels; + uint32_t uptime; /* seconds, 0 when not running */ + uint32_t exitstatus; /* raw waitpid(2) status */ + uint32_t starts; + int32_t restart_tot; + int32_t restart_cnt; + int32_t restart_max; /* -1 = restart:always */ + int manual; + int forking; + int started; +}; + +static void fill_row_from_svc(struct svc_row *r, svc_t *svc) +{ + long now = jiffies(); + + memset(r, 0, sizeof(*r)); + svc_ident(svc, r->ident, sizeof(r->ident)); + strlcpy(r->state, svc_status(svc), sizeof(r->state)); + strlcpy(r->type, svc_typestr(svc), sizeof(r->type)); + strlcpy(r->desc, svc->desc, sizeof(r->desc)); + svc_command(svc, r->cmdline, sizeof(r->cmdline)); + strlcpy(r->env, svc->env, sizeof(r->env)); + strlcpy(r->cond, svc->cond, sizeof(r->cond)); + strlcpy(r->pidfile, svc->pidfile, sizeof(r->pidfile)); + strlcpy(r->user, svc->username, sizeof(r->user)); + strlcpy(r->grp, svc->group, sizeof(r->grp)); + strlcpy(r->origin, svc->file, sizeof(r->origin)); + r->pid = svc->pid > 0 ? (uint32_t)svc->pid : 0; + r->runlevels = (uint32_t)svc->runlevels; + if (svc->pid && now > svc->start_time) + r->uptime = (uint32_t)(now - svc->start_time); + r->exitstatus = (uint32_t)svc->status; + r->starts = (uint32_t)svc->once; + r->restart_tot = (int32_t)svc->restart_tot; + r->restart_cnt = (int32_t)svc->restart_cnt; + r->restart_max = (int32_t)svc->restart_max; + r->manual = svc->manual; + r->forking = svc->forking; + r->started = svc->started; +} + +static const char *pidfile_str(struct svc_row *r) +{ + const char *pidfn = r->pidfile; + + if (pidfn[0] == '!') + pidfn++; + else if (pidfn[0] == 0) + pidfn = "none"; + + return pidfn; +} + +/* string-only twin of svc_checkenv() */ +static int checkenv_str(const char *env) +{ + if (!env || !env[0] || env[0] == '-') + return 1; + + return fexist(env); +} + +/* same matching rules as svc_compare() */ +static int row_compare(struct svc_row *r, char *arg) +{ + char ident[MAX_IDENT_LEN]; + char *ptr; + + strlcpy(ident, r->ident, sizeof(ident)); + ptr = strchr(ident, ':'); + if (ptr && !strchr(arg, ':')) + *ptr = 0; + + return !strcmp(ident, arg); +} + +static int row_missing(struct svc_row *r) +{ + char argv0[512]; + char *sep; + + if (strcmp(r->state, "missing")) + return 0; + + strlcpy(argv0, r->cmdline, sizeof(argv0)); + sep = strchr(argv0, ' '); + if (sep) + *sep = 0; + + return !whichp(argv0); +} + +/* the words svc_status() emits for a blocked/halted service */ +static const char *halted_words[] = { + "halted", "missing", "crashed", "stopped", "busy", + "restart", "conflict", "unknown", NULL +}; + +/* transport-independent status(svc, full) */ +static char *row_status(struct svc_row *r, int full) +{ + static char buf[96]; + const char *color; + char ok[48] = {0}; + int st = (int)r->exitstatus; + size_t i; + + if (!full) { + snprintf(buf, sizeof(buf), "%-8.8s", r->state); + return buf; + } + + if (!strcmp(r->state, "running")) { + color = "\e[1;32m"; + } else if (!strcmp(r->state, "done") || !strcmp(r->state, "failed")) { + exit_status_raw(st, r->manual, ok, sizeof(ok)); + if (WIFEXITED(st)) + color = WEXITSTATUS(st) ? "\e[1;31m" : "\e[1;32m"; + else if (WIFSIGNALED(st)) + color = "\e[1;31m"; + else + color = "\e[1;33m"; + } else { + exit_status_raw(st, r->manual, ok, sizeof(ok)); + color = "\e[1;33m"; + for (i = 0; halted_words[i]; i++) { + if (!strcmp(r->state, halted_words[i])) { + color = "\e[1m"; + break; + } + } + } + + if (plain) + color = NULL; + + snprintf(buf, sizeof(buf), "%s%s%s%s", + color ? color : "", r->state, ok, color ? "\e[0m" : ""); + + return buf; +} + +/* scripting mode: exit code only, same rules as the legacy path */ +static int quiet_row(struct svc_row *r) +{ + if (!strcmp(r->type, "run") || !strcmp(r->type, "task")) + return r->started ? 0 : 1; + + return strcmp(r->state, "running") != 0; +} + /* * Escape a string for safe JSON output. Handles quotes, backslashes, * and control characters. Returns pointer to static buffer. @@ -1531,46 +1646,40 @@ static char *json_escape(const char *str) return buf; } -static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) +static int json_status_one(FILE *fp, struct svc_row *r, char *indent, int prev) { - long now = jiffies(); - char *pidfn = NULL; + const char *pidfn = pidfile_str(r); char buf[512]; - pidfn = svc->pidfile; - if (pidfn[0] == '!') - pidfn++; - else if (pidfn[0] == 0) - pidfn = "none"; - fprintf(fp, "%s" "%s{\n" "%s \"identity\": \"%s\",\n", prev ? ",\n" : indent, prev ? indent : "", - indent, svc_ident(svc, NULL, 0)); + indent, r->ident); fprintf(fp, "%s \"description\": \"%s\",\n", - indent, json_escape(svc->desc)); + indent, json_escape(r->desc)); fprintf(fp, "%s \"type\": \"%s\",\n" "%s \"forking\": %s,\n" "%s \"status\": \"%s\",\n", - indent, svc_typestr(svc), - indent, svc->forking ? "true" : "false", - indent, svc_status(svc)); + indent, r->type, + indent, r->forking ? "true" : "false", + indent, r->state); - if (svc->state != SVC_RUNNING_STATE) { + if (strcmp(r->state, "running")) { + int st = (int)r->exitstatus; int rc, sig; - rc = WEXITSTATUS(svc->status); - sig = WTERMSIG(svc->status); + rc = WEXITSTATUS(st); + sig = WTERMSIG(st); - if (WIFEXITED(svc->status)) + if (WIFEXITED(st)) fprintf(fp, "%s \"exit\": { \"%s\": %d },\n", indent, "code", rc); - else if (WIFSIGNALED(svc->status)) + else if (WIFSIGNALED(st)) fprintf(fp, "%s \"exit\": { \"%s\": %d },\n", indent, "signal", sig); @@ -1578,32 +1687,30 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) fprintf(fp, "%s \"origin\": \"%s\",\n", - indent, svc->file[0] ? svc->file : "built-in"); - svc_command(svc, buf, sizeof(buf), 0); + indent, r->origin[0] ? r->origin : "built-in"); fprintf(fp, "%s \"command\": \"%s\",\n", - indent, json_escape(buf)); + indent, json_escape(r->cmdline)); - svc_environ(svc, buf, sizeof(buf), 0); - if (buf[0]) + if (r->env[0]) fprintf(fp, - "%s \"environment\": \"%s\",\n", indent, json_escape(buf)); + "%s \"environment\": \"%s\",\n", indent, json_escape(r->env)); - svc_cond(svc, buf, sizeof(buf), 0); + cond_string(r->cond, buf, sizeof(buf), 0); if (buf[0]) fprintf(fp, "%s \"condition\": %s,\n", indent, buf); - if (svc->manual) + if (r->manual) fprintf(fp, - "%s \"starts\": %d,\n", indent, svc->once); + "%s \"starts\": %u,\n", indent, r->starts); fprintf(fp, - "%s \"restarts\": %d,\n", indent, svc->restart_tot); /* XXX: add restart_cnt and restart_max */ + "%s \"restarts\": %d,\n", indent, r->restart_tot); /* XXX: add restart_cnt and restart_max */ /* Add memory and CPU information if cgroup support is available */ - if (cgrp && svc->pid > 1) { - char *group = pid_cgroup(svc->pid); + if (cgrp && r->pid > 1) { + char *group = pid_cgroup(r->pid); if (group) { uint64_t throttled_usec = 0; @@ -1707,37 +1814,466 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) fprintf(fp, "%s \"pidfile\": \"%s\",\n" - "%s \"pid\": %d,\n" + "%s \"pid\": %u,\n" "%s \"user\": \"%s\",\n" "%s \"group\": \"%s\",\n" "%s \"uptime\": %ld,\n" "%s \"runlevels\": %s\n" "%s}", indent, pidfn, - indent, svc->pid, - indent, svc->username, - indent, svc->group, - indent, svc->pid ? now - svc->start_time : 0, - indent, runlevel_arr(svc->runlevels), + indent, r->pid, + indent, r->user, + indent, r->grp, + indent, (long)r->uptime, + indent, runlevel_arr((int)r->runlevels), indent); return 0; } -static int show_status(char *arg) +static void status_heading(int pidw, int identw) { - char ident[MAX_IDENT_LEN]; + char title[80]; + + snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", + pidw, "PID", identw, "IDENT", "STATUS", "RUNLEVELS"); + strlcat(title, !verbose ? "DESCRIPTION" : "COMMAND", sizeof(title)); + print_header("%s", title); +} + +static void print_runlevels(const char *lvls) +{ + /* ANSI escapes for the active level eat into the field width */ + if (strchr(lvls, '\e')) + printf("%-21.21s ", lvls); + else + printf("%-13.13s ", lvls); +} + +/* + * The detail view, shared by the legacy and D-Bus paths; the caller + * fills a svc_row from its transport first. Cgroup statistics and + * the log tail are always local operations, they only need the PID. + */ +static int show_one_row(struct svc_row *r) +{ + char uptm[42] = "N/A"; char buf[512]; + int bold; + + printf(" Status : %s\n", row_status(r, 1)); + printf(" Identity : %s\n", r->ident); + printf("Description : %s\n", r->desc); + printf(" Origin : %s\n", r->origin[0] ? r->origin : "built-in"); + + if (r->env[0]) { + bold = !plain && !strcmp(r->state, "missing") && + !checkenv_str(r->env); + printf("Environment : %s%s%s\n", bold ? "\e[1m" : "", + r->env, bold ? "\e[0m" : ""); + } + + cond_string(r->cond, buf, sizeof(buf), !plain); + if (buf[0]) + printf("Condition(s): <%s>\n", buf); + + bold = !plain && row_missing(r); + printf(" Command : %s%s%s\n", bold ? "\e[1m" : "", + r->cmdline, bold ? "\e[0m" : ""); + printf(" PID file : %s\n", pidfile_str(r)); + printf(" PID : %u\n", r->pid); + printf(" User : %s\n", r->user); + printf(" Group : %s\n", r->grp); + printf(" Uptime : %s\n", r->pid + ? uptime((long)r->uptime, uptm, sizeof(uptm)) : uptm); + if (r->manual) + printf(" Starts : %u\n", r->starts); + printf(" Restarts : %d (%d/%d)\n", + r->restart_tot, r->restart_cnt, r->restart_max); + printf(" Runlevels : %s\n", + runlevel_string(runlevel, (int)r->runlevels)); + + if (cgrp && r->pid > 1) { + const struct cg *cg; + uint64_t throttled_usec = 0; + uint64_t nr_throttled = 0; + char path[256]; + char *group; + + group = pid_cgroup(r->pid); + if (!group) + goto no_cgroup; /* ... or PID doesn't exist (anymore) */ + + snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group); + cg = cg_conf(path); + + printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm))); + + if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) { + printf("CPU Throttle : %lu usec (%lu times)\n", + throttled_usec, nr_throttled); + } + + printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n", + group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max, + cg->cg_mem.min, cg->cg_mem.max); + show_cgroup_tree(group, " "); + + free(group); + } +no_cgroup: + printf("\n"); + + return do_log_named(r->ident, r->pid, "| tail -10"); +} + +static void render_table(struct svc_row *rows, int n, char *filter) +{ + int i, identw = 0, pidw = 0; + + for (i = 0; i < n; i++) { + int w; + + w = (int)strlen(rows[i].ident); + if (w > identw) + identw = w; + w = snprintf(NULL, 0, "%u", rows[i].pid); + if (w > pidw) + pidw = w; + } + if (identw < 6) + identw = 6; + if (pidw < 3) + pidw = 3; + + if (heading) + status_heading(pidw, identw); + + for (i = 0; i < n; i++) { + struct svc_row *r = &rows[i]; + + if (filter && !row_compare(r, filter)) + continue; + + printf("%-*u ", pidw, r->pid); + printf("%-*s %s ", identw, r->ident, row_status(r, 0)); + print_runlevels(runlevel_string(runlevel, (int)r->runlevels)); + + if (!verbose) + puts(r->desc); + else { + int bold = !plain && row_missing(r); + + printf("%s%s%s\n", bold ? "\e[1m" : "", + r->cmdline, bold ? "\e[0m" : ""); + } + } +} + +#ifdef HAVE_DBUS +/* + * Service1 wire properties -> svc_row fields. Unknown keys and + * value types are skipped so newer finit keeps working with older + * initctl. + */ +#define ROW_STR(k, f) { k, 's', offsetof(struct svc_row, f), \ + sizeof(((struct svc_row *)0)->f) } +#define ROW_U32(k, f) { k, 'u', offsetof(struct svc_row, f), 0 } +#define ROW_BOOL(k, f) { k, 'b', offsetof(struct svc_row, f), 0 } + +static const struct row_field { + const char *key; + char type; + size_t off; + size_t len; +} row_fields[] = { + ROW_STR ("State", state), + ROW_STR ("Type", type), + ROW_STR ("Description", desc), + ROW_STR ("Command", cmdline), + ROW_STR ("Environment", env), + ROW_STR ("Conditions", cond), + ROW_STR ("PidFile", pidfile), + ROW_STR ("User", user), + ROW_STR ("Group", grp), + ROW_STR ("Origin", origin), + ROW_U32 ("Pid", pid), + ROW_U32 ("Runlevels", runlevels), + ROW_U32 ("Uptime", uptime), + ROW_U32 ("ExitStatus", exitstatus), + ROW_U32 ("Starts", starts), + ROW_U32 ("RestartsTotal", restart_tot), + ROW_U32 ("RestartCount", restart_cnt), + ROW_U32 ("RestartMax", restart_max), + ROW_BOOL("ManualStart", manual), + ROW_BOOL("Forking", forking), + ROW_BOOL("Started", started), + { NULL, 0, 0, 0 } +}; + +/* + * One service's Properties.GetAll into a row. Returns 0 on success, + * 1 when the object vanished since ListServices (bus error reply), + * -1 on transport failure. + */ +static int dbus_fill_row(link_client_t *c, struct svc_row *row) +{ + const link_reply_t *r; + link_reader_t reader; + char path[256]; + size_t end; + int rc; + + if (dbus_svc_path(row->ident, path, sizeof(path)) < 0) + return -1; + + rc = link_client_call_v(c, path, + "org.freedesktop.DBus.Properties", + "GetAll", "s", "org.finit.Service1"); + if (rc == LINK_CALL_ERROR) + return 1; /* gone since ListServices */ + if (rc != LINK_CALL_OK) + return -1; + r = link_client_reply(c); + if (!r || !r->body) + 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 struct row_field *f; + const char *key, *val; + uint32_t u; + int b; + char type; + + if (dbus_dict_next(&reader, &key, &type) < 0) + return -1; + + for (f = row_fields; f->key; f++) + if (f->type == type && !strcmp(f->key, key)) + break; + + if (!f->key) { + if (link_r_skip_basic(&reader, type) < 0) + return -1; + continue; + } + + switch (type) { + case 's': + if (link_r_string(&reader, &val) < 0) + return -1; + strlcpy((char *)row + f->off, val, f->len); + break; + case 'u': + if (link_r_u32(&reader, &u) < 0) + return -1; + memcpy((char *)row + f->off, &u, sizeof(u)); + break; + case 'b': + if (link_r_bool(&reader, &b) < 0) + return -1; + memcpy((char *)row + f->off, &b, sizeof(b)); + break; + } + } + + return 0; +} + +/* + * ListServices into a caller-freed ident array. One round trip; + * show_ident() needs nothing more. Returns count or -1. + */ +static int dbus_list_idents(link_client_t *c, char (**out)[MAX_IDENT_LEN]) +{ + char (*idents)[MAX_IDENT_LEN] = NULL, (*tmp)[MAX_IDENT_LEN]; + const link_reply_t *r; + link_reader_t reader; + size_t n = 0, end; + int rc; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "ListServices", NULL); + if (rc != LINK_CALL_OK) + return -1; + + r = link_client_reply(c); + if (!r || !r->body) + 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 *ident; + + if (link_r_string(&reader, &ident) < 0) + goto fail; + tmp = realloc(idents, (n + 1) * sizeof(*idents)); + if (!tmp) + goto fail; + idents = tmp; + /* copied: the next call clobbers the client rx buffer */ + strlcpy(idents[n], ident, sizeof(idents[n])); + n++; + } + + *out = idents; + return (int)n; +fail: + free(idents); + return -1; +} + +/* + * Rows for every service matching `arg` (all when NULL), fetched over + * D-Bus. Returns the row count (caller frees *out), or -1 to fall + * back to the legacy socket. + */ +static int dbus_fetch_svc_rows(char *arg, struct svc_row **out) +{ + char (*idents)[MAX_IDENT_LEN] = NULL; + struct svc_row *rows = NULL; + link_client_t *c; + int n, i, m = 0; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + n = dbus_list_idents(c, &idents); + if (n < 0) + goto fail; + + rows = calloc(n ? n : 1, sizeof(*rows)); + if (!rows) + goto fail; + + for (i = 0; i < n; i++) { + struct svc_row *row = &rows[m]; + int rc; + + strlcpy(row->ident, idents[i], sizeof(row->ident)); + if (arg && arg[0] && !row_compare(row, arg)) + continue; + + rc = dbus_fill_row(c, row); + if (rc < 0) + goto fail; + if (rc > 0) + continue; /* vanished, skip */ + m++; + } + + free(idents); + link_client_close(c); + *out = rows; + return m; +fail: + free(idents); + free(rows); + link_client_close(c); + return -1; +} + +/* + * All show_status() views over D-Bus, self-contained (the current + * runlevel comes from Manager1, not the legacy socket). Returns 0 + * with the command exit code in *retval, or -1 to fall back. + */ +static int dbus_show_status(char *arg, int *retval) +{ + static const char *const wanted[] = { "Runlevel", NULL }; + char curr[16] = "0"; + char *outv[] = { curr }; + struct svc_row *rows = NULL; + char *filter; + int n, i; + + n = dbus_fetch_svc_rows(arg, &rows); + if (n < 0) + return -1; + + *retval = 0; + + if (arg && arg[0]) { + if (!n) { + free(rows); + /* exits, exactly like the legacy path */ + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", arg); + } + if (n == 1) { + if (quiet) { + *retval = quiet_row(&rows[0]); + free(rows); + return 0; + } + /* runlevel feeds the detail view's Runlevels line */ + if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) + goto fail; + runlevel = atoi(curr); + if (json) { + *retval = json_status_one(stdout, &rows[0], "", 0); + puts(""); + } else + *retval = show_one_row(&rows[0]); + free(rows); + return 0; + } + /* several matches: filtered table below */ + } + + if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) + goto fail; + runlevel = atoi(curr); + + filter = (arg && arg[0]) ? arg : NULL; + if (json) { + int prev = 0; + + for (i = 0; i < n; i++) { + if (filter && !row_compare(&rows[i], filter)) + continue; + if (!prev) + fputs("[\n", stdout); + json_status_one(stdout, &rows[i], " ", prev++); + } + if (prev) + fputs("\n]\n", stdout); + } else + render_table(rows, n, filter); + + free(rows); + return 0; +fail: + free(rows); + return -1; +} +#endif /* HAVE_DBUS */ + +static int show_status(char *arg) +{ + struct svc_row row; int num = 0; svc_t *svc; +#ifdef HAVE_DBUS + { + int rc; + + if (dbus_show_status(arg, &rc) == 0) + return rc; + } +#endif + runlevel = runlevel_get(NULL); while (arg && arg[0]) { - long now = jiffies(); - char uptm[42] = "N/A"; - char *pidfn = NULL; - for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) num += svc_compare(svc, arg); @@ -1748,95 +2284,32 @@ static int show_status(char *arg) if (!svc) ERRX(noerr ? 0 : 69, "no such task or service(s): %s", arg); - if (quiet) { - if (svc_is_runtask(svc)) { - if (svc->started) - return 0; - return 1; - } - return svc->state != SVC_RUNNING_STATE; - } + fill_row_from_svc(&row, svc); + if (quiet) + return quiet_row(&row); if (json) { int rc; - rc = json_status_one(stdout, svc, "", 0); + rc = json_status_one(stdout, &row, "", 0); puts(""); return rc; } - pidfn = svc->pidfile; - if (pidfn[0] == '!') - pidfn++; - else if (pidfn[0] == 0) - pidfn = "none"; - - printf(" Status : %s\n", status(svc, 1)); - printf(" Identity : %s\n", svc_ident(svc, ident, sizeof(ident))); - printf("Description : %s\n", svc->desc); - printf(" Origin : %s\n", svc->file[0] ? svc->file : "built-in"); - svc_environ(svc, buf, sizeof(buf), !plain); - if (buf[0]) - printf("Environment : %s\n", buf); - svc_cond(svc, buf, sizeof(buf), !plain); - if (buf[0]) - printf("Condition(s): <%s>\n", buf); - printf(" Command : %s\n", svc_command(svc, buf, sizeof(buf), !plain)); - printf(" PID file : %s\n", pidfn); - printf(" PID : %d\n", svc->pid); - printf(" User : %s\n", svc->username); - printf(" Group : %s\n", svc->group); - printf(" Uptime : %s\n", svc->pid ? uptime(now - svc->start_time, uptm, sizeof(uptm)) : uptm); - if (svc->manual) - printf(" Starts : %d\n", svc->once); - printf(" Restarts : %d (%d/%d)\n", svc->restart_tot, svc->restart_cnt, svc->restart_max); - printf(" Runlevels : %s\n", runlevel_string(runlevel, svc->runlevels)); - - if (cgrp && svc->pid > 1) { - const struct cg *cg; - uint64_t throttled_usec = 0; - uint64_t nr_throttled = 0; - char path[256]; - char *group; - - group = pid_cgroup(svc->pid); - if (!group) - goto no_cgroup; /* ... or PID doesn't exist (anymore) */ - - snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group); - cg = cg_conf(path); - - printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm))); - - if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) { - printf("CPU Throttle : %lu usec (%lu times)\n", - throttled_usec, nr_throttled); - } - - printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n", - group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max, - cg->cg_mem.min, cg->cg_mem.max); - show_cgroup_tree(group, " "); - - free(group); - } - no_cgroup: - printf("\n"); - - return do_log(svc, "| tail -10"); + return show_one_row(&row); } if (json) { int prev = 0; for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { - svc_ident(svc, ident, sizeof(ident)); if (num && !svc_compare(svc, arg)) continue; if (!prev) fputs("[\n", stdout); - json_status_one(stdout, svc, " ", prev++); + fill_row_from_svc(&row, svc); + json_status_one(stdout, &row, " ", prev++); } if (prev) fputs("\n]\n", stdout); @@ -1844,64 +2317,71 @@ static int show_status(char *arg) return 0; } - col_widths(); - if (heading) { - char title[80]; - - snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", - pw, "PID", iw, "IDENT", "STATUS", "RUNLEVELS"); - if (!verbose) - strlcat(title, "DESCRIPTION", sizeof(title)); - else - strlcat(title, "COMMAND", sizeof(title)); + { + struct svc_row *rows = NULL, *tmp; + int n = 0; - print_header("%s", title); + for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { + tmp = realloc(rows, (n + 1) * sizeof(*rows)); + if (!tmp) { + free(rows); + return 1; + } + rows = tmp; + fill_row_from_svc(&rows[n++], svc); + } + render_table(rows, n, num > 1 ? arg : NULL); + free(rows); } - for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { - char *lvls; - - svc_ident(svc, ident, sizeof(ident)); - if (num && !svc_compare(svc, arg)) - continue; - - printf("%-*d ", pw, svc->pid); - printf("%-*s %s ", iw, ident, status(svc, 0)); + return 0; +} - lvls = runlevel_string(runlevel, svc->runlevels); - if (strchr(lvls, '\e')) - printf("%-21.21s ", lvls); - else - printf("%-13.13s ", lvls); +static void ident_line(const char *identity, char *arg) +{ + size_t len; + char *pos; - if (!verbose) - puts(svc->desc); - else - puts(svc_command(svc, buf, sizeof(buf), !plain)); - } + pos = strchr(identity, ':'); + if (pos) + len = (size_t)(pos - identity); + else + len = strlen(identity); + if (arg && arg[0] && strncasecmp(identity, arg, len)) + return; - return 0; + puts(identity); } static int show_ident(char *arg) { svc_t *svc; +#ifdef HAVE_DBUS + { + char (*idents)[MAX_IDENT_LEN]; + link_client_t *c; + int n = -1, i; + + c = link_client_open(FINIT_BUS_SOCKET); + if (c) { + n = dbus_list_idents(c, &idents); + link_client_close(c); + } + if (n >= 0) { + for (i = 0; i < n; i++) + ident_line(idents[i], arg); + free(idents); + return 0; + } + } +#endif + for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { char ident[MAX_IDENT_LEN]; - size_t len; - char *pos; svc_ident(svc, ident, sizeof(ident)); - pos = strchr(ident, ':'); - if (pos) - len = pos - ident; - else - len = strlen(ident); - if (arg && arg[0] && strncasecmp(ident, arg, len)) - continue; - - puts(ident); + ident_line(ident, arg); } return 0; diff --git a/src/service.c b/src/service.c index abb1a00e..3c2e05f4 100644 --- a/src/service.c +++ b/src/service.c @@ -520,7 +520,7 @@ static int is_norespawn(void) fexist("/tmp/norespawn"); } -static void compose_cmdline(svc_t *svc, char *buf, size_t len) +void compose_cmdline(svc_t *svc, char *buf, size_t len) { size_t i; diff --git a/src/service.h b/src/service.h index 4d6038a8..59699f3f 100644 --- a/src/service.h +++ b/src/service.h @@ -54,6 +54,7 @@ void service_ready_script (svc_t *svc); /* XXX: only for pidfile plugin b int service_timeout_after (svc_t *svc, int timeout, void (*cb)(svc_t *svc)); int service_timeout_cancel (svc_t *svc); +void compose_cmdline (svc_t *svc, char *buf, size_t len); void service_forked (svc_t *svc); void service_ready (svc_t *svc, int ready); diff --git a/test/Makefile.am b/test/Makefile.am index e5a3d5ac..98723738 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -77,6 +77,7 @@ EXTRA_DIST += dbus-manager.sh EXTRA_DIST += dbus-service.sh EXTRA_DIST += dbus-cond.sh EXTRA_DIST += dbus-initctl.sh +EXTRA_DIST += dbus-introspect.sh AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -139,6 +140,7 @@ TESTS += dbus-manager.sh TESTS += dbus-service.sh TESTS += dbus-cond.sh TESTS += dbus-initctl.sh +TESTS += dbus-introspect.sh endif check-recursive: setup-chroot diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh index 2a1b47c1..f903f0cf 100755 --- a/test/dbus-initctl.sh +++ b/test/dbus-initctl.sh @@ -94,3 +94,51 @@ 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 + +say "initctl status views render over D-Bus alone" +# hide the legacy socket: everything below must come over the bus +texec mv /run/finit/socket /run/finit/socket.hidden + +set +e +out=$(texec initctl -p status) +status_rc=$? +one=$(texec initctl -p status keventd) +one_rc=$? +texec initctl -q status keventd +quiet_rc=$? +jout=$(texec initctl -j status) +json_rc=$? +idout=$(texec initctl ident) +ident_rc=$? +set -e + +texec mv /run/finit/socket.hidden /run/finit/socket + +assert "status table ok without legacy socket (rc=$status_rc)" \ + "$status_rc" -eq 0 +case "$out" in + *keventd*running*) assert "keventd running in table" 0 -eq 0 ;; + *) fail "keventd/running missing from status table" ;; +esac + +assert "status detail view ok (rc=$one_rc)" "$one_rc" -eq 0 +case "$one" in + *"Status : running"*"Identity : keventd"*"Restarts :"*) + assert "detail view fields present" 0 -eq 0 ;; + *) fail "detail view incomplete: $one" ;; +esac + +assert "quiet mode exit code (rc=$quiet_rc)" "$quiet_rc" -eq 0 + +assert "json mode ok (rc=$json_rc)" "$json_rc" -eq 0 +case "$jout" in + *'"identity": "keventd"'*'"status": "running"'*) + assert "json fields present" 0 -eq 0 ;; + *) fail "json output incomplete: $jout" ;; +esac + +assert "ident ok (rc=$ident_rc)" "$ident_rc" -eq 0 +case "$idout" in + *keventd*) assert "ident lists keventd" 0 -eq 0 ;; + *) fail "ident missing keventd" ;; +esac diff --git a/test/dbus-introspect.sh b/test/dbus-introspect.sh new file mode 100755 index 00000000..08e4927c --- /dev/null +++ b/test/dbus-introspect.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# libink: introspection XML well-formedness. +# +# Covers compound type signatures in generated introspection XML: +# Cond1.Dump declares out_sig "a(ss)", which must appear as a single +# , not one per signature character. + +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 "Introspect on /org/finit/cond" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/cond) + +say "Cond1.Dump advertises a single a(ss) out arg" +case "$xml" in + *''*) assert "a(ss) intact" 0 -eq 0 ;; + *) fail "a(ss) not found as a single arg" ;; +esac + +say "No per-character type fragments in the XML" +case "$xml" in + *'type="("'* | *'type=")"'* | *'type="{"'* | *'type="}"'* | *'type="a"'*) + fail "per-character fragment leaked" ;; + *) assert "no bracket/array fragments" 0 -eq 0 ;; +esac diff --git a/test/dbus-service.sh b/test/dbus-service.sh index 9099a637..e812651b 100755 --- a/test/dbus-service.sh +++ b/test/dbus-service.sh @@ -2,9 +2,10 @@ # 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. +# GetService lookup, Introspect on a service object, Service1 +# properties via Properties.Get, Service1.Restart, authorization +# (non-root rejected), and the Manager1.ServiceStateChanged signal +# that Service1.Restart triggers. set -eu @@ -29,6 +30,27 @@ case "$xml" in fail "Service1 not visible on /org/finit/service/keventd: $xml" ;; esac +say "Service1 properties: Identity, State, Pid" +ident=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Identity) +assert "Identity is keventd (got: $ident)" "$ident" = "keventd" + +state=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 State) +assert "State is running (got: $state)" "$state" = "running" + +pid=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Pid) +assert "Pid is non-zero (got: $pid)" "$pid" -gt 0 + +say "Service1 properties are advertised in introspection XML" +case "$xml" in + *'/dev/null \ diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index fd0e3bb2..14796647 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -338,6 +338,47 @@ static int mode_get_service(int argc, char *argv[]) return rc; } +/* + * getprop BUS PATH IFACE PROP -- Properties.Get, prints the variant + * value; dispatches on the wire signature, "s" and "u" supported. + */ +static int mode_getprop(int argc, char *argv[]) +{ + const link_reply_t *r; + link_client_t *c; + link_reader_t reader; + const char *s; + uint32_t u; + 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], + "org.freedesktop.DBus.Properties", "Get", + "ss", argv[4], argv[5]); + rc = report_rc(c, rc); + if (rc == 0) { + char type = 0; + + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_variant_begin(&reader, &type) != 0) + rc = 2; + else if (type == 's' && link_r_string(&reader, &s) == 0) + printf("%s\n", s); + else if (type == 'u' && link_r_u32(&reader, &u) == 0) + printf("%u\n", u); + else { + fprintf(stderr, "unsupported variant type '%c'\n", type); + rc = 2; + } + } + link_client_close(c); + return rc; +} + static int mode_monitor_signal(int argc, char *argv[]) { link_client_t *c; @@ -441,6 +482,7 @@ int main(int argc, char *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], "getprop")) return mode_getprop (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]); From b55dada80bba1b8f114f997a137d1fcc339fdc66 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:26 +0200 Subject: [PATCH 05/22] libink: a message bus is not a peer libink was written against the only bus it had, its own, where the peer on the other end is the client. A broker is not: it routes for senders it names itself, expects a DESTINATION on anything addressed through it, and answers on its own schedule rather than next. Runlevels go on the wire as S and N rather than the digits Finit keeps internally, since that is what a caller outside Finit means by one. The library stays a convenience library, linked into finit and initctl and installed nowhere: the ABI promise waits until libink is its own project. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 10 +- configure.ac | 2 +- doc/build.md | 9 +- doc/dbus.md | 326 +++++++++++++++++------------------- libink/.gitignore | 1 - libink/Makefile.am | 18 +- libink/README.md | 64 +++++++ libink/client.c | 45 ++++- libink/libink.pc.in | 10 -- libink/link.h | 7 + libink/proto.c | 4 + libink/proto.h | 4 +- src/dbus.c | 37 +++- src/initctl.c | 21 ++- test/lib/setup.sh | 1 - test/setup-sysroot.sh | 39 +---- 16 files changed, 344 insertions(+), 254 deletions(-) create mode 100644 libink/README.md delete mode 100644 libink/libink.pc.in diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84e7cae3..cec7aeab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,16 +62,16 @@ jobs: tree /tmp || true - name: Check dependencies run: | - LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/finit + ldd /tmp/sbin/finit size /tmp/sbin/finit - LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/initctl + ldd /tmp/sbin/initctl size /tmp/sbin/initctl - LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/reboot + ldd /tmp/sbin/reboot size /tmp/sbin/reboot - name: Verify starting and showing usage text run: | - sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/finit -h - sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/initctl -h + sudo /tmp/sbin/finit -h + sudo /tmp/sbin/initctl -h - name: Enable unprivileged userns (unshare) run: | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 diff --git a/configure.ac b/configure.ac index dcd02548..e13316b7 100644 --- a/configure.ac +++ b/configure.ac @@ -13,7 +13,7 @@ AC_CONFIG_FILES([Makefile contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile doc/Makefile doc/config/Makefile dbus-1/Makefile - libink/Makefile libink/libink.pc + libink/Makefile libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile plugins/Makefile diff --git a/doc/build.md b/doc/build.md index 65bf5365..298437b6 100644 --- a/doc/build.md +++ b/doc/build.md @@ -50,9 +50,16 @@ Below are a few of the main switches to configure: `/proc/cmdline`, this is *not recommended* since Finit may be running as the init for container apps that can see the host's `/proc` filesystem +* `--disable-dbus`: Opt out of Finit's built-in D-Bus support, enabled by + default. See [D-Bus Integration](dbus.md) for what it provides. Not to + be confused with `--enable-dbus-plugin` below, which only starts an + external `dbus-daemon` + * `--enable-alsa-utils-plugin`: Enable the optional `alsa-utils.so` sound plugin. -* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin. +* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin, which + launches `dbus-daemon` at boot. Unrelated to the built-in bus, see + `--disable-dbus` above. * `--enable-resolvconf-plugin`: Enable the `resolvconf.so` optional plugin. diff --git a/doc/dbus.md b/doc/dbus.md index 60efff2d..2c89a842 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -1,45 +1,45 @@ D-Bus Integration ================= -Finit ships with a built-in, brokerless [D-Bus][] implementation, -**libink**, that exposes the running init system as a peer on its own -private bus, and optionally on the system bus when `dbus-daemon` is -available. Everything `initctl` does is also reachable from any -generic D-Bus tooling — `dbus-send`, `dbus-monitor`, `gdbus`, -language bindings, dashboards, monitoring agents, etc. +Finit ships with a built-in, brokerless [D-Bus][] implementation, **libink** +(`-link`), that exposes the running init system as a peer on its own private +bus, and optionally on the system bus when `dbus-daemon` is available. +Everything `initctl` does is also reachable from any generic D-Bus tooling — +`dbus-send`, `dbus-monitor`, `gdbus`, language bindings, dashboards, +monitoring agents, etc. > [!NOTE] -> D-Bus support is enabled at build time with `--enable-dbus`. See -> [Building](build.md) for details. When disabled, `initctl` keeps -> using the legacy `INIT_SOCKET` transport and Finit exposes no bus. +> D-Bus support is enabled by default, opt out at build time with +> `--disable-dbus`, see [Building](build.md) for details. When disabled, +> `initctl` keeps using the legacy `INIT_SOCKET` transport and Finit +> exposes no bus. Bus address ----------- -| Bus | Address | -| --- | --- | -| Local (always) | `unix:path=/run/finit/bus` | -| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket`, well-known name `org.finit` | +| Bus | Address | +|------------------------|---------------------------------------------| +| Local (always) | `unix:path=/run/finit/bus` | +| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket` | -The **local** bus is brokerless: clients connect straight to Finit -over a Unix-domain socket using the standard D-Bus SASL EXTERNAL -handshake. No `dbus-daemon` is required, which makes it suitable for -embedded systems that don't ship one. +The **local** bus is brokerless: clients connect straight to Finit over a +Unix-domain socket using the standard D-Bus SASL EXTERNAL handshake. No +`dbus-daemon` is required, which makes it suitable for embedded systems that +don't ship one. -The **system** bus is best-effort: at start-up Finit probes for a -running `dbus-daemon` and, if reachable, registers `org.finit` so that -standard tooling sees Finit just like any other system service: +The **system** bus is best-effort: Finit probes for a running `dbus-daemon` +and, when reachable, claims the well-known name `org.finit` so that standard +tooling sees Finit just like any other system service: ```sh -dbus-send --system --print-reply --dest=org.finit \ - /org/finit/manager \ +dbus-send --system --print-reply --dest=org.finit /org/finit/manager \ org.finit.Manager1.ListServices dbus-monitor --system "sender='org.finit'" ``` -If no system bus is present (the common case on embedded targets), -this step is silently skipped. +If no system bus is present (the common case on embedded targets), this step +is silently skipped. Object tree ----------- @@ -59,12 +59,15 @@ Object tree Every node implements the usual stock interfaces: -| Interface | Purpose | -| ------------------------------------ | ------- | -| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` (on `/org/freedesktop/DBus`) | -| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | -| `org.freedesktop.DBus.Introspectable`| `Introspect()` — XML description | -| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | +| Interface | Purpose | +|---------------------------------------|-------------------------------------------| +| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` | +| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | +| `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description | +| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | + +Note: `Hello`, `AddMatch`, and `RemoveMatch` are answered on the canonical +`/org/freedesktop/DBus` object only, as per the D-Bus specification. `org.finit.Manager1` -------------------- @@ -74,88 +77,89 @@ and the service registry. ### Methods -| Method | In sig | Out sig | Privileged | Notes | -| ----------------------- | ------ | ------- | ---------- | ----- | -| `ListServices` | — | `as` | no | Returns the identities (`name`, `name:id`) of every loaded service. | -| `GetService` | `s` | `o` | no | Resolves a service identity to its `Service1` object path. | -| `Start` | `s` | — | yes | Start the service(s) matching the identity. | -| `Stop` | `s` | — | yes | Stop the service(s) matching the identity. | -| `Restart` | `s` | — | yes | Restart (stop + start) the service(s). | -| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes (same as `initctl reload`). | -| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6). | -| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag. | -| `Signal` | `su` | — | yes | Send signal number `u` (1–31) to every running service matching identity `s`. Halted matches are silently skipped. | -| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM. | -| `Reboot` / `Halt` / `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence. | +| Method | In sig | Out sig | Priv. | Notes | +|------------------------------|--------|---------|-------|-----------------------------------------------------------| +| `ListServices` | — | `as` | no | Returns identities (`name`, `name:id`) of loaded services | +| `GetService` | `s` | `o` | no | Resolves an identity to its `Service1` object path | +| `Start` | `s` | — | yes | Start the service(s) matching the identity | +| `Stop` | `s` | — | yes | Stop the service(s) matching the identity | +| `Restart` | `s` | — | yes | Restart (stop + start) the service(s) | +| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes | +| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6) | +| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag | +| `Signal` | `su` | — | yes | Send signal `u` (1–31) to services matching identity `s` | +| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM | +| `Reboot`, `Halt`, `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence | ### Properties All read-only strings; observable via `Properties.Get` and `Properties.GetAll`. -| Property | Type | Returns | -| -------------- | ---- | ------- | -| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"`. | -| `PrevRunlevel` | `s` | Previous runlevel, same encoding. | -| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`). | +| Property | Type | Returns | +|----------------|------|--------------------------------------------------------| +| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"` | +| `PrevRunlevel` | `s` | Previous runlevel, same encoding, `"N"` when none | +| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`) | ### Signals -| Signal | Body | Fires when | -| ----------------------- | ---- | ---------- | -| `ServiceStateChanged` | `(sss)` — identity, old state, new state | A service transitions between supervisor states. | -| `RunlevelChanged` | `(ss)` — old level, new level | The system enters a new runlevel. | +| Signal | Body | Fires when | +|-----------------------|----------------------------------------|------------------------| +| `ServiceStateChanged` | `sss` — identity, old state, new state | Service transitions | +| `RunlevelChanged` | `ss` — old level, new level | System runlevel change | State names emitted by `ServiceStateChanged` are stable wire strings: `halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, -`paused`, `waiting`, `starting`, `running`. +`paused`, `waiting`, `starting`, `running`. `RunlevelChanged` levels +use the same encoding as the `Runlevel` property: digits, `"S"`, `"N"`. `org.finit.Service1` (per-service objects) ------------------------------------------ -Lives at **`/org/finit/service/`**, one object per loaded -service. `` is the service identity (name, or `name:id` for -templated services) put through systemd-style `_HH` hex escaping — -ASCII alphanumerics and `_` pass through, anything else becomes `_HH` -where `HH` is the hex byte. Use `Manager1.GetService(identity)` to -look up the exact path rather than constructing it by hand. +Lives at **`/org/finit/service/`**, one object per loaded service. +`` is the service identity (name, or `name:id` for templated +services) put through systemd-style `_HH` hex escaping — ASCII alphanumerics +and `_` pass through, anything else becomes `_HH` where `HH` is the hex byte. +Use `Manager1.GetService(identity)` to look up the exact path rather than +constructing it by hand. -| Method | In sig | Out sig | Privileged | Notes | -| --------- | ------ | ------- | ---------- | ----- | -| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service. | -| `Stop` | — | — | yes | … | -| `Restart` | — | — | yes | … | -| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | +| Method | In sig | Out sig | Priv. | Notes | +|-----------|--------|---------|-------|-------------------------------------------------------------| +| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service | +| `Stop` | — | — | yes | … | +| `Restart` | — | — | yes | … | +| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart) | ### Properties All read-only; observable via `Properties.Get` and `Properties.GetAll`. -| Property | Type | Returns | -| -------------- | ---- | ------- | -| `Identity` | `s` | Service identity, `name` or `name:id`. | -| `Name` | `s` | Program name (basename of the command). | -| `State` | `s` | Current status, same vocabulary as `initctl status` (richer than the coarse `ServiceStateChanged` strings). | -| `Pid` | `u` | Current PID, 0 when not running. | -| `RestartCount` | `u` | Restarts since the last stable run. | -| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S. | -| `Description` | `s` | The service's `description` string. | -| `Command` | `s` | Full command line, arguments included. | -| `Conditions` | `s` | Declared conditions, raw `.conf` form. | -| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free`. | -| `Origin` | `s` | Source `.conf` file, empty for built-ins. | -| `Environment` | `s` | The service's `env` setting, raw. | -| `PidFile` | `s` | Declared PID file, raw (`!` prefix included). | -| `User` | `s` | User the service runs as. | -| `Group` | `s` | Group the service runs as. | -| `Uptime` | `u` | Seconds since start, 0 when not running. | -| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit. | -| `RestartsTotal`| `u` | Restarts over the service's lifetime. | -| `RestartMax` | `u` | Restart limit before the service is blocked. | -| `Starts` | `u` | Times started, for `manual-start` units. | -| `ManualStart` | `b` | `manual-start` set in the `.conf`. | -| `Forking` | `b` | Daemon forks to background. | -| `Started` | `b` | Run/task completed successfully. | +| Property | Type | Returns | +|-----------------|------|----------------------------------------------------------------| +| `Identity` | `s` | Service identity, `name` or `name:id` | +| `Name` | `s` | Program name (basename of the command) | +| `State` | `s` | Current status, same vocabulary as `initctl status` | +| `Pid` | `u` | Current PID, 0 when not running | +| `RestartCount` | `u` | Restarts since the last stable run | +| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S | +| `Description` | `s` | The service's `description` string | +| `Command` | `s` | Full command line, arguments included | +| `Conditions` | `s` | Declared conditions, raw `.conf` form | +| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free` | +| `Origin` | `s` | Source `.conf` file, empty for built-ins | +| `Environment` | `s` | The service's `env` setting, raw | +| `PidFile` | `s` | Declared PID file, raw (`!` prefix included) | +| `User` | `s` | User the service runs as | +| `Group` | `s` | Group the service runs as | +| `Uptime` | `u` | Seconds since start, 0 when not running | +| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit | +| `RestartsTotal` | `u` | Restarts over the service's lifetime | +| `RestartMax` | `u` | Restart limit before the service is blocked | +| `Starts` | `u` | Times started, for `manual-start` units | +| `ManualStart` | `b` | `manual-start` set in the `.conf` | +| `Forking` | `b` | Daemon forks to background | +| `Started` | `b` | Run/task completed successfully | On every state transition the object also emits the standard `org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State` @@ -174,89 +178,87 @@ Lives at **`/org/finit/cond`**. Exposes Finit's ### Methods -| Method | In sig | Out sig | Privileged | Notes | -| -------- | ------ | ------- | ---------- | ----- | -| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition. | -| `Set` | `s` | — | yes | Assert a `usr/` condition. Non-`usr/*` paths are rejected with `InvalidArgs` (system conditions belong to Finit's state machine). | -| `Clear` | `s` | — | yes | Deassert a `usr/` condition. | -| `List` | — | `as` | no | Names of all known conditions. | -| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns. | +| Method | In sig | Out sig | Priv. | Notes | +|---------|--------|---------|-------|--------------------------------------------------------------| +| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition | +| `Set` | `s` | — | yes | Assert a `usr/` condition | +| `Clear` | `s` | — | yes | Deassert a `usr/` condition | +| `List` | — | `as` | no | Names of all known conditions | +| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns | + +Note: non-`usr/*` paths are rejected with `InvalidArgs` -- system +conditions belong to Finit's state machine. ### Signals -| Signal | Body | Fires when | -| ------------------- | ---- | ---------- | -| `ConditionChanged` | `(ss)` — name, new state | A condition is asserted or deasserted. | +| Signal | Body | Fires when | +|--------------------|------------------------|---------------------------------------| +| `ConditionChanged` | `ss` — name, new state | A condition is asserted or deasserted | Authorization ------------- -Privileged methods reject any caller whose peer `uid` isn't 0. -On the **local** bus the kernel's `SO_PEERCRED` socket option tells -Finit exactly who's calling, so privilege escalation through the bus -is impossible. +Privileged methods reject any caller whose peer `uid` isn't 0. On the +**local** bus the kernel's `SO_PEERCRED` socket option tells Finit exactly +who's calling, so privilege escalation through the bus is impossible. -On the **system** bus, all incoming traffic is treated as -unprivileged: it arrives through `dbus-daemon` (typically running as -root) and Finit cannot yet ask the daemon for the real requester's -uid via `GetConnectionUnixUser`. This means external tooling can -freely `Get`/`Introspect`/`ListServices`, but every state-changing -method returns `org.freedesktop.DBus.Error.AccessDenied`. Per-sender -uid lookup is on the roadmap. +On the **system** bus, all incoming traffic is treated as unprivileged: it +arrives through `dbus-daemon` (typically running as root) and Finit cannot yet +ask the daemon for the real requester's uid via `GetConnectionUnixUser`. This +means external tooling can freely `Get`/`Introspect`/`ListServices`, but every +state-changing method returns `org.freedesktop.DBus.Error.AccessDenied`. +Per-sender uid lookup is on the roadmap. When a privileged method is rejected the error name is exactly -`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a -short reason string (e.g. `"permission denied: Start requires root"`). +`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a short reason +string (e.g. `"permission denied: Start requires root"`). `initctl` integration --------------------- -`initctl` transparently routes through D-Bus when the bus socket is -present, and falls back to the legacy `INIT_SOCKET` transport -otherwise. Concretely, the following subcommands use the bus first: - -| 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. +`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/clear` | `Cond1.{Set,Get,Clear}`, `clr` is an alias | + +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` emits the standard `Cond1.ConditionChanged` signal as a side + effect, so subscribers observe user-driven state changes the same way they + observe service-driven ones. Examples -------- -The examples below use `dbus-send` and `dbus-monitor`, which ship as -part of the [dbus][] reference implementation; they're widely -packaged and don't pull in any extra runtime. Any tool that speaks -D-Bus over an AF_UNIX socket works equally well — `gdbus`, Python's -`jeepney`/`dasbus`, etc. — substitute their syntax for setting the -bus address. +The examples below use `dbus-send` and `dbus-monitor`, which ship as part of +the [dbus][] reference implementation; they're widely packaged and don't pull +in any extra runtime. Any tool that speaks D-Bus over an AF_UNIX socket works +equally well — `gdbus`, Python's `jeepney`/`dasbus`, etc. — substitute their +syntax for setting the bus address. The wire protocol is the compatibility +surface: Finit's own **libink** is an internal implementation detail, external +clients should use any standard D-Bus library. When `org.finit` is registered on the system bus you can replace -`--address=unix:path=/run/finit/bus` with `--system` in any example -below. +`--address=unix:path=/run/finit/bus` with `--system` in any example below. List the running services: @@ -309,21 +311,5 @@ The `--dest=org.finit` argument is informational on the local brokerless bus — Finit accepts any destination because there's no broker to route by name — but `dbus-send` requires it syntactically. -[dbus]: https://gitlab.freedesktop.org/dbus/dbus - -Implementation notes --------------------- - -The D-Bus server library lives in `libink/`. It speaks the binary -D-Bus 1.0 wire format directly, has no `libdbus`/`sd-bus`/`GIO` -dependency, and is tiny — a few thousand lines of C. The Finit-side -glue in `src/dbus.c` registers vtables for Manager1/Service1/Cond1, -emits the four signals from the appropriate hook points (state -transitions, runlevel transitions, condition flips), and bridges the -event loop to the libink server. - -The `initctl` client uses the same library — `link_client_open`, -`link_client_call_v`, `link_client_reply`, `link_reader_*` — so the -single wire-format implementation serves both ends. - +[dbus]: https://gitlab.freedesktop.org/dbus/dbus [D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/libink/.gitignore b/libink/.gitignore index 8f7eb2a5..ffdcdbf7 100644 --- a/libink/.gitignore +++ b/libink/.gitignore @@ -3,6 +3,5 @@ .dirstamp *.lo libink.* -!libink.pc.in Makefile Makefile.in diff --git a/libink/Makefile.am b/libink/Makefile.am index 7f908a9b..e2f46e3a 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -1,5 +1,10 @@ # libink — brokerless D-Bus library (server + client), born inside Finit -lib_LTLIBRARIES = libink.la +# +# Internal convenience library: linked statically into finit and +# initctl, nothing installed. Public install (and the ABI promise +# that comes with it) is deferred until libink is extracted into its +# own project. +noinst_LTLIBRARIES = libink.la libink_la_SOURCES = server.c auth.c connection.c \ proto.c proto.h \ marshal.c marshal.h \ @@ -7,16 +12,7 @@ libink_la_SOURCES = server.c auth.c connection.c \ match.c \ path.c \ client.c io.c \ - internal.h + link.h path.h internal.h -libink_la_LDFLAGS = -version-info 0:0:0 libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 - -# pkg-config support -pkgconfigdir = $(libdir)/pkgconfig -pkgconfig_DATA = libink.pc - -# Public headers install to $(includedir)/ink/ -inkdir = $(includedir)/ink -ink_HEADERS = link.h path.h diff --git a/libink/README.md b/libink/README.md new file mode 100644 index 00000000..fbbf64c1 --- /dev/null +++ b/libink/README.md @@ -0,0 +1,64 @@ +libink — brokerless D-Bus for Finit +=================================== + +libink is a small C library implementing the [D-Bus wire protocol][spec], +both the server and the client side, without a broker and without any +dependency on `libdbus`, `sd-bus`, or GIO. It was born inside Finit to +let PID 1 be a bus of its own: clients connect straight to the listening +socket, authenticate with the standard SASL EXTERNAL handshake, and get +kernel-authenticated credentials via `SO_PEERCRED`. + +For what the bus exposes and how to talk to it, see the User Guide, +[D-Bus Integration](../doc/dbus.md). This file covers the library +itself. + +Status +------ + +libink is an internal implementation detail of Finit: built as a libtool +convenience library, linked statically into `finit` and `initctl`, +nothing installed. There is deliberately no ABI promise yet — that +comes if/when libink is extracted into a project of its own. External +D-Bus clients need none of this; the wire protocol is the compatibility +surface, any standard D-Bus library works. + +Layout +------ + +| File | Contents | +|-----------------|-------------------------------------------------------| +| `server.c` | Listening socket, accept, peer credential capture | +| `auth.c` | SASL EXTERNAL handshake, uid verification | +| `connection.c` | Per-peer state machine, message framing | +| `proto.c` | Wire header parse/build | +| `marshal.c` | Body (de)marshalling: basic types, arrays, variants | +| `dispatch.c` | Object tree, vtable registration, method dispatch | +| `builtin.c` | `org.freedesktop.DBus.*` stock interfaces | +| `match.c` | AddMatch/RemoveMatch rule parsing and signal filter | +| `path.c` | systemd-style `_HH` object path encoding | +| `client.c` | Outgoing connections, method calls, reply/signal wait | +| `io.c` | Shared EINTR-resilient read/write loops | + +Public API symbols carry the `link_*` prefix (`link.h`), internal ones +`__*` (`internal.h`). Method handlers are registered as vtables of +`link_method_t`/`link_property_t`; the framework emits variant +signatures from the property table so the declared type is the single +source of truth. + +The boundary to Finit is deliberate: nothing under `libink/` includes a +Finit header. All glue lives in `src/dbus.c` — object registration, +signal emission from the service/condition/runlevel hook points, and +the uev event loop bridge. `initctl` uses the client half of the same +library, so one wire-format implementation serves both ends. If libink +is ever spun out, that file is the cut line. + +Testing +------- + +The `test/dbus-*.sh` suite exercises the library end to end against a +live Finit in a namespace, driven by `test/src/dbus-auth-client.c`. +Wire-format conformance against third-party tools (`dbus-send`, +`dbus-monitor`) and fuzzing of the parsers are tracked as pre-merge +work — this is PID 1's attack surface. + +[spec]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/libink/client.c b/libink/client.c index fbfbe70f..15a1760d 100644 --- a/libink/client.c +++ b/libink/client.c @@ -24,6 +24,7 @@ struct link_client { int fd; uint32_t next_serial; + const char *destination; /* not owned; NULL when brokerless */ link_reply_t reply; /* most recent reply view (points into rxbuf) */ /* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which * is a wire-valid (if malformed) type, so we need an out-of-band @@ -87,6 +88,12 @@ link_client_t *link_client_open(const char *path) return link_client_open_timeout(path, 0); } +void link_client_set_destination(link_client_t *c, const char *destination) +{ + if (c) + c->destination = destination; +} + void link_client_close(link_client_t *c) { if (!c) @@ -187,6 +194,41 @@ static void clear_reply(link_client_t *c) c->have_reply = 0; } +/* A broker interleaves traffic of its own with our replies: claiming a + * name makes it emit NameAcquired, and it arrives before the reply to + * the call that caused it. Read past anything that is not the reply + * we are waiting for. On a brokerless link nothing is interleaved and + * the first message read is always the one we want. + * + * Bounded so a chatty or hostile broker cannot stall PID 1 here; each + * read is bounded in turn by SO_RCVTIMEO when the caller asked for a + * timeout at open. */ +#define LINK_CALL_MAX_SKIP 16 + +static int read_reply(link_client_t *c, uint32_t serial) +{ + int i; + + for (i = 0; i < LINK_CALL_MAX_SKIP; i++) { + struct link_msg msg; + + if (read_one(c, &msg) < 0) + return -1; + + /* Not a reply at all, or a reply to something else. */ + if (msg.type != LINK_MSG_METHOD_RETURN && msg.type != LINK_MSG_ERROR) + continue; + if (msg.reply_serial != serial) + continue; + + publish_reply(c, &msg); + return 0; + } + + errno = EPROTO; + return -1; +} + /* Wait up to timeout_ms (-1 = forever) for one full inbound frame * and publish it. Returns 0 on success, 1 on timeout, -1 on error. */ static int read_and_publish(link_client_t *c, int timeout_ms) @@ -235,6 +277,7 @@ int link_client_call(link_client_t *c, serial = c->next_serial++; hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, obj_path, interface, member, + c->destination, signature, (uint32_t)body_len); if (hlen < 0) return LINK_CALL_FAIL; @@ -244,7 +287,7 @@ int link_client_call(link_client_t *c, if (body_len > 0 && send_all(c->fd, body, body_len) < 0) return LINK_CALL_FAIL; - if (read_and_publish(c, -1) != 0) + if (read_reply(c, serial) < 0) return LINK_CALL_FAIL; if (c->reply.type == LINK_MSG_METHOD_RETURN) diff --git a/libink/libink.pc.in b/libink/libink.pc.in deleted file mode 100644 index f84248de..00000000 --- a/libink/libink.pc.in +++ /dev/null @@ -1,10 +0,0 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - -Name: libink -Description: Brokerless D-Bus server library, born inside Finit -Version: @PACKAGE_VERSION@ -Libs: -L${libdir} -link -Cflags: -I${includedir} diff --git a/libink/link.h b/libink/link.h index 265905cb..5f1988a8 100644 --- a/libink/link.h +++ b/libink/link.h @@ -226,6 +226,13 @@ link_client_t *link_client_open_timeout(const char *path, int timeout_ms); void link_client_close(link_client_t *c); +/* Address subsequent calls on `c` to a well-known name. Needed when + * a broker routes the message, e.g. "org.freedesktop.DBus" to reach + * the bus driver itself; a brokerless link has a single peer and + * needs no destination, which is the default. `destination` is not + * copied, so it must outlive the client. */ +void link_client_set_destination(link_client_t *c, const char *destination); + /* Detach the authenticated socket from the client and return the raw * fd; subsequent link_client_close on `c` is invalid because the * structure has already been freed. Used by callers (e.g. system-bus diff --git a/libink/proto.c b/libink/proto.c index d9bf712e..1d7f27c6 100644 --- a/libink/proto.c +++ b/libink/proto.c @@ -354,6 +354,7 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap, const char *path, const char *interface, const char *member, + const char *destination, const char *signature, uint32_t body_len) { @@ -369,6 +370,9 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap, return -1; if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0) return -1; + if (destination && + put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0) + return -1; if (signature && *signature && put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) return -1; diff --git a/libink/proto.h b/libink/proto.h index f7ef0866..ef28f4b7 100644 --- a/libink/proto.h +++ b/libink/proto.h @@ -89,12 +89,14 @@ ssize_t __msg_build_signal(uint8_t *buf, size_t cap, const char *member, const char *signature, uint32_t body_len); -/* Build a method-call header (client side). */ +/* Build a method-call header (client side). `destination` is NULL + * when no broker routes the message. */ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap, uint32_t serial, const char *path, const char *interface, const char *member, + const char *destination, const char *signature, uint32_t body_len); diff --git a/src/dbus.c b/src/dbus.c index 8f98b3d1..95c9af66 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -457,15 +457,31 @@ static int manager_suspend(link_call_t *call, void *u) * org.freedesktop.DBus.Properties interface. Getters write a * variant containing a single string. */ -/* Two distinct getters because the property table is static const -- - * we can't bind &runlevel/&prevlevel through userdata. */ +/* + * Two distinct getters because the property table is static const -- + * we can't bind &runlevel/&prevlevel through userdata. The values + * use the same encoding as the runlevel(8) command and `initctl + * runlevel`: "S" for single-user, "N" for no previous runlevel -- + * the internal digit is not a wire format. + */ +static const char *runlevel_encode(int level, char *buf, size_t len) +{ + if (level == INIT_LEVEL) + strlcpy(buf, "S", len); + else if (level >= 0 && level <= 9) + snprintf(buf, len, "%d", level); + else + strlcpy(buf, "N", len); + + return buf; +} + static int prop_runlevel(link_writer_t *w, void *u) { char buf[8]; (void)u; - snprintf(buf, sizeof(buf), "%d", runlevel); - link_w_string(w, buf); + link_w_string(w, runlevel_encode(runlevel, buf, sizeof(buf))); return 0; } @@ -474,7 +490,10 @@ static int prop_prevrunlevel(link_writer_t *w, void *u) char buf[8]; (void)u; - snprintf(buf, sizeof(buf), "%d", prevlevel); + if (prevlevel <= 0 || prevlevel > 9) + strlcpy(buf, "N", sizeof(buf)); + else + snprintf(buf, sizeof(buf), "%d", prevlevel); link_w_string(w, buf); return 0; } @@ -893,8 +912,8 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) /* ---------- signal emission: RunlevelChanged ---------- * * Fired by sm.c right after the runlevel global flips. Body is - * (old, new) as one-digit strings, matching the format that - * Manager1.Runlevel (the property) returns. */ + * (old, new) in the same runlevel(8) encoding as the Manager1 + * Runlevel property: digits, "S", or "N". */ void dbus_notify_runlevel_change(int old_level, int new_level) { uint8_t body[64]; @@ -902,8 +921,8 @@ void dbus_notify_runlevel_change(int old_level, int new_level) char old_s[8], new_s[8]; ssize_t blen; - snprintf(old_s, sizeof(old_s), "%d", old_level); - snprintf(new_s, sizeof(new_s), "%d", new_level); + runlevel_encode(old_level, old_s, sizeof(old_s)); + runlevel_encode(new_level, new_s, sizeof(new_s)); link_writer_init(&w, body, sizeof(body)); link_w_string(&w, old_s); diff --git a/src/initctl.c b/src/initctl.c index f246a861..e5f0df70 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -308,12 +308,8 @@ static int do_runlevel(char *arg) if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 && curr_buf[0] && prev_buf[0]) { - int cl = atoi(curr_buf); - int pl = atoi(prev_buf); - - curr = (cl == INIT_LEVEL) ? 'S' : (char)(cl + '0'); - prev = (pl > 0 && pl <= 9) ? (char)(pl + '0') : 'N'; - printf("%c %c\n", prev, curr); + /* already in runlevel(8) encoding: digits, S, N */ + printf("%s %s\n", prev_buf, curr_buf); return 0; } #endif @@ -2181,6 +2177,15 @@ static int dbus_fetch_svc_rows(char *arg, struct svc_row **out) return -1; } +/* wire encoding is runlevel(8) style: digits, S, N */ +static int runlevel_from_str(const char *s) +{ + if (!strcmp(s, "S")) + return INIT_LEVEL; + + return atoi(s); +} + /* * All show_status() views over D-Bus, self-contained (the current * runlevel comes from Manager1, not the legacy socket). Returns 0 @@ -2216,7 +2221,7 @@ static int dbus_show_status(char *arg, int *retval) /* runlevel feeds the detail view's Runlevels line */ if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) goto fail; - runlevel = atoi(curr); + runlevel = runlevel_from_str(curr); if (json) { *retval = json_status_one(stdout, &rows[0], "", 0); puts(""); @@ -2230,7 +2235,7 @@ static int dbus_show_status(char *arg, int *retval) if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) goto fail; - runlevel = atoi(curr); + runlevel = runlevel_from_str(curr); filter = (arg && arg[0]) ? arg : NULL; if (json) { diff --git a/test/lib/setup.sh b/test/lib/setup.sh index 18b90138..41065998 100755 --- a/test/lib/setup.sh +++ b/test/lib/setup.sh @@ -330,7 +330,6 @@ export SYSROOT top_builddir="${top_builddir:-$TEST_DIR/..}" sysroot_finit="$SYSROOT/sbin/finit" built_finit="$top_builddir/src/finit" -[ -x "$top_builddir/src/.libs/finit" ] && built_finit="$top_builddir/src/.libs/finit" if [ -x "$built_finit" ] && [ -e "$sysroot_finit" ] && ! cmp -s "$built_finit" "$sysroot_finit"; then fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'" diff --git a/test/setup-sysroot.sh b/test/setup-sysroot.sh index f986125e..59af2272 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -2,41 +2,17 @@ set -eu -echo "=== Finit Test Sysroot Setup ===" -echo "Date: $(date)" -echo "SYSROOT: $SYSROOT" -echo "top_builddir: $top_builddir" -echo "srcdir: $srcdir" -echo "================================" -echo - # shellcheck disable=SC2154 make -C "$top_builddir" DESTDIR="$SYSROOT" install mkdir -p "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" -# Prefer the real ELF in .libs/ over the libtool wrapper script -- -# since the test client links libink.la, libtool wraps the top-level -# dbus-auth-client as a shell script that re-execs the real binary -# via its own RPATH, which falls apart inside the test namespace. -if [ -x "$top_builddir/test/src/.libs/dbus-auth-client" ]; then - cp "$top_builddir/test/src/.libs/dbus-auth-client" "$SYSROOT/sbin/" -elif [ -x "$top_builddir/test/src/dbus-auth-client" ]; then - cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" +if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then + cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" fi # shellcheck disable=SC2154 -# Prefer the real ELF in .libs/ over the libtool wrapper script at -# $top_builddir/src/finit. Libtool generates a shell wrapper when -# the binary depends on an in-tree convenience library (e.g. libink), -# and `ldd ` returns "not a dynamic executable", which -# silently makes sysroot.mk copy zero host libs into the sysroot. -if [ -f "$top_builddir/src/.libs/finit" ]; then - finitbin_for_ldd="$(pwd)/$top_builddir/src/.libs/finit" -else - finitbin_for_ldd="$(pwd)/$top_builddir/src/finit" -fi -FINITBIN="$finitbin_for_ldd" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk" +FINITBIN="$(pwd)/$top_builddir/src/finit" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk" # Drop plugins we don't need in test, only causes confusing FAIL in logs. for plugin in tty.so urandom.so rtc.so modprobe.so; do @@ -50,11 +26,4 @@ for conf in 10-hotplug.conf; do done # Update dynamic linker cache for /usr/local/lib libraries -echo "Running ldconfig in sysroot: $SYSROOT" -echo "Contents of $SYSROOT/etc/ld.so.conf:" -cat "$SYSROOT/etc/ld.so.conf" || echo "Warning: ld.so.conf not found" -echo "Libraries in $SYSROOT/usr/local/lib:" -ls -la "$SYSROOT/usr/local/lib/" 2>/dev/null || echo "Warning: /usr/local/lib not found in sysroot" -ldconfig -v -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?" -echo "Verifying ldconfig cache was created:" -ls -la "$SYSROOT/etc/ld.so.cache" || echo "Warning: ld.so.cache not created" +ldconfig -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?" From 3499a5eb76ac24171b92f3508a53ea504c9c028e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 7 Aug 2026 07:23:29 +0200 Subject: [PATCH 06/22] dbus: say why the system-bus attach failed The attach is best effort and its diagnostics were tuned for the case where no broker exists, so a broker that answers but refuses us was reported as a bare rc=1 at debug level. Chasing that meant reading the header builder to find out what the number meant. Failures now quote the error name the broker sent, and the one for a name we could not claim says which of the three ways it went wrong. Repeats stay quiet. The probe runs on every service and condition change, and before syslog is up each line is an open, write and close on /dev/kmsg, so a broker that keeps refusing would otherwise flood the console during boot. Signed-off-by: Joachim Wiberg --- src/dbus.c | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index 95c9af66..945b881e 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -1201,9 +1201,27 @@ void dbus_notify_condition_change(const char *name, const char *state) * rather log than silently sit in the queue). */ #define DBUS_NAME_FLAG_DO_NOT_QUEUE 0x04 +/* sysbus_probe() re-runs on every service and condition change, so a + * broker that keeps refusing would repeat itself for every event, and + * before syslog is up each line is an open/write/close on /dev/kmsg. + * Say it once, then trace, until an attach succeeds. */ +static int sysbus_warned; + +#define sysbus_level() (sysbus_warned ? LOG_DEBUG : LOG_WARNING) + +/* What the broker said, for the log. A refused call carries an error + * name; anything that failed below that has nothing to quote. */ +static const char *sysbus_errstr(link_client_t *c) +{ + const link_reply_t *r = link_client_reply(c); + + return (r && r->error_name) ? r->error_name : "transport or parse failure"; +} + static int sysbus_request_name(link_client_t *c) { - uint32_t result = 0; + const char *reason; + uint32_t result; int rc; rc = link_client_call_v(c, "/org/freedesktop/DBus", @@ -1211,11 +1229,19 @@ static int sysbus_request_name(link_client_t *c) "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; + reason = sysbus_errstr(c); + else if (link_reply_get_u32(link_client_reply(c), &result) < 0) + reason = "malformed RequestName reply"; + else if (result != 1) /* 2/3/4 mean we did not get the name */ + reason = "name already owned"; + else + return 0; + + logit(sysbus_level(), "Failed to claim %s on system bus: %s", + FINIT_BUS_NAME, reason); + sysbus_warned = 1; + + return -1; } static int try_attach_system_bus(uev_ctx_t *ctx) @@ -1232,16 +1258,21 @@ static int try_attach_system_bus(uev_ctx_t *ctx) return -1; } + /* Unlike the local bus, a broker routes by destination, and it + * drops anything not addressed to the driver before Hello. */ + link_client_set_destination(c, "org.freedesktop.DBus"); + 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); + logit(sysbus_level(), "System-bus Hello failed: %s", + sysbus_errstr(c)); + sysbus_warned = 1; link_client_close(c); return -1; } if (sysbus_request_name(c) < 0) { - logit(LOG_WARNING, "Failed to claim %s on system bus", FINIT_BUS_NAME); link_client_close(c); return -1; } @@ -1260,6 +1291,7 @@ static int try_attach_system_bus(uev_ctx_t *ctx) } sysbus_peer = p; + sysbus_warned = 0; /* arm the warning for a later broker restart */ logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); return 0; } From abab0e0fd4f25d5ee6686e6b39177c4c3fabdf8e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 7 Aug 2026 13:53:18 +0200 Subject: [PATCH 07/22] test: depserv waits for the transition to settle The last assertion sampled bar's state one step after asking Finit to stop foo, but bar passes through stopped on its way to waiting, so the test failed roughly one run in eight. Signed-off-by: Joachim Wiberg --- test/depserv.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/depserv.sh b/test/depserv.sh index 0d0eaa1d..396afae2 100755 --- a/test/depserv.sh +++ b/test/depserv.sh @@ -66,7 +66,9 @@ test_one() run "initctl stop foo" run "initctl status" run "initctl status bar" - assert_status "bar" "waiting" + # bar passes through stopped on its way to waiting, so this has to + # wait for the end of the transition rather than sample it once + retry 'assert_status "bar" "waiting"' } run "initctl debug" From c71ccce7424440ec0b5cfc8bb6c90422a810bf8f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 7 Aug 2026 13:54:56 +0200 Subject: [PATCH 08/22] libink: a broker peer is not an ordinary client libink was written peer to peer, where one connection is one client and one principal. Attaching to a message bus breaks both halves of that, and two things followed from it. Signals never reached the system bus. Fan-out is gated on the peer having sent AddMatch, but a broker subscribes for its own clients and never sends us one, so every ServiceStateChanged was dropped on the floor. A connection attached with LINK_ATTACH_BROKER gets them all. Hello, AddMatch and RemoveMatch write per-connection state. Shared by every caller behind a broker, that lets one sender exhaust the match cap or drop another's rule, so we leave all three to the bus, whose job they are. Signed-off-by: Joachim Wiberg --- libink/builtin.c | 14 ++++++++++++++ libink/dispatch.c | 10 ++++++---- libink/internal.h | 4 +++- libink/link.h | 12 ++++++++++-- libink/server.c | 4 +++- src/dbus.c | 3 ++- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/libink/builtin.c b/libink/builtin.c index f1d5dd23..e3e7bae7 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -459,6 +459,20 @@ static int handle_remove_match(link_connection_t *conn, const struct link_msg *m int __handle_builtin(link_connection_t *conn, const struct link_msg *m) { + /* Hello and AddMatch/RemoveMatch are per-connection state, and on + * a broker link the connection is shared by every caller: one + * sender could exhaust the match cap or drop another's rule. + * Naming and subscription belong to the broker for its own + * clients, so we do not answer these there. */ + if (conn->broker && m->member && + (!strcmp(m->member, "Hello") || + !strcmp(m->member, "AddMatch") || + !strcmp(m->member, "RemoveMatch"))) { + return __send_error(conn, m, + "org.freedesktop.DBus.Error.AccessDenied", + "Handled by the message bus, not by this peer"); + } + if (member_is(m, "org.freedesktop.DBus", "Hello") && m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) return handle_hello(conn, m); diff --git a/libink/dispatch.c b/libink/dispatch.c index 2899d8ff..1bfb59ef 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -187,12 +187,14 @@ int link_connection_emit_signal(link_connection_t *conn, if (conn->auth != LINK_AUTH_DONE) return 0; /* peer hasn't finished the SASL phase */ - for (i = 0; i < conn->matches_count; i++) { + /* A broker routes to whoever subscribed with it, so it wants + * every signal and never sends us AddMatch of its own. */ + matched = conn->broker; + + for (i = 0; !matched && i < conn->matches_count; i++) { if (__match_matches(conn->matches[i], path, - interface, member)) { + interface, member)) matched = 1; - break; - } } if (!matched) return 0; /* peer didn't subscribe — nothing to do */ diff --git a/libink/internal.h b/libink/internal.h index 7528db5c..b48c3e51 100644 --- a/libink/internal.h +++ b/libink/internal.h @@ -91,9 +91,11 @@ struct link_connection { /* Match rules registered via org.freedesktop.DBus.AddMatch. * Bounded for PID 1 hygiene; a peer that exceeds the cap gets - * a LimitsExceeded error reply. */ + * a LimitsExceeded error reply. A broker never registers any, + * it matches for its own clients, so `broker` bypasses them. */ struct link_match *matches[LINK_MATCH_PEER_CAP]; size_t matches_count; + int broker; uint8_t rxbuf[LINK_RX_BUF_SIZE]; size_t rxlen; diff --git a/libink/link.h b/libink/link.h index 5f1988a8..ba4f2fad 100644 --- a/libink/link.h +++ b/libink/link.h @@ -105,8 +105,16 @@ int link_server_accept(link_server_t *server, link_connection_t **conn); * * 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); + * have to track partial state. + * + * LINK_ATTACH_BROKER says the peer is a message bus rather than an + * ordinary client. A broker subscribes on behalf of its own clients + * and never sends us AddMatch, so signals go to it unconditionally + * instead of being filtered by this connection's match rules. */ +#define LINK_ATTACH_BROKER 0x01 + +link_connection_t *link_server_attach(link_server_t *server, int fd, uid_t peer_uid, + unsigned int attach_flags); int link_connection_get_fd (const link_connection_t *conn); uid_t link_connection_get_uid (const link_connection_t *conn); diff --git a/libink/server.c b/libink/server.c index f5a82431..19d1fa3c 100644 --- a/libink/server.c +++ b/libink/server.c @@ -162,7 +162,8 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) return 0; } -link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid) +link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid, + unsigned int attach_flags) { link_connection_t *conn; int flags; @@ -194,6 +195,7 @@ link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid conn->auth = LINK_AUTH_DONE; /* caller already handshook */ conn->server = srv; conn->peer_uid = peer_uid; + conn->broker = !!(attach_flags & LINK_ATTACH_BROKER); __auth_generate_guid(conn->guid); return conn; diff --git a/src/dbus.c b/src/dbus.c index 945b881e..5bc14618 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -1280,7 +1280,8 @@ static int try_attach_system_bus(uev_ctx_t *ctx) /* 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); + conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1, + LINK_ATTACH_BROKER); if (!conn) return -1; From d710a235130fe5fd009ba887166be1d6315e0efc Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 7 Aug 2026 13:55:41 +0200 Subject: [PATCH 09/22] dbus: gate the bus socket like INIT_SOCKET The D-Bus socket was bound world read/write, on the reasoning that SO_PEERCRED authorizes each method anyway. That leaves the read-only surface open to every local user, and it quietly ignores --with-group: a system that restricts initctl to the wheel group still handed the same service state to anyone who asked over the bus. Bind it 0660 and chown it to the configured group, the same gate the fallback socket has always had. libink takes the mode as an argument rather than assuming one, since who may connect is the embedder's policy, not the library's. The mode is applied at bind(), so there is no window where the socket is more permissive than intended. Signed-off-by: Joachim Wiberg --- configure.ac | 4 ++-- doc/dbus.md | 6 ++++++ libink/link.h | 6 +++++- libink/server.c | 9 +++++---- src/dbus.c | 8 +++++++- test/dbus-auth.sh | 10 ++++++++-- 6 files changed, 33 insertions(+), 10 deletions(-) diff --git a/configure.ac b/configure.ac index e13316b7..33074b59 100644 --- a/configure.ac +++ b/configure.ac @@ -161,7 +161,7 @@ AC_ARG_WITH(sysconfig, [sysconfig=$withval], [sysconfig=default]) AC_ARG_WITH(group, - AS_HELP_STRING([--with-group=NAME], [Group for /run/finit/socket (initctl), default: root]), + AS_HELP_STRING([--with-group=NAME], [Group for /run/finit/socket and /run/finit/bus, default: root]), [group=$withval], [group=root]) AC_ARG_WITH(hostname, @@ -298,7 +298,7 @@ AS_IF([test "x$with_heading" != "xno"], [ AS_IF([test "x$with_group" != "xno"], [ AS_IF([test "x$group" = "xyes"], [ group=root])]) -AC_DEFINE_UNQUOTED(DEFGROUP, "$group", [For /run/finit/socket]) +AC_DEFINE_UNQUOTED(DEFGROUP, "$group", [For /run/finit/socket and /run/finit/bus]) AS_IF([test "x$with_hostname" != "xno"], [ AS_IF([test "x$hostname" = "xyes"], [ diff --git a/doc/dbus.md b/doc/dbus.md index 2c89a842..e7453118 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -27,6 +27,12 @@ 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 socket is `0660`, owned by `root` and the group given to +`--with-group` at build time, the same gate as `/run/finit/socket` that +`initctl` falls back on. The bus reaches every operation `initctl` +does, so restricting one and not the other would leave the door open. +Members of that group may use it, see [Authorization](#authorization). + The **system** bus is best-effort: Finit probes for a running `dbus-daemon` and, when reachable, claims the well-known name `org.finit` so that standard tooling sees Finit just like any other system service: diff --git a/libink/link.h b/libink/link.h index ba4f2fad..5a16e778 100644 --- a/libink/link.h +++ b/libink/link.h @@ -90,7 +90,11 @@ typedef struct { /* ---------- server / connection lifecycle ---------- */ -int link_server_new (link_server_t **server, const char *path); +/* Bind a listening socket at `path` with file mode `mode`, e.g. 0660 + * to keep it to root and one group. The mode is applied at bind(), + * so the socket is never briefly more permissive than asked; setting + * the owning group afterwards is the caller's job. */ +int link_server_new (link_server_t **server, const char *path, mode_t mode); void link_server_free (link_server_t *server); int link_server_get_fd(const link_server_t *server); diff --git a/libink/server.c b/libink/server.c index 19d1fa3c..dc4b9bff 100644 --- a/libink/server.c +++ b/libink/server.c @@ -22,7 +22,7 @@ static void close_save_errno(int fd) errno = saved; } -int link_server_new(link_server_t **out, const char *path) +int link_server_new(link_server_t **out, const char *path, mode_t mode) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; link_server_t *srv; @@ -55,12 +55,13 @@ int link_server_new(link_server_t **out, const char *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; + * umask around the bind() so the socket appears with the mode the + * caller asked for atomically, with no window where it is more + * permissive. Who may connect is the caller's policy to set; * per-method authorization happens later in dispatch via * SO_PEERCRED. */ { - mode_t oldmask = umask(0111); + mode_t oldmask = umask(0777 & ~mode); int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun)); int saved = errno; diff --git a/src/dbus.c b/src/dbus.c index 5bc14618..9f343c8c 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -1341,11 +1341,17 @@ 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) { + /* Same access policy as INIT_SOCKET: the bus reaches every + * service operation initctl does, so --with-group has to gate + * both or it gates neither. */ + if (link_server_new(&server, FINIT_BUS_SOCKET, 0660) < 0) { err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET); return 1; } + if (chown(FINIT_BUS_SOCKET, geteuid(), getgroup(DEFGROUP))) + err(1, "Failed setting group %s on %s", DEFGROUP, FINIT_BUS_SOCKET); + if (link_server_add_object(server, "/org/finit/manager", &manager_vtable, NULL) < 0) { err(1, "Failed registering Manager1 object"); diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 1e9535d3..ce12bd48 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -14,9 +14,15 @@ TEST_DIR=$(dirname "$0") # shellcheck source=/dev/null . "$TEST_DIR/lib/dbus-setup.sh" -say "Socket mode is 0666" +# The bus reaches every service operation initctl does, so it has to +# be gated like INIT_SOCKET: 0660, owned by root and the --with-group +# group. Only the mode is asserted here, the test namespace does not +# enforce it -- a setuid() client still connects to a 0660 socket. +say "Socket is gated like INIT_SOCKET, not world-accessible" mode=$(texec stat -c %a "$BUS") -assert "Socket mode is 666 (got $mode)" "$mode" = "666" +sock=$(texec stat -c %a /run/finit/socket) +assert "Socket mode is 660 (got $mode)" "$mode" = "660" +assert "Bus and INIT_SOCKET agree ($mode vs $sock)" "$mode" = "$sock" say "AUTH EXTERNAL: claim correct UID (root = 0)" reply=$(texec "$CLIENT" auth "$BUS" 0) From 63aabaa6df5afad0eb3d50417d7f4402de103475 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 10:15:06 +0200 Subject: [PATCH 10/22] libink/dbus: identify the caller behind a broker On the local bus SO_PEERCRED says who is calling and the kernel is the one saying it. Behind a broker one connection carries every caller, so that credential describes dbus-daemon and nothing else, and every privileged method was refused there, root included. Ask the bus driver instead. libink parks the call and hands us the sender; we ask GetConnectionUnixUser and answer when the reply lands, through the same event loop as everything else. Nothing blocks: blocking in PID 1 is why libuEv exists. That needs calls libink can make on a connection it already has, so it gained those too. Answers are cached, since a bus never reuses a unique name while it runs. Not across a restart though: a new dbus-daemon numbers from scratch and :1.7 becomes somebody else, so the cache goes when the broker does. A sender name too long to key on is refused rather than truncated, two callers sharing a truncated key would share an identity. Privilege is no longer uid 0 alone. The socket is already owned by the --with-group group, so refusing its members every method that changes anything left a wheel user able to open the bus and unable to reboot. Both gates now say the same thing. Group membership needs NSS, which the C library loads with dlopen(), so the lookup is compiled out where Finit is built to link statically. That leaves such a build root-only, which is worth saying out loud rather than leaving to be discovered. Signed-off-by: Joachim Wiberg --- doc/build.md | 3 +- doc/dbus.md | 39 +++++-- libink/client.c | 56 +--------- libink/connection.c | 72 ++++++++++++- libink/dispatch.c | 249 ++++++++++++++++++++++++++++++++++++++++--- libink/internal.h | 58 +++++++++- libink/link.h | 61 +++++++++++ libink/marshal.c | 31 ++++++ libink/marshal.h | 8 ++ libink/proto.c | 12 +++ libink/proto.h | 5 + libink/server.c | 18 ++++ src/dbus.c | 196 ++++++++++++++++++++++++++++++++-- test/Makefile.am | 2 + test/dbus-authz.sh | 52 +++++++++ test/skel/etc/group | 3 +- test/skel/etc/passwd | 1 + 17 files changed, 775 insertions(+), 91 deletions(-) create mode 100755 test/dbus-authz.sh diff --git a/doc/build.md b/doc/build.md index 298437b6..5deb0ec8 100644 --- a/doc/build.md +++ b/doc/build.md @@ -44,7 +44,8 @@ Below are a few of the main switches to configure: * `--enable-static`: Build Finit statically. The plugins will be built-ins (.o files) and all external libraries, except the C library - will be linked statically. + will be linked statically. Privileged D-Bus methods then accept only + `root`, see [Authorization](dbus.md#authorization) * `--enable-kernel-cmdline`: Enable Finit pre-4.1 parsing of init args from `/proc/cmdline`, this is *not recommended* since Finit may be running as the diff --git a/doc/dbus.md b/doc/dbus.md index e7453118..15118946 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -204,20 +204,37 @@ conditions belong to Finit's state machine. 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. +Privileged methods accept `root`, and any caller belonging to the group given +to `--with-group` at build time. That is the same set the socket mode already +admits, so the two gates agree instead of the socket letting the group in and +every method turning it away. + +A build configured with `--enable-static` accepts only `root`. Group +membership comes from NSS, which the C library loads with `dlopen()`, and a +build meant to link statically cannot count on that, so the lookup is compiled +out rather than left to fail open. The socket mode is unchanged, so the group +can still connect, it just cannot invoke a privileged method. + +On the **local** bus the kernel decides this at `connect()`, supplementary +groups included, and `SO_PEERCRED` tells Finit exactly who is calling, so +privilege escalation through the bus is impossible. + +On the **system** bus one connection carries every caller, so `SO_PEERCRED` +describes `dbus-daemon` rather than whoever asked. For a privileged method +Finit asks the bus driver `GetConnectionUnixUser` about the message sender and +holds the call until the answer arrives. Nothing blocks: the reply comes back +through the same event loop as everything else, and the held call is then +dispatched or refused on its merits. + +Answers are cached per sender. A bus never reuses a unique name while it +runs, so an answer holds for as long as that bus does; Finit empties the cache +when the broker goes away, since a new one numbers its clients from scratch. +A caller Finit cannot identify is refused, so the failure mode is a denial +rather than an escalation. 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"`). +string. `initctl` integration --------------------- diff --git a/libink/client.c b/libink/client.c index 15a1760d..6a0c60b4 100644 --- a/libink/client.c +++ b/libink/client.c @@ -171,15 +171,8 @@ static int read_one(link_client_t *c, struct link_msg *msg) 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; + __msg_to_reply(&c->reply, m); + c->have_reply = 1; } /* The reply view in c->reply points into c->rxbuf and is invalidated @@ -261,12 +254,7 @@ int link_client_call(link_client_t *c, 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]; + uint8_t hdr[LINK_CALL_HDR_MAX]; ssize_t hlen; uint32_t serial; @@ -328,54 +316,20 @@ int link_reply_get_u32(const link_reply_t *r, uint32_t *out) 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]; + uint8_t body[LINK_CALL_BODY_MAX]; ssize_t body_len = 0; if (signature && *signature) { va_list ap; va_start(ap, signature); - body_len = marshal_va(body, sizeof(body), signature, ap); + body_len = __marshal_va(body, sizeof(body), signature, ap); va_end(ap); if (body_len < 0) return LINK_CALL_FAIL; diff --git a/libink/connection.c b/libink/connection.c index f88afda8..3fe799bc 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -21,6 +22,70 @@ uid_t link_connection_get_uid(const link_connection_t *conn) return conn ? conn->peer_uid : (uid_t)-1; } +/* Issue a method call and remember the serial so the reply can be + * handed back to `cb` when the read loop picks it up. Nothing here + * waits: this is the counterpart of link_client_call() for a + * connection already owned by the event loop. */ +int link_connection_call(link_connection_t *conn, const char *destination, + const char *path, const char *interface, const char *member, + link_reply_cb_t cb, void *userdata, + const char *signature, ...) +{ + uint8_t body[LINK_CALL_BODY_MAX]; + uint8_t hdr[LINK_CALL_HDR_MAX]; + ssize_t blen = 0; + ssize_t hlen; + uint32_t serial; + int i; + + if (!conn || conn->fd < 0 || !path || !member) { + errno = EINVAL; + return -1; + } + + for (i = 0; i < LINK_PENDING_CAP; i++) { + if (!conn->pending[i].used) + break; + } + if (i == LINK_PENDING_CAP) { + errno = EBUSY; + return -1; + } + + if (signature && *signature) { + va_list ap; + + va_start(ap, signature); + blen = __marshal_va(body, sizeof(body), signature, ap); + va_end(ap); + if (blen < 0) { + errno = EMSGSIZE; + return -1; + } + } + + serial = ++conn->next_serial; + hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, path, interface, + member, destination, signature, (uint32_t)blen); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (__io_write_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (blen > 0 && __io_write_all(conn->fd, body, (size_t)blen) < 0) + return -1; + + + conn->pending[i].used = 1; + conn->pending[i].serial = serial; + conn->pending[i].cb = cb; + conn->pending[i].userdata = userdata; + + return 0; +} + void link_connection_close(link_connection_t *conn) { size_t i; @@ -28,6 +93,11 @@ void link_connection_close(link_connection_t *conn) if (!conn) return; + + /* Anything waiting on this connection has to be told, or a parked + * call sits forever and its caller never hears back. */ + __dispatch_forget_conn(conn); + for (i = 0; i < conn->matches_count; i++) __match_free(conn->matches[i]); @@ -52,7 +122,7 @@ static int process_binary(link_connection_t *conn) if (consumed < 0) return -1; - if (__dispatch_message(conn, &msg) < 0) + if (__dispatch_message(conn, &msg, (size_t)consumed) < 0) return -1; memmove(conn->rxbuf, conn->rxbuf + consumed, diff --git a/libink/dispatch.c b/libink/dispatch.c index 1bfb59ef..44642d7b 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -260,7 +260,7 @@ int __send_error(link_connection_t *conn, const struct link_msg *req, 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; } +uid_t link_call_uid (const link_call_t *c) { return c ? c->uid : LINK_UID_UNKNOWN; } link_writer_t *link_call_reply(link_call_t *call) { @@ -324,20 +324,213 @@ 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) +/* ---------- replies to our own outbound calls ---------- */ + +/* Hand a reply to whoever issued the matching link_connection_call(). + * Unmatched replies are dropped: a broker is free to send us things we + * never asked for, and that is not a reason to drop the connection. */ +static void deliver_reply(link_connection_t *conn, const struct link_msg *m) +{ + link_reply_cb_t cb; + link_reply_t r; + void *userdata; + int i; + + for (i = 0; i < LINK_PENDING_CAP; i++) { + if (conn->pending[i].used && conn->pending[i].serial == m->reply_serial) + break; + } + if (i == LINK_PENDING_CAP) { + return; + } + + cb = conn->pending[i].cb; + userdata = conn->pending[i].userdata; + conn->pending[i].used = 0; + + if (!cb) + return; + + __msg_to_reply(&r, m); + cb(conn, &r, userdata); +} + +/* ---------- calls parked while their caller is identified ---------- */ + +/* Every park gets a token that is never issued twice, so a resolver + * answering late, twice, or after its connection went away resumes + * nothing rather than whatever call has since taken the slot. */ +static struct link_parked *park(link_connection_t *conn, const uint8_t *frame, + size_t len, link_authz_t *tok) +{ + link_server_t *srv = conn->server; + int i; + + if (!frame || !len || len > LINK_PARKED_MSG_MAX) + return NULL; + + for (i = 0; i < LINK_PARKED_CAP; i++) { + if (!srv->parked[i].tok) + break; + } + if (i == LINK_PARKED_CAP) + return NULL; + + srv->parked[i].tok = ++srv->next_tok; + srv->parked[i].conn = conn; + srv->parked[i].len = len; + memcpy(srv->parked[i].buf, frame, len); + *tok = srv->parked[i].tok; + + return &srv->parked[i]; +} + +static void unpark(struct link_parked *p) +{ + p->tok = 0; + p->conn = NULL; +} + +void __dispatch_forget_conn(link_connection_t *conn) +{ + link_server_t *srv = conn->server; + int i; + + /* Drop parked calls first. A pending callback below may try to + * resolve one, and resuming a dispatch on a connection that is + * being torn down is no use to anyone; an invalidated slot makes + * that resolve a no-op instead. */ + if (srv) { + for (i = 0; i < LINK_PARKED_CAP; i++) { + if (srv->parked[i].conn == conn) + unpark(&srv->parked[i]); + } + } + + for (i = 0; i < LINK_PENDING_CAP; i++) { + if (conn->pending[i].used && conn->pending[i].cb) + conn->pending[i].cb(conn, NULL, conn->pending[i].userdata); + conn->pending[i].used = 0; + } +} + +static int dispatch_call(link_connection_t *conn, const struct link_msg *m, + const uint8_t *frame, size_t framelen, + const uid_t *known_uid); + +void link_uid_resolved(link_server_t *server, link_authz_t tok, uid_t uid) +{ + uint8_t buf[LINK_PARKED_MSG_MAX]; + struct link_parked *p = NULL; + link_connection_t *conn; + struct link_msg msg; + size_t len; + int i; + + if (!server || !tok) + return; + + for (i = 0; i < LINK_PARKED_CAP; i++) { + if (server->parked[i].tok == tok) { + p = &server->parked[i]; + break; + } + } + if (!p) + return; /* stale handle, already answered */ + + /* Copy the message out and free the slot before dispatching: + * the handler may park a call of its own. */ + conn = p->conn; + len = p->len; + memcpy(buf, p->buf, len); + unpark(p); + + if (!conn || __msg_parse(buf, len, &msg) <= 0) + return; + + (void)dispatch_call(conn, &msg, NULL, 0, &uid); +} + +int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen) +{ + if (m->type == LINK_MSG_METHOD_RETURN || m->type == LINK_MSG_ERROR) { + deliver_reply(conn, m); + return 0; + } + + if (m->type != LINK_MSG_METHOD_CALL) { + /* Signals from a client to PID 1 are nonsense; drop. */ + return 0; + } + + return dispatch_call(conn, m, conn->rxbuf, framelen, NULL); +} + +/* Who may invoke a privileged method. Without an authorizer, only + * root, which is what libink can decide on its own. */ +static int caller_may(link_server_t *srv, uid_t uid) +{ + if (uid == LINK_UID_UNKNOWN) + return 0; + if (srv && srv->authorizer) + return srv->authorizer(uid, srv->authz_userdata); + + return uid == 0; +} + +/* Ask who is calling on a broker connection, where the message is the + * only evidence. Returns 0 with *uid set, 1 when the call was parked + * and will be dispatched again once the resolver answers, -1 when the + * caller cannot be identified, and -2 when we have no room to ask. */ +#define CALLER_UID_BUSY (-2) + +static int resolve_caller(link_connection_t *conn, const struct link_msg *m, + const uint8_t *frame, size_t framelen, uid_t *uid) +{ + link_server_t *srv = conn->server; + struct link_parked *p; + link_authz_t tok; + int rc; + + if (!srv || !srv->uid_resolver || !m->sender) + return -1; + + /* Enforce the rule link.h states, rather than trusting every + * resolver to remember it: a truncated sender key would let two + * callers share one identity. */ + if (strlen(m->sender) >= LINK_SENDER_MAX) { + return -1; + } + + /* Park first so the resolver has somewhere to answer, then let + * it release the slot immediately if it already knew. */ + p = park(conn, frame, framelen, &tok); + if (!p) + return CALLER_UID_BUSY; + + rc = srv->uid_resolver(conn, m->sender, tok, uid, srv->uid_userdata); + if (rc != 1) + unpark(p); + + return rc; +} + +static int dispatch_call(link_connection_t *conn, const struct link_msg *m, + const uint8_t *frame, size_t framelen, + const uid_t *known_uid) { struct link_object *o; struct link_vtable_entry *e = NULL; const link_method_t *meth; struct link_call call; ssize_t blen; + uid_t call_uid; int rc; - if (m->type != LINK_MSG_METHOD_CALL) { - /* Signals and replies from a client to PID 1 are nonsense; - * silently drop. */ - return 0; - } + /* What a handler sees via link_call_uid(). Unresolved on a broker + * connection until a privileged method forces the question. */ + call_uid = known_uid ? *known_uid : conn->peer_uid; if (!m->path || !m->member) { return __send_error(conn, m, @@ -345,6 +538,7 @@ int __dispatch_message(link_connection_t *conn, const struct link_msg *m) "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 @@ -374,24 +568,47 @@ int __dispatch_message(link_connection_t *conn, const struct link_msg *m) const char *got = m->signature ? m->signature : ""; const char *want = meth->in_sig ? meth->in_sig : ""; - if (strcmp(got, want) != 0) + 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"); + /* Per-method authorization. PRIVILEGED methods require uid 0. + * On an ordinary connection the peer's uid was captured via + * SO_PEERCRED at accept time and verified against the AUTH + * EXTERNAL claim, so conn->peer_uid is the answer. A broker + * connection carries every caller at once, so who is asking has + * to be established per message, which may park the call. */ + if (meth->flags & LINK_METHOD_PRIVILEGED) { + if (conn->broker && !known_uid) { + rc = resolve_caller(conn, m, frame, framelen, &call_uid); + if (rc == 1) + return 0; /* parked, resumed later */ + + if (rc == CALLER_UID_BUSY) { + /* Not a permission problem: root may well + * be asking, we just have no slot to find + * out in. Say so, it is retryable. */ + return __send_error(conn, m, + "org.freedesktop.DBus.Error.LimitsExceeded", + "Too many calls awaiting authorization"); + } + if (rc < 0) + call_uid = (uid_t)-1; + } + + if (!caller_may(conn->server, call_uid)) { + return __send_error(conn, m, + "org.freedesktop.DBus.Error.AccessDenied", + "Caller is not privileged for this method"); + } } memset(&call, 0, sizeof(call)); call.conn = conn; + call.uid = call_uid; call.incoming = *m; __r_init(&call.read_cursor, m->body, m->body_avail); diff --git a/libink/internal.h b/libink/internal.h index b48c3e51..5f0f778a 100644 --- a/libink/internal.h +++ b/libink/internal.h @@ -24,9 +24,22 @@ typedef enum { #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_UNIQUE_NAME_LEN LINK_SENDER_MAX #define LINK_MATCH_RULE_MAX 256 /* per-peer match rule cap */ #define LINK_MATCH_PEER_CAP 16 /* max active match rules per peer */ +#define LINK_PENDING_CAP 4 /* outbound calls awaiting a reply */ +/* Staging for an outgoing method call. Generous on purpose: headers + * for the calls libink makes run to ~150 B, and both the synchronous + * and the connection-side path build into these, so one answer rather + * than a number per call site. */ +#define LINK_CALL_HDR_MAX 1024 +#define LINK_CALL_BODY_MAX 1024 +#define LINK_PARKED_CAP 4 /* inbound calls awaiting a uid */ +/* A call parked for authorization is a privileged one: an object path + * and at most a service name. Finit's per-service paths alone run to + * 512 bytes, so leave room for the header around one. Anything that + * does not fit is denied rather than held. */ +#define LINK_PARKED_MSG_MAX 1024 /* Per-vtable record attached to an object's interface list. */ struct link_vtable_entry { @@ -46,19 +59,44 @@ struct link_object { TAILQ_HEAD(link_object_list, link_object); +/* An inbound method call held while we find out who sent it. The + * message is copied because rxbuf is reused as soon as we return to + * the read loop. `tok` is the handle the resolver answers with, and + * zero when the slot is free. */ +struct link_parked { + link_authz_t tok; + link_connection_t *conn; + size_t len; + uint8_t buf[LINK_PARKED_MSG_MAX]; +}; + struct link_server { int fd; char path[LINK_PATH_MAX]; struct link_object_list objects; uint32_t next_unique_id; /* for ":1.N" names */ + + /* Set by link_server_set_uid_resolver(); see link.h. */ + link_uid_resolver_t uid_resolver; + void *uid_userdata; + + /* Set by link_server_set_authorizer(); see link.h. */ + link_authorizer_t authorizer; + void *authz_userdata; + struct link_parked parked[LINK_PARKED_CAP]; + link_authz_t next_tok; }; /* 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. */ + * connection's txbuf is safe because a reply is marshalled and sent + * without yielding. Note that parking means several calls can be in + * flight on one connection: what is held is the request, and + * link_uid_resolved() resumes from a copy, so txbuf is still only + * ever used by one reply at a time. An async handler that returned + * before writing its reply would break that. */ struct link_call { link_connection_t *conn; struct link_msg incoming; @@ -66,6 +104,7 @@ struct link_call { struct link_writer reply_writer; /* writes into conn->txbuf */ int reply_consumed; int error_sent; + uid_t uid; /* caller, resolved for a broker peer */ }; /* A parsed AddMatch rule. Fields are NULL when the rule omits the @@ -108,6 +147,16 @@ struct link_connection { uint32_t next_serial; + /* Outbound calls we made on this connection, awaiting replies. + * Only a broker connection uses these today, to ask the bus + * driver who a sender is. */ + struct { + int used; + uint32_t serial; + link_reply_cb_t cb; + void *userdata; + } pending[LINK_PENDING_CAP]; + struct link_server *server; /* back-pointer for dispatch */ }; @@ -121,7 +170,8 @@ 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 __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen); +void __dispatch_forget_conn(link_connection_t *conn); 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, diff --git a/libink/link.h b/libink/link.h index 5a16e778..afdd5d30 100644 --- a/libink/link.h +++ b/libink/link.h @@ -88,6 +88,67 @@ typedef struct { size_t body_len; } link_reply_t; +/* ---------- caller identity on a broker connection ---------- */ + +/* Handle for a call parked while its caller is identified. Opaque, + * and safe to hold: it encodes a slot and a generation, so resolving + * a stale handle is a no-op rather than a use-after-free. */ +typedef uint64_t link_authz_t; + +/* "Nobody asked yet", distinct from any real uid. link_call_uid() + * returns this on a broker connection until something forces the + * question, which today only a LINK_METHOD_PRIVILEGED method does. */ +#define LINK_UID_UNKNOWN ((uid_t)-1) + +/* A broker sets SENDER to a unique name, ":1.", so this is very + * generous. Callers that key anything on a sender must reject longer + * names rather than truncate: two senders sharing a truncated key + * would share an identity. */ +#define LINK_SENDER_MAX 64 + +/* Answer "which uid is `sender`?" for a privileged call arriving on a + * broker connection, where SO_PEERCRED describes the broker and not + * the caller. + * + * Return 0 with *uid set when the answer is already known, 1 to answer + * later by calling link_uid_resolved() with `tok`, or -1 when it + * cannot be determined, which fails the call closed. Returning 1 + * without ever calling link_uid_resolved() leaks the slot and leaves + * the caller without a reply, so always answer. */ +typedef int (*link_uid_resolver_t)(link_connection_t *conn, const char *sender, + link_authz_t tok, uid_t *uid, void *userdata); + +void link_server_set_uid_resolver(link_server_t *server, link_uid_resolver_t cb, + void *userdata); + +/* May `uid` invoke a LINK_METHOD_PRIVILEGED method? Return non-zero + * to allow. Who counts as privileged is the embedder's policy, not + * the library's; with no authorizer installed only uid 0 may. */ +typedef int (*link_authorizer_t)(uid_t uid, void *userdata); + +void link_server_set_authorizer(link_server_t *server, link_authorizer_t cb, + void *userdata); + +/* Complete a deferred resolve and resume the parked call. Pass + * (uid_t)-1 to say the caller could not be identified, which denies + * it. Resolving a handle twice, or one whose connection has since + * closed, does nothing. */ +void link_uid_resolved(link_server_t *server, link_authz_t tok, uid_t uid); + +/* Called with the reply to an outbound link_connection_call(). `reply` + * is NULL if the connection dropped before one arrived. */ +typedef void (*link_reply_cb_t)(link_connection_t *conn, const link_reply_t *reply, + void *userdata); + +/* Issue a method call on an established connection and invoke `cb` + * when the reply lands. Unlike link_client_call() this never blocks: + * the reply is picked up by the normal read loop. Argument marshalling + * matches link_client_call_v(). */ +int link_connection_call(link_connection_t *conn, const char *destination, + const char *path, const char *interface, const char *member, + link_reply_cb_t cb, void *userdata, + const char *signature, ...); + /* ---------- server / connection lifecycle ---------- */ /* Bind a listening socket at `path` with file mode `mode`, e.g. 0660 diff --git a/libink/marshal.c b/libink/marshal.c index 48f00363..ecc41166 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -376,3 +376,34 @@ int __r_array_begin(struct link_reader *r, size_t *out_end) *out_end = end; return 0; } + +ssize_t __marshal_va(uint8_t *body, size_t cap, const char *sig, va_list ap) +{ + struct link_writer w; + const char *s; + + __w_init(&w, body, cap); + for (s = sig; *s; s++) { + switch (*s) { + case 'y': + __w_byte(&w, (uint8_t)va_arg(ap, int)); + break; + case 'b': + __w_bool(&w, va_arg(ap, int)); + break; + case 'u': + __w_u32(&w, va_arg(ap, uint32_t)); + break; + case 's': + __w_string(&w, va_arg(ap, const char *)); + break; + case 'o': + __w_path(&w, va_arg(ap, const char *)); + break; + default: + return -1; + } + } + + return __w_finish(&w); +} diff --git a/libink/marshal.h b/libink/marshal.h index 3246d3e7..0a210989 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -6,8 +6,10 @@ #ifndef LIBINK_MARSHAL_H_ #define LIBINK_MARSHAL_H_ +#include #include #include +#include /* struct link_writer is defined in ink.h (public). Field layout is * "opaque" per the public contract; this file's helpers manipulate @@ -52,4 +54,10 @@ 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); +/* Marshal varargs into `body` (capacity `cap`) according to `sig`. + * Returns the marshalled length, or -1 on overflow or an unsupported + * type code. Shared by the synchronous client and the asynchronous + * connection-side call. */ +ssize_t __marshal_va(uint8_t *body, size_t cap, const char *sig, va_list ap); + #endif /* LIBINK_MARSHAL_H_ */ diff --git a/libink/proto.c b/libink/proto.c index 1d7f27c6..96e96cf5 100644 --- a/libink/proto.c +++ b/libink/proto.c @@ -389,3 +389,15 @@ size_t __msg_header_size(const struct link_msg *m) /* Generous upper bound used by callers to size send buffers. */ return 512; } + +void __msg_to_reply(link_reply_t *r, const struct link_msg *m) +{ + r->type = m->type; + r->signature = m->signature; + r->error_name = m->error_name; + r->path = m->path; + r->interface = m->interface; + r->member = m->member; + r->body = m->body_avail ? m->body : NULL; + r->body_len = m->body_avail; +} diff --git a/libink/proto.h b/libink/proto.h index ef28f4b7..0ea345b1 100644 --- a/libink/proto.h +++ b/libink/proto.h @@ -60,6 +60,11 @@ struct link_msg { * needed, -1 on malformed input. */ ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out); +/* Project a parsed message onto the public reply view. Shared by the + * synchronous client and the connection-side reply routing so the two + * cannot drift as link_reply_t grows. */ +void __msg_to_reply(link_reply_t *r, const struct link_msg *m); + /* 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); diff --git a/libink/server.c b/libink/server.c index dc4b9bff..cb1cd790 100644 --- a/libink/server.c +++ b/libink/server.c @@ -159,10 +159,27 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) __auth_generate_guid(conn->guid); + *out = conn; return 0; } +void link_server_set_uid_resolver(link_server_t *srv, link_uid_resolver_t cb, void *userdata) +{ + if (!srv) + return; + srv->uid_resolver = cb; + srv->uid_userdata = userdata; +} + +void link_server_set_authorizer(link_server_t *srv, link_authorizer_t cb, void *userdata) +{ + if (!srv) + return; + srv->authorizer = cb; + srv->authz_userdata = userdata; +} + link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid, unsigned int attach_flags) { @@ -199,6 +216,7 @@ link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid conn->broker = !!(attach_flags & LINK_ATTACH_BROKER); __auth_generate_guid(conn->guid); + return conn; err_close: diff --git a/src/dbus.c b/src/dbus.c index 9f343c8c..c42cf0e5 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -31,6 +31,9 @@ #ifdef HAVE_DBUS #include +#include +#include +#include #include #include #include @@ -68,6 +71,7 @@ static size_t peer_count; static struct peer *sysbus_peer; static void sysbus_probe(void); +static void sender_cache_flush(void); static void peer_drop(struct peer *p) { @@ -79,9 +83,13 @@ static void peer_drop(struct peer *p) peer_count--; free(p); - /* broker gone; the notify paths probe for its return */ - if (was_sysbus) + /* broker gone; the notify paths probe for its return. Its unique + * names die with it, so nothing we learned about them is safe to + * carry over to whatever takes its place. */ + if (was_sysbus) { sysbus_peer = NULL; + sender_cache_flush(); + } } static void peer_cb(uev_t *w, void *arg, int events) @@ -1171,6 +1179,180 @@ void dbus_notify_condition_change(const char *name, const char *state) "ConditionChanged", "ss", body, (size_t)blen); } +/* ---------- who may change things ---------- + * + * Root, or a member of the group the bus socket is owned by, which is + * the same set --with-group already lets connect. Both gates then say + * the same thing, rather than the socket admitting the wheel group and + * every method turning it away. + * + * On the local bus the kernel already made this decision at connect(), + * supplementary groups and all, so the lookup only confirms it. The + * system bus has no socket mode to lean on, which is why we ask here + * rather than trusting the connection. + * + * Deliberately uncached: /etc/group changes while Finit runs, and a + * privileged call is an operator action, not a hot path. + */ +static int caller_is_privileged(uid_t uid, void *userdata) +{ + (void)userdata; + + if (uid == 0) + return 1; + + /* Group membership comes from NSS, which the C library loads + * with dlopen(), so a build meant to link statically cannot + * count on it. Compiled out rather than left to fail open: + * root only there, see doc/dbus.md. */ +#ifndef ENABLE_STATIC + { + gid_t groups[NGROUPS_MAX]; + int ngroups = NGROUPS_MAX; + struct passwd *pw; + int gid, i; + + gid = getgroup(DEFGROUP); + if (gid < 0) + return 0; + + pw = getpwuid(uid); + if (!pw) + return 0; + + if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) < 0) + return 0; + + for (i = 0; i < ngroups; i++) { + if (groups[i] == (gid_t)gid) + return 1; + } + } +#endif + + return 0; +} + +/* ---------- caller identity on the system bus ---------- + * + * libink parks a privileged call and asks us who sent it; we ask the + * bus driver with GetConnectionUnixUser and answer when the reply + * lands, through the same event loop as everything else. + * + * A bus never reuses a unique name, so an answer holds for as long as + * that bus runs. It does not survive the bus restarting, though: + * a new dbus-daemon numbers from scratch and :1.7 becomes somebody + * else, so peer_drop() empties the cache when the broker goes. + * + * It is a ring: the oldest entry loses on overflow, and losing one + * only costs another round trip. + */ +#define SENDER_CACHE_LEN 16 + +struct sender_uid { + char name[LINK_SENDER_MAX]; + uid_t uid; +}; + +static struct sender_uid sender_cache[SENDER_CACHE_LEN]; +static unsigned sender_next; + +static void sender_cache_flush(void) +{ + memset(sender_cache, 0, sizeof(sender_cache)); + sender_next = 0; +} + +static int sender_cached(const char *sender, uid_t *uid) +{ + int i; + + for (i = 0; i < SENDER_CACHE_LEN; i++) { + if (sender_cache[i].name[0] && !strcmp(sender_cache[i].name, sender)) { + *uid = sender_cache[i].uid; + return 1; + } + } + + return 0; +} + +static void sender_remember(const char *sender, uid_t uid) +{ + unsigned i = sender_next++ % SENDER_CACHE_LEN; + + strlcpy(sender_cache[i].name, sender, sizeof(sender_cache[i].name)); + sender_cache[i].uid = uid; +} + +/* One outstanding GetConnectionUnixUser. Freed by the reply callback, + * which libink guarantees to run exactly once, with a NULL reply if + * the connection drops first. */ +struct uid_query { + link_authz_t tok; + char sender[LINK_SENDER_MAX]; +}; + +static void uid_reply_cb(link_connection_t *conn, const link_reply_t *reply, void *userdata) +{ + struct uid_query *q = userdata; + uid_t uid = (uid_t)-1; + uint32_t val; + + (void)conn; + + if (!reply) { + dbg("connection dropped before %s was identified", q->sender); + } else if (reply->type == LINK_MSG_METHOD_RETURN && + link_reply_get_u32(reply, &val) == 0) { + uid = (uid_t)val; + sender_remember(q->sender, uid); + dbg("sender %s is uid %d", q->sender, (int)uid); + } else { + dbg("GetConnectionUnixUser(%s) failed: %s", q->sender, + reply->error_name ? reply->error_name : "unexpected reply"); + } + + link_uid_resolved(server, q->tok, uid); + free(q); +} + +static int sysbus_uid_resolver(link_connection_t *conn, const char *sender, + link_authz_t tok, uid_t *uid, void *userdata) +{ + struct uid_query *q; + + (void)userdata; + + /* Never truncate: a shortened key could match a different + * sender and hand it someone else's privileges. */ + if (strlen(sender) >= LINK_SENDER_MAX) + return -1; + + if (sender_cached(sender, uid)) { + dbg("sender %s is uid %d, from cache", sender, (int)*uid); + return 0; + } + + dbg("asking the bus driver who %s is ...", sender); + + q = calloc(1, sizeof(*q)); + if (!q) + return -1; + q->tok = tok; + strlcpy(q->sender, sender, sizeof(q->sender)); + + if (link_connection_call(conn, "org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "GetConnectionUnixUser", + uid_reply_cb, q, "s", sender) < 0) { + dbg("Failed asking the bus driver about %s: %s", sender, strerror(errno)); + free(q); + return -1; + } + + return 1; /* parked; uid_reply_cb() answers */ +} + /* ---------- system-bus attach (opportunistic) ---------- * * If /var/run/dbus/system_bus_socket is reachable, libink connects to @@ -1179,10 +1361,9 @@ void dbus_notify_condition_change(const char *name, const char *state) * 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. + * peer_uid is (uid_t)-1 because the connection has no single owner; + * who is calling is established per message by sysbus_uid_resolver() + * below. * * A bounded SO_SNDTIMEO/SO_RCVTIMEO budget is applied via * link_client_open_timeout so a hung dbus-daemon can't stall boot; @@ -1292,6 +1473,7 @@ static int try_attach_system_bus(uev_ctx_t *ctx) } sysbus_peer = p; + link_server_set_uid_resolver(server, sysbus_uid_resolver, NULL); sysbus_warned = 0; /* arm the warning for a later broker restart */ logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); return 0; @@ -1352,6 +1534,8 @@ int dbus_init(uev_ctx_t *ctx) if (chown(FINIT_BUS_SOCKET, geteuid(), getgroup(DEFGROUP))) err(1, "Failed setting group %s on %s", DEFGROUP, FINIT_BUS_SOCKET); + link_server_set_authorizer(server, caller_is_privileged, NULL); + if (link_server_add_object(server, "/org/finit/manager", &manager_vtable, NULL) < 0) { err(1, "Failed registering Manager1 object"); diff --git a/test/Makefile.am b/test/Makefile.am index 98723738..a06d9d20 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -72,6 +72,7 @@ EXTRA_DIST += signal-service.sh EXTRA_DIST += testserv.sh EXTRA_DIST += unexpected-restart.sh EXTRA_DIST += dbus-auth.sh +EXTRA_DIST += dbus-authz.sh EXTRA_DIST += dbus-bus.sh EXTRA_DIST += dbus-manager.sh EXTRA_DIST += dbus-service.sh @@ -135,6 +136,7 @@ endif TESTS += unexpected-restart.sh if DBUS TESTS += dbus-auth.sh +TESTS += dbus-authz.sh TESTS += dbus-bus.sh TESTS += dbus-manager.sh TESTS += dbus-service.sh diff --git a/test/dbus-authz.sh b/test/dbus-authz.sh new file mode 100755 index 00000000..66badd54 --- /dev/null +++ b/test/dbus-authz.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Who may invoke a privileged method. Root always, and anyone in the +# group the bus socket is owned by, which is DEFGROUP from +# --with-group, 'root' in a test build. Everyone else is refused. +# +# A caller that gets past the check still has to name a service that +# exists, so NoSuchService is how we tell "allowed, then failed" apart +# from "not allowed at all". + +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" + +# uid 1000 is 'wheelie', a member of group root in the test sysroot. +# uid 2 is 'bin', a member of nothing that matters here. +say "A member of the group may call a privileged method" +set +e +allowed=$(texec "$CLIENT" call-s-as-uid 1000 "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart nosuchservice 2>&1) +set -e +case "$allowed" in + *AccessDenied*) fail "Group member was refused: $allowed" ;; + *NoSuchService*) assert "Group member passed authorization" 0 -eq 0 ;; + *) fail "Unexpected reply for group member: $allowed" ;; +esac + +say "A caller outside the group may not" +set +e +denied=$(texec "$CLIENT" call-s-as-uid 2 "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart nosuchservice 2>&1) +set -e +case "$denied" in + *AccessDenied*) assert "Non-member refused" 0 -eq 0 ;; + *NoSuchService*) fail "Non-member passed authorization: $denied" ;; + *) fail "Unexpected reply for non-member: $denied" ;; +esac + +say "Root is still allowed" +set +e +asroot=$(texec "$CLIENT" call-s "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart nosuchservice 2>&1) +set -e +case "$asroot" in + *AccessDenied*) fail "Root was refused: $asroot" ;; + *NoSuchService*) assert "Root passed authorization" 0 -eq 0 ;; + *) fail "Unexpected reply for root: $asroot" ;; +esac diff --git a/test/skel/etc/group b/test/skel/etc/group index 696ac862..197977f1 100644 --- a/test/skel/etc/group +++ b/test/skel/etc/group @@ -1,4 +1,4 @@ -root:x:0: +root:x:0:wheelie daemon:x:1: bin:x:2: sys:x:3: @@ -7,3 +7,4 @@ tty:x:5: disk:x:6: dialout:x:20: nogroup:x:65534: +wheelie:x:1000: diff --git a/test/skel/etc/passwd b/test/skel/etc/passwd index 2933891f..bede8cf6 100644 --- a/test/skel/etc/passwd +++ b/test/skel/etc/passwd @@ -3,3 +3,4 @@ daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +wheelie:x:1000:1000:wheelie:/home/wheelie:/bin/sh From 1e508f9168ad4a171046ca59b241adb0d0039459 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 7 Aug 2026 13:58:53 +0200 Subject: [PATCH 11/22] libink: trace connections, calls, and authorization decisions The legacy socket logs a line per command under initctl debug; the bus logged nothing, so the transport that now carries most of initctl was the one you could not watch. libink gets a logger hook rather than a dependency on Finit's: it passes the emitting function and a formatted message, and dbus.c hands both to logit() so the two sources read alike. Trace points cover the connection lifecycle, every inbound call, and why a call was refused. Signed-off-by: Joachim Wiberg --- libink/Makefile.am | 2 +- libink/builtin.c | 1 + libink/connection.c | 3 +++ libink/dispatch.c | 15 +++++++++++++++ libink/internal.h | 5 +++++ libink/link.h | 14 ++++++++++++++ libink/log.c | 40 ++++++++++++++++++++++++++++++++++++++++ libink/server.c | 4 ++++ src/dbus.c | 11 +++++++++++ 9 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 libink/log.c diff --git a/libink/Makefile.am b/libink/Makefile.am index e2f46e3a..e81d921f 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -11,7 +11,7 @@ libink_la_SOURCES = server.c auth.c connection.c \ dispatch.c builtin.c \ match.c \ path.c \ - client.c io.c \ + client.c io.c log.c \ link.h path.h internal.h libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE diff --git a/libink/builtin.c b/libink/builtin.c index e3e7bae7..fa511c1a 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -468,6 +468,7 @@ int __handle_builtin(link_connection_t *conn, const struct link_msg *m) (!strcmp(m->member, "Hello") || !strcmp(m->member, "AddMatch") || !strcmp(m->member, "RemoveMatch"))) { + __dbg("%s is the broker's to answer, not ours", m->member); return __send_error(conn, m, "org.freedesktop.DBus.Error.AccessDenied", "Handled by the message bus, not by this peer"); diff --git a/libink/connection.c b/libink/connection.c index 3fe799bc..0a2327dd 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -77,6 +77,8 @@ int link_connection_call(link_connection_t *conn, const char *destination, if (blen > 0 && __io_write_all(conn->fd, body, (size_t)blen) < 0) return -1; + __dbg("calling %s.%s on %s, serial %u", interface ? interface : "-", + member, destination ? destination : "peer", serial); conn->pending[i].used = 1; conn->pending[i].serial = serial; @@ -93,6 +95,7 @@ void link_connection_close(link_connection_t *conn) if (!conn) return; + __dbg("closing peer on fd %d", conn->fd); /* Anything waiting on this connection has to be told, or a parked * call sits forever and its caller never hears back. */ diff --git a/libink/dispatch.c b/libink/dispatch.c index 44642d7b..c09106f0 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -341,6 +341,7 @@ static void deliver_reply(link_connection_t *conn, const struct link_msg *m) break; } if (i == LINK_PENDING_CAP) { + __dbg("unsolicited reply, serial %u", m->reply_serial); return; } @@ -449,6 +450,7 @@ void link_uid_resolved(link_server_t *server, link_authz_t tok, uid_t uid) if (!conn || __msg_parse(buf, len, &msg) <= 0) return; + __dbg("resumed %s, caller uid %d", msg.member ? msg.member : "call", (int)uid); (void)dispatch_call(conn, &msg, NULL, 0, &uid); } @@ -500,6 +502,7 @@ static int resolve_caller(link_connection_t *conn, const struct link_msg *m, * resolver to remember it: a truncated sender key would let two * callers share one identity. */ if (strlen(m->sender) >= LINK_SENDER_MAX) { + __dbg("sender name too long, refusing to identify it"); return -1; } @@ -512,6 +515,8 @@ static int resolve_caller(link_connection_t *conn, const struct link_msg *m, rc = srv->uid_resolver(conn, m->sender, tok, uid, srv->uid_userdata); if (rc != 1) unpark(p); + else + __dbg("parked %s from %s, awaiting caller uid", m->member, m->sender); return rc; } @@ -533,11 +538,15 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m, call_uid = known_uid ? *known_uid : conn->peer_uid; if (!m->path || !m->member) { + __dbg("malformed call, no path or member"); return __send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Method call without path or member"); } + __dbg("call %s %s.%s from %s", m->path, + m->interface ? m->interface : "-", m->member, + m->sender ? m->sender : "peer"); /* Built-in DBus interfaces (Hello, Ping, Introspect, Properties) * are handled here before object-tree lookup, which means they @@ -551,6 +560,7 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m, o = find_object(conn->server, m->path); if (!o) { + __dbg("no such object %s", m->path); return __send_error(conn, m, "org.freedesktop.DBus.Error.UnknownObject", "No such object"); @@ -558,6 +568,7 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m, meth = resolve(o, m->interface, m->member, &e); if (!meth) { + __dbg("no such method %s on %s", m->member, m->path); return __send_error(conn, m, "org.freedesktop.DBus.Error.UnknownMethod", "No such method on this object"); @@ -569,6 +580,7 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m, const char *want = meth->in_sig ? meth->in_sig : ""; if (strcmp(got, want) != 0) { + __dbg("%s takes '%s', caller sent '%s'", m->member, want, got); return __send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Argument signature mismatch"); @@ -591,6 +603,7 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m, /* Not a permission problem: root may well * be asking, we just have no slot to find * out in. Say so, it is retryable. */ + __dbg("no free slot to identify %s", m->sender); return __send_error(conn, m, "org.freedesktop.DBus.Error.LimitsExceeded", "Too many calls awaiting authorization"); @@ -600,6 +613,8 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m, } if (!caller_may(conn->server, call_uid)) { + __dbg("denied %s, caller uid %d is not privileged", + m->member, (int)call_uid); return __send_error(conn, m, "org.freedesktop.DBus.Error.AccessDenied", "Caller is not privileged for this method"); diff --git a/libink/internal.h b/libink/internal.h index 5f0f778a..7df8da91 100644 --- a/libink/internal.h +++ b/libink/internal.h @@ -160,6 +160,11 @@ struct link_connection { struct link_server *server; /* back-pointer for dispatch */ }; +/* log.c — tracing, no-op unless the embedder installed a callback. */ +void __log(const char *func, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +#define __dbg(fmt, ...) __log(__func__, fmt, ##__VA_ARGS__) + /* io.c — shared EINTR-resilient I/O loops. */ int __io_write_all(int fd, const void *buf, size_t len); int __io_read_full(int fd, void *buf, size_t len); diff --git a/libink/link.h b/libink/link.h index afdd5d30..a3a447ba 100644 --- a/libink/link.h +++ b/libink/link.h @@ -88,6 +88,20 @@ typedef struct { size_t body_len; } link_reply_t; +/* ---------- debug tracing ---------- */ + +/* Receives one line per connection, method call, and authorization + * decision. `func` is the libink function that emitted it, so a host + * can format it the way it formats its own traces. + * + * With no logger installed, the default, a trace point costs one NULL + * test. With one installed the message is formatted before the host + * sees it, so a host that only wants tracing sometimes should install + * and remove the callback rather than discard by level. */ +typedef void (*link_log_cb_t)(void *userdata, const char *func, const char *msg); + +void link_set_logger(link_log_cb_t cb, void *userdata); + /* ---------- caller identity on a broker connection ---------- */ /* Handle for a call parked while its caller is identified. Opaque, diff --git a/libink/log.c b/libink/log.c new file mode 100644 index 00000000..226e263c --- /dev/null +++ b/libink/log.c @@ -0,0 +1,40 @@ +/* libink — optional debug tracing, routed to whoever embeds us. + * + * libink has no logger of its own by design: it must not depend on the + * host's logging, and PID 1 already has one. The embedder installs a + * callback and gets a line per connection, call, and authorization + * decision. Formatting happens before the callback, so an embedder + * that wants tracing off should uninstall rather than filter. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "internal.h" + +static link_log_cb_t logger; +static void *logger_userdata; + +void link_set_logger(link_log_cb_t cb, void *userdata) +{ + logger = cb; + logger_userdata = userdata; +} + +void __log(const char *func, const char *fmt, ...) +{ + char msg[256]; + va_list ap; + + if (!logger) + return; + + va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + + logger(logger_userdata, func, msg); +} diff --git a/libink/server.c b/libink/server.c index cb1cd790..9c32c5ce 100644 --- a/libink/server.c +++ b/libink/server.c @@ -60,6 +60,7 @@ int link_server_new(link_server_t **out, const char *path, mode_t mode) * permissive. Who may connect is the caller's policy to set; * per-method authorization happens later in dispatch via * SO_PEERCRED. */ + __dbg("binding %s, mode %04o", path, (unsigned)mode); { mode_t oldmask = umask(0777 & ~mode); int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun)); @@ -159,6 +160,7 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) __auth_generate_guid(conn->guid); + __dbg("new peer on fd %d, uid %d", cfd, (int)conn->peer_uid); *out = conn; return 0; @@ -216,6 +218,8 @@ link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid conn->broker = !!(attach_flags & LINK_ATTACH_BROKER); __auth_generate_guid(conn->guid); + __dbg("attached %s peer on fd %d, uid %d", + conn->broker ? "broker" : "external", fd, (int)peer_uid); return conn; diff --git a/src/dbus.c b/src/dbus.c index c42cf0e5..a0930992 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -73,6 +73,15 @@ static struct peer *sysbus_peer; static void sysbus_probe(void); static void sender_cache_flush(void); +/* libink has no logger of its own, so give it ours. The func name it + * passes takes the place of dbg()'s __func__, which would otherwise + * always read "link_log_cb". */ +static void link_log_cb(void *userdata, const char *func, const char *msg) +{ + (void)userdata; + logit(LOG_DEBUG, "%s():%s", func, msg); +} + static void peer_drop(struct peer *p) { int was_sysbus = p == sysbus_peer; @@ -1526,6 +1535,8 @@ int dbus_init(uev_ctx_t *ctx) /* Same access policy as INIT_SOCKET: the bus reaches every * service operation initctl does, so --with-group has to gate * both or it gates neither. */ + link_set_logger(link_log_cb, NULL); + if (link_server_new(&server, FINIT_BUS_SOCKET, 0660) < 0) { err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET); return 1; From 9875e764197319ee632466344a7c2f93e088ccef Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 10:15:06 +0200 Subject: [PATCH 12/22] dbus: let a dropped peer outlive its own read loop A peer can be dropped from inside its own read loop: a handler emits a signal, the write to that very peer fails, and the drop lands while link_connection_process() still holds the connection and will touch its rx buffer on the way out. Freeing there pulls the ground out from under it. Unlink the peer and let the event loop free it once the stack has unwound. The work has to be scheduled with a non-zero delay. A uev timer armed with zero is a disarmed timer, so the queue would never run and the connections would leak instead. Losing a peer is also not a warning. It is what shutdown looks like from here, and every reboot said so on the console. Signed-off-by: Joachim Wiberg --- src/dbus.c | 84 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index a0930992..2fdcb193 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -50,6 +50,7 @@ #include "conf.h" #include "log.h" #include "private.h" +#include "schedule.h" #include "service.h" #include "sig.h" #include "sm.h" @@ -61,10 +62,12 @@ struct peer { uev_t watcher; link_connection_t *conn; + int dead; TAILQ_ENTRY(peer) link; }; static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); +static TAILQ_HEAD(, peer) reapq = TAILQ_HEAD_INITIALIZER(reapq); static link_server_t *server; static uev_t accept_watcher; static size_t peer_count; @@ -82,23 +85,55 @@ static void link_log_cb(void *userdata, const char *func, const char *msg) logit(LOG_DEBUG, "%s():%s", func, msg); } +/* Close and free everything dropped since the last time round the + * event loop. Safe here because no connection's read loop is on the + * stack; see peer_drop(). */ +static void peer_reap(void *arg) +{ + struct peer *p; + + (void)arg; + while ((p = TAILQ_FIRST(&reapq))) { + TAILQ_REMOVE(&reapq, p, link); + link_connection_close(p->conn); + free(p); + } +} + +/* Not zero: a uev timer armed with a zero timeout is a disarmed + * timer, so the work would never run and the connections would leak. + * Any short delay does, the peer is already unlinked and its watcher + * stopped, so nothing touches it in the meantime. */ +static struct wq reap_work = { .cb = peer_reap, .delay = 10 }; + +/* + * A peer can be dropped from inside its own read loop: a handler emits + * a signal, the write to this very peer fails, and dbus_emit_signal() + * lands here while link_connection_process() still holds the + * connection and will touch its rx buffer on the way out. Freeing now + * would pull that out from under it, so unlink the peer and let the + * event loop free it once the stack has unwound. + */ static void peer_drop(struct peer *p) { - int was_sysbus = p == sysbus_peer; + if (p->dead) + return; /* already on its way out */ + p->dead = 1; uev_io_stop(&p->watcher); - link_connection_close(p->conn); TAILQ_REMOVE(&peers, p, link); peer_count--; - free(p); /* broker gone; the notify paths probe for its return. Its unique * names die with it, so nothing we learned about them is safe to * carry over to whatever takes its place. */ - if (was_sysbus) { + if (p == sysbus_peer) { sysbus_peer = NULL; sender_cache_flush(); } + + TAILQ_INSERT_TAIL(&reapq, p, link); + schedule_work(&reap_work); } static void peer_cb(uev_t *w, void *arg, int events) @@ -813,6 +848,19 @@ void dbus_unregister_service(svc_t *svc) * 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. */ +/* A peer that has gone away is routine rather than a fault: initctl + * calls and exits, and on the way down every peer goes at once, which + * is not something an operator watching the shutdown needs to read + * about. Keep the warning for a write that failed for some other + * reason, where something really is wrong. */ +static int quiet_drop(int err) +{ + if (err == EPIPE || err == ECONNRESET || err == ENOTCONN) + return 1; + + return runlevel == 0 || runlevel == 6; +} + static void dbus_emit_signal(const char *path, const char *interface, const char *member, @@ -824,18 +872,23 @@ static void dbus_emit_signal(const char *path, if (!server || TAILQ_EMPTY(&peers)) return; TAILQ_FOREACH_SAFE(p, &peers, link, tmp) { + int err; + 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); - } + signature, body, body_len) >= 0) + continue; + + err = errno; + + /* nothing hit the wire, and same for every peer */ + if (err == EMSGSIZE || err == EINVAL) + break; + + logit(quiet_drop(err) ? LOG_DEBUG : LOG_WARNING, + "D-Bus peer fd %d write failed: %s, dropping", + link_connection_get_fd(p->conn), strerror(err)); + peer_drop(p); } } @@ -1601,6 +1654,9 @@ int dbus_exit(void) while ((p = TAILQ_FIRST(&peers))) peer_drop(p); + /* No read loop is running now, and the timer never will again. */ + peer_reap(NULL); + if (server) { link_server_free(server); server = NULL; From 2e0b1d6f8b194ebc4c4d3fdb6bfb331a394b5557 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 10:15:03 +0200 Subject: [PATCH 13/22] plugin: start a system bus by default Finit speaks D-Bus itself now and claims org.finit on the system bus when it finds one, but nothing in a default build ever brings that bus up. The plugin that does was opt-in, so the built-in support sat idle unless the integrator knew to ask for both halves. Defaulting it on is only reasonable if the result stays the admin's to change, and a service registered from C through conf_save_service() is not: it lands in the run path where it cannot be overridden or emptied out. So the daemon moves to 20-dbus.conf and its directories to tmpfiles.d/dbus.conf, the same way hotplug and every other daemon we ship them for. The plugin keeps only what has to look at the running system, the stale pidfile and the machine UUID. Those directories are no longer chowned to messagebus. tmpfiles.d skips a line whose user does not exist rather than falling back, so the plugin's messagebus/dbus/root ladder has no equivalent there, and dbus-daemon binds its socket before dropping privileges anyway. The plugin already bows out where there is no dbus-daemon installed, so systems that never wanted a bus are unaffected, and --disable-dbus-plugin is there for those that have one and would still rather init left it alone. Signed-off-by: Joachim Wiberg --- configure.ac | 2 +- doc/ChangeLog.md | 8 +++++ doc/build.md | 12 ++++--- doc/plugins.md | 5 ++- plugins/dbus.c | 75 ++++++------------------------------------ system/20-dbus.conf | 19 +++++++++++ system/Makefile.am | 4 +++ tmpfiles.d/Makefile.am | 4 +++ tmpfiles.d/dbus.conf | 4 +++ 9 files changed, 61 insertions(+), 72 deletions(-) create mode 100644 system/20-dbus.conf create mode 100644 tmpfiles.d/dbus.conf diff --git a/configure.ac b/configure.ac index 33074b59..3a0a971d 100644 --- a/configure.ac +++ b/configure.ac @@ -117,7 +117,7 @@ AC_ARG_ENABLE(all_plugins, AS_HELP_STRING([--enable-all-plugins], [Enable all plugins, default: auto]), enable_all_plugins=$enableval, enable_all_plugins=auto) AC_PLUGIN([alsa-utils], [no], [Save and restore ALSA sound settings using alsactl]) -AC_PLUGIN([dbus], [no], [Setup and start system message bus, D-Bus]) +AC_PLUGIN([dbus], [yes], [Setup and start system message bus, D-Bus]) AC_PLUGIN([modules-load], [no], [Scans /etc/modules-load.d for modules to load]) AC_PLUGIN([modprobe], [no], [Coldplug modules using modalias magic]) AC_PLUGIN([resolvconf], [no], [Setup necessary files for resolvconf]) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index b6c21350..9a3082cc 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -8,6 +8,14 @@ All relevant changes are documented in this file. ### Changes +- The `dbus.so` plugin, which starts an external `dbus-daemon`, is now + enabled by default. It does nothing on systems without a + `dbus-daemon` installed, and `--disable-dbus-plugin` opts out. The + daemon it used to register from C now lives in `20-dbus.conf`, and + the directories it needs in `tmpfiles.d/dbus.conf`, so both can be + overridden from `/etc` like any other system file. Those + directories are no longer chowned to `messagebus`, matching how + Finit ships directories for other daemons - The `tty` block takes `passenv`, which the line-based format has had since v4.4 (issue #286) and the block format was missing - New `provides` setting for run/task/service/sysv blocks, naming diff --git a/doc/build.md b/doc/build.md index 5deb0ec8..0d9cc5cf 100644 --- a/doc/build.md +++ b/doc/build.md @@ -53,14 +53,16 @@ Below are a few of the main switches to configure: * `--disable-dbus`: Opt out of Finit's built-in D-Bus support, enabled by default. See [D-Bus Integration](dbus.md) for what it provides. Not to - be confused with `--enable-dbus-plugin` below, which only starts an - external `dbus-daemon` + be confused with `--disable-dbus-plugin` below, which only governs + starting an external `dbus-daemon` * `--enable-alsa-utils-plugin`: Enable the optional `alsa-utils.so` sound plugin. -* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin, which - launches `dbus-daemon` at boot. Unrelated to the built-in bus, see - `--disable-dbus` above. +* `--disable-dbus-plugin`: Drop the `dbus.so` plugin, which launches + `dbus-daemon` at boot. Enabled by default; the plugin does nothing on + a system that has no `dbus-daemon` installed, so opting out is only + needed to keep init from starting a bus on a system that has one. + Unrelated to the built-in bus, see `--disable-dbus` above. * `--enable-resolvconf-plugin`: Enable the `resolvconf.so` optional plugin. diff --git a/doc/plugins.md b/doc/plugins.md index 36b6ddde..458544a7 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -35,7 +35,10 @@ For your convenience a set of *optional* plugins are available: > the `/etc/fstab` file and in the Linux kernel config. * *dbus.so*: Setup and start system message bus, D-Bus, at boot. - _Optional plugin._ + Enabled by default, and a no-op on systems without a `dbus-daemon`. + The daemon is declared in `20-dbus.conf` and its directories in + `tmpfiles.d/dbus.conf`, both overridable from `/etc`; the plugin + itself only handles what has to inspect the running system. * *hook-scripts.so*: Trigger the execution of scripts from plugin hook points (see [Hooks](#hooks)). _Optional plugin._ diff --git a/plugins/dbus.c b/plugins/dbus.c index 80c653e9..39b6ec02 100644 --- a/plugins/dbus.c +++ b/plugins/dbus.c @@ -32,22 +32,11 @@ #include "config.h" #include "helpers.h" #include "plugin.h" -#include "service.h" -#include "util.h" #include "conf.h" +#include "util.h" #include "log.h" #define DBUS_DAEMON "dbus-daemon" -#define DBUS_ARGS "--nofork --system --syslog-only" -#define DBUS_DESC "D-Bus message bus daemon" - -#ifndef DBUS_DAEMONUSER -#define DBUS_DAEMONUSER "messagebus" -#endif - -#ifndef DBUS_DAEMONGROUP -#define DBUS_DAEMONGROUP "messagebus" -#endif /* * Dumnpster diving for the D-Bus main configuration file @@ -96,79 +85,35 @@ static char *dbus_pidfn(void) return NULL; } +/* + * The directories live in tmpfiles.d/dbus.conf and the service in + * system/20-dbus.conf, both of which an administrator can override. + * What is left needs to look at the running system, so it stays here. + */ static void setup(void *arg) { - char *group = DBUS_DAEMONGROUP; - char *user = DBUS_DAEMONUSER; - char esccmd[256]; - char pid[300]; char *pidfn; - mode_t prev; - char *cmd; if (rescue) { dbg("Skipping %s plugin in rescue mode.", "dbus"); return; } - cmd = which(DBUS_DAEMON); - if (!cmd) { + if (!whichp(DBUS_DAEMON)) { dbg("Skipping plugin, %s is not installed.", DBUS_DAEMON); return; } - if (getuser(user, NULL) == -1) { - if (getuser("dbus", NULL) == -1) - user = "root"; /* fallback */ - else - user = "dbus"; /* e.g., Buildroot */ - } - - if (getgroup(group) == -1) { - if (getgroup("dbus") == -1) - group = "root"; /* fallback */ - else - group = "dbus"; /* e.g., Buildroot */ - } - /* Clean up from any previous pre-bootstrap run */ pidfn = dbus_pidfn(); - if (pidfn) + if (pidfn) { remove(pidfn); - - dbg("Creating D-Bus Required Directories ..."); - prev = umask(0); - mksubsys("/var/run/dbus", 0755, user, group); - mksubsys("/var/run/lock/subsys", 0755, user, group); - mksubsys("/var/lib/dbus", 0755, user, group); - mksubsys("/tmp/dbus", 0755, user, group); - umask(prev); + free(pidfn); + } /* Generate machine id for dbus */ if (whichp("dbus-uuidgen")) run_interactive("dbus-uuidgen --ensure", "Verifying D-Bus machine UUID"); - - /* - * Register service with Finit - * Note: dbus drops privs after starting up. - */ - pid[0] = 0; - if (pidfn) { - char esc[280]; - - snprintf(pid, sizeof(pid), "\tpidfile = \"%s\"\n", - conf_escape(pidfn, esc, sizeof(esc))); - free(pidfn); - } - conf_save_service(SVC_TYPE_SERVICE, "dbus", "dbus.conf", - "\tdescription = \"" DBUS_DESC "\"\n" - "\trunlevel = \"S123456789\"\n" - "\tnotify = \"none\"\n" - "\tcgroup system {}\n" - "%s" - "\tcommand = \"%s " DBUS_ARGS "\"\n", - pid, conf_escape(cmd, esccmd, sizeof(esccmd))); - free(cmd); } static plugin_t plugin = { diff --git a/system/20-dbus.conf b/system/20-dbus.conf new file mode 100644 index 00000000..f68f23ab --- /dev/null +++ b/system/20-dbus.conf @@ -0,0 +1,19 @@ +# System message bus, D-Bus +# +# The dbus.so plugin creates the directories dbus-daemon needs and makes +# sure the machine has a UUID, but the daemon itself is declared here so +# you can change it. The leading '-' says it is fine if dbus-daemon is +# not installed; the service is then skipped without a warning. +# +# Finit registers org.finit on this bus once it is up, see docs/dbus.md. +# +# Override this file by copying it to /etc/finit.d/, using the same name, +# then change the contents any way you like, it can even be empty. + +service dbus { + description = "D-Bus message bus daemon" + runlevel = "S123456789" + notify = "none" + cgroup system {} + command = "-dbus-daemon --nofork --system --syslog-only" +} diff --git a/system/Makefile.am b/system/Makefile.am index 5dac235b..284aebf9 100644 --- a/system/Makefile.am +++ b/system/Makefile.am @@ -7,6 +7,10 @@ if BUILD_HOTPLUG_PLUGIN system_DATA += 10-hotplug.conf endif +if BUILD_DBUS_PLUGIN +dist_system_DATA += 20-dbus.conf +endif + if BUILD_TESTSERV_PLUGIN dist_system_DATA += 90-testserv.conf endif diff --git a/tmpfiles.d/Makefile.am b/tmpfiles.d/Makefile.am index 5155277c..facb9722 100644 --- a/tmpfiles.d/Makefile.am +++ b/tmpfiles.d/Makefile.am @@ -6,6 +6,10 @@ dist_tmpfiles_DATA = etc.conf legacy.conf var.conf dist_tmpfiles_DATA += dnsmasq.conf frr.conf lldpd.conf openswan.conf \ quagga.conf sshd.conf uuidd.conf +if BUILD_DBUS_PLUGIN +dist_tmpfiles_DATA += dbus.conf +endif + if BUILD_TESTSERV_PLUGIN dist_tmpfiles_DATA += testserv.conf endif diff --git a/tmpfiles.d/dbus.conf b/tmpfiles.d/dbus.conf new file mode 100644 index 00000000..f98e0252 --- /dev/null +++ b/tmpfiles.d/dbus.conf @@ -0,0 +1,4 @@ +d /var/run/dbus 0755 - - - +d /var/run/lock/subsys 0755 - - - +d /var/lib/dbus 0755 - - - +d /tmp/dbus 0755 - - - From 127049d925525ab4ebb834fb2bfd9671b03dd0e4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 10:47:32 +0200 Subject: [PATCH 14/22] test: Finit against a real dbus-daemon The other dbus-*.sh tests drive libink's own client, so the wire format was only ever checked against the implementation that wrote it, and the broker path had no coverage at all. Every bug found in it so far was found by hand on a target. Let the dbus plugin bring up a real dbus-daemon, wait for Finit to claim org.finit, then talk to Finit with dbus-send, which shares no code with us. The privileged call is the interesting one: it can only be answered by parking the call and asking the broker who sent it. The bus reads the policy Finit installs, so a malformed org.finit.conf fails here rather than on a target. Tests no longer build --with-libsystemd. Our replacement carries the real soname but only the sd_notify() symbols, so in the test root it shadowed the libsystemd the host's libdbus-1 wants and dbus-daemon died on a missing sd_is_socket. Nothing under test needs the shared library: serv is the only consumer and it compiles sd-daemon.c straight in, which it now does regardless of the flag so notify.sh keeps testing notify:systemd either way. Staged from the host by lib/sysroot.mk like any other binary, and skipped when the host has neither program. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 2 +- test/Makefile.am | 6 +++ test/check.sh | 2 +- test/dbus-broker.sh | 61 ++++++++++++++++++++++++++ test/lib/sysroot.mk | 18 ++++++-- test/skel/etc/machine-id | 1 + test/skel/usr/share/dbus-1/system.conf | 36 +++++++++++++++ test/src/Makefile.am | 7 +-- 8 files changed, 125 insertions(+), 8 deletions(-) create mode 100755 test/dbus-broker.sh create mode 100644 test/skel/etc/machine-id create mode 100644 test/skel/usr/share/dbus-1/system.conf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec7aeab..37c56b74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: run: | ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \ --enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \ - --with-keventd --with-libsystemd \ + --with-keventd \ CFLAGS="-fsanitize=address -ggdb" make -j9 clean make -j9 V=1 diff --git a/test/Makefile.am b/test/Makefile.am index a06d9d20..6e8cfd1a 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -10,6 +10,7 @@ EXTRA_DIST = skel/bin/busybox-x86_64.sha256 skel/sbin/service.sh skel/etc/env skel/etc/rcS.d/S01abc.sh skel/etc/rcS.d/S02def.sh \ skel/cdrom/.empty skel/dev/shm/.empty skel/dev/pts/.empty \ skel/etc/inittab skel/etc/hostname skel/etc/fstab \ + skel/usr/share/dbus-1/system.conf skel/etc/machine-id \ skel/etc/passwd skel/etc/group skel/etc/ld.so.conf \ skel/etc/init.d/rcS skel/etc/init.d/rcK skel/tmp/.empty \ skel/etc/finit.d/.empty skel/etc/finit.d/available/.empty \ @@ -73,6 +74,7 @@ EXTRA_DIST += testserv.sh EXTRA_DIST += unexpected-restart.sh EXTRA_DIST += dbus-auth.sh EXTRA_DIST += dbus-authz.sh +EXTRA_DIST += dbus-broker.sh EXTRA_DIST += dbus-bus.sh EXTRA_DIST += dbus-manager.sh EXTRA_DIST += dbus-service.sh @@ -143,6 +145,10 @@ TESTS += dbus-service.sh TESTS += dbus-cond.sh TESTS += dbus-initctl.sh TESTS += dbus-introspect.sh +# Needs the plugin to bring up the bus, not just the built-in one +if BUILD_DBUS_PLUGIN +TESTS += dbus-broker.sh +endif endif check-recursive: setup-chroot diff --git a/test/check.sh b/test/check.sh index c12a2614..e76d4af7 100755 --- a/test/check.sh +++ b/test/check.sh @@ -26,7 +26,7 @@ fi ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \ --enable-dbus --enable-x11-common-plugin --enable-testserv-plugin \ - --with-watchdog --with-keventd --with-libsystemd \ + --with-watchdog --with-keventd \ CFLAGS='-fsanitize=address -ggdb' if [ "$run_make" -eq 1 ]; then diff --git a/test/dbus-broker.sh b/test/dbus-broker.sh new file mode 100755 index 00000000..e82897f5 --- /dev/null +++ b/test/dbus-broker.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Finit against a real message bus. +# +# Every other dbus-*.sh test drives libink's own client, so the wire +# format is only ever checked against the implementation that produced +# it. Here the dbus plugin brings up a real dbus-daemon, Finit finds +# it and claims org.finit, and then dbus-send talks to Finit: a client +# that shares no code with us. +# +# It also covers the only path the brokerless bus cannot reach, where +# one connection carries every caller and Finit has to ask the broker +# who sent each privileged call. +# +# Skipped when the host has no dbus-daemon or dbus-send to stage. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +# Staged by lib/sysroot.mk from the host, when it has them. +for prog in dbus-daemon dbus-send; do + texec sh -c "command -v $prog >/dev/null" \ + || skip "no $prog in the test root, need it on the host" +done + +say 'The dbus plugin started a system bus' +retry 'assert_file_exists /var/run/dbus/system_bus_socket' + +say 'Finit claims org.finit on the system bus' +retry "texec dbus-send --system --print-reply --dest=org.freedesktop.DBus \ + /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.finit" 60 0.5 + +say 'A read-only method answers through the broker' +list=$(texec dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager org.finit.Manager1.ListServices) +assert "ListServices returned the dbus service" \ + "$(printf '%s' "$list" | grep -c '"dbus"')" -ge 1 + +say 'Properties.Get answers through the broker' +rl=$(texec dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager org.freedesktop.DBus.Properties.Get \ + string:org.finit.Manager1 string:Runlevel) +assert "Runlevel property is 2" "$(printf '%s' "$rl" | grep -c '"2"')" -eq 1 + +# The point of the exercise: a privileged call over the broker means +# Finit parks it, asks the driver who the sender is, and dispatches on +# the answer. Root is allowed, so reaching NoSuchService proves the +# whole round trip worked rather than a blanket denial. The repeat +# call goes the same way, only answered from the sender cache. +for pass in first repeat; do + say "A privileged method resolves the caller, $pass call" + priv=$(texec dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager org.finit.Manager1.Restart string:nosuchservice 2>&1 || true) + case "$priv" in + *NoSuchService*) assert "Caller identified on the $pass call" 0 -eq 0 ;; + *) fail "Unexpected reply to the $pass privileged call: $priv" ;; + esac +done diff --git a/test/lib/sysroot.mk b/test/lib/sysroot.mk index ea2d8c7c..ae71e7f2 100644 --- a/test/lib/sysroot.mk +++ b/test/lib/sysroot.mk @@ -36,11 +36,23 @@ BBURL ?= $(BBHOME)/$(BBVER)/$(BBBIN) # glibc dlopen()s NSS modules at runtime, so ldd does not list them, but # without libnss_files getpwnam() cannot resolve users inside the chroot -_libs_nss = $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ +_libs_nss := $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ /usr/lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ /lib64/libnss_files.so.2 /lib/libnss_files.so.2)) -_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) -libs = $(foreach path,$(_libs_src),$(abspath $(DEST))$(path)) +# A real broker and a real client, staged when the host has them, so +# one test can check Finit against dbus-daemon instead of only against +# libink's own client. Absent is fine, dbus-broker.sh skips. +dbus_bins := $(foreach b,dbus-daemon dbus-send,$(firstword $(wildcard /usr/bin/$(b) /bin/$(b)))) +# Given several binaries ldd prefixes each with a 'path:' header, and +# that trailing colon would land in a make target. Excluding it here +# is enough, no need for one ldd per binary. +_libs_dbus := $(if $(dbus_bins),$(shell ldd $(dbus_bins) | grep -Eo '/[^ :]+')) + +# The binaries stage exactly like the libraries: same host path, same +# path under DEST, copied by the rule below. +_libs_src := $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) \ + $(_libs_dbus) $(dbus_bins) +libs := $(foreach path,$(sort $(_libs_src)),$(abspath $(DEST))$(path)) all: $(libs) $(DEST)/bin/$(BBBIN) @(cd $(DEST); \ diff --git a/test/skel/etc/machine-id b/test/skel/etc/machine-id new file mode 100644 index 00000000..cc671894 --- /dev/null +++ b/test/skel/etc/machine-id @@ -0,0 +1 @@ +0123456789abcdef0123456789abcdef diff --git a/test/skel/usr/share/dbus-1/system.conf b/test/skel/usr/share/dbus-1/system.conf new file mode 100644 index 00000000..5d256837 --- /dev/null +++ b/test/skel/usr/share/dbus-1/system.conf @@ -0,0 +1,36 @@ + + + + + system + + + /etc/dbus-1/system.d + unix:path=/var/run/dbus/system_bus_socket + EXTERNAL + + + + + + + + + + + + + + diff --git a/test/src/Makefile.am b/test/src/Makefile.am index 87d6e362..6897b3ab 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -2,11 +2,12 @@ noinst_PROGRAMS = serv serv_SOURCES = serv.c serv_CPPFLAGS = -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_GNU_SOURCE -D_DEFAULT_SOURCE -I$(top_builddir) -if LIBSYSTEMD -serv_CPPFLAGS += -I$(top_srcdir)/libsystemd $(lite_CFLAGS) +# serv is what notify.sh drives to test notify:systemd, so it always +# needs sd_notify(). The source is in-tree and it links straight in, +# unrelated to whether --with-libsystemd installs the shared library. +serv_CPPFLAGS += -DHAVE_LIBSYSTEMD=1 -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 From 44e7da6d5645f6b96de774bdb8b51f34cb531292 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 10:47:32 +0200 Subject: [PATCH 15/22] dbus: policy comment predates the per-sender uid lookup It still described treating every system-bus caller as unprivileged as the state of things, which stopped being true when Finit learned to ask the broker who sent a call. Signed-off-by: Joachim Wiberg --- dbus-1/org.finit.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dbus-1/org.finit.conf b/dbus-1/org.finit.conf index ea730e9b..5eb40fc2 100644 --- a/dbus-1/org.finit.conf +++ b/dbus-1/org.finit.conf @@ -11,10 +11,10 @@ every state-changing method at the broker. finit *also* enforces per-method authorisation itself (LINK_METHOD_PRIVILEGED), so defense-in-depth: even if a permissive policy is installed by - mistake, finit rejects unprivileged state changes via SO_PEERCRED - on the local bus and by treating every system-bus caller as - unprivileged (peer_uid = (uid_t)-1) until a per-sender uid lookup - via GetConnectionUnixUser is implemented. + mistake, finit rejects unprivileged state changes. It reads the + caller from SO_PEERCRED on the local bus, and on the system bus it + asks the broker who sent each privileged call, via + GetConnectionUnixUser. --> From a1969efac147d5b1ceb15505b7f94ba956d4e918 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 12:20:12 +0200 Subject: [PATCH 16/22] libink: broker state belongs to the connection that has a broker Parked calls and outbound calls awaiting a reply only ever happen on a connection talking to a broker, but the parked array sat on the server and the pending array on every connection. A server with no broker carried 4 KiB of slots it could never fill, and both were reachable from code paths that have no business in them. Move both behind one struct, allocated on the first park or call and freed with the connection. link_server_t goes from 4400 to 168 bytes; link_connection_t barely moves, its buffers dominate, but an ordinary peer no longer carries reply-tracking it never uses. Tokens are now per bus rather than per server, so link_uid_resolved() takes the connection the answer is about. Every resolver already has it: it is the first argument to both the resolver and the reply callback. Signed-off-by: Joachim Wiberg --- libink/connection.c | 29 +++++++++++++++---- libink/dispatch.c | 69 +++++++++++++++++++++++++-------------------- libink/internal.h | 38 +++++++++++++++++-------- libink/link.h | 5 ++-- src/dbus.c | 4 +-- 5 files changed, 94 insertions(+), 51 deletions(-) diff --git a/libink/connection.c b/libink/connection.c index 0a2327dd..755e125f 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -35,6 +35,7 @@ int link_connection_call(link_connection_t *conn, const char *destination, uint8_t hdr[LINK_CALL_HDR_MAX]; ssize_t blen = 0; ssize_t hlen; + struct link_bus *bus; uint32_t serial; int i; @@ -43,8 +44,12 @@ int link_connection_call(link_connection_t *conn, const char *destination, return -1; } + bus = __bus_get(conn); + if (!bus) + return -1; + for (i = 0; i < LINK_PENDING_CAP; i++) { - if (!conn->pending[i].used) + if (!bus->pending[i].used) break; } if (i == LINK_PENDING_CAP) { @@ -80,14 +85,28 @@ int link_connection_call(link_connection_t *conn, const char *destination, __dbg("calling %s.%s on %s, serial %u", interface ? interface : "-", member, destination ? destination : "peer", serial); - conn->pending[i].used = 1; - conn->pending[i].serial = serial; - conn->pending[i].cb = cb; - conn->pending[i].userdata = userdata; + bus->pending[i].used = 1; + bus->pending[i].serial = serial; + bus->pending[i].cb = cb; + bus->pending[i].userdata = userdata; return 0; } +struct link_bus *__bus_get(link_connection_t *conn) +{ + if (!conn->bus) + conn->bus = calloc(1, sizeof(*conn->bus)); + + return conn->bus; +} + +void __bus_free(link_connection_t *conn) +{ + free(conn->bus); + conn->bus = NULL; +} + void link_connection_close(link_connection_t *conn) { size_t i; diff --git a/libink/dispatch.c b/libink/dispatch.c index c09106f0..c79a9b6a 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -336,8 +336,14 @@ static void deliver_reply(link_connection_t *conn, const struct link_msg *m) void *userdata; int i; + if (!conn->bus) { + __dbg("unsolicited reply, serial %u", m->reply_serial); + return; + } + for (i = 0; i < LINK_PENDING_CAP; i++) { - if (conn->pending[i].used && conn->pending[i].serial == m->reply_serial) + if (conn->bus->pending[i].used && + conn->bus->pending[i].serial == m->reply_serial) break; } if (i == LINK_PENDING_CAP) { @@ -345,9 +351,9 @@ static void deliver_reply(link_connection_t *conn, const struct link_msg *m) return; } - cb = conn->pending[i].cb; - userdata = conn->pending[i].userdata; - conn->pending[i].used = 0; + cb = conn->bus->pending[i].cb; + userdata = conn->bus->pending[i].userdata; + conn->bus->pending[i].used = 0; if (!cb) return; @@ -364,26 +370,30 @@ static void deliver_reply(link_connection_t *conn, const struct link_msg *m) static struct link_parked *park(link_connection_t *conn, const uint8_t *frame, size_t len, link_authz_t *tok) { - link_server_t *srv = conn->server; + struct link_bus *bus; int i; if (!frame || !len || len > LINK_PARKED_MSG_MAX) return NULL; + bus = __bus_get(conn); + if (!bus) + return NULL; + for (i = 0; i < LINK_PARKED_CAP; i++) { - if (!srv->parked[i].tok) + if (!bus->parked[i].tok) break; } if (i == LINK_PARKED_CAP) return NULL; - srv->parked[i].tok = ++srv->next_tok; - srv->parked[i].conn = conn; - srv->parked[i].len = len; - memcpy(srv->parked[i].buf, frame, len); - *tok = srv->parked[i].tok; + bus->parked[i].tok = ++bus->next_tok; + bus->parked[i].conn = conn; + bus->parked[i].len = len; + memcpy(bus->parked[i].buf, frame, len); + *tok = bus->parked[i].tok; - return &srv->parked[i]; + return &bus->parked[i]; } static void unpark(struct link_parked *p) @@ -394,46 +404,46 @@ static void unpark(struct link_parked *p) void __dispatch_forget_conn(link_connection_t *conn) { - link_server_t *srv = conn->server; + struct link_bus *bus = conn->bus; int i; + if (!bus) + return; + /* Drop parked calls first. A pending callback below may try to * resolve one, and resuming a dispatch on a connection that is * being torn down is no use to anyone; an invalidated slot makes * that resolve a no-op instead. */ - if (srv) { - for (i = 0; i < LINK_PARKED_CAP; i++) { - if (srv->parked[i].conn == conn) - unpark(&srv->parked[i]); - } - } + for (i = 0; i < LINK_PARKED_CAP; i++) + unpark(&bus->parked[i]); for (i = 0; i < LINK_PENDING_CAP; i++) { - if (conn->pending[i].used && conn->pending[i].cb) - conn->pending[i].cb(conn, NULL, conn->pending[i].userdata); - conn->pending[i].used = 0; + if (bus->pending[i].used && bus->pending[i].cb) + bus->pending[i].cb(conn, NULL, bus->pending[i].userdata); + bus->pending[i].used = 0; } + + __bus_free(conn); } static int dispatch_call(link_connection_t *conn, const struct link_msg *m, const uint8_t *frame, size_t framelen, const uid_t *known_uid); -void link_uid_resolved(link_server_t *server, link_authz_t tok, uid_t uid) +void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid) { uint8_t buf[LINK_PARKED_MSG_MAX]; struct link_parked *p = NULL; - link_connection_t *conn; struct link_msg msg; size_t len; int i; - if (!server || !tok) + if (!conn || !conn->bus || !tok) return; for (i = 0; i < LINK_PARKED_CAP; i++) { - if (server->parked[i].tok == tok) { - p = &server->parked[i]; + if (conn->bus->parked[i].tok == tok) { + p = &conn->bus->parked[i]; break; } } @@ -442,12 +452,11 @@ void link_uid_resolved(link_server_t *server, link_authz_t tok, uid_t uid) /* Copy the message out and free the slot before dispatching: * the handler may park a call of its own. */ - conn = p->conn; - len = p->len; + len = p->len; memcpy(buf, p->buf, len); unpark(p); - if (!conn || __msg_parse(buf, len, &msg) <= 0) + if (__msg_parse(buf, len, &msg) <= 0) return; __dbg("resumed %s, caller uid %d", msg.member ? msg.member : "call", (int)uid); diff --git a/libink/internal.h b/libink/internal.h index 7df8da91..6d6aaa75 100644 --- a/libink/internal.h +++ b/libink/internal.h @@ -70,6 +70,27 @@ struct link_parked { uint8_t buf[LINK_PARKED_MSG_MAX]; }; +/* Calls in flight in either direction: inbound ones held while we ask + * who sent them, outbound ones waiting for their reply. Both belong + * to a conversation with a broker, so this hangs off the connection + * and is allocated on first use. An ordinary peer, which only ever + * calls in and is identified by SO_PEERCRED, never gets one. + * + * Tokens are handed out per bus, which is all link_uid_resolved() + * needs: it is told the connection the answer belongs to. */ +struct link_bus { + struct link_parked parked[LINK_PARKED_CAP]; + link_authz_t next_tok; + + /* Outbound calls we made on this connection, awaiting replies. */ + struct { + int used; + uint32_t serial; + link_reply_cb_t cb; + void *userdata; + } pending[LINK_PENDING_CAP]; +}; + struct link_server { int fd; char path[LINK_PATH_MAX]; @@ -83,8 +104,6 @@ struct link_server { /* Set by link_server_set_authorizer(); see link.h. */ link_authorizer_t authorizer; void *authz_userdata; - struct link_parked parked[LINK_PARKED_CAP]; - link_authz_t next_tok; }; /* The reply being assembled inside a method handler. @@ -147,15 +166,8 @@ struct link_connection { uint32_t next_serial; - /* Outbound calls we made on this connection, awaiting replies. - * Only a broker connection uses these today, to ask the bus - * driver who a sender is. */ - struct { - int used; - uint32_t serial; - link_reply_cb_t cb; - void *userdata; - } pending[LINK_PENDING_CAP]; + /* Allocated on the first park or outbound call, see above. */ + struct link_bus *bus; struct link_server *server; /* back-pointer for dispatch */ }; @@ -174,6 +186,10 @@ int __auth_process(link_connection_t *conn); void __auth_generate_guid(char out[33]); int __auth_client(int fd, uid_t uid); +/* connection.c — the per-connection bus state, made on demand. */ +struct link_bus *__bus_get (link_connection_t *conn); +void __bus_free(link_connection_t *conn); + /* dispatch.c */ int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen); void __dispatch_forget_conn(link_connection_t *conn); diff --git a/libink/link.h b/libink/link.h index a3a447ba..f81a804b 100644 --- a/libink/link.h +++ b/libink/link.h @@ -146,8 +146,9 @@ void link_server_set_authorizer(link_server_t *server, link_authorizer_t cb, /* Complete a deferred resolve and resume the parked call. Pass * (uid_t)-1 to say the caller could not be identified, which denies * it. Resolving a handle twice, or one whose connection has since - * closed, does nothing. */ -void link_uid_resolved(link_server_t *server, link_authz_t tok, uid_t uid); + * closed, does nothing. The connection is the one the resolver was + * asked about; a reply callback is handed it as its first argument. */ +void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid); /* Called with the reply to an outbound link_connection_call(). `reply` * is NULL if the connection dropped before one arrived. */ diff --git a/src/dbus.c b/src/dbus.c index 2fdcb193..2a610688 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -1361,8 +1361,6 @@ static void uid_reply_cb(link_connection_t *conn, const link_reply_t *reply, voi uid_t uid = (uid_t)-1; uint32_t val; - (void)conn; - if (!reply) { dbg("connection dropped before %s was identified", q->sender); } else if (reply->type == LINK_MSG_METHOD_RETURN && @@ -1375,7 +1373,7 @@ static void uid_reply_cb(link_connection_t *conn, const link_reply_t *reply, voi reply->error_name ? reply->error_name : "unexpected reply"); } - link_uid_resolved(server, q->tok, uid); + link_uid_resolved(conn, q->tok, uid); free(q); } From 853e2268123830d8d9a50ddea78075812c4e80b3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 10:15:19 +0200 Subject: [PATCH 17/22] libink/dbus: give parked and outstanding calls a deadline A call is parked until the resolver says who sent it, and an outbound call sits in a pending slot until its reply lands. Neither had a way to give up. A broker that answers GetConnectionUnixUser slowly, or not at all, leaves the caller waiting forever and keeps the slot; four of those and every later privileged call is refused with LimitsExceeded until Finit restarts. libink cannot time itself out, it has no event loop, so the deadline is the embedder's to keep. One sweep per connection covers both, and the ordering between them stays in the library rather than in each embedder: calls first, because one timing out usually resolves the park it was made for, and AccessDenied tells that caller more than a bare timeout. The sweep is armed when a resolve is deferred and stops rearming as soon as nothing is outstanding, so a system that never meets a broker never wakes up for it. Signed-off-by: Joachim Wiberg --- libink/connection.c | 40 ++++++++++++++++++++++++++++++++++ libink/dispatch.c | 52 ++++++++++++++++++++++++++++++++++++++++++--- libink/internal.h | 10 +++++++-- libink/io.c | 15 +++++++++++++ libink/link.h | 12 +++++++++++ src/dbus.c | 48 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 5 deletions(-) diff --git a/libink/connection.c b/libink/connection.c index 755e125f..d5cff196 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -87,6 +87,7 @@ int link_connection_call(link_connection_t *conn, const char *destination, bus->pending[i].used = 1; bus->pending[i].serial = serial; + bus->pending[i].stamp = __now_ms(); bus->pending[i].cb = cb; bus->pending[i].userdata = userdata; @@ -107,6 +108,45 @@ void __bus_free(link_connection_t *conn) conn->bus = NULL; } +/* Neither half can time itself out, libink has no event loop, so the + * embedder sweeps. Calls go first: one timing out usually resolves + * the park it was made for, and being told AccessDenied says more to + * that caller than a bare timeout. */ +int link_connection_expire(link_connection_t *conn, unsigned int age_ms) +{ + struct link_bus *bus; + uint64_t now; + int i, live = 0; + + if (!conn || !conn->bus) + return 0; + + bus = conn->bus; + now = __now_ms(); + for (i = 0; i < LINK_PENDING_CAP; i++) { + link_reply_cb_t cb; + void *userdata; + + if (!bus->pending[i].used) + continue; + + if (now - bus->pending[i].stamp < age_ms) { + live++; + continue; + } + + cb = bus->pending[i].cb; + userdata = bus->pending[i].userdata; + bus->pending[i].used = 0; + + __dbg("timed out call serial %u, no reply", bus->pending[i].serial); + if (cb) + cb(conn, NULL, userdata); + } + + return live + __dispatch_expire_parked(conn, age_ms); +} + void link_connection_close(link_connection_t *conn) { size_t i; diff --git a/libink/dispatch.c b/libink/dispatch.c index c79a9b6a..5597407d 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -387,9 +387,10 @@ static struct link_parked *park(link_connection_t *conn, const uint8_t *frame, if (i == LINK_PARKED_CAP) return NULL; - bus->parked[i].tok = ++bus->next_tok; - bus->parked[i].conn = conn; - bus->parked[i].len = len; + bus->parked[i].tok = ++bus->next_tok; + bus->parked[i].conn = conn; + bus->parked[i].stamp = __now_ms(); + bus->parked[i].len = len; memcpy(bus->parked[i].buf, frame, len); *tok = bus->parked[i].tok; @@ -402,6 +403,51 @@ static void unpark(struct link_parked *p) p->conn = NULL; } +/* A resolver that answers late, or never, would otherwise hold both + * the slot and the caller forever. Returns how many are still parked; + * link_connection_expire() is what calls this. */ +int __dispatch_expire_parked(link_connection_t *conn, unsigned int age_ms) +{ + uint64_t now; + int i, live = 0; + + if (!conn->bus) + return 0; + + now = __now_ms(); + for (i = 0; i < LINK_PARKED_CAP; i++) { + struct link_parked *p = &conn->bus->parked[i]; + uint8_t buf[LINK_PARKED_MSG_MAX]; + struct link_msg msg; + size_t len; + + if (!p->tok) + continue; + + if (now - p->stamp < age_ms) { + live++; + continue; + } + + /* Free the slot before replying, as link_uid_resolved() + * does: the send path must not find it still parked. */ + len = p->len; + memcpy(buf, p->buf, len); + unpark(p); + + if (__msg_parse(buf, len, &msg) <= 0) + continue; + + __dbg("timed out %s, nobody said who the caller was", + msg.member ? msg.member : "call"); + (void)__send_error(conn, &msg, + "org.freedesktop.DBus.Error.TimedOut", + "Timed out identifying the caller"); + } + + return live; +} + void __dispatch_forget_conn(link_connection_t *conn) { struct link_bus *bus = conn->bus; diff --git a/libink/internal.h b/libink/internal.h index 6d6aaa75..e71fcf00 100644 --- a/libink/internal.h +++ b/libink/internal.h @@ -62,10 +62,12 @@ TAILQ_HEAD(link_object_list, link_object); /* An inbound method call held while we find out who sent it. The * message is copied because rxbuf is reused as soon as we return to * the read loop. `tok` is the handle the resolver answers with, and - * zero when the slot is free. */ + * zero when the slot is free. `stamp` is when it was parked, for + * link_connection_expire(). */ struct link_parked { link_authz_t tok; link_connection_t *conn; + uint64_t stamp; size_t len; uint8_t buf[LINK_PARKED_MSG_MAX]; }; @@ -86,6 +88,7 @@ struct link_bus { struct { int used; uint32_t serial; + uint64_t stamp; /* for link_connection_expire() */ link_reply_cb_t cb; void *userdata; } pending[LINK_PENDING_CAP]; @@ -177,9 +180,11 @@ void __log(const char *func, const char *fmt, ...) __attribute__((format(printf, 2, 3))); #define __dbg(fmt, ...) __log(__func__, fmt, ##__VA_ARGS__) -/* io.c — shared EINTR-resilient I/O loops. */ +/* io.c — shared EINTR-resilient I/O loops, and the clock the expiry + * sweeps measure against. */ int __io_write_all(int fd, const void *buf, size_t len); int __io_read_full(int fd, void *buf, size_t len); +uint64_t __now_ms(void); /* auth.c */ int __auth_process(link_connection_t *conn); @@ -193,6 +198,7 @@ void __bus_free(link_connection_t *conn); /* dispatch.c */ int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen); void __dispatch_forget_conn(link_connection_t *conn); +int __dispatch_expire_parked(link_connection_t *conn, unsigned int age_ms); 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, diff --git a/libink/io.c b/libink/io.c index 7f95eafc..2d96045e 100644 --- a/libink/io.c +++ b/libink/io.c @@ -5,15 +5,30 @@ * read_full. On any other error they return -1 with an unknown * number of bytes already transferred. * + * Also the clock the expiry sweeps measure against: monotonic, so a + * step in wall time cannot make a call look older or younger than it + * is. + * * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ #include +#include #include #include "internal.h" +uint64_t __now_ms(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + return 0; + + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)(ts.tv_nsec / 1000000); +} + int __io_write_all(int fd, const void *buf, size_t len) { const char *p = buf; diff --git a/libink/link.h b/libink/link.h index f81a804b..40f6f4b9 100644 --- a/libink/link.h +++ b/libink/link.h @@ -164,6 +164,18 @@ int link_connection_call(link_connection_t *conn, const char *destination, link_reply_cb_t cb, void *userdata, const char *signature, ...); +/* Nothing in libink runs a clock, it has no event loop, so calls that + * go unanswered in either direction are the embedder's to time out. + * This drops anything held longer than `age_ms` on one connection: a + * park whose resolver never answered, which leaves its caller + * org.freedesktop.DBus.Error.TimedOut, and a call whose reply never + * came, whose callback runs once with a NULL reply exactly as a + * dropped connection would. + * + * Returns how many are still outstanding, so a sweep can stop + * rearming once nothing is left. */ +int link_connection_expire(link_connection_t *conn, unsigned int age_ms); + /* ---------- server / connection lifecycle ---------- */ /* Bind a listening socket at `path` with file mode `mode`, e.g. 0660 diff --git a/src/dbus.c b/src/dbus.c index 2a610688..8f18f1f0 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -106,6 +106,50 @@ static void peer_reap(void *arg) * stopped, so nothing touches it in the meantime. */ static struct wq reap_work = { .cb = peer_reap, .delay = 10 }; +/* + * Nothing in libink can time itself out, it has no event loop, so the + * deadline for a parked call and for a call we made on the broker is + * ours to keep. The sweep only runs while something is outstanding: + * expire_arm() starts it, and it stops rearming as soon as nothing is + * left, so a system that never talks to a broker never wakes up for + * this. + */ +#define DBUS_CALL_TIMEOUT_MS 5000 +#define DBUS_SWEEP_MS 1000 + +static void expire_sweep(void *arg); +static struct wq expire_work = { .cb = expire_sweep, .delay = DBUS_SWEEP_MS }; +static int expire_armed; + +/* Idempotent: several parks in one turn of the loop share one sweep. */ +static void expire_arm(void) +{ + if (expire_armed) + return; + if (!schedule_work(&expire_work)) + expire_armed = 1; +} + +static void expire_sweep(void *arg) +{ + struct peer *p, *tmp; + int live = 0; + + (void)arg; + expire_armed = 0; + + /* _SAFE because expiring a call runs its callback, and a + * callback that ends up writing to a peer can drop it, which + * unlinks it from this very list. */ + TAILQ_FOREACH_SAFE(p, &peers, link, tmp) { + if (!p->dead) + live += link_connection_expire(p->conn, DBUS_CALL_TIMEOUT_MS); + } + + if (live) + expire_arm(); +} + /* * A peer can be dropped from inside its own read loop: a handler emits * a signal, the write to this very peer fails, and dbus_emit_signal() @@ -1410,6 +1454,10 @@ static int sysbus_uid_resolver(link_connection_t *conn, const char *sender, return -1; } + /* Both the call and the park it belongs to now have a deadline + * to answer by, so make sure something is watching the clock. */ + expire_arm(); + return 1; /* parked; uid_reply_cb() answers */ } From bb43b5670c231d88094dc099759b794a49e561e7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 11:28:55 +0200 Subject: [PATCH 18/22] libink: mark the big-endian gap where it is decided Only the file header said we refuse 'B' messages, and nobody reads a header comment when they are looking at why a parse failed. Put it at the check, with an XXX so it turns up in a grep for known gaps. Signed-off-by: Joachim Wiberg --- libink/proto.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libink/proto.c b/libink/proto.c index 96e96cf5..15068f14 100644 --- a/libink/proto.c +++ b/libink/proto.c @@ -85,6 +85,9 @@ ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out) if (len < HDR_FIXED_SIZE) return 0; + /* XXX: a big-endian sender is refused rather than byte-swapped. + * Every conforming client on the platforms Finit targets sends + * 'l', so the swap paths have not been worth writing. */ if (buf[0] != 'l') { errno = EPROTO; return -1; From f3c73f11922ca4b837b1a0eaf0a2c3ebb6815094 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 11:28:55 +0200 Subject: [PATCH 19/22] doc: Properties.Set is absent by design, not pending "Set not yet implemented" reads as a promise. Finit exposes no writable property and has no use for one: everything a caller might want to change is a Manager1 method, where the authorization lives. Signed-off-by: Joachim Wiberg --- doc/dbus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dbus.md b/doc/dbus.md index 15118946..e57898a2 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -70,7 +70,7 @@ Every node implements the usual stock interfaces: | `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` | | `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | | `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description | -| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | +| `org.freedesktop.DBus.Properties` | `Get`, `GetAll`; nothing is writable | Note: `Hello`, `AddMatch`, and `RemoveMatch` are answered on the canonical `/org/freedesktop/DBus` object only, as per the D-Bus specification. From 5cc41b3ba438d4ddc4287aac18882e4c95126874 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 11:39:41 +0200 Subject: [PATCH 20/22] libink: take the match keys clients send without asking A rule carrying sender, destination, or eavesdrop was refused whole, and a peer whose AddMatch fails gets no signals at all. That is a poor trade for keys clients attach as a matter of course: better a filter wider than asked for than a subscription that never happened. They are accepted and ignored rather than honoured. Widening costs nothing here since Finit is the only sender on this bus, and what it emits through the match table is state any peer that got this far may already read. argN and argNpath still take the whole rule down. Honouring them means parsing message bodies, and nothing asks for them yet. Signed-off-by: Joachim Wiberg --- libink/match.c | 21 ++++++++++++++++++++- test/dbus-bus.sh | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/libink/match.c b/libink/match.c index add721b4..eaaa87f5 100644 --- a/libink/match.c +++ b/libink/match.c @@ -7,6 +7,14 @@ * keys cause the whole rule to be rejected so a peer learns its * filter didn't take, rather than silently receiving everything. * + * The exceptions are sender, destination, and eavesdrop, which are + * accepted and then ignored. Clients send them as a matter of + * course, and refusing the rule leaves such a peer with no signals at + * all, which serves it far worse than a filter wider than it asked + * for. Widening is safe here: Finit is the only sender on this bus, + * and everything it emits through the match table is state any peer + * that reached the bus may already read. + * * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ @@ -103,7 +111,18 @@ struct link_match *__match_parse(const char *rule) 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 { + else if (!strcmp(key, "sender") || + !strcmp(key, "destination") || + !strcmp(key, "eavesdrop")) { + /* Understood well enough to accept, see above. */ + free(key); + free(value); + continue; + } else { + /* XXX: argN and argNpath land here, so a rule + * using them takes nothing rather than too + * much. They narrow on body contents, which + * means parsing the body to honour them. */ free(key); free(value); goto bad; diff --git a/test/dbus-bus.sh b/test/dbus-bus.sh index 7bd53704..57a21da8 100755 --- a/test/dbus-bus.sh +++ b/test/dbus-bus.sh @@ -54,6 +54,26 @@ case "$(cat /tmp/dbus-match.out)" in *) fail "Unexpected reply: $(cat /tmp/dbus-match.out)" ;; esac +# A client that sends these must still get its signals: refusing the +# rule would leave it with none at all. The sender here is deliberately +# wrong, so a delivered signal proves the key was ignored and not +# quietly honoured. +say "AddMatch accepts, and ignores, sender/destination/eavesdrop" +rm -f /tmp/dbus-ignored.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',sender='org.freedesktop.DBus',destination=':1.99',eavesdrop='false',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-ignored.out 2>&1 ) & +ign_pid=$! +sleep 0.5 +texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "dbus-ignored-keys" >/dev/null \ + || fail "Cond1.Set returned non-zero" +set +e +wait "$ign_pid" +ign_rc=$? +set -e +assert "Signal still delivered through the wider rule (rc=$ign_rc)" "$ign_rc" -eq 0 + say "Unknown method on a Finit interface gets an org.freedesktop.DBus.Error.* reply" set +e texec "$CLIENT" unknown "$BUS" From c28acf21a17ea68d52b7520a92a0aec4cf8c01e8 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 12 Aug 2026 13:06:41 +0200 Subject: [PATCH 21/22] test: fuzz target for the message parser __msg_parse() turns bytes off a socket into pointers, before anything has vouched for the peer, and it is the only place in libink that does. It had no test of its own beyond whatever the other tests happened to send it, all of it well-formed. The target checks the parser's contract, not merely that it survived. A header field must point into the header field array, and terminate inside it, and the parse must never claim more bytes than it was handed. Crash-only would pass a parser that walked into the body and returned fields from there, since those bytes were handed over too. The expected bounds are derived from the raw header rather than from the parser, so the two have to agree independently. Every input is copied into an allocation sized to it first. Reading past the end of a roomy buffer stays inside the allocation and the sanitizer never sees it; against an exact one the same read is a fault, which is where the sharpest findings come from. Under libFuzzer it is an ordinary fuzz target and named files replay, which is how a find gets reproduced. With no arguments it runs a fixed sweep -- every truncation, every single-byte corruption, every value of the length that decides where the header ends, and seeded garbage -- so the suite covers the same contract on every build, without clang or a corpus in the tree. It takes 40 ms. CI fuzzes it properly on every pull request, keeps the crashers, and carries the corpus between runs so it reaches deeper over time than any single run can. Note that clang links the fuzzer runtime against the newest GCC tree it finds, so the libstdc++ headers have to match that one and not the default compiler, which is worth saying since installing the obvious package leaves you exactly where you started. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 68 +++++++++++ .gitignore | 5 + Makefile.am | 7 ++ doc/build.md | 54 +++++++++ test/Makefile.am | 2 + test/fuzz-msg-parse.sh | 20 ++++ test/src/Makefile.am | 5 + test/src/fuzz-msg-parse.c | 232 ++++++++++++++++++++++++++++++++++++ 8 files changed, 393 insertions(+) create mode 100755 test/fuzz-msg-parse.sh create mode 100644 test/src/fuzz-msg-parse.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37c56b74..a8ce1e36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,74 @@ concurrency: cancel-in-progress: true jobs: + fuzz: + # The message parser is the only place bytes off a socket become + # pointers, so give it a real fuzzer on every PR, not just the + # fixed sweep 'make check' runs. + name: fuzz msg-parse + runs-on: ubuntu-latest + if: github.event_name != 'push' || github.ref == 'refs/heads/master' + steps: + - name: Install dependencies + run: | + sudo apt-get -y update + sudo apt-get -y install pkg-config libconfuse-dev clang + # clang picks the newest gcc tree it finds and needs the + # matching libstdc++ headers to link the fuzzer runtime + sudo apt-get -y install libstdc++-14-dev || true + wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz + wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz + tar xf libuev-2.4.1.tar.xz + tar xf libite-2.6.2.tar.gz + (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) + (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) + sudo ldconfig + - uses: actions/checkout@v4 + - name: Configure + run: | + ./autogen.sh + ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var + - name: Build fuzz target + run: | + clang -fsanitize=fuzzer,address -DLINK_FUZZ_LIBFUZZER -D_GNU_SOURCE \ + -I libink -I . -o fuzz-msg-parse \ + test/src/fuzz-msg-parse.c libink/*.c + # Restores the newest corpus and saves a fresh one, since a cache + # entry is immutable once written. Caches made on a branch are + # private to it, so the corpus that accumulates on master is what + # pull requests start from, rather than nothing. + - name: Restore corpus + uses: actions/cache@v4 + with: + path: .fuzz-corpus + key: fuzz-corpus-${{ github.run_id }} + restore-keys: fuzz-corpus- + - name: Fuzz + run: | + mkdir -p .fuzz-corpus + ./fuzz-msg-parse .fuzz-corpus -max_total_time=120 -max_len=4096 \ + -print_final_stats=1 + # Without this the corpus only ever grows, and most of what it + # accumulates reaches code some earlier input already reached. + - name: Minimise corpus + if: always() + run: | + mkdir -p .fuzz-corpus-min + ./fuzz-msg-parse -merge=1 .fuzz-corpus-min .fuzz-corpus + rm -rf .fuzz-corpus + mv .fuzz-corpus-min .fuzz-corpus + echo "corpus: $(ls .fuzz-corpus | wc -l) inputs" + - name: Upload crashers + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-crashers + path: | + crash-* + leak-* + timeout-* + if-no-files-found: ignore + build: # Verify we can build on latest Ubuntu with both gcc and clang name: ${{ matrix.compiler }} diff --git a/.gitignore b/.gitignore index d30aefef..5b943e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,8 @@ GTAGS # VS Code user settings .vscode + +# Fuzzing: the target is built by hand with clang, see doc/build.md, +# and the corpus it grows is a local artefact, not something to ship +/fuzz-msg-parse +/.fuzz-corpus/ diff --git a/Makefile.am b/Makefile.am index b3afffea..f5e82762 100644 --- a/Makefile.am +++ b/Makefile.am @@ -35,6 +35,13 @@ endif install-dev: @make -C src install-pkgincludeHEADERS +# The fuzz target is built by hand with clang (doc/build.md), so it is +# not in any _PROGRAMS and nothing else would clear it or the corpus +# it grows. +distclean-local: + -rm -f $(top_builddir)/fuzz-msg-parse + -rm -rf $(top_builddir)/.fuzz-corpus + # Target to run when building a release release: distcheck @for file in $(DIST_ARCHIVES); do \ diff --git a/doc/build.md b/doc/build.md index 0d9cc5cf..c018ff47 100644 --- a/doc/build.md +++ b/doc/build.md @@ -126,6 +126,60 @@ Linux config to: > `/etc/fstab`. +Testing +------- + +`make check` runs the test suite in a private namespace, so it is safe +on a running system. It needs `unshare` and, on Ubuntu, unprivileged +user namespaces enabled: + +```shell +sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 +make check +``` + +The D-Bus message parser is the only place in Finit where bytes off a +socket become pointers, so it also has a fuzz target. `make check` +runs it as a fixed sweep, which takes milliseconds and needs nothing +beyond the normal build. To fuzz it properly, build it with clang and +libFuzzer: + +```shell +clang -fsanitize=fuzzer,address -DLINK_FUZZ_LIBFUZZER -D_GNU_SOURCE \ + -I libink -I . -o fuzz-msg-parse \ + test/src/fuzz-msg-parse.c libink/*.c +mkdir -p .fuzz-corpus +./fuzz-msg-parse .fuzz-corpus -max_total_time=300 +``` + +Give it a corpus directory as above and it saves what it learns there, +so the next run picks up where this one left off instead of starting +cold. Nothing writes to it unless you name it: without the argument +libFuzzer keeps everything in memory and the run leaves only crashes +behind. `make distclean` clears the corpus and the target. + +Once a corpus has grown, most of it reaches code some earlier input +already reached. Minimise it: + +```shell +./fuzz-msg-parse -merge=1 .fuzz-corpus-min .fuzz-corpus +rm -rf .fuzz-corpus && mv .fuzz-corpus-min .fuzz-corpus +``` + +Run `./configure` first, the target needs the generated `config.h`. +If the link fails with `cannot find -lstdc++`, install the `libstdc++` +headers matching the newest GCC on the system, not the default one: +clang picks the newest tree it finds, and that is the one that needs +them. CI runs this target on every pull request, carrying its +corpus between runs so it reaches deeper over time. + +Feed a file back to the target to reproduce a find: + +```shell +./fuzz-msg-parse crash-3f2a... +``` + + Running ------- diff --git a/test/Makefile.am b/test/Makefile.am index 6e8cfd1a..82f80bb7 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -75,6 +75,7 @@ EXTRA_DIST += unexpected-restart.sh EXTRA_DIST += dbus-auth.sh EXTRA_DIST += dbus-authz.sh EXTRA_DIST += dbus-broker.sh +EXTRA_DIST += fuzz-msg-parse.sh EXTRA_DIST += dbus-bus.sh EXTRA_DIST += dbus-manager.sh EXTRA_DIST += dbus-service.sh @@ -145,6 +146,7 @@ TESTS += dbus-service.sh TESTS += dbus-cond.sh TESTS += dbus-initctl.sh TESTS += dbus-introspect.sh +TESTS += fuzz-msg-parse.sh # Needs the plugin to bring up the bus, not just the built-in one if BUILD_DBUS_PLUGIN TESTS += dbus-broker.sh diff --git a/test/fuzz-msg-parse.sh b/test/fuzz-msg-parse.sh new file mode 100755 index 00000000..6e18850e --- /dev/null +++ b/test/fuzz-msg-parse.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# libink: __msg_parse() against truncation, corruption, and garbage. +# +# Runs the fuzz target's fixed sweep, which is the same contract check +# libFuzzer drives, so the suite covers it on every build without +# needing clang. Anything it finds aborts, and the sanitizers CI +# builds with turn a stray read into a failure here rather than a +# puzzle on a target. + +set -eu + +TEST_DIR=$(dirname "$0") +DRIVER="$TEST_DIR/src/fuzz-msg-parse" + +[ -x "$DRIVER" ] || { + echo "fuzz-msg-parse not built, D-Bus support is off" + exit 77 +} + +exec "$DRIVER" diff --git a/test/src/Makefile.am b/test/src/Makefile.am index 6897b3ab..d907ff86 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -14,4 +14,9 @@ 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 + +noinst_PROGRAMS += fuzz-msg-parse +fuzz_msg_parse_SOURCES = fuzz-msg-parse.c +fuzz_msg_parse_CPPFLAGS = -D_GNU_SOURCE -I$(top_srcdir)/libink -I$(top_builddir) +fuzz_msg_parse_LDADD = $(top_builddir)/libink/libink.la endif diff --git a/test/src/fuzz-msg-parse.c b/test/src/fuzz-msg-parse.c new file mode 100644 index 00000000..e9cb02c9 --- /dev/null +++ b/test/src/fuzz-msg-parse.c @@ -0,0 +1,232 @@ +/* libink — fuzz target for __msg_parse(). + * + * __msg_parse() is the one place where bytes off a socket become + * pointers, before any authentication has vouched for the peer. It + * hands back borrowed pointers into the caller's buffer, so "did not + * crash" is too weak a pass: a field that points outside the header it + * was supposed to come from, or a string with no terminator inside it, + * is a bug the caller hits later and somewhere else. Every input is + * checked against that contract here. + * + * Built two ways. With libFuzzer (clang -fsanitize=fuzzer,address + * -DLINK_FUZZ_LIBFUZZER) it is a normal fuzz target. Otherwise it + * gets the driver below: named files are replayed, which is how a + * crash found by the fuzzer is reproduced, and with no arguments it + * runs a fixed sweep so the suite exercises the same contract on + * every build without needing clang or a corpus in the tree. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include "internal.h" + +#define FRAME_MAX 512 +#define HDR_FIXED_SIZE 16 /* must agree with proto.c */ + +static void fail(const char *what, size_t size) +{ + fprintf(stderr, "fuzz-msg-parse: %s, on a %zu byte input\n", what, size); + abort(); +} + +/* A header field is read out of the header field array and nowhere + * else. Checking it against the whole buffer would be too generous: + * a parser that walked off the end of the fields and into the body + * would still be pointing at bytes it was handed, and pass. The + * bound is derived from the raw header here rather than taken from + * the parser, so the two have to agree independently. + * + * The string must also terminate inside that region, or whoever + * borrows it reads past what it was given. */ +static void check_str(const char *s, const uint8_t *base, size_t len, + size_t hdr_end, const char *what) +{ + const char *first = (const char *)base + HDR_FIXED_SIZE; + const char *last = (const char *)base + hdr_end; + size_t room; + + if (!s) + return; + if (s < first || s >= last) + fail(what, len); + + room = (size_t)(last - s); + if (strnlen(s, room) == room) + fail(what, len); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct link_msg m; + size_t hdr_end, body_off; + uint32_t fields_len; + ssize_t rc; + + rc = __msg_parse(data, size, &m); + if (rc <= 0) + return 0; /* need more, or refused: both fine */ + + /* Consuming more than it was handed would desynchronise the + * read loop and make it skip into the next message. */ + if ((size_t)rc > size) + fail("claimed more bytes than it was given", size); + + /* Only 'l' messages parse, so reading the length this way is + * safe, and rc > 0 means the fixed header was all there. */ + memcpy(&fields_len, data + 12, sizeof(fields_len)); + hdr_end = HDR_FIXED_SIZE + fields_len; + body_off = (hdr_end + 7) & ~(size_t)7; + if (hdr_end > size || body_off > size) + fail("accepted a header longer than the message", size); + + check_str(m.path, data, size, hdr_end, "path outside the header"); + check_str(m.interface, data, size, hdr_end, "interface outside the header"); + check_str(m.member, data, size, hdr_end, "member outside the header"); + check_str(m.error_name, data, size, hdr_end, "error name outside the header"); + check_str(m.destination, data, size, hdr_end, "destination outside the header"); + check_str(m.sender, data, size, hdr_end, "sender outside the header"); + check_str(m.signature, data, size, hdr_end, "signature outside the header"); + + if (m.body_avail) { + if (m.body != data + body_off) + fail("body does not start where the header ends", size); + if (m.body_avail > size - body_off) + fail("body runs past the buffer", size); + } + + return 0; +} + +#ifndef LINK_FUZZ_LIBFUZZER +/* Hand the parser a buffer sized to the input and nothing more. + * Reading past the end of a roomy array stays inside the allocation + * and the sanitizer never sees it; against an exact allocation the + * same read is a fault. This is what libFuzzer does, and the reason + * it finds things a fixed buffer cannot. */ +static void run(const uint8_t *data, size_t size) +{ + uint8_t *exact = malloc(size ? size : 1); + + if (!exact) { + fprintf(stderr, "fuzz-msg-parse: out of memory\n"); + abort(); + } + memcpy(exact, data, size); + LLVMFuzzerTestOneInput(exact, size); + free(exact); +} + +/* Deterministic, so a failure reproduces from the same build. */ +static uint32_t prng(uint32_t *state) +{ + uint32_t x = *state; + + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + + return *state = x; +} + +static int sweep(void) +{ + uint8_t frame[FRAME_MAX], copy[FRAME_MAX]; + static const uint8_t poke[] = { 0x00, 0x01, 0x7f, 0x80, 0xff }; + uint32_t state = 0x1234abcd; + ssize_t len; + size_t i, j, n; + + len = __msg_build_method_call(frame, sizeof(frame), 1, + "/org/finit/manager", "org.finit.Manager1", + "ListServices", NULL, NULL, 0); + if (len <= 0) { + fprintf(stderr, "fuzz-msg-parse: cannot build a reference frame\n"); + return 1; + } + + /* Every prefix: the read loop hands over whatever arrived, and + * a short read must come back "need more", never a parse. */ + for (i = 0; i <= (size_t)len; i++) + run(frame, i); + + /* One byte wrong, everywhere, with the values that flip a + * length or an offset furthest. */ + for (i = 0; i < (size_t)len; i++) { + for (j = 0; j < sizeof(poke); j++) { + memcpy(copy, frame, (size_t)len); + copy[i] = poke[j]; + run(copy, (size_t)len); + } + } + + /* fields_len decides where the header walk stops, so it is the + * byte that decides whether a field is read from the header or + * from somewhere else. Walk it across the whole frame, and + * past it, at every truncation the read loop could hand over. */ + for (i = 0; i <= (size_t)len + 8; i++) { + uint32_t fl = (uint32_t)i; + + memcpy(copy, frame, (size_t)len); + memcpy(copy + 12, &fl, sizeof(fl)); + for (j = 0; j <= (size_t)len; j++) + run(copy, j); + } + + /* And input that was never a message to begin with. */ + for (n = 0; n < 20000; n++) { + size_t sz = prng(&state) % (FRAME_MAX + 1); + + for (i = 0; i < sz; i++) + copy[i] = (uint8_t)prng(&state); + + /* Half of them keep a plausible header, so the parser + * gets past its first checks and into field walking. */ + if (sz >= 16 && (n & 1)) { + copy[0] = 'l'; + copy[3] = LINK_PROTOCOL_VERSION; + } + run(copy, sz); + } + + return 0; +} + +static int replay(const char *path) +{ + uint8_t buf[64 * 1024]; + size_t len; + FILE *fp; + + fp = fopen(path, "rb"); + if (!fp) { + perror(path); + return 1; + } + len = fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + run(buf, len); + return 0; +} + +int main(int argc, char *argv[]) +{ + int i, rc = 0; + + if (argc < 2) + return sweep(); + + for (i = 1; i < argc; i++) + rc |= replay(argv[i]); + + return rc; +} +#endif /* !LINK_FUZZ_LIBFUZZER */ From e62b463852be05042c83220cd0311fa1b79331ac Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:10:23 +0200 Subject: [PATCH 22/22] .github: bump actions to node24 Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 15 ++++++--------- .github/workflows/coverity.yml | 6 +++--- .github/workflows/docs.yml | 10 +++++----- .github/workflows/release.yml | 4 ++-- .github/workflows/weekly.yml | 6 +++--- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8ce1e36..e3a148d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,7 @@ concurrency: jobs: fuzz: - # The message parser is the only place bytes off a socket become - # pointers, so give it a real fuzzer on every PR, not just the - # fixed sweep 'make check' runs. - name: fuzz msg-parse + name: fuzz runs-on: ubuntu-latest if: github.event_name != 'push' || github.ref == 'refs/heads/master' steps: @@ -40,7 +37,7 @@ jobs: (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) sudo ldconfig - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Configure run: | ./autogen.sh @@ -55,7 +52,7 @@ jobs: # private to it, so the corpus that accumulates on master is what # pull requests start from, rather than nothing. - name: Restore corpus - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: .fuzz-corpus key: fuzz-corpus-${{ github.run_id }} @@ -77,7 +74,7 @@ jobs: echo "corpus: $(ls .fuzz-corpus | wc -l) inputs" - name: Upload crashers if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: fuzz-crashers path: | @@ -110,7 +107,7 @@ jobs: (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) sudo ldconfig - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Static Finit run: | ./autogen.sh @@ -148,7 +145,7 @@ jobs: make -j1 check || (cat test/test-suite.log; false) - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: finit-test-${{ matrix.compiler }} path: test/*.log diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index f9b51781..d3090c99 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -19,7 +19,7 @@ jobs: coverity: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Fetch latest Coverity Scan MD5 id: var env: @@ -29,7 +29,7 @@ jobs: --post-data "token=$TOKEN&project=${COVERITY_PROJ}&md5=1" \ -O coverity-latest.tar.gz.md5 echo "md5=$(cat coverity-latest.tar.gz.md5)" | tee -a $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v6 id: cache with: path: coverity-latest.tar.gz @@ -86,7 +86,7 @@ jobs: --form description="${PROJECT_NAME} $(git rev-parse HEAD)" \ https://scan.coverity.com/builds?project=${COVERITY_PROJ} - name: Upload build.log - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverity-build.log path: cov-int/build-log.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 28e98055..ee3cb619 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 # Needed for git-revision-date-localized plugin @@ -42,7 +42,7 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -58,7 +58,7 @@ jobs: run: mkdocs build --clean - name: Upload site artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: site path: site/ @@ -72,13 +72,13 @@ jobs: steps: - name: Download site artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: site path: site/ - name: Checkout pages repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: finit-project/finit-project.github.io path: pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0bfcbd4..eb22a67e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Installing dependencies ... run: | sudo apt-get -y update @@ -64,7 +64,7 @@ jobs: mv ../*.tar.* artifacts/ - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: finit-release-test-logs path: finit-*/_build/sub/test/*.log diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index d2d8cbe4..b8b31da6 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -16,7 +16,7 @@ jobs: outputs: run: ${{ steps.schedule.outputs.run }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Check for any commits since last week id: schedule run: | @@ -42,7 +42,7 @@ jobs: (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) sudo ldconfig - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Base run run: | ./autogen.sh @@ -61,7 +61,7 @@ jobs: make -j1 distcheck || (cat $dir/test/test-suite.log; false) - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: finit-weekly-test-logs path: finit-*/_build/sub/test/*.log