From cd7b5370c03ebe61ff0e9c1b563024a8f3718bf2 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 13 May 2026 12:07:33 +0200 Subject: [PATCH 01/22] libink: brokerless D-Bus server library, for #396 First step toward native D-Bus support. libink is a small server library written from scratch -- no broker dependency, Finit always listens on its own socket at /run/finit/bus. Lives at top-level libink/ as a standalone-shape library, no Finit headers leak in. This drop has the listening socket, SO_PEERCRED capture, and the SASL AUTH EXTERNAL handshake. Marshaller, dispatch and the object tree follow. Drive-by: setup-sysroot.sh now points ldd at src/.libs/finit (the real ELF) -- libtool wraps src/finit when in-tree .la files link in, and the shell wrapper has no DT_NEEDED for sysroot.mk to follow. SUBDIRS reordered so libink builds before src. Signed-off-by: Joachim Wiberg --- Makefile.am | 13 +- configure.ac | 10 ++ libink/.gitignore | 8 ++ libink/Makefile.am | 15 +++ libink/auth.c | 231 ++++++++++++++++++++++++++++++++++++ libink/connection.c | 44 +++++++ libink/ink-internal.h | 45 +++++++ libink/ink.h | 63 ++++++++++ libink/libink.pc.in | 10 ++ libink/server.c | 143 ++++++++++++++++++++++ src/Makefile.am | 7 ++ src/dbus.c | 174 +++++++++++++++++++++++++++ src/finit.c | 5 + src/finit.h | 1 + src/private.h | 5 + test/Makefile.am | 4 + test/check.sh | 4 +- test/dbus-auth.sh | 65 ++++++++++ test/setup-sysroot.sh | 15 ++- test/src/.gitignore | 1 + test/src/Makefile.am | 5 + test/src/dbus-auth-client.c | 149 +++++++++++++++++++++++ 22 files changed, 1013 insertions(+), 4 deletions(-) create mode 100644 libink/.gitignore create mode 100644 libink/Makefile.am create mode 100644 libink/auth.c create mode 100644 libink/connection.c create mode 100644 libink/ink-internal.h create mode 100644 libink/ink.h create mode 100644 libink/libink.pc.in 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 7b0943ac..d165a9b2 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 @@ -88,6 +89,10 @@ AC_ARG_ENABLE(logrotate, AS_HELP_STRING([--disable-logrotate], [Disable built-in rotation of /var/log/wtmp]),,[ enable_logrotate=yes]) +AC_ARG_ENABLE(dbus, + AS_HELP_STRING([--disable-dbus], [Disable D-Bus support (libink + Finit object tree)]),,[ + enable_dbus=yes]) + AC_ARG_ENABLE(doc, AS_HELP_STRING([--disable-doc], [Disable build and install of doc/ section]),,[ enable_doc=yes]) @@ -237,6 +242,10 @@ AS_IF([test "x$enable_rescue" != "xno"], [ AM_CONDITIONAL(LOGROTATE, [test "x$enable_logrotate" = "xyes"]) +AS_IF([test "x$enable_dbus" = "xyes"], [ + AC_DEFINE(HAVE_DBUS, 1, [Build D-Bus support via libink])]) +AM_CONDITIONAL(DBUS, [test "x$enable_dbus" = "xyes"]) + ### With features ############################################################################## AS_IF([test "x$bash_dir" = "xyes"], [ PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0], @@ -432,6 +441,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..1292d9c2 --- /dev/null +++ b/libink/Makefile.am @@ -0,0 +1,15 @@ +# libink — D-Bus server library born inside Finit +lib_LTLIBRARIES = libink.la +libink_la_SOURCES = server.c auth.c connection.c \ + ink-internal.h +libink_la_LDFLAGS = -version-info 0:0:0 +libink_la_CPPFLAGS = -D_GNU_SOURCE +libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 + +# pkg-config support +pkgconfigdir = $(libdir)/pkgconfig +pkgconfig_DATA = libink.pc + +# Public header installs to $(includedir)/ink/ink.h +inkdir = $(includedir)/ink +ink_HEADERS = ink.h diff --git a/libink/auth.c b/libink/auth.c new file mode 100644 index 00000000..660c5371 --- /dev/null +++ b/libink/auth.c @@ -0,0 +1,231 @@ +/* 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 "ink-internal.h" + +static const char rejected_ext[] = "REJECTED EXTERNAL\r\n"; + +static int write_all(int fd, const char *buf, size_t len) +{ + while (len > 0) { + ssize_t n = write(fd, buf, len); + + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + buf += n; + len -= (size_t)n; + } + return 0; +} + +static int reply(int fd, const char *line) +{ + return write_all(fd, line, strlen(line)); +} + +static int reject(ink_connection_t *conn) +{ + return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1); +} + +void ink__auth_generate_guid(char out[33]) +{ + static const char hex[] = "0123456789abcdef"; + 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[raw[i] >> 4]; + out[i * 2 + 1] = hex[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(ink_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 = INK_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(ink_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 ink__auth_process(ink_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 == INK_AUTH_NUL) { + if (buf[0] != 0x00) { + conn->auth = INK_AUTH_FAILED; + return -1; + } + off = 1; + conn->auth = INK_AUTH_LINE; + } + + if (conn->auth == INK_AUTH_DONE) { + /* Post-BEGIN bytes are discarded until the marshaller lands. */ + return 0; + } + + if (conn->auth == INK_AUTH_LINE) { + size_t take = (size_t)n - off; + char line[INK_AUTH_LINEBUF_SIZE]; + size_t linelen; + + if (conn->linelen + take > sizeof(conn->linebuf)) { + conn->auth = INK_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 != INK_AUTH_LINE) + break; + } + } + + return 0; +} diff --git a/libink/connection.c b/libink/connection.c new file mode 100644 index 00000000..5892b22e --- /dev/null +++ b/libink/connection.c @@ -0,0 +1,44 @@ +/* libink — per-connection lifecycle and dispatch entry point + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include "ink-internal.h" + +int ink_connection_get_fd(const ink_connection_t *conn) +{ + return conn ? conn->fd : -1; +} + +uid_t ink_connection_get_uid(const ink_connection_t *conn) +{ + return conn ? conn->peer_uid : (uid_t)-1; +} + +void ink_connection_close(ink_connection_t *conn) +{ + if (!conn) + return; + + if (conn->fd >= 0) + close(conn->fd); + free(conn); +} + +int ink_connection_process(ink_connection_t *conn) +{ + if (!conn) { + errno = EINVAL; + return -1; + } + + if (conn->auth == INK_AUTH_FAILED) + return -1; + + return ink__auth_process(conn); +} diff --git a/libink/ink-internal.h b/libink/ink-internal.h new file mode 100644 index 00000000..67401ec8 --- /dev/null +++ b/libink/ink-internal.h @@ -0,0 +1,45 @@ +/* libink internal types — not for external consumers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_INK_INTERNAL_H_ +#define LIBINK_INK_INTERNAL_H_ + +#include + +#include "ink.h" + +typedef enum { + INK_AUTH_NUL = 0, /* waiting for the leading nul byte */ + INK_AUTH_LINE, /* line-mode SASL exchange */ + INK_AUTH_DONE, /* BEGIN received, binary mode */ + INK_AUTH_FAILED, /* terminal: drop the connection */ +} ink_auth_state_t; + +/* AF_UNIX sun_path is 108 bytes on Linux, 104 on BSD — bound to the + * Linux value, which is the maximum we'll ever encounter. */ +#define INK_PATH_MAX 108 +#define INK_AUTH_LINEBUF_SIZE 256 + +struct ink_server { + int fd; + char path[INK_PATH_MAX]; +}; + +struct ink_connection { + int fd; + uid_t peer_uid; + + /* Server-assigned GUID, 32 hex chars + nul. */ + char guid[33]; + + ink_auth_state_t auth; + char linebuf[INK_AUTH_LINEBUF_SIZE]; + size_t linelen; +}; + +int ink__auth_process(ink_connection_t *conn); +void ink__auth_generate_guid(char out[33]); + +#endif /* LIBINK_INK_INTERNAL_H_ */ diff --git a/libink/ink.h b/libink/ink.h new file mode 100644 index 00000000..275fc0a1 --- /dev/null +++ b/libink/ink.h @@ -0,0 +1,63 @@ +/* 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_INK_H_ +#define LIBINK_INK_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ink_server ink_server_t; +typedef struct ink_connection ink_connection_t; + +/* Server lifecycle. path is the AF_UNIX socket pathname to bind. */ +int ink_server_new (ink_server_t **server, const char *path); +void ink_server_free (ink_server_t *server); +int ink_server_get_fd(const ink_server_t *server); + +/* Accept a pending connection from the listening socket. On success + * returns 0 and stores a new connection in *conn. Returns -1 with + * errno set if accept() failed or memory could not be allocated. + * Callers should use ink_connection_get_fd() to register the new + * connection with their event loop. */ +int ink_server_accept(ink_server_t *server, ink_connection_t **conn); + +/* Per-connection. */ +int ink_connection_get_fd (const ink_connection_t *conn); +uid_t ink_connection_get_uid(const ink_connection_t *conn); + +/* Drive the connection state machine when its fd is readable. Returns + * 0 on success (keep watching), -1 on error or peer close (caller + * should drop the connection via ink_connection_close()). */ +int ink_connection_process(ink_connection_t *conn); + +void ink_connection_close (ink_connection_t *conn); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBINK_INK_H_ */ 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/server.c b/libink/server.c new file mode 100644 index 00000000..42dee5e9 --- /dev/null +++ b/libink/server.c @@ -0,0 +1,143 @@ +/* libink — listening socket, accept, peer-credential capture + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "ink-internal.h" + +static void close_save_errno(int fd) +{ + int saved = errno; + close(fd); + errno = saved; +} + +int ink_server_new(ink_server_t **out, const char *path) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + ink_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 >= INK_PATH_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + srv = calloc(1, sizeof(*srv)); + if (!srv) + return -1; + + 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 ink_server_free(ink_server_t *srv) +{ + if (!srv) + return; + + if (srv->fd >= 0) + close(srv->fd); + if (srv->path[0]) + (void)unlink(srv->path); + free(srv); +} + +int ink_server_get_fd(const ink_server_t *srv) +{ + if (!srv) + return -1; + + return srv->fd; +} + +int ink_server_accept(ink_server_t *srv, ink_connection_t **out) +{ + struct ucred cred = { 0 }; + socklen_t credlen = sizeof(cred); + ink_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 = INK_AUTH_NUL; + + if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0) + conn->peer_uid = cred.uid; + else + conn->peer_uid = (uid_t)-1; + + ink__auth_generate_guid(conn->guid); + + *out = conn; + return 0; +} diff --git a/src/Makefile.am b/src/Makefile.am index bd126600..aec3af55 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -80,6 +80,9 @@ finit_SOURCES = api.c cgroup.c cgroup.h \ if LOGROTATE finit_SOURCES += logrotate.c endif +if DBUS +finit_SOURCES += dbus.c +endif pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \ plugin.h svc.h service.h @@ -93,6 +96,10 @@ finit_LDADD += ../plugins/libplug.la else finit_LDADD += -ldl endif +if DBUS +finit_CPPFLAGS += -I$(top_srcdir)/libink +finit_LDADD += $(top_builddir)/libink/libink.la +endif initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \ client.c client.h cond.c cond.h reboot.c \ diff --git a/src/dbus.c b/src/dbus.c new file mode 100644 index 00000000..d8498c67 --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,174 @@ +/* Finit-side glue between the event loop and libink. + * + * Owns the libink server, accepts new peers, and drives each peer's + * state machine when its fd becomes readable. Nothing in this file + * leaks finit-internal types into libink, by design: the boundary + * here is the prospective extraction line. + * + * 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 "ink.h" + +#include "finit.h" +#include "log.h" + +struct peer { + uev_t watcher; + ink_connection_t *conn; + TAILQ_ENTRY(peer) link; +}; + +/* Cap the per-process peer count. The socket is world-accessible by + * design (introspection should work for unprivileged users) so an + * unprivileged local actor could connect in a loop until EMFILE + * without this. */ +#define DBUS_MAX_PEERS 64 + +static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); +static ink_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); + ink_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 (ink_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 (;;) { + ink_connection_t *conn = NULL; + struct peer *p; + + if (ink_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); + ink_connection_close(conn); + continue; + } + + p = calloc(1, sizeof(*p)); + if (!p) { + ink_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, + ink_connection_get_fd(conn), UEV_READ)) { + err(1, "Failed registering D-Bus peer watcher"); + peer_drop(p); + } + } +} + +int dbus_init(uev_ctx_t *ctx) +{ + dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET); + + if (ink_server_new(&server, FINIT_BUS_SOCKET) < 0) { + err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET); + return 1; + } + + if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, + ink_server_get_fd(server), UEV_READ)) { + err(1, "Failed registering D-Bus accept watcher"); + ink_server_free(server); + server = NULL; + return 1; + } + + return 0; +} + +int dbus_exit(void) +{ + struct peer *p; + + uev_io_stop(&accept_watcher); + + while ((p = TAILQ_FIRST(&peers))) + peer_drop(p); + + if (server) { + ink_server_free(server); + server = NULL; + } + + return 0; +} + +#endif /* HAVE_DBUS */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/src/finit.c b/src/finit.c index 63df48a1..24e081c5 100644 --- a/src/finit.c +++ b/src/finit.c @@ -761,6 +761,11 @@ int main(int argc, char *argv[]) dbg("Starting initctl API responder ..."); api_init(&loop); +#ifdef HAVE_DBUS + dbg("Starting D-Bus listener ..."); + dbus_init(&loop); +#endif + dbg("Starting service interval monitor ..."); service_init(&loop); diff --git a/src/finit.h b/src/finit.h index 5bef212b..30657537 100644 --- a/src/finit.h +++ b/src/finit.h @@ -71,6 +71,7 @@ #define BUF_SIZE 4096 #define INIT_SOCKET _PATH_VARRUN "finit/socket" +#define FINIT_BUS_SOCKET _PATH_VARRUN "finit/bus" #define INIT_MAGIC 0x03091969 #define INIT_LEVEL 10 diff --git a/src/private.h b/src/private.h index 1612803b..aa831af8 100644 --- a/src/private.h +++ b/src/private.h @@ -45,6 +45,11 @@ 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); +#endif void conf_flush_events(void); void service_monitor (pid_t lost, int status); diff --git a/test/Makefile.am b/test/Makefile.am index da6eb905..420f40d1 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -62,6 +62,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; @@ -108,6 +109,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..a83651b8 --- /dev/null +++ b/test/dbus-auth.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Smoke test for libink AUTH EXTERNAL handshake over /run/finit/bus. +# +# Verifies that with --enable-dbus (the default), Finit opens its +# brokerless D-Bus socket and that the SASL-style AUTH EXTERNAL +# handshake completes for a peer claiming the right UID and is +# rejected for one claiming a wrong UID. + +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 "Verify socket permissions are 0666" +mode=$(texec stat -c %a "$BUS") +assert "Socket mode is 666 (got $mode)" "$mode" = "666" + +say "Happy path — claim correct UID (root inside namespace = 0)" +reply=$(texec "$CLIENT" "$BUS" 0) +assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK" + +# GUID must be 32 hex chars; a server blindly emitting "OK foo" would +# otherwise pass the prefix check above. +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 "Wrong UID is rejected" +# Claim uid 1, which does not match peer's real uid 0. Exit code 1 +# means "REJECTED" (the expected denial); 0 would be acceptance, +# 2 a transport error. Distinguish all three for a useful failure +# message. +set +e +wrong_reply=$(texec "$CLIENT" "$BUS" 1) +wrong_rc=$? +set -e +assert "Wrong UID rejected (rc=$wrong_rc, reply: $wrong_reply)" \ + "$wrong_rc" -eq 1 + +say "Two sequential connections both authenticate independently" +reply1=$(texec "$CLIENT" "$BUS" 0) +reply2=$(texec "$CLIENT" "$BUS" 0) +assert "First connection OK" "${reply1%% *}" = "OK" +assert "Second connection OK" "${reply2%% *}" = "OK" + +# The two GUIDs in the OK lines should differ; libink generates one +# per connection. This catches a regression where we accidentally +# share GUID state across peers. +guid1=${reply1#OK } +guid2=${reply2#OK } +assert "Per-connection GUIDs differ ($guid1 vs $guid2)" \ + "$guid1" != "$guid2" 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..0c12bb53 --- /dev/null +++ b/test/src/dbus-auth-client.c @@ -0,0 +1,149 @@ +/* Minimal D-Bus AUTH EXTERNAL client used by the libink smoke test. + * + * Usage: dbus-auth-client + * + * exit 0 — server reply began with "OK " + * exit 1 — server reply began with "REJECTED " (the expected denial) + * exit 2 — any other reply, short read, or transport error + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include + +static const char hex[] = "0123456789abcdef"; + +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; +} + +/* Read until '\n' or buffer-full. Returns line length excluding any + * trailing CR/LF, -1 on transport error / EOF / overflow. */ +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; +} + +int main(int argc, char *argv[]) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + char hexuid[32]; + char line[64]; + char reply[256]; + const char *path, *claimed; + size_t i, claimed_len, plen; + int fd, rc; + + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + path = argv[1]; + claimed = argv[2]; + + plen = strlen(path); + if (plen >= sizeof(sun.sun_path)) { + fprintf(stderr, "%s: path too long\n", argv[0]); + return 2; + } + + claimed_len = strlen(claimed); + if (claimed_len * 2 >= sizeof(hexuid)) { + fprintf(stderr, "%s: uid too long\n", argv[0]); + 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) { + perror("write"); + close(fd); + return 2; + } + + rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); + if (rc < 0 || (size_t)rc >= sizeof(line)) { + fprintf(stderr, "%s: auth line too long\n", argv[0]); + close(fd); + return 2; + } + if (write_all(fd, line, (size_t)rc) < 0) { + perror("write"); + close(fd); + return 2; + } + + if (read_line(fd, reply, sizeof(reply)) < 0) { + fprintf(stderr, "%s: short read or oversized reply\n", argv[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; +} From e6acdf130ef4a04e78924b03bba78d09d2868e24 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 14 May 2026 09:00:02 +0200 Subject: [PATCH 02/22] libink: binary protocol, dispatch, Manager1.ListServices The binary D-Bus message layer on top of AUTH EXTERNAL: header parser and builder, writer-side body marshaller (byte/bool/u32/ string/path/signature/arrays/structs), and the object-tree dispatcher. Built-in interfaces -- Hello, Peer.Ping, Peer.GetMachineId, Introspectable.Introspect -- run before user handlers. First Finit method lands too: Manager1.ListServices on /org/finit/manager, returning every loaded service's identity. Native little-endian only. Header field count and total message size capped so PID 1 isn't exposed to oversized inputs. Signed-off-by: Joachim Wiberg --- libink/Makefile.am | 5 +- libink/auth.c | 17 +- libink/builtin.c | 255 ++++++++++++++++++ libink/connection.c | 59 +++- libink/dispatch.c | 318 ++++++++++++++++++++++ libink/ink-internal.h | 90 +++++-- libink/ink.h | 75 +++++- libink/marshal.c | 168 ++++++++++++ libink/marshal.h | 47 ++++ libink/proto.c | 357 ++++++++++++++++++++++++ libink/proto.h | 97 +++++++ libink/server.c | 24 +- src/dbus.c | 64 ++++- test/dbus-auth.sh | 97 ++++--- test/src/dbus-auth-client.c | 523 ++++++++++++++++++++++++++++++++---- 15 files changed, 2062 insertions(+), 134 deletions(-) create mode 100644 libink/builtin.c create mode 100644 libink/dispatch.c create mode 100644 libink/marshal.c create mode 100644 libink/marshal.h create mode 100644 libink/proto.c create mode 100644 libink/proto.h diff --git a/libink/Makefile.am b/libink/Makefile.am index 1292d9c2..3b584928 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -1,9 +1,12 @@ # libink — D-Bus server library 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 \ ink-internal.h libink_la_LDFLAGS = -version-info 0:0:0 -libink_la_CPPFLAGS = -D_GNU_SOURCE +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 diff --git a/libink/auth.c b/libink/auth.c index 660c5371..a458b32f 100644 --- a/libink/auth.c +++ b/libink/auth.c @@ -202,11 +202,6 @@ int ink__auth_process(ink_connection_t *conn) conn->auth = INK_AUTH_LINE; } - if (conn->auth == INK_AUTH_DONE) { - /* Post-BEGIN bytes are discarded until the marshaller lands. */ - return 0; - } - if (conn->auth == INK_AUTH_LINE) { size_t take = (size_t)n - off; char line[INK_AUTH_LINEBUF_SIZE]; @@ -225,6 +220,18 @@ int ink__auth_process(ink_connection_t *conn) if (conn->auth != INK_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 == INK_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; diff --git a/libink/builtin.c b/libink/builtin.c new file mode 100644 index 00000000..521432a2 --- /dev/null +++ b/libink/builtin.c @@ -0,0 +1,255 @@ +/* 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 "ink-internal.h" + +/* ---------- helpers ---------- */ + +static int member_is(const struct ink_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(ink_connection_t *conn, const struct ink_msg *req, + const char *s) +{ + uint8_t body[INK_TX_BUF_SIZE - 512]; + struct ink_writer w; + ssize_t blen; + + ink__w_init(&w, body, sizeof(body)); + ink__w_string(&w, s); + blen = ink__w_finish(&w); + if (blen < 0) { + errno = EMSGSIZE; + return -1; + } + return ink__send_method_return(conn, req, "s", body, (size_t)blen); +} + +/* ---------- Hello ---------- */ + +static int handle_hello(ink_connection_t *conn, const struct ink_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(ink_connection_t *conn, const struct ink_msg *m) +{ + return ink__send_method_return(conn, m, NULL, NULL, 0); +} + +static int handle_get_machine_id(ink_connection_t *conn, const struct ink_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]) + ink__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 ink_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"); +} + +/* The user-side input/output signatures are flat strings ("ss", + * "a(ss)" etc.). The introspection emit above prints one + * per top-level type. For complex types like "a(soss)" each + * character produces an , which is wrong for `(` `)` `{` + * `}` `a` — but it's good enough for the simple "s", "u", "as" + * signatures we expose right now, and we can refine later when + * complex types appear. */ + +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(ink_connection_t *conn, const struct ink_msg *m) +{ + char xml[4096]; + struct xbuf x = { .buf = xml, .cap = sizeof(xml) }; + struct ink_object *o; + const char *path = m->path; + + xprintf(&x, + "\n" + "\n"); + + xprintf(&x, "%s", STANDARD_INTERFACES_XML); + + o = NULL; + { + struct ink_object *p; + + TAILQ_FOREACH(p, &conn->server->objects, link) { + if (strcmp(p->path, path) == 0) { + o = p; + break; + } + } + } + + if (o) { + struct ink_vtable_entry *e; + const ink_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 ink_object *p; + char prev_seg[INK_PATH_MAX] = { 0 }; + char seg [INK_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 ink__send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Introspection XML overflow"); + + return send_string_reply(conn, m, xml); +} + +/* ---------- entry point ---------- */ + +int ink__handle_builtin(ink_connection_t *conn, const struct ink_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.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/connection.c b/libink/connection.c index 5892b22e..5350076c 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -6,6 +6,7 @@ #include #include +#include #include #include "ink-internal.h" @@ -30,6 +31,32 @@ void ink_connection_close(ink_connection_t *conn) 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(ink_connection_t *conn) +{ + while (conn->rxlen > 0) { + struct ink_msg msg; + ssize_t consumed; + + consumed = ink__msg_parse(conn->rxbuf, conn->rxlen, &msg); + if (consumed == 0) + break; /* incomplete; wait for more bytes */ + if (consumed < 0) + return -1; + + if (ink__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 ink_connection_process(ink_connection_t *conn) { if (!conn) { @@ -40,5 +67,35 @@ int ink_connection_process(ink_connection_t *conn) if (conn->auth == INK_AUTH_FAILED) return -1; - return ink__auth_process(conn); + if (conn->auth != INK_AUTH_DONE) + return ink__auth_process(conn); + + /* Authenticated: read into rxbuf and dispatch any complete messages. */ + for (;;) { + ssize_t n; + size_t room = sizeof(conn->rxbuf) - conn->rxlen; + + if (room == 0) { + /* Pathological message size; drop the peer. */ + 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) + break; + return -1; + } + conn->rxlen += (size_t)n; + if (process_binary(conn) < 0) + return -1; + /* Loop again — there may be more readable bytes. */ + } + + return process_binary(conn); } diff --git a/libink/dispatch.c b/libink/dispatch.c new file mode 100644 index 00000000..26c23ddb --- /dev/null +++ b/libink/dispatch.c @@ -0,0 +1,318 @@ +/* libink — object tree, vtable registration, and method dispatch. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +#include "ink-internal.h" + +/* ---------- object/vtable registration ---------- */ + +static struct ink_object *find_object(ink_server_t *srv, const char *path) +{ + struct ink_object *o; + + TAILQ_FOREACH(o, &srv->objects, link) + if (strcmp(o->path, path) == 0) + return o; + return NULL; +} + +int ink_server_add_object(ink_server_t *srv, const char *path, + const ink_vtable_t *vt, void *userdata) +{ + struct ink_object *o; + struct ink_vtable_entry *e; + size_t plen; + + if (!srv || !path || !*path || !vt || !vt->interface) { + errno = EINVAL; + return -1; + } + plen = strlen(path); + if (plen >= INK_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 ink_method_t *find_method(const ink_vtable_t *vt, const char *name) +{ + const ink_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 ink_method_t *resolve(struct ink_object *o, + const char *iface, const char *member, + struct ink_vtable_entry **out_e) +{ + struct ink_vtable_entry *e; + const ink_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 ---------- */ + +static int send_all(int fd, const uint8_t *buf, size_t len) +{ + while (len > 0) { + ssize_t n = write(fd, buf, len); + + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + buf += n; + len -= (size_t)n; + } + return 0; +} + +int ink__send_method_return(ink_connection_t *conn, const struct ink_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 = ink__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 ink__send_error(ink_connection_t *conn, const struct ink_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 ink_writer w; + ssize_t n; + + ink__w_init(&w, body, sizeof(body)); + ink__w_string(&w, text); + n = ink__w_finish(&w); + if (n < 0) { + errno = EMSGSIZE; + return -1; + } + blen = (size_t)n; + sig = "s"; + } + + hlen = ink__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; +} + +/* ---------- ink_call public surface ---------- */ + +const char *ink_call_path (const ink_call_t *c) { return c ? c->incoming.path : NULL; } +const char *ink_call_interface(const ink_call_t *c) { return c ? c->incoming.interface : NULL; } +const char *ink_call_member (const ink_call_t *c) { return c ? c->incoming.member : NULL; } +uid_t ink_call_uid (const ink_call_t *c) { return c ? c->conn->peer_uid : (uid_t)-1; } + +ink_writer_t *ink_call_reply(ink_call_t *call) +{ + if (!call || call->reply_consumed || call->error_sent) + return NULL; + call->reply_consumed = 1; + ink__w_init(&call->reply_writer, call->reply_body, sizeof(call->reply_body)); + return &call->reply_writer; +} + +int ink_call_reply_error(ink_call_t *call, const char *name, const char *message) +{ + if (!call || call->error_sent) { + errno = EINVAL; + return -1; + } + call->error_sent = 1; + return ink__send_error(call->conn, &call->incoming, name, message); +} + +/* ---------- public writer wrappers ---------- */ + +void ink_w_byte (ink_writer_t *w, uint8_t v) { ink__w_byte(w, v); } +void ink_w_bool (ink_writer_t *w, int v) { ink__w_bool(w, v); } +void ink_w_u32 (ink_writer_t *w, uint32_t v) { ink__w_u32(w, v); } +void ink_w_string (ink_writer_t *w, const char *s) { ink__w_string(w, s); } +void ink_w_path (ink_writer_t *w, const char *s) { ink__w_path(w, s); } +void ink_w_array_begin (ink_writer_t *w, char ec) { ink__w_array_begin(w, ec); } +void ink_w_array_end (ink_writer_t *w) { ink__w_array_end(w); } +void ink_w_struct_begin(ink_writer_t *w) { ink__w_struct_begin(w); } +void ink_w_struct_end (ink_writer_t *w) { ink__w_struct_end(w); } + +/* ---------- dispatch entry point ---------- */ + +int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) +{ + struct ink_object *o; + struct ink_vtable_entry *e = NULL; + const ink_method_t *meth; + struct ink_call call; + ssize_t blen; + int rc; + + if (m->type != INK_MSG_METHOD_CALL) { + /* Signals and replies from a client to PID 1 are nonsense; + * silently drop. */ + return 0; + } + + if (!m->path || !m->member) { + return ink__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. */ + rc = ink__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 ink__send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownObject", + "No such object"); + } + + meth = resolve(o, m->interface, m->member, &e); + if (!meth) { + return ink__send_error(conn, m, + m->interface + ? "org.freedesktop.DBus.Error.UnknownMethod" + : "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 ink__send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Argument signature mismatch"); + } + + memset(&call, 0, sizeof(call)); + call.conn = conn; + call.incoming = *m; + + rc = meth->handler(&call, e->userdata); + if (rc < 0 && !call.reply_consumed && !call.error_sent) { + /* Handler returned an error without sending one. */ + ink__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 "". */ + ink__send_method_return(conn, m, NULL, NULL, 0); + return 0; + } + + if (call.reply_consumed && !call.error_sent) { + blen = ink__w_finish(&call.reply_writer); + if (blen < 0) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply marshalling overflow"); + return ink__send_method_return(conn, m, meth->out_sig, + call.reply_body, (size_t)blen); + } + + return 0; +} diff --git a/libink/ink-internal.h b/libink/ink-internal.h index 67401ec8..40c6ec39 100644 --- a/libink/ink-internal.h +++ b/libink/ink-internal.h @@ -7,39 +7,99 @@ #define LIBINK_INK_INTERNAL_H_ #include +#include #include "ink.h" +#include "marshal.h" +#include "proto.h" typedef enum { - INK_AUTH_NUL = 0, /* waiting for the leading nul byte */ - INK_AUTH_LINE, /* line-mode SASL exchange */ - INK_AUTH_DONE, /* BEGIN received, binary mode */ - INK_AUTH_FAILED, /* terminal: drop the connection */ + INK_AUTH_NUL = 0, + INK_AUTH_LINE, + INK_AUTH_DONE, + INK_AUTH_FAILED, } ink_auth_state_t; -/* AF_UNIX sun_path is 108 bytes on Linux, 104 on BSD — bound to the - * Linux value, which is the maximum we'll ever encounter. */ #define INK_PATH_MAX 108 #define INK_AUTH_LINEBUF_SIZE 256 +#define INK_RX_BUF_SIZE (64 * 1024) +#define INK_TX_BUF_SIZE (64 * 1024) +#define INK_UNIQUE_NAME_LEN 16 + +/* Per-vtable record attached to an object's interface list. */ +struct ink_vtable_entry { + const ink_vtable_t *vt; + void *userdata; + TAILQ_ENTRY(ink_vtable_entry) link; +}; + +TAILQ_HEAD(ink_vtable_list, ink_vtable_entry); + +/* An object exposed at one path. */ +struct ink_object { + char path[INK_PATH_MAX]; + struct ink_vtable_list vtables; + TAILQ_ENTRY(ink_object) link; +}; + +TAILQ_HEAD(ink_object_list, ink_object); struct ink_server { - int fd; - char path[INK_PATH_MAX]; + int fd; + char path[INK_PATH_MAX]; + struct ink_object_list objects; + uint32_t next_unique_id; /* for ":1.N" names */ +}; + +/* The reply being assembled inside a method handler. */ +struct ink_call { + ink_connection_t *conn; + struct ink_msg incoming; /* parsed view of the request */ + + /* Reply scratch area. Header is built into reply_header_buf + * after the body is finished (because body length is part of + * the header). Body is written into a separate buffer the + * caller marshals into via ink_w_*. */ + struct ink_writer reply_writer; + uint8_t reply_body[INK_TX_BUF_SIZE - 512]; + uint8_t reply_header[512]; + + int reply_consumed; /* 0/1 — ink_call_reply called */ + int error_sent; /* 0/1 — error reply done */ }; struct ink_connection { - int fd; - uid_t peer_uid; + int fd; + uid_t peer_uid; + + char guid[33]; + char unique_name[INK_UNIQUE_NAME_LEN]; /* ":1.N" */ + + ink_auth_state_t auth; + char linebuf[INK_AUTH_LINEBUF_SIZE]; + size_t linelen; - /* Server-assigned GUID, 32 hex chars + nul. */ - char guid[33]; + uint8_t rxbuf[INK_RX_BUF_SIZE]; + size_t rxlen; - ink_auth_state_t auth; - char linebuf[INK_AUTH_LINEBUF_SIZE]; - size_t linelen; + uint32_t next_serial; + + struct ink_server *server; /* back-pointer for dispatch */ }; +/* auth.c */ int ink__auth_process(ink_connection_t *conn); void ink__auth_generate_guid(char out[33]); +/* dispatch.c */ +int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m); +int ink__send_error(ink_connection_t *conn, const struct ink_msg *req, + const char *error_name, const char *text); +int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, + const char *out_sig, + const uint8_t *body, size_t body_len); + +/* builtin.c */ +int ink__handle_builtin(ink_connection_t *conn, const struct ink_msg *m); + #endif /* LIBINK_INK_INTERNAL_H_ */ diff --git a/libink/ink.h b/libink/ink.h index 275fc0a1..5538e72e 100644 --- a/libink/ink.h +++ b/libink/ink.h @@ -32,29 +32,76 @@ extern "C" { typedef struct ink_server ink_server_t; typedef struct ink_connection ink_connection_t; +typedef struct ink_call ink_call_t; +typedef struct ink_writer ink_writer_t; + +/* ---------- server / connection lifecycle ---------- */ -/* Server lifecycle. path is the AF_UNIX socket pathname to bind. */ int ink_server_new (ink_server_t **server, const char *path); void ink_server_free (ink_server_t *server); int ink_server_get_fd(const ink_server_t *server); -/* Accept a pending connection from the listening socket. On success - * returns 0 and stores a new connection in *conn. Returns -1 with - * errno set if accept() failed or memory could not be allocated. - * Callers should use ink_connection_get_fd() to register the new - * connection with their event loop. */ int ink_server_accept(ink_server_t *server, ink_connection_t **conn); -/* Per-connection. */ -int ink_connection_get_fd (const ink_connection_t *conn); -uid_t ink_connection_get_uid(const ink_connection_t *conn); +int ink_connection_get_fd (const ink_connection_t *conn); +uid_t ink_connection_get_uid (const ink_connection_t *conn); +int ink_connection_process (ink_connection_t *conn); +void ink_connection_close (ink_connection_t *conn); + +/* ---------- object registration ---------- */ + +typedef int (*ink_method_fn)(ink_call_t *call, void *userdata); + +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 */ + ink_method_fn handler; +} ink_method_t; + +typedef struct { + const char *interface; /* e.g. "org.finit.Manager1" */ + const ink_method_t *methods; /* terminated by {NULL, ...} */ +} ink_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 ink_server_add_object(ink_server_t *server, const char *path, + const ink_vtable_t *vt, void *userdata); + +/* ---------- call accessors ---------- */ + +const char *ink_call_path (const ink_call_t *call); +const char *ink_call_interface(const ink_call_t *call); +const char *ink_call_member (const ink_call_t *call); +uid_t ink_call_uid (const ink_call_t *call); + +/* ---------- 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. */ +ink_writer_t *ink_call_reply(ink_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 ink_call_reply_error(ink_call_t *call, const char *name, const char *message); -/* Drive the connection state machine when its fd is readable. Returns - * 0 on success (keep watching), -1 on error or peer close (caller - * should drop the connection via ink_connection_close()). */ -int ink_connection_process(ink_connection_t *conn); +/* ---------- writer (mirrors the internal marshaller) ---------- */ -void ink_connection_close (ink_connection_t *conn); +void ink_w_byte (ink_writer_t *w, uint8_t v); +void ink_w_bool (ink_writer_t *w, int v); +void ink_w_u32 (ink_writer_t *w, uint32_t v); +void ink_w_string (ink_writer_t *w, const char *s); /* "s" */ +void ink_w_path (ink_writer_t *w, const char *s); /* "o" */ +void ink_w_array_begin (ink_writer_t *w, char element_sig); +void ink_w_array_end (ink_writer_t *w); +void ink_w_struct_begin(ink_writer_t *w); +void ink_w_struct_end (ink_writer_t *w); #ifdef __cplusplus } diff --git a/libink/marshal.c b/libink/marshal.c new file mode 100644 index 00000000..4d21df36 --- /dev/null +++ b/libink/marshal.c @@ -0,0 +1,168 @@ +/* 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 ink__w_init(struct ink_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 ink__w_finish(struct ink_writer *w) +{ + if (w->err || w->array_depth != 0) + return -1; + return (ssize_t)w->off; +} + +static int reserve(struct ink_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 ink_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 ink_writer *w, uint32_t v) +{ + put_u32_at(w, w->off, v); + w->off += 4; +} + +void ink__w_byte(struct ink_writer *w, uint8_t v) +{ + if (reserve(w, 1, 1) < 0) + return; + w->buf[w->off++] = v; +} + +void ink__w_bool(struct ink_writer *w, int v) +{ + if (reserve(w, 4, 4) < 0) + return; + put_u32(w, v ? 1u : 0u); +} + +void ink__w_u32(struct ink_writer *w, uint32_t v) +{ + if (reserve(w, 4, 4) < 0) + return; + put_u32(w, v); +} + +static void write_lenprefixed(struct ink_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 ink__w_string(struct ink_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void ink__w_path (struct ink_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void ink__w_sig (struct ink_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 ink__w_array_begin(struct ink_writer *w, char element_sig_first_char) +{ + size_t lenpos; + + if (w->err) + return; + if (w->array_depth >= INK_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 ink__w_array_end(struct ink_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 ink__w_struct_begin(struct ink_writer *w) +{ + reserve(w, 8, 0); +} + +void ink__w_struct_end(struct ink_writer *w) +{ + (void)w; +} diff --git a/libink/marshal.h b/libink/marshal.h new file mode 100644 index 00000000..95409e01 --- /dev/null +++ b/libink/marshal.h @@ -0,0 +1,47 @@ +/* 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 + +#define INK_WRITER_MAX_NESTING 8 + +struct ink_writer { + uint8_t *buf; + size_t cap; + size_t off; + int err; + + /* Stack of open arrays, used to back-patch the length prefix + * on array_end with the elements-only byte count. */ + struct { + size_t lenpos; /* offset of the u32 length */ + size_t elemstart; /* offset of first element (post-padding) */ + } arrays[INK_WRITER_MAX_NESTING]; + size_t array_depth; +}; + +void ink__w_init (struct ink_writer *w, uint8_t *buf, size_t cap); +ssize_t ink__w_finish(struct ink_writer *w); + +void ink__w_byte (struct ink_writer *w, uint8_t v); +void ink__w_bool (struct ink_writer *w, int v); +void ink__w_u32 (struct ink_writer *w, uint32_t v); +void ink__w_string (struct ink_writer *w, const char *s); /* "s" */ +void ink__w_path (struct ink_writer *w, const char *s); /* "o" */ +void ink__w_sig (struct ink_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 ink__w_array_begin (struct ink_writer *w, char element_sig_first_char); +void ink__w_array_end (struct ink_writer *w); + +void ink__w_struct_begin(struct ink_writer *w); +void ink__w_struct_end (struct ink_writer *w); + +#endif /* LIBINK_MARSHAL_H_ */ diff --git a/libink/proto.c b/libink/proto.c new file mode 100644 index 00000000..aa577392 --- /dev/null +++ b/libink/proto.c @@ -0,0 +1,357 @@ +/* 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 ink__msg_parse(const uint8_t *buf, size_t len, struct ink_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] != INK_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 INK_HDR_PATH: out->path = s; break; + case INK_HDR_INTERFACE: out->interface = s; break; + case INK_HDR_MEMBER: out->member = s; break; + case INK_HDR_ERROR_NAME: out->error_name = s; break; + case INK_HDR_DESTINATION: out->destination = s; break; + case INK_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 == INK_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 == INK_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] = INK_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 ink__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, INK_HDR_REPLY_SERIAL, reply_serial) < 0) + return -1; + if (destination && + put_field_string(buf, cap, &off, INK_HDR_DESTINATION, 's', destination) < 0) + return -1; + if (signature && *signature && + put_field_string(buf, cap, &off, INK_HDR_SIGNATURE, 'g', signature) < 0) + return -1; + + return finalize_header(buf, cap, INK_MSG_METHOD_RETURN, + INK_FLAG_NO_REPLY_EXPECTED, + body_len, serial, off); +} + +ssize_t ink__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, INK_HDR_REPLY_SERIAL, reply_serial) < 0) + return -1; + if (put_field_string(buf, cap, &off, INK_HDR_ERROR_NAME, 's', error_name) < 0) + return -1; + if (destination && + put_field_string(buf, cap, &off, INK_HDR_DESTINATION, 's', destination) < 0) + return -1; + if (signature && *signature && + put_field_string(buf, cap, &off, INK_HDR_SIGNATURE, 'g', signature) < 0) + return -1; + + return finalize_header(buf, cap, INK_MSG_ERROR, + INK_FLAG_NO_REPLY_EXPECTED, + body_len, serial, off); +} + +ssize_t ink__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, INK_HDR_PATH, 'o', path) < 0) + return -1; + if (put_field_string(buf, cap, &off, INK_HDR_INTERFACE, 's', interface) < 0) + return -1; + if (put_field_string(buf, cap, &off, INK_HDR_MEMBER, 's', member) < 0) + return -1; + if (signature && *signature && + put_field_string(buf, cap, &off, INK_HDR_SIGNATURE, 'g', signature) < 0) + return -1; + + return finalize_header(buf, cap, INK_MSG_SIGNAL, + INK_FLAG_NO_REPLY_EXPECTED, + body_len, serial, off); +} + +size_t ink__msg_header_size(const struct ink_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..50c6b8d0 --- /dev/null +++ b/libink/proto.h @@ -0,0 +1,97 @@ +/* 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 + +/* Message types (D-Bus spec §4: "Message Format"). */ +#define INK_MSG_INVALID 0 +#define INK_MSG_METHOD_CALL 1 +#define INK_MSG_METHOD_RETURN 2 +#define INK_MSG_ERROR 3 +#define INK_MSG_SIGNAL 4 + +/* Message flags. */ +#define INK_FLAG_NO_REPLY_EXPECTED 0x01 +#define INK_FLAG_NO_AUTO_START 0x02 +#define INK_FLAG_ALLOW_INTERACTIVE_AUTHORIZATION 0x04 + +/* Header field codes. */ +#define INK_HDR_PATH 1 +#define INK_HDR_INTERFACE 2 +#define INK_HDR_MEMBER 3 +#define INK_HDR_ERROR_NAME 4 +#define INK_HDR_REPLY_SERIAL 5 +#define INK_HDR_DESTINATION 6 +#define INK_HDR_SENDER 7 +#define INK_HDR_SIGNATURE 8 +#define INK_HDR_UNIX_FDS 9 + +#define INK_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 ink_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 ink__msg_parse(const uint8_t *buf, size_t len, struct ink_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 ink__msg_header_size(const struct ink_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 ink__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 ink__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 ink__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); + +#endif /* LIBINK_PROTO_H_ */ diff --git a/libink/server.c b/libink/server.c index 42dee5e9..919582e9 100644 --- a/libink/server.c +++ b/libink/server.c @@ -42,6 +42,7 @@ int ink_server_new(ink_server_t **out, const char *path) 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) @@ -88,9 +89,27 @@ int ink_server_new(ink_server_t **out, const char *path) void ink_server_free(ink_server_t *srv) { + struct ink_object *o, *otmp = NULL; + if (!srv) return; + (void)otmp; /* unused now */ + o = TAILQ_FIRST(&srv->objects); + while (o) { + struct ink_object *next_o = TAILQ_NEXT(o, link); + struct ink_vtable_entry *e = TAILQ_FIRST(&o->vtables); + + while (e) { + struct ink_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]) @@ -128,8 +147,9 @@ int ink_server_accept(ink_server_t *srv, ink_connection_t **out) return -1; } - conn->fd = cfd; - conn->auth = INK_AUTH_NUL; + conn->fd = cfd; + conn->auth = INK_AUTH_NUL; + conn->server = srv; if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0) conn->peer_uid = cred.uid; diff --git a/src/dbus.c b/src/dbus.c index d8498c67..b547e2e2 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -1,9 +1,9 @@ /* Finit-side glue between the event loop and libink. * - * Owns the libink server, accepts new peers, and drives each peer's - * state machine when its fd becomes readable. Nothing in this file - * leaks finit-internal types into libink, by design: the boundary - * here is the prospective extraction line. + * 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 * @@ -40,6 +40,10 @@ #include "finit.h" #include "log.h" +#include "service.h" +#include "svc.h" + +#define DBUS_MAX_PEERS 64 struct peer { uev_t watcher; @@ -47,12 +51,6 @@ struct peer { TAILQ_ENTRY(peer) link; }; -/* Cap the per-process peer count. The socket is world-accessible by - * design (introspection should work for unprivileged users) so an - * unprivileged local actor could connect in a loop until EMFILE - * without this. */ -#define DBUS_MAX_PEERS 64 - static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); static ink_server_t *server; static uev_t accept_watcher; @@ -127,6 +125,44 @@ static void accept_cb(uev_t *w, void *arg, int events) } } +/* ---------- org.finit.Manager1 ---------- */ + +static int manager_list_services(ink_call_t *call, void *userdata) +{ + ink_writer_t *w; + svc_t *iter = NULL; + svc_t *svc; + + (void)userdata; + + w = ink_call_reply(call); + if (!w) + return -1; + + ink_w_array_begin(w, 's'); + for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { + char ident[64]; + + svc_ident(svc, ident, sizeof(ident)); + ink_w_string(w, ident); + } + ink_w_array_end(w); + return 0; +} + +static const ink_method_t manager_methods[] = { + { .name = "ListServices", .in_sig = "", .out_sig = "as", + .handler = manager_list_services }, + { NULL, NULL, NULL, NULL } +}; + +static const ink_vtable_t manager_vtable = { + .interface = "org.finit.Manager1", + .methods = manager_methods, +}; + +/* ---------- init / exit ---------- */ + int dbus_init(uev_ctx_t *ctx) { dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET); @@ -136,6 +172,14 @@ int dbus_init(uev_ctx_t *ctx) return 1; } + if (ink_server_add_object(server, "/org/finit/manager", + &manager_vtable, NULL) < 0) { + err(1, "Failed registering Manager1 object"); + ink_server_free(server); + server = NULL; + return 1; + } + if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, ink_server_get_fd(server), UEV_READ)) { err(1, "Failed registering D-Bus accept watcher"); diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index a83651b8..c9fa7eea 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -1,10 +1,10 @@ #!/bin/sh -# Smoke test for libink AUTH EXTERNAL handshake over /run/finit/bus. -# -# Verifies that with --enable-dbus (the default), Finit opens its -# brokerless D-Bus socket and that the SASL-style AUTH EXTERNAL -# handshake completes for a peer claiming the right UID and is -# rejected for one claiming a wrong UID. +# 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 @@ -23,43 +23,80 @@ fi say "Wait for $BUS to appear" retry "texec test -S $BUS" -say "Verify socket permissions are 0666" +say "Socket mode is 0666" mode=$(texec stat -c %a "$BUS") assert "Socket mode is 666 (got $mode)" "$mode" = "666" -say "Happy path — claim correct UID (root inside namespace = 0)" -reply=$(texec "$CLIENT" "$BUS" 0) +# ---------- 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 must be 32 hex chars; a server blindly emitting "OK foo" would -# otherwise pass the prefix check above. 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 "Wrong UID is rejected" -# Claim uid 1, which does not match peer's real uid 0. Exit code 1 -# means "REJECTED" (the expected denial); 0 would be acceptance, -# 2 a transport error. Distinguish all three for a useful failure -# message. +say "AUTH EXTERNAL: wrong UID is rejected" set +e -wrong_reply=$(texec "$CLIENT" "$BUS" 1) +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 connections both authenticate independently" -reply1=$(texec "$CLIENT" "$BUS" 0) -reply2=$(texec "$CLIENT" "$BUS" 0) -assert "First connection OK" "${reply1%% *}" = "OK" -assert "Second connection OK" "${reply2%% *}" = "OK" - -# The two GUIDs in the OK lines should differ; libink generates one -# per connection. This catches a regression where we accidentally -# share GUID state across peers. -guid1=${reply1#OK } -guid2=${reply2#OK } -assert "Per-connection GUIDs differ ($guid1 vs $guid2)" \ - "$guid1" != "$guid2" +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" + +# ---------- 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/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 0c12bb53..b66a072d 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -1,25 +1,50 @@ -/* Minimal D-Bus AUTH EXTERNAL client used by the libink smoke test. +/* Minimal D-Bus client used by the libink smoke tests. * - * Usage: dbus-auth-client + * 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. * - * exit 0 — server reply began with "OK " - * exit 1 — server reply began with "REJECTED " (the expected denial) - * exit 2 — any other reply, short read, or transport error + * 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 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; @@ -28,8 +53,25 @@ static int write_all(int fd, const void *buf, size_t len) ssize_t n = write(fd, p, len); if (n < 0) { - if (errno == EINTR) - continue; + 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; @@ -38,8 +80,6 @@ static int write_all(int fd, const void *buf, size_t len) return 0; } -/* Read until '\n' or buffer-full. Returns line length excluding any - * trailing CR/LF, -1 on transport error / EOF / overflow. */ static ssize_t read_line(int fd, char *buf, size_t bufsz) { size_t off = 0; @@ -47,11 +87,9 @@ static ssize_t read_line(int fd, char *buf, size_t bufsz) while (off + 1 < bufsz) { ssize_t n = read(fd, buf + off, 1); - if (n == 0) - return -1; + if (n == 0) return -1; if (n < 0) { - if (errno == EINTR) - continue; + if (errno == EINTR) continue; return -1; } if (buf[off] == '\n') { @@ -65,34 +103,342 @@ static ssize_t read_line(int fd, char *buf, size_t bufsz) return -1; } -int main(int argc, char *argv[]) +/* ---------- 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]; - const char *path, *claimed; - size_t i, claimed_len, plen; + size_t i, n; int fd, rc; - if (argc != 3) { - fprintf(stderr, "usage: %s \n", argv[0]); - return 2; + 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; } - path = argv[1]; - claimed = argv[2]; - plen = strlen(path); - if (plen >= sizeof(sun.sun_path)) { - fprintf(stderr, "%s: path too long\n", argv[0]); - return 2; + 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'; - claimed_len = strlen(claimed); - if (claimed_len * 2 >= sizeof(hexuid)) { - fprintf(stderr, "%s: uid too long\n", argv[0]); - return 2; + 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; +} + +static int send_method_call(int fd, + const char *path, + const char *interface, + const char *member) +{ + uint8_t hdr[1024]; + struct buf b = { .p = hdr, .cap = sizeof(hdr) }; + size_t fields_start, fields_end, padded_end; + + hdr[0] = 'l'; hdr[1] = 1; hdr[2] = 0; hdr[3] = 1; + b.off = 16; + b_put_u32(&b, 0); /* placeholder: body_len at offset 4 — actually we already wrote raw bytes above; rewrite */ + + /* Restart: write the fixed header by hand. */ + b.off = 0; + memset(hdr, 0, 16); + hdr[0] = 'l'; + hdr[1] = 1; /* METHOD_CALL */ + hdr[2] = 0; /* flags */ + hdr[3] = 1; /* protocol */ + /* body_len (4..8) = 0 */ + hdr[8] = 1; /* serial (4 bytes; little-endian 1) */ + /* fields_len at 12..16 — patched later */ + b.off = 16; + fields_start = b.off; + + /* PATH (code 1, type 'o') */ + b_reserve(&b, 8, 0); + b_put_byte(&b, 1); + b_put_signature(&b, "o"); + b_put_string(&b, path); + + if (interface) { + /* INTERFACE (code 2, type 's') */ + b_reserve(&b, 8, 0); + b_put_byte(&b, 2); + b_put_signature(&b, "s"); + b_put_string(&b, interface); + } + + /* MEMBER (code 3, type 's') */ + b_reserve(&b, 8, 0); + b_put_byte(&b, 3); + b_put_signature(&b, "s"); + b_put_string(&b, member); + + fields_end = b.off; + hdr[12] = (uint8_t)((fields_end - fields_start) & 0xff); + hdr[13] = (uint8_t)(((fields_end - fields_start) >> 8) & 0xff); + + /* Pad header to 8-byte boundary; body is empty so we stop here. */ + padded_end = ALIGN_UP(fields_end, 8); + while (b.off < padded_end) hdr[b.off++] = 0; + + if (b.err) return -1; + return write_all(fd, hdr, b.off); +} + +/* 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]; + uint8_t body[8192]; +}; + +static int read_reply(int fd, struct reply *r) +{ + uint8_t hdr_fixed[16]; + uint8_t hdr_fields[2048]; + uint32_t fields_len; + size_t body_off; + size_t pos; + + memset(r, 0, sizeof(*r)); + + if (read_full(fd, hdr_fixed, 16) < 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; + + /* Skip header padding to 8 bytes. */ + 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; + } + + /* Walk header fields, capture SIGNATURE and ERROR_NAME. */ + pos = 0; + while (pos < fields_len) { + uint8_t code; + size_t vsig_len; + + 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; + const char *vsig = (const char *)(hdr_fields + pos); + pos += vsig_len + 1; + + if (vsig[0] == 's' || vsig[0] == 'o') { + uint32_t slen; + 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; + if (code == 4 && slen < sizeof(r->error_name)) { + memcpy(r->error_name, hdr_fields + pos, slen); + r->error_name[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" — write into out, return 0. */ +static int decode_string(struct reply *r, char *out, size_t outsz) +{ + uint32_t len; + + if (strcmp(r->signature, "s") != 0 || r->body_len < 5) + 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]; @@ -102,48 +448,113 @@ int main(int argc, char *argv[]) hexuid[claimed_len * 2] = '\0'; fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) { - perror("socket"); - return 2; - } - + 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; + 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; +} - if (write_all(fd, "\0", 1) < 0) { - perror("write"); - close(fd); - return 2; - } +static int do_call(const char *path, const char *obj_path, + const char *iface, const char *method, + struct reply *r) +{ + int fd = connect_and_auth(path, getuid()); - rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); - if (rc < 0 || (size_t)rc >= sizeof(line)) { - fprintf(stderr, "%s: auth line too long\n", argv[0]); + if (fd < 0) return 2; + if (send_method_call(fd, obj_path, iface, method) < 0) { + fprintf(stderr, "send: %s\n", strerror(errno)); close(fd); return 2; } - if (write_all(fd, line, (size_t)rc) < 0) { - perror("write"); + if (read_reply(fd, r) < 0) { + fprintf(stderr, "read_reply\n"); close(fd); return 2; } - - if (read_line(fd, reply, sizeof(reply)) < 0) { - fprintf(stderr, "%s: short read or oversized reply\n", argv[0]); - close(fd); - return 2; + close(fd); + if (r->type == 3) { + fprintf(stderr, "ERROR: %s\n", r->error_name); + return 1; } + return 0; +} - printf("%s\n", reply); - close(fd); +static int mode_hello(int argc, char *argv[]) +{ + struct reply r; + char name[256]; + int rc; - if (strncmp(reply, "OK ", 3) == 0) + 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; +} + +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 (strncmp(reply, "REJECTED ", 9) == 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], "unknown") == 0) return mode_unknown(argc, argv); + fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); + return 2; +} From 8bfacc1287593b807e49381fb947b5600def74a5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 14 May 2026 10:35:41 +0200 Subject: [PATCH 03/22] libink: read-side marshaller and Manager1 service-control methods Mirror of the writer's primitives: __r_byte/_bool/_u32/_string/_path with a per-call cursor wired into struct ink_call, public via ink_call_read_*. Manager1 grows the obvious service-control surface: Start/Stop/Restart take "s" identity; Reload no args; SetRunlevel "u"; Reboot/Poweroff/Halt no args. Bogus identity returns NoSuchService; runlevel ops while in S, 0 or 6 return WrongRunlevel. Drive-by: Introspect XML buffer moved out of the stack (8 KB static) so a growing interface can't push PID 1 toward its stack limit. Signed-off-by: Joachim Wiberg --- libink/builtin.c | 2 +- libink/dispatch.c | 9 ++ libink/ink-internal.h | 11 +-- libink/ink.h | 15 ++++ libink/marshal.c | 92 ++++++++++++++++++++ libink/marshal.h | 21 +++++ src/dbus.c | 167 +++++++++++++++++++++++++++++++++++- test/dbus-auth.sh | 20 +++++ test/src/dbus-auth-client.c | 137 ++++++++++++++++++++++++----- 9 files changed, 443 insertions(+), 31 deletions(-) diff --git a/libink/builtin.c b/libink/builtin.c index 521432a2..434a07fd 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -171,7 +171,7 @@ static int child_segment(const char *parent, const char *child, static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) { - char xml[4096]; + static char xml[8192]; /* static keeps the stack small in PID 1 */ struct xbuf x = { .buf = xml, .cap = sizeof(xml) }; struct ink_object *o; const char *path = m->path; diff --git a/libink/dispatch.c b/libink/dispatch.c index 26c23ddb..b29a1465 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -216,6 +216,14 @@ int ink_call_reply_error(ink_call_t *call, const char *name, const char *message return ink__send_error(call->conn, &call->incoming, name, message); } +/* ---------- public reader wrappers ---------- */ + +int ink_call_read_byte (ink_call_t *c, uint8_t *o) { return ink__r_byte (&c->read_cursor, o); } +int ink_call_read_bool (ink_call_t *c, int *o) { return ink__r_bool (&c->read_cursor, o); } +int ink_call_read_u32 (ink_call_t *c, uint32_t *o) { return ink__r_u32 (&c->read_cursor, o); } +int ink_call_read_string(ink_call_t *c, const char **o) { return ink__r_string(&c->read_cursor, o); } +int ink_call_read_path (ink_call_t *c, const char **o) { return ink__r_path (&c->read_cursor, o); } + /* ---------- public writer wrappers ---------- */ void ink_w_byte (ink_writer_t *w, uint8_t v) { ink__w_byte(w, v); } @@ -287,6 +295,7 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) memset(&call, 0, sizeof(call)); call.conn = conn; call.incoming = *m; + ink__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) { diff --git a/libink/ink-internal.h b/libink/ink-internal.h index 40c6ec39..d4126956 100644 --- a/libink/ink-internal.h +++ b/libink/ink-internal.h @@ -56,16 +56,13 @@ struct ink_call { ink_connection_t *conn; struct ink_msg incoming; /* parsed view of the request */ - /* Reply scratch area. Header is built into reply_header_buf - * after the body is finished (because body length is part of - * the header). Body is written into a separate buffer the - * caller marshals into via ink_w_*. */ + struct ink_reader read_cursor; /* over incoming.body */ + struct ink_writer reply_writer; uint8_t reply_body[INK_TX_BUF_SIZE - 512]; - uint8_t reply_header[512]; - int reply_consumed; /* 0/1 — ink_call_reply called */ - int error_sent; /* 0/1 — error reply done */ + int reply_consumed; + int error_sent; }; struct ink_connection { diff --git a/libink/ink.h b/libink/ink.h index 5538e72e..b9eca6db 100644 --- a/libink/ink.h +++ b/libink/ink.h @@ -78,6 +78,21 @@ const char *ink_call_interface(const ink_call_t *call); const char *ink_call_member (const ink_call_t *call); uid_t ink_call_uid (const ink_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 ink_call_read_byte (ink_call_t *call, uint8_t *out); +int ink_call_read_bool (ink_call_t *call, int *out); +int ink_call_read_u32 (ink_call_t *call, uint32_t *out); +int ink_call_read_string(ink_call_t *call, const char **out); /* "s" */ +int ink_call_read_path (ink_call_t *call, const char **out); /* "o" */ + /* ---------- reply construction ---------- */ /* Get the writer for the reply body, write args into it, return 0 diff --git a/libink/marshal.c b/libink/marshal.c index 4d21df36..03001443 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -166,3 +166,95 @@ void ink__w_struct_end(struct ink_writer *w) { (void)w; } + +/* ---- reader ---- */ + +void ink__r_init(struct ink_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 ink_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 ink__r_byte(struct ink_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 ink__r_u32(struct ink_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 ink__r_bool(struct ink_reader *r, int *out) +{ + uint32_t v; + + if (ink__r_u32(r, &v) < 0) + return -1; + *out = v ? 1 : 0; + return 0; +} + +static int read_string_like(struct ink_reader *r, const char **out) +{ + uint32_t len; + + if (ink__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 ink__r_string(struct ink_reader *r, const char **out) { return read_string_like(r, out); } +int ink__r_path (struct ink_reader *r, const char **out) { return read_string_like(r, out); } + +int ink__r_done(const struct ink_reader *r) +{ + return !r->err && r->off == r->cap; +} diff --git a/libink/marshal.h b/libink/marshal.h index 95409e01..cf128fdf 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -44,4 +44,25 @@ void ink__w_array_end (struct ink_writer *w); void ink__w_struct_begin(struct ink_writer *w); void ink__w_struct_end (struct ink_writer *w); +/* ---- reader ---- + * + * Reads from a message body pointer + length, advancing a cursor. + * String pointers returned by ink__r_string reference the input + * buffer and are valid for the lifetime of that buffer (i.e. for + * the duration of the current call dispatch). */ +struct ink_reader { + const uint8_t *base; + size_t off; + size_t cap; + int err; /* sticky */ +}; + +void ink__r_init (struct ink_reader *r, const uint8_t *body, size_t len); +int ink__r_byte (struct ink_reader *r, uint8_t *out); +int ink__r_bool (struct ink_reader *r, int *out); +int ink__r_u32 (struct ink_reader *r, uint32_t *out); +int ink__r_string(struct ink_reader *r, const char **out); /* "s" */ +int ink__r_path (struct ink_reader *r, const char **out); /* "o" */ +int ink__r_done (const struct ink_reader *r); + #endif /* LIBINK_MARSHAL_H_ */ diff --git a/src/dbus.c b/src/dbus.c index b547e2e2..bd313eb2 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -39,8 +39,12 @@ #include "ink.h" #include "finit.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 @@ -150,9 +154,168 @@ static int manager_list_services(ink_call_t *call, void *userdata) 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[128]; + 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(ink_call_t *call, + int (*action)(svc_t *, void *)) +{ + const char *ident; + + if (ink_call_read_string(call, &ident) < 0) + return ink_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + if (dispatch_action(ident, action) != 0) + return ink_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + (void)ink_call_reply(call); /* empty reply */ + return 0; +} + +static int manager_start (ink_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); } +static int manager_stop (ink_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); } +static int manager_restart(ink_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); } + +static int manager_reload(ink_call_t *call, void *userdata) +{ + (void)userdata; + if (IS_RESERVED_RUNLEVEL(runlevel)) + return ink_call_reply_error(call, + "org.finit.Error.WrongRunlevel", + "Reload not allowed in runlevel S or 0/6"); + sm_reload(); + (void)ink_call_reply(call); + return 0; +} + +static int manager_set_runlevel(ink_call_t *call, void *userdata) +{ + uint32_t lvl; + + (void)userdata; + if (ink_call_read_u32(call, &lvl) < 0) + return ink_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (u)"); + if (lvl > 9 || lvl == INIT_LEVEL) + return ink_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)ink_call_reply(call); + return 0; +} + +static int dbus_shutdown(ink_call_t *call, shutop_t target, int level) +{ + if (IS_RESERVED_RUNLEVEL(runlevel)) + return ink_call_reply_error(call, + "org.finit.Error.WrongRunlevel", + "Already in shutdown"); + halt = target; + sm_runlevel(level); + (void)ink_call_reply(call); + return 0; +} + +static int manager_reboot (ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); } +static int manager_poweroff(ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); } +static int manager_halt (ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); } + static const ink_method_t manager_methods[] = { - { .name = "ListServices", .in_sig = "", .out_sig = "as", - .handler = manager_list_services }, + { .name = "ListServices", .in_sig = "", .out_sig = "as", .handler = manager_list_services }, + { .name = "Start", .in_sig = "s", .out_sig = "", .handler = manager_start }, + { .name = "Stop", .in_sig = "s", .out_sig = "", .handler = manager_stop }, + { .name = "Restart", .in_sig = "s", .out_sig = "", .handler = manager_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", .handler = manager_reload }, + { .name = "SetRunlevel", .in_sig = "u", .out_sig = "", .handler = manager_set_runlevel }, + { .name = "Reboot", .in_sig = "", .out_sig = "", .handler = manager_reboot }, + { .name = "Poweroff", .in_sig = "", .out_sig = "", .handler = manager_poweroff }, + { .name = "Halt", .in_sig = "", .out_sig = "", .handler = manager_halt }, { NULL, NULL, NULL, NULL } }; diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index c9fa7eea..45d287eb 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -92,6 +92,26 @@ 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 + # ---------- Error reply ---------- say "Unknown method gets an org.freedesktop.DBus.Error.* reply" diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index b66a072d..69f317bb 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -215,62 +215,118 @@ static void b_put_signature(struct buf *b, const char *s) 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) { - uint8_t hdr[1024]; + 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; + } - hdr[0] = 'l'; hdr[1] = 1; hdr[2] = 0; hdr[3] = 1; - b.off = 16; - b_put_u32(&b, 0); /* placeholder: body_len at offset 4 — actually we already wrote raw bytes above; rewrite */ - - /* Restart: write the fixed header by hand. */ - b.off = 0; + /* Fixed header */ memset(hdr, 0, 16); hdr[0] = 'l'; hdr[1] = 1; /* METHOD_CALL */ hdr[2] = 0; /* flags */ hdr[3] = 1; /* protocol */ - /* body_len (4..8) = 0 */ - hdr[8] = 1; /* serial (4 bytes; little-endian 1) */ - /* fields_len at 12..16 — patched later */ + 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 (code 1, type 'o') */ + /* PATH */ b_reserve(&b, 8, 0); b_put_byte(&b, 1); b_put_signature(&b, "o"); b_put_string(&b, path); if (interface) { - /* INTERFACE (code 2, type 's') */ b_reserve(&b, 8, 0); b_put_byte(&b, 2); b_put_signature(&b, "s"); b_put_string(&b, interface); } - /* MEMBER (code 3, type 's') */ 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; - hdr[12] = (uint8_t)((fields_end - fields_start) & 0xff); - hdr[13] = (uint8_t)(((fields_end - fields_start) >> 8) & 0xff); + { + 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); + } - /* Pad header to 8-byte boundary; body is empty so we stop here. */ padded_end = ALIGN_UP(fields_end, 8); while (b.off < padded_end) hdr[b.off++] = 0; if (b.err) return -1; - return write_all(fd, hdr, b.off); + + 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. @@ -465,14 +521,16 @@ static int mode_auth(int argc, char *argv[]) return 2; } -static int do_call(const char *path, const char *obj_path, - const char *iface, const char *method, - struct reply *r) +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(fd, obj_path, iface, method) < 0) { + 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; @@ -490,6 +548,13 @@ static int do_call(const char *path, const char *obj_path, 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; @@ -532,6 +597,34 @@ static int mode_liststrings(int argc, char *argv[]) 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; +} + static int mode_unknown(int argc, char *argv[]) { struct reply r; @@ -554,6 +647,8 @@ int main(int argc, char *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], "unknown") == 0) return mode_unknown(argc, argv); fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); return 2; From 124a5679bd49fc638a1a06e3b0216849c8d92ab0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 14 May 2026 12:10:20 +0200 Subject: [PATCH 04/22] libink: fix dispatch race on coalesced BEGIN+request When a peer sent "BEGIN\r\n" and the first binary method call close enough that the kernel delivered both in one read(), the auth path correctly moved the post-BEGIN bytes into rxbuf and went to AUTH_DONE -- but ink_connection_process returned without dispatching them. The libuev fd watcher then waited for more data that never came; the peer deadlocked until the watchdog killed it. Fix: after auth transitions to DONE, fall through to process_binary before reading more bytes. Whether the writes coalesced depended on kernel scheduling, hence the intermittent dbus-auth.sh flake. Drive-by: * Reply scratch moved out of struct ink_call (was ~64 KB on stack in dispatch) into a per-connection txbuf; ink_call is ~700 B now. * Drop a dead `otmp` in ink_server_free and a redundant trailing process_binary in ink_connection_process. * Collapse the duplicate-branch ternary in dispatch.c's UnknownMethod error path. Signed-off-by: Joachim Wiberg --- libink/builtin.c | 21 +++++++++------------ libink/connection.c | 29 +++++++++++++++++++++-------- libink/dispatch.c | 9 ++++----- libink/ink-internal.h | 25 ++++++++++++++++--------- libink/server.c | 3 +-- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/libink/builtin.c b/libink/builtin.c index 434a07fd..1eb68468 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -31,18 +31,17 @@ static int member_is(const struct ink_msg *m, const char *iface, const char *mem static int send_string_reply(ink_connection_t *conn, const struct ink_msg *req, const char *s) { - uint8_t body[INK_TX_BUF_SIZE - 512]; - struct ink_writer w; - ssize_t blen; + struct ink_writer w; + ssize_t blen; - ink__w_init(&w, body, sizeof(body)); + ink__w_init(&w, conn->txbuf, sizeof(conn->txbuf)); ink__w_string(&w, s); blen = ink__w_finish(&w); if (blen < 0) { errno = EMSGSIZE; return -1; } - return ink__send_method_return(conn, req, "s", body, (size_t)blen); + return ink__send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); } /* ---------- Hello ---------- */ @@ -118,13 +117,11 @@ static void emit_method(struct xbuf *x, const ink_method_t *m) xprintf(x, " \n"); } -/* The user-side input/output signatures are flat strings ("ss", - * "a(ss)" etc.). The introspection emit above prints one - * per top-level type. For complex types like "a(soss)" each - * character produces an , which is wrong for `(` `)` `{` - * `}` `a` — but it's good enough for the simple "s", "u", "as" - * signatures we expose right now, and we can refine later when - * complex types appear. */ +/* 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" diff --git a/libink/connection.c b/libink/connection.c index 5350076c..dea13e29 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -67,16 +67,32 @@ int ink_connection_process(ink_connection_t *conn) if (conn->auth == INK_AUTH_FAILED) return -1; - if (conn->auth != INK_AUTH_DONE) - return ink__auth_process(conn); + if (conn->auth != INK_AUTH_DONE) { + if (ink__auth_process(conn) < 0) + return -1; + + /* Still in SASL phase — wait for more bytes. */ + if (conn->auth != INK_AUTH_DONE) + return 0; - /* Authenticated: read into rxbuf and dispatch any complete messages. */ + /* 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) { - /* Pathological message size; drop the peer. */ errno = E2BIG; return -1; } @@ -88,14 +104,11 @@ int ink_connection_process(ink_connection_t *conn) if (errno == EINTR) continue; if (errno == EAGAIN || errno == EWOULDBLOCK) - break; + return 0; return -1; } conn->rxlen += (size_t)n; if (process_binary(conn) < 0) return -1; - /* Loop again — there may be more readable bytes. */ } - - return process_binary(conn); } diff --git a/libink/dispatch.c b/libink/dispatch.c index b29a1465..b3780318 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -202,7 +202,8 @@ ink_writer_t *ink_call_reply(ink_call_t *call) if (!call || call->reply_consumed || call->error_sent) return NULL; call->reply_consumed = 1; - ink__w_init(&call->reply_writer, call->reply_body, sizeof(call->reply_body)); + ink__w_init(&call->reply_writer, + call->conn->txbuf, sizeof(call->conn->txbuf)); return &call->reply_writer; } @@ -275,9 +276,7 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) meth = resolve(o, m->interface, m->member, &e); if (!meth) { return ink__send_error(conn, m, - m->interface - ? "org.freedesktop.DBus.Error.UnknownMethod" - : "org.freedesktop.DBus.Error.UnknownMethod", + "org.freedesktop.DBus.Error.UnknownMethod", "No such method on this object"); } @@ -320,7 +319,7 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) "org.freedesktop.DBus.Error.Failed", "Reply marshalling overflow"); return ink__send_method_return(conn, m, meth->out_sig, - call.reply_body, (size_t)blen); + conn->txbuf, (size_t)blen); } return 0; diff --git a/libink/ink-internal.h b/libink/ink-internal.h index d4126956..60d4b0a5 100644 --- a/libink/ink-internal.h +++ b/libink/ink-internal.h @@ -23,7 +23,7 @@ typedef enum { #define INK_PATH_MAX 108 #define INK_AUTH_LINEBUF_SIZE 256 #define INK_RX_BUF_SIZE (64 * 1024) -#define INK_TX_BUF_SIZE (64 * 1024) +#define INK_TX_BUF_SIZE (16 * 1024) #define INK_UNIQUE_NAME_LEN 16 /* Per-vtable record attached to an object's interface list. */ @@ -51,16 +51,17 @@ struct ink_server { uint32_t next_unique_id; /* for ":1.N" names */ }; -/* The reply being assembled inside a method handler. */ +/* The reply being assembled inside a method handler. + * + * The reply body lives in conn->txbuf, not on this struct, so a + * stack-allocated ink_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 ink_call { ink_connection_t *conn; - struct ink_msg incoming; /* parsed view of the request */ - - struct ink_reader read_cursor; /* over incoming.body */ - - struct ink_writer reply_writer; - uint8_t reply_body[INK_TX_BUF_SIZE - 512]; - + struct ink_msg incoming; + struct ink_reader read_cursor; + struct ink_writer reply_writer; /* writes into conn->txbuf */ int reply_consumed; int error_sent; }; @@ -79,6 +80,12 @@ struct ink_connection { uint8_t rxbuf[INK_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[INK_TX_BUF_SIZE]; + uint32_t next_serial; struct ink_server *server; /* back-pointer for dispatch */ diff --git a/libink/server.c b/libink/server.c index 919582e9..e629a4c0 100644 --- a/libink/server.c +++ b/libink/server.c @@ -89,12 +89,11 @@ int ink_server_new(ink_server_t **out, const char *path) void ink_server_free(ink_server_t *srv) { - struct ink_object *o, *otmp = NULL; + struct ink_object *o; if (!srv) return; - (void)otmp; /* unused now */ o = TAILQ_FIRST(&srv->objects); while (o) { struct ink_object *next_o = TAILQ_NEXT(o, link); From 3be433c03b3cd5fad86f9eb69d9664476f73739b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 07:46:50 +0200 Subject: [PATCH 05/22] libink: per-method authorization INK_METHOD_PRIVILEGED on ink_method_t; dispatch rejects a privileged call with AccessDenied unless conn->peer_uid is 0. The uid was captured via SO_PEERCRED at accept time and AUTH EXTERNAL refuses a claim that doesn't match it, so the value is trusted here. State-changing Manager1 methods marked privileged: Start, Stop, Restart, Reload, SetRunlevel, Reboot, Poweroff, Halt. ListServices stays public (read-only). Built-ins (Hello, Peer.Ping, Peer.GetMachineId, Introspect) run before the authz check -- they're inherently read-only and never reach user handlers. Test: a "call-s-as-uid" mode in the test client setuid()s before connecting, since the test namespace maps the outer user to inner uid 0. Signed-off-by: Joachim Wiberg --- libink/dispatch.c | 18 ++++++++++++++-- libink/ink.h | 4 ++++ src/dbus.c | 29 +++++++++++++++++--------- test/dbus-auth.sh | 31 ++++++++++++++++++++++++++++ test/src/dbus-auth-client.c | 41 +++++++++++++++++++++++++++++++++++-- 5 files changed, 109 insertions(+), 14 deletions(-) diff --git a/libink/dispatch.c b/libink/dispatch.c index b3780318..15e2d2c3 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -260,8 +260,12 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) "Method call without path or member"); } - /* Built-in DBus interfaces (Hello, Ping, Introspect, Properties) - * are handled here before object-tree lookup. */ + /* Built-in DBus interfaces (Hello, Ping, Introspect) are handled + * here before object-tree lookup, which means they also run + * before the INK_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 + * ink__handle_builtin. */ rc = ink__handle_builtin(conn, m); if (rc >= 0) return rc; /* 0 = handled OK, 1 = built-in but failed; <0 = not a built-in */ @@ -291,6 +295,16 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) "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 & INK_METHOD_PRIVILEGED) && conn->peer_uid != 0) { + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.AccessDenied", + "Method requires root privileges"); + } + memset(&call, 0, sizeof(call)); call.conn = conn; call.incoming = *m; diff --git a/libink/ink.h b/libink/ink.h index b9eca6db..7253376c 100644 --- a/libink/ink.h +++ b/libink/ink.h @@ -52,10 +52,14 @@ void ink_connection_close (ink_connection_t *conn); typedef int (*ink_method_fn)(ink_call_t *call, void *userdata); +/* Method flags for ink_method_t.flags */ +#define INK_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 INK_METHOD_* */ ink_method_fn handler; } ink_method_t; diff --git a/src/dbus.c b/src/dbus.c index bd313eb2..a58f72d3 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -307,16 +307,25 @@ static int manager_poweroff(ink_call_t *c, void *u) { (void)u; return dbus_shutd static int manager_halt (ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); } static const ink_method_t manager_methods[] = { - { .name = "ListServices", .in_sig = "", .out_sig = "as", .handler = manager_list_services }, - { .name = "Start", .in_sig = "s", .out_sig = "", .handler = manager_start }, - { .name = "Stop", .in_sig = "s", .out_sig = "", .handler = manager_stop }, - { .name = "Restart", .in_sig = "s", .out_sig = "", .handler = manager_restart }, - { .name = "Reload", .in_sig = "", .out_sig = "", .handler = manager_reload }, - { .name = "SetRunlevel", .in_sig = "u", .out_sig = "", .handler = manager_set_runlevel }, - { .name = "Reboot", .in_sig = "", .out_sig = "", .handler = manager_reboot }, - { .name = "Poweroff", .in_sig = "", .out_sig = "", .handler = manager_poweroff }, - { .name = "Halt", .in_sig = "", .out_sig = "", .handler = manager_halt }, - { NULL, NULL, NULL, NULL } + { .name = "ListServices", .in_sig = "", .out_sig = "as", + .handler = manager_list_services }, + { .name = "Start", .in_sig = "s", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_start }, + { .name = "Stop", .in_sig = "s", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_stop }, + { .name = "Restart", .in_sig = "s", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_reload }, + { .name = "SetRunlevel", .in_sig = "u", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_set_runlevel }, + { .name = "Reboot", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_reboot }, + { .name = "Poweroff", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_poweroff }, + { .name = "Halt", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = manager_halt }, + { NULL, NULL, NULL, 0, NULL } }; static const ink_vtable_t manager_vtable = { diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 45d287eb..3642c689 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -112,6 +112,37 @@ case "$(cat /tmp/dbus-stop.out)" in *) 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 + # ---------- Error reply ---------- say "Unknown method gets an org.freedesktop.DBus.Error.* reply" diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 69f317bb..8160df44 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -625,6 +625,42 @@ static int mode_call_void(int argc, char *argv[]) return rc; } +/* 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; + uid_t drop_to; + char *ep = NULL; + long v; + int rc; + + if (argc != 8) return 2; + + errno = 0; + v = strtol(argv[2], &ep, 10); + if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) { + fprintf(stderr, "%s: bad uid: %s\n", argv[0], argv[2]); + return 2; + } + drop_to = (uid_t)v; + + if (setuid(drop_to) < 0) { + perror("setuid"); + return 2; + } + + 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; +} + static int mode_unknown(int argc, char *argv[]) { struct reply r; @@ -647,8 +683,9 @@ int main(int argc, char *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], "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], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv); fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); return 2; From ed2a12da2cf92a439566ccb79eb82156ef7f81fa Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 08:51:05 +0200 Subject: [PATCH 06/22] libink: Service1 per-service objects, path encoding Per-service object surface so D-Bus clients can drive one service directly instead of passing its identity as an argument every time. Each loaded service gets a Service1 vtable at /org/finit/service/, where the encoding is systemd-style reversible hex escape: bytes outside [A-Za-z0-9] become "_HH". Methods Start/Stop/Restart/Reload all privileged. Manager1.GetService(s) -> o looks up the canonical path so callers never have to compute encodings themselves. svc_new/svc_del drive dbus_register_service/_unregister so the object tree tracks the service list. dbus_init replays existing services since early conf parsing runs before D-Bus is up. Refactor: extracted service_reload(svc) so api.c (legacy initctl path) and src/dbus.c (Service1.Reload) share the "mark dirty + step" body. Renamed the existing static service_reload helper to service_reload_apply to free the cleaner name. Signed-off-by: Joachim Wiberg --- libink/Makefile.am | 6 +- libink/dispatch.c | 25 ++++++ libink/ink.h | 4 + libink/path.c | 43 ++++++++++ libink/path.h | 21 +++++ src/api.c | 35 +------- src/dbus.c | 158 ++++++++++++++++++++++++++++++++++++ src/private.h | 2 + src/service.c | 64 +++++++++++++-- src/service.h | 1 + src/svc.c | 9 ++ test/dbus-auth.sh | 35 ++++++++ test/src/dbus-auth-client.c | 84 +++++++++++++++---- 13 files changed, 429 insertions(+), 58 deletions(-) create mode 100644 libink/path.c create mode 100644 libink/path.h diff --git a/libink/Makefile.am b/libink/Makefile.am index 3b584928..3f7e48ca 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -4,7 +4,11 @@ libink_la_SOURCES = server.c auth.c connection.c \ proto.c proto.h \ marshal.c marshal.h \ dispatch.c builtin.c \ + path.c \ ink-internal.h + +# Public header: ink.h installed by `ink_HEADERS` below; path.h +# is also exported, so consumers get ink_path_encode. 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 @@ -15,4 +19,4 @@ pkgconfig_DATA = libink.pc # Public header installs to $(includedir)/ink/ink.h inkdir = $(includedir)/ink -ink_HEADERS = ink.h +ink_HEADERS = ink.h path.h diff --git a/libink/dispatch.c b/libink/dispatch.c index 15e2d2c3..beb4cd90 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -24,6 +24,31 @@ static struct ink_object *find_object(ink_server_t *srv, const char *path) return NULL; } +int ink_server_remove_object(ink_server_t *srv, const char *path) +{ + struct ink_object *o; + struct ink_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 ink_server_add_object(ink_server_t *srv, const char *path, const ink_vtable_t *vt, void *userdata) { diff --git a/libink/ink.h b/libink/ink.h index 7253376c..f7d5f69a 100644 --- a/libink/ink.h +++ b/libink/ink.h @@ -75,6 +75,10 @@ typedef struct { int ink_server_add_object(ink_server_t *server, const char *path, const ink_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 ink_server_remove_object(ink_server_t *server, const char *path); + /* ---------- call accessors ---------- */ const char *ink_call_path (const ink_call_t *call); diff --git a/libink/path.c b/libink/path.c new file mode 100644 index 00000000..1899e4b7 --- /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 ink_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..d4f3b149 --- /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 ink_path_encode(const char *in, char *out, size_t outsz); + +#endif /* LIBINK_PATH_H_ */ diff --git a/src/api.c b/src/api.c index a0af4542..be14e215 100644 --- a/src/api.c +++ b/src/api.c @@ -112,41 +112,8 @@ static int restart(svc_t *svc, void *user_data) static int reload(svc_t *svc, void *user_data) { - char cond[MAX_COND_LEN]; - (void)user_data; - - if (!svc) - return 1; - - if (svc_is_blocked(svc)) - svc_start(svc); - else - service_timeout_cancel(svc); - - /* - * Clear conditions before reload to ensure dependent services - * are properly updated. Only needed when the service does NOT - * support SIGHUP (noreload), because then it will be stopped - * and restarted, so conditions genuinely go away. When the - * service handles SIGHUP, its PID and pidfile persist, so the - * condition stays valid and dependents should not be disrupted. - * - * Note: only clear 'ready' for services where the pidfile - * inotify handler reasserts it (pid/none). For s6/systemd - * services readiness relies on their respective notification - * mechanism which may not re-trigger on SIGHUP. - */ - if (svc_is_noreload(svc)) { - cond_clear(mkcond(svc, cond, sizeof(cond))); - if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) - service_ready(svc, 0); - } - - svc_mark_dirty(svc); - service_step(svc); - - return 0; + return service_reload(svc); } static int do_stop (char *buf, size_t len) { return call(stop, buf, len); } diff --git a/src/dbus.c b/src/dbus.c index a58f72d3..cb47bfa8 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -37,6 +37,7 @@ #include #include "ink.h" +#include "path.h" #include "finit.h" #include "conf.h" @@ -131,6 +132,13 @@ static void accept_cb(uev_t *w, void *arg, int events) /* ---------- 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(ink_call_t *call, void *userdata) { ink_writer_t *w; @@ -154,6 +162,37 @@ static int manager_list_services(ink_call_t *call, void *userdata) return 0; } +static int manager_get_service(ink_call_t *call, void *userdata) +{ + const char *ident; + svc_t *svc; + char path[FINIT_SVC_PATH_MAX]; + ink_writer_t *w; + + (void)userdata; + + if (ink_call_read_string(call, &ident) < 0) + return ink_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + svc = svc_find_by_str(ident); + if (!svc) + return ink_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + if (service_path_for(svc, path, sizeof(path)) < 0) + return ink_call_reply_error(call, + "org.finit.Error.Failed", + "Path encoding overflow"); + + w = ink_call_reply(call); + if (!w) + return -1; + ink_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. */ @@ -309,6 +348,8 @@ static int manager_halt (ink_call_t *c, void *u) { (void)u; return dbus_shutd static const ink_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 = INK_METHOD_PRIVILEGED, .handler = manager_start }, { .name = "Stop", .in_sig = "s", .out_sig = "", @@ -333,6 +374,111 @@ static const ink_vtable_t manager_vtable = { .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(ink_call_t *call, void *userdata, + int (*action)(svc_t *, void *)) +{ + svc_t *svc = userdata; + + if (!svc) + return ink_call_reply_error(call, + "org.finit.Error.NoSuchService", + "Service object no longer valid"); + + action(svc, NULL); + (void)ink_call_reply(call); + return 0; +} + +static int service1_start (ink_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); } +static int service1_stop (ink_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); } +static int service1_restart(ink_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); } + +static int service1_reload(ink_call_t *call, void *userdata) +{ + svc_t *svc = userdata; + + if (!svc) + return ink_call_reply_error(call, + "org.finit.Error.NoSuchService", + "Service object no longer valid"); + + service_reload(svc); + (void)ink_call_reply(call); + return 0; +} + +static const ink_method_t service_methods[] = { + { .name = "Start", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = service1_start }, + { .name = "Stop", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = service1_stop }, + { .name = "Restart", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = service1_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = service1_reload }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const ink_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 = ink_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 (ink_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)ink_server_remove_object(server, path); +} + /* ---------- init / exit ---------- */ int dbus_init(uev_ctx_t *ctx) @@ -360,6 +506,18 @@ int dbus_init(uev_ctx_t *ctx) 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; } diff --git a/src/private.h b/src/private.h index aa831af8..82f61192 100644 --- a/src/private.h +++ b/src/private.h @@ -49,6 +49,8 @@ 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); #endif void conf_flush_events(void); diff --git a/src/service.c b/src/service.c index 127e0099..62915796 100644 --- a/src/service.c +++ b/src/service.c @@ -1328,17 +1328,67 @@ int service_stop(svc_t *svc) } /** - * service_reload - Reload a service + * service_reload - Request reload of a service, driven by the state machine * @svc: Service to reload * - * This function does some basic checks of the runtime state of Finit - * and a sanity check of the @svc before sending %SIGHUP or calling - * the reload:script command. + * Mark a service dirty so the state machine will (re-)apply its + * configuration, then advance the state machine. For services that + * don't handle SIGHUP this also clears the readiness condition and + * the pidfile/none-notify readiness flag so dependents are properly + * notified once the service comes back. + * + * Returns: + * POSIX OK(0) on success, non-zero if @svc is NULL. + */ +int service_reload(svc_t *svc) +{ + char cond[MAX_COND_LEN]; + + if (!svc) + return 1; + + if (svc_is_blocked(svc)) + svc_start(svc); + else + service_timeout_cancel(svc); + + /* + * Clear conditions before reload to ensure dependent services + * are properly updated. Only needed when the service does NOT + * support SIGHUP (noreload), because then it will be stopped + * and restarted, so conditions genuinely go away. When the + * service handles SIGHUP, its PID and pidfile persist, so the + * condition stays valid and dependents should not be disrupted. + * + * Note: only clear 'ready' for services where the pidfile + * inotify handler reasserts it (pid/none). For s6/systemd + * services readiness relies on their respective notification + * mechanism which may not re-trigger on SIGHUP. + */ + if (svc_is_noreload(svc)) { + cond_clear(mkcond(svc, cond, sizeof(cond))); + if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) + service_ready(svc, 0); + } + + svc_mark_dirty(svc); + service_step(svc); + + return 0; +} + +/** + * service_reload_apply - Perform the actual reload of a running service + * @svc: Service to reload + * + * Low-level reload step: sends %SIGHUP or runs the reload:script + * command. Called by the state machine when a service marked + * reload-pending by service_reload() reaches the right state. * * Returns: * POSIX OK(0) or non-zero on error. */ -static int service_reload(svc_t *svc) +static int service_reload_apply(svc_t *svc) { const char *id = svc_ident(svc, NULL, 0); int do_progress = 1; @@ -3225,7 +3275,7 @@ int service_step(svc_t *svc) if (sm_in_reload()) break; - service_reload(svc); + service_reload_apply(svc); } svc_mark_clean(svc); @@ -3263,7 +3313,7 @@ int service_step(svc_t *svc) if (svc_is_noreload(svc)) service_stop(svc); else - service_reload(svc); + service_reload_apply(svc); break; } diff --git a/src/service.h b/src/service.h index 5efc2354..9c50de9e 100644 --- a/src/service.h +++ b/src/service.h @@ -43,6 +43,7 @@ void service_forked (svc_t *svc); void service_ready (svc_t *svc, int ready); int service_stop (svc_t *svc); +int service_reload (svc_t *svc); int service_step (svc_t *svc); void service_step_all (int types); void service_worker (void *unused); diff --git a/src/svc.c b/src/svc.c index 272c48f1..c756697d 100644 --- a/src/svc.c +++ b/src/svc.c @@ -38,6 +38,7 @@ #include "finit.h" #include "conf.h" +#include "private.h" #include "svc.h" #include "helpers.h" #include "pid.h" @@ -156,6 +157,10 @@ svc_t *svc_new(char *cmd, char *name, char *id, int type) TAILQ_INSERT_TAIL(&svc_list, svc, link); +#ifdef HAVE_DBUS + dbus_register_service(svc); +#endif + return svc; } @@ -173,6 +178,10 @@ static struct wq work = { */ int svc_del(svc_t *svc) { +#ifdef HAVE_DBUS + dbus_unregister_service(svc); +#endif + TAILQ_REMOVE(&svc_list, svc, link); TAILQ_INSERT_TAIL(&gc_list, svc, link); diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 3642c689..7abac1c0 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -143,6 +143,41 @@ case "$result" in *) 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 + # ---------- Error reply ---------- say "Unknown method gets an org.freedesktop.DBus.Error.* reply" diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 8160df44..12b0ca8d 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -428,12 +428,15 @@ static int read_reply(int fd, struct reply *r) return 0; } -/* Decode a body containing exactly one "s" — write into out, 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 (strcmp(r->signature, "s") != 0 || r->body_len < 5) + 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) @@ -625,26 +628,41 @@ static int mode_call_void(int argc, char *argv[]) return rc; } -/* call-s-as-uid +/* get-service * - * 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[]) + * 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; - uid_t drop_to; - char *ep = NULL; - long v; - int rc; + char path[256]; + int rc; - if (argc != 8) return 2; + 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(argv[2], &ep, 10); + v = strtol(uid_arg, &ep, 10); if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) { - fprintf(stderr, "%s: bad uid: %s\n", argv[0], argv[2]); + fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg); return 2; } drop_to = (uid_t)v; @@ -653,7 +671,23 @@ static int mode_call_s_as_uid(int argc, char *argv[]) perror("setuid"); return 2; } + 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) @@ -661,6 +695,22 @@ static int mode_call_s_as_uid(int argc, char *argv[]) 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; @@ -685,7 +735,9 @@ int main(int argc, char *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], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); + if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); + if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv); + if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv); if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv); fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); return 2; From 12ac7f32e84e8084e385fdd462cbce13c7c82a82 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 11:18:07 +0200 Subject: [PATCH 07/22] libink: D-Bus signals + AddMatch/RemoveMatch Signal emission and per-peer subscription. match.c parses the filter grammar subset we need (type/interface/member/path, single-quoted values); unknown keys are rejected so a peer learns its filter didn't take rather than silently receiving everything. AddMatch/RemoveMatch handlers in builtin.c gate on /org/freedesktop/DBus. ink_connection_emit_signal builds a SIGNAL header, checks the peer's match rules, and sends if anything matches. struct ink_writer is now public so callers can stack-allocate one and marshal signal bodies outside the dispatch path. First user: Manager1.ServiceStateChanged(identity, old, new), emitted from svc_set_state under HAVE_DBUS. Coarse svc_state_t -> string mapping with no `default:` -- a new SVC_*_STATE in svc.h surfaces as -Wswitch rather than silently emitting "unknown". TODO on send_all: a slow peer can stall PID 1 once kernel buffers fill. The peer cap of 64 hides the symptom for now; non-blocking plus drop-on-EAGAIN is the proper fix. Signed-off-by: Joachim Wiberg --- libink/Makefile.am | 1 + libink/builtin.c | 62 +++++++++++ libink/connection.c | 5 + libink/dispatch.c | 53 ++++++++++ libink/ink-internal.h | 27 +++++ libink/ink.h | 41 +++++++- libink/marshal.h | 20 +--- libink/match.c | 199 ++++++++++++++++++++++++++++++++++++ src/dbus.c | 63 ++++++++++++ src/private.h | 5 +- src/service.c | 4 + test/dbus-auth.sh | 36 +++++++ test/src/dbus-auth-client.c | 135 ++++++++++++++++++++++-- 13 files changed, 623 insertions(+), 28 deletions(-) create mode 100644 libink/match.c diff --git a/libink/Makefile.am b/libink/Makefile.am index 3f7e48ca..7935edc4 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -4,6 +4,7 @@ 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 \ ink-internal.h diff --git a/libink/builtin.c b/libink/builtin.c index 1eb68468..d1afdacc 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -231,6 +231,60 @@ static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) return send_string_reply(conn, m, xml); } +/* ---------- AddMatch / RemoveMatch ---------- */ + +static int handle_add_match(ink_connection_t *conn, const struct ink_msg *m) +{ + const char *rule; + struct ink_reader r; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "AddMatch takes a single string"); + + ink__r_init(&r, m->body, m->body_avail); + if (ink__r_string(&r, &rule) < 0) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + if (ink__match_add(conn, rule) < 0) { + if (errno == ENOSPC) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.LimitsExceeded", + "Too many active match rules"); + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.MatchRuleInvalid", + "Unrecognised key or malformed rule"); + } + return ink__send_method_return(conn, m, NULL, NULL, 0); +} + +static int handle_remove_match(ink_connection_t *conn, const struct ink_msg *m) +{ + const char *rule; + struct ink_reader r; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "RemoveMatch takes a single string"); + + ink__r_init(&r, m->body, m->body_avail); + if (ink__r_string(&r, &rule) < 0) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + if (ink__match_remove(conn, rule) < 0) + return ink__send_error(conn, m, + "org.freedesktop.DBus.Error.MatchRuleNotFound", + "No such match rule on this connection"); + + return ink__send_method_return(conn, m, NULL, NULL, 0); +} + /* ---------- entry point ---------- */ int ink__handle_builtin(ink_connection_t *conn, const struct ink_msg *m) @@ -239,6 +293,14 @@ int ink__handle_builtin(ink_connection_t *conn, const struct ink_msg *m) 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); diff --git a/libink/connection.c b/libink/connection.c index dea13e29..90d0b5ee 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -23,9 +23,14 @@ uid_t ink_connection_get_uid(const ink_connection_t *conn) void ink_connection_close(ink_connection_t *conn) { + size_t i; + if (!conn) return; + for (i = 0; i < conn->matches_count; i++) + ink__match_free(conn->matches[i]); + if (conn->fd >= 0) close(conn->fd); free(conn); diff --git a/libink/dispatch.c b/libink/dispatch.c index beb4cd90..132d4d85 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -135,6 +135,10 @@ static const ink_method_t *resolve(struct ink_object *o, /* ---------- send helpers ---------- */ +/* TODO: send_all is blocking. A slow or paused peer that lets its + * socket buffer fill will stall PID 1 here. The 64-peer cap + + * kernel buffer (~256 KB) hide the symptom for now; for production + * hardening, make the fd non-blocking and drop the peer on EAGAIN. */ static int send_all(int fd, const uint8_t *buf, size_t len) { while (len > 0) { @@ -175,6 +179,52 @@ int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, return 0; } +int ink_connection_emit_signal(ink_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 != INK_AUTH_DONE) + return 0; /* peer hasn't finished the SASL phase */ + + for (i = 0; i < conn->matches_count; i++) { + if (ink__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 = ink__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 ink__send_error(ink_connection_t *conn, const struct ink_msg *req, const char *error_name, const char *text) { @@ -252,6 +302,9 @@ int ink_call_read_path (ink_call_t *c, const char **o) { return ink__r_path /* ---------- public writer wrappers ---------- */ +void ink_writer_init (ink_writer_t *w, uint8_t *buf, size_t cap) { ink__w_init(w, buf, cap); } +ssize_t ink_writer_finish(ink_writer_t *w) { return ink__w_finish(w); } + void ink_w_byte (ink_writer_t *w, uint8_t v) { ink__w_byte(w, v); } void ink_w_bool (ink_writer_t *w, int v) { ink__w_bool(w, v); } void ink_w_u32 (ink_writer_t *w, uint32_t v) { ink__w_u32(w, v); } diff --git a/libink/ink-internal.h b/libink/ink-internal.h index 60d4b0a5..3b05d711 100644 --- a/libink/ink-internal.h +++ b/libink/ink-internal.h @@ -25,6 +25,8 @@ typedef enum { #define INK_RX_BUF_SIZE (64 * 1024) #define INK_TX_BUF_SIZE (16 * 1024) #define INK_UNIQUE_NAME_LEN 16 +#define INK_MATCH_RULE_MAX 256 /* per-peer match rule cap */ +#define INK_MATCH_PEER_CAP 16 /* max active match rules per peer */ /* Per-vtable record attached to an object's interface list. */ struct ink_vtable_entry { @@ -66,6 +68,16 @@ struct ink_call { 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 ink_match { + char *raw; /* original string, for RemoveMatch */ + char *type; /* "signal", or NULL */ + char *interface; + char *member; + char *path; +}; + struct ink_connection { int fd; uid_t peer_uid; @@ -77,6 +89,12 @@ struct ink_connection { char linebuf[INK_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 ink_match *matches[INK_MATCH_PEER_CAP]; + size_t matches_count; + uint8_t rxbuf[INK_RX_BUF_SIZE]; size_t rxlen; @@ -106,4 +124,13 @@ int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, /* builtin.c */ int ink__handle_builtin(ink_connection_t *conn, const struct ink_msg *m); +/* match.c */ +struct ink_match *ink__match_parse (const char *rule); +void ink__match_free (struct ink_match *m); +int ink__match_matches(const struct ink_match *m, + const char *path, const char *iface, + const char *member); +int ink__match_add (ink_connection_t *conn, const char *rule); +int ink__match_remove (ink_connection_t *conn, const char *rule); + #endif /* LIBINK_INK_INTERNAL_H_ */ diff --git a/libink/ink.h b/libink/ink.h index f7d5f69a..288138b1 100644 --- a/libink/ink.h +++ b/libink/ink.h @@ -23,6 +23,7 @@ #ifndef LIBINK_INK_H_ #define LIBINK_INK_H_ +#include #include #include @@ -33,7 +34,23 @@ extern "C" { typedef struct ink_server ink_server_t; typedef struct ink_connection ink_connection_t; typedef struct ink_call ink_call_t; -typedef struct ink_writer ink_writer_t; + +/* Writer is exposed so callers can stack-allocate one for marshalling + * signal/reply bodies. Treat the fields as opaque; use ink_writer_init + * + the ink_w_* helpers + ink_writer_finish. Sized for typical D-Bus + * messages -- the array stack supports up to 8 levels of nesting. */ +#define INK_WRITER_MAX_NESTING 8 +typedef struct ink_writer { + uint8_t *buf; + size_t cap; + size_t off; + int err; + struct { + size_t lenpos; + size_t elemstart; + } arrays[INK_WRITER_MAX_NESTING]; + size_t array_depth; +} ink_writer_t; /* ---------- server / connection lifecycle ---------- */ @@ -114,6 +131,28 @@ ink_writer_t *ink_call_reply(ink_call_t *call); * may be NULL. */ int ink_call_reply_error(ink_call_t *call, const char *name, const char *message); +/* ---------- signal emission ---------- + * + * Send a signal to a single peer if its AddMatch rules accept it. + * Callers marshal the body separately and pass the resulting bytes. + * Returns 0 on success (or "filtered out, nothing sent"), -1 on + * underlying transport failure. */ +int ink_connection_emit_signal(ink_connection_t *conn, + const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len); + +/* ---------- standalone writer ---------- + * + * For marshalling bodies outside a method-call handler (signals, + * pre-computed replies). Initialise on a caller-owned buffer, + * write args via ink_w_*, then call ink_writer_finish which + * returns the body length or -1 on overflow. */ +void ink_writer_init (ink_writer_t *w, uint8_t *buf, size_t cap); +ssize_t ink_writer_finish(ink_writer_t *w); + /* ---------- writer (mirrors the internal marshaller) ---------- */ void ink_w_byte (ink_writer_t *w, uint8_t v); diff --git a/libink/marshal.h b/libink/marshal.h index cf128fdf..57f0b5a4 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -9,22 +9,10 @@ #include #include -#define INK_WRITER_MAX_NESTING 8 - -struct ink_writer { - uint8_t *buf; - size_t cap; - size_t off; - int err; - - /* Stack of open arrays, used to back-patch the length prefix - * on array_end with the elements-only byte count. */ - struct { - size_t lenpos; /* offset of the u32 length */ - size_t elemstart; /* offset of first element (post-padding) */ - } arrays[INK_WRITER_MAX_NESTING]; - size_t array_depth; -}; +/* struct ink_writer is defined in ink.h (public). Field layout is + * "opaque" per the public contract; this file's helpers manipulate + * the fields directly. */ +#include "ink.h" void ink__w_init (struct ink_writer *w, uint8_t *buf, size_t cap); ssize_t ink__w_finish(struct ink_writer *w); diff --git a/libink/match.c b/libink/match.c new file mode 100644 index 00000000..8bba7bbc --- /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 "ink-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 ink_match *ink__match_parse(const char *rule) +{ + struct ink_match *m; + const char *p; + + if (!rule || strlen(rule) >= INK_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: + ink__match_free(m); + errno = EINVAL; + return NULL; +} + +void ink__match_free(struct ink_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 ink__match_matches(const struct ink_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 ink__match_add(ink_connection_t *conn, const char *rule) +{ + struct ink_match *m; + + if (conn->matches_count >= INK_MATCH_PEER_CAP) { + errno = ENOSPC; + return -1; + } + + m = ink__match_parse(rule); + if (!m) + return -1; + + conn->matches[conn->matches_count++] = m; + return 0; +} + +int ink__match_remove(ink_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) { + ink__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/src/dbus.c b/src/dbus.c index cb47bfa8..269bb03f 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -479,6 +479,69 @@ void dbus_unregister_service(svc_t *svc) (void)ink_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 -Wswitch-enum makes the missing + * case a build error. */ +static const char *state_name(svc_state_t s) +{ + switch (s) { + case SVC_HALTED_STATE: return "halted"; + case SVC_DONE_STATE: return "done"; + case SVC_DEAD_STATE: return "dead"; + case SVC_CLEANUP_STATE: return "cleanup"; + case SVC_TEARDOWN_STATE: return "teardown"; + case SVC_STOPPING_STATE: return "stopping"; + case SVC_SETUP_STATE: return "setup"; + case SVC_PAUSED_STATE: return "paused"; + case SVC_WAITING_STATE: return "waiting"; + case SVC_STARTING_STATE: return "starting"; + case SVC_RUNNING_STATE: return "running"; + } + return "unknown"; +} + +void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) +{ + uint8_t body[256]; + ink_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)); + + ink_writer_init(&w, body, sizeof(body)); + ink_w_string(&w, ident); + ink_w_string(&w, state_name(o)); + ink_w_string(&w, state_name(n)); + blen = ink_writer_finish(&w); + if (blen < 0) + return; + + TAILQ_FOREACH(p, &peers, link) + (void)ink_connection_emit_signal(p->conn, + "/org/finit/manager", + "org.finit.Manager1", + "ServiceStateChanged", + "sss", + body, (size_t)blen); +} + /* ---------- init / exit ---------- */ int dbus_init(uev_ctx_t *ctx) diff --git a/src/private.h b/src/private.h index 82f61192..18ad9c0b 100644 --- a/src/private.h +++ b/src/private.h @@ -49,8 +49,9 @@ 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_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); #endif void conf_flush_events(void); diff --git a/src/service.c b/src/service.c index 62915796..ecabbc77 100644 --- a/src/service.c +++ b/src/service.c @@ -2914,6 +2914,10 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state) return; *state = new_state; +#ifdef HAVE_DBUS + dbus_notify_service_state(svc, old_state, new_state); +#endif + if (svc_is_runtask(svc)) { char success[MAX_COND_LEN], failure[MAX_COND_LEN]; diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 7abac1c0..4eee22eb 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -178,6 +178,42 @@ case "$(cat /tmp/dbus-svcauthz.out)" in *) 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 + +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 + # ---------- Error reply ---------- say "Unknown method gets an org.freedesktop.DBus.Error.* reply" diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 12b0ca8d..00e275a4 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -38,6 +38,7 @@ #include #include #include +#include static const char hex[] = "0123456789abcdef"; @@ -80,6 +81,25 @@ static int read_full(int fd, void *buf, size_t len) 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; @@ -338,20 +358,36 @@ struct reply { uint32_t body_len; char signature[64]; char error_name[128]; + char interface[128]; + char member[128]; uint8_t body[8192]; }; -static int read_reply(int fd, struct reply *r) +/* 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 (read_full(fd, hdr_fixed, 16) < 0) return -1; + 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] @@ -369,7 +405,6 @@ static int read_reply(int fd, struct reply *r) if (fields_len > sizeof(hdr_fields)) return -1; if (read_full(fd, hdr_fields, fields_len) < 0) return -1; - /* Skip header padding to 8 bytes. */ body_off = (size_t)ALIGN_UP(16 + fields_len, 8); if (body_off > 16 + fields_len) { uint8_t pad[8]; @@ -377,22 +412,25 @@ static int read_reply(int fd, struct reply *r) return -1; } - /* Walk header fields, capture SIGNATURE and ERROR_NAME. */ 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; - const char *vsig = (const char *)(hdr_fields + pos); + 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] @@ -401,9 +439,16 @@ static int read_reply(int fd, struct reply *r) | ((uint32_t)hdr_fields[pos + 3] << 24); pos += 4; if (pos + slen + 1 > fields_len) return -1; - if (code == 4 && slen < sizeof(r->error_name)) { - memcpy(r->error_name, hdr_fields + pos, slen); - r->error_name[slen] = '\0'; + + 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') { @@ -538,7 +583,7 @@ static int do_call_arg(const char *path, const char *obj_path, close(fd); return 2; } - if (read_reply(fd, r) < 0) { + if (read_reply(fd, r, 0) < 0) { fprintf(stderr, "read_reply\n"); close(fd); return 2; @@ -674,6 +719,77 @@ static int drop_uid_from_arg(const char *uid_arg, const char *progname) 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 @@ -735,6 +851,7 @@ int main(int argc, char *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); From d2d5eab6e4bb63ccefd72c02540078907085e61b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 12:16:13 +0200 Subject: [PATCH 08/22] libink: org.finit.Cond1 with ConditionChanged signal Exposes Finit's condition engine on D-Bus at /org/finit/cond: Get (s) -> s state ("on"/"off"/"flux") for any condition Set (s) -> () assert a usr/* condition [privileged] Clear (s) -> () deassert a usr/* condition [privileged] List () -> as names of all asserted conditions Dump () -> a(ss) (name, state) pairs Set/Clear are restricted to usr/* so clients can't twiddle pid/sys/hook entries owned by Finit's state machine. Names are normalised the way initctl does it: bare "foo" becomes "usr/foo", periods rejected, multi-slash tails rejected. ConditionChanged fires from cond_set/cond_set_oneshot/cond_clear, after the state is persisted and before cond_update notifies dependents -- so subscribers see the new state before service-side reactions land. Signed-off-by: Joachim Wiberg --- src/cond-w.c | 16 +++ src/dbus.c | 253 ++++++++++++++++++++++++++++++++++++++++++++++ src/private.h | 1 + test/dbus-auth.sh | 56 ++++++++++ 4 files changed, 326 insertions(+) diff --git a/src/cond-w.c b/src/cond-w.c index 4ec119d2..d4489494 100644 --- a/src/cond-w.c +++ b/src/cond-w.c @@ -37,6 +37,13 @@ #include "service.h" #include "sm.h" +#ifdef HAVE_DBUS +/* Forward-declared locally to keep cond-w.c independent of the + * daemon's private.h (which pulls in svc/plugin headers). The full + * prototype lives in private.h for callers in finit's main loop. */ +void dbus_notify_condition_change(const char *name, const char *state); +#endif + struct cond_boot { TAILQ_ENTRY(cond_boot) link; char *name; @@ -328,6 +335,9 @@ void cond_set(const char *name) if (cond_set_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "on"); +#endif cond_update(name); } @@ -358,6 +368,9 @@ void cond_set_oneshot(const char *name) if (cond_set_oneshot_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "on"); +#endif cond_update(name); } @@ -379,6 +392,9 @@ void cond_clear(const char *name) if (cond_clear_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "off"); +#endif cond_update(name); } diff --git a/src/dbus.c b/src/dbus.c index 269bb03f..22c024c5 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -36,10 +36,13 @@ #include #include +#include + #include "ink.h" #include "path.h" #include "finit.h" +#include "cond.h" #include "conf.h" #include "log.h" #include "private.h" @@ -542,6 +545,248 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) 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(ink_call_t *call, void *userdata) +{ + const char *name; + ink_writer_t *w; + + (void)userdata; + + if (ink_call_read_string(call, &name) < 0) + return ink_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + if (!cond_name_valid(name)) + return ink_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "invalid condition name"); + + w = ink_call_reply(call); + if (!w) + return -1; + ink_w_string(w, condstr(cond_get(name))); + return 0; +} + +static int cond1_set_or_clear(ink_call_t *call, int do_set) +{ + const char *name; + char buf[128]; + const char *full; + + if (ink_call_read_string(call, &name) < 0) + return ink_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + full = normalise_usr_cond(name, buf, sizeof(buf)); + if (!full) + return ink_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)ink_call_reply(call); + return 0; +} + +static int cond1_set (ink_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); } +static int cond1_clear(ink_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 ink_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)); + ink_w_struct_begin(cond_walk_writer); + ink_w_string(cond_walk_writer, name); + ink_w_string(cond_walk_writer, state); + ink_w_struct_end(cond_walk_writer); + } else { + ink_w_string(cond_walk_writer, name); + } + return 0; +} + +static int cond1_list(ink_call_t *call, void *userdata) +{ + ink_writer_t *w; + + (void)userdata; + + w = ink_call_reply(call); + if (!w) + return -1; + + ink_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; + ink_w_array_end(w); + return 0; +} + +static int cond1_dump(ink_call_t *call, void *userdata) +{ + ink_writer_t *w; + + (void)userdata; + + w = ink_call_reply(call); + if (!w) + return -1; + + ink_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; + ink_w_array_end(w); + return 0; +} + +static const ink_method_t cond_methods[] = { + { .name = "Get", .in_sig = "s", .out_sig = "s", + .handler = cond1_get }, + { .name = "Set", .in_sig = "s", .out_sig = "", + .flags = INK_METHOD_PRIVILEGED, .handler = cond1_set }, + { .name = "Clear", .in_sig = "s", .out_sig = "", + .flags = INK_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 ink_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]; + ink_writer_t w; + struct peer *p; + ssize_t blen; + + if (!server || !name || !state) + return; + if (TAILQ_EMPTY(&peers)) + return; + + ink_writer_init(&w, body, sizeof(body)); + ink_w_string(&w, name); + ink_w_string(&w, state); + blen = ink_writer_finish(&w); + if (blen < 0) + return; + + TAILQ_FOREACH(p, &peers, link) + (void)ink_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) @@ -561,6 +806,14 @@ int dbus_init(uev_ctx_t *ctx) return 1; } + if (ink_server_add_object(server, COND_PATH_OBJECT, + &cond_vtable, NULL) < 0) { + err(1, "Failed registering Cond1 object"); + ink_server_free(server); + server = NULL; + return 1; + } + if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, ink_server_get_fd(server), UEV_READ)) { err(1, "Failed registering D-Bus accept watcher"); diff --git a/src/private.h b/src/private.h index 18ad9c0b..3b03f510 100644 --- a/src/private.h +++ b/src/private.h @@ -52,6 +52,7 @@ 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); diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 4eee22eb..818ce7b6 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -202,6 +202,62 @@ case "$(cat /tmp/dbus-sig.out)" in 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 \ From 7218f8f267e208c35d18dd55a7b4d75df804721f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 12:47:20 +0200 Subject: [PATCH 09/22] initctl: route Start/Stop/Restart/Reload/Reboot via D-Bus initctl now talks to /run/finit/bus for the simple state-changing commands when D-Bus is reachable, falling back automatically to the legacy INIT_SOCKET when it isn't (--disable-dbus, or finit built without HAVE_DBUS). CLI surface unchanged. Routes: initctl start/stop/restart -> Manager1.{Start,Stop,Restart} initctl reload -> Manager1.Reload (no args) initctl reboot/halt/poweroff -> Manager1.{Reboot,Halt,Poweroff} The reboot/halt/poweroff path keeps the legacy sleep(5), since /run/finit/bus is about to disappear once the shutdown is acted on. Still on the legacy IPC: per-service reload (needs ink_path_encode), suspend (no Manager1.Suspend yet), and the read-only commands. Marked in the code. Approach: rather than link libink into initctl, this commit adds a minimal client wire implementation in src/dbus-client.{c,h}. That duplicates marshalling with libink and the test client; a follow-up folds them together once libink grows a client side. Test: dbus-auth.sh subscribes to Manager1.ServiceStateChanged in a background monitor and runs initctl restart keventd -- only the D-Bus path fires that signal, so observing it proves the route. Signed-off-by: Joachim Wiberg --- src/Makefile.am | 3 + src/dbus-client.c | 446 ++++++++++++++++++++++++++++++++++++++++++++++ src/dbus-client.h | 48 +++++ src/initctl.c | 118 +++++++++++- test/dbus-auth.sh | 35 ++++ 5 files changed, 644 insertions(+), 6 deletions(-) create mode 100644 src/dbus-client.c create mode 100644 src/dbus-client.h diff --git a/src/Makefile.am b/src/Makefile.am index aec3af55..25f6ae28 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -107,6 +107,9 @@ 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_SOURCES += dbus-client.c dbus-client.h +endif INIT_LNKS = init telinit REBOOT_LNKS = reboot shutdown halt poweroff suspend diff --git a/src/dbus-client.c b/src/dbus-client.c new file mode 100644 index 00000000..e336103d --- /dev/null +++ b/src/dbus-client.c @@ -0,0 +1,446 @@ +/* Minimal D-Bus client for initctl. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include "config.h" + +#ifdef HAVE_DBUS + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus-client.h" + +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +struct dbusc { + int fd; + uint32_t next_serial; +}; + +static const char hex[] = "0123456789abcdef"; + +/* ---- transport helpers ---- */ + +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 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; +} + +/* ---- SASL handshake ---- */ + +static int do_auth(int fd) +{ + char uidstr[16]; + char hexuid[32]; + char line[64]; + char reply[256]; + size_t i, n; + int rc; + + if (write_all(fd, "\0", 1) < 0) + return -1; + + /* geteuid() matches what the kernel reports via SO_PEERCRED on + * the receiving side; getuid() would diverge if initctl is ever + * shipped setuid (it isn't today, but precision is cheap). */ + n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", + (unsigned)geteuid()); + if (n * 2 >= sizeof(hexuid)) + return -1; + 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'; + + 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 (read_line(fd, reply, sizeof(reply)) < 0) + return -1; + if (strncmp(reply, "OK ", 3) != 0) + return -1; + if (write_all(fd, "BEGIN\r\n", 7) < 0) + return -1; + return 0; +} + +/* ---- public ---- */ + +dbusc_t *dbusc_open(const char *path) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + dbusc_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 (do_auth(fd) < 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 dbusc_close(dbusc_t *c) +{ + if (!c) + return; + if (c->fd >= 0) + close(c->fd); + free(c); +} + +/* ---- 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_sig(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; +} + +static int send_method_call(struct dbusc *c, + const char *obj_path, + const char *iface, + const char *member, + const char *arg_sig, + const char *arg_str, + 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) }; + uint32_t body_len = 0; + size_t fields_start, fields_end, padded_end; + uint32_t serial = c->next_serial++; + + if (arg_sig && *arg_sig) { + if (!strcmp(arg_sig, "s")) + b_put_string(&bb, arg_str ? arg_str : ""); + else if (!strcmp(arg_sig, "u")) + b_put_u32(&bb, arg_u32); + else + return -1; + if (bb.err) return -1; + body_len = (uint32_t)bb.off; + } + + memset(hdr, 0, 16); + hdr[0] = 'l'; + hdr[1] = 1; /* METHOD_CALL */ + 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] = (uint8_t)( serial & 0xff); + hdr[9] = (uint8_t)((serial >> 8) & 0xff); + hdr[10] = (uint8_t)((serial >> 16) & 0xff); + hdr[11] = (uint8_t)((serial >> 24) & 0xff); + b.off = 16; + fields_start = b.off; + + /* PATH (code 1, type 'o') */ + b_reserve(&b, 8, 0); + b_put_byte(&b, 1); + b_put_sig (&b, "o"); + b_put_string(&b, obj_path); + + if (iface) { + b_reserve(&b, 8, 0); + b_put_byte(&b, 2); + b_put_sig (&b, "s"); + b_put_string(&b, iface); + } + + b_reserve(&b, 8, 0); + b_put_byte(&b, 3); + b_put_sig (&b, "s"); + b_put_string(&b, member); + + if (arg_sig && *arg_sig) { + b_reserve(&b, 8, 0); + b_put_byte(&b, 8); + b_put_sig (&b, "g"); + b_put_sig (&b, arg_sig); + } + + 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(c->fd, hdr, b.off) < 0) return -1; + if (body_len > 0 && write_all(c->fd, body, body_len) < 0) return -1; + return 0; +} + +/* Read one method-return or error reply. Captures the ERROR_NAME + * header field on type=3 messages; everything else is consumed and + * discarded. */ +static int read_reply(int fd, uint8_t *out_type, + char *err_buf, size_t err_buf_sz) +{ + uint8_t hdr_fixed[16]; + uint8_t hdr_fields[2048]; + uint8_t body_dump[1024]; + uint32_t body_len, fields_len; + size_t body_off; + size_t pos; + + if (err_buf && err_buf_sz) + err_buf[0] = '\0'; + + if (read_full(fd, hdr_fixed, 16) < 0) + return -1; + if (hdr_fixed[0] != 'l') + return -1; + *out_type = hdr_fixed[1]; + 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); + 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; + + 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; + if (code == 4 && err_buf && slen < err_buf_sz) { + memcpy(err_buf, hdr_fields + pos, slen); + err_buf[slen] = '\0'; + } + pos += slen + 1; + } else if (vsig[0] == 'g') { + uint32_t slen = hdr_fields[pos++]; + + if (pos + slen + 1 > fields_len) return -1; + pos += slen + 1; + } else if (vsig[0] == 'u') { + pos = ALIGN_UP(pos, 4); + pos += 4; + } else { + return -1; + } + } + + /* Drain the body in chunks of body_dump[]. */ + while (body_len > 0) { + size_t take = body_len > sizeof(body_dump) + ? sizeof(body_dump) : body_len; + + if (read_full(fd, body_dump, take) < 0) + return -1; + body_len -= (uint32_t)take; + } + return 0; +} + +int dbusc_call(dbusc_t *c, + const char *obj_path, + const char *iface, + const char *method, + const char *arg_sig, + const char *arg_str, + uint32_t arg_u32, + char *err_buf, + size_t err_buf_sz) +{ + uint8_t type = 0; + + if (!c || c->fd < 0 || !obj_path || !iface || !method) + return -1; + + if (send_method_call(c, obj_path, iface, method, + arg_sig, arg_str, arg_u32) < 0) + return -1; + if (read_reply(c->fd, &type, err_buf, err_buf_sz) < 0) + return -1; + + if (type == 2) /* METHOD_RETURN */ + return 0; + if (type == 3) /* ERROR */ + return 1; + return -1; +} + +#endif /* HAVE_DBUS */ diff --git a/src/dbus-client.h b/src/dbus-client.h new file mode 100644 index 00000000..a386bdd1 --- /dev/null +++ b/src/dbus-client.h @@ -0,0 +1,48 @@ +/* Minimal D-Bus client used by initctl when /run/finit/bus is + * available. This is the client side of the protocol implemented + * by libink server-side; we don't link libink because that would + * pull dispatch + builtins + match into initctl unnecessarily. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef FINIT_DBUS_CLIENT_H_ +#define FINIT_DBUS_CLIENT_H_ + +#include +#include + +#ifdef HAVE_DBUS + +typedef struct dbusc dbusc_t; + +/* Connect, AUTH EXTERNAL with the caller's real uid, send BEGIN. + * Returns NULL on any failure (no warning printed -- caller is + * expected to fall back to a different transport). */ +dbusc_t *dbusc_open(const char *path); + +void dbusc_close(dbusc_t *c); + +/* Send a method call, await reply. arg_sig must be "" (no body), + * "s" (one string argument), or "u" (one uint32 argument). On an + * error reply, the error name is copied into err_buf (which may be + * NULL). + * + * Returns: + * 0 method return (success) + * 1 error reply -- err_buf has the org.* error name + * -1 transport / parse failure + */ +int dbusc_call(dbusc_t *c, + const char *obj_path, + const char *iface, + const char *method, + const char *arg_sig, + const char *arg_str, + uint32_t arg_u32, + char *err_buf, + size_t err_buf_sz); + +#endif /* HAVE_DBUS */ + +#endif /* FINIT_DBUS_CLIENT_H_ */ diff --git a/src/initctl.c b/src/initctl.c index 435e2da9..1f74bb21 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -268,19 +268,87 @@ 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 "dbus-client.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) +{ + dbusc_t *c; + char err[128]; + int rc; + + c = dbusc_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + err[0] = '\0'; + rc = dbusc_call(c, "/org/finit/manager", "org.finit.Manager1", + method, arg_sig, arg, 0, err, sizeof(err)); + dbusc_close(c); + + if (rc == 1) { + /* 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") == 0) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", + arg ? arg : ""); + if (strcmp(err, "org.freedesktop.DBus.Error.AccessDenied") == 0) + ERRX(1, "permission denied: %s requires root", method); + ERRX(1, "%s: %s", method, err[0] ? err : "D-Bus error"); + } + return rc; +} +#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); @@ -641,9 +709,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/test/dbus-auth.sh b/test/dbus-auth.sh index 818ce7b6..27bf6bea 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -270,6 +270,41 @@ case "$(cat /tmp/dbus-match.out)" in *) 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" From 483a202327f17103e44f7bbaa08cc12e12f26693 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 13:18:37 +0200 Subject: [PATCH 10/22] libink: rename API ink_* -> link_* (and INK_* -> LINK_*) Mechanical sweep across libink, src/dbus.c, src/dbus-client.* and test/src/dbus-auth-client.c. No behaviour change. The library is libink (linked with -link); the API operates on D-Bus connections -- which are links between peers. The link_* prefix matches the linker-flag pun and reads more naturally at call sites. ink_* was an artefact of the early naming discussion that didn't survive the API growing. Signed-off-by: Joachim Wiberg --- libink/auth.c | 28 ++--- libink/builtin.c | 84 +++++++------- libink/connection.c | 26 ++--- libink/dispatch.c | 148 ++++++++++++------------ libink/ink-internal.h | 102 ++++++++--------- libink/ink.h | 104 ++++++++--------- libink/marshal.c | 56 ++++----- libink/marshal.h | 44 ++++---- libink/match.c | 24 ++-- libink/path.c | 2 +- libink/path.h | 2 +- libink/proto.c | 64 +++++------ libink/proto.h | 48 ++++---- libink/server.c | 26 ++--- src/dbus.c | 256 +++++++++++++++++++++--------------------- 15 files changed, 507 insertions(+), 507 deletions(-) diff --git a/libink/auth.c b/libink/auth.c index a458b32f..876b064e 100644 --- a/libink/auth.c +++ b/libink/auth.c @@ -49,12 +49,12 @@ static int reply(int fd, const char *line) return write_all(fd, line, strlen(line)); } -static int reject(ink_connection_t *conn) +static int reject(link_connection_t *conn) { return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1); } -void ink__auth_generate_guid(char out[33]) +void link__auth_generate_guid(char out[33]) { static const char hex[] = "0123456789abcdef"; uint8_t raw[16]; @@ -116,7 +116,7 @@ static int parse_external_uid(const char *arg, size_t arglen, uid_t *out) return 0; } -static int handle_line(ink_connection_t *conn, const char *line, size_t len) +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; @@ -138,7 +138,7 @@ static int handle_line(ink_connection_t *conn, const char *line, size_t len) return reply(conn->fd, "ERROR fd-passing not supported\r\n"); if (len == 5 && memcmp(line, "BEGIN", 5) == 0) { - conn->auth = INK_AUTH_DONE; + conn->auth = LINK_AUTH_DONE; return 0; } @@ -154,7 +154,7 @@ static int handle_line(ink_connection_t *conn, const char *line, size_t len) /* 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(ink_connection_t *conn, char *out, size_t outsz) +static size_t take_line(link_connection_t *conn, char *out, size_t outsz) { size_t i; @@ -178,7 +178,7 @@ static size_t take_line(ink_connection_t *conn, char *out, size_t outsz) return 0; } -int ink__auth_process(ink_connection_t *conn) +int link__auth_process(link_connection_t *conn) { uint8_t buf[256]; ssize_t n; @@ -193,22 +193,22 @@ int ink__auth_process(ink_connection_t *conn) return -1; } - if (conn->auth == INK_AUTH_NUL) { + if (conn->auth == LINK_AUTH_NUL) { if (buf[0] != 0x00) { - conn->auth = INK_AUTH_FAILED; + conn->auth = LINK_AUTH_FAILED; return -1; } off = 1; - conn->auth = INK_AUTH_LINE; + conn->auth = LINK_AUTH_LINE; } - if (conn->auth == INK_AUTH_LINE) { + if (conn->auth == LINK_AUTH_LINE) { size_t take = (size_t)n - off; - char line[INK_AUTH_LINEBUF_SIZE]; + char line[LINK_AUTH_LINEBUF_SIZE]; size_t linelen; if (conn->linelen + take > sizeof(conn->linebuf)) { - conn->auth = INK_AUTH_FAILED; + conn->auth = LINK_AUTH_FAILED; return -1; } memcpy(conn->linebuf + conn->linelen, buf + off, take); @@ -217,7 +217,7 @@ int ink__auth_process(ink_connection_t *conn) while ((linelen = take_line(conn, line, sizeof(line))) > 0) { if (handle_line(conn, line, linelen) < 0) return -1; - if (conn->auth != INK_AUTH_LINE) + if (conn->auth != LINK_AUTH_LINE) break; } @@ -225,7 +225,7 @@ int ink__auth_process(ink_connection_t *conn) * 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 == INK_AUTH_DONE && conn->linelen > 0) { + if (conn->auth == LINK_AUTH_DONE && conn->linelen > 0) { if (conn->linelen > sizeof(conn->rxbuf)) return -1; memcpy(conn->rxbuf, conn->linebuf, conn->linelen); diff --git a/libink/builtin.c b/libink/builtin.c index d1afdacc..acf9198b 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -19,7 +19,7 @@ /* ---------- helpers ---------- */ -static int member_is(const struct ink_msg *m, const char *iface, const char *member) +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; @@ -28,25 +28,25 @@ static int member_is(const struct ink_msg *m, const char *iface, const char *mem return 1; } -static int send_string_reply(ink_connection_t *conn, const struct ink_msg *req, +static int send_string_reply(link_connection_t *conn, const struct link_msg *req, const char *s) { - struct ink_writer w; + struct link_writer w; ssize_t blen; - ink__w_init(&w, conn->txbuf, sizeof(conn->txbuf)); - ink__w_string(&w, s); - blen = ink__w_finish(&w); + link__w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + link__w_string(&w, s); + blen = link__w_finish(&w); if (blen < 0) { errno = EMSGSIZE; return -1; } - return ink__send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); + return link__send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); } /* ---------- Hello ---------- */ -static int handle_hello(ink_connection_t *conn, const struct ink_msg *m) +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; @@ -59,12 +59,12 @@ static int handle_hello(ink_connection_t *conn, const struct ink_msg *m) /* ---------- Ping / GetMachineId ---------- */ -static int handle_ping(ink_connection_t *conn, const struct ink_msg *m) +static int handle_ping(link_connection_t *conn, const struct link_msg *m) { - return ink__send_method_return(conn, m, NULL, NULL, 0); + return link__send_method_return(conn, m, NULL, NULL, 0); } -static int handle_get_machine_id(ink_connection_t *conn, const struct ink_msg *m) +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, @@ -74,7 +74,7 @@ static int handle_get_machine_id(ink_connection_t *conn, const struct ink_msg *m static char machine_id[33]; if (!machine_id[0]) - ink__auth_generate_guid(machine_id); + link__auth_generate_guid(machine_id); return send_string_reply(conn, m, machine_id); } @@ -105,7 +105,7 @@ static void xprintf(struct xbuf *x, const char *fmt, ...) } /* Emit a single stanza for one method definition. */ -static void emit_method(struct xbuf *x, const ink_method_t *m) +static void emit_method(struct xbuf *x, const link_method_t *m) { const char *p; @@ -166,11 +166,11 @@ static int child_segment(const char *parent, const char *child, return 1; } -static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) +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 ink_object *o; + struct link_object *o; const char *path = m->path; xprintf(&x, @@ -182,7 +182,7 @@ static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) o = NULL; { - struct ink_object *p; + struct link_object *p; TAILQ_FOREACH(p, &conn->server->objects, link) { if (strcmp(p->path, path) == 0) { @@ -193,8 +193,8 @@ static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) } if (o) { - struct ink_vtable_entry *e; - const ink_method_t *meth; + struct link_vtable_entry *e; + const link_method_t *meth; TAILQ_FOREACH(e, &o->vtables, link) { xprintf(&x, " \n", @@ -207,9 +207,9 @@ static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) } { - struct ink_object *p; - char prev_seg[INK_PATH_MAX] = { 0 }; - char seg [INK_PATH_MAX]; + 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))) @@ -224,7 +224,7 @@ static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) xprintf(&x, "\n"); if (x.err) - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.Failed", "Introspection XML overflow"); @@ -233,61 +233,61 @@ static int handle_introspect(ink_connection_t *conn, const struct ink_msg *m) /* ---------- AddMatch / RemoveMatch ---------- */ -static int handle_add_match(ink_connection_t *conn, const struct ink_msg *m) +static int handle_add_match(link_connection_t *conn, const struct link_msg *m) { const char *rule; - struct ink_reader r; + struct link_reader r; if (!m->signature || strcmp(m->signature, "s") != 0) - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "AddMatch takes a single string"); - ink__r_init(&r, m->body, m->body_avail); - if (ink__r_string(&r, &rule) < 0) - return ink__send_error(conn, m, + link__r_init(&r, m->body, m->body_avail); + if (link__r_string(&r, &rule) < 0) + return link__send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Malformed argument"); - if (ink__match_add(conn, rule) < 0) { + if (link__match_add(conn, rule) < 0) { if (errno == ENOSPC) - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.LimitsExceeded", "Too many active match rules"); - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.MatchRuleInvalid", "Unrecognised key or malformed rule"); } - return ink__send_method_return(conn, m, NULL, NULL, 0); + return link__send_method_return(conn, m, NULL, NULL, 0); } -static int handle_remove_match(ink_connection_t *conn, const struct ink_msg *m) +static int handle_remove_match(link_connection_t *conn, const struct link_msg *m) { const char *rule; - struct ink_reader r; + struct link_reader r; if (!m->signature || strcmp(m->signature, "s") != 0) - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "RemoveMatch takes a single string"); - ink__r_init(&r, m->body, m->body_avail); - if (ink__r_string(&r, &rule) < 0) - return ink__send_error(conn, m, + link__r_init(&r, m->body, m->body_avail); + if (link__r_string(&r, &rule) < 0) + return link__send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Malformed argument"); - if (ink__match_remove(conn, rule) < 0) - return ink__send_error(conn, m, + if (link__match_remove(conn, rule) < 0) + return link__send_error(conn, m, "org.freedesktop.DBus.Error.MatchRuleNotFound", "No such match rule on this connection"); - return ink__send_method_return(conn, m, NULL, NULL, 0); + return link__send_method_return(conn, m, NULL, NULL, 0); } /* ---------- entry point ---------- */ -int ink__handle_builtin(ink_connection_t *conn, const struct ink_msg *m) +int link__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) diff --git a/libink/connection.c b/libink/connection.c index 90d0b5ee..030573cc 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -11,17 +11,17 @@ #include "ink-internal.h" -int ink_connection_get_fd(const ink_connection_t *conn) +int link_connection_get_fd(const link_connection_t *conn) { return conn ? conn->fd : -1; } -uid_t ink_connection_get_uid(const ink_connection_t *conn) +uid_t link_connection_get_uid(const link_connection_t *conn) { return conn ? conn->peer_uid : (uid_t)-1; } -void ink_connection_close(ink_connection_t *conn) +void link_connection_close(link_connection_t *conn) { size_t i; @@ -29,7 +29,7 @@ void ink_connection_close(ink_connection_t *conn) return; for (i = 0; i < conn->matches_count; i++) - ink__match_free(conn->matches[i]); + link__match_free(conn->matches[i]); if (conn->fd >= 0) close(conn->fd); @@ -40,19 +40,19 @@ void ink_connection_close(ink_connection_t *conn) * 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(ink_connection_t *conn) +static int process_binary(link_connection_t *conn) { while (conn->rxlen > 0) { - struct ink_msg msg; + struct link_msg msg; ssize_t consumed; - consumed = ink__msg_parse(conn->rxbuf, conn->rxlen, &msg); + consumed = link__msg_parse(conn->rxbuf, conn->rxlen, &msg); if (consumed == 0) break; /* incomplete; wait for more bytes */ if (consumed < 0) return -1; - if (ink__dispatch_message(conn, &msg) < 0) + if (link__dispatch_message(conn, &msg) < 0) return -1; memmove(conn->rxbuf, conn->rxbuf + consumed, @@ -62,22 +62,22 @@ static int process_binary(ink_connection_t *conn) return 0; } -int ink_connection_process(ink_connection_t *conn) +int link_connection_process(link_connection_t *conn) { if (!conn) { errno = EINVAL; return -1; } - if (conn->auth == INK_AUTH_FAILED) + if (conn->auth == LINK_AUTH_FAILED) return -1; - if (conn->auth != INK_AUTH_DONE) { - if (ink__auth_process(conn) < 0) + if (conn->auth != LINK_AUTH_DONE) { + if (link__auth_process(conn) < 0) return -1; /* Still in SASL phase — wait for more bytes. */ - if (conn->auth != INK_AUTH_DONE) + if (conn->auth != LINK_AUTH_DONE) return 0; /* Fall through: BEGIN may have arrived in the same read diff --git a/libink/dispatch.c b/libink/dispatch.c index 132d4d85..e7ac7503 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -14,9 +14,9 @@ /* ---------- object/vtable registration ---------- */ -static struct ink_object *find_object(ink_server_t *srv, const char *path) +static struct link_object *find_object(link_server_t *srv, const char *path) { - struct ink_object *o; + struct link_object *o; TAILQ_FOREACH(o, &srv->objects, link) if (strcmp(o->path, path) == 0) @@ -24,10 +24,10 @@ static struct ink_object *find_object(ink_server_t *srv, const char *path) return NULL; } -int ink_server_remove_object(ink_server_t *srv, const char *path) +int link_server_remove_object(link_server_t *srv, const char *path) { - struct ink_object *o; - struct ink_vtable_entry *e; + struct link_object *o; + struct link_vtable_entry *e; if (!srv || !path) { errno = EINVAL; @@ -49,11 +49,11 @@ int ink_server_remove_object(ink_server_t *srv, const char *path) return 0; } -int ink_server_add_object(ink_server_t *srv, const char *path, - const ink_vtable_t *vt, void *userdata) +int link_server_add_object(link_server_t *srv, const char *path, + const link_vtable_t *vt, void *userdata) { - struct ink_object *o; - struct ink_vtable_entry *e; + struct link_object *o; + struct link_vtable_entry *e; size_t plen; if (!srv || !path || !*path || !vt || !vt->interface) { @@ -61,7 +61,7 @@ int ink_server_add_object(ink_server_t *srv, const char *path, return -1; } plen = strlen(path); - if (plen >= INK_PATH_MAX) { + if (plen >= LINK_PATH_MAX) { errno = ENAMETOOLONG; return -1; } @@ -87,9 +87,9 @@ int ink_server_add_object(ink_server_t *srv, const char *path, /* ---------- lookup ---------- */ -static const ink_method_t *find_method(const ink_vtable_t *vt, const char *name) +static const link_method_t *find_method(const link_vtable_t *vt, const char *name) { - const ink_method_t *m; + const link_method_t *m; if (!vt->methods) return NULL; @@ -102,12 +102,12 @@ static const ink_method_t *find_method(const ink_vtable_t *vt, const char *name) /* 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 ink_method_t *resolve(struct ink_object *o, +static const link_method_t *resolve(struct link_object *o, const char *iface, const char *member, - struct ink_vtable_entry **out_e) + struct link_vtable_entry **out_e) { - struct ink_vtable_entry *e; - const ink_method_t *m; + struct link_vtable_entry *e; + const link_method_t *m; if (iface) { TAILQ_FOREACH(e, &o->vtables, link) { @@ -155,7 +155,7 @@ static int send_all(int fd, const uint8_t *buf, size_t len) return 0; } -int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, +int link__send_method_return(link_connection_t *conn, const struct link_msg *req, const char *out_sig, const uint8_t *body, size_t body_len) { @@ -163,7 +163,7 @@ int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, ssize_t hlen; uint32_t serial = ++conn->next_serial; - hlen = ink__msg_build_return(hdr, sizeof(hdr), serial, + hlen = link__msg_build_return(hdr, sizeof(hdr), serial, req->serial, req->sender, out_sig, (uint32_t)body_len); @@ -179,7 +179,7 @@ int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, return 0; } -int ink_connection_emit_signal(ink_connection_t *conn, +int link_connection_emit_signal(link_connection_t *conn, const char *path, const char *interface, const char *member, @@ -196,11 +196,11 @@ int ink_connection_emit_signal(ink_connection_t *conn, errno = EINVAL; return -1; } - if (conn->auth != INK_AUTH_DONE) + if (conn->auth != LINK_AUTH_DONE) return 0; /* peer hasn't finished the SASL phase */ for (i = 0; i < conn->matches_count; i++) { - if (ink__match_matches(conn->matches[i], path, + if (link__match_matches(conn->matches[i], path, interface, member)) { matched = 1; break; @@ -210,7 +210,7 @@ int ink_connection_emit_signal(ink_connection_t *conn, return 0; /* peer didn't subscribe — nothing to do */ serial = ++conn->next_serial; - hlen = ink__msg_build_signal(hdr, sizeof(hdr), serial, + hlen = link__msg_build_signal(hdr, sizeof(hdr), serial, path, interface, member, signature, (uint32_t)body_len); if (hlen < 0) { @@ -225,7 +225,7 @@ int ink_connection_emit_signal(ink_connection_t *conn, return 0; } -int ink__send_error(ink_connection_t *conn, const struct ink_msg *req, +int link__send_error(link_connection_t *conn, const struct link_msg *req, const char *error_name, const char *text) { uint8_t hdr[512]; @@ -236,12 +236,12 @@ int ink__send_error(ink_connection_t *conn, const struct ink_msg *req, const char *sig = NULL; if (text && *text) { - struct ink_writer w; + struct link_writer w; ssize_t n; - ink__w_init(&w, body, sizeof(body)); - ink__w_string(&w, text); - n = ink__w_finish(&w); + link__w_init(&w, body, sizeof(body)); + link__w_string(&w, text); + n = link__w_finish(&w); if (n < 0) { errno = EMSGSIZE; return -1; @@ -250,7 +250,7 @@ int ink__send_error(ink_connection_t *conn, const struct ink_msg *req, sig = "s"; } - hlen = ink__msg_build_error(hdr, sizeof(hdr), serial, + hlen = link__msg_build_error(hdr, sizeof(hdr), serial, req->serial, req->sender, error_name, sig, (uint32_t)blen); if (hlen < 0) { @@ -265,99 +265,99 @@ int ink__send_error(ink_connection_t *conn, const struct ink_msg *req, return 0; } -/* ---------- ink_call public surface ---------- */ +/* ---------- link_call public surface ---------- */ -const char *ink_call_path (const ink_call_t *c) { return c ? c->incoming.path : NULL; } -const char *ink_call_interface(const ink_call_t *c) { return c ? c->incoming.interface : NULL; } -const char *ink_call_member (const ink_call_t *c) { return c ? c->incoming.member : NULL; } -uid_t ink_call_uid (const ink_call_t *c) { return c ? c->conn->peer_uid : (uid_t)-1; } +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; } -ink_writer_t *ink_call_reply(ink_call_t *call) +link_writer_t *link_call_reply(link_call_t *call) { if (!call || call->reply_consumed || call->error_sent) return NULL; call->reply_consumed = 1; - ink__w_init(&call->reply_writer, + link__w_init(&call->reply_writer, call->conn->txbuf, sizeof(call->conn->txbuf)); return &call->reply_writer; } -int ink_call_reply_error(ink_call_t *call, const char *name, const char *message) +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 ink__send_error(call->conn, &call->incoming, name, message); + return link__send_error(call->conn, &call->incoming, name, message); } /* ---------- public reader wrappers ---------- */ -int ink_call_read_byte (ink_call_t *c, uint8_t *o) { return ink__r_byte (&c->read_cursor, o); } -int ink_call_read_bool (ink_call_t *c, int *o) { return ink__r_bool (&c->read_cursor, o); } -int ink_call_read_u32 (ink_call_t *c, uint32_t *o) { return ink__r_u32 (&c->read_cursor, o); } -int ink_call_read_string(ink_call_t *c, const char **o) { return ink__r_string(&c->read_cursor, o); } -int ink_call_read_path (ink_call_t *c, const char **o) { return ink__r_path (&c->read_cursor, o); } +int link_call_read_byte (link_call_t *c, uint8_t *o) { return link__r_byte (&c->read_cursor, o); } +int link_call_read_bool (link_call_t *c, int *o) { return link__r_bool (&c->read_cursor, o); } +int link_call_read_u32 (link_call_t *c, uint32_t *o) { return link__r_u32 (&c->read_cursor, o); } +int link_call_read_string(link_call_t *c, const char **o) { return link__r_string(&c->read_cursor, o); } +int link_call_read_path (link_call_t *c, const char **o) { return link__r_path (&c->read_cursor, o); } /* ---------- public writer wrappers ---------- */ -void ink_writer_init (ink_writer_t *w, uint8_t *buf, size_t cap) { ink__w_init(w, buf, cap); } -ssize_t ink_writer_finish(ink_writer_t *w) { return ink__w_finish(w); } +void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap) { link__w_init(w, buf, cap); } +ssize_t link_writer_finish(link_writer_t *w) { return link__w_finish(w); } -void ink_w_byte (ink_writer_t *w, uint8_t v) { ink__w_byte(w, v); } -void ink_w_bool (ink_writer_t *w, int v) { ink__w_bool(w, v); } -void ink_w_u32 (ink_writer_t *w, uint32_t v) { ink__w_u32(w, v); } -void ink_w_string (ink_writer_t *w, const char *s) { ink__w_string(w, s); } -void ink_w_path (ink_writer_t *w, const char *s) { ink__w_path(w, s); } -void ink_w_array_begin (ink_writer_t *w, char ec) { ink__w_array_begin(w, ec); } -void ink_w_array_end (ink_writer_t *w) { ink__w_array_end(w); } -void ink_w_struct_begin(ink_writer_t *w) { ink__w_struct_begin(w); } -void ink_w_struct_end (ink_writer_t *w) { ink__w_struct_end(w); } +void link_w_byte (link_writer_t *w, uint8_t v) { link__w_byte(w, v); } +void link_w_bool (link_writer_t *w, int v) { link__w_bool(w, v); } +void link_w_u32 (link_writer_t *w, uint32_t v) { link__w_u32(w, v); } +void link_w_string (link_writer_t *w, const char *s) { link__w_string(w, s); } +void link_w_path (link_writer_t *w, const char *s) { link__w_path(w, s); } +void link_w_array_begin (link_writer_t *w, char ec) { link__w_array_begin(w, ec); } +void link_w_array_end (link_writer_t *w) { link__w_array_end(w); } +void link_w_struct_begin(link_writer_t *w) { link__w_struct_begin(w); } +void link_w_struct_end (link_writer_t *w) { link__w_struct_end(w); } /* ---------- dispatch entry point ---------- */ -int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) +int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) { - struct ink_object *o; - struct ink_vtable_entry *e = NULL; - const ink_method_t *meth; - struct ink_call call; + 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 != INK_MSG_METHOD_CALL) { + 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 ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Method call without path or member"); } /* Built-in DBus interfaces (Hello, Ping, Introspect) are handled * here before object-tree lookup, which means they also run - * before the INK_METHOD_PRIVILEGED authz gate further down. The + * 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 - * ink__handle_builtin. */ - rc = ink__handle_builtin(conn, m); + * link__handle_builtin. */ + rc = link__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 ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.UnknownObject", "No such object"); } meth = resolve(o, m->interface, m->member, &e); if (!meth) { - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.UnknownMethod", "No such method on this object"); } @@ -368,7 +368,7 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) const char *want = meth->in_sig ? meth->in_sig : ""; if (strcmp(got, want) != 0) - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Argument signature mismatch"); } @@ -377,8 +377,8 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) * 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 & INK_METHOD_PRIVILEGED) && conn->peer_uid != 0) { - return ink__send_error(conn, m, + if ((meth->flags & LINK_METHOD_PRIVILEGED) && conn->peer_uid != 0) { + return link__send_error(conn, m, "org.freedesktop.DBus.Error.AccessDenied", "Method requires root privileges"); } @@ -386,12 +386,12 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) memset(&call, 0, sizeof(call)); call.conn = conn; call.incoming = *m; - ink__r_init(&call.read_cursor, m->body, m->body_avail); + link__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. */ - ink__send_error(conn, m, + link__send_error(conn, m, "org.freedesktop.DBus.Error.Failed", "Handler failed"); return 0; @@ -400,17 +400,17 @@ int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m) if (!call.reply_consumed && !call.error_sent) { /* Handler returned 0 but never produced a reply; treat as * empty reply with out_sig "". */ - ink__send_method_return(conn, m, NULL, NULL, 0); + link__send_method_return(conn, m, NULL, NULL, 0); return 0; } if (call.reply_consumed && !call.error_sent) { - blen = ink__w_finish(&call.reply_writer); + blen = link__w_finish(&call.reply_writer); if (blen < 0) - return ink__send_error(conn, m, + return link__send_error(conn, m, "org.freedesktop.DBus.Error.Failed", "Reply marshalling overflow"); - return ink__send_method_return(conn, m, meth->out_sig, + return link__send_method_return(conn, m, meth->out_sig, conn->txbuf, (size_t)blen); } diff --git a/libink/ink-internal.h b/libink/ink-internal.h index 3b05d711..386ff774 100644 --- a/libink/ink-internal.h +++ b/libink/ink-internal.h @@ -14,63 +14,63 @@ #include "proto.h" typedef enum { - INK_AUTH_NUL = 0, - INK_AUTH_LINE, - INK_AUTH_DONE, - INK_AUTH_FAILED, -} ink_auth_state_t; - -#define INK_PATH_MAX 108 -#define INK_AUTH_LINEBUF_SIZE 256 -#define INK_RX_BUF_SIZE (64 * 1024) -#define INK_TX_BUF_SIZE (16 * 1024) -#define INK_UNIQUE_NAME_LEN 16 -#define INK_MATCH_RULE_MAX 256 /* per-peer match rule cap */ -#define INK_MATCH_PEER_CAP 16 /* max active match rules per peer */ + 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 ink_vtable_entry { - const ink_vtable_t *vt; +struct link_vtable_entry { + const link_vtable_t *vt; void *userdata; - TAILQ_ENTRY(ink_vtable_entry) link; + TAILQ_ENTRY(link_vtable_entry) link; }; -TAILQ_HEAD(ink_vtable_list, ink_vtable_entry); +TAILQ_HEAD(link_vtable_list, link_vtable_entry); /* An object exposed at one path. */ -struct ink_object { - char path[INK_PATH_MAX]; - struct ink_vtable_list vtables; - TAILQ_ENTRY(ink_object) link; +struct link_object { + char path[LINK_PATH_MAX]; + struct link_vtable_list vtables; + TAILQ_ENTRY(link_object) link; }; -TAILQ_HEAD(ink_object_list, ink_object); +TAILQ_HEAD(link_object_list, link_object); -struct ink_server { +struct link_server { int fd; - char path[INK_PATH_MAX]; - struct ink_object_list objects; + 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 ink_call (in dispatch) stays small. Sharing the + * 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 ink_call { - ink_connection_t *conn; - struct ink_msg incoming; - struct ink_reader read_cursor; - struct ink_writer reply_writer; /* writes into conn->txbuf */ +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 ink_match { +struct link_match { char *raw; /* original string, for RemoveMatch */ char *type; /* "signal", or NULL */ char *interface; @@ -78,59 +78,59 @@ struct ink_match { char *path; }; -struct ink_connection { +struct link_connection { int fd; uid_t peer_uid; char guid[33]; - char unique_name[INK_UNIQUE_NAME_LEN]; /* ":1.N" */ + char unique_name[LINK_UNIQUE_NAME_LEN]; /* ":1.N" */ - ink_auth_state_t auth; - char linebuf[INK_AUTH_LINEBUF_SIZE]; + 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 ink_match *matches[INK_MATCH_PEER_CAP]; + struct link_match *matches[LINK_MATCH_PEER_CAP]; size_t matches_count; - uint8_t rxbuf[INK_RX_BUF_SIZE]; + 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[INK_TX_BUF_SIZE]; + uint8_t txbuf[LINK_TX_BUF_SIZE]; uint32_t next_serial; - struct ink_server *server; /* back-pointer for dispatch */ + struct link_server *server; /* back-pointer for dispatch */ }; /* auth.c */ -int ink__auth_process(ink_connection_t *conn); -void ink__auth_generate_guid(char out[33]); +int link__auth_process(link_connection_t *conn); +void link__auth_generate_guid(char out[33]); /* dispatch.c */ -int ink__dispatch_message(ink_connection_t *conn, const struct ink_msg *m); -int ink__send_error(ink_connection_t *conn, const struct ink_msg *req, +int link__dispatch_message(link_connection_t *conn, const struct link_msg *m); +int link__send_error(link_connection_t *conn, const struct link_msg *req, const char *error_name, const char *text); -int ink__send_method_return(ink_connection_t *conn, const struct ink_msg *req, +int link__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 ink__handle_builtin(ink_connection_t *conn, const struct ink_msg *m); +int link__handle_builtin(link_connection_t *conn, const struct link_msg *m); /* match.c */ -struct ink_match *ink__match_parse (const char *rule); -void ink__match_free (struct ink_match *m); -int ink__match_matches(const struct ink_match *m, +struct link_match *link__match_parse (const char *rule); +void link__match_free (struct link_match *m); +int link__match_matches(const struct link_match *m, const char *path, const char *iface, const char *member); -int ink__match_add (ink_connection_t *conn, const char *rule); -int ink__match_remove (ink_connection_t *conn, const char *rule); +int link__match_add (link_connection_t *conn, const char *rule); +int link__match_remove (link_connection_t *conn, const char *rule); #endif /* LIBINK_INK_INTERNAL_H_ */ diff --git a/libink/ink.h b/libink/ink.h index 288138b1..c4687fe6 100644 --- a/libink/ink.h +++ b/libink/ink.h @@ -31,16 +31,16 @@ extern "C" { #endif -typedef struct ink_server ink_server_t; -typedef struct ink_connection ink_connection_t; -typedef struct ink_call ink_call_t; +typedef struct link_server link_server_t; +typedef struct link_connection link_connection_t; +typedef struct link_call link_call_t; /* Writer is exposed so callers can stack-allocate one for marshalling - * signal/reply bodies. Treat the fields as opaque; use ink_writer_init - * + the ink_w_* helpers + ink_writer_finish. Sized for typical D-Bus + * 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 INK_WRITER_MAX_NESTING 8 -typedef struct ink_writer { +#define LINK_WRITER_MAX_NESTING 8 +typedef struct link_writer { uint8_t *buf; size_t cap; size_t off; @@ -48,60 +48,60 @@ typedef struct ink_writer { struct { size_t lenpos; size_t elemstart; - } arrays[INK_WRITER_MAX_NESTING]; + } arrays[LINK_WRITER_MAX_NESTING]; size_t array_depth; -} ink_writer_t; +} link_writer_t; /* ---------- server / connection lifecycle ---------- */ -int ink_server_new (ink_server_t **server, const char *path); -void ink_server_free (ink_server_t *server); -int ink_server_get_fd(const ink_server_t *server); +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 ink_server_accept(ink_server_t *server, ink_connection_t **conn); +int link_server_accept(link_server_t *server, link_connection_t **conn); -int ink_connection_get_fd (const ink_connection_t *conn); -uid_t ink_connection_get_uid (const ink_connection_t *conn); -int ink_connection_process (ink_connection_t *conn); -void ink_connection_close (ink_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 (*ink_method_fn)(ink_call_t *call, void *userdata); +typedef int (*link_method_fn)(link_call_t *call, void *userdata); -/* Method flags for ink_method_t.flags */ -#define INK_METHOD_PRIVILEGED (1u << 0) /* peer must be uid 0 (root) */ +/* 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 INK_METHOD_* */ - ink_method_fn handler; -} ink_method_t; + unsigned flags; /* OR of LINK_METHOD_* */ + link_method_fn handler; +} link_method_t; typedef struct { const char *interface; /* e.g. "org.finit.Manager1" */ - const ink_method_t *methods; /* terminated by {NULL, ...} */ -} ink_vtable_t; + 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 ink_server_add_object(ink_server_t *server, const char *path, - const ink_vtable_t *vt, void *userdata); +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 ink_server_remove_object(ink_server_t *server, const char *path); +int link_server_remove_object(link_server_t *server, const char *path); /* ---------- call accessors ---------- */ -const char *ink_call_path (const ink_call_t *call); -const char *ink_call_interface(const ink_call_t *call); -const char *ink_call_member (const ink_call_t *call); -uid_t ink_call_uid (const ink_call_t *call); +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 ---------- * @@ -112,11 +112,11 @@ uid_t ink_call_uid (const ink_call_t *call); * connection's rx buffer and are valid for the duration of the * method handler. */ -int ink_call_read_byte (ink_call_t *call, uint8_t *out); -int ink_call_read_bool (ink_call_t *call, int *out); -int ink_call_read_u32 (ink_call_t *call, uint32_t *out); -int ink_call_read_string(ink_call_t *call, const char **out); /* "s" */ -int ink_call_read_path (ink_call_t *call, const char **out); /* "o" */ +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 ---------- */ @@ -124,12 +124,12 @@ int ink_call_read_path (ink_call_t *call, const char **out); /* "o" */ * from the handler. Dispatch finalizes and sends the reply with * the out_sig declared on the vtable. May be called once per * call. */ -ink_writer_t *ink_call_reply(ink_call_t *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 ink_call_reply_error(ink_call_t *call, const char *name, const char *message); +int link_call_reply_error(link_call_t *call, const char *name, const char *message); /* ---------- signal emission ---------- * @@ -137,7 +137,7 @@ int ink_call_reply_error(ink_call_t *call, const char *name, const char *message * Callers marshal the body separately and pass the resulting bytes. * Returns 0 on success (or "filtered out, nothing sent"), -1 on * underlying transport failure. */ -int ink_connection_emit_signal(ink_connection_t *conn, +int link_connection_emit_signal(link_connection_t *conn, const char *path, const char *interface, const char *member, @@ -148,22 +148,22 @@ int ink_connection_emit_signal(ink_connection_t *conn, * * For marshalling bodies outside a method-call handler (signals, * pre-computed replies). Initialise on a caller-owned buffer, - * write args via ink_w_*, then call ink_writer_finish which + * write args via link_w_*, then call link_writer_finish which * returns the body length or -1 on overflow. */ -void ink_writer_init (ink_writer_t *w, uint8_t *buf, size_t cap); -ssize_t ink_writer_finish(ink_writer_t *w); +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 ink_w_byte (ink_writer_t *w, uint8_t v); -void ink_w_bool (ink_writer_t *w, int v); -void ink_w_u32 (ink_writer_t *w, uint32_t v); -void ink_w_string (ink_writer_t *w, const char *s); /* "s" */ -void ink_w_path (ink_writer_t *w, const char *s); /* "o" */ -void ink_w_array_begin (ink_writer_t *w, char element_sig); -void ink_w_array_end (ink_writer_t *w); -void ink_w_struct_begin(ink_writer_t *w); -void ink_w_struct_end (ink_writer_t *w); +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); #ifdef __cplusplus } diff --git a/libink/marshal.c b/libink/marshal.c index 03001443..9bbc1c52 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -11,7 +11,7 @@ #define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) -void ink__w_init(struct ink_writer *w, uint8_t *buf, size_t cap) +void link__w_init(struct link_writer *w, uint8_t *buf, size_t cap) { w->buf = buf; w->cap = cap; @@ -20,14 +20,14 @@ void ink__w_init(struct ink_writer *w, uint8_t *buf, size_t cap) w->array_depth = 0; } -ssize_t ink__w_finish(struct ink_writer *w) +ssize_t link__w_finish(struct link_writer *w) { if (w->err || w->array_depth != 0) return -1; return (ssize_t)w->off; } -static int reserve(struct ink_writer *w, size_t align, size_t bytes) +static int reserve(struct link_writer *w, size_t align, size_t bytes) { size_t pad; @@ -44,7 +44,7 @@ static int reserve(struct ink_writer *w, size_t align, size_t bytes) return 0; } -static void put_u32_at(struct ink_writer *w, size_t pos, uint32_t v) +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); @@ -52,34 +52,34 @@ static void put_u32_at(struct ink_writer *w, size_t pos, uint32_t v) w->buf[pos + 3] = (uint8_t)((v >> 24) & 0xff); } -static void put_u32(struct ink_writer *w, uint32_t v) +static void put_u32(struct link_writer *w, uint32_t v) { put_u32_at(w, w->off, v); w->off += 4; } -void ink__w_byte(struct ink_writer *w, uint8_t v) +void link__w_byte(struct link_writer *w, uint8_t v) { if (reserve(w, 1, 1) < 0) return; w->buf[w->off++] = v; } -void ink__w_bool(struct ink_writer *w, int v) +void link__w_bool(struct link_writer *w, int v) { if (reserve(w, 4, 4) < 0) return; put_u32(w, v ? 1u : 0u); } -void ink__w_u32(struct ink_writer *w, uint32_t v) +void link__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 ink_writer *w, const char *s, int onebyte_len) +static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_len) { size_t len = s ? strlen(s) : 0; @@ -98,9 +98,9 @@ static void write_lenprefixed(struct ink_writer *w, const char *s, int onebyte_l w->buf[w->off++] = 0; } -void ink__w_string(struct ink_writer *w, const char *s) { write_lenprefixed(w, s, 0); } -void ink__w_path (struct ink_writer *w, const char *s) { write_lenprefixed(w, s, 0); } -void ink__w_sig (struct ink_writer *w, const char *s) { write_lenprefixed(w, s, 1); } +void link__w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void link__w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void link__w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); } static size_t element_align(char c) { @@ -115,13 +115,13 @@ static size_t element_align(char c) } } -void ink__w_array_begin(struct ink_writer *w, char element_sig_first_char) +void link__w_array_begin(struct link_writer *w, char element_sig_first_char) { size_t lenpos; if (w->err) return; - if (w->array_depth >= INK_WRITER_MAX_NESTING) { + if (w->array_depth >= LINK_WRITER_MAX_NESTING) { w->err = 1; return; } @@ -141,7 +141,7 @@ void ink__w_array_begin(struct ink_writer *w, char element_sig_first_char) w->array_depth++; } -void ink__w_array_end(struct ink_writer *w) +void link__w_array_end(struct link_writer *w) { size_t elemstart, lenpos; uint32_t actual; @@ -157,19 +157,19 @@ void ink__w_array_end(struct ink_writer *w) put_u32_at(w, lenpos, actual); } -void ink__w_struct_begin(struct ink_writer *w) +void link__w_struct_begin(struct link_writer *w) { reserve(w, 8, 0); } -void ink__w_struct_end(struct ink_writer *w) +void link__w_struct_end(struct link_writer *w) { (void)w; } /* ---- reader ---- */ -void ink__r_init(struct ink_reader *r, const uint8_t *body, size_t len) +void link__r_init(struct link_reader *r, const uint8_t *body, size_t len) { r->base = body; r->off = 0; @@ -177,7 +177,7 @@ void ink__r_init(struct ink_reader *r, const uint8_t *body, size_t len) r->err = 0; } -static int r_skip_align(struct ink_reader *r, size_t align) +static int r_skip_align(struct link_reader *r, size_t align) { size_t pad; @@ -200,7 +200,7 @@ static uint32_t rd_u32(const uint8_t *p) | ((uint32_t)p[3] << 24); } -int ink__r_byte(struct ink_reader *r, uint8_t *out) +int link__r_byte(struct link_reader *r, uint8_t *out) { if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) { r->err = 1; @@ -210,7 +210,7 @@ int ink__r_byte(struct ink_reader *r, uint8_t *out) return 0; } -int ink__r_u32(struct ink_reader *r, uint32_t *out) +int link__r_u32(struct link_reader *r, uint32_t *out) { if (r_skip_align(r, 4) < 0 || r->off + 4 > r->cap) { r->err = 1; @@ -221,21 +221,21 @@ int ink__r_u32(struct ink_reader *r, uint32_t *out) return 0; } -int ink__r_bool(struct ink_reader *r, int *out) +int link__r_bool(struct link_reader *r, int *out) { uint32_t v; - if (ink__r_u32(r, &v) < 0) + if (link__r_u32(r, &v) < 0) return -1; *out = v ? 1 : 0; return 0; } -static int read_string_like(struct ink_reader *r, const char **out) +static int read_string_like(struct link_reader *r, const char **out) { uint32_t len; - if (ink__r_u32(r, &len) < 0) + if (link__r_u32(r, &len) < 0) return -1; if (r->off + (size_t)len + 1 > r->cap) { r->err = 1; @@ -251,10 +251,10 @@ static int read_string_like(struct ink_reader *r, const char **out) return 0; } -int ink__r_string(struct ink_reader *r, const char **out) { return read_string_like(r, out); } -int ink__r_path (struct ink_reader *r, const char **out) { return read_string_like(r, out); } +int link__r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } +int link__r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } -int ink__r_done(const struct ink_reader *r) +int link__r_done(const struct link_reader *r) { return !r->err && r->off == r->cap; } diff --git a/libink/marshal.h b/libink/marshal.h index 57f0b5a4..e7027b88 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -9,48 +9,48 @@ #include #include -/* struct ink_writer is defined in ink.h (public). Field layout is +/* 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 "ink.h" -void ink__w_init (struct ink_writer *w, uint8_t *buf, size_t cap); -ssize_t ink__w_finish(struct ink_writer *w); +void link__w_init (struct link_writer *w, uint8_t *buf, size_t cap); +ssize_t link__w_finish(struct link_writer *w); -void ink__w_byte (struct ink_writer *w, uint8_t v); -void ink__w_bool (struct ink_writer *w, int v); -void ink__w_u32 (struct ink_writer *w, uint32_t v); -void ink__w_string (struct ink_writer *w, const char *s); /* "s" */ -void ink__w_path (struct ink_writer *w, const char *s); /* "o" */ -void ink__w_sig (struct ink_writer *w, const char *s); /* "g" */ +void link__w_byte (struct link_writer *w, uint8_t v); +void link__w_bool (struct link_writer *w, int v); +void link__w_u32 (struct link_writer *w, uint32_t v); +void link__w_string (struct link_writer *w, const char *s); /* "s" */ +void link__w_path (struct link_writer *w, const char *s); /* "o" */ +void link__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 ink__w_array_begin (struct ink_writer *w, char element_sig_first_char); -void ink__w_array_end (struct ink_writer *w); +void link__w_array_begin (struct link_writer *w, char element_sig_first_char); +void link__w_array_end (struct link_writer *w); -void ink__w_struct_begin(struct ink_writer *w); -void ink__w_struct_end (struct ink_writer *w); +void link__w_struct_begin(struct link_writer *w); +void link__w_struct_end (struct link_writer *w); /* ---- reader ---- * * Reads from a message body pointer + length, advancing a cursor. - * String pointers returned by ink__r_string reference the input + * String pointers returned by link__r_string reference the input * buffer and are valid for the lifetime of that buffer (i.e. for * the duration of the current call dispatch). */ -struct ink_reader { +struct link_reader { const uint8_t *base; size_t off; size_t cap; int err; /* sticky */ }; -void ink__r_init (struct ink_reader *r, const uint8_t *body, size_t len); -int ink__r_byte (struct ink_reader *r, uint8_t *out); -int ink__r_bool (struct ink_reader *r, int *out); -int ink__r_u32 (struct ink_reader *r, uint32_t *out); -int ink__r_string(struct ink_reader *r, const char **out); /* "s" */ -int ink__r_path (struct ink_reader *r, const char **out); /* "o" */ -int ink__r_done (const struct ink_reader *r); +void link__r_init (struct link_reader *r, const uint8_t *body, size_t len); +int link__r_byte (struct link_reader *r, uint8_t *out); +int link__r_bool (struct link_reader *r, int *out); +int link__r_u32 (struct link_reader *r, uint32_t *out); +int link__r_string(struct link_reader *r, const char **out); /* "s" */ +int link__r_path (struct link_reader *r, const char **out); /* "o" */ +int link__r_done (const struct link_reader *r); #endif /* LIBINK_MARSHAL_H_ */ diff --git a/libink/match.c b/libink/match.c index 8bba7bbc..c8101e09 100644 --- a/libink/match.c +++ b/libink/match.c @@ -73,12 +73,12 @@ static int parse_kv(const char **p, char **out_key, char **out_value) return 0; } -struct ink_match *ink__match_parse(const char *rule) +struct link_match *link__match_parse(const char *rule) { - struct ink_match *m; + struct link_match *m; const char *p; - if (!rule || strlen(rule) >= INK_MATCH_RULE_MAX) { + if (!rule || strlen(rule) >= LINK_MATCH_RULE_MAX) { errno = EINVAL; return NULL; } @@ -126,12 +126,12 @@ struct ink_match *ink__match_parse(const char *rule) return m; bad: - ink__match_free(m); + link__match_free(m); errno = EINVAL; return NULL; } -void ink__match_free(struct ink_match *m) +void link__match_free(struct link_match *m) { if (!m) return; @@ -152,7 +152,7 @@ static int field_matches(const char *want, const char *got) return strcmp(want, got) == 0; } -int ink__match_matches(const struct ink_match *m, +int link__match_matches(const struct link_match *m, const char *path, const char *iface, const char *member) { @@ -165,16 +165,16 @@ int ink__match_matches(const struct ink_match *m, && field_matches(m->member, member); } -int ink__match_add(ink_connection_t *conn, const char *rule) +int link__match_add(link_connection_t *conn, const char *rule) { - struct ink_match *m; + struct link_match *m; - if (conn->matches_count >= INK_MATCH_PEER_CAP) { + if (conn->matches_count >= LINK_MATCH_PEER_CAP) { errno = ENOSPC; return -1; } - m = ink__match_parse(rule); + m = link__match_parse(rule); if (!m) return -1; @@ -182,13 +182,13 @@ int ink__match_add(ink_connection_t *conn, const char *rule) return 0; } -int ink__match_remove(ink_connection_t *conn, const char *rule) +int link__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) { - ink__match_free(conn->matches[i]); + link__match_free(conn->matches[i]); conn->matches[i] = conn->matches[conn->matches_count - 1]; conn->matches_count--; return 0; diff --git a/libink/path.c b/libink/path.c index 1899e4b7..80f97963 100644 --- a/libink/path.c +++ b/libink/path.c @@ -15,7 +15,7 @@ static int is_safe(unsigned char c) || (c >= '0' && c <= '9'); } -int ink_path_encode(const char *in, char *out, size_t outsz) +int link_path_encode(const char *in, char *out, size_t outsz) { static const char hex[] = "0123456789abcdef"; size_t off = 0; diff --git a/libink/path.h b/libink/path.h index d4f3b149..784c634e 100644 --- a/libink/path.h +++ b/libink/path.h @@ -16,6 +16,6 @@ * 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 ink_path_encode(const char *in, char *out, size_t outsz); +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 index aa577392..5ff94b7d 100644 --- a/libink/proto.c +++ b/libink/proto.c @@ -75,7 +75,7 @@ static const char *parse_signature(const uint8_t *buf, size_t avail, size_t *con return (const char *)(buf + 1); } -ssize_t ink__msg_parse(const uint8_t *buf, size_t len, struct ink_msg *out) +ssize_t link__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; @@ -89,7 +89,7 @@ ssize_t ink__msg_parse(const uint8_t *buf, size_t len, struct ink_msg *out) errno = EPROTO; return -1; } - if (buf[3] != INK_PROTOCOL_VERSION) { + if (buf[3] != LINK_PROTOCOL_VERSION) { errno = EPROTONOSUPPORT; return -1; } @@ -138,25 +138,25 @@ ssize_t ink__msg_parse(const uint8_t *buf, size_t len, struct ink_msg *out) const char *s = parse_string(fp, (size_t)(fend - fp), &used); if (!s) { errno = EPROTO; return -1; } switch (code) { - case INK_HDR_PATH: out->path = s; break; - case INK_HDR_INTERFACE: out->interface = s; break; - case INK_HDR_MEMBER: out->member = s; break; - case INK_HDR_ERROR_NAME: out->error_name = s; break; - case INK_HDR_DESTINATION: out->destination = s; break; - case INK_HDR_SENDER: out->sender = s; break; + 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 == INK_HDR_SIGNATURE) + 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 == INK_HDR_REPLY_SERIAL) + if (code == LINK_HDR_REPLY_SERIAL) out->reply_serial = v; fp += 4; } else { @@ -259,7 +259,7 @@ static ssize_t finalize_header(uint8_t *buf, size_t cap, buf[0] = 'l'; buf[1] = type; buf[2] = flags; - buf[3] = INK_PROTOCOL_VERSION; + 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)); @@ -271,7 +271,7 @@ static ssize_t finalize_header(uint8_t *buf, size_t cap, return (ssize_t)hdr_end; } -ssize_t ink__msg_build_return(uint8_t *buf, size_t cap, +ssize_t link__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) @@ -281,21 +281,21 @@ ssize_t ink__msg_build_return(uint8_t *buf, size_t cap, if (cap < HDR_FIXED_SIZE) return -1; - if (put_field_u32(buf, cap, &off, INK_HDR_REPLY_SERIAL, reply_serial) < 0) + if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0) return -1; if (destination && - put_field_string(buf, cap, &off, INK_HDR_DESTINATION, 's', destination) < 0) + put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0) return -1; if (signature && *signature && - put_field_string(buf, cap, &off, INK_HDR_SIGNATURE, 'g', signature) < 0) + put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) return -1; - return finalize_header(buf, cap, INK_MSG_METHOD_RETURN, - INK_FLAG_NO_REPLY_EXPECTED, + return finalize_header(buf, cap, LINK_MSG_METHOD_RETURN, + LINK_FLAG_NO_REPLY_EXPECTED, body_len, serial, off); } -ssize_t ink__msg_build_error(uint8_t *buf, size_t cap, +ssize_t link__msg_build_error(uint8_t *buf, size_t cap, uint32_t serial, uint32_t reply_serial, const char *destination, const char *error_name, @@ -306,23 +306,23 @@ ssize_t ink__msg_build_error(uint8_t *buf, size_t cap, if (cap < HDR_FIXED_SIZE || !error_name) return -1; - if (put_field_u32(buf, cap, &off, INK_HDR_REPLY_SERIAL, reply_serial) < 0) + if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0) return -1; - if (put_field_string(buf, cap, &off, INK_HDR_ERROR_NAME, 's', error_name) < 0) + 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, INK_HDR_DESTINATION, 's', destination) < 0) + put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0) return -1; if (signature && *signature && - put_field_string(buf, cap, &off, INK_HDR_SIGNATURE, 'g', signature) < 0) + put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) return -1; - return finalize_header(buf, cap, INK_MSG_ERROR, - INK_FLAG_NO_REPLY_EXPECTED, + return finalize_header(buf, cap, LINK_MSG_ERROR, + LINK_FLAG_NO_REPLY_EXPECTED, body_len, serial, off); } -ssize_t ink__msg_build_signal(uint8_t *buf, size_t cap, +ssize_t link__msg_build_signal(uint8_t *buf, size_t cap, uint32_t serial, const char *path, const char *interface, @@ -334,22 +334,22 @@ ssize_t ink__msg_build_signal(uint8_t *buf, size_t cap, if (cap < HDR_FIXED_SIZE || !path || !interface || !member) return -1; - if (put_field_string(buf, cap, &off, INK_HDR_PATH, 'o', path) < 0) + if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0) return -1; - if (put_field_string(buf, cap, &off, INK_HDR_INTERFACE, 's', interface) < 0) + if (put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0) return -1; - if (put_field_string(buf, cap, &off, INK_HDR_MEMBER, 's', member) < 0) + if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0) return -1; if (signature && *signature && - put_field_string(buf, cap, &off, INK_HDR_SIGNATURE, 'g', signature) < 0) + put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0) return -1; - return finalize_header(buf, cap, INK_MSG_SIGNAL, - INK_FLAG_NO_REPLY_EXPECTED, + return finalize_header(buf, cap, LINK_MSG_SIGNAL, + LINK_FLAG_NO_REPLY_EXPECTED, body_len, serial, off); } -size_t ink__msg_header_size(const struct ink_msg *m) +size_t link__msg_header_size(const struct link_msg *m) { (void)m; /* Generous upper bound used by callers to size send buffers. */ diff --git a/libink/proto.h b/libink/proto.h index 50c6b8d0..cbba820e 100644 --- a/libink/proto.h +++ b/libink/proto.h @@ -11,34 +11,34 @@ #include /* Message types (D-Bus spec §4: "Message Format"). */ -#define INK_MSG_INVALID 0 -#define INK_MSG_METHOD_CALL 1 -#define INK_MSG_METHOD_RETURN 2 -#define INK_MSG_ERROR 3 -#define INK_MSG_SIGNAL 4 +#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 /* Message flags. */ -#define INK_FLAG_NO_REPLY_EXPECTED 0x01 -#define INK_FLAG_NO_AUTO_START 0x02 -#define INK_FLAG_ALLOW_INTERACTIVE_AUTHORIZATION 0x04 +#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 INK_HDR_PATH 1 -#define INK_HDR_INTERFACE 2 -#define INK_HDR_MEMBER 3 -#define INK_HDR_ERROR_NAME 4 -#define INK_HDR_REPLY_SERIAL 5 -#define INK_HDR_DESTINATION 6 -#define INK_HDR_SENDER 7 -#define INK_HDR_SIGNATURE 8 -#define INK_HDR_UNIX_FDS 9 +#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 INK_PROTOCOL_VERSION 1 +#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 ink_msg { +struct link_msg { uint8_t type; uint8_t flags; uint8_t endian; /* 'l' or 'B' */ @@ -63,31 +63,31 @@ struct ink_msg { * 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 ink__msg_parse(const uint8_t *buf, size_t len, struct ink_msg *out); +ssize_t link__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 ink__msg_header_size(const struct ink_msg *m); +size_t link__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 ink__msg_build_return(uint8_t *buf, size_t cap, +ssize_t link__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 ink__msg_build_error(uint8_t *buf, size_t cap, +ssize_t link__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 ink__msg_build_signal(uint8_t *buf, size_t cap, +ssize_t link__msg_build_signal(uint8_t *buf, size_t cap, uint32_t serial, const char *path, const char *interface, diff --git a/libink/server.c b/libink/server.c index e629a4c0..9edf3e8b 100644 --- a/libink/server.c +++ b/libink/server.c @@ -21,10 +21,10 @@ static void close_save_errno(int fd) errno = saved; } -int ink_server_new(ink_server_t **out, const char *path) +int link_server_new(link_server_t **out, const char *path) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; - ink_server_t *srv; + link_server_t *srv; size_t plen; int fd; @@ -34,7 +34,7 @@ int ink_server_new(ink_server_t **out, const char *path) } plen = strlen(path); - if (plen >= sizeof(sun.sun_path) || plen >= INK_PATH_MAX) { + if (plen >= sizeof(sun.sun_path) || plen >= LINK_PATH_MAX) { errno = ENAMETOOLONG; return -1; } @@ -87,20 +87,20 @@ int ink_server_new(ink_server_t **out, const char *path) return -1; } -void ink_server_free(ink_server_t *srv) +void link_server_free(link_server_t *srv) { - struct ink_object *o; + struct link_object *o; if (!srv) return; o = TAILQ_FIRST(&srv->objects); while (o) { - struct ink_object *next_o = TAILQ_NEXT(o, link); - struct ink_vtable_entry *e = TAILQ_FIRST(&o->vtables); + struct link_object *next_o = TAILQ_NEXT(o, link); + struct link_vtable_entry *e = TAILQ_FIRST(&o->vtables); while (e) { - struct ink_vtable_entry *next_e = TAILQ_NEXT(e, link); + struct link_vtable_entry *next_e = TAILQ_NEXT(e, link); free(e); e = next_e; @@ -116,7 +116,7 @@ void ink_server_free(ink_server_t *srv) free(srv); } -int ink_server_get_fd(const ink_server_t *srv) +int link_server_get_fd(const link_server_t *srv) { if (!srv) return -1; @@ -124,11 +124,11 @@ int ink_server_get_fd(const ink_server_t *srv) return srv->fd; } -int ink_server_accept(ink_server_t *srv, ink_connection_t **out) +int link_server_accept(link_server_t *srv, link_connection_t **out) { struct ucred cred = { 0 }; socklen_t credlen = sizeof(cred); - ink_connection_t *conn; + link_connection_t *conn; int cfd; if (!srv || !out) { @@ -147,7 +147,7 @@ int ink_server_accept(ink_server_t *srv, ink_connection_t **out) } conn->fd = cfd; - conn->auth = INK_AUTH_NUL; + conn->auth = LINK_AUTH_NUL; conn->server = srv; if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0) @@ -155,7 +155,7 @@ int ink_server_accept(ink_server_t *srv, ink_connection_t **out) else conn->peer_uid = (uid_t)-1; - ink__auth_generate_guid(conn->guid); + link__auth_generate_guid(conn->guid); *out = conn; return 0; diff --git a/src/dbus.c b/src/dbus.c index 22c024c5..f3e745e5 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -55,19 +55,19 @@ struct peer { uev_t watcher; - ink_connection_t *conn; + link_connection_t *conn; TAILQ_ENTRY(peer) link; }; static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); -static ink_server_t *server; +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); - ink_connection_close(p->conn); + link_connection_close(p->conn); TAILQ_REMOVE(&peers, p, link); peer_count--; free(p); @@ -84,7 +84,7 @@ static void peer_cb(uev_t *w, void *arg, int events) return; } - if (ink_connection_process(p->conn) < 0) + if (link_connection_process(p->conn) < 0) peer_drop(p); } @@ -98,10 +98,10 @@ static void accept_cb(uev_t *w, void *arg, int events) } for (;;) { - ink_connection_t *conn = NULL; + link_connection_t *conn = NULL; struct peer *p; - if (ink_server_accept(server, &conn) < 0) { + if (link_server_accept(server, &conn) < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) err(1, "Failed accepting D-Bus client"); break; @@ -110,13 +110,13 @@ static void accept_cb(uev_t *w, void *arg, int events) if (peer_count >= DBUS_MAX_PEERS) { logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", peer_count); - ink_connection_close(conn); + link_connection_close(conn); continue; } p = calloc(1, sizeof(*p)); if (!p) { - ink_connection_close(conn); + link_connection_close(conn); err(1, "Out of memory accepting D-Bus client"); break; } @@ -126,7 +126,7 @@ static void accept_cb(uev_t *w, void *arg, int events) peer_count++; if (uev_io_init(w->ctx, &p->watcher, peer_cb, p, - ink_connection_get_fd(conn), UEV_READ)) { + link_connection_get_fd(conn), UEV_READ)) { err(1, "Failed registering D-Bus peer watcher"); peer_drop(p); } @@ -142,57 +142,57 @@ static void accept_cb(uev_t *w, void *arg, int events) #define FINIT_SVC_PATH_MAX 512 static int service_path_for(svc_t *svc, char *buf, size_t bufsz); -static int manager_list_services(ink_call_t *call, void *userdata) +static int manager_list_services(link_call_t *call, void *userdata) { - ink_writer_t *w; + link_writer_t *w; svc_t *iter = NULL; svc_t *svc; (void)userdata; - w = ink_call_reply(call); + w = link_call_reply(call); if (!w) return -1; - ink_w_array_begin(w, 's'); + link_w_array_begin(w, 's'); for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { char ident[64]; svc_ident(svc, ident, sizeof(ident)); - ink_w_string(w, ident); + link_w_string(w, ident); } - ink_w_array_end(w); + link_w_array_end(w); return 0; } -static int manager_get_service(ink_call_t *call, void *userdata) +static int manager_get_service(link_call_t *call, void *userdata) { const char *ident; svc_t *svc; char path[FINIT_SVC_PATH_MAX]; - ink_writer_t *w; + link_writer_t *w; (void)userdata; - if (ink_call_read_string(call, &ident) < 0) - return ink_call_reply_error(call, + 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 ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.NoSuchService", ident); if (service_path_for(svc, path, sizeof(path)) < 0) - return ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.Failed", "Path encoding overflow"); - w = ink_call_reply(call); + w = link_call_reply(call); if (!w) return -1; - ink_w_path(w, path); + link_w_path(w, path); return 0; } @@ -277,50 +277,50 @@ static int dispatch_action(const char *ident, return rc; } -static int manager_take_string_method(ink_call_t *call, +static int manager_take_string_method(link_call_t *call, int (*action)(svc_t *, void *)) { const char *ident; - if (ink_call_read_string(call, &ident) < 0) - return ink_call_reply_error(call, + 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 ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.NoSuchService", ident); - (void)ink_call_reply(call); /* empty reply */ + (void)link_call_reply(call); /* empty reply */ return 0; } -static int manager_start (ink_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); } -static int manager_stop (ink_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); } -static int manager_restart(ink_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); } +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(ink_call_t *call, void *userdata) +static int manager_reload(link_call_t *call, void *userdata) { (void)userdata; if (IS_RESERVED_RUNLEVEL(runlevel)) - return ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.WrongRunlevel", "Reload not allowed in runlevel S or 0/6"); sm_reload(); - (void)ink_call_reply(call); + (void)link_call_reply(call); return 0; } -static int manager_set_runlevel(ink_call_t *call, void *userdata) +static int manager_set_runlevel(link_call_t *call, void *userdata) { uint32_t lvl; (void)userdata; - if (ink_call_read_u32(call, &lvl) < 0) - return ink_call_reply_error(call, + 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 ink_call_reply_error(call, + return link_call_reply_error(call, "org.freedesktop.DBus.Error.InvalidArgs", "runlevel must be 0-9 (excluding internal levels)"); @@ -328,51 +328,51 @@ static int manager_set_runlevel(ink_call_t *call, void *userdata) if (lvl == 6) halt = SHUT_REBOOT; sm_runlevel((int)lvl); - (void)ink_call_reply(call); + (void)link_call_reply(call); return 0; } -static int dbus_shutdown(ink_call_t *call, shutop_t target, int level) +static int dbus_shutdown(link_call_t *call, shutop_t target, int level) { if (IS_RESERVED_RUNLEVEL(runlevel)) - return ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.WrongRunlevel", "Already in shutdown"); halt = target; sm_runlevel(level); - (void)ink_call_reply(call); + (void)link_call_reply(call); return 0; } -static int manager_reboot (ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); } -static int manager_poweroff(ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); } -static int manager_halt (ink_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 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 ink_method_t manager_methods[] = { +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 = INK_METHOD_PRIVILEGED, .handler = manager_start }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_start }, { .name = "Stop", .in_sig = "s", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_stop }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_stop }, { .name = "Restart", .in_sig = "s", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_restart }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_restart }, { .name = "Reload", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_reload }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload }, { .name = "SetRunlevel", .in_sig = "u", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_set_runlevel }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel }, { .name = "Reboot", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_reboot }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot }, { .name = "Poweroff", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_poweroff }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff }, { .name = "Halt", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = manager_halt }, + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt }, { NULL, NULL, NULL, 0, NULL } }; -static const ink_vtable_t manager_vtable = { +static const link_vtable_t manager_vtable = { .interface = "org.finit.Manager1", .methods = manager_methods, }; @@ -388,52 +388,52 @@ static const ink_vtable_t manager_vtable = { * defined near the top of the file so Manager1.GetService can refer * to them. */ -static int service_action_method(ink_call_t *call, void *userdata, +static int service_action_method(link_call_t *call, void *userdata, int (*action)(svc_t *, void *)) { svc_t *svc = userdata; if (!svc) - return ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.NoSuchService", "Service object no longer valid"); action(svc, NULL); - (void)ink_call_reply(call); + (void)link_call_reply(call); return 0; } -static int service1_start (ink_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); } -static int service1_stop (ink_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); } -static int service1_restart(ink_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); } +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(ink_call_t *call, void *userdata) +static int service1_reload(link_call_t *call, void *userdata) { svc_t *svc = userdata; if (!svc) - return ink_call_reply_error(call, + return link_call_reply_error(call, "org.finit.Error.NoSuchService", "Service object no longer valid"); service_reload(svc); - (void)ink_call_reply(call); + (void)link_call_reply(call); return 0; } -static const ink_method_t service_methods[] = { +static const link_method_t service_methods[] = { { .name = "Start", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = service1_start }, + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_start }, { .name = "Stop", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = service1_stop }, + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_stop }, { .name = "Restart", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = service1_restart }, + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_restart }, { .name = "Reload", .in_sig = "", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = service1_reload }, + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_reload }, { NULL, NULL, NULL, 0, NULL } }; -static const ink_vtable_t service_vtable = { +static const link_vtable_t service_vtable = { .interface = "org.finit.Service1", .methods = service_methods, }; @@ -451,7 +451,7 @@ static int service_path_for(svc_t *svc, char *buf, size_t bufsz) memcpy(buf, SERVICE_PATH_PREFIX, plen); svc_ident(svc, ident, sizeof(ident)); - enc = ink_path_encode(ident, buf + plen, bufsz - plen); + enc = link_path_encode(ident, buf + plen, bufsz - plen); if (enc < 0) return -1; return (int)plen + enc; @@ -466,7 +466,7 @@ void dbus_register_service(svc_t *svc) if (service_path_for(svc, path, sizeof(path)) < 0) return; - if (ink_server_add_object(server, path, &service_vtable, svc) < 0) + if (link_server_add_object(server, path, &service_vtable, svc) < 0) logit(LOG_WARNING, "dbus: failed registering %s", path); } @@ -479,7 +479,7 @@ void dbus_unregister_service(svc_t *svc) if (service_path_for(svc, path, sizeof(path)) < 0) return; - (void)ink_server_remove_object(server, path); + (void)link_server_remove_object(server, path); } /* ---------- signal emission: ServiceStateChanged ---------- */ @@ -514,7 +514,7 @@ static const char *state_name(svc_state_t s) void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) { uint8_t body[256]; - ink_writer_t w; + link_writer_t w; struct peer *p; char ident[MAX_IDENT_LEN]; ssize_t blen; @@ -528,16 +528,16 @@ void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) svc_ident(svc, ident, sizeof(ident)); - ink_writer_init(&w, body, sizeof(body)); - ink_w_string(&w, ident); - ink_w_string(&w, state_name(o)); - ink_w_string(&w, state_name(n)); - blen = ink_writer_finish(&w); + 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)ink_connection_emit_signal(p->conn, + (void)link_connection_emit_signal(p->conn, "/org/finit/manager", "org.finit.Manager1", "ServiceStateChanged", @@ -604,43 +604,43 @@ static int cond_name_valid(const char *name) return 1; } -static int cond1_get(ink_call_t *call, void *userdata) +static int cond1_get(link_call_t *call, void *userdata) { const char *name; - ink_writer_t *w; + link_writer_t *w; (void)userdata; - if (ink_call_read_string(call, &name) < 0) - return ink_call_reply_error(call, + 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 ink_call_reply_error(call, + return link_call_reply_error(call, "org.freedesktop.DBus.Error.InvalidArgs", "invalid condition name"); - w = ink_call_reply(call); + w = link_call_reply(call); if (!w) return -1; - ink_w_string(w, condstr(cond_get(name))); + link_w_string(w, condstr(cond_get(name))); return 0; } -static int cond1_set_or_clear(ink_call_t *call, int do_set) +static int cond1_set_or_clear(link_call_t *call, int do_set) { const char *name; char buf[128]; const char *full; - if (ink_call_read_string(call, &name) < 0) - return ink_call_reply_error(call, + 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 ink_call_reply_error(call, + return link_call_reply_error(call, "org.freedesktop.DBus.Error.InvalidArgs", "Set/Clear is restricted to usr/* conditions"); @@ -656,16 +656,16 @@ static int cond1_set_or_clear(ink_call_t *call, int do_set) else cond_clear(full); - (void)ink_call_reply(call); + (void)link_call_reply(call); return 0; } -static int cond1_set (ink_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); } -static int cond1_clear(ink_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 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 ink_writer_t *cond_walk_writer; +static link_writer_t *cond_walk_writer; static int cond_walk_dump; static int cond_walk_cb(const char *fpath, const struct stat *sb, @@ -690,61 +690,61 @@ static int cond_walk_cb(const char *fpath, const struct stat *sb, if (cond_walk_dump) { state = condstr(cond_get_path(fpath)); - ink_w_struct_begin(cond_walk_writer); - ink_w_string(cond_walk_writer, name); - ink_w_string(cond_walk_writer, state); - ink_w_struct_end(cond_walk_writer); + 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 { - ink_w_string(cond_walk_writer, name); + link_w_string(cond_walk_writer, name); } return 0; } -static int cond1_list(ink_call_t *call, void *userdata) +static int cond1_list(link_call_t *call, void *userdata) { - ink_writer_t *w; + link_writer_t *w; (void)userdata; - w = ink_call_reply(call); + w = link_call_reply(call); if (!w) return -1; - ink_w_array_begin(w, 's'); + 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; - ink_w_array_end(w); + link_w_array_end(w); return 0; } -static int cond1_dump(ink_call_t *call, void *userdata) +static int cond1_dump(link_call_t *call, void *userdata) { - ink_writer_t *w; + link_writer_t *w; (void)userdata; - w = ink_call_reply(call); + w = link_call_reply(call); if (!w) return -1; - ink_w_array_begin(w, '('); + 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; - ink_w_array_end(w); + link_w_array_end(w); return 0; } -static const ink_method_t cond_methods[] = { +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 = INK_METHOD_PRIVILEGED, .handler = cond1_set }, + .flags = LINK_METHOD_PRIVILEGED, .handler = cond1_set }, { .name = "Clear", .in_sig = "s", .out_sig = "", - .flags = INK_METHOD_PRIVILEGED, .handler = cond1_clear }, + .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)", @@ -752,7 +752,7 @@ static const ink_method_t cond_methods[] = { { NULL, NULL, NULL, 0, NULL } }; -static const ink_vtable_t cond_vtable = { +static const link_vtable_t cond_vtable = { .interface = COND_INTERFACE, .methods = cond_methods, }; @@ -762,7 +762,7 @@ static const ink_vtable_t cond_vtable = { void dbus_notify_condition_change(const char *name, const char *state) { uint8_t body[256]; - ink_writer_t w; + link_writer_t w; struct peer *p; ssize_t blen; @@ -771,15 +771,15 @@ void dbus_notify_condition_change(const char *name, const char *state) if (TAILQ_EMPTY(&peers)) return; - ink_writer_init(&w, body, sizeof(body)); - ink_w_string(&w, name); - ink_w_string(&w, state); - blen = ink_writer_finish(&w); + 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)ink_connection_emit_signal(p->conn, + (void)link_connection_emit_signal(p->conn, COND_PATH_OBJECT, COND_INTERFACE, "ConditionChanged", @@ -793,31 +793,31 @@ int dbus_init(uev_ctx_t *ctx) { dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET); - if (ink_server_new(&server, FINIT_BUS_SOCKET) < 0) { + if (link_server_new(&server, FINIT_BUS_SOCKET) < 0) { err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET); return 1; } - if (ink_server_add_object(server, "/org/finit/manager", + if (link_server_add_object(server, "/org/finit/manager", &manager_vtable, NULL) < 0) { err(1, "Failed registering Manager1 object"); - ink_server_free(server); + link_server_free(server); server = NULL; return 1; } - if (ink_server_add_object(server, COND_PATH_OBJECT, + if (link_server_add_object(server, COND_PATH_OBJECT, &cond_vtable, NULL) < 0) { err(1, "Failed registering Cond1 object"); - ink_server_free(server); + link_server_free(server); server = NULL; return 1; } if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, - ink_server_get_fd(server), UEV_READ)) { + link_server_get_fd(server), UEV_READ)) { err(1, "Failed registering D-Bus accept watcher"); - ink_server_free(server); + link_server_free(server); server = NULL; return 1; } @@ -847,7 +847,7 @@ int dbus_exit(void) peer_drop(p); if (server) { - ink_server_free(server); + link_server_free(server); server = NULL; } From bfadd908566c404c7a4392a9caf52b317a33773e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 14:22:16 +0200 Subject: [PATCH 11/22] libink: client + server (was server-only) initctl drops its private wire implementation (src/dbus-client.{c,h}, ~490 lines) and links libink directly. The earlier server-only framing pushed the same wire format into three places: libink, the initctl client, and the test helper. Lifting it costs little and removes ~500 LOC of duplication. New in libink: * client.c -- link_client_open/close/call. Synchronous: connect, AUTH EXTERNAL, BEGIN, send METHOD_CALL, read reply. * io.c -- __io_read_full / __io_write_all consolidate the four EINTR-resilient blocking loops auth.c, dispatch.c and client.c all needed. * auth.c gains __auth_client (mirror of __auth_process); proto.c gains __msg_build_method_call. Naming settled at the same time: public API link_*, internal __*, public header libink/link.h (was libink/ink.h), internal libink/internal.h. Fixed a real bug caught by simplify: read_one added wire-supplied fields_len to 16 before bounds-checking it, so a peer sending 0xFFFFFFF0 wrapped past zero and bypassed the rxbuf guard. Both lengths now checked against rxbuf before the arithmetic. Signed-off-by: Joachim Wiberg --- libink/Makefile.am | 11 +- libink/auth.c | 102 ++++-- libink/builtin.c | 48 +-- libink/client.c | 178 ++++++++++ libink/connection.c | 10 +- libink/dispatch.c | 116 +++---- libink/{ink-internal.h => internal.h} | 36 ++- libink/io.c | 54 ++++ libink/{ink.h => link.h} | 37 ++- libink/marshal.c | 42 +-- libink/marshal.h | 42 +-- libink/match.c | 18 +- libink/proto.c | 40 ++- libink/proto.h | 19 +- libink/server.c | 4 +- src/Makefile.am | 3 +- src/dbus-client.c | 446 -------------------------- src/dbus-client.h | 48 --- src/dbus.c | 2 +- src/initctl.c | 43 ++- 20 files changed, 592 insertions(+), 707 deletions(-) create mode 100644 libink/client.c rename libink/{ink-internal.h => internal.h} (75%) create mode 100644 libink/io.c rename libink/{ink.h => link.h} (83%) delete mode 100644 src/dbus-client.c delete mode 100644 src/dbus-client.h diff --git a/libink/Makefile.am b/libink/Makefile.am index 7935edc4..7f908a9b 100644 --- a/libink/Makefile.am +++ b/libink/Makefile.am @@ -1,4 +1,4 @@ -# libink — D-Bus server library born inside Finit +# 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 \ @@ -6,10 +6,9 @@ libink_la_SOURCES = server.c auth.c connection.c \ dispatch.c builtin.c \ match.c \ path.c \ - ink-internal.h + client.c io.c \ + internal.h -# Public header: ink.h installed by `ink_HEADERS` below; path.h -# is also exported, so consumers get ink_path_encode. 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 @@ -18,6 +17,6 @@ libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = libink.pc -# Public header installs to $(includedir)/ink/ink.h +# Public headers install to $(includedir)/ink/ inkdir = $(includedir)/ink -ink_HEADERS = ink.h path.h +ink_HEADERS = link.h path.h diff --git a/libink/auth.c b/libink/auth.c index 876b064e..76baee13 100644 --- a/libink/auth.c +++ b/libink/auth.c @@ -24,25 +24,15 @@ #include #include -#include "ink-internal.h" +#include "internal.h" static const char rejected_ext[] = "REJECTED EXTERNAL\r\n"; -static int write_all(int fd, const char *buf, size_t len) -{ - while (len > 0) { - ssize_t n = write(fd, buf, len); +#define write_all(fd, buf, len) __io_write_all((fd), (buf), (len)) - if (n < 0) { - if (errno == EINTR) - continue; - return -1; - } - buf += n; - len -= (size_t)n; - } - return 0; -} +/* 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) { @@ -54,9 +44,8 @@ static int reject(link_connection_t *conn) return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1); } -void link__auth_generate_guid(char out[33]) +void __auth_generate_guid(char out[33]) { - static const char hex[] = "0123456789abcdef"; uint8_t raw[16]; size_t i; @@ -69,8 +58,8 @@ void link__auth_generate_guid(char out[33]) } for (i = 0; i < sizeof(raw); i++) { - out[i * 2] = hex[raw[i] >> 4]; - out[i * 2 + 1] = hex[raw[i] & 0xf]; + out[i * 2] = hex_digits[raw[i] >> 4]; + out[i * 2 + 1] = hex_digits[raw[i] & 0xf]; } out[32] = '\0'; } @@ -178,7 +167,7 @@ static size_t take_line(link_connection_t *conn, char *out, size_t outsz) return 0; } -int link__auth_process(link_connection_t *conn) +int __auth_process(link_connection_t *conn) { uint8_t buf[256]; ssize_t n; @@ -236,3 +225,76 @@ int link__auth_process(link_connection_t *conn) 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 index acf9198b..715157db 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -15,7 +15,7 @@ #include #include -#include "ink-internal.h" +#include "internal.h" /* ---------- helpers ---------- */ @@ -34,14 +34,14 @@ static int send_string_reply(link_connection_t *conn, const struct link_msg *req struct link_writer w; ssize_t blen; - link__w_init(&w, conn->txbuf, sizeof(conn->txbuf)); - link__w_string(&w, s); - blen = link__w_finish(&w); + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_string(&w, s); + blen = __w_finish(&w); if (blen < 0) { errno = EMSGSIZE; return -1; } - return link__send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); + return __send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); } /* ---------- Hello ---------- */ @@ -61,7 +61,7 @@ static int handle_hello(link_connection_t *conn, const struct link_msg *m) static int handle_ping(link_connection_t *conn, const struct link_msg *m) { - return link__send_method_return(conn, m, NULL, NULL, 0); + return __send_method_return(conn, m, NULL, NULL, 0); } static int handle_get_machine_id(link_connection_t *conn, const struct link_msg *m) @@ -74,7 +74,7 @@ static int handle_get_machine_id(link_connection_t *conn, const struct link_msg static char machine_id[33]; if (!machine_id[0]) - link__auth_generate_guid(machine_id); + __auth_generate_guid(machine_id); return send_string_reply(conn, m, machine_id); } @@ -224,7 +224,7 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) xprintf(&x, "\n"); if (x.err) - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", "Introspection XML overflow"); @@ -239,26 +239,26 @@ static int handle_add_match(link_connection_t *conn, const struct link_msg *m) struct link_reader r; if (!m->signature || strcmp(m->signature, "s") != 0) - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "AddMatch takes a single string"); - link__r_init(&r, m->body, m->body_avail); - if (link__r_string(&r, &rule) < 0) - return link__send_error(conn, m, + __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 (link__match_add(conn, rule) < 0) { + if (__match_add(conn, rule) < 0) { if (errno == ENOSPC) - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.LimitsExceeded", "Too many active match rules"); - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.MatchRuleInvalid", "Unrecognised key or malformed rule"); } - return link__send_method_return(conn, m, NULL, NULL, 0); + return __send_method_return(conn, m, NULL, NULL, 0); } static int handle_remove_match(link_connection_t *conn, const struct link_msg *m) @@ -267,27 +267,27 @@ static int handle_remove_match(link_connection_t *conn, const struct link_msg *m struct link_reader r; if (!m->signature || strcmp(m->signature, "s") != 0) - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "RemoveMatch takes a single string"); - link__r_init(&r, m->body, m->body_avail); - if (link__r_string(&r, &rule) < 0) - return link__send_error(conn, m, + __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 (link__match_remove(conn, rule) < 0) - return link__send_error(conn, m, + if (__match_remove(conn, rule) < 0) + return __send_error(conn, m, "org.freedesktop.DBus.Error.MatchRuleNotFound", "No such match rule on this connection"); - return link__send_method_return(conn, m, NULL, NULL, 0); + return __send_method_return(conn, m, NULL, NULL, 0); } /* ---------- entry point ---------- */ -int link__handle_builtin(link_connection_t *conn, const struct link_msg *m) +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) diff --git a/libink/client.c b/libink/client.c new file mode 100644 index 00000000..381d3eb1 --- /dev/null +++ b/libink/client.c @@ -0,0 +1,178 @@ +/* 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 "internal.h" + +struct link_client { + int fd; + uint32_t next_serial; + /* 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; +} + +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, + char *err_buf, size_t err_buf_sz) +{ + /* 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; + struct link_msg reply; + + if (!c || c->fd < 0 || !obj_path || !member) + return LINK_CALL_FAIL; + if (err_buf && err_buf_sz) + err_buf[0] = '\0'; + + 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_one(c, &reply) < 0) + return LINK_CALL_FAIL; + + if (reply.type == LINK_MSG_METHOD_RETURN) + return LINK_CALL_OK; + if (reply.type == LINK_MSG_ERROR) { + if (err_buf && err_buf_sz && reply.error_name) { + size_t n = strlen(reply.error_name); + + if (n >= err_buf_sz) + n = err_buf_sz - 1; + memcpy(err_buf, reply.error_name, n); + err_buf[n] = '\0'; + } + return LINK_CALL_ERROR; + } + return LINK_CALL_FAIL; +} diff --git a/libink/connection.c b/libink/connection.c index 030573cc..f88afda8 100644 --- a/libink/connection.c +++ b/libink/connection.c @@ -9,7 +9,7 @@ #include #include -#include "ink-internal.h" +#include "internal.h" int link_connection_get_fd(const link_connection_t *conn) { @@ -29,7 +29,7 @@ void link_connection_close(link_connection_t *conn) return; for (i = 0; i < conn->matches_count; i++) - link__match_free(conn->matches[i]); + __match_free(conn->matches[i]); if (conn->fd >= 0) close(conn->fd); @@ -46,13 +46,13 @@ static int process_binary(link_connection_t *conn) struct link_msg msg; ssize_t consumed; - consumed = link__msg_parse(conn->rxbuf, conn->rxlen, &msg); + consumed = __msg_parse(conn->rxbuf, conn->rxlen, &msg); if (consumed == 0) break; /* incomplete; wait for more bytes */ if (consumed < 0) return -1; - if (link__dispatch_message(conn, &msg) < 0) + if (__dispatch_message(conn, &msg) < 0) return -1; memmove(conn->rxbuf, conn->rxbuf + consumed, @@ -73,7 +73,7 @@ int link_connection_process(link_connection_t *conn) return -1; if (conn->auth != LINK_AUTH_DONE) { - if (link__auth_process(conn) < 0) + if (__auth_process(conn) < 0) return -1; /* Still in SASL phase — wait for more bytes. */ diff --git a/libink/dispatch.c b/libink/dispatch.c index e7ac7503..1035ab2b 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -10,7 +10,7 @@ #include #include -#include "ink-internal.h" +#include "internal.h" /* ---------- object/vtable registration ---------- */ @@ -135,27 +135,15 @@ static const link_method_t *resolve(struct link_object *o, /* ---------- send helpers ---------- */ -/* TODO: send_all is blocking. A slow or paused peer that lets its +/* send_all is blocking. A slow or paused peer that lets its * socket buffer fill will stall PID 1 here. The 64-peer cap + * kernel buffer (~256 KB) hide the symptom for now; for production - * hardening, make the fd non-blocking and drop the peer on EAGAIN. */ -static int send_all(int fd, const uint8_t *buf, size_t len) -{ - while (len > 0) { - ssize_t n = write(fd, buf, len); - - if (n < 0) { - if (errno == EINTR) - continue; - return -1; - } - buf += n; - len -= (size_t)n; - } - return 0; -} + * hardening, make the fd non-blocking and drop the peer on EAGAIN. + * The shared implementation lives in libink/io.c so the eventual + * fix lands in one place. */ +#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) -int link__send_method_return(link_connection_t *conn, const struct link_msg *req, +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) { @@ -163,7 +151,7 @@ int link__send_method_return(link_connection_t *conn, const struct link_msg *req ssize_t hlen; uint32_t serial = ++conn->next_serial; - hlen = link__msg_build_return(hdr, sizeof(hdr), serial, + hlen = __msg_build_return(hdr, sizeof(hdr), serial, req->serial, req->sender, out_sig, (uint32_t)body_len); @@ -200,7 +188,7 @@ int link_connection_emit_signal(link_connection_t *conn, return 0; /* peer hasn't finished the SASL phase */ for (i = 0; i < conn->matches_count; i++) { - if (link__match_matches(conn->matches[i], path, + if (__match_matches(conn->matches[i], path, interface, member)) { matched = 1; break; @@ -210,7 +198,7 @@ int link_connection_emit_signal(link_connection_t *conn, return 0; /* peer didn't subscribe — nothing to do */ serial = ++conn->next_serial; - hlen = link__msg_build_signal(hdr, sizeof(hdr), serial, + hlen = __msg_build_signal(hdr, sizeof(hdr), serial, path, interface, member, signature, (uint32_t)body_len); if (hlen < 0) { @@ -225,7 +213,7 @@ int link_connection_emit_signal(link_connection_t *conn, return 0; } -int link__send_error(link_connection_t *conn, const struct link_msg *req, +int __send_error(link_connection_t *conn, const struct link_msg *req, const char *error_name, const char *text) { uint8_t hdr[512]; @@ -239,9 +227,9 @@ int link__send_error(link_connection_t *conn, const struct link_msg *req, struct link_writer w; ssize_t n; - link__w_init(&w, body, sizeof(body)); - link__w_string(&w, text); - n = link__w_finish(&w); + __w_init(&w, body, sizeof(body)); + __w_string(&w, text); + n = __w_finish(&w); if (n < 0) { errno = EMSGSIZE; return -1; @@ -250,7 +238,7 @@ int link__send_error(link_connection_t *conn, const struct link_msg *req, sig = "s"; } - hlen = link__msg_build_error(hdr, sizeof(hdr), serial, + hlen = __msg_build_error(hdr, sizeof(hdr), serial, req->serial, req->sender, error_name, sig, (uint32_t)blen); if (hlen < 0) { @@ -277,7 +265,7 @@ link_writer_t *link_call_reply(link_call_t *call) if (!call || call->reply_consumed || call->error_sent) return NULL; call->reply_consumed = 1; - link__w_init(&call->reply_writer, + __w_init(&call->reply_writer, call->conn->txbuf, sizeof(call->conn->txbuf)); return &call->reply_writer; } @@ -289,35 +277,35 @@ int link_call_reply_error(link_call_t *call, const char *name, const char *messa return -1; } call->error_sent = 1; - return link__send_error(call->conn, &call->incoming, name, message); + 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 link__r_byte (&c->read_cursor, o); } -int link_call_read_bool (link_call_t *c, int *o) { return link__r_bool (&c->read_cursor, o); } -int link_call_read_u32 (link_call_t *c, uint32_t *o) { return link__r_u32 (&c->read_cursor, o); } -int link_call_read_string(link_call_t *c, const char **o) { return link__r_string(&c->read_cursor, o); } -int link_call_read_path (link_call_t *c, const char **o) { return link__r_path (&c->read_cursor, o); } +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) { link__w_init(w, buf, cap); } -ssize_t link_writer_finish(link_writer_t *w) { return link__w_finish(w); } +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) { link__w_byte(w, v); } -void link_w_bool (link_writer_t *w, int v) { link__w_bool(w, v); } -void link_w_u32 (link_writer_t *w, uint32_t v) { link__w_u32(w, v); } -void link_w_string (link_writer_t *w, const char *s) { link__w_string(w, s); } -void link_w_path (link_writer_t *w, const char *s) { link__w_path(w, s); } -void link_w_array_begin (link_writer_t *w, char ec) { link__w_array_begin(w, ec); } -void link_w_array_end (link_writer_t *w) { link__w_array_end(w); } -void link_w_struct_begin(link_writer_t *w) { link__w_struct_begin(w); } -void link_w_struct_end (link_writer_t *w) { link__w_struct_end(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); } /* ---------- dispatch entry point ---------- */ -int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) +int __dispatch_message(link_connection_t *conn, const struct link_msg *m) { struct link_object *o; struct link_vtable_entry *e = NULL; @@ -333,31 +321,31 @@ int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) } if (!m->path || !m->member) { - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Method call without path or member"); } - /* Built-in DBus interfaces (Hello, Ping, Introspect) 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 - * link__handle_builtin. */ - rc = link__handle_builtin(conn, m); + /* 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 link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.UnknownObject", "No such object"); } meth = resolve(o, m->interface, m->member, &e); if (!meth) { - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.UnknownMethod", "No such method on this object"); } @@ -368,7 +356,7 @@ int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) const char *want = meth->in_sig ? meth->in_sig : ""; if (strcmp(got, want) != 0) - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.InvalidArgs", "Argument signature mismatch"); } @@ -378,7 +366,7 @@ int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) * 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 link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.AccessDenied", "Method requires root privileges"); } @@ -386,12 +374,12 @@ int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) memset(&call, 0, sizeof(call)); call.conn = conn; call.incoming = *m; - link__r_init(&call.read_cursor, m->body, m->body_avail); + __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. */ - link__send_error(conn, m, + __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", "Handler failed"); return 0; @@ -400,17 +388,17 @@ int link__dispatch_message(link_connection_t *conn, const struct link_msg *m) if (!call.reply_consumed && !call.error_sent) { /* Handler returned 0 but never produced a reply; treat as * empty reply with out_sig "". */ - link__send_method_return(conn, m, NULL, NULL, 0); + __send_method_return(conn, m, NULL, NULL, 0); return 0; } if (call.reply_consumed && !call.error_sent) { - blen = link__w_finish(&call.reply_writer); + blen = __w_finish(&call.reply_writer); if (blen < 0) - return link__send_error(conn, m, + return __send_error(conn, m, "org.freedesktop.DBus.Error.Failed", "Reply marshalling overflow"); - return link__send_method_return(conn, m, meth->out_sig, + return __send_method_return(conn, m, meth->out_sig, conn->txbuf, (size_t)blen); } diff --git a/libink/ink-internal.h b/libink/internal.h similarity index 75% rename from libink/ink-internal.h rename to libink/internal.h index 386ff774..ac5b7153 100644 --- a/libink/ink-internal.h +++ b/libink/internal.h @@ -3,13 +3,13 @@ * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ -#ifndef LIBINK_INK_INTERNAL_H_ -#define LIBINK_INK_INTERNAL_H_ +#ifndef LIBINK_INTERNAL_H_ +#define LIBINK_INTERNAL_H_ #include #include -#include "ink.h" +#include "link.h" #include "marshal.h" #include "proto.h" @@ -109,28 +109,34 @@ struct link_connection { struct link_server *server; /* back-pointer for dispatch */ }; +/* io.c — shared blocking I/O loops. See the TODO by send_all + * callers about future non-blocking + drop-on-EAGAIN hardening. */ +int __io_write_all(int fd, const void *buf, size_t len); +int __io_read_full(int fd, void *buf, size_t len); + /* auth.c */ -int link__auth_process(link_connection_t *conn); -void link__auth_generate_guid(char out[33]); +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 link__dispatch_message(link_connection_t *conn, const struct link_msg *m); -int link__send_error(link_connection_t *conn, const struct link_msg *req, +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 link__send_method_return(link_connection_t *conn, const struct link_msg *req, +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 link__handle_builtin(link_connection_t *conn, const struct link_msg *m); +int __handle_builtin(link_connection_t *conn, const struct link_msg *m); /* match.c */ -struct link_match *link__match_parse (const char *rule); -void link__match_free (struct link_match *m); -int link__match_matches(const struct link_match *m, +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 link__match_add (link_connection_t *conn, const char *rule); -int link__match_remove (link_connection_t *conn, const char *rule); +int __match_add (link_connection_t *conn, const char *rule); +int __match_remove (link_connection_t *conn, const char *rule); -#endif /* LIBINK_INK_INTERNAL_H_ */ +#endif /* LIBINK_INTERNAL_H_ */ diff --git a/libink/io.c b/libink/io.c new file mode 100644 index 00000000..cf29e651 --- /dev/null +++ b/libink/io.c @@ -0,0 +1,54 @@ +/* libink — shared blocking I/O helpers. + * + * Server send paths (libink/dispatch.c, libink/auth.c) and the + * client (libink/client.c) all need EINTR-resilient write_all / + * read_full. Consolidate them here so a future hardening change + * (e.g. non-blocking sends with drop-peer-on-EAGAIN, see the TODO + * by send_all callers) only has to land in one place. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "internal.h" + +int __io_write_all(int fd, const void *buf, size_t len) +{ + const char *p = buf; + + while (len > 0) { + ssize_t n = write(fd, p, len); + + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += n; + len -= (size_t)n; + } + return 0; +} + +int __io_read_full(int fd, void *buf, size_t len) +{ + char *p = buf; + + while (len > 0) { + ssize_t n = read(fd, p, len); + + if (n == 0) + return -1; + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + p += n; + len -= (size_t)n; + } + return 0; +} diff --git a/libink/ink.h b/libink/link.h similarity index 83% rename from libink/ink.h rename to libink/link.h index c4687fe6..11f4fda4 100644 --- a/libink/ink.h +++ b/libink/link.h @@ -20,8 +20,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -#ifndef LIBINK_INK_H_ -#define LIBINK_INK_H_ +#ifndef LIBINK_LINK_H_ +#define LIBINK_LINK_H_ #include #include @@ -34,6 +34,7 @@ extern "C" { 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; /* Writer is exposed so callers can stack-allocate one for marshalling * signal/reply bodies. Treat the fields as opaque; use link_writer_init @@ -144,6 +145,36 @@ int link_connection_emit_signal(link_connection_t *conn, 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. */ +#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. + * + * On LINK_CALL_ERROR the peer's org.* error name is copied into + * err_buf (truncated if needed; pass NULL if you don't care). */ +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, + char *err_buf, size_t err_buf_sz); + /* ---------- standalone writer ---------- * * For marshalling bodies outside a method-call handler (signals, @@ -169,4 +200,4 @@ void link_w_struct_end (link_writer_t *w); } #endif -#endif /* LIBINK_INK_H_ */ +#endif /* LIBINK_LINK_H_ */ diff --git a/libink/marshal.c b/libink/marshal.c index 9bbc1c52..d22c7c8c 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -11,7 +11,7 @@ #define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) -void link__w_init(struct link_writer *w, uint8_t *buf, size_t cap) +void __w_init(struct link_writer *w, uint8_t *buf, size_t cap) { w->buf = buf; w->cap = cap; @@ -20,7 +20,7 @@ void link__w_init(struct link_writer *w, uint8_t *buf, size_t cap) w->array_depth = 0; } -ssize_t link__w_finish(struct link_writer *w) +ssize_t __w_finish(struct link_writer *w) { if (w->err || w->array_depth != 0) return -1; @@ -58,21 +58,21 @@ static void put_u32(struct link_writer *w, uint32_t v) w->off += 4; } -void link__w_byte(struct link_writer *w, uint8_t v) +void __w_byte(struct link_writer *w, uint8_t v) { if (reserve(w, 1, 1) < 0) return; w->buf[w->off++] = v; } -void link__w_bool(struct link_writer *w, int v) +void __w_bool(struct link_writer *w, int v) { if (reserve(w, 4, 4) < 0) return; put_u32(w, v ? 1u : 0u); } -void link__w_u32(struct link_writer *w, uint32_t v) +void __w_u32(struct link_writer *w, uint32_t v) { if (reserve(w, 4, 4) < 0) return; @@ -98,9 +98,9 @@ static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_ w->buf[w->off++] = 0; } -void link__w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } -void link__w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } -void link__w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); } +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) { @@ -115,7 +115,7 @@ static size_t element_align(char c) } } -void link__w_array_begin(struct link_writer *w, char element_sig_first_char) +void __w_array_begin(struct link_writer *w, char element_sig_first_char) { size_t lenpos; @@ -141,7 +141,7 @@ void link__w_array_begin(struct link_writer *w, char element_sig_first_char) w->array_depth++; } -void link__w_array_end(struct link_writer *w) +void __w_array_end(struct link_writer *w) { size_t elemstart, lenpos; uint32_t actual; @@ -157,19 +157,19 @@ void link__w_array_end(struct link_writer *w) put_u32_at(w, lenpos, actual); } -void link__w_struct_begin(struct link_writer *w) +void __w_struct_begin(struct link_writer *w) { reserve(w, 8, 0); } -void link__w_struct_end(struct link_writer *w) +void __w_struct_end(struct link_writer *w) { (void)w; } /* ---- reader ---- */ -void link__r_init(struct link_reader *r, const uint8_t *body, size_t len) +void __r_init(struct link_reader *r, const uint8_t *body, size_t len) { r->base = body; r->off = 0; @@ -200,7 +200,7 @@ static uint32_t rd_u32(const uint8_t *p) | ((uint32_t)p[3] << 24); } -int link__r_byte(struct link_reader *r, uint8_t *out) +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; @@ -210,7 +210,7 @@ int link__r_byte(struct link_reader *r, uint8_t *out) return 0; } -int link__r_u32(struct link_reader *r, uint32_t *out) +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; @@ -221,11 +221,11 @@ int link__r_u32(struct link_reader *r, uint32_t *out) return 0; } -int link__r_bool(struct link_reader *r, int *out) +int __r_bool(struct link_reader *r, int *out) { uint32_t v; - if (link__r_u32(r, &v) < 0) + if (__r_u32(r, &v) < 0) return -1; *out = v ? 1 : 0; return 0; @@ -235,7 +235,7 @@ static int read_string_like(struct link_reader *r, const char **out) { uint32_t len; - if (link__r_u32(r, &len) < 0) + if (__r_u32(r, &len) < 0) return -1; if (r->off + (size_t)len + 1 > r->cap) { r->err = 1; @@ -251,10 +251,10 @@ static int read_string_like(struct link_reader *r, const char **out) return 0; } -int link__r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } -int link__r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } +int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } +int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } -int link__r_done(const struct link_reader *r) +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 index e7027b88..ef77262a 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -12,30 +12,30 @@ /* 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 "ink.h" +#include "link.h" -void link__w_init (struct link_writer *w, uint8_t *buf, size_t cap); -ssize_t link__w_finish(struct link_writer *w); +void __w_init (struct link_writer *w, uint8_t *buf, size_t cap); +ssize_t __w_finish(struct link_writer *w); -void link__w_byte (struct link_writer *w, uint8_t v); -void link__w_bool (struct link_writer *w, int v); -void link__w_u32 (struct link_writer *w, uint32_t v); -void link__w_string (struct link_writer *w, const char *s); /* "s" */ -void link__w_path (struct link_writer *w, const char *s); /* "o" */ -void link__w_sig (struct link_writer *w, const char *s); /* "g" */ +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 link__w_array_begin (struct link_writer *w, char element_sig_first_char); -void link__w_array_end (struct link_writer *w); +void __w_array_begin (struct link_writer *w, char element_sig_first_char); +void __w_array_end (struct link_writer *w); -void link__w_struct_begin(struct link_writer *w); -void link__w_struct_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. - * String pointers returned by link__r_string reference the input + * String pointers returned by __r_string reference the input * buffer and are valid for the lifetime of that buffer (i.e. for * the duration of the current call dispatch). */ struct link_reader { @@ -45,12 +45,12 @@ struct link_reader { int err; /* sticky */ }; -void link__r_init (struct link_reader *r, const uint8_t *body, size_t len); -int link__r_byte (struct link_reader *r, uint8_t *out); -int link__r_bool (struct link_reader *r, int *out); -int link__r_u32 (struct link_reader *r, uint32_t *out); -int link__r_string(struct link_reader *r, const char **out); /* "s" */ -int link__r_path (struct link_reader *r, const char **out); /* "o" */ -int link__r_done (const struct link_reader *r); +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 index c8101e09..add721b4 100644 --- a/libink/match.c +++ b/libink/match.c @@ -15,7 +15,7 @@ #include #include -#include "ink-internal.h" +#include "internal.h" static char *dup_range(const char *p, size_t n) { @@ -73,7 +73,7 @@ static int parse_kv(const char **p, char **out_key, char **out_value) return 0; } -struct link_match *link__match_parse(const char *rule) +struct link_match *__match_parse(const char *rule) { struct link_match *m; const char *p; @@ -126,12 +126,12 @@ struct link_match *link__match_parse(const char *rule) return m; bad: - link__match_free(m); + __match_free(m); errno = EINVAL; return NULL; } -void link__match_free(struct link_match *m) +void __match_free(struct link_match *m) { if (!m) return; @@ -152,7 +152,7 @@ static int field_matches(const char *want, const char *got) return strcmp(want, got) == 0; } -int link__match_matches(const struct link_match *m, +int __match_matches(const struct link_match *m, const char *path, const char *iface, const char *member) { @@ -165,7 +165,7 @@ int link__match_matches(const struct link_match *m, && field_matches(m->member, member); } -int link__match_add(link_connection_t *conn, const char *rule) +int __match_add(link_connection_t *conn, const char *rule) { struct link_match *m; @@ -174,7 +174,7 @@ int link__match_add(link_connection_t *conn, const char *rule) return -1; } - m = link__match_parse(rule); + m = __match_parse(rule); if (!m) return -1; @@ -182,13 +182,13 @@ int link__match_add(link_connection_t *conn, const char *rule) return 0; } -int link__match_remove(link_connection_t *conn, const char *rule) +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) { - link__match_free(conn->matches[i]); + __match_free(conn->matches[i]); conn->matches[i] = conn->matches[conn->matches_count - 1]; conn->matches_count--; return 0; diff --git a/libink/proto.c b/libink/proto.c index 5ff94b7d..d9bf712e 100644 --- a/libink/proto.c +++ b/libink/proto.c @@ -75,7 +75,7 @@ static const char *parse_signature(const uint8_t *buf, size_t avail, size_t *con return (const char *)(buf + 1); } -ssize_t link__msg_parse(const uint8_t *buf, size_t len, struct link_msg *out) +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; @@ -271,7 +271,7 @@ static ssize_t finalize_header(uint8_t *buf, size_t cap, return (ssize_t)hdr_end; } -ssize_t link__msg_build_return(uint8_t *buf, size_t cap, +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) @@ -295,7 +295,7 @@ ssize_t link__msg_build_return(uint8_t *buf, size_t cap, body_len, serial, off); } -ssize_t link__msg_build_error(uint8_t *buf, size_t cap, +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, @@ -322,7 +322,7 @@ ssize_t link__msg_build_error(uint8_t *buf, size_t cap, body_len, serial, off); } -ssize_t link__msg_build_signal(uint8_t *buf, size_t cap, +ssize_t __msg_build_signal(uint8_t *buf, size_t cap, uint32_t serial, const char *path, const char *interface, @@ -349,7 +349,37 @@ ssize_t link__msg_build_signal(uint8_t *buf, size_t cap, body_len, serial, off); } -size_t link__msg_header_size(const struct link_msg *m) +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. */ diff --git a/libink/proto.h b/libink/proto.h index cbba820e..2abac893 100644 --- a/libink/proto.h +++ b/libink/proto.h @@ -63,35 +63,44 @@ struct link_msg { * 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 link__msg_parse(const uint8_t *buf, size_t len, struct link_msg *out); +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 link__msg_header_size(const struct link_msg *m); +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 link__msg_build_return(uint8_t *buf, size_t cap, +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 link__msg_build_error(uint8_t *buf, size_t cap, +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 link__msg_build_signal(uint8_t *buf, size_t cap, +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 index 9edf3e8b..ef6325ae 100644 --- a/libink/server.c +++ b/libink/server.c @@ -12,7 +12,7 @@ #include #include -#include "ink-internal.h" +#include "internal.h" static void close_save_errno(int fd) { @@ -155,7 +155,7 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) else conn->peer_uid = (uid_t)-1; - link__auth_generate_guid(conn->guid); + __auth_generate_guid(conn->guid); *out = conn; return 0; diff --git a/src/Makefile.am b/src/Makefile.am index 25f6ae28..73125001 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -108,7 +108,8 @@ 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_SOURCES += dbus-client.c dbus-client.h +initctl_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/libink +initctl_LDADD += $(top_builddir)/libink/libink.la endif INIT_LNKS = init telinit diff --git a/src/dbus-client.c b/src/dbus-client.c deleted file mode 100644 index e336103d..00000000 --- a/src/dbus-client.c +++ /dev/null @@ -1,446 +0,0 @@ -/* Minimal D-Bus client for initctl. - * - * Copyright (c) 2026 Joachim Wiberg - * SPDX-License-Identifier: MIT - */ - -#include "config.h" - -#ifdef HAVE_DBUS - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "dbus-client.h" - -#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) - -struct dbusc { - int fd; - uint32_t next_serial; -}; - -static const char hex[] = "0123456789abcdef"; - -/* ---- transport helpers ---- */ - -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 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; -} - -/* ---- SASL handshake ---- */ - -static int do_auth(int fd) -{ - char uidstr[16]; - char hexuid[32]; - char line[64]; - char reply[256]; - size_t i, n; - int rc; - - if (write_all(fd, "\0", 1) < 0) - return -1; - - /* geteuid() matches what the kernel reports via SO_PEERCRED on - * the receiving side; getuid() would diverge if initctl is ever - * shipped setuid (it isn't today, but precision is cheap). */ - n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", - (unsigned)geteuid()); - if (n * 2 >= sizeof(hexuid)) - return -1; - 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'; - - 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 (read_line(fd, reply, sizeof(reply)) < 0) - return -1; - if (strncmp(reply, "OK ", 3) != 0) - return -1; - if (write_all(fd, "BEGIN\r\n", 7) < 0) - return -1; - return 0; -} - -/* ---- public ---- */ - -dbusc_t *dbusc_open(const char *path) -{ - struct sockaddr_un sun = { .sun_family = AF_UNIX }; - dbusc_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 (do_auth(fd) < 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 dbusc_close(dbusc_t *c) -{ - if (!c) - return; - if (c->fd >= 0) - close(c->fd); - free(c); -} - -/* ---- 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_sig(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; -} - -static int send_method_call(struct dbusc *c, - const char *obj_path, - const char *iface, - const char *member, - const char *arg_sig, - const char *arg_str, - 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) }; - uint32_t body_len = 0; - size_t fields_start, fields_end, padded_end; - uint32_t serial = c->next_serial++; - - if (arg_sig && *arg_sig) { - if (!strcmp(arg_sig, "s")) - b_put_string(&bb, arg_str ? arg_str : ""); - else if (!strcmp(arg_sig, "u")) - b_put_u32(&bb, arg_u32); - else - return -1; - if (bb.err) return -1; - body_len = (uint32_t)bb.off; - } - - memset(hdr, 0, 16); - hdr[0] = 'l'; - hdr[1] = 1; /* METHOD_CALL */ - 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] = (uint8_t)( serial & 0xff); - hdr[9] = (uint8_t)((serial >> 8) & 0xff); - hdr[10] = (uint8_t)((serial >> 16) & 0xff); - hdr[11] = (uint8_t)((serial >> 24) & 0xff); - b.off = 16; - fields_start = b.off; - - /* PATH (code 1, type 'o') */ - b_reserve(&b, 8, 0); - b_put_byte(&b, 1); - b_put_sig (&b, "o"); - b_put_string(&b, obj_path); - - if (iface) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 2); - b_put_sig (&b, "s"); - b_put_string(&b, iface); - } - - b_reserve(&b, 8, 0); - b_put_byte(&b, 3); - b_put_sig (&b, "s"); - b_put_string(&b, member); - - if (arg_sig && *arg_sig) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 8); - b_put_sig (&b, "g"); - b_put_sig (&b, arg_sig); - } - - 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(c->fd, hdr, b.off) < 0) return -1; - if (body_len > 0 && write_all(c->fd, body, body_len) < 0) return -1; - return 0; -} - -/* Read one method-return or error reply. Captures the ERROR_NAME - * header field on type=3 messages; everything else is consumed and - * discarded. */ -static int read_reply(int fd, uint8_t *out_type, - char *err_buf, size_t err_buf_sz) -{ - uint8_t hdr_fixed[16]; - uint8_t hdr_fields[2048]; - uint8_t body_dump[1024]; - uint32_t body_len, fields_len; - size_t body_off; - size_t pos; - - if (err_buf && err_buf_sz) - err_buf[0] = '\0'; - - if (read_full(fd, hdr_fixed, 16) < 0) - return -1; - if (hdr_fixed[0] != 'l') - return -1; - *out_type = hdr_fixed[1]; - 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); - 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; - - 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; - if (code == 4 && err_buf && slen < err_buf_sz) { - memcpy(err_buf, hdr_fields + pos, slen); - err_buf[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'g') { - uint32_t slen = hdr_fields[pos++]; - - if (pos + slen + 1 > fields_len) return -1; - pos += slen + 1; - } else if (vsig[0] == 'u') { - pos = ALIGN_UP(pos, 4); - pos += 4; - } else { - return -1; - } - } - - /* Drain the body in chunks of body_dump[]. */ - while (body_len > 0) { - size_t take = body_len > sizeof(body_dump) - ? sizeof(body_dump) : body_len; - - if (read_full(fd, body_dump, take) < 0) - return -1; - body_len -= (uint32_t)take; - } - return 0; -} - -int dbusc_call(dbusc_t *c, - const char *obj_path, - const char *iface, - const char *method, - const char *arg_sig, - const char *arg_str, - uint32_t arg_u32, - char *err_buf, - size_t err_buf_sz) -{ - uint8_t type = 0; - - if (!c || c->fd < 0 || !obj_path || !iface || !method) - return -1; - - if (send_method_call(c, obj_path, iface, method, - arg_sig, arg_str, arg_u32) < 0) - return -1; - if (read_reply(c->fd, &type, err_buf, err_buf_sz) < 0) - return -1; - - if (type == 2) /* METHOD_RETURN */ - return 0; - if (type == 3) /* ERROR */ - return 1; - return -1; -} - -#endif /* HAVE_DBUS */ diff --git a/src/dbus-client.h b/src/dbus-client.h deleted file mode 100644 index a386bdd1..00000000 --- a/src/dbus-client.h +++ /dev/null @@ -1,48 +0,0 @@ -/* Minimal D-Bus client used by initctl when /run/finit/bus is - * available. This is the client side of the protocol implemented - * by libink server-side; we don't link libink because that would - * pull dispatch + builtins + match into initctl unnecessarily. - * - * Copyright (c) 2026 Joachim Wiberg - * SPDX-License-Identifier: MIT - */ -#ifndef FINIT_DBUS_CLIENT_H_ -#define FINIT_DBUS_CLIENT_H_ - -#include -#include - -#ifdef HAVE_DBUS - -typedef struct dbusc dbusc_t; - -/* Connect, AUTH EXTERNAL with the caller's real uid, send BEGIN. - * Returns NULL on any failure (no warning printed -- caller is - * expected to fall back to a different transport). */ -dbusc_t *dbusc_open(const char *path); - -void dbusc_close(dbusc_t *c); - -/* Send a method call, await reply. arg_sig must be "" (no body), - * "s" (one string argument), or "u" (one uint32 argument). On an - * error reply, the error name is copied into err_buf (which may be - * NULL). - * - * Returns: - * 0 method return (success) - * 1 error reply -- err_buf has the org.* error name - * -1 transport / parse failure - */ -int dbusc_call(dbusc_t *c, - const char *obj_path, - const char *iface, - const char *method, - const char *arg_sig, - const char *arg_str, - uint32_t arg_u32, - char *err_buf, - size_t err_buf_sz); - -#endif /* HAVE_DBUS */ - -#endif /* FINIT_DBUS_CLIENT_H_ */ diff --git a/src/dbus.c b/src/dbus.c index f3e745e5..00064d3b 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -38,7 +38,7 @@ #include -#include "ink.h" +#include "link.h" #include "path.h" #include "finit.h" diff --git a/src/initctl.c b/src/initctl.c index 1f74bb21..1404eab7 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -269,7 +269,7 @@ static int do_startstop(int cmd, char *arg) } #ifdef HAVE_DBUS -#include "dbus-client.h" +#include "link.h" /* Try the D-Bus path for a Manager1 method. Returns: * 0 succeeded via D-Bus @@ -283,20 +283,39 @@ static int do_startstop(int cmd, char *arg) static int try_dbus_manager(const char *method, const char *arg_sig, const char *arg) { - dbusc_t *c; - char err[128]; - int rc; - - c = dbusc_open(FINIT_BUS_SOCKET); + link_client_t *c; + uint8_t body[256]; + link_writer_t w; + ssize_t body_len = 0; + char err[128]; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); if (!c) return -1; + if (arg_sig && *arg_sig) { + link_writer_init(&w, body, sizeof(body)); + if (!strcmp(arg_sig, "s")) + link_w_string(&w, arg ? arg : ""); + else { + link_client_close(c); + return -1; + } + body_len = link_writer_finish(&w); + if (body_len < 0) { + link_client_close(c); + return -1; + } + } + err[0] = '\0'; - rc = dbusc_call(c, "/org/finit/manager", "org.finit.Manager1", - method, arg_sig, arg, 0, err, sizeof(err)); - dbusc_close(c); + rc = link_client_call(c, "/org/finit/manager", "org.finit.Manager1", + method, arg_sig, body, (size_t)body_len, + err, sizeof(err)); + link_client_close(c); - if (rc == 1) { + if (rc == LINK_CALL_ERROR) { /* Exact match on the fully-qualified error name; substring * matching would misfire on a future name that contains * one of these as a substring. */ @@ -307,7 +326,9 @@ static int try_dbus_manager(const char *method, const char *arg_sig, ERRX(1, "permission denied: %s requires root", method); ERRX(1, "%s: %s", method, err[0] ? err : "D-Bus error"); } - return rc; + if (rc == LINK_CALL_OK) + return 0; + return -1; /* LINK_CALL_FAIL or anything else: fall back */ } #endif /* HAVE_DBUS */ From 161f364e09e6228022a8b93315fd2c215b0647e4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 17:35:15 +0200 Subject: [PATCH 12/22] libink: public reader, link_reply_t, varargs call Three additions to the public client API so callers can do more than just check call success/error: * struct link_reader moves from marshal.h to link.h, with link_reader_init + link_r_byte/_bool/_u32/_string/_path/_done public wrappers. Mirrors the writer surface. * link_reply_t + link_client_reply() expose a view of the most recent reply: type, signature, error_name (set on ERROR), and body bytes. Pointers reference the client's rxbuf and are invalidated by the next call or close. * link_client_call_v: sd-bus-style varargs wrapper over link_client_call. Pass a signature ("s", "u", "", ...) and the args inline; marshals into a 1 KiB stack buffer. Supports y/b/u/s/o today. The err_buf out-param on link_client_call goes away -- callers now read link_client_reply()->error_name instead. initctl's try_dbus_manager is updated to the new shape. Signed-off-by: Joachim Wiberg --- libink/client.c | 111 ++++++++++++++++++++++++++++++++++++++-------- libink/dispatch.c | 10 +++++ libink/link.h | 67 +++++++++++++++++++++++++--- libink/marshal.h | 12 +---- src/initctl.c | 51 ++++++++++----------- 5 files changed, 190 insertions(+), 61 deletions(-) diff --git a/libink/client.c b/libink/client.c index 381d3eb1..dd9be764 100644 --- a/libink/client.c +++ b/libink/client.c @@ -9,7 +9,7 @@ * SPDX-License-Identifier: MIT */ -#include +#include #include #include #include @@ -19,13 +19,18 @@ #include "internal.h" struct link_client { - int fd; - uint32_t next_serial; + 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; + uint8_t rxbuf[LINK_RX_BUF_SIZE]; + size_t rxlen; }; link_client_t *link_client_open(const char *path) @@ -128,8 +133,7 @@ int link_client_call(link_client_t *c, const char *interface, const char *member, const char *signature, - const uint8_t *body, size_t body_len, - char *err_buf, size_t err_buf_sz) + 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 @@ -141,10 +145,17 @@ int link_client_call(link_client_t *c, uint32_t serial; struct link_msg reply; + /* Reply view from a previous call points into c->rxbuf and + * is invalidated by every entry here -- clear it before the + * arg-validation guard so a bad-args call also clears, rather + * than leaving stale pointers visible via link_client_reply(). */ + if (c) { + memset(&c->reply, 0, sizeof(c->reply)); + c->have_reply = 0; + } + if (!c || c->fd < 0 || !obj_path || !member) return LINK_CALL_FAIL; - if (err_buf && err_buf_sz) - err_buf[0] = '\0'; serial = c->next_serial++; hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, @@ -161,18 +172,80 @@ int link_client_call(link_client_t *c, if (read_one(c, &reply) < 0) return LINK_CALL_FAIL; + c->reply.type = reply.type; + c->reply.signature = reply.signature; + c->reply.error_name = reply.error_name; + c->reply.body = reply.body_avail ? reply.body : NULL; + c->reply.body_len = reply.body_avail; + c->have_reply = 1; + if (reply.type == LINK_MSG_METHOD_RETURN) return LINK_CALL_OK; - if (reply.type == LINK_MSG_ERROR) { - if (err_buf && err_buf_sz && reply.error_name) { - size_t n = strlen(reply.error_name); - - if (n >= err_buf_sz) - n = err_buf_sz - 1; - memcpy(err_buf, reply.error_name, n); - err_buf[n] = '\0'; - } + if (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); +} diff --git a/libink/dispatch.c b/libink/dispatch.c index 1035ab2b..92c2d799 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -303,6 +303,16 @@ 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); } + /* ---------- dispatch entry point ---------- */ int __dispatch_message(link_connection_t *conn, const struct link_msg *m) diff --git a/libink/link.h b/libink/link.h index 11f4fda4..3e9dbbce 100644 --- a/libink/link.h +++ b/libink/link.h @@ -53,6 +53,29 @@ typedef struct link_writer { 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 a method-call reply, populated by link_client_call(_v) and + * returned by link_client_reply(). All pointers reference internal + * client storage and are invalidated by the next call 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`. */ +typedef struct { + uint8_t type; /* LINK_MSG_METHOD_RETURN or _ERROR */ + const char *signature; + const char *error_name; + 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); @@ -153,7 +176,7 @@ int link_connection_emit_signal(link_connection_t *conn, link_client_t *link_client_open(const char *path); void link_client_close(link_client_t *c); -/* Status codes returned by link_client_call. */ +/* 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 */ @@ -165,15 +188,36 @@ void link_client_close(link_client_t *c); * + link_writer_finish. Pass signature=NULL and body=NULL for * methods that take no arguments. * - * On LINK_CALL_ERROR the peer's org.* error name is copied into - * err_buf (truncated if needed; pass NULL if you don't care). */ + * 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, - char *err_buf, size_t err_buf_sz); + 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); /* ---------- standalone writer ---------- * @@ -196,6 +240,19 @@ 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); + #ifdef __cplusplus } #endif diff --git a/libink/marshal.h b/libink/marshal.h index ef77262a..b7717813 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -35,16 +35,8 @@ void __w_struct_end (struct link_writer *w); /* ---- reader ---- * * Reads from a message body pointer + length, advancing a cursor. - * String pointers returned by __r_string reference the input - * buffer and are valid for the lifetime of that buffer (i.e. for - * the duration of the current call dispatch). */ -struct link_reader { - const uint8_t *base; - size_t off; - size_t cap; - int err; /* sticky */ -}; - + * 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); diff --git a/src/initctl.c b/src/initctl.c index 1404eab7..3abba899 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -284,48 +284,45 @@ static int try_dbus_manager(const char *method, const char *arg_sig, const char *arg) { link_client_t *c; - uint8_t body[256]; - link_writer_t w; - ssize_t body_len = 0; - char err[128]; + const char *err; int rc; c = link_client_open(FINIT_BUS_SOCKET); if (!c) return -1; - if (arg_sig && *arg_sig) { - link_writer_init(&w, body, sizeof(body)); - if (!strcmp(arg_sig, "s")) - link_w_string(&w, arg ? arg : ""); - else { - link_client_close(c); - return -1; - } - body_len = link_writer_finish(&w); - if (body_len < 0) { - link_client_close(c); - return -1; - } - } - - err[0] = '\0'; - rc = link_client_call(c, "/org/finit/manager", "org.finit.Manager1", - method, arg_sig, body, (size_t)body_len, - err, sizeof(err)); - link_client_close(c); + /* "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") == 0) + 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") == 0) + } + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) { + link_client_close(c); ERRX(1, "permission denied: %s requires root", method); - ERRX(1, "%s: %s", method, err[0] ? err : "D-Bus error"); + } + 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 */ From 3af3d4e0d5abe47356c5422cdc4d7d92349ed729 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 18:41:07 +0200 Subject: [PATCH 13/22] libink: link_client_wait + signal-aware reply view * link_client_wait(c, timeout_ms) waits for the next inbound frame (typically a SIGNAL after AddMatch) and publishes it via the same link_client_reply() accessor link_client_call uses. timeout_ms < 0 blocks; == 0 polls; > 0 waits. * link_reply_t gains path/interface/member so callers can identify the signal that fired. NULL on method-returns and errors. * link_r_pos(reader) exposes the reader cursor for walking arrays. The D-Bus message-type codes (LINK_MSG_METHOD_RETURN/_ERROR/_SIGNAL, _METHOD_CALL, _INVALID) move from internal proto.h to public link.h so callers can interpret link_reply_t.type without an internal header. Signed-off-by: Joachim Wiberg --- libink/client.c | 84 ++++++++++++++++++++++++++++++++++++----------- libink/dispatch.c | 1 + libink/link.h | 56 ++++++++++++++++++++++++------- libink/proto.h | 7 +--- 4 files changed, 110 insertions(+), 38 deletions(-) diff --git a/libink/client.c b/libink/client.c index dd9be764..a5c245a1 100644 --- a/libink/client.c +++ b/libink/client.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT */ +#include +#include #include #include #include @@ -128,6 +130,56 @@ static int read_one(link_client_t *c, struct link_msg *msg) 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, @@ -143,17 +195,8 @@ int link_client_call(link_client_t *c, uint8_t hdr[1024]; ssize_t hlen; uint32_t serial; - struct link_msg reply; - - /* Reply view from a previous call points into c->rxbuf and - * is invalidated by every entry here -- clear it before the - * arg-validation guard so a bad-args call also clears, rather - * than leaving stale pointers visible via link_client_reply(). */ - if (c) { - memset(&c->reply, 0, sizeof(c->reply)); - c->have_reply = 0; - } + clear_reply(c); if (!c || c->fd < 0 || !obj_path || !member) return LINK_CALL_FAIL; @@ -169,19 +212,12 @@ int link_client_call(link_client_t *c, if (body_len > 0 && send_all(c->fd, body, body_len) < 0) return LINK_CALL_FAIL; - if (read_one(c, &reply) < 0) + if (read_and_publish(c, -1) != 0) return LINK_CALL_FAIL; - c->reply.type = reply.type; - c->reply.signature = reply.signature; - c->reply.error_name = reply.error_name; - c->reply.body = reply.body_avail ? reply.body : NULL; - c->reply.body_len = reply.body_avail; - c->have_reply = 1; - - if (reply.type == LINK_MSG_METHOD_RETURN) + if (c->reply.type == LINK_MSG_METHOD_RETURN) return LINK_CALL_OK; - if (reply.type == LINK_MSG_ERROR) + if (c->reply.type == LINK_MSG_ERROR) return LINK_CALL_ERROR; return LINK_CALL_FAIL; } @@ -249,3 +285,11 @@ int link_client_call_v(link_client_t *c, 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/dispatch.c b/libink/dispatch.c index 92c2d799..651e136a 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -312,6 +312,7 @@ 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 ---------- */ diff --git a/libink/link.h b/libink/link.h index 3e9dbbce..a6b74bda 100644 --- a/libink/link.h +++ b/libink/link.h @@ -36,6 +36,13 @@ 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 @@ -63,15 +70,20 @@ typedef struct link_reader { int err; /* sticky */ } link_reader_t; -/* View of a method-call reply, populated by link_client_call(_v) and +/* 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 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`. */ + * 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 or _ERROR */ + 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; @@ -219,6 +231,20 @@ int link_client_call_v(link_client_t *c, 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, @@ -245,13 +271,19 @@ void link_w_struct_end (link_writer_t *w); * 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); +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 } diff --git a/libink/proto.h b/libink/proto.h index 2abac893..f7ef0866 100644 --- a/libink/proto.h +++ b/libink/proto.h @@ -10,12 +10,7 @@ #include #include -/* Message types (D-Bus spec §4: "Message Format"). */ -#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 +#include "link.h" /* LINK_MSG_* type codes */ /* Message flags. */ #define LINK_FLAG_NO_REPLY_EXPECTED 0x01 From 38384d07745e56daad2d1af31d315a7a87e08186 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 18:41:23 +0200 Subject: [PATCH 14/22] test: port dbus-auth-client to libink The test client originally reimplemented the entire D-Bus wire format -- struct buf marshaller, struct reply parser, manual header walk, custom "s"/"o"/"as" decoders -- because libink was server-only. With libink bidirectional and exposing the reader, writer, message-type constants, link_client_wait and a signal-aware reply view, all of that can go. What still uses raw wire access: * mode_auth exists *to* test the AUTH EXTERNAL handshake itself, including REJECTED cases driven by deliberately-wrong uids. libink's __auth_client won't do -- it swallows the reply line but the test needs to print it for the shell to grep. * The setuid-as-uid modes drop the effective uid and then go through libink like everything else (AUTH EXTERNAL claims geteuid()). Net: 861 -> 467 LOC in the test client. Signed-off-by: Joachim Wiberg --- test/setup-sysroot.sh | 8 +- test/src/Makefile.am | 6 +- test/src/dbus-auth-client.c | 928 +++++++++++------------------------- 3 files changed, 278 insertions(+), 664 deletions(-) diff --git a/test/setup-sysroot.sh b/test/setup-sysroot.sh index 102cbad2..f986125e 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -15,7 +15,13 @@ make -C "$top_builddir" DESTDIR="$SYSROOT" install mkdir -p "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" -if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then +# Prefer the real ELF in .libs/ over the libtool wrapper script -- +# since the test client links libink.la, libtool wraps the top-level +# dbus-auth-client as a shell script that re-execs the real binary +# via its own RPATH, which falls apart inside the test namespace. +if [ -x "$top_builddir/test/src/.libs/dbus-auth-client" ]; then + cp "$top_builddir/test/src/.libs/dbus-auth-client" "$SYSROOT/sbin/" +elif [ -x "$top_builddir/test/src/dbus-auth-client" ]; then cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" fi diff --git a/test/src/Makefile.am b/test/src/Makefile.am index b8d607d1..87d6e362 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -9,6 +9,8 @@ serv_LDADD = $(lite_LIBS) endif if DBUS -noinst_PROGRAMS += dbus-auth-client -dbus_auth_client_SOURCES = dbus-auth-client.c +noinst_PROGRAMS += dbus-auth-client +dbus_auth_client_SOURCES = dbus-auth-client.c +dbus_auth_client_CPPFLAGS = -D_GNU_SOURCE -I$(top_srcdir)/libink +dbus_auth_client_LDADD = $(top_builddir)/libink/libink.la endif diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 00e275a4..53f85223 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -4,32 +4,44 @@ * dbus-auth-client auth * Send the SASL handshake claiming ; print server reply line. * Exit 0 if reply begins "OK ", 1 if "REJECTED ", 2 otherwise. + * (Manual SASL: this mode exists *to* test AUTH itself.) * * dbus-auth-client hello - * Auth as own uid; call org.freedesktop.DBus.Hello on - * /org/freedesktop/DBus. Print the assigned unique name. + * Call org.freedesktop.DBus.Hello, print the assigned unique name. * * dbus-auth-client introspect - * Auth + org.freedesktop.DBus.Introspectable.Introspect. - * Print the XML reply. + * Call org.freedesktop.DBus.Introspectable.Introspect, print XML. * * dbus-auth-client liststrings - * Auth + method call expecting reply signature "as"; print one - * string per line. + * Call method expecting reply signature "as", print one per line. + * + * dbus-auth-client call-s + * dbus-auth-client call-void + * Issue method call with the given (or no) argument; "OK" or + * "ERROR: " on stderr. + * + * dbus-auth-client call-{s,void}-as-uid ... + * As above, but setuid() first so AUTH EXTERNAL claims . + * + * dbus-auth-client get-service + * Manager1.GetService(identity) -> print the encoded path. + * + * dbus-auth-client monitor-signal + * AddMatch + wait for one SIGNAL. Print "SIGNAL " + * then any string-typed body args. Exit 0 on signal, 1 on timeout. * * dbus-auth-client unknown - * Auth + call a bogus method; exits 0 only if the server replies - * with an "org.freedesktop.DBus.Error.*" error. + * Call a bogus method, exit 0 iff the server replies with an + * org.freedesktop.DBus.Error.* error. * - * In every non-auth mode the program exits 0 on a successful method - * reply, 1 on a server-side error reply, 2 on transport / parse error. + * Exit codes for the non-auth modes: 0 on success, 1 on server-side + * error reply, 2 on transport / parse / arg error. * * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ #include -#include #include #include #include @@ -37,76 +49,35 @@ #include #include #include -#include -#include -static const char hex[] = "0123456789abcdef"; +#include "link.h" -#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) +/* ---------- manual SASL: only mode_auth uses this ---------- */ -/* ---------- low level I/O ---------- */ +static const char hex[] = "0123456789abcdef"; -static int write_all(int fd, const void *buf, size_t len) +static int write_all_fd(int fd, const void *buf, size_t len) { const char *p = buf; while (len > 0) { ssize_t n = write(fd, p, len); - - if (n < 0) { - if (errno == EINTR) continue; - return -1; - } - p += n; - len -= (size_t)n; - } - return 0; -} - -static int read_full(int fd, void *buf, size_t len) -{ - char *p = buf; - - while (len > 0) { - ssize_t n = read(fd, p, len); - - if (n == 0) return -1; if (n < 0) { if (errno == EINTR) continue; return -1; } - p += n; + p += n; len -= (size_t)n; } return 0; } -static int read_with_timeout(int fd, void *buf, size_t len, int timeout_ms) -{ - struct pollfd pfd = { .fd = fd, .events = POLLIN }; - int rc; - - for (;;) { - rc = poll(&pfd, 1, timeout_ms); - if (rc < 0) { - if (errno == EINTR) - continue; - return -1; - } - if (rc == 0) - return 0; /* timed out */ - break; - } - return (int)read(fd, buf, len); -} - -static ssize_t read_line(int fd, char *buf, size_t bufsz) +static ssize_t read_line_fd(int fd, char *buf, size_t bufsz) { size_t off = 0; while (off + 1 < bufsz) { ssize_t n = read(fd, buf + off, 1); - if (n == 0) return -1; if (n < 0) { if (errno == EINTR) continue; @@ -123,410 +94,6 @@ static ssize_t read_line(int fd, char *buf, size_t bufsz) return -1; } -/* ---------- connect + AUTH ---------- */ - -static int connect_and_auth(const char *path, uid_t claimed_uid) -{ - struct sockaddr_un sun = { .sun_family = AF_UNIX }; - char uidstr[16]; - char hexuid[32]; - char line[64]; - char reply[256]; - size_t i, n; - int fd, rc; - - if (strlen(path) >= sizeof(sun.sun_path)) - return -1; - memcpy(sun.sun_path, path, strlen(path) + 1); - - fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) return -1; - if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { - close(fd); - return -1; - } - - n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)claimed_uid); - for (i = 0; i < n; i++) { - unsigned c = (unsigned char)uidstr[i]; - - hexuid[i * 2] = hex[c >> 4]; - hexuid[i * 2 + 1] = hex[c & 0xf]; - } - hexuid[n * 2] = '\0'; - - if (write_all(fd, "\0", 1) < 0) goto io; - - rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); - if (rc < 0 || (size_t)rc >= sizeof(line)) goto io; - if (write_all(fd, line, (size_t)rc) < 0) goto io; - - if (read_line(fd, reply, sizeof(reply)) < 0) goto io; - if (strncmp(reply, "OK ", 3) != 0) { - fprintf(stderr, "auth failed: %s\n", reply); - close(fd); - return -1; - } - - if (write_all(fd, "BEGIN\r\n", 7) < 0) goto io; - return fd; - -io: - perror("auth handshake"); - close(fd); - return -1; -} - -/* ---------- D-Bus message build / parse ---------- */ - -struct buf { - uint8_t *p; - size_t cap; - size_t off; - int err; -}; - -static int b_reserve(struct buf *b, size_t align, size_t bytes) -{ - size_t pad = ALIGN_UP(b->off, align) - b->off; - - if (b->err || b->off + pad + bytes > b->cap) { - b->err = 1; - return -1; - } - while (pad--) b->p[b->off++] = 0; - return 0; -} - -static void b_put_u32(struct buf *b, uint32_t v) -{ - if (b_reserve(b, 4, 4) < 0) return; - b->p[b->off++] = (uint8_t)(v & 0xff); - b->p[b->off++] = (uint8_t)((v >> 8) & 0xff); - b->p[b->off++] = (uint8_t)((v >> 16) & 0xff); - b->p[b->off++] = (uint8_t)((v >> 24) & 0xff); -} - -static void b_put_byte(struct buf *b, uint8_t v) -{ - if (b_reserve(b, 1, 1) < 0) return; - b->p[b->off++] = v; -} - -static void b_put_string(struct buf *b, const char *s) -{ - size_t len = strlen(s); - - if (b_reserve(b, 4, 4 + len + 1) < 0) return; - b_put_u32(b, (uint32_t)len); - memcpy(b->p + b->off, s, len); - b->off += len; - b->p[b->off++] = 0; -} - -static void b_put_signature(struct buf *b, const char *s) -{ - size_t len = strlen(s); - - if (b_reserve(b, 1, 1 + len + 1) < 0) return; - b->p[b->off++] = (uint8_t)len; - memcpy(b->p + b->off, s, len); - b->off += len; - b->p[b->off++] = 0; -} - -/* Send a method call with an optional argument. - * arg_sig == NULL or "" -> no body - * arg_sig == "s" -> arg_string used - * arg_sig == "u" -> arg_u32 used - */ -static int send_method_call_with_arg(int fd, - const char *path, - const char *interface, - const char *member, - const char *arg_sig, - const char *arg_string, - uint32_t arg_u32); - -static int send_method_call(int fd, - const char *path, - const char *interface, - const char *member) -{ - return send_method_call_with_arg(fd, path, interface, member, - NULL, NULL, 0); -} - -static int send_method_call_with_arg(int fd, - const char *path, - const char *interface, - const char *member, - const char *arg_sig, - const char *arg_string, - uint32_t arg_u32) -{ - uint8_t hdr[2048]; - uint8_t body[1024]; - struct buf b = { .p = hdr, .cap = sizeof(hdr) }; - struct buf bb = { .p = body, .cap = sizeof(body) }; - size_t fields_start, fields_end, padded_end; - uint32_t body_len = 0; - - /* Build body first so its length and the signature are known - * before we write the header. */ - if (arg_sig && *arg_sig) { - if (strcmp(arg_sig, "s") == 0) { - b_put_string(&bb, arg_string ? arg_string : ""); - } else if (strcmp(arg_sig, "u") == 0) { - b_put_u32(&bb, arg_u32); - } else { - return -1; - } - if (bb.err) return -1; - body_len = (uint32_t)bb.off; - } - - /* Fixed header */ - memset(hdr, 0, 16); - hdr[0] = 'l'; - hdr[1] = 1; /* METHOD_CALL */ - hdr[2] = 0; /* flags */ - hdr[3] = 1; /* protocol */ - hdr[4] = (uint8_t)( body_len & 0xff); - hdr[5] = (uint8_t)((body_len >> 8) & 0xff); - hdr[6] = (uint8_t)((body_len >> 16) & 0xff); - hdr[7] = (uint8_t)((body_len >> 24) & 0xff); - hdr[8] = 1; /* serial */ - b.off = 16; - fields_start = b.off; - - /* PATH */ - b_reserve(&b, 8, 0); - b_put_byte(&b, 1); - b_put_signature(&b, "o"); - b_put_string(&b, path); - - if (interface) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 2); - b_put_signature(&b, "s"); - b_put_string(&b, interface); - } - - b_reserve(&b, 8, 0); - b_put_byte(&b, 3); - b_put_signature(&b, "s"); - b_put_string(&b, member); - - if (arg_sig && *arg_sig) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 8); - b_put_signature(&b, "g"); - /* SIGNATURE wire form: 1-byte len, bytes, nul */ - b_put_byte(&b, (uint8_t)strlen(arg_sig)); - if (b.off + strlen(arg_sig) + 1 > b.cap) return -1; - memcpy(b.p + b.off, arg_sig, strlen(arg_sig)); - b.off += strlen(arg_sig); - b.p[b.off++] = 0; - } - - fields_end = b.off; - { - uint32_t flen = (uint32_t)(fields_end - fields_start); - hdr[12] = (uint8_t)( flen & 0xff); - hdr[13] = (uint8_t)((flen >> 8) & 0xff); - hdr[14] = (uint8_t)((flen >> 16) & 0xff); - hdr[15] = (uint8_t)((flen >> 24) & 0xff); - } - - padded_end = ALIGN_UP(fields_end, 8); - while (b.off < padded_end) hdr[b.off++] = 0; - - if (b.err) return -1; - - if (write_all(fd, hdr, b.off) < 0) return -1; - if (body_len > 0 && write_all(fd, body, body_len) < 0) return -1; - return 0; -} - -/* Read one D-Bus message header + body into msg/body buffers. - * Returns 0 on success. Caller-supplied buffers must be large - * enough; we set them generously. */ -struct reply { - uint8_t type; - uint32_t serial; - uint32_t body_len; - char signature[64]; - char error_name[128]; - char interface[128]; - char member[128]; - uint8_t body[8192]; -}; - -/* Read one D-Bus message into *r. - * timeout_ms == 0 -> block forever waiting for the header byte - * timeout_ms > 0 -> wait that long for the header to start; once - * bytes arrive, the remainder of the frame is - * read without a timeout (it's "in flight"). - * Returns 0 on success, -1 on EOF / parse error / timeout. */ -static int read_reply(int fd, struct reply *r, int timeout_ms) -{ - uint8_t hdr_fixed[16]; - uint8_t hdr_fields[2048]; - uint32_t fields_len; - size_t body_off; - size_t pos; - size_t off = 0; - - memset(r, 0, sizeof(*r)); - - if (timeout_ms > 0) { - int n = read_with_timeout(fd, hdr_fixed, 1, timeout_ms); - if (n <= 0) return -1; - off = 1; - } - if (off < 16 && read_full(fd, hdr_fixed + off, 16 - off) < 0) - return -1; - - if (hdr_fixed[0] != 'l') return -1; - r->type = hdr_fixed[1]; - r->body_len = (uint32_t)hdr_fixed[4] - | ((uint32_t)hdr_fixed[5] << 8) - | ((uint32_t)hdr_fixed[6] << 16) - | ((uint32_t)hdr_fixed[7] << 24); - r->serial = (uint32_t)hdr_fixed[8] - | ((uint32_t)hdr_fixed[9] << 8) - | ((uint32_t)hdr_fixed[10] << 16) - | ((uint32_t)hdr_fixed[11] << 24); - fields_len = (uint32_t)hdr_fixed[12] - | ((uint32_t)hdr_fixed[13] << 8) - | ((uint32_t)hdr_fixed[14] << 16) - | ((uint32_t)hdr_fixed[15] << 24); - if (fields_len > sizeof(hdr_fields)) return -1; - if (read_full(fd, hdr_fields, fields_len) < 0) return -1; - - body_off = (size_t)ALIGN_UP(16 + fields_len, 8); - if (body_off > 16 + fields_len) { - uint8_t pad[8]; - if (read_full(fd, pad, body_off - 16 - fields_len) < 0) - return -1; - } - - pos = 0; - while (pos < fields_len) { - uint8_t code; - size_t vsig_len; - const char *vsig; - - pos = ALIGN_UP(pos, 8); - if (pos >= fields_len) break; - code = hdr_fields[pos++]; - vsig_len = hdr_fields[pos++]; - if (pos + vsig_len + 1 > fields_len) return -1; - vsig = (const char *)(hdr_fields + pos); - pos += vsig_len + 1; - - if (vsig[0] == 's' || vsig[0] == 'o') { - uint32_t slen; - char *dst = NULL; - size_t dst_sz = 0; - - pos = ALIGN_UP(pos, 4); - if (pos + 4 > fields_len) return -1; - slen = (uint32_t)hdr_fields[pos] - | ((uint32_t)hdr_fields[pos + 1] << 8) - | ((uint32_t)hdr_fields[pos + 2] << 16) - | ((uint32_t)hdr_fields[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > fields_len) return -1; - - switch (code) { - case 2: dst = r->interface; dst_sz = sizeof(r->interface); break; - case 3: dst = r->member; dst_sz = sizeof(r->member); break; - case 4: dst = r->error_name; dst_sz = sizeof(r->error_name); break; - default: break; - } - if (dst && slen < dst_sz) { - memcpy(dst, hdr_fields + pos, slen); - dst[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'g') { - uint32_t slen = hdr_fields[pos++]; - if (pos + slen + 1 > fields_len) return -1; - if (code == 8 && slen < sizeof(r->signature)) { - memcpy(r->signature, hdr_fields + pos, slen); - r->signature[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'u') { - pos = ALIGN_UP(pos, 4); - pos += 4; - } else { - return -1; - } - } - - if (r->body_len > sizeof(r->body)) return -1; - if (r->body_len > 0 && read_full(fd, r->body, r->body_len) < 0) - return -1; - return 0; -} - -/* Decode a body containing exactly one "s" or "o" -- the wire form - * is identical for both (u32 length + bytes + nul). */ -static int decode_string(struct reply *r, char *out, size_t outsz) -{ - uint32_t len; - - if (r->body_len < 5) - return -1; - if (strcmp(r->signature, "s") != 0 && strcmp(r->signature, "o") != 0) - return -1; - len = (uint32_t)r->body[0] - | ((uint32_t)r->body[1] << 8) - | ((uint32_t)r->body[2] << 16) - | ((uint32_t)r->body[3] << 24); - if (4 + len + 1 > r->body_len) return -1; - if (len + 1 > outsz) return -1; - memcpy(out, r->body + 4, len); - out[len] = '\0'; - return 0; -} - -/* Decode a body with signature "as", print one string per line. */ -static int decode_array_of_strings(struct reply *r) -{ - uint32_t array_len; - size_t pos; - - if (strcmp(r->signature, "as") != 0 || r->body_len < 4) - return -1; - array_len = (uint32_t)r->body[0] - | ((uint32_t)r->body[1] << 8) - | ((uint32_t)r->body[2] << 16) - | ((uint32_t)r->body[3] << 24); - pos = ALIGN_UP(4, 4); - if (pos + array_len > r->body_len) return -1; - - while (pos < 4 + array_len) { - uint32_t slen; - pos = ALIGN_UP(pos, 4); - if (pos + 4 > r->body_len) return -1; - slen = (uint32_t)r->body[pos] - | ((uint32_t)r->body[pos + 1] << 8) - | ((uint32_t)r->body[pos + 2] << 16) - | ((uint32_t)r->body[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > r->body_len) return -1; - printf("%.*s\n", (int)slen, r->body + pos); - pos += slen + 1; - } - return 0; -} - -/* ---------- modes ---------- */ - static int mode_auth(int argc, char *argv[]) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; @@ -557,11 +124,11 @@ static int mode_auth(int argc, char *argv[]) if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { perror("connect"); close(fd); return 2; } - if (write_all(fd, "\0", 1) < 0) { close(fd); return 2; } + if (write_all_fd(fd, "\0", 1) < 0) { close(fd); return 2; } rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); if (rc < 0 || (size_t)rc >= sizeof(line)) { close(fd); return 2; } - if (write_all(fd, line, (size_t)rc) < 0) { close(fd); return 2; } - if (read_line(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; } + if (write_all_fd(fd, line, (size_t)rc) < 0) { close(fd); return 2; } + if (read_line_fd(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; } printf("%s\n", reply); close(fd); if (strncmp(reply, "OK ", 3) == 0) return 0; @@ -569,170 +136,236 @@ static int mode_auth(int argc, char *argv[]) return 2; } -static int do_call_arg(const char *path, const char *obj_path, - const char *iface, const char *method, - const char *arg_sig, const char *arg_string, - uint32_t arg_u32, struct reply *r) +/* ---------- libink-driven modes ---------- */ + +/* Convert link_client_call rc to the test client's 0/1/2 convention, + * printing the error name on stderr for ERROR replies. */ +static int report_rc(link_client_t *c, int rc) +{ + if (rc == LINK_CALL_OK) + return 0; + if (rc == LINK_CALL_ERROR) { + const link_reply_t *r = link_client_reply(c); + fprintf(stderr, "ERROR: %s\n", + (r && r->error_name) ? r->error_name : ""); + return 1; + } + return 2; +} + +/* Drop effective uid to argv-supplied value (decimal). */ +static int drop_uid(const char *uid_arg, const char *progname) { - int fd = connect_and_auth(path, getuid()); + char *ep = NULL; + long v; - if (fd < 0) return 2; - if (send_method_call_with_arg(fd, obj_path, iface, method, - arg_sig, arg_string, arg_u32) < 0) { - fprintf(stderr, "send: %s\n", strerror(errno)); - close(fd); + errno = 0; + v = strtol(uid_arg, &ep, 10); + if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) { + fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg); return 2; } - if (read_reply(fd, r, 0) < 0) { - fprintf(stderr, "read_reply\n"); - close(fd); + if (setuid((uid_t)v) < 0) { + perror("setuid"); return 2; } - close(fd); - if (r->type == 3) { - fprintf(stderr, "ERROR: %s\n", r->error_name); - return 1; - } return 0; } -static int do_call(const char *path, const char *obj_path, - const char *iface, const char *method, - struct reply *r) -{ - return do_call_arg(path, obj_path, iface, method, NULL, NULL, 0, r); -} - static int mode_hello(int argc, char *argv[]) { - struct reply r; - char name[256]; + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *name; int rc; if (argc != 3) return 2; - rc = do_call(argv[2], "/org/freedesktop/DBus", - "org.freedesktop.DBus", "Hello", &r); - if (rc != 0) return rc; - if (decode_string(&r, name, sizeof(name)) < 0) return 2; - printf("%s\n", name); - return 0; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + rc = report_rc(c, rc); + if (rc == 0) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_string(&reader, &name) == 0) + printf("%s\n", name); + else + rc = 2; + } + link_client_close(c); + return rc; } static int mode_introspect(int argc, char *argv[]) { - struct reply r; - char xml[8192]; + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *xml; int rc; if (argc != 4) return 2; - rc = do_call(argv[2], argv[3], - "org.freedesktop.DBus.Introspectable", "Introspect", &r); - if (rc != 0) return rc; - if (decode_string(&r, xml, sizeof(xml)) < 0) return 2; - printf("%s\n", xml); + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], + "org.freedesktop.DBus.Introspectable", + "Introspect", NULL); + rc = report_rc(c, rc); + if (rc == 0) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_string(&reader, &xml) == 0) + printf("%s\n", xml); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +/* Decode body with signature "as" -- u32 array byte-len, then "s" strings. + * libink's public reader doesn't yet have an array helper, so we walk + * the wire form with link_r_u32 + link_r_string + link_r_pos. */ +static int print_string_array(const link_reply_t *r) +{ + link_reader_t reader; + uint32_t array_len; + size_t end; + + if (!r || !r->signature || strcmp(r->signature, "as") != 0) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_u32(&reader, &array_len) < 0) + return -1; + end = link_r_pos(&reader) + array_len; + if (end > r->body_len) + return -1; + + while (link_r_pos(&reader) < end) { + const char *s; + + if (link_r_string(&reader, &s) < 0) + return -1; + printf("%s\n", s); + } return 0; } static int mode_liststrings(int argc, char *argv[]) { - struct reply r; + link_client_t *c; int rc; if (argc != 6) return 2; - rc = do_call(argv[2], argv[3], argv[4], argv[5], &r); - if (rc != 0) return rc; - if (decode_array_of_strings(&r) < 0) return 2; - return 0; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], argv[4], argv[5], NULL); + rc = report_rc(c, rc); + if (rc == 0 && print_string_array(link_client_reply(c)) < 0) + rc = 2; + link_client_close(c); + return rc; } -/* call-s: method taking one string arg, void/error reply. - * call-void: method taking no args, void/error reply. */ -static int mode_call_s(int argc, char *argv[]) +/* Shared call helpers used by both the plain and the -as-uid modes. + * `arg` may be NULL (void method); when non-NULL the call signature + * is "s" with `arg` as the single string argument. */ +static int do_call(const char *sock, const char *obj, const char *iface, + const char *method, const char *arg) { - struct reply r; + link_client_t *c; int rc; - if (argc != 7) return 2; - rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], - "s", argv[6], 0, &r); + c = link_client_open(sock); + if (!c) return 2; + + rc = arg + ? link_client_call_v(c, obj, iface, method, "s", arg) + : link_client_call_v(c, obj, iface, method, NULL); + rc = report_rc(c, rc); if (rc == 0) printf("OK\n"); + link_client_close(c); return rc; } -static int mode_call_void(int argc, char *argv[]) +static int mode_call_s(int argc, char *argv[]) { - struct reply r; - int rc; + if (argc != 7) return 2; + return do_call(argv[2], argv[3], argv[4], argv[5], argv[6]); +} +static int mode_call_void(int argc, char *argv[]) +{ if (argc != 6) return 2; - rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], - NULL, NULL, 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; + return do_call(argv[2], argv[3], argv[4], argv[5], NULL); } -/* get-service - * - * Calls Manager1.GetService(identity) and prints the returned - * object path. Exit 0 on success, 1 on server error, 2 transport. */ -static int mode_get_service(int argc, char *argv[]) +static int mode_call_s_as_uid(int argc, char *argv[]) { - struct reply r; - char path[256]; - int rc; + int rc; - if (argc != 4) return 2; - rc = do_call_arg(argv[2], "/org/finit/manager", - "org.finit.Manager1", "GetService", - "s", argv[3], 0, &r); - if (rc != 0) return rc; - /* decode_string accepts both "s" and "o" — wire form is - * identical; no need to pre-check the signature here. */ - if (decode_string(&r, path, sizeof(path)) < 0) - return 2; - printf("%s\n", path); - return 0; + if (argc != 8) return 2; + if ((rc = drop_uid(argv[2], argv[0])) != 0) + return rc; + return do_call(argv[3], argv[4], argv[5], argv[6], argv[7]); } -/* Drop effective uid to argv[2], parsed as decimal. Returns 0 on - * success, 2 (the program's "transport error" code) on failure. */ -static int drop_uid_from_arg(const char *uid_arg, const char *progname) +static int mode_call_void_as_uid(int argc, char *argv[]) { - uid_t drop_to; - char *ep = NULL; - long v; + int rc; - errno = 0; - v = strtol(uid_arg, &ep, 10); - if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) { - fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg); - return 2; - } - drop_to = (uid_t)v; + if (argc != 7) return 2; + if ((rc = drop_uid(argv[2], argv[0])) != 0) + return rc; + return do_call(argv[3], argv[4], argv[5], argv[6], NULL); +} - if (setuid(drop_to) < 0) { - perror("setuid"); - return 2; +static int mode_get_service(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *path; + int rc; + + if (argc != 4) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "GetService", + "s", argv[3]); + rc = report_rc(c, rc); + if (rc == 0) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + /* Reply signature is "o" but link_r_path / link_r_string + * have the same wire form. */ + if (link_r_path(&reader, &path) == 0) + printf("%s\n", path); + else + rc = 2; } - return 0; + link_client_close(c); + return rc; } -/* monitor-signal - * - * Subscribes via org.freedesktop.DBus.AddMatch, then reads - * incoming messages until either a SIGNAL is received or the - * timeout elapses. On a signal: prints "SIGNAL " - * followed by any "s" args, one per line. Exit 0 on signal, 1 on - * timeout, 2 on transport error. */ static int mode_monitor_signal(int argc, char *argv[]) { - int fd; - int timeout_ms; - struct reply r; + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; char *ep = NULL; long v; + int timeout_ms; + int rc; if (argc != 5) return 2; @@ -744,118 +377,91 @@ static int mode_monitor_signal(int argc, char *argv[]) } timeout_ms = (int)v; - fd = connect_and_auth(argv[2], getuid()); - if (fd < 0) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; - /* AddMatch on org.freedesktop.DBus */ - if (send_method_call_with_arg(fd, "/org/freedesktop/DBus", - "org.freedesktop.DBus", "AddMatch", - "s", argv[3], 0) < 0) { - close(fd); return 2; - } - if (read_reply(fd, &r, 0) < 0) { close(fd); return 2; } - if (r.type == 3) { - fprintf(stderr, "AddMatch ERROR: %s\n", r.error_name); - close(fd); return 2; + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", argv[3]); + if (rc != LINK_CALL_OK) { + rc = report_rc(c, rc); + link_client_close(c); + return rc; } - /* Now read messages until a signal or timeout. */ for (;;) { - if (read_reply(fd, &r, timeout_ms) < 0) { - close(fd); - return 1; /* timeout / transport */ + rc = link_client_wait(c, timeout_ms); + if (rc != 0) { + link_client_close(c); + return 1; /* timeout or transport */ } - if (r.type != 4) /* not a SIGNAL */ + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) continue; - printf("SIGNAL %s %s\n", r.interface, r.member); - /* Decode body as a sequence of strings; print one per line. */ - { - size_t pos = 0; - while (pos + 4 <= r.body_len) { - uint32_t slen; - pos = ALIGN_UP(pos, 4); - if (pos + 4 > r.body_len) break; - slen = (uint32_t)r.body[pos] - | ((uint32_t)r.body[pos + 1] << 8) - | ((uint32_t)r.body[pos + 2] << 16) - | ((uint32_t)r.body[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > r.body_len) break; - printf("%.*s\n", (int)slen, r.body + pos); - pos += slen + 1; + + printf("SIGNAL %s %s\n", + r->interface ? r->interface : "", + r->member ? r->member : ""); + /* Print any leading "s" args (other types are silently + * skipped -- callers test for the strings only). */ + link_reader_init(&reader, r->body, r->body_len); + if (r->signature) { + const char *s; + const char *p; + + for (p = r->signature; *p == 's'; p++) { + if (link_r_string(&reader, &s) < 0) + break; + printf("%s\n", s); } } - close(fd); + link_client_close(c); return 0; } } -/* call-s-as-uid - * - * Drops effective uid to (must work inside the test - * namespace where additional uids are mapped) before connecting, - * so AUTH EXTERNAL captures as the peer's real identity. - * Used to verify per-method authorization gating. */ -static int mode_call_s_as_uid(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 8) return 2; - if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) - return rc; - rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], - "s", argv[7], 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - -/* call-void-as-uid */ -static int mode_call_void_as_uid(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 7) return 2; - if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) - return rc; - rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], - NULL, NULL, 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - static int mode_unknown(int argc, char *argv[]) { - struct reply r; + link_client_t *c; + const link_reply_t *r; int rc; if (argc != 3) return 2; - rc = do_call(argv[2], "/org/finit/manager", - "org.finit.Manager1", "NotARealMethod", &r); - if (rc == 1 && strstr(r.error_name, "org.freedesktop.DBus.Error.") == r.error_name) - return 0; - if (rc == 1) - return 1; - return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "NotARealMethod", NULL); + if (rc != LINK_CALL_ERROR) { + link_client_close(c); + return rc == LINK_CALL_OK ? 1 : 2; + } + + r = link_client_reply(c); + { + static const char prefix[] = "org.freedesktop.DBus.Error."; + rc = (r && r->error_name && + strncmp(r->error_name, prefix, sizeof(prefix) - 1) == 0) + ? 0 : 1; + } + link_client_close(c); + return rc; } int main(int argc, char *argv[]) { if (argc < 2) return 2; - if (strcmp(argv[1], "auth") == 0) return mode_auth(argc, argv); - if (strcmp(argv[1], "hello") == 0) return mode_hello(argc, argv); - if (strcmp(argv[1], "introspect") == 0) return mode_introspect(argc, argv); - if (strcmp(argv[1], "liststrings") == 0) return mode_liststrings(argc, argv); - if (strcmp(argv[1], "call-s") == 0) return mode_call_s(argc, argv); - if (strcmp(argv[1], "call-void") == 0) return mode_call_void(argc, argv); - if (strcmp(argv[1], "monitor-signal") == 0) return mode_monitor_signal(argc, argv); - if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); - if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv); - if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv); - if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv); + if (!strcmp(argv[1], "auth")) return mode_auth (argc, argv); + if (!strcmp(argv[1], "hello")) return mode_hello (argc, argv); + if (!strcmp(argv[1], "introspect")) return mode_introspect (argc, argv); + if (!strcmp(argv[1], "liststrings")) return mode_liststrings (argc, argv); + if (!strcmp(argv[1], "call-s")) return mode_call_s (argc, argv); + if (!strcmp(argv[1], "call-void")) return mode_call_void (argc, argv); + if (!strcmp(argv[1], "call-s-as-uid")) return mode_call_s_as_uid (argc, argv); + if (!strcmp(argv[1], "call-void-as-uid")) return mode_call_void_as_uid(argc, argv); + if (!strcmp(argv[1], "get-service")) return mode_get_service (argc, argv); + if (!strcmp(argv[1], "monitor-signal")) return mode_monitor_signal (argc, argv); + if (!strcmp(argv[1], "unknown")) return mode_unknown (argc, argv); fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); return 2; } From caad1958e2b9aec55ac1051527fa9f389878e910 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 20:00:54 +0200 Subject: [PATCH 15/22] test: split dbus-auth.sh by area The original test started as an AUTH-handshake smoke test and accreted coverage for everything we shipped -- Manager1, Service1, Cond1, signals, AddMatch, initctl routing -- to the point the name was misleading and the file ran 315 LOC. Replaced with six focused tests, each runnable in isolation: dbus-auth.sh AUTH EXTERNAL + GUID uniqueness + socket mode dbus-bus.sh org.freedesktop.DBus built-ins dbus-manager.sh Manager1 vtable + per-method authorization dbus-service.sh Service1 vtable + ServiceStateChanged dbus-cond.sh Cond1 vtable + ConditionChanged + usr/ policy dbus-initctl.sh initctl restart/reload routed via D-Bus Common preamble (CLIENT/BUS vars, skip-if-not-built, retry until the bus socket appears) lives in test/lib/dbus-setup.sh. /tmp/* filenames are unique per test so the parallel runner can't clobber across them. Signed-off-by: Joachim Wiberg --- test/Makefile.am | 10 ++ test/dbus-auth.sh | 286 +---------------------------------------- test/dbus-bus.sh | 62 +++++++++ test/dbus-cond.sh | 69 ++++++++++ test/dbus-initctl.sh | 42 ++++++ test/dbus-manager.sh | 69 ++++++++++ test/dbus-service.sh | 71 ++++++++++ test/lib/Makefile.am | 2 +- test/lib/dbus-setup.sh | 20 +++ 9 files changed, 351 insertions(+), 280 deletions(-) create mode 100755 test/dbus-bus.sh create mode 100755 test/dbus-cond.sh create mode 100755 test/dbus-initctl.sh create mode 100755 test/dbus-manager.sh create mode 100755 test/dbus-service.sh create mode 100644 test/lib/dbus-setup.sh diff --git a/test/Makefile.am b/test/Makefile.am index 420f40d1..be9b0947 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -63,6 +63,11 @@ EXTRA_DIST += signal-service.sh EXTRA_DIST += testserv.sh EXTRA_DIST += unexpected-restart.sh EXTRA_DIST += dbus-auth.sh +EXTRA_DIST += dbus-bus.sh +EXTRA_DIST += dbus-manager.sh +EXTRA_DIST += dbus-service.sh +EXTRA_DIST += dbus-cond.sh +EXTRA_DIST += dbus-initctl.sh AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -111,6 +116,11 @@ endif TESTS += unexpected-restart.sh if DBUS TESTS += dbus-auth.sh +TESTS += dbus-bus.sh +TESTS += dbus-manager.sh +TESTS += dbus-service.sh +TESTS += dbus-cond.sh +TESTS += dbus-initctl.sh endif check-recursive: setup-chroot diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 27bf6bea..1e9535d3 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -1,10 +1,9 @@ #!/bin/sh -# End-to-end smoke test for libink: -# - AUTH EXTERNAL handshake (happy and wrong-uid paths) -# - org.freedesktop.DBus.Hello -# - org.freedesktop.DBus.Introspectable.Introspect (root, manager) -# - org.finit.Manager1.ListServices -# - Error reply for an unknown method. +# libink: D-Bus AUTH EXTERNAL handshake. +# +# Verifies the SASL handshake itself in isolation -- everything else +# the bus does (built-in DBus interface, vtables, signals, initctl +# routing) lives in the other dbus-*.sh tests. set -eu @@ -12,23 +11,13 @@ TEST_DIR=$(dirname "$0") # shellcheck source=/dev/null . "$TEST_DIR/lib/setup.sh" - -CLIENT=/sbin/dbus-auth-client -BUS=/run/finit/bus - -if ! texec test -x "$CLIENT"; then - skip "dbus-auth-client not built (configured with --disable-dbus?)" -fi - -say "Wait for $BUS to appear" -retry "texec test -S $BUS" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" say "Socket mode is 0666" mode=$(texec stat -c %a "$BUS") assert "Socket mode is 666 (got $mode)" "$mode" = "666" -# ---------- AUTH ---------- - say "AUTH EXTERNAL: claim correct UID (root = 0)" reply=$(texec "$CLIENT" auth "$BUS" 0) assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK" @@ -52,264 +41,3 @@ r2=$(texec "$CLIENT" auth "$BUS" 0) g1=${r1#OK } g2=${r2#OK } assert "Per-connection GUIDs differ ($g1 vs $g2)" "$g1" != "$g2" - -# ---------- Built-in interfaces ---------- - -say "Hello() returns a unique name beginning with ':1.'" -name=$(texec "$CLIENT" hello "$BUS") -case "$name" in - :1.*) assert "Hello returned a :1.N name (got $name)" 0 -eq 0 ;; - *) fail "Hello returned unexpected name: $name" ;; -esac - -say "Two Hello() calls produce different unique names" -n1=$(texec "$CLIENT" hello "$BUS") -n2=$(texec "$CLIENT" hello "$BUS") -assert "Unique names increment ($n1 vs $n2)" "$n1" != "$n2" - -say "Introspect on root path returns valid XML referencing /manager" -xml=$(texec "$CLIENT" introspect "$BUS" /) -case "$xml" in - *' root (good)" 0 -eq 0 ;; - *) fail "Root introspect missing : $xml" ;; -esac - -say "Introspect on /org/finit/manager exposes Manager1.ListServices" -xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) -case "$xml" in - *'org.finit.Manager1'*'ListServices'*) - assert "Manager1 and ListServices visible in XML" 0 -eq 0 ;; - *) - fail "Manager1 XML missing; got: $xml" ;; -esac - -# ---------- Real method call ---------- - -say "Manager1.ListServices returns the running services" -list=$(texec "$CLIENT" liststrings "$BUS" /org/finit/manager \ - org.finit.Manager1 ListServices) -assert "ListServices returned at least one service" \ - "$(printf '%s' "$list" | wc -l | tr -d ' ')" -ge 1 -echo "$list" - -# ---------- Method with arguments ---------- - -say "Manager1.Reload (void) succeeds" -texec "$CLIENT" call-void "$BUS" /org/finit/manager \ - org.finit.Manager1 Reload >/dev/null \ - || fail "Reload returned non-zero" -assert "Reload void method ok" 0 -eq 0 - -say "Manager1.Stop with bogus identity returns NoSuchService error" -set +e -texec "$CLIENT" call-s "$BUS" /org/finit/manager \ - org.finit.Manager1 Stop "no-such-service-here" >/tmp/dbus-stop.out 2>&1 -stop_rc=$? -set -e -assert "Bogus service rejected (rc=$stop_rc)" "$stop_rc" -eq 1 -case "$(cat /tmp/dbus-stop.out)" in - *NoSuchService*) assert "Error is NoSuchService" 0 -eq 0 ;; - *) fail "Unexpected error reply: $(cat /tmp/dbus-stop.out)" ;; -esac - -# ---------- Authorization ---------- - -say "Manager1.Restart from non-root is rejected with AccessDenied" -set +e -texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \ - org.finit.Manager1 Restart "testserv" >/tmp/dbus-authz.out 2>&1 -authz_rc=$? -set -e -assert "Non-root Restart rejected (rc=$authz_rc)" "$authz_rc" -eq 1 -case "$(cat /tmp/dbus-authz.out)" in - *AccessDenied*) assert "Error is AccessDenied" 0 -eq 0 ;; - *) fail "Unexpected error: $(cat /tmp/dbus-authz.out)" ;; -esac - -say "Manager1.ListServices is reachable as non-root (not blocked by authz)" -# call-s-as-uid sends an "s" body; ListServices expects "", so the -# server must reply with org.freedesktop.DBus.Error.InvalidArgs. -# Asserting that *positive* marker (not just "no AccessDenied") -# ensures we don't silently pass if setuid() failed or the client -# never reached the server (e.g. a transport error would print -# neither AccessDenied nor InvalidArgs). -set +e -result=$(texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \ - org.finit.Manager1 ListServices "" 2>&1) -set -e -case "$result" in - *AccessDenied*) fail "Non-root ListServices rejected by authz: $result" ;; - *InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;; - *) fail "Unexpected reply from non-root ListServices: $result" ;; -esac - -# ---------- Per-service objects (Service1) ---------- - -say "Manager1.GetService(keventd) returns the encoded object path" -path=$(texec "$CLIENT" get-service "$BUS" keventd) -expected="/org/finit/service/keventd" -assert "GetService returned expected path (got: $path)" "$path" = "$expected" - -say "Introspect on the service object exposes Service1 methods" -xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/service/keventd) -case "$xml" in - *'org.finit.Service1'*'Restart'*) - assert "Service1.Restart visible in service-object XML" 0 -eq 0 ;; - *) - fail "Service1 not visible on /org/finit/service/keventd: $xml" ;; -esac - -say "Service1.Restart on /org/finit/service/keventd succeeds" -texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \ - org.finit.Service1 Restart >/dev/null \ - || fail "Service1.Restart returned non-zero" -assert "Per-service Restart ok" 0 -eq 0 - -say "Service1.Restart from non-root is rejected with AccessDenied" -set +e -texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/service/keventd \ - org.finit.Service1 Restart >/tmp/dbus-svcauthz.out 2>&1 -svc_authz_rc=$? -set -e -assert "Non-root Service1.Restart rejected (rc=$svc_authz_rc)" \ - "$svc_authz_rc" -eq 1 -case "$(cat /tmp/dbus-svcauthz.out)" in - *AccessDenied*) assert "Service1 authz fires" 0 -eq 0 ;; - *) fail "Expected AccessDenied, got: $(cat /tmp/dbus-svcauthz.out)" ;; -esac - -# ---------- Signals ---------- - -say "Service1.Restart fires Manager1.ServiceStateChanged" -rm -f /tmp/dbus-sig.out -( texec "$CLIENT" monitor-signal "$BUS" \ - "type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \ - 5000 > /tmp/dbus-sig.out 2>&1 ) & -mon_pid=$! -sleep 0.5 -texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \ - org.finit.Service1 Restart >/dev/null \ - || fail "Restart trigger returned non-zero" -set +e -wait "$mon_pid" -mon_rc=$? -set -e -assert "monitor saw a signal (rc=$mon_rc)" "$mon_rc" -eq 0 -case "$(cat /tmp/dbus-sig.out)" in - *"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*) - assert "Signal payload contains the keventd identity" 0 -eq 0 ;; - *) - fail "Unexpected signal output: $(cat /tmp/dbus-sig.out)" ;; -esac - -# ---------- Cond1 ---------- - -say "Cond1.Get returns 'off' for an unset condition" -result=$(texec "$CLIENT" call-s "$BUS" /org/finit/cond \ - org.finit.Cond1 Get "no-such-cond") -case "$result" in - OK*) : ;; # ok, the cond reports a state, fall through - *) fail "Cond1.Get failed: $result" ;; -esac - -say "Cond1.Set fires Cond1.ConditionChanged and Get reflects the change" -rm -f /tmp/dbus-cond.out -( texec "$CLIENT" monitor-signal "$BUS" \ - "type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \ - 5000 > /tmp/dbus-cond.out 2>&1 ) & -cond_mon_pid=$! -sleep 0.5 -texec "$CLIENT" call-s "$BUS" /org/finit/cond \ - org.finit.Cond1 Set "dbus-test-cond" >/dev/null \ - || fail "Cond1.Set returned non-zero" -set +e -wait "$cond_mon_pid" -cond_mon_rc=$? -set -e -assert "Cond1 monitor saw a signal (rc=$cond_mon_rc)" "$cond_mon_rc" -eq 0 -case "$(cat /tmp/dbus-cond.out)" in - *"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/dbus-test-cond"*on*) - assert "ConditionChanged carries usr/dbus-test-cond and 'on'" 0 -eq 0 ;; - *) - fail "Unexpected Cond1 signal: $(cat /tmp/dbus-cond.out)" ;; -esac - -say "Cond1.Set/Clear on non-usr/* is rejected" -set +e -texec "$CLIENT" call-s "$BUS" /org/finit/cond \ - org.finit.Cond1 Set "pid/sshd" >/tmp/dbus-condrej.out 2>&1 -condrej_rc=$? -set -e -assert "pid/* rejected (rc=$condrej_rc)" "$condrej_rc" -eq 1 -case "$(cat /tmp/dbus-condrej.out)" in - *InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;; - *) fail "Unexpected reply: $(cat /tmp/dbus-condrej.out)" ;; -esac - -say "Cond1.Set from non-root is rejected with AccessDenied" -set +e -texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/cond \ - org.finit.Cond1 Set "would-be-cond" >/tmp/dbus-condauthz.out 2>&1 -ca_rc=$? -set -e -assert "Non-root Cond1.Set rejected (rc=$ca_rc)" "$ca_rc" -eq 1 -case "$(cat /tmp/dbus-condauthz.out)" in - *AccessDenied*) assert "Cond1 authz fires" 0 -eq 0 ;; - *) fail "Unexpected reply: $(cat /tmp/dbus-condauthz.out)" ;; -esac - -say "AddMatch with a bogus key is rejected" -set +e -texec "$CLIENT" call-s "$BUS" /org/freedesktop/DBus \ - org.freedesktop.DBus AddMatch "bogus='whatever'" >/tmp/dbus-match.out 2>&1 -am_rc=$? -set -e -assert "Bad rule rejected (rc=$am_rc)" "$am_rc" -eq 1 -case "$(cat /tmp/dbus-match.out)" in - *MatchRuleInvalid*) assert "Error is MatchRuleInvalid" 0 -eq 0 ;; - *) fail "Unexpected reply: $(cat /tmp/dbus-match.out)" ;; -esac - -# ---------- initctl port ---------- - -# initctl now talks to /run/finit/bus when available. Verify by -# subscribing to ServiceStateChanged on a background monitor and -# then running initctl restart -- if D-Bus is in use, the signal -# fires. If the legacy socket were still in use, the dbus subscriber -# would see nothing. - -say "initctl restart drives D-Bus (signal observed via dbus-auth-client)" -rm -f /tmp/dbus-initctl-sig.out -( texec "$CLIENT" monitor-signal "$BUS" \ - "type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \ - 5000 > /tmp/dbus-initctl-sig.out 2>&1 ) & -ic_pid=$! -sleep 0.5 -texec initctl restart keventd >/dev/null \ - || fail "initctl restart returned non-zero" -set +e -wait "$ic_pid" -ic_rc=$? -set -e -assert "ServiceStateChanged fired from initctl restart (rc=$ic_rc)" \ - "$ic_rc" -eq 0 -case "$(cat /tmp/dbus-initctl-sig.out)" in - *"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*) - assert "initctl restart routed through D-Bus" 0 -eq 0 ;; - *) - fail "initctl restart didn't produce expected signal: $(cat /tmp/dbus-initctl-sig.out)" ;; -esac - -say "initctl reload (no args) routes through Manager1.Reload" -texec initctl reload >/dev/null \ - || fail "initctl reload returned non-zero" -assert "initctl reload ok" 0 -eq 0 - -# ---------- Error reply ---------- - -say "Unknown method gets an org.freedesktop.DBus.Error.* reply" -set +e -texec "$CLIENT" unknown "$BUS" -unknown_rc=$? -set -e -assert "Unknown method returned an error (rc=$unknown_rc)" "$unknown_rc" -eq 0 diff --git a/test/dbus-bus.sh b/test/dbus-bus.sh new file mode 100755 index 00000000..7bd53704 --- /dev/null +++ b/test/dbus-bus.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# libink: org.freedesktop.DBus built-in interface. +# +# Covers the stock D-Bus interface every conforming bus implements: +# Hello (peer name allocation), Introspect (XML), and AddMatch's +# error-path rule parser. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Hello() returns a unique name beginning with ':1.'" +name=$(texec "$CLIENT" hello "$BUS") +case "$name" in + :1.*) assert "Hello returned a :1.N name (got $name)" 0 -eq 0 ;; + *) fail "Hello returned unexpected name: $name" ;; +esac + +say "Two Hello() calls produce different unique names" +n1=$(texec "$CLIENT" hello "$BUS") +n2=$(texec "$CLIENT" hello "$BUS") +assert "Unique names increment ($n1 vs $n2)" "$n1" != "$n2" + +say "Introspect on root path returns valid XML" +xml=$(texec "$CLIENT" introspect "$BUS" /) +case "$xml" in + *' root (good)" 0 -eq 0 ;; + *) fail "Root introspect missing : $xml" ;; +esac + +say "Introspect on /org/finit/manager exposes Manager1.ListServices" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) +case "$xml" in + *'org.finit.Manager1'*'ListServices'*) + assert "Manager1 and ListServices visible in XML" 0 -eq 0 ;; + *) + fail "Manager1 XML missing; got: $xml" ;; +esac + +say "AddMatch with a bogus key is rejected" +set +e +texec "$CLIENT" call-s "$BUS" /org/freedesktop/DBus \ + org.freedesktop.DBus AddMatch "bogus='whatever'" >/tmp/dbus-match.out 2>&1 +am_rc=$? +set -e +assert "Bad rule rejected (rc=$am_rc)" "$am_rc" -eq 1 +case "$(cat /tmp/dbus-match.out)" in + *MatchRuleInvalid*) assert "Error is MatchRuleInvalid" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-match.out)" ;; +esac + +say "Unknown method on a Finit interface gets an org.freedesktop.DBus.Error.* reply" +set +e +texec "$CLIENT" unknown "$BUS" +unknown_rc=$? +set -e +assert "Unknown method returned an error (rc=$unknown_rc)" "$unknown_rc" -eq 0 diff --git a/test/dbus-cond.sh b/test/dbus-cond.sh new file mode 100755 index 00000000..f0e55da1 --- /dev/null +++ b/test/dbus-cond.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# libink: org.finit.Cond1 vtable + ConditionChanged signal. +# +# Covers user-condition manipulation: Get, Set (with signal fan-out), +# the usr/* policy guard (non-usr conditions are rejected), and the +# non-root authorization gate. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Cond1.Get returns 'off' for an unset condition" +result=$(texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Get "no-such-cond") +case "$result" in + OK*) : ;; # ok, the cond reports a state, fall through + *) fail "Cond1.Get failed: $result" ;; +esac + +say "Cond1.Set fires Cond1.ConditionChanged and Get reflects the change" +rm -f /tmp/dbus-cond.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-cond.out 2>&1 ) & +cond_mon_pid=$! +sleep 0.5 +texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "dbus-test-cond" >/dev/null \ + || fail "Cond1.Set returned non-zero" +set +e +wait "$cond_mon_pid" +cond_mon_rc=$? +set -e +assert "Cond1 monitor saw a signal (rc=$cond_mon_rc)" "$cond_mon_rc" -eq 0 +case "$(cat /tmp/dbus-cond.out)" in + *"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/dbus-test-cond"*on*) + assert "ConditionChanged carries usr/dbus-test-cond and 'on'" 0 -eq 0 ;; + *) + fail "Unexpected Cond1 signal: $(cat /tmp/dbus-cond.out)" ;; +esac + +say "Cond1.Set/Clear on non-usr/* is rejected" +set +e +texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "pid/sshd" >/tmp/dbus-condrej.out 2>&1 +condrej_rc=$? +set -e +assert "pid/* rejected (rc=$condrej_rc)" "$condrej_rc" -eq 1 +case "$(cat /tmp/dbus-condrej.out)" in + *InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-condrej.out)" ;; +esac + +say "Cond1.Set from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "would-be-cond" >/tmp/dbus-condauthz.out 2>&1 +ca_rc=$? +set -e +assert "Non-root Cond1.Set rejected (rc=$ca_rc)" "$ca_rc" -eq 1 +case "$(cat /tmp/dbus-condauthz.out)" in + *AccessDenied*) assert "Cond1 authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-condauthz.out)" ;; +esac diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh new file mode 100755 index 00000000..2d942d5c --- /dev/null +++ b/test/dbus-initctl.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# initctl: confirms the legacy CLI now routes through D-Bus. +# +# Subscribes to ServiceStateChanged on a background monitor and then +# runs initctl -- if D-Bus is in use, the signal fires. If the legacy +# socket were still in use, the dbus subscriber would see nothing. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "initctl restart drives D-Bus (signal observed via dbus-auth-client)" +rm -f /tmp/dbus-initctl-sig.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \ + 5000 > /tmp/dbus-initctl-sig.out 2>&1 ) & +ic_pid=$! +sleep 0.5 +texec initctl restart keventd >/dev/null \ + || fail "initctl restart returned non-zero" +set +e +wait "$ic_pid" +ic_rc=$? +set -e +assert "ServiceStateChanged fired from initctl restart (rc=$ic_rc)" \ + "$ic_rc" -eq 0 +case "$(cat /tmp/dbus-initctl-sig.out)" in + *"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*) + assert "initctl restart routed through D-Bus" 0 -eq 0 ;; + *) + fail "initctl restart didn't produce expected signal: $(cat /tmp/dbus-initctl-sig.out)" ;; +esac + +say "initctl reload (no args) routes through Manager1.Reload" +texec initctl reload >/dev/null \ + || fail "initctl reload returned non-zero" +assert "initctl reload ok" 0 -eq 0 diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh new file mode 100755 index 00000000..e26b4c11 --- /dev/null +++ b/test/dbus-manager.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# libink: org.finit.Manager1 vtable. +# +# Covers the Manager1 method surface: ListServices, Reload, Stop with +# bogus service, plus per-method authorization (Restart from non-root +# is rejected, ListServices remains reachable as non-root). + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Manager1.ListServices returns the running services" +list=$(texec "$CLIENT" liststrings "$BUS" /org/finit/manager \ + org.finit.Manager1 ListServices) +assert "ListServices returned at least one service" \ + "$(printf '%s' "$list" | wc -l | tr -d ' ')" -ge 1 +echo "$list" + +say "Manager1.Reload (void) succeeds" +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 Reload >/dev/null \ + || fail "Reload returned non-zero" +assert "Reload void method ok" 0 -eq 0 + +say "Manager1.Stop with bogus identity returns NoSuchService error" +set +e +texec "$CLIENT" call-s "$BUS" /org/finit/manager \ + org.finit.Manager1 Stop "no-such-service-here" >/tmp/dbus-stop.out 2>&1 +stop_rc=$? +set -e +assert "Bogus service rejected (rc=$stop_rc)" "$stop_rc" -eq 1 +case "$(cat /tmp/dbus-stop.out)" in + *NoSuchService*) assert "Error is NoSuchService" 0 -eq 0 ;; + *) fail "Unexpected error reply: $(cat /tmp/dbus-stop.out)" ;; +esac + +say "Manager1.Restart from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart "testserv" >/tmp/dbus-authz.out 2>&1 +authz_rc=$? +set -e +assert "Non-root Restart rejected (rc=$authz_rc)" "$authz_rc" -eq 1 +case "$(cat /tmp/dbus-authz.out)" in + *AccessDenied*) assert "Error is AccessDenied" 0 -eq 0 ;; + *) fail "Unexpected error: $(cat /tmp/dbus-authz.out)" ;; +esac + +# Send call-s-as-uid an "s" body where the server expects "" -- the +# server must reply with org.freedesktop.DBus.Error.InvalidArgs. +# Asserting that *positive* marker (not just "no AccessDenied") +# ensures we don't silently pass if setuid() failed or the client +# never reached the server (a transport error would print neither +# AccessDenied nor InvalidArgs). +say "Manager1.ListServices is reachable as non-root (not blocked by authz)" +set +e +result=$(texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 ListServices "" 2>&1) +set -e +case "$result" in + *AccessDenied*) fail "Non-root ListServices rejected by authz: $result" ;; + *InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;; + *) fail "Unexpected reply from non-root ListServices: $result" ;; +esac diff --git a/test/dbus-service.sh b/test/dbus-service.sh new file mode 100755 index 00000000..9099a637 --- /dev/null +++ b/test/dbus-service.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# libink: org.finit.Service1 vtable + ServiceStateChanged signal. +# +# Covers per-service objects exposed at /org/finit/service/: +# GetService lookup, Introspect on a service object, Service1.Restart, +# authorization (non-root rejected), and the Manager1.ServiceStateChanged +# signal that Service1.Restart triggers. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +say "Manager1.GetService(keventd) returns the encoded object path" +path=$(texec "$CLIENT" get-service "$BUS" keventd) +expected="/org/finit/service/keventd" +assert "GetService returned expected path (got: $path)" "$path" = "$expected" + +say "Introspect on the service object exposes Service1 methods" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/service/keventd) +case "$xml" in + *'org.finit.Service1'*'Restart'*) + assert "Service1.Restart visible in service-object XML" 0 -eq 0 ;; + *) + fail "Service1 not visible on /org/finit/service/keventd: $xml" ;; +esac + +say "Service1.Restart on /org/finit/service/keventd succeeds" +texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Restart >/dev/null \ + || fail "Service1.Restart returned non-zero" +assert "Per-service Restart ok" 0 -eq 0 + +say "Service1.Restart from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Restart >/tmp/dbus-svcauthz.out 2>&1 +svc_authz_rc=$? +set -e +assert "Non-root Service1.Restart rejected (rc=$svc_authz_rc)" \ + "$svc_authz_rc" -eq 1 +case "$(cat /tmp/dbus-svcauthz.out)" in + *AccessDenied*) assert "Service1 authz fires" 0 -eq 0 ;; + *) fail "Expected AccessDenied, got: $(cat /tmp/dbus-svcauthz.out)" ;; +esac + +say "Service1.Restart fires Manager1.ServiceStateChanged" +rm -f /tmp/dbus-sig.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \ + 5000 > /tmp/dbus-sig.out 2>&1 ) & +mon_pid=$! +sleep 0.5 +texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Restart >/dev/null \ + || fail "Restart trigger returned non-zero" +set +e +wait "$mon_pid" +mon_rc=$? +set -e +assert "monitor saw a signal (rc=$mon_rc)" "$mon_rc" -eq 0 +case "$(cat /tmp/dbus-sig.out)" in + *"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*) + assert "Signal payload contains the keventd identity" 0 -eq 0 ;; + *) + fail "Unexpected signal output: $(cat /tmp/dbus-sig.out)" ;; +esac diff --git a/test/lib/Makefile.am b/test/lib/Makefile.am index a281a920..ab85f6a4 100644 --- a/test/lib/Makefile.am +++ b/test/lib/Makefile.am @@ -1 +1 @@ -EXTRA_DIST = exec.sh setup.sh start.sh sysroot.mk +EXTRA_DIST = exec.sh setup.sh start.sh sysroot.mk dbus-setup.sh diff --git a/test/lib/dbus-setup.sh b/test/lib/dbus-setup.sh new file mode 100644 index 00000000..5b969da7 --- /dev/null +++ b/test/lib/dbus-setup.sh @@ -0,0 +1,20 @@ +# shellcheck shell=sh +# Shared preamble for the D-Bus smoke tests. Expects test/lib/setup.sh +# to have been sourced already (so texec, skip, retry, say, assert are +# available). Skips the test when the libink-driven client was not +# built, otherwise blocks until the bus socket appears. +# +# Exports: CLIENT, BUS. + +command -v texec >/dev/null \ + || { echo "dbus-setup.sh: source test/lib/setup.sh first" >&2; exit 99; } + +CLIENT=/sbin/dbus-auth-client +BUS=/run/finit/bus + +if ! texec test -x "$CLIENT"; then + skip "dbus-auth-client not built (configured with --disable-dbus?)" +fi + +say "Wait for $BUS to appear" +retry "texec test -S $BUS" From 27736dc8c1a9e20a8efba80ddadb142a841c6d22 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 20:16:34 +0200 Subject: [PATCH 16/22] initctl: monitor subcommand + cond {get,set,clr} via D-Bus Two new D-Bus routes: * initctl monitor streams every signal on the bus until ^C, one line per delivery: "HH:MM:SS iface.member(args)". First real consumer of link_client_wait; decodes leading 's' args only. Exits with a clear error when the bus connection drops. * initctl cond {get,set,clear} opens an org.finit.Cond1 client up front and routes each parsed condition arg through Cond1.{Get,Set,Clear} when reachable, falling back to the legacy filesystem symlink path otherwise. Set now fires the ConditionChanged signal as a side effect. cond_dbus_call owns the bus-handle lifecycle and the LINK_CALL_{OK,ERROR,FAIL} fan-out so do_cond_act stays flat. Test: dbus-initctl.sh asserts initctl cond set fires ConditionChanged -- only the D-Bus path emits it. Signed-off-by: Joachim Wiberg --- src/initctl.c | 167 ++++++++++++++++++++++++++++++++++++++++++- test/dbus-initctl.sh | 26 +++++++ 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/src/initctl.c b/src/initctl.c index 3abba899..936e1902 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -42,6 +42,9 @@ #include "cgutil.h" #include "utmp-api.h" +/* Used by both do_cond_act and (with HAVE_DBUS) cond_dbus_call. */ +typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; + struct cmd { char *cmd; struct cmd *ctx; @@ -327,6 +330,139 @@ static int try_dbus_manager(const char *method, const char *arg_sig, return 0; return -1; /* LINK_CALL_FAIL or anything else: fall back */ } + +/* Try one Cond1.{Get,Set,Clear} call. On COND_GET success the helper + * fills *out_exit with the exit code (0 = on, 1 = off, 255 = flux). + * Outcomes: + * 1 call succeeded; for GET the result is in *out_exit, for + * SET/CLR the caller loops to the next arg + * 0 bus not reachable, or LINK_CALL_FAIL -- *bus is closed/NULLed + * and the caller should drop to the legacy filesystem path + * (LINK_CALL_ERROR exits via ERRX inside the helper) + * + * `*bus` is borrowed; the helper closes it (and sets NULL) on every + * exit path that leaves the bus unusable. */ +static int cond_dbus_call(link_client_t **bus, condop_t op, + const char *arg, int *out_exit) +{ + const char *method = (op == COND_GET) ? "Get" + : (op == COND_SET) ? "Set" : "Clear"; + int rc; + + if (!*bus) + return 0; + + rc = link_client_call_v(*bus, "/org/finit/cond", + "org.finit.Cond1", method, + "s", arg); + if (rc == LINK_CALL_OK) { + if (op == COND_GET) { + const link_reply_t *r = link_client_reply(*bus); + link_reader_t reader; + const char *state = NULL; + + if (r && r->body) { + link_reader_init(&reader, r->body, r->body_len); + link_r_string(&reader, &state); + } + if (verbose && state) + puts(state); + *out_exit = (state && !strcmp(state, "on")) ? 0 + : (state && !strcmp(state, "off")) ? 1 : 255; + } + return 1; + } + if (rc == LINK_CALL_ERROR) { + const link_reply_t *r = link_client_reply(*bus); + const char *err = (r && r->error_name) ? r->error_name : ""; + + link_client_close(*bus); + *bus = NULL; + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: cond %s requires root", method); + ERRX(73, "Failed %s condition <%s>: %s", + op == COND_SET ? "asserting" : "deasserting", + arg, *err ? err : "D-Bus error"); + } + /* LINK_CALL_FAIL */ + link_client_close(*bus); + *bus = NULL; + return 0; +} + +/* Subscribe to every signal on the bus and print one line per + * incoming message: + * HH:MM:SS interface.member(arg1, arg2, ...) + * + * Only string-typed leading args are decoded (matches what our two + * current signals -- ServiceStateChanged (sss) and ConditionChanged + * (ss) -- emit). Non-string args are silently skipped. Loops until + * the connection drops or the user hits ^C. */ +static int do_monitor(char *arg) +{ + link_client_t *c; + int rc; + + (void)arg; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + ERRX(1, "monitor requires the D-Bus socket at %s", FINIT_BUS_SOCKET); + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", "type='signal'"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + ERRX(1, "AddMatch failed (rc=%d)", rc); + } + + for (;;) { + const link_reply_t *r; + link_reader_t reader; + char ts[16]; + time_t now; + struct tm tm; + + rc = link_client_wait(c, -1); + if (rc < 0) { + link_client_close(c); + ERRX(1, "bus connection lost"); + } + if (rc > 0) /* impossible with timeout=-1, but harmless */ + continue; + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) + continue; + + now = time(NULL); + localtime_r(&now, &tm); + strftime(ts, sizeof(ts), "%H:%M:%S", &tm); + printf("%s %s.%s(", ts, + r->interface ? r->interface : "?", + r->member ? r->member : "?"); + + link_reader_init(&reader, r->body, r->body_len); + if (r->signature) { + const char *p; + int first = 1; + + for (p = r->signature; *p == 's'; p++) { + const char *s; + + if (link_r_string(&reader, &s) < 0) + break; + printf("%s%s", first ? "" : ", ", s); + first = 0; + } + } + printf(")\n"); + fflush(stdout); + } + + link_client_close(c); + return 0; +} #endif /* HAVE_DBUS */ static int do_start (char *arg) @@ -494,8 +630,6 @@ static int do_cond_dump(char *arg) return 0; } -typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; - static cond_state_t cond_read(char *path) { int now, gen; @@ -525,6 +659,9 @@ static int do_cond_act(char *args, condop_t op) cond_state_t cstate; char path[256]; char *arg; +#ifdef HAVE_DBUS + link_client_t *bus = link_client_open(FINIT_BUS_SOCKET); +#endif if (!args || !args[0]) ERRX(2, "Invalid condition (empty)"); @@ -544,6 +681,22 @@ static int do_cond_act(char *args, condop_t op) ERRX(2, "Invalid condition (periods)"); } +#ifdef HAVE_DBUS + { + int exit_code; + + if (cond_dbus_call(&bus, op, arg, &exit_code)) { + if (op == COND_GET) { + link_client_close(bus); + return exit_code; + } + arg = strtok(NULL, " \t"); + continue; + } + /* bus is NULL now -- drop through to legacy */ + } +#endif + if (strchr(arg, '/')) snprintf(path, sizeof(path), _PATH_COND "%s", arg); else @@ -580,6 +733,10 @@ static int do_cond_act(char *args, condop_t op) arg = strtok(NULL, " \t"); } +#ifdef HAVE_DBUS + if (bus) + link_client_close(bus); +#endif return 0; } @@ -1694,6 +1851,9 @@ static int usage(int rc) " Note: Finit .conf file(s) are *not* reloaded!\n" " restart [:ID] Restart (stop/start) service by name\n" " kill [:ID] Send signal S to service by name, with optional ID\n" +#ifdef HAVE_DBUS + " monitor Stream D-Bus signals (service state, conditions) until ^C\n" +#endif " ident [NAME] Show matching identities for NAME, or all\n" " status [:ID] Show service status, by name\n" " status Show status of services, default command\n"); @@ -1878,6 +2038,9 @@ int main(int argc, char *argv[]) { "start", NULL, do_start, NULL, NULL }, { "stop", NULL, do_stop, NULL, NULL }, { "restart", NULL, do_restart, NULL, NULL }, +#ifdef HAVE_DBUS + { "monitor", NULL, do_monitor, NULL, NULL }, +#endif { "signal", NULL, NULL, NULL, do_signal }, { "kill", NULL, NULL, NULL, do_signal }, /* alias */ diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh index 2d942d5c..c51c1a3e 100755 --- a/test/dbus-initctl.sh +++ b/test/dbus-initctl.sh @@ -40,3 +40,29 @@ say "initctl reload (no args) routes through Manager1.Reload" texec initctl reload >/dev/null \ || fail "initctl reload returned non-zero" assert "initctl reload ok" 0 -eq 0 + +# A ConditionChanged signal can only originate from Cond1.Set going +# through finit (the legacy filesystem path doesn't emit signals). +# So if the monitor sees one, we know initctl cond set was routed +# via D-Bus. +say "initctl cond set drives Cond1.Set via D-Bus" +rm -f /tmp/dbus-initctl-cond.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-initctl-cond.out 2>&1 ) & +ic_cond_pid=$! +sleep 0.5 +texec initctl cond set "via-initctl" >/dev/null \ + || fail "initctl cond set returned non-zero" +set +e +wait "$ic_cond_pid" +ic_cond_rc=$? +set -e +assert "ConditionChanged fired from initctl cond set (rc=$ic_cond_rc)" \ + "$ic_cond_rc" -eq 0 +case "$(cat /tmp/dbus-initctl-cond.out)" in + *"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/via-initctl"*on*) + assert "initctl cond set routed through D-Bus" 0 -eq 0 ;; + *) + fail "initctl cond set didn't produce expected signal: $(cat /tmp/dbus-initctl-cond.out)" ;; +esac From a5d5101b3a837bd6f50b434720692c2f6a58e4b7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 20:29:16 +0200 Subject: [PATCH 17/22] libink/finit: org.freedesktop.DBus.Properties + Manager1 props The standard Properties interface lets dbus-send, gdbus and other D-Bus tooling introspect and read out Finit state in the canonical way. libink: * String variant marshalling (link_w/r_variant_string) plus link_property_t and a .properties slot on link_vtable_t. Read-only properties only for now; non-string variants deferred until a property needs them. * Properties.Get/GetAll handlers in builtin.c. Introspection XML emits entries and advertises the interface. * link_r_align (public) for walking a{sv} reply bodies. Finit: * Manager1 declares Runlevel, PrevRunlevel and Version as read-only string properties. * Manager1.RunlevelChanged(ss) -- (old, new) -- emitted from sm.c right after the runlevel global flips. * dbus_emit_signal() consolidates the three near-identical signal fan-out helpers onto one. * initctl runlevel reads via Properties.GetAll when reachable, falls back to runlevel_get otherwise. Signed-off-by: Joachim Wiberg --- libink/builtin.c | 154 +++++++++++++++++++++++++++++++++++++++++++ libink/dispatch.c | 3 + libink/link.h | 23 ++++++- libink/marshal.c | 41 ++++++++++++ libink/marshal.h | 3 + src/dbus.c | 144 ++++++++++++++++++++++++++++++---------- src/initctl.c | 90 ++++++++++++++++++++++++- src/private.h | 1 + src/sm.c | 3 + test/dbus-manager.sh | 28 ++++++++ 10 files changed, 450 insertions(+), 40 deletions(-) diff --git a/libink/builtin.c b/libink/builtin.c index 715157db..5a5a2ae5 100644 --- a/libink/builtin.c +++ b/libink/builtin.c @@ -117,6 +117,15 @@ static void emit_method(struct xbuf *x, const link_method_t *m) xprintf(x, " \n"); } +static void emit_property(struct xbuf *x, const link_property_t *p) +{ + /* Setters are not implemented, so every property advertises + * access="read" today. When Properties.Set lands, switch on + * a writable flag. */ + xprintf(x, " \n", + p->name, p->sig ? p->sig : "s"); +} + /* Introspection limitation: emit_method prints one per * character of the signature, which is wrong for compound types * (an "a(ss)" arg appears as four args). Good enough for the @@ -134,6 +143,17 @@ static const char STANDARD_INTERFACES_XML[] = " \n" " \n" " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" " \n"; /* Is `child` a path under `parent`? If so, write the first segment @@ -197,11 +217,16 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) const link_method_t *meth; TAILQ_FOREACH(e, &o->vtables, link) { + const link_property_t *prop; + xprintf(&x, " \n", e->vt->interface); if (e->vt->methods) for (meth = e->vt->methods; meth->name; meth++) emit_method(&x, meth); + if (e->vt->properties) + for (prop = e->vt->properties; prop->name; prop++) + emit_property(&x, prop); xprintf(&x, " \n"); } } @@ -231,6 +256,129 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m) return send_string_reply(conn, m, xml); } +/* ---------- Properties.Get / GetAll ---------- */ + +/* Find the (object, vtable-entry) pair matching `path` and `interface`. + * Returns NULL if the path is unknown or the interface isn't exposed + * on it. */ +static struct link_vtable_entry * +find_vtable(link_connection_t *conn, const char *path, const char *interface) +{ + struct link_object *o; + + if (!path || !interface) + return NULL; + TAILQ_FOREACH(o, &conn->server->objects, link) { + struct link_vtable_entry *e; + + if (strcmp(o->path, path) != 0) + continue; + TAILQ_FOREACH(e, &o->vtables, link) { + if (strcmp(e->vt->interface, interface) == 0) + return e; + } + } + return NULL; +} + +static int handle_properties_get(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface, *prop_name; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "ss") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.Get takes (interface, property)"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0 || __r_string(&r, &prop_name) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e || !e->vt->properties) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + for (p = e->vt->properties; p->name; p++) { + if (strcmp(p->name, prop_name) != 0) + continue; + if (!p->getter) + break; + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + return __send_method_return(conn, m, "v", + conn->txbuf, (size_t)blen); + } + + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownProperty", + "No such property on this interface"); +} + +static int handle_properties_get_all(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.GetAll takes one string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_array_begin(&w, '{'); + if (e->vt->properties) { + for (p = e->vt->properties; p->name; p++) { + if (!p->getter) + continue; + __w_struct_begin(&w); + __w_string(&w, p->name); + if (p->getter(&w, e->userdata) != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + __w_struct_end(&w); + } + } + __w_array_end(&w); + + blen = __w_finish(&w); + if (blen < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply too large"); + + return __send_method_return(conn, m, "a{sv}", + conn->txbuf, (size_t)blen); +} + /* ---------- AddMatch / RemoveMatch ---------- */ static int handle_add_match(link_connection_t *conn, const struct link_msg *m) @@ -310,5 +458,11 @@ int __handle_builtin(link_connection_t *conn, const struct link_msg *m) if (member_is(m, "org.freedesktop.DBus.Introspectable", "Introspect")) return handle_introspect(conn, m); + if (member_is(m, "org.freedesktop.DBus.Properties", "Get")) + return handle_properties_get(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Properties", "GetAll")) + return handle_properties_get_all(conn, m); + return -1; /* not a built-in */ } diff --git a/libink/dispatch.c b/libink/dispatch.c index 651e136a..9e68793b 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -298,6 +298,7 @@ void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); } void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); } void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); } void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); } +void link_w_variant_string(link_writer_t *w, const char *s) { __w_variant_string(w, s); } void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); } void link_w_array_end (link_writer_t *w) { __w_array_end(w); } void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); } @@ -311,6 +312,8 @@ int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); } int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); } int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); } +int link_r_variant_string(link_reader_t *r, const char **o) { return __r_variant_string(r, o); } +int link_r_align (link_reader_t *r, size_t n) { return __r_align (r, n); } int link_r_done (const link_reader_t *r) { return __r_done (r); } size_t link_r_pos (const link_reader_t *r) { return r->off; } diff --git a/libink/link.h b/libink/link.h index a6b74bda..5188a503 100644 --- a/libink/link.h +++ b/libink/link.h @@ -116,9 +116,22 @@ typedef struct { link_method_fn handler; } link_method_t; +/* A read-only property descriptor. Set via the Properties.Set side + * is not yet implemented; only Get and GetAll are. The getter writes + * the property's value as a D-Bus variant (use link_w_variant_string + * for "s"-typed properties) into the provided writer. */ +typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); + +typedef struct { + const char *name; /* property name */ + const char *sig; /* D-Bus signature, e.g. "s" */ + link_property_getter_fn getter; +} link_property_t; + typedef struct { - const char *interface; /* e.g. "org.finit.Manager1" */ - const link_method_t *methods; /* terminated by {NULL, ...} */ + const char *interface; /* e.g. "org.finit.Manager1" */ + const link_method_t *methods; /* terminated by {NULL, ...}, or NULL */ + const link_property_t *properties; /* terminated by {NULL, ...}, or NULL */ } link_vtable_t; /* Register one (interface, methods) at `path`. Calling repeatedly @@ -261,6 +274,7 @@ void link_w_bool (link_writer_t *w, int v); void link_w_u32 (link_writer_t *w, uint32_t v); void link_w_string (link_writer_t *w, const char *s); /* "s" */ void link_w_path (link_writer_t *w, const char *s); /* "o" */ +void link_w_variant_string(link_writer_t *w, const char *s); /* "v" containing "s" */ void link_w_array_begin (link_writer_t *w, char element_sig); void link_w_array_end (link_writer_t *w); void link_w_struct_begin(link_writer_t *w); @@ -277,12 +291,15 @@ int link_r_bool (link_reader_t *r, int *out); int link_r_u32 (link_reader_t *r, uint32_t *out); int link_r_string (link_reader_t *r, const char **out); /* "s" */ int link_r_path (link_reader_t *r, const char **out); /* "o" */ +int link_r_variant_string(link_reader_t *r, const char **out); /* "v" containing "s" */ +int link_r_align (link_reader_t *r, size_t n); /* skip to next n-byte boundary */ int link_r_done (const link_reader_t *r); /* Byte offset of the next read inside the original body buffer. Used * to detect end-of-array when walking "a" payloads: read the array * byte-length prefix with link_r_u32 first, record (pos+length) as the - * end, then loop while link_r_pos < end. */ + * end, then loop while link_r_pos < end. For "a{T}" (dict-entry + * arrays) call link_r_align(r, 8) at the top of each iteration. */ size_t link_r_pos (const link_reader_t *r); #ifdef __cplusplus diff --git a/libink/marshal.c b/libink/marshal.c index d22c7c8c..60406221 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -102,6 +102,14 @@ void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, void __w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } void __w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); } +/* Variant "v" containing a string. Wire form: + * 1-byte sig length (1), 's', NUL, then the string per __w_string. */ +void __w_variant_string(struct link_writer *w, const char *s) +{ + __w_sig (w, "s"); + __w_string(w, s); +} + static size_t element_align(char c) { switch (c) { @@ -254,7 +262,40 @@ static int read_string_like(struct link_reader *r, const char **out) int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } +/* Read a variant "v" expected to contain a string. Fails if the + * inner signature is anything other than "s" (returns -1, *out set + * to NULL). */ +int __r_variant_string(struct link_reader *r, const char **out) +{ + uint8_t sig_len; + uint32_t slen; + + *out = NULL; + + /* signature is "g" wire form: 1-byte length, bytes, NUL */ + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) { r->err = 1; return -1; } + sig_len = r->base[r->off++]; + if (sig_len != 1 || r->off + 2 > r->cap) { r->err = 1; return -1; } + if (r->base[r->off] != 's' || r->base[r->off + 1] != 0) { r->err = 1; return -1; } + r->off += 2; + + /* now a normal string */ + if (__r_u32(r, &slen) < 0) return -1; + if (r->off + (size_t)slen + 1 > r->cap || r->base[r->off + slen] != 0) { + r->err = 1; + return -1; + } + *out = (const char *)(r->base + r->off); + r->off += (size_t)slen + 1; + return 0; +} + int __r_done(const struct link_reader *r) { return !r->err && r->off == r->cap; } + +int __r_align(struct link_reader *r, size_t n) +{ + return r_skip_align(r, n); +} diff --git a/libink/marshal.h b/libink/marshal.h index b7717813..e40b4f41 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -23,6 +23,7 @@ void __w_u32 (struct link_writer *w, uint32_t v); void __w_string (struct link_writer *w, const char *s); /* "s" */ void __w_path (struct link_writer *w, const char *s); /* "o" */ void __w_sig (struct link_writer *w, const char *s); /* "g" */ +void __w_variant_string(struct link_writer *w, const char *s); /* "v" containing "s" */ /* element_sig_first_char drives the alignment padding inserted * between the array length prefix and the first element. */ @@ -43,6 +44,8 @@ int __r_bool (struct link_reader *r, int *out); int __r_u32 (struct link_reader *r, uint32_t *out); int __r_string(struct link_reader *r, const char **out); /* "s" */ int __r_path (struct link_reader *r, const char **out); /* "o" */ +int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ +int __r_align (struct link_reader *r, size_t n); /* skip to n-byte boundary */ int __r_done (const struct link_reader *r); #endif /* LIBINK_MARSHAL_H_ */ diff --git a/src/dbus.c b/src/dbus.c index 00064d3b..4b11118a 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -348,6 +348,48 @@ static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shut static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); } static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); } +/* ---------- Manager1 properties ---------- + * + * Read-only string properties exposed via the standard + * org.freedesktop.DBus.Properties interface. Getters write a + * variant containing a single string. */ + +/* Two distinct getters because the property table is static const -- + * we can't bind &runlevel/&prevlevel through userdata. */ +static int prop_runlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + snprintf(buf, sizeof(buf), "%d", runlevel); + link_w_variant_string(w, buf); + return 0; +} + +static int prop_prevrunlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + snprintf(buf, sizeof(buf), "%d", prevlevel); + link_w_variant_string(w, buf); + return 0; +} + +static int prop_version(link_writer_t *w, void *u) +{ + (void)u; + link_w_variant_string(w, PACKAGE_VERSION); + return 0; +} + +static const link_property_t manager_properties[] = { + { .name = "Runlevel", .sig = "s", .getter = prop_runlevel }, + { .name = "PrevRunlevel", .sig = "s", .getter = prop_prevrunlevel }, + { .name = "Version", .sig = "s", .getter = prop_version }, + { NULL, NULL, NULL } +}; + static const link_method_t manager_methods[] = { { .name = "ListServices", .in_sig = "", .out_sig = "as", .handler = manager_list_services }, @@ -373,8 +415,9 @@ static const link_method_t manager_methods[] = { }; static const link_vtable_t manager_vtable = { - .interface = "org.finit.Manager1", - .methods = manager_methods, + .interface = "org.finit.Manager1", + .methods = manager_methods, + .properties = manager_properties, }; /* ---------- org.finit.Service1 (one object per service) ---------- @@ -482,6 +525,28 @@ void dbus_unregister_service(svc_t *svc) (void)link_server_remove_object(server, path); } +/* ---------- signal fan-out helper ---------- + * + * Fan out a pre-marshalled signal body to every connected peer, + * letting each connection apply its AddMatch filter. Short-circuits + * when no peers are connected so dbus_notify_* callers don't have + * to inspect that state themselves. */ +static void dbus_emit_signal(const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + struct peer *p; + + if (!server || TAILQ_EMPTY(&peers)) + return; + TAILQ_FOREACH(p, &peers, link) + (void)link_connection_emit_signal(p->conn, + path, interface, member, + signature, body, body_len); +} + /* ---------- signal emission: ServiceStateChanged ---------- */ /* Coarse svc_state_t -> string. svc_status() in svc.h returns a @@ -513,36 +578,51 @@ static const char *state_name(svc_state_t s) void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) { - uint8_t body[256]; - link_writer_t w; - struct peer *p; - char ident[MAX_IDENT_LEN]; - ssize_t blen; - svc_state_t o = (svc_state_t)old_state; - svc_state_t n = (svc_state_t)new_state; + uint8_t body[256]; + link_writer_t w; + char ident[MAX_IDENT_LEN]; + ssize_t blen; - if (!server || !svc) + if (!svc) return; - if (TAILQ_EMPTY(&peers)) - return; /* nobody could possibly be listening */ - svc_ident(svc, ident, sizeof(ident)); link_writer_init(&w, body, sizeof(body)); link_w_string(&w, ident); - link_w_string(&w, state_name(o)); - link_w_string(&w, state_name(n)); + link_w_string(&w, state_name((svc_state_t)old_state)); + link_w_string(&w, state_name((svc_state_t)new_state)); blen = link_writer_finish(&w); if (blen < 0) return; - TAILQ_FOREACH(p, &peers, link) - (void)link_connection_emit_signal(p->conn, - "/org/finit/manager", - "org.finit.Manager1", - "ServiceStateChanged", - "sss", - body, (size_t)blen); + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "ServiceStateChanged", "sss", body, (size_t)blen); +} + +/* ---------- signal emission: RunlevelChanged ---------- + * + * Fired by sm.c right after the runlevel global flips. Body is + * (old, new) as one-digit strings, matching the format that + * Manager1.Runlevel (the property) returns. */ +void dbus_notify_runlevel_change(int old_level, int new_level) +{ + uint8_t body[64]; + link_writer_t w; + char old_s[8], new_s[8]; + ssize_t blen; + + snprintf(old_s, sizeof(old_s), "%d", old_level); + snprintf(new_s, sizeof(new_s), "%d", new_level); + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, old_s); + link_w_string(&w, new_s); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "RunlevelChanged", "ss", body, (size_t)blen); } /* ---------- org.finit.Cond1 ---------- */ @@ -761,14 +841,11 @@ static const link_vtable_t cond_vtable = { void dbus_notify_condition_change(const char *name, const char *state) { - uint8_t body[256]; - link_writer_t w; - struct peer *p; - ssize_t blen; + uint8_t body[256]; + link_writer_t w; + ssize_t blen; - if (!server || !name || !state) - return; - if (TAILQ_EMPTY(&peers)) + if (!name || !state) return; link_writer_init(&w, body, sizeof(body)); @@ -778,13 +855,8 @@ void dbus_notify_condition_change(const char *name, const char *state) if (blen < 0) return; - TAILQ_FOREACH(p, &peers, link) - (void)link_connection_emit_signal(p->conn, - COND_PATH_OBJECT, - COND_INTERFACE, - "ConditionChanged", - "ss", - body, (size_t)blen); + dbus_emit_signal(COND_PATH_OBJECT, COND_INTERFACE, + "ConditionChanged", "ss", body, (size_t)blen); } /* ---------- init / exit ---------- */ diff --git a/src/initctl.c b/src/initctl.c index 936e1902..1f176b60 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -41,6 +41,10 @@ #include "service.h" #include "cgutil.h" #include "utmp-api.h" +#ifdef HAVE_DBUS +#include "link.h" +#include "path.h" +#endif /* Used by both do_cond_act and (with HAVE_DBUS) cond_dbus_call. */ typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; @@ -196,6 +200,74 @@ static int show_log(char *arg) return do_log(svc, ""); } +#ifdef HAVE_DBUS +/* Fetch all org.finit.Manager1 string properties in one Properties.GetAll + * round-trip, then pick out a subset. `wanted` is a NULL-terminated array + * of property names; `out` parallel-receives the values (each entry left + * untouched if its property wasn't returned). Returns 0 on transport + * success (even if some properties weren't present), -1 on transport or + * parse failure. */ +static int dbus_get_manager_props(const char *const *wanted, char **out, size_t out_sz) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + uint32_t array_bytes; + size_t end; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.freedesktop.DBus.Properties", "GetAll", + "s", "org.finit.Manager1"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + return -1; + } + + r = link_client_reply(c); + if (!r || !r->body) { + link_client_close(c); + return -1; + } + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_u32(&reader, &array_bytes) < 0) { + link_client_close(c); + return -1; + } + end = link_r_pos(&reader) + array_bytes; + if (end > r->body_len) { + link_client_close(c); + return -1; + } + + while (link_r_pos(&reader) < end) { + const char *key = NULL; + const char *val = NULL; + size_t i; + + if (link_r_align(&reader, 8) < 0) break; + if (link_r_string(&reader, &key) < 0) break; + if (link_r_variant_string(&reader, &val) < 0) break; + if (!key || !val) break; + + for (i = 0; wanted[i]; i++) { + if (!strcmp(key, wanted[i])) { + strlcpy(out[i], val, out_sz); + break; + } + } + } + + link_client_close(c); + return 0; +} +#endif + static int do_runlevel(char *arg) { struct init_request rq = { @@ -208,6 +280,23 @@ static int do_runlevel(char *arg) int currlevel; char prev, curr; +#ifdef HAVE_DBUS + char curr_buf[16] = { 0 }, prev_buf[16] = { 0 }; + const char *const wanted[] = { "Runlevel", "PrevRunlevel", NULL }; + char *out[] = { curr_buf, prev_buf }; + + if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 && + curr_buf[0] && prev_buf[0]) { + int cl = atoi(curr_buf); + int pl = atoi(prev_buf); + + curr = (cl == INIT_LEVEL) ? 'S' : (char)(cl + '0'); + prev = (pl > 0 && pl <= 9) ? (char)(pl + '0') : 'N'; + printf("%c %c\n", prev, curr); + return 0; + } +#endif + currlevel = runlevel_get(&prevlevel); switch (currlevel) { case 255: @@ -272,7 +361,6 @@ static int do_startstop(int cmd, char *arg) } #ifdef HAVE_DBUS -#include "link.h" /* Try the D-Bus path for a Manager1 method. Returns: * 0 succeeded via D-Bus diff --git a/src/private.h b/src/private.h index 3b03f510..ae501161 100644 --- a/src/private.h +++ b/src/private.h @@ -53,6 +53,7 @@ void dbus_register_service (svc_t *svc); void dbus_unregister_service (svc_t *svc); void dbus_notify_service_state (svc_t *svc, int old_state, int new_state); void dbus_notify_condition_change(const char *name, const char *state); +void dbus_notify_runlevel_change(int old_level, int new_level); #endif void conf_flush_events(void); diff --git a/src/sm.c b/src/sm.c index ab3abdb9..44d0972d 100644 --- a/src/sm.c +++ b/src/sm.c @@ -388,6 +388,9 @@ void sm_step(void) prevlevel = runlevel; runlevel = sm.newlevel; sm.newlevel = -1; +#ifdef HAVE_DBUS + dbus_notify_runlevel_change(prevlevel, runlevel); +#endif /* Restore terse mode and run hooks before shutdown */ if (runlevel == 0 || runlevel == 6) { diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh index e26b4c11..05bc6e22 100755 --- a/test/dbus-manager.sh +++ b/test/dbus-manager.sh @@ -67,3 +67,31 @@ case "$result" in *InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;; *) fail "Unexpected reply from non-root ListServices: $result" ;; esac + +# ---------- Properties ---------- + +say "Introspect on /org/finit/manager advertises org.freedesktop.DBus.Properties" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) +case "$xml" in + *'org.freedesktop.DBus.Properties'*) assert "Properties interface in XML" 0 -eq 0 ;; + *) fail "Properties interface missing from XML" ;; +esac + +say "Manager1 declares Runlevel + Version as in introspection XML" +case "$xml" in + *' ", e.g. "N 2". Just check +# we get a sensible two-token line. +rl=$(texec initctl runlevel) +case "$rl" in + [N0-9S]\ [0-9S]) + assert "initctl runlevel returned '$rl'" 0 -eq 0 ;; + *) + fail "Unexpected initctl runlevel output: $rl" ;; +esac From 7fd1a46edb241246040f6a30df8c396caa2594b7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 20:38:10 +0200 Subject: [PATCH 18/22] libink/finit: opportunistic system-bus registration If a dbus-daemon is reachable at /var/run/dbus/system_bus_socket when dbus_init() runs, libink claims "org.finit" on the system bus and integrates the authenticated connection into the same peer machinery that /run/finit/bus uses. dbus-send, dbus-monitor, gdbus and any binding that speaks D-Bus over AF_UNIX can then talk to Finit without going through our custom socket. Two new libink primitives make this work: link_server_attach(server, fd, peer_uid) Promote an externally-authenticated fd into a server-tracked connection (skips AUTH, sets CLOEXEC+NONBLOCK, owns the fd from entry). peer_uid sets what privileged-method checks see; (uid_t)-1 makes all privileged methods reject by default -- the safe default for external system-bus traffic until per-sender uid lookup (GetConnectionUnixUser) lands. link_client_steal_fd(c) Detach the authenticated socket from a libink client and free the container, so callers can hand the fd directly to link_server_attach. src/dbus.c gains try_attach_system_bus() called once at the end of dbus_init. peer_register() is extracted from accept_cb so both the accept path and the new attach path enforce DBUS_MAX_PEERS and unwind correctly on alloc/uev_io failure. Test coverage is manual -- the test fixture has no dbus-daemon. Signed-off-by: Joachim Wiberg --- Makefile.am | 2 +- configure.ac | 1 + dbus-1/.gitignore | 2 + dbus-1/Makefile.am | 6 ++ dbus-1/org.finit.conf | 63 +++++++++++++++++ libink/client.c | 12 ++++ libink/link.h | 19 +++++ libink/server.c | 42 ++++++++++++ src/dbus.c | 156 +++++++++++++++++++++++++++++++++++------- 9 files changed, 278 insertions(+), 25 deletions(-) create mode 100644 dbus-1/.gitignore create mode 100644 dbus-1/Makefile.am create mode 100644 dbus-1/org.finit.conf diff --git a/Makefile.am b/Makefile.am index bc3b885d..b3afffea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,7 +8,7 @@ ACLOCAL_AMFLAGS = -I m4 # after src is fine. SUBDIRS = man plugins if DBUS -SUBDIRS += libink +SUBDIRS += libink dbus-1 endif SUBDIRS += src system tmpfiles.d dist_doc_DATA = README.md LICENSE contrib/finit.conf diff --git a/configure.ac b/configure.ac index d165a9b2..8f4349f3 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,7 @@ AC_CONFIG_FILES([Makefile contrib/debian/Makefile contrib/debian/finit.d/Makefile contrib/debian/finit.d/available/Makefile contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile doc/Makefile doc/config/Makefile + dbus-1/Makefile libink/Makefile libink/libink.pc libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile diff --git a/dbus-1/.gitignore b/dbus-1/.gitignore new file mode 100644 index 00000000..b336cc7c --- /dev/null +++ b/dbus-1/.gitignore @@ -0,0 +1,2 @@ +/Makefile +/Makefile.in diff --git a/dbus-1/Makefile.am b/dbus-1/Makefile.am new file mode 100644 index 00000000..dec4a333 --- /dev/null +++ b/dbus-1/Makefile.am @@ -0,0 +1,6 @@ +EXTRA_DIST = org.finit.conf + +if DBUS +dbuspolicydir = $(sysconfdir)/dbus-1/system.d +dist_dbuspolicy_DATA = org.finit.conf +endif diff --git a/dbus-1/org.finit.conf b/dbus-1/org.finit.conf new file mode 100644 index 00000000..ea730e9b --- /dev/null +++ b/dbus-1/org.finit.conf @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libink/client.c b/libink/client.c index a5c245a1..57181e1d 100644 --- a/libink/client.c +++ b/libink/client.c @@ -76,6 +76,18 @@ void link_client_close(link_client_t *c) free(c); } +int link_client_steal_fd(link_client_t *c) +{ + int fd; + + if (!c) + return -1; + fd = c->fd; + c->fd = -1; + free(c); + return fd; +} + /* read_full / send_all live in libink/io.c. */ #define read_full(fd, buf, len) __io_read_full ((fd), (buf), (len)) #define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) diff --git a/libink/link.h b/libink/link.h index 5188a503..44ae6786 100644 --- a/libink/link.h +++ b/libink/link.h @@ -96,6 +96,18 @@ int link_server_get_fd(const link_server_t *server); int link_server_accept(link_server_t *server, link_connection_t **conn); +/* Insert an externally-authenticated fd into the server's connection + * set. Used to integrate an outbound peer (e.g. a client-side + * handshake against an external dbus-daemon) so the same dispatch + + * signal-fan-out machinery covers it. `peer_uid` becomes what + * privileged-method checks see; pass (uid_t)-1 to make all + * LINK_METHOD_PRIVILEGED methods reject by default. + * + * On success the connection takes ownership of `fd`. On any failure + * `fd` is closed before the function returns NULL, so callers never + * have to track partial state. */ +link_connection_t *link_server_attach(link_server_t *server, int fd, uid_t peer_uid); + int link_connection_get_fd (const link_connection_t *conn); uid_t link_connection_get_uid (const link_connection_t *conn); int link_connection_process (link_connection_t *conn); @@ -201,6 +213,13 @@ int link_connection_emit_signal(link_connection_t *conn, link_client_t *link_client_open(const char *path); void link_client_close(link_client_t *c); +/* Detach the authenticated socket from the client and return the raw + * fd; subsequent link_client_close on `c` is invalid because the + * structure has already been freed. Used by callers (e.g. system-bus + * integration) that want to promote an outbound client connection + * into a server-attached peer via link_server_attach(). */ +int link_client_steal_fd(link_client_t *c); + /* Status codes returned by link_client_call(_v). */ #define LINK_CALL_OK 0 /* method-return received */ #define LINK_CALL_ERROR 1 /* server replied with an error */ diff --git a/libink/server.c b/libink/server.c index ef6325ae..f5a82431 100644 --- a/libink/server.c +++ b/libink/server.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -160,3 +161,44 @@ int link_server_accept(link_server_t *srv, link_connection_t **out) *out = conn; return 0; } + +link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid) +{ + link_connection_t *conn; + int flags; + + /* On entry we always own `fd` -- close it on every failure path + * so callers don't have to track whether we touched fcntl state. */ + if (!srv || fd < 0) { + if (fd >= 0) + close_save_errno(fd); + errno = EINVAL; + return NULL; + } + + /* Match server_accept's fd setup: CLOEXEC first (so a fork-and- + * exec between the two calls cannot leak the fd), then NONBLOCK + * so process_binary's read loop can drain without hanging. */ + flags = fcntl(fd, F_GETFD, 0); + if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + goto err_close; + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + goto err_close; + + conn = calloc(1, sizeof(*conn)); + if (!conn) + goto err_close; + + conn->fd = fd; + conn->auth = LINK_AUTH_DONE; /* caller already handshook */ + conn->server = srv; + conn->peer_uid = peer_uid; + __auth_generate_guid(conn->guid); + + return conn; + +err_close: + close_save_errno(fd); + return NULL; +} diff --git a/src/dbus.c b/src/dbus.c index 4b11118a..ec4f96dd 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -88,6 +88,39 @@ static void peer_cb(uev_t *w, void *arg, int events) peer_drop(p); } +/* Wrap an authenticated connection in a struct peer, insert into the + * peer list, and register an event-loop watcher. Enforces + * DBUS_MAX_PEERS. Closes the connection and returns NULL on failure. + * Used by both the accept path and the system-bus attach path. */ +static struct peer *peer_register(uev_ctx_t *ctx, link_connection_t *conn) +{ + struct peer *p; + + if (peer_count >= DBUS_MAX_PEERS) { + logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", + peer_count); + link_connection_close(conn); + return NULL; + } + + p = calloc(1, sizeof(*p)); + if (!p) { + link_connection_close(conn); + return NULL; + } + + p->conn = conn; + TAILQ_INSERT_TAIL(&peers, p, link); + peer_count++; + + if (uev_io_init(ctx, &p->watcher, peer_cb, p, + link_connection_get_fd(conn), UEV_READ)) { + peer_drop(p); + return NULL; + } + return p; +} + static void accept_cb(uev_t *w, void *arg, int events) { (void)arg; @@ -99,7 +132,6 @@ static void accept_cb(uev_t *w, void *arg, int events) for (;;) { link_connection_t *conn = NULL; - struct peer *p; if (link_server_accept(server, &conn) < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) @@ -107,29 +139,8 @@ static void accept_cb(uev_t *w, void *arg, int events) break; } - if (peer_count >= DBUS_MAX_PEERS) { - logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", - peer_count); - link_connection_close(conn); - continue; - } - - p = calloc(1, sizeof(*p)); - if (!p) { - link_connection_close(conn); - err(1, "Out of memory accepting D-Bus client"); - break; - } - - p->conn = conn; - TAILQ_INSERT_TAIL(&peers, p, link); - peer_count++; - - if (uev_io_init(w->ctx, &p->watcher, peer_cb, p, - link_connection_get_fd(conn), UEV_READ)) { - err(1, "Failed registering D-Bus peer watcher"); - peer_drop(p); - } + if (!peer_register(w->ctx, conn)) + continue; /* logged inside */ } } @@ -859,6 +870,101 @@ void dbus_notify_condition_change(const char *name, const char *state) "ConditionChanged", "ss", body, (size_t)blen); } +/* ---------- system-bus attach (opportunistic) ---------- + * + * If /var/run/dbus/system_bus_socket is reachable, libink connects to + * the system bus as a regular client, claims org.finit as a well-known + * name, then promotes the authenticated fd into a server-attached + * peer so the same vtables serve incoming method calls and outgoing + * signal fan-out reaches the system bus. + * + * peer_uid is set to (uid_t)-1 so LINK_METHOD_PRIVILEGED methods + * reject by default -- per-request sender uid lookup via + * GetConnectionUnixUser is a follow-up. Read-only methods + * (ListServices, Properties.Get, Introspect, ...) work as expected. + * + * Caveat: the AUTH + Hello + RequestName round-trips run synchronously + * from dbus_init() in PID 1, so a hung dbus-daemon would block boot. + * The kernel has no default socket-level timeout; adding one (via + * SO_RCVTIMEO around the handshake, or threading a timeout through + * link_client_call) is a follow-up. In practice dbus-daemon is rarely + * present on the embedded systems Finit primarily targets, and where + * it is present it is reliable. */ + +#define SYSTEM_BUS_PATH "/var/run/dbus/system_bus_socket" +#define FINIT_BUS_NAME "org.finit" +/* DBUS_NAME_FLAG_DO_NOT_QUEUE: fail fast if the name is taken + * (something else owns org.finit -- shouldn't happen and we'd + * rather log than silently sit in the queue). */ +#define DBUS_NAME_FLAG_DO_NOT_QUEUE 0x04 + +static int sysbus_request_name(link_client_t *c) +{ + const link_reply_t *r; + link_reader_t reader; + uint32_t result = 0; + int rc; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "RequestName", + "su", FINIT_BUS_NAME, + (uint32_t)DBUS_NAME_FLAG_DO_NOT_QUEUE); + if (rc != LINK_CALL_OK) + return -1; + + r = link_client_reply(c); + if (!r || !r->body) + return -1; + link_reader_init(&reader, r->body, r->body_len); + if (link_r_u32(&reader, &result) < 0) + return -1; + + /* 1 = primary owner; 2/3/4 mean we didn't get the name */ + return (result == 1) ? 0 : -1; +} + +static void try_attach_system_bus(uev_ctx_t *ctx) +{ + link_client_t *c; + link_connection_t *conn; + int rc; + + c = link_client_open(SYSTEM_BUS_PATH); + if (!c) { + dbg("System bus unavailable at %s; skipping registration", + SYSTEM_BUS_PATH); + return; + } + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + if (rc != LINK_CALL_OK) { + dbg("System-bus Hello failed (rc=%d); skipping", rc); + link_client_close(c); + return; + } + + if (sysbus_request_name(c) < 0) { + logit(LOG_WARNING, "Failed to claim %s on system bus", FINIT_BUS_NAME); + link_client_close(c); + return; + } + + /* link_server_attach owns the fd from this point on whether it + * succeeds or fails, so the steal-then-attach pair has no leak + * window. */ + conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1); + if (!conn) + return; + + if (!peer_register(ctx, conn)) { + logit(LOG_WARNING, "Failed registering system-bus peer"); + return; + } + + logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); +} + /* ---------- init / exit ---------- */ int dbus_init(uev_ctx_t *ctx) @@ -906,6 +1012,8 @@ int dbus_init(uev_ctx_t *ctx) dbus_register_service(svc); } + try_attach_system_bus(ctx); + return 0; } From b76175224c966b2a76998a97e59a08a709ffc534 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 15 May 2026 22:51:40 +0200 Subject: [PATCH 19/22] libink: array iterator, reply accessors, connect timeout Three small additions distilled from earlier review rounds. * link_r_array_begin(r, &out_end) reads the u32 byte-length prefix of an "a" payload and returns the end offset. Callers loop while link_r_pos < *out_end. Replaces the manual u32+pos walk in print_string_array (test client) and dbus_get_manager_props (initctl). * link_reply_get_string(r, &out) and link_reply_get_u32(r, &out) wrap link_reader_init + link_r_*. Five sites switch from a 3-line dance to a one-liner. * link_client_open_timeout(path, ms) sets SO_SNDTIMEO + SO_RCVTIMEO before AUTH. Once the fd is handed to link_server_attach and flipped to non-blocking the timeouts are silently inert. try_attach_system_bus uses a 2-second budget, so a wedged dbus-daemon can no longer stall boot. link_client_open(path) is preserved as an alias for link_client_open_timeout(path, 0); no caller needs updating unless it actually wants a budget. Signed-off-by: Joachim Wiberg --- libink/client.c | 46 ++++++++++++++++++++++++++++++++++++- libink/dispatch.c | 1 + libink/link.h | 32 ++++++++++++++++++++++---- libink/marshal.c | 20 ++++++++++++++++ libink/marshal.h | 1 + src/dbus.c | 28 +++++++++------------- src/initctl.c | 17 +++----------- test/src/dbus-auth-client.c | 31 +++++-------------------- 8 files changed, 114 insertions(+), 62 deletions(-) diff --git a/libink/client.c b/libink/client.c index 57181e1d..fbfbe70f 100644 --- a/libink/client.c +++ b/libink/client.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -35,7 +36,7 @@ struct link_client { size_t rxlen; }; -link_client_t *link_client_open(const char *path) +link_client_t *link_client_open_timeout(const char *path, int timeout_ms) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; link_client_t *c; @@ -48,6 +49,20 @@ link_client_t *link_client_open(const char *path) fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return NULL; + + if (timeout_ms > 0) { + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + /* Cover both directions so the AUTH write and the + * subsequent read both honour the budget. setsockopt + * failure is non-fatal -- the bus may still respond + * quickly enough; we just lose the safety net. */ + (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { close(fd); return NULL; @@ -67,6 +82,11 @@ link_client_t *link_client_open(const char *path) return c; } +link_client_t *link_client_open(const char *path) +{ + return link_client_open_timeout(path, 0); +} + void link_client_close(link_client_t *c) { if (!c) @@ -241,6 +261,30 @@ const link_reply_t *link_client_reply(link_client_t *c) return &c->reply; } +int link_reply_get_string(const link_reply_t *r, const char **out) +{ + link_reader_t reader; + + if (out) + *out = NULL; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_string(&reader, out); +} + +int link_reply_get_u32(const link_reply_t *r, uint32_t *out) +{ + link_reader_t reader; + + if (out) + *out = 0; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_u32(&reader, out); +} + /* Marshal varargs into `body` (capacity `cap`) according to `sig`. * Returns the marshalled length on success, -1 on overflow or * unsupported type code. */ diff --git a/libink/dispatch.c b/libink/dispatch.c index 9e68793b..7e9536fa 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -314,6 +314,7 @@ int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); } int link_r_variant_string(link_reader_t *r, const char **o) { return __r_variant_string(r, o); } int link_r_align (link_reader_t *r, size_t n) { return __r_align (r, n); } +int link_r_array_begin(link_reader_t *r, size_t *e) { return __r_array_begin(r, e); } int link_r_done (const link_reader_t *r) { return __r_done (r); } size_t link_r_pos (const link_reader_t *r) { return r->off; } diff --git a/libink/link.h b/libink/link.h index 44ae6786..11e4a2c4 100644 --- a/libink/link.h +++ b/libink/link.h @@ -211,6 +211,14 @@ int link_connection_emit_signal(link_connection_t *conn, * Returns NULL on any failure (caller can fall back to another * transport if it has one). */ link_client_t *link_client_open(const char *path); + +/* As link_client_open but applies SO_SNDTIMEO + SO_RCVTIMEO before + * the connect/AUTH handshake. After link_server_attach flips the fd + * to non-blocking the timeout is silently inert; it only protects + * the synchronous open path against a hung peer. timeout_ms == 0 + * disables the budget (same behaviour as link_client_open). */ +link_client_t *link_client_open_timeout(const char *path, int timeout_ms); + void link_client_close(link_client_t *c); /* Detach the authenticated socket from the client and return the raw @@ -263,6 +271,14 @@ int link_client_call_v(link_client_t *c, const link_reply_t *link_client_reply(link_client_t *c); +/* Convenience accessors for the common case where a reply carries + * exactly one string ("s" or "o") or one u32 ("u"). They wrap the + * link_reader_init + link_r_* pattern; on success return 0 and + * populate *out, on parse failure or missing body return -1. Use + * link_client_reply + link_reader_init directly for richer payloads. */ +int link_reply_get_string(const link_reply_t *r, const char **out); +int link_reply_get_u32 (const link_reply_t *r, uint32_t *out); + /* Wait up to `timeout_ms` milliseconds for the next inbound message * (typically a SIGNAL delivered after an AddMatch subscription), and * populate the same view returned by link_client_reply(). @@ -314,11 +330,17 @@ int link_r_variant_string(link_reader_t *r, const char **out); /* "v" contain int link_r_align (link_reader_t *r, size_t n); /* skip to next n-byte boundary */ int link_r_done (const link_reader_t *r); -/* Byte offset of the next read inside the original body buffer. Used - * to detect end-of-array when walking "a" payloads: read the array - * byte-length prefix with link_r_u32 first, record (pos+length) as the - * end, then loop while link_r_pos < end. For "a{T}" (dict-entry - * arrays) call link_r_align(r, 8) at the top of each iteration. */ +/* Begin reading an "a" array. On success returns 0 and sets + * *out_end to the absolute reader offset at which the array ends; + * caller loops while link_r_pos < *out_end. For dict-entry arrays + * ("a{T}") call link_r_align(r, 8) at the top of each iteration -- + * the element-alignment skip from the array prefix only covers the + * first entry. */ +int link_r_array_begin(link_reader_t *r, size_t *out_end); + +/* Byte offset of the next read inside the original body buffer. + * Use together with the *out_end returned by link_r_array_begin to + * walk the elements of an "a" payload. */ size_t link_r_pos (const link_reader_t *r); #ifdef __cplusplus diff --git a/libink/marshal.c b/libink/marshal.c index 60406221..ae215893 100644 --- a/libink/marshal.c +++ b/libink/marshal.c @@ -299,3 +299,23 @@ int __r_align(struct link_reader *r, size_t n) { return r_skip_align(r, n); } + +/* Begin reading an "a" array. Reads the u32 byte-length prefix + * and sets *out_end to the absolute reader offset at which the array + * ends. Caller loops while r->off < *out_end. Returns -1 on a + * truncated or oversized array length. */ +int __r_array_begin(struct link_reader *r, size_t *out_end) +{ + uint32_t array_bytes; + size_t end; + + if (__r_u32(r, &array_bytes) < 0) + return -1; + end = r->off + (size_t)array_bytes; + if (end > r->cap) { + r->err = 1; + return -1; + } + *out_end = end; + return 0; +} diff --git a/libink/marshal.h b/libink/marshal.h index e40b4f41..313205b2 100644 --- a/libink/marshal.h +++ b/libink/marshal.h @@ -46,6 +46,7 @@ int __r_string(struct link_reader *r, const char **out); /* "s" */ int __r_path (struct link_reader *r, const char **out); /* "o" */ int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ int __r_align (struct link_reader *r, size_t n); /* skip to n-byte boundary */ +int __r_array_begin(struct link_reader *r, size_t *out_end); int __r_done (const struct link_reader *r); #endif /* LIBINK_MARSHAL_H_ */ diff --git a/src/dbus.c b/src/dbus.c index ec4f96dd..a032eaeb 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -883,16 +883,18 @@ void dbus_notify_condition_change(const char *name, const char *state) * GetConnectionUnixUser is a follow-up. Read-only methods * (ListServices, Properties.Get, Introspect, ...) work as expected. * - * Caveat: the AUTH + Hello + RequestName round-trips run synchronously - * from dbus_init() in PID 1, so a hung dbus-daemon would block boot. - * The kernel has no default socket-level timeout; adding one (via - * SO_RCVTIMEO around the handshake, or threading a timeout through - * link_client_call) is a follow-up. In practice dbus-daemon is rarely - * present on the embedded systems Finit primarily targets, and where - * it is present it is reliable. */ + * A bounded SO_SNDTIMEO/SO_RCVTIMEO budget is applied via + * link_client_open_timeout so a hung dbus-daemon can't stall boot; + * once the connection is attached and flipped to non-blocking, those + * timeouts are silently inert. */ #define SYSTEM_BUS_PATH "/var/run/dbus/system_bus_socket" #define FINIT_BUS_NAME "org.finit" +/* Budget for the synchronous AUTH + Hello + RequestName round-trips. + * If the system bus is alive but the daemon is wedged we'd rather + * give up after a couple of seconds than stall the rest of dbus_init + * (and through it, boot). */ +#define SYSTEM_BUS_TIMEOUT_MS 2000 /* DBUS_NAME_FLAG_DO_NOT_QUEUE: fail fast if the name is taken * (something else owns org.finit -- shouldn't happen and we'd * rather log than silently sit in the queue). */ @@ -900,8 +902,6 @@ void dbus_notify_condition_change(const char *name, const char *state) static int sysbus_request_name(link_client_t *c) { - const link_reply_t *r; - link_reader_t reader; uint32_t result = 0; int rc; @@ -911,14 +911,8 @@ static int sysbus_request_name(link_client_t *c) (uint32_t)DBUS_NAME_FLAG_DO_NOT_QUEUE); if (rc != LINK_CALL_OK) return -1; - - r = link_client_reply(c); - if (!r || !r->body) - return -1; - link_reader_init(&reader, r->body, r->body_len); - if (link_r_u32(&reader, &result) < 0) + if (link_reply_get_u32(link_client_reply(c), &result) < 0) return -1; - /* 1 = primary owner; 2/3/4 mean we didn't get the name */ return (result == 1) ? 0 : -1; } @@ -929,7 +923,7 @@ static void try_attach_system_bus(uev_ctx_t *ctx) link_connection_t *conn; int rc; - c = link_client_open(SYSTEM_BUS_PATH); + c = link_client_open_timeout(SYSTEM_BUS_PATH, SYSTEM_BUS_TIMEOUT_MS); if (!c) { dbg("System bus unavailable at %s; skipping registration", SYSTEM_BUS_PATH); diff --git a/src/initctl.c b/src/initctl.c index 1f176b60..2d3ccc82 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -212,7 +212,6 @@ static int dbus_get_manager_props(const char *const *wanted, char **out, size_t link_client_t *c; const link_reply_t *r; link_reader_t reader; - uint32_t array_bytes; size_t end; int rc; @@ -235,12 +234,7 @@ static int dbus_get_manager_props(const char *const *wanted, char **out, size_t } link_reader_init(&reader, r->body, r->body_len); - if (link_r_u32(&reader, &array_bytes) < 0) { - link_client_close(c); - return -1; - } - end = link_r_pos(&reader) + array_bytes; - if (end > r->body_len) { + if (link_r_array_begin(&reader, &end) < 0) { link_client_close(c); return -1; } @@ -445,14 +439,9 @@ static int cond_dbus_call(link_client_t **bus, condop_t op, "s", arg); if (rc == LINK_CALL_OK) { if (op == COND_GET) { - const link_reply_t *r = link_client_reply(*bus); - link_reader_t reader; - const char *state = NULL; + const char *state = NULL; - if (r && r->body) { - link_reader_init(&reader, r->body, r->body_len); - link_r_string(&reader, &state); - } + link_reply_get_string(link_client_reply(*bus), &state); if (verbose && state) puts(state); *out_exit = (state && !strcmp(state, "on")) ? 0 diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 53f85223..fd0e3bb2 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -175,8 +175,6 @@ static int drop_uid(const char *uid_arg, const char *progname) static int mode_hello(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *name; int rc; @@ -188,9 +186,7 @@ static int mode_hello(int argc, char *argv[]) "org.freedesktop.DBus", "Hello", NULL); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - if (link_r_string(&reader, &name) == 0) + if (link_reply_get_string(link_client_reply(c), &name) == 0) printf("%s\n", name); else rc = 2; @@ -202,8 +198,6 @@ static int mode_hello(int argc, char *argv[]) static int mode_introspect(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *xml; int rc; @@ -216,9 +210,7 @@ static int mode_introspect(int argc, char *argv[]) "Introspect", NULL); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - if (link_r_string(&reader, &xml) == 0) + if (link_reply_get_string(link_client_reply(c), &xml) == 0) printf("%s\n", xml); else rc = 2; @@ -227,23 +219,17 @@ static int mode_introspect(int argc, char *argv[]) return rc; } -/* Decode body with signature "as" -- u32 array byte-len, then "s" strings. - * libink's public reader doesn't yet have an array helper, so we walk - * the wire form with link_r_u32 + link_r_string + link_r_pos. */ +/* Decode body with signature "as", print one string per line. */ static int print_string_array(const link_reply_t *r) { link_reader_t reader; - uint32_t array_len; size_t end; if (!r || !r->signature || strcmp(r->signature, "as") != 0) return -1; link_reader_init(&reader, r->body, r->body_len); - if (link_r_u32(&reader, &array_len) < 0) - return -1; - end = link_r_pos(&reader) + array_len; - if (end > r->body_len) + if (link_r_array_begin(&reader, &end) < 0) return -1; while (link_r_pos(&reader) < end) { @@ -330,8 +316,6 @@ static int mode_call_void_as_uid(int argc, char *argv[]) static int mode_get_service(int argc, char *argv[]) { link_client_t *c; - const link_reply_t *r; - link_reader_t reader; const char *path; int rc; @@ -344,11 +328,8 @@ static int mode_get_service(int argc, char *argv[]) "s", argv[3]); rc = report_rc(c, rc); if (rc == 0) { - r = link_client_reply(c); - link_reader_init(&reader, r->body, r->body_len); - /* Reply signature is "o" but link_r_path / link_r_string - * have the same wire form. */ - if (link_r_path(&reader, &path) == 0) + /* Reply sig is "o", same wire form as "s". */ + if (link_reply_get_string(link_client_reply(c), &path) == 0) printf("%s\n", path); else rc = 2; From 390e278097b5eb77909186ed9b5b54af28326ee8 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 16 May 2026 08:13:02 +0200 Subject: [PATCH 20/22] initctl: SetDebug, Signal, Suspend, per-svc Reload via D-Bus The state-changing subcommands that still went exclusively through INIT_SOCKET now have matching Manager1/Service1 methods and route through them when reachable. src/dbus.c: * Manager1.SetDebug(): toggles the debug flag (log_debug()). * Manager1.Signal(s, u): sends signal u (1..31) to every running service matching identity s. signal_one() silently skips matched-but-not-running services so a multi-match ident doesn't spuriously fail. * Manager1.Suspend(): sync(); suspend(). Maps EINVAL to "kernel does not support suspend to RAM". * dispatch_action grew a udata pointer so signal_one can receive the signo. initctl: * toggle_debug, do_suspend and do_reload() gain the standard try_dbus_manager / try_dbus_service preamble with legacy fallback. * do_signal inlines its Manager1.Signal call (try_dbus_manager doesn't yet have a "su" body grammar). The error-mapping pattern across all three sites is now a small map_dbus_err helper. * try_dbus_service builds /org/finit/service/ via link_path_encode and dispatches Service1.. * Server-side signo bound is 1..31 to match initctl's existing range (str2sig + strtonum); both transports user-visible equivalent. Signed-off-by: Joachim Wiberg --- src/dbus.c | 89 +++++++++++++++++++++++--- src/initctl.c | 145 +++++++++++++++++++++++++++++++++---------- test/dbus-initctl.sh | 28 +++++++++ test/dbus-manager.sh | 38 ++++++++++++ 4 files changed, 260 insertions(+), 40 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index a032eaeb..acea312f 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -31,9 +31,11 @@ #ifdef HAVE_DBUS #include +#include #include #include #include +#include #include #include @@ -50,6 +52,7 @@ #include "sig.h" #include "sm.h" #include "svc.h" +#include "util.h" #define DBUS_MAX_PEERS 64 @@ -251,6 +254,7 @@ static int dbus_apply_restart(svc_t *svc, void *user_data) struct dispatch_ctx { int (*action)(svc_t *, void *); + void *udata; int matched; }; @@ -259,7 +263,7 @@ static int dispatch_found(svc_t *svc, void *udata) struct dispatch_ctx *ctx = udata; ctx->matched++; - return ctx->action(svc, NULL); + return ctx->action(svc, ctx->udata); } static int dispatch_missing(char *job, char *id, void *udata) @@ -268,14 +272,15 @@ static int dispatch_missing(char *job, char *id, void *udata) return 0; /* don't penalise the return; we'll check ->matched */ } -/* Apply `action` to every service matched by `ident`. Returns 0 if - * at least one service matched and the action succeeded on all; - * -1 if no service matched the identity (caller sends NoSuchService). */ +/* Apply `action(svc, udata)` to every service matched by `ident`. + * Returns 0 if at least one service matched and the action succeeded + * on all; -1 if no service matched the identity (caller sends + * NoSuchService). */ static int dispatch_action(const char *ident, - int (*action)(svc_t *, void *)) + int (*action)(svc_t *, void *), void *udata) { char buf[128]; - struct dispatch_ctx ctx = { .action = action }; + struct dispatch_ctx ctx = { .action = action, .udata = udata }; int rc; if (!ident || !*ident || strlen(ident) >= sizeof(buf)) @@ -297,7 +302,7 @@ static int manager_take_string_method(link_call_t *call, return link_call_reply_error(call, "org.freedesktop.DBus.Error.InvalidArgs", "expected (s)"); - if (dispatch_action(ident, action) != 0) + if (dispatch_action(ident, action, NULL) != 0) return link_call_reply_error(call, "org.finit.Error.NoSuchService", ident); @@ -359,6 +364,70 @@ static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shut static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); } static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); } +static int manager_set_debug(link_call_t *call, void *u) +{ + (void)u; + log_debug(); + (void)link_call_reply(call); + return 0; +} + +static int signal_one(svc_t *svc, void *udata) +{ + int signo = *(int *)udata; + + /* Silently skip stopped services -- a multi-match ident + * (e.g. "sshd:*") should not fail the whole call just because + * one of the matches happens to be in a halted state. */ + if (!svc_is_running(svc)) + return 0; + return !!kill(svc->pid, signo); +} + +static int manager_signal(link_call_t *call, void *u) +{ + const char *ident; + uint32_t signo; + int sig; + + (void)u; + if (link_call_read_string(call, &ident) < 0 || + link_call_read_u32 (call, &signo) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s, u)"); + /* Match the upper bound `initctl signal` allows (1..31). RT + * signals are a future story; keep both sides in lockstep so + * users see the same range regardless of transport. */ + if (signo == 0 || signo > 31) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "signal out of range (1..31)"); + + sig = (int)signo; + if (dispatch_action(ident, signal_one, &sig) != 0) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + (void)link_call_reply(call); + return 0; +} + +static int manager_suspend(link_call_t *call, void *u) +{ + (void)u; + sync(); + if (suspend() < 0) { + const char *msg = (errno == EINVAL) + ? "Kernel does not support suspend to RAM" + : strerror(errno); + return link_call_reply_error(call, + "org.finit.Error.Failed", msg); + } + (void)link_call_reply(call); + return 0; +} + /* ---------- Manager1 properties ---------- * * Read-only string properties exposed via the standard @@ -422,6 +491,12 @@ static const link_method_t manager_methods[] = { .flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff }, { .name = "Halt", .in_sig = "", .out_sig = "", .flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt }, + { .name = "Suspend", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_suspend }, + { .name = "SetDebug", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_debug }, + { .name = "Signal", .in_sig = "su", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_signal }, { NULL, NULL, NULL, 0, NULL } }; diff --git a/src/initctl.c b/src/initctl.c index 2d3ccc82..2febc596 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -152,6 +152,11 @@ static int runlevel_get(int *prevlevel) return rc; } +#ifdef HAVE_DBUS +static int try_dbus_manager(const char *method, const char *arg_sig, + const char *arg); +#endif + static int toggle_debug(char *arg) { struct init_request rq = { @@ -159,6 +164,13 @@ static int toggle_debug(char *arg) .cmd = INIT_CMD_DEBUG, }; + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("SetDebug", "", NULL); + if (rc >= 0) return rc; + } +#endif return client_send(&rq, sizeof(rq)); } @@ -356,20 +368,45 @@ static int do_startstop(int cmd, char *arg) #ifdef HAVE_DBUS +/* Map a LINK_CALL_ERROR reply on `c` to the appropriate ERRX exit: + * org.finit.Error.NoSuchService -> exit 69 (legacy "no such svc") + * org.freedesktop.DBus.Error.AccessDenied -> exit 1 (permission denied) + * anything else -> exit 1 (method: err) + * `c` is closed before exit either way. Use exact-match on the + * fully-qualified error name; a substring match would misfire on a + * future name that contained one of these as a prefix. */ +static void map_dbus_err(link_client_t *c, const char *method, const char *ident) +{ + const link_reply_t *r = link_client_reply(c); + char err[128]; + + /* The reply view points into c->rxbuf; copy the error name out + * before link_client_close() frees the client. Otherwise the + * strcmps below read freed memory. */ + if (r && r->error_name) + strlcpy(err, r->error_name, sizeof(err)); + else + err[0] = '\0'; + link_client_close(c); + + if (!strcmp(err, "org.finit.Error.NoSuchService")) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", + ident ? ident : ""); + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: %s requires root", method); + ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); +} + /* Try the D-Bus path for a Manager1 method. Returns: * 0 succeeded via D-Bus - * 1 D-Bus replied with an error -- callers should error out * -1 D-Bus not reachable -- callers should fall back to the * legacy INIT_SOCKET transport - * - * On D-Bus error replies the function maps the org.* error name to - * the same exit code initctl historically printed for that case - * (e.g. NoSuchService -> 69 with the legacy message). */ + * LINK_CALL_ERROR is handled internally via map_dbus_err (does not + * return). */ static int try_dbus_manager(const char *method, const char *arg_sig, const char *arg) { link_client_t *c; - const char *err; int rc; c = link_client_open(FINIT_BUS_SOCKET); @@ -388,29 +425,37 @@ static int try_dbus_manager(const char *method, const char *arg_sig, else rc = LINK_CALL_FAIL; - if (rc == LINK_CALL_ERROR) { - const link_reply_t *r = link_client_reply(c); + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, arg); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; +} - err = (r && r->error_name) ? r->error_name : ""; - /* Exact match on the fully-qualified error name; substring - * matching would misfire on a future name that contains - * one of these as a substring. */ - if (!strcmp(err, "org.finit.Error.NoSuchService")) { - link_client_close(c); - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", - arg ? arg : ""); - } - if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) { - link_client_close(c); - ERRX(1, "permission denied: %s requires root", method); - } - link_client_close(c); - ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); - } +/* Call a void-arg method on Service1 at /org/finit/service/. + * Same return convention as try_dbus_manager. */ +static int try_dbus_service(const char *method, const char *ident) +{ + char path[256]; + const char *prefix = "/org/finit/service/"; + size_t plen = strlen(prefix); + link_client_t *c; + int rc; + + if (!ident || !*ident) + return -1; + memcpy(path, prefix, plen); + if (link_path_encode(ident, path + plen, sizeof(path) - plen) < 0) + return -1; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, path, "org.finit.Service1", method, NULL); + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, ident); /* exits */ link_client_close(c); - if (rc == LINK_CALL_OK) - return 0; - return -1; /* LINK_CALL_FAIL or anything else: fall back */ + return (rc == LINK_CALL_OK) ? 0 : -1; } /* Try one Cond1.{Get,Set,Clear} call. On COND_GET success the helper @@ -570,6 +615,12 @@ static int do_reload (char *arg) return do_svc(INIT_CMD_RELOAD, NULL); } +#ifdef HAVE_DBUS + { + int rc = try_dbus_service("Reload", arg); + if (rc >= 0) return rc; + } +#endif return do_startstop(INIT_CMD_RELOAD_SVC, arg); } @@ -607,10 +658,6 @@ int do_signal(int argc, char *argv[]) if (argc != 2) ERRX(2, "invalid number of arguments to signal"); - strlcpy(rq.data, argv[0], sizeof(rq.data)); - if (client_send(&rq, sizeof(rq))) - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); - signo = str2sig(argv[1]); if (signo == -1) { const char *errstr = NULL; @@ -620,6 +667,29 @@ int do_signal(int argc, char *argv[]) ERRX(65, "%s signal: %s", errstr, argv[1]); } +#ifdef HAVE_DBUS + { + link_client_t *c = link_client_open(FINIT_BUS_SOCKET); + + if (c) { + int rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "Signal", + "su", argv[0], (uint32_t)signo); + + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, "Signal", argv[0]); /* exits */ + link_client_close(c); + if (rc == LINK_CALL_OK) + return 0; + /* LINK_CALL_FAIL: drop to legacy */ + } + } +#endif + + strlcpy(rq.data, argv[0], sizeof(rq.data)); + if (client_send(&rq, sizeof(rq))) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); + /* Reuse runlevel for signal number. */ rq.magic = INIT_MAGIC; rq.cmd = INIT_CMD_SIGNAL; @@ -1001,8 +1071,17 @@ int do_poweroff(char *arg) return do_cmd(INIT_CMD_POWEROFF); } -/* Suspend has no Manager1 equivalent yet; uses the legacy IPC. */ -int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); } +int do_suspend(char *arg) +{ + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("Suspend", "", NULL); + if (rc >= 0) return rc; + } +#endif + return do_cmd(INIT_CMD_SUSPEND); +} /** * do_switch_root - Switch to a new root filesystem (initramfs only) diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh index c51c1a3e..2a1b47c1 100755 --- a/test/dbus-initctl.sh +++ b/test/dbus-initctl.sh @@ -66,3 +66,31 @@ case "$(cat /tmp/dbus-initctl-cond.out)" in *) fail "initctl cond set didn't produce expected signal: $(cat /tmp/dbus-initctl-cond.out)" ;; esac + +# ---------- arc B: signal + reload routing ---------- + +# initctl signal exits 0 iff the bus call succeeded. We send SIGCONT +# (no observable side-effect on a healthy daemon) so the test stays +# benign regardless of how keventd handles it. +say "initctl signal routes through Manager1.Signal" +texec initctl signal keventd CONT >/dev/null \ + || fail "initctl signal returned non-zero" +assert "initctl signal ok" 0 -eq 0 + +say "initctl signal on a bogus identity reports NoSuchService" +set +e +texec initctl signal no-such-svc-anywhere CONT >/tmp/dbus-sig-bad.out 2>&1 +sigbad_rc=$? +set -e +assert "Bogus signal target rejected (rc=$sigbad_rc)" "$sigbad_rc" -ne 0 +case "$(cat /tmp/dbus-sig-bad.out)" in + *"no such task or service"*) + assert "Error message mentions missing service" 0 -eq 0 ;; + *) + fail "Unexpected initctl signal output: $(cat /tmp/dbus-sig-bad.out)" ;; +esac + +say "initctl reload routes through Service1.Reload" +texec initctl reload keventd >/dev/null \ + || fail "initctl reload keventd returned non-zero" +assert "Per-service reload ok" 0 -eq 0 diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh index 05bc6e22..c3bdce33 100755 --- a/test/dbus-manager.sh +++ b/test/dbus-manager.sh @@ -85,6 +85,44 @@ case "$xml" in fail "Property declarations missing or out of order: $xml" ;; esac +# ---------- arc B: SetDebug + Suspend authz ---------- + +say "Manager1.SetDebug as root succeeds" +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/dev/null \ + || fail "SetDebug returned non-zero" +# Toggle back so this test leaves debug in the same state we found it +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/dev/null || true +assert "SetDebug round-trip ok" 0 -eq 0 + +say "Manager1.SetDebug from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/tmp/dbus-setdbg.out 2>&1 +sdbg_rc=$? +set -e +assert "Non-root SetDebug rejected (rc=$sdbg_rc)" "$sdbg_rc" -eq 1 +case "$(cat /tmp/dbus-setdbg.out)" in + *AccessDenied*) assert "SetDebug authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-setdbg.out)" ;; +esac + +# Suspend would actually suspend the test sysroot if it succeeded -- so +# we only test the non-root rejection path, which fails before suspend() +# is called. +say "Manager1.Suspend from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 Suspend >/tmp/dbus-susp.out 2>&1 +susp_rc=$? +set -e +assert "Non-root Suspend rejected (rc=$susp_rc)" "$susp_rc" -eq 1 +case "$(cat /tmp/dbus-susp.out)" in + *AccessDenied*) assert "Suspend authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-susp.out)" ;; +esac + say "initctl runlevel reads via Properties.Get when D-Bus available" # runlevel output format is " ", e.g. "N 2". Just check # we get a sensible two-token line. From dbf6a88fd03a7d04c577e9ea605723547fb179be Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 16 May 2026 08:41:32 +0200 Subject: [PATCH 21/22] doc: D-Bus integration reference + ChangeLog doc/dbus.md is the user-facing reference for the D-Bus surface: bus address, object tree, methods/properties/signals per interface, identity encoding, authorization model, and the mapping table from each initctl subcommand to the D-Bus method it routes through. Examples use dbus-send and dbus-monitor (which ship with the dbus reference implementation); gdbus and pure-Python bindings work the same way. Closes #396 Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 49 ++++++++ doc/dbus.md | 294 +++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 344 insertions(+) create mode 100644 doc/dbus.md diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 35c61552..3597609b 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -8,6 +8,55 @@ All relevant changes are documented in this file. ### Changes +- Finit now ships with a built-in brokerless D-Bus implementation, + **libink**, exposing the running init system as a peer on its own + private bus at `/run/finit/bus`, and -- opportunistically -- + registering `org.finit` on the standard system bus when a + `dbus-daemon` is reachable. No external `libdbus`/`sd-bus`/`GIO` + dependency. + + The bus implements the stock `org.freedesktop.DBus`, + `org.freedesktop.DBus.Peer`, `org.freedesktop.DBus.Introspectable`, + and `org.freedesktop.DBus.Properties` interfaces, plus three + Finit-specific ones: + + * `org.finit.Manager1` at `/org/finit/manager` -- + `ListServices`, `GetService`, `Start`/`Stop`/`Restart`/`Reload`, + `SetRunlevel`, `SetDebug`, `Signal`, `Suspend`, and the + `Reboot`/`Halt`/`Poweroff` triplet. Read-only properties + `Runlevel`, `PrevRunlevel`, `Version`. Signals + `ServiceStateChanged (sss)` and `RunlevelChanged (ss)`. + + * `org.finit.Service1` at `/org/finit/service/` -- one + object per loaded service, with `Start`/`Stop`/`Restart`/`Reload` + for working off an object handle rather than passing the + identity string around. + + * `org.finit.Cond1` at `/org/finit/cond` -- `Get`, `Set`, `Clear`, + `List`, `Dump` for [user-defined conditions](conditions.md), + with a `ConditionChanged (ss)` signal. + + Privileged methods reject non-root callers based on the kernel- + authenticated peer uid (`SO_PEERCRED`); read-only methods are open. + See [D-Bus Integration](dbus.md) for the full surface, build flag, + and `dbus-send`/`dbus-monitor` examples. + +- `initctl` now transparently routes through D-Bus when the bus is + reachable, with the legacy `INIT_SOCKET` transport as a fallback: + `start`, `stop`, `restart`, `reload`, `reload `, `reboot`, + `halt`, `poweroff`, `suspend`, `debug`, `signal`, `runlevel`, and + `cond {get,set,clr}` all use the new path. Two new subcommands + show up that have no legacy equivalent: + + * `initctl monitor` -- streams every signal on the bus to the + terminal, one line per delivery (`HH:MM:SS iface.member(args)`), + until interrupted. Same idea as `dbus-monitor`, but scoped to + Finit and with no address plumbing required. + + * Issuing `initctl cond set/clr` over D-Bus also fires the + `Cond1.ConditionChanged` signal, so observers see user-driven + state changes the same way they see service-driven ones. + - Restart log now spells out the signal name and flags core dumps, e.g. `killed by SIGKILL` or `killed by SIGSEGV, core dumped`, in place of the bare numeric `by signal: N`. Gives operators a much diff --git a/doc/dbus.md b/doc/dbus.md new file mode 100644 index 00000000..3b035909 --- /dev/null +++ b/doc/dbus.md @@ -0,0 +1,294 @@ +D-Bus Integration +================= + +Finit ships with a built-in, brokerless [D-Bus][] implementation, +**libink**, that exposes the running init system as a peer on its own +private bus, and optionally on the system bus when `dbus-daemon` is +available. Everything `initctl` does is also reachable from any +generic D-Bus tooling — `dbus-send`, `dbus-monitor`, `gdbus`, +language bindings, dashboards, monitoring agents, etc. + +> [!NOTE] +> D-Bus support is enabled at build time with `--enable-dbus`. See +> [Building](build.md) for details. When disabled, `initctl` keeps +> using the legacy `INIT_SOCKET` transport and Finit exposes no bus. + +Bus address +----------- + +| Bus | Address | +| --- | --- | +| Local (always) | `unix:path=/run/finit/bus` | +| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket`, well-known name `org.finit` | + +The **local** bus is brokerless: clients connect straight to Finit +over a Unix-domain socket using the standard D-Bus SASL EXTERNAL +handshake. No `dbus-daemon` is required, which makes it suitable for +embedded systems that don't ship one. + +The **system** bus is best-effort: at start-up Finit probes for a +running `dbus-daemon` and, if reachable, registers `org.finit` so that +standard tooling sees Finit just like any other system service: + +```sh +dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager \ + org.finit.Manager1.ListServices + +dbus-monitor --system "sender='org.finit'" +``` + +If no system bus is present (the common case on embedded targets), +this step is silently skipped. + +Object tree +----------- + +``` +/ +├── org/ +│ └── finit/ +│ ├── manager Manager1 +│ ├── cond Cond1 +│ └── service/ +│ ├── keventd Service1 (one per service) +│ ├── sshd +│ └── … +└── org/freedesktop/DBus Standard well-known interfaces +``` + +Every node implements the usual stock interfaces: + +| Interface | Purpose | +| ------------------------------------ | ------- | +| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` (on `/org/freedesktop/DBus`) | +| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | +| `org.freedesktop.DBus.Introspectable`| `Introspect()` — XML description | +| `org.freedesktop.DBus.Properties` | `Get`, `GetAll` (Set not yet implemented) | + +`org.finit.Manager1` +-------------------- + +Lives at **`/org/finit/manager`**. Owns the global init operations +and the service registry. + +### Methods + +| Method | In sig | Out sig | Privileged | Notes | +| ----------------------- | ------ | ------- | ---------- | ----- | +| `ListServices` | — | `as` | no | Returns the identities (`name`, `name:id`) of every loaded service. | +| `GetService` | `s` | `o` | no | Resolves a service identity to its `Service1` object path. | +| `Start` | `s` | — | yes | Start the service(s) matching the identity. | +| `Stop` | `s` | — | yes | Stop the service(s) matching the identity. | +| `Restart` | `s` | — | yes | Restart (stop + start) the service(s). | +| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes (same as `initctl reload`). | +| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6). | +| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag. | +| `Signal` | `su` | — | yes | Send signal number `u` (1–31) to every running service matching identity `s`. Halted matches are silently skipped. | +| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM. | +| `Reboot` / `Halt` / `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence. | + +### Properties + +All read-only strings; observable via `Properties.Get` and +`Properties.GetAll`. + +| Property | Type | Returns | +| -------------- | ---- | ------- | +| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"`. | +| `PrevRunlevel` | `s` | Previous runlevel, same encoding. | +| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`). | + +### Signals + +| Signal | Body | Fires when | +| ----------------------- | ---- | ---------- | +| `ServiceStateChanged` | `(sss)` — identity, old state, new state | A service transitions between supervisor states. | +| `RunlevelChanged` | `(ss)` — old level, new level | The system enters a new runlevel. | + +State names emitted by `ServiceStateChanged` are stable wire strings: +`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, +`paused`, `waiting`, `starting`, `running`. + +`org.finit.Service1` (per-service objects) +------------------------------------------ + +Lives at **`/org/finit/service/`**, one object per loaded +service. `` is the service identity (name, or `name:id` for +templated services) put through systemd-style `_HH` hex escaping — +ASCII alphanumerics and `_` pass through, anything else becomes `_HH` +where `HH` is the hex byte. Use `Manager1.GetService(identity)` to +look up the exact path rather than constructing it by hand. + +| Method | In sig | Out sig | Privileged | Notes | +| --------- | ------ | ------- | ---------- | ----- | +| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service. | +| `Stop` | — | — | yes | … | +| `Restart` | — | — | yes | … | +| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart). | + +The per-service surface lets generic tooling supply an object handle +once and then invoke methods on it, instead of repeatedly passing the +identity string. + +`org.finit.Cond1` +----------------- + +Lives at **`/org/finit/cond`**. Exposes Finit's +[condition system](conditions.md) to bus clients. + +### Methods + +| Method | In sig | Out sig | Privileged | Notes | +| -------- | ------ | ------- | ---------- | ----- | +| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition. | +| `Set` | `s` | — | yes | Assert a `usr/` condition. Non-`usr/*` paths are rejected with `InvalidArgs` (system conditions belong to Finit's state machine). | +| `Clear` | `s` | — | yes | Deassert a `usr/` condition. | +| `List` | — | `as` | no | Names of all known conditions. | +| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns. | + +### Signals + +| Signal | Body | Fires when | +| ------------------- | ---- | ---------- | +| `ConditionChanged` | `(ss)` — name, new state | A condition is asserted or deasserted. | + +Authorization +------------- + +Privileged methods reject any caller whose peer `uid` isn't 0. +On the **local** bus the kernel's `SO_PEERCRED` socket option tells +Finit exactly who's calling, so privilege escalation through the bus +is impossible. + +On the **system** bus, all incoming traffic is treated as +unprivileged: it arrives through `dbus-daemon` (typically running as +root) and Finit cannot yet ask the daemon for the real requester's +uid via `GetConnectionUnixUser`. This means external tooling can +freely `Get`/`Introspect`/`ListServices`, but every state-changing +method returns `org.freedesktop.DBus.Error.AccessDenied`. Per-sender +uid lookup is on the roadmap. + +When a privileged method is rejected the error name is exactly +`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a +short reason string (e.g. `"permission denied: Start requires root"`). + +`initctl` integration +--------------------- + +`initctl` transparently routes through D-Bus when the bus socket is +present, and falls back to the legacy `INIT_SOCKET` transport +otherwise. Concretely, the following subcommands use the bus first: + +| Subcommand | Method | +| ------------------- | ------------------------------- | +| `initctl start` | `Manager1.Start(svc)` | +| `initctl stop` | `Manager1.Stop(svc)` | +| `initctl restart` | `Manager1.Restart(svc)` | +| `initctl reload` | `Manager1.Reload()` | +| `initctl reload S` | `Service1.Reload()` (per-svc) | +| `initctl reboot` | `Manager1.Reboot()` | +| `initctl halt` | `Manager1.Halt()` | +| `initctl poweroff` | `Manager1.Poweroff()` | +| `initctl suspend` | `Manager1.Suspend()` | +| `initctl debug` | `Manager1.SetDebug()` | +| `initctl signal` | `Manager1.Signal(svc, signo)` | +| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` | +| `initctl cond set/get/clr` | `Cond1.{Set,Get,Clear}` | + +Two `initctl` subcommands are pure D-Bus features without legacy +equivalents: + +* `initctl monitor` — subscribes to every signal on the local bus and + prints one line per delivery (with timestamp, interface and + member). Same idea as `dbus-monitor`, but scoped to Finit and with + no need to pass `--address`. + +* `initctl cond` (when D-Bus is reachable) emits the standard + `Cond1.ConditionChanged` signal as a side effect, so subscribers + observe user-driven state changes the same way they observe + service-driven ones. + +Examples +-------- + +The examples below use `dbus-send` and `dbus-monitor`, which ship as +part of the [dbus][] reference implementation; they're widely +packaged and don't pull in any extra runtime. Any tool that speaks +D-Bus over an AF_UNIX socket works equally well — `gdbus`, Python's +`jeepney`/`dasbus`, etc. — substitute their syntax for setting the +bus address. + +When `org.finit` is registered on the system bus you can replace +`--address=unix:path=/run/finit/bus` with `--system` in any example +below. + +List the running services: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.finit.Manager1.ListServices +``` + +Read the current runlevel via the Properties interface: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.freedesktop.DBus.Properties.Get \ + string:org.finit.Manager1 string:Runlevel +``` + +Subscribe to every state change on the manager object: + +```sh +dbus-monitor --address=unix:path=/run/finit/bus \ + "type='signal',interface='org.finit.Manager1'" +``` + +Or use `initctl monitor`, which does the same without any address +plumbing. + +Restart a service by its object path: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/service/sshd \ + org.finit.Service1.Restart +``` + +Trigger a `usr/`-condition assertion that wakes any dependent service: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/cond \ + org.finit.Cond1.Set string:"data-ready" +``` + +The `--dest=org.finit` argument is informational on the local +brokerless bus — Finit accepts any destination because there's no +broker to route by name — but `dbus-send` requires it syntactically. + +[dbus]: https://gitlab.freedesktop.org/dbus/dbus + +Implementation notes +-------------------- + +The D-Bus server library lives in `libink/`. It speaks the binary +D-Bus 1.0 wire format directly, has no `libdbus`/`sd-bus`/`GIO` +dependency, and is tiny — a few thousand lines of C. The Finit-side +glue in `src/dbus.c` registers vtables for Manager1/Service1/Cond1, +emits the four signals from the appropriate hook points (state +transitions, runlevel transitions, condition flips), and bridges the +event loop to the libink server. + +The `initctl` client uses the same library — `link_client_open`, +`link_client_call_v`, `link_client_reply`, `link_reader_*` — so the +single wire-format implementation serves both ends. + +[D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/mkdocs.yml b/mkdocs.yml index 8d02f735..d07fdb9a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,6 +90,7 @@ nav: - Plugins: plugins.md - Watchdog: watchdog.md - keventd: keventd.md + - D-Bus Integration: dbus.md - Service State Machine: state-machine.md - Distributions: distro.md - Requirements: requirements.md From 7e771b856502d58a3f8bfedb797f3c444f010381 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 17 May 2026 20:40:41 +0200 Subject: [PATCH 22/22] .github: point ld at /tmp/lib for the libink smoke checks Use LD_LIBRARY_PATH=/tmp/lib only on the commands that actually need libink. The unit test step uses its own sysroot and is unaffected. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 460d378a..aa6d0c50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,16 +63,16 @@ jobs: tree /tmp || true - name: Check dependencies run: | - ldd /tmp/sbin/finit + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/finit size /tmp/sbin/finit - ldd /tmp/sbin/initctl + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/initctl size /tmp/sbin/initctl - ldd /tmp/sbin/reboot + LD_LIBRARY_PATH=/tmp/lib ldd /tmp/sbin/reboot size /tmp/sbin/reboot - name: Verify starting and showing usage text run: | - sudo /tmp/sbin/finit -h - sudo /tmp/sbin/initctl -h + sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/finit -h + sudo env LD_LIBRARY_PATH=/tmp/lib /tmp/sbin/initctl -h - name: Enable unprivileged userns (unshare) run: | sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0