From 0a269f329814741d631e8c78880726c6fc162e6f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:14 +0200 Subject: [PATCH] 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 @@ restart: if (sm_in_reload()) break; - service_reload(svc); + service_reload_apply(svc); } svc_mark_clean(svc); @@ -3422,7 +3474,7 @@ restart: 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; +}