libink: a brokerless D-Bus implementation for Finit

Finit had no way to answer the question every service manager gets
asked: what is running, and change it.  D-Bus is how the rest of
userspace asks, but linking libdbus, sd-bus or GIO into PID 1 buys a
dependency, an allocator and a main loop we do not control.

So libink: the wire format, an object tree, and a bus of Finit's own
at /run/finit/bus, gated like INIT_SOCKET.  It speaks the standard
org.freedesktop.DBus, .Peer, .Introspectable interfaces, and Finit's
own Manager1, Service1 and Cond1 on top.  Methods that change
something are marked privileged and answered only for a caller the
kernel vouched for, via SO_PEERCRED.

Server and client both, since initctl is the first thing that needs
to talk to it, and its Start/Stop/Restart/Reload now go over the bus
rather than the legacy socket.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-08-13 09:28:14 +02:00
1 parent ae9a24f44f
commit 0a269f3298
39 files changed
+5530 -49

No files matched your search

+12 -1
View File
@@ -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
+10
View File
@@ -12,6 +12,7 @@ AC_CONFIG_FILES([Makefile
contrib/debian/Makefile contrib/debian/finit.d/Makefile contrib/debian/finit.d/available/Makefile
contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile
doc/Makefile doc/config/Makefile
libink/Makefile libink/libink.pc
libsystemd/Makefile libsystemd/libsystemd.pc
man/Makefile
plugins/Makefile
@@ -94,6 +95,10 @@ AC_ARG_ENABLE(logrotate,
AS_HELP_STRING([--disable-logrotate], [Disable built-in rotation of /var/log/wtmp]),,[
enable_logrotate=yes])
AC_ARG_ENABLE(dbus,
AS_HELP_STRING([--disable-dbus], [Disable D-Bus support (libink + Finit object tree)]),,[
enable_dbus=yes])
AC_ARG_ENABLE(doc,
AS_HELP_STRING([--disable-doc], [Disable build and install of doc/ section]),,[
enable_doc=yes])
@@ -243,6 +248,10 @@ AS_IF([test "x$enable_rescue" != "xno"], [
AM_CONDITIONAL(LOGROTATE, [test "x$enable_logrotate" = "xyes"])
AS_IF([test "x$enable_dbus" = "xyes"], [
AC_DEFINE(HAVE_DBUS, 1, [Build D-Bus support via libink])])
AM_CONDITIONAL(DBUS, [test "x$enable_dbus" = "xyes"])
### With features ##############################################################################
AS_IF([test "x$bash_dir" = "xyes"], [
PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0],
@@ -438,6 +447,7 @@ Optional features:
Built-in sulogin......: $with_sulogin $sulogin
Built-in watchdogd....: $with_watchdog $watchdog
Built-in logrotate....: $enable_logrotate
D-Bus support (libink): $enable_dbus
Replacement libsystemd: $with_libsystemd
Use cgroup v2.........: $enable_cgroup
Use libcap............: $enable_libcap
+8
View File
@@ -0,0 +1,8 @@
.deps/*
.libs/*
.dirstamp
*.lo
libink.*
!libink.pc.in
Makefile
Makefile.in
+22
View File
@@ -0,0 +1,22 @@
# libink — brokerless D-Bus library (server + client), born inside Finit
lib_LTLIBRARIES = libink.la
libink_la_SOURCES = server.c auth.c connection.c \
proto.c proto.h \
marshal.c marshal.h \
dispatch.c builtin.c \
match.c \
path.c \
client.c io.c \
internal.h
libink_la_LDFLAGS = -version-info 0:0:0
libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE
libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
# pkg-config support
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libink.pc
# Public headers install to $(includedir)/ink/
inkdir = $(includedir)/ink
ink_HEADERS = link.h path.h
+300
View File
@@ -0,0 +1,300 @@
/* libink — D-Bus AUTH EXTERNAL handshake
*
* Implements the line-based SASL-style exchange described in the
* D-Bus specification, section "Authentication Protocol". Only the
* AUTH EXTERNAL mechanism is offered; everything else is rejected.
*
* The exchange:
*
* client --> [nul byte]
* client --> "AUTH EXTERNAL <hex-uid>\r\n"
* server <-- "OK <hex-guid>\r\n"
* client --> "NEGOTIATE_UNIX_FD\r\n" [optional]
* server <-- "ERROR <reason>\r\n" (no fd-passing yet)
* client --> "BEGIN\r\n"
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#include <unistd.h>
#include "internal.h"
static const char rejected_ext[] = "REJECTED EXTERNAL\r\n";
#define write_all(fd, buf, len) __io_write_all((fd), (buf), (len))
/* Shared by __auth_generate_guid (server) and __auth_client
* (client) for hex-encoding GUIDs and uid claims. */
static const char hex_digits[] = "0123456789abcdef";
static int reply(int fd, const char *line)
{
return write_all(fd, line, strlen(line));
}
static int reject(link_connection_t *conn)
{
return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1);
}
void __auth_generate_guid(char out[33])
{
uint8_t raw[16];
size_t i;
if (getrandom(raw, sizeof(raw), 0) != (ssize_t)sizeof(raw)) {
/* Extraordinarily unlikely; GUID is informational, not a
* security primitive — fall back to something deterministic
* rather than uninitialized memory. */
for (i = 0; i < sizeof(raw); i++)
raw[i] = (uint8_t)(i ^ 0xa5);
}
for (i = 0; i < sizeof(raw); i++) {
out[i * 2] = hex_digits[raw[i] >> 4];
out[i * 2 + 1] = hex_digits[raw[i] & 0xf];
}
out[32] = '\0';
}
static int hexval(int c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
/* Parse "AUTH EXTERNAL <hex>" payload into a uid. The argument is
* an even-length hex string whose decoded form is a decimal uid in
* ASCII. Returns 0 on success, -1 on malformed input. */
static int parse_external_uid(const char *arg, size_t arglen, uid_t *out)
{
char decoded[24];
char *ep = NULL;
unsigned long v;
size_t i, dlen;
if (arglen == 0 || (arglen & 1) || arglen / 2 >= sizeof(decoded))
return -1;
dlen = arglen / 2;
for (i = 0; i < dlen; i++) {
int hi = hexval((unsigned char)arg[i * 2]);
int lo = hexval((unsigned char)arg[i * 2 + 1]);
if (hi < 0 || lo < 0)
return -1;
decoded[i] = (char)((hi << 4) | lo);
}
decoded[dlen] = '\0';
errno = 0;
v = strtoul(decoded, &ep, 10);
if (errno || !ep || *ep != '\0' || v > (unsigned long)((uid_t)-1))
return -1;
*out = (uid_t)v;
return 0;
}
static int handle_line(link_connection_t *conn, const char *line, size_t len)
{
if (len >= 14 && memcmp(line, "AUTH EXTERNAL ", 14) == 0) {
uid_t claimed;
char ok[64];
if (parse_external_uid(line + 14, len - 14, &claimed) < 0)
return reject(conn);
if (conn->peer_uid == (uid_t)-1 || claimed != conn->peer_uid)
return reject(conn);
snprintf(ok, sizeof(ok), "OK %s\r\n", conn->guid);
return reply(conn->fd, ok);
}
if (len == 4 && memcmp(line, "AUTH", 4) == 0)
return reject(conn);
if (len == 17 && memcmp(line, "NEGOTIATE_UNIX_FD", 17) == 0)
return reply(conn->fd, "ERROR fd-passing not supported\r\n");
if (len == 5 && memcmp(line, "BEGIN", 5) == 0) {
conn->auth = LINK_AUTH_DONE;
return 0;
}
if (len == 6 && memcmp(line, "CANCEL", 6) == 0)
return reject(conn);
if (len >= 5 && memcmp(line, "ERROR", 5) == 0)
return reject(conn);
return reply(conn->fd, "ERROR Unknown command\r\n");
}
/* Pull one CR+LF-terminated line out of conn->linebuf. Returns the
* line length (without the CR+LF), or 0 if no complete line is
* present yet. Consumes the line on success. */
static size_t take_line(link_connection_t *conn, char *out, size_t outsz)
{
size_t i;
for (i = 0; i + 1 < conn->linelen; i++) {
if (conn->linebuf[i] == '\r' && conn->linebuf[i + 1] == '\n') {
size_t linelen = i;
size_t consumed = i + 2;
if (linelen >= outsz)
linelen = outsz - 1;
memcpy(out, conn->linebuf, linelen);
out[linelen] = '\0';
memmove(conn->linebuf, conn->linebuf + consumed,
conn->linelen - consumed);
conn->linelen -= consumed;
return linelen;
}
}
return 0;
}
int __auth_process(link_connection_t *conn)
{
uint8_t buf[256];
ssize_t n;
size_t off = 0;
n = read(conn->fd, buf, sizeof(buf));
if (n == 0)
return -1; /* peer closed */
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
return 0;
return -1;
}
if (conn->auth == LINK_AUTH_NUL) {
if (buf[0] != 0x00) {
conn->auth = LINK_AUTH_FAILED;
return -1;
}
off = 1;
conn->auth = LINK_AUTH_LINE;
}
if (conn->auth == LINK_AUTH_LINE) {
size_t take = (size_t)n - off;
char line[LINK_AUTH_LINEBUF_SIZE];
size_t linelen;
if (conn->linelen + take > sizeof(conn->linebuf)) {
conn->auth = LINK_AUTH_FAILED;
return -1;
}
memcpy(conn->linebuf + conn->linelen, buf + off, take);
conn->linelen += take;
while ((linelen = take_line(conn, line, sizeof(line))) > 0) {
if (handle_line(conn, line, linelen) < 0)
return -1;
if (conn->auth != LINK_AUTH_LINE)
break;
}
/* If BEGIN flipped us to DONE, any remaining linebuf bytes
* are the first bytes of the binary D-Bus stream — move
* them to rxbuf so the dispatcher can pick them up on the
* next process() call. */
if (conn->auth == LINK_AUTH_DONE && conn->linelen > 0) {
if (conn->linelen > sizeof(conn->rxbuf))
return -1;
memcpy(conn->rxbuf, conn->linebuf, conn->linelen);
conn->rxlen = conn->linelen;
conn->linelen = 0;
}
}
return 0;
}
/* ---- client-side SASL composer ---- */
/* Read a single CR+LF (or just LF) terminated line from fd into buf.
* Returns the line length (without the terminator), or -1 on EOF or
* buffer overflow. Blocks until a complete line arrives.
*
* Used only by __auth_client; the server-side parser does its
* own line extraction out of conn->linebuf. */
static ssize_t client_read_line(int fd, char *buf, size_t bufsz)
{
size_t off = 0;
while (off + 1 < bufsz) {
ssize_t n = read(fd, buf + off, 1);
if (n == 0)
return -1;
if (n < 0) {
if (errno == EINTR)
continue;
return -1;
}
if (buf[off] == '\n') {
buf[off] = '\0';
if (off > 0 && buf[off - 1] == '\r')
buf[--off] = '\0';
return (ssize_t)off;
}
off++;
}
return -1;
}
int __auth_client(int fd, uid_t uid)
{
char uidstr[16];
char hexuid[32];
char line[64];
char reply_line[256];
size_t i, n;
int rc;
if (write_all(fd, "\0", 1) < 0)
return -1;
n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)uid);
if (n * 2 >= sizeof(hexuid))
return -1;
for (i = 0; i < n; i++) {
unsigned c = (unsigned char)uidstr[i];
hexuid[i * 2] = hex_digits[c >> 4];
hexuid[i * 2 + 1] = hex_digits[c & 0xf];
}
hexuid[n * 2] = '\0';
rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid);
if (rc < 0 || (size_t)rc >= sizeof(line))
return -1;
if (write_all(fd, line, (size_t)rc) < 0)
return -1;
if (client_read_line(fd, reply_line, sizeof(reply_line)) < 0)
return -1;
if (strncmp(reply_line, "OK ", 3) != 0)
return -1;
if (write_all(fd, "BEGIN\r\n", 7) < 0)
return -1;
return 0;
}
+314
View File
@@ -0,0 +1,314 @@
/* libink — built-in implementations of the well-known
* org.freedesktop.DBus.* interfaces (Hello, Peer, Introspectable).
*
* These run before object-tree lookup in the dispatcher; returning
* 0 means "handled, reply sent"; <0 means "not a built-in, fall
* through to user-registered handlers".
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include "internal.h"
/* ---------- helpers ---------- */
static int member_is(const struct link_msg *m, const char *iface, const char *member)
{
if (!m->member || strcmp(m->member, member) != 0)
return 0;
if (m->interface && strcmp(m->interface, iface) != 0)
return 0;
return 1;
}
static int send_string_reply(link_connection_t *conn, const struct link_msg *req,
const char *s)
{
struct link_writer w;
ssize_t blen;
__w_init(&w, conn->txbuf, sizeof(conn->txbuf));
__w_string(&w, s);
blen = __w_finish(&w);
if (blen < 0) {
errno = EMSGSIZE;
return -1;
}
return __send_method_return(conn, req, "s", conn->txbuf, (size_t)blen);
}
/* ---------- Hello ---------- */
static int handle_hello(link_connection_t *conn, const struct link_msg *m)
{
if (!conn->unique_name[0]) {
uint32_t n = ++conn->server->next_unique_id;
snprintf(conn->unique_name, sizeof(conn->unique_name),
":1.%u", n);
}
return send_string_reply(conn, m, conn->unique_name);
}
/* ---------- Ping / GetMachineId ---------- */
static int handle_ping(link_connection_t *conn, const struct link_msg *m)
{
return __send_method_return(conn, m, NULL, NULL, 0);
}
static int handle_get_machine_id(link_connection_t *conn, const struct link_msg *m)
{
/* D-Bus mandates a 32-char hex machine-id. Use the per-server
* GUID-style identifier we already generate for each connection,
* promoted to a per-server constant on first call. Good enough
* for the brokerless case where clients use this only as a
* sanity hint. */
static char machine_id[33];
if (!machine_id[0])
__auth_generate_guid(machine_id);
return send_string_reply(conn, m, machine_id);
}
/* ---------- Introspect ---------- */
struct xbuf {
char *buf;
size_t cap;
size_t off;
int err;
};
static void xprintf(struct xbuf *x, const char *fmt, ...)
{
va_list ap;
int n;
if (x->err)
return;
va_start(ap, fmt);
n = vsnprintf(x->buf + x->off, x->cap - x->off, fmt, ap);
va_end(ap);
if (n < 0 || (size_t)n >= x->cap - x->off) {
x->err = 1;
return;
}
x->off += (size_t)n;
}
/* Emit a single <method> stanza for one method definition. */
static void emit_method(struct xbuf *x, const link_method_t *m)
{
const char *p;
xprintf(x, " <method name=\"%s\">\n", m->name);
for (p = m->in_sig ? m->in_sig : ""; *p; p++)
xprintf(x, " <arg type=\"%c\" direction=\"in\"/>\n", *p);
for (p = m->out_sig ? m->out_sig : ""; *p; p++)
xprintf(x, " <arg type=\"%c\" direction=\"out\"/>\n", *p);
xprintf(x, " </method>\n");
}
/* Introspection limitation: emit_method prints one <arg> 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[] =
" <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
" <method name=\"Introspect\">\n"
" <arg name=\"data\" type=\"s\" direction=\"out\"/>\n"
" </method>\n"
" </interface>\n"
" <interface name=\"org.freedesktop.DBus.Peer\">\n"
" <method name=\"Ping\"/>\n"
" <method name=\"GetMachineId\">\n"
" <arg name=\"machine_uuid\" type=\"s\" direction=\"out\"/>\n"
" </method>\n"
" </interface>\n";
/* Is `child` a path under `parent`? If so, write the first segment
* of the relative remainder into out (max outsz) and return 1. */
static int child_segment(const char *parent, const char *child,
char *out, size_t outsz)
{
size_t plen = strlen(parent);
const char *rest, *slash;
size_t seglen;
if (strncmp(parent, child, plen) != 0)
return 0;
/* Special case for "/" */
if (plen == 1 && parent[0] == '/')
rest = child + 1;
else if (child[plen] != '/')
return 0;
else
rest = child + plen + 1;
if (!*rest)
return 0;
slash = strchr(rest, '/');
seglen = slash ? (size_t)(slash - rest) : strlen(rest);
if (seglen + 1 > outsz)
return 0;
memcpy(out, rest, seglen);
out[seglen] = '\0';
return 1;
}
static int handle_introspect(link_connection_t *conn, const struct link_msg *m)
{
static char xml[8192]; /* static keeps the stack small in PID 1 */
struct xbuf x = { .buf = xml, .cap = sizeof(xml) };
struct link_object *o;
const char *path = m->path;
xprintf(&x,
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
" \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
"<node>\n");
xprintf(&x, "%s", STANDARD_INTERFACES_XML);
o = NULL;
{
struct link_object *p;
TAILQ_FOREACH(p, &conn->server->objects, link) {
if (strcmp(p->path, path) == 0) {
o = p;
break;
}
}
}
if (o) {
struct link_vtable_entry *e;
const link_method_t *meth;
TAILQ_FOREACH(e, &o->vtables, link) {
xprintf(&x, " <interface name=\"%s\">\n",
e->vt->interface);
if (e->vt->methods)
for (meth = e->vt->methods; meth->name; meth++)
emit_method(&x, meth);
xprintf(&x, " </interface>\n");
}
}
{
struct link_object *p;
char prev_seg[LINK_PATH_MAX] = { 0 };
char seg [LINK_PATH_MAX];
TAILQ_FOREACH(p, &conn->server->objects, link) {
if (!child_segment(path, p->path, seg, sizeof(seg)))
continue;
if (strcmp(prev_seg, seg) == 0)
continue;
xprintf(&x, " <node name=\"%s\"/>\n", seg);
memcpy(prev_seg, seg, sizeof(prev_seg));
}
}
xprintf(&x, "</node>\n");
if (x.err)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.Failed",
"Introspection XML overflow");
return send_string_reply(conn, m, xml);
}
/* ---------- AddMatch / RemoveMatch ---------- */
static int handle_add_match(link_connection_t *conn, const struct link_msg *m)
{
const char *rule;
struct link_reader r;
if (!m->signature || strcmp(m->signature, "s") != 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"AddMatch takes a single string");
__r_init(&r, m->body, m->body_avail);
if (__r_string(&r, &rule) < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Malformed argument");
if (__match_add(conn, rule) < 0) {
if (errno == ENOSPC)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.LimitsExceeded",
"Too many active match rules");
return __send_error(conn, m,
"org.freedesktop.DBus.Error.MatchRuleInvalid",
"Unrecognised key or malformed rule");
}
return __send_method_return(conn, m, NULL, NULL, 0);
}
static int handle_remove_match(link_connection_t *conn, const struct link_msg *m)
{
const char *rule;
struct link_reader r;
if (!m->signature || strcmp(m->signature, "s") != 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"RemoveMatch takes a single string");
__r_init(&r, m->body, m->body_avail);
if (__r_string(&r, &rule) < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Malformed argument");
if (__match_remove(conn, rule) < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.MatchRuleNotFound",
"No such match rule on this connection");
return __send_method_return(conn, m, NULL, NULL, 0);
}
/* ---------- entry point ---------- */
int __handle_builtin(link_connection_t *conn, const struct link_msg *m)
{
if (member_is(m, "org.freedesktop.DBus", "Hello") &&
m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0)
return handle_hello(conn, m);
if (member_is(m, "org.freedesktop.DBus", "AddMatch") &&
m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0)
return handle_add_match(conn, m);
if (member_is(m, "org.freedesktop.DBus", "RemoveMatch") &&
m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0)
return handle_remove_match(conn, m);
if (member_is(m, "org.freedesktop.DBus.Peer", "Ping"))
return handle_ping(conn, m);
if (member_is(m, "org.freedesktop.DBus.Peer", "GetMachineId"))
return handle_get_machine_id(conn, m);
if (member_is(m, "org.freedesktop.DBus.Introspectable", "Introspect"))
return handle_introspect(conn, m);
return -1; /* not a built-in */
}
+295
View File
@@ -0,0 +1,295 @@
/* libink — synchronous client-side D-Bus calls.
*
* Pairs with server.c / connection.c on the receiving end. The
* intent is for short-lived CLI tools (initctl) and tests to use
* libink as their D-Bus client rather than reimplementing the
* wire format.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <poll.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include "internal.h"
struct link_client {
int fd;
uint32_t next_serial;
link_reply_t reply; /* most recent reply view (points into rxbuf) */
/* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which
* is a wire-valid (if malformed) type, so we need an out-of-band
* "have we ever produced a reply?" flag. */
int have_reply;
/* Re-use the server-side rx buffer size for incoming replies.
* Replies to our methods are bounded by the same per-message
* sanity cap as everything else. */
uint8_t rxbuf[LINK_RX_BUF_SIZE];
size_t rxlen;
};
link_client_t *link_client_open(const char *path)
{
struct sockaddr_un sun = { .sun_family = AF_UNIX };
link_client_t *c;
int fd;
if (!path || strlen(path) >= sizeof(sun.sun_path))
return NULL;
memcpy(sun.sun_path, path, strlen(path) + 1);
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
return NULL;
if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
close(fd);
return NULL;
}
if (__auth_client(fd, geteuid()) < 0) {
close(fd);
return NULL;
}
c = calloc(1, sizeof(*c));
if (!c) {
close(fd);
return NULL;
}
c->fd = fd;
c->next_serial = 1;
return c;
}
void link_client_close(link_client_t *c)
{
if (!c)
return;
if (c->fd >= 0)
close(c->fd);
free(c);
}
/* read_full / send_all live in libink/io.c. */
#define read_full(fd, buf, len) __io_read_full ((fd), (buf), (len))
#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len))
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
/* Read one complete D-Bus message: the 16-byte fixed header tells
* us fields_len + body_len, so we then issue exactly one more read
* for the remainder. Both lengths are bounded against rxbuf before
* arithmetic so a malformed wire u32 can't wrap into a near-4-GiB
* read. */
static int read_one(link_client_t *c, struct link_msg *msg)
{
uint32_t body_len, fields_len, body_off, total;
ssize_t consumed;
memset(msg, 0, sizeof(*msg));
if (read_full(c->fd, c->rxbuf, 16) < 0)
return -1;
if (c->rxbuf[0] != 'l')
return -1;
body_len = (uint32_t)c->rxbuf[4]
| ((uint32_t)c->rxbuf[5] << 8)
| ((uint32_t)c->rxbuf[6] << 16)
| ((uint32_t)c->rxbuf[7] << 24);
fields_len = (uint32_t)c->rxbuf[12]
| ((uint32_t)c->rxbuf[13] << 8)
| ((uint32_t)c->rxbuf[14] << 16)
| ((uint32_t)c->rxbuf[15] << 24);
/* Bound the wire-supplied lengths before any arithmetic on
* them. Without this, fields_len = 0xFFFFFFF0 would wrap
* 16u + fields_len to near zero, bypass the total < rxbuf
* check, and trigger an out-of-bounds read. */
if (fields_len > sizeof(c->rxbuf) || body_len > sizeof(c->rxbuf))
return -1;
body_off = (uint32_t)ALIGN_UP(16u + fields_len, 8u);
total = body_off + body_len;
if (total > sizeof(c->rxbuf) || total < 16)
return -1;
if (read_full(c->fd, c->rxbuf + 16, total - 16) < 0)
return -1;
c->rxlen = total;
consumed = __msg_parse(c->rxbuf, c->rxlen, msg);
if (consumed <= 0)
return -1;
return 0;
}
static void publish_reply(link_client_t *c, const struct link_msg *m)
{
c->reply.type = m->type;
c->reply.signature = m->signature;
c->reply.error_name = m->error_name;
c->reply.path = m->path;
c->reply.interface = m->interface;
c->reply.member = m->member;
c->reply.body = m->body_avail ? m->body : NULL;
c->reply.body_len = m->body_avail;
c->have_reply = 1;
}
/* The reply view in c->reply points into c->rxbuf and is invalidated
* the moment we touch that buffer again -- clear it at every entry,
* even on the bad-args path, so link_client_reply() cannot return
* stale dangling pointers from a previous call. */
static void clear_reply(link_client_t *c)
{
if (!c)
return;
memset(&c->reply, 0, sizeof(c->reply));
c->have_reply = 0;
}
/* Wait up to timeout_ms (-1 = forever) for one full inbound frame
* and publish it. Returns 0 on success, 1 on timeout, -1 on error. */
static int read_and_publish(link_client_t *c, int timeout_ms)
{
struct link_msg msg;
if (timeout_ms >= 0) {
struct pollfd pfd = { .fd = c->fd, .events = POLLIN };
int rc;
do {
rc = poll(&pfd, 1, timeout_ms);
} while (rc < 0 && errno == EINTR);
if (rc < 0)
return -1;
if (rc == 0)
return 1;
}
if (read_one(c, &msg) < 0)
return -1;
publish_reply(c, &msg);
return 0;
}
int link_client_call(link_client_t *c,
const char *obj_path,
const char *interface,
const char *member,
const char *signature,
const uint8_t *body, size_t body_len)
{
/* Generous: Manager1 headers fit in ~150 B, but the buffer is
* shared with whatever future callers throw at us, and an
* overflow only manifests as a silent LINK_CALL_FAIL via
* __msg_build_method_call returning -1. 1 KiB on stack
* is cheap insurance. */
uint8_t hdr[1024];
ssize_t hlen;
uint32_t serial;
clear_reply(c);
if (!c || c->fd < 0 || !obj_path || !member)
return LINK_CALL_FAIL;
serial = c->next_serial++;
hlen = __msg_build_method_call(hdr, sizeof(hdr), serial,
obj_path, interface, member,
signature, (uint32_t)body_len);
if (hlen < 0)
return LINK_CALL_FAIL;
if (send_all(c->fd, hdr, (size_t)hlen) < 0)
return LINK_CALL_FAIL;
if (body_len > 0 && send_all(c->fd, body, body_len) < 0)
return LINK_CALL_FAIL;
if (read_and_publish(c, -1) != 0)
return LINK_CALL_FAIL;
if (c->reply.type == LINK_MSG_METHOD_RETURN)
return LINK_CALL_OK;
if (c->reply.type == LINK_MSG_ERROR)
return LINK_CALL_ERROR;
return LINK_CALL_FAIL;
}
const link_reply_t *link_client_reply(link_client_t *c)
{
if (!c || !c->have_reply)
return NULL;
return &c->reply;
}
/* Marshal varargs into `body` (capacity `cap`) according to `sig`.
* Returns the marshalled length on success, -1 on overflow or
* unsupported type code. */
static ssize_t marshal_va(uint8_t *body, size_t cap,
const char *sig, va_list ap)
{
link_writer_t w;
const char *s;
link_writer_init(&w, body, cap);
for (s = sig; *s; s++) {
switch (*s) {
case 'y':
link_w_byte(&w, (uint8_t)va_arg(ap, int));
break;
case 'b':
link_w_bool(&w, va_arg(ap, int));
break;
case 'u':
link_w_u32(&w, va_arg(ap, uint32_t));
break;
case 's':
link_w_string(&w, va_arg(ap, const char *));
break;
case 'o':
link_w_path(&w, va_arg(ap, const char *));
break;
default:
return -1;
}
}
return link_writer_finish(&w);
}
int link_client_call_v(link_client_t *c,
const char *obj_path,
const char *interface,
const char *member,
const char *signature, ...)
{
uint8_t body[1024];
ssize_t body_len = 0;
if (signature && *signature) {
va_list ap;
va_start(ap, signature);
body_len = marshal_va(body, sizeof(body), signature, ap);
va_end(ap);
if (body_len < 0)
return LINK_CALL_FAIL;
}
return link_client_call(c, obj_path, interface, member,
signature, body, (size_t)body_len);
}
int link_client_wait(link_client_t *c, int timeout_ms)
{
clear_reply(c);
if (!c || c->fd < 0)
return -1;
return read_and_publish(c, timeout_ms);
}
+119
View File
@@ -0,0 +1,119 @@
/* libink — per-connection lifecycle and dispatch entry point
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "internal.h"
int link_connection_get_fd(const link_connection_t *conn)
{
return conn ? conn->fd : -1;
}
uid_t link_connection_get_uid(const link_connection_t *conn)
{
return conn ? conn->peer_uid : (uid_t)-1;
}
void link_connection_close(link_connection_t *conn)
{
size_t i;
if (!conn)
return;
for (i = 0; i < conn->matches_count; i++)
__match_free(conn->matches[i]);
if (conn->fd >= 0)
close(conn->fd);
free(conn);
}
/* Process buffered binary D-Bus messages, dispatching each complete
* message and shifting consumed bytes out of rxbuf. Returns -1 if
* we should drop the connection (peer closed, protocol error,
* downstream send failure). */
static int process_binary(link_connection_t *conn)
{
while (conn->rxlen > 0) {
struct link_msg msg;
ssize_t consumed;
consumed = __msg_parse(conn->rxbuf, conn->rxlen, &msg);
if (consumed == 0)
break; /* incomplete; wait for more bytes */
if (consumed < 0)
return -1;
if (__dispatch_message(conn, &msg) < 0)
return -1;
memmove(conn->rxbuf, conn->rxbuf + consumed,
conn->rxlen - (size_t)consumed);
conn->rxlen -= (size_t)consumed;
}
return 0;
}
int link_connection_process(link_connection_t *conn)
{
if (!conn) {
errno = EINVAL;
return -1;
}
if (conn->auth == LINK_AUTH_FAILED)
return -1;
if (conn->auth != LINK_AUTH_DONE) {
if (__auth_process(conn) < 0)
return -1;
/* Still in SASL phase — wait for more bytes. */
if (conn->auth != LINK_AUTH_DONE)
return 0;
/* Fall through: BEGIN may have arrived in the same read
* as the first binary message. auth_process moved those
* bytes into rxbuf; they must be dispatched now, because
* no further wake-up is guaranteed (the kernel has
* already delivered everything that was readable). */
if (process_binary(conn) < 0)
return -1;
}
/* Read additional bytes and dispatch any complete messages.
* process_binary is called inside the loop after every
* successful read; no second call after EAGAIN because the
* buffer hasn't changed. */
for (;;) {
ssize_t n;
size_t room = sizeof(conn->rxbuf) - conn->rxlen;
if (room == 0) {
errno = E2BIG;
return -1;
}
n = read(conn->fd, conn->rxbuf + conn->rxlen, room);
if (n == 0)
return -1; /* peer closed */
if (n < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN || errno == EWOULDBLOCK)
return 0;
return -1;
}
conn->rxlen += (size_t)n;
if (process_binary(conn) < 0)
return -1;
}
}
+417
View File
@@ -0,0 +1,417 @@
/* libink — object tree, vtable registration, and method dispatch.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "internal.h"
/* ---------- object/vtable registration ---------- */
static struct link_object *find_object(link_server_t *srv, const char *path)
{
struct link_object *o;
TAILQ_FOREACH(o, &srv->objects, link)
if (strcmp(o->path, path) == 0)
return o;
return NULL;
}
int link_server_remove_object(link_server_t *srv, const char *path)
{
struct link_object *o;
struct link_vtable_entry *e;
if (!srv || !path) {
errno = EINVAL;
return -1;
}
o = find_object(srv, path);
if (!o) {
errno = ENOENT;
return -1;
}
while ((e = TAILQ_FIRST(&o->vtables))) {
TAILQ_REMOVE(&o->vtables, e, link);
free(e);
}
TAILQ_REMOVE(&srv->objects, o, link);
free(o);
return 0;
}
int link_server_add_object(link_server_t *srv, const char *path,
const link_vtable_t *vt, void *userdata)
{
struct link_object *o;
struct link_vtable_entry *e;
size_t plen;
if (!srv || !path || !*path || !vt || !vt->interface) {
errno = EINVAL;
return -1;
}
plen = strlen(path);
if (plen >= LINK_PATH_MAX) {
errno = ENAMETOOLONG;
return -1;
}
o = find_object(srv, path);
if (!o) {
o = calloc(1, sizeof(*o));
if (!o)
return -1;
memcpy(o->path, path, plen + 1);
TAILQ_INIT(&o->vtables);
TAILQ_INSERT_TAIL(&srv->objects, o, link);
}
e = calloc(1, sizeof(*e));
if (!e)
return -1;
e->vt = vt;
e->userdata = userdata;
TAILQ_INSERT_TAIL(&o->vtables, e, link);
return 0;
}
/* ---------- lookup ---------- */
static const link_method_t *find_method(const link_vtable_t *vt, const char *name)
{
const link_method_t *m;
if (!vt->methods)
return NULL;
for (m = vt->methods; m->name; m++)
if (strcmp(m->name, name) == 0)
return m;
return NULL;
}
/* If incoming.interface is NULL, search every interface on the
* object for a member with this name. Returns the matching method
* and writes back its vtable_entry in *out_e. */
static const link_method_t *resolve(struct link_object *o,
const char *iface, const char *member,
struct link_vtable_entry **out_e)
{
struct link_vtable_entry *e;
const link_method_t *m;
if (iface) {
TAILQ_FOREACH(e, &o->vtables, link) {
if (strcmp(e->vt->interface, iface) != 0)
continue;
m = find_method(e->vt, member);
if (m) {
*out_e = e;
return m;
}
return NULL;
}
return NULL;
}
TAILQ_FOREACH(e, &o->vtables, link) {
m = find_method(e->vt, member);
if (m) {
*out_e = e;
return m;
}
}
return NULL;
}
/* ---------- send helpers ---------- */
/*
* Peer fds are non-blocking, so send_all may fail mid-frame (e.g.
* EAGAIN from a peer that stopped draining its socket). Any failure
* poisons the peer's stream: never retry on the same connection,
* drop the peer.
*/
#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len))
int __send_method_return(link_connection_t *conn, const struct link_msg *req,
const char *out_sig,
const uint8_t *body, size_t body_len)
{
uint8_t hdr[512];
ssize_t hlen;
uint32_t serial = ++conn->next_serial;
hlen = __msg_build_return(hdr, sizeof(hdr), serial,
req->serial,
req->sender,
out_sig, (uint32_t)body_len);
if (hlen < 0) {
errno = EMSGSIZE;
return -1;
}
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
return -1;
if (body_len > 0 && send_all(conn->fd, body, body_len) < 0)
return -1;
return 0;
}
int link_connection_emit_signal(link_connection_t *conn,
const char *path,
const char *interface,
const char *member,
const char *signature,
const uint8_t *body, size_t body_len)
{
uint8_t hdr[512];
ssize_t hlen;
uint32_t serial;
size_t i;
int matched = 0;
if (!conn || !path || !interface || !member) {
errno = EINVAL;
return -1;
}
if (conn->auth != LINK_AUTH_DONE)
return 0; /* peer hasn't finished the SASL phase */
for (i = 0; i < conn->matches_count; i++) {
if (__match_matches(conn->matches[i], path,
interface, member)) {
matched = 1;
break;
}
}
if (!matched)
return 0; /* peer didn't subscribe — nothing to do */
serial = ++conn->next_serial;
hlen = __msg_build_signal(hdr, sizeof(hdr), serial,
path, interface, member,
signature, (uint32_t)body_len);
if (hlen < 0) {
errno = EMSGSIZE;
return -1;
}
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
return -1;
if (body_len > 0 && send_all(conn->fd, body, body_len) < 0)
return -1;
return 0;
}
int __send_error(link_connection_t *conn, const struct link_msg *req,
const char *error_name, const char *text)
{
uint8_t hdr[512];
uint8_t body[256];
ssize_t hlen;
size_t blen = 0;
uint32_t serial = ++conn->next_serial;
const char *sig = NULL;
if (text && *text) {
struct link_writer w;
ssize_t n;
__w_init(&w, body, sizeof(body));
__w_string(&w, text);
n = __w_finish(&w);
if (n < 0) {
errno = EMSGSIZE;
return -1;
}
blen = (size_t)n;
sig = "s";
}
hlen = __msg_build_error(hdr, sizeof(hdr), serial,
req->serial, req->sender,
error_name, sig, (uint32_t)blen);
if (hlen < 0) {
errno = EMSGSIZE;
return -1;
}
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
return -1;
if (blen > 0 && send_all(conn->fd, body, blen) < 0)
return -1;
return 0;
}
/* ---------- link_call public surface ---------- */
const char *link_call_path (const link_call_t *c) { return c ? c->incoming.path : NULL; }
const char *link_call_interface(const link_call_t *c) { return c ? c->incoming.interface : NULL; }
const char *link_call_member (const link_call_t *c) { return c ? c->incoming.member : NULL; }
uid_t link_call_uid (const link_call_t *c) { return c ? c->conn->peer_uid : (uid_t)-1; }
link_writer_t *link_call_reply(link_call_t *call)
{
if (!call || call->reply_consumed || call->error_sent)
return NULL;
call->reply_consumed = 1;
__w_init(&call->reply_writer,
call->conn->txbuf, sizeof(call->conn->txbuf));
return &call->reply_writer;
}
int link_call_reply_error(link_call_t *call, const char *name, const char *message)
{
if (!call || call->error_sent) {
errno = EINVAL;
return -1;
}
call->error_sent = 1;
return __send_error(call->conn, &call->incoming, name, message);
}
/* ---------- public reader wrappers ---------- */
int link_call_read_byte (link_call_t *c, uint8_t *o) { return __r_byte (&c->read_cursor, o); }
int link_call_read_bool (link_call_t *c, int *o) { return __r_bool (&c->read_cursor, o); }
int link_call_read_u32 (link_call_t *c, uint32_t *o) { return __r_u32 (&c->read_cursor, o); }
int link_call_read_string(link_call_t *c, const char **o) { return __r_string(&c->read_cursor, o); }
int link_call_read_path (link_call_t *c, const char **o) { return __r_path (&c->read_cursor, o); }
/* ---------- public writer wrappers ---------- */
void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap) { __w_init(w, buf, cap); }
ssize_t link_writer_finish(link_writer_t *w) { return __w_finish(w); }
void link_w_byte (link_writer_t *w, uint8_t v) { __w_byte(w, v); }
void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); }
void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); }
void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); }
void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); }
void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); }
void link_w_array_end (link_writer_t *w) { __w_array_end(w); }
void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); }
void link_w_struct_end (link_writer_t *w) { __w_struct_end(w); }
/* ---------- public reader wrappers ---------- */
void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len) { __r_init(r, body, len); }
int link_r_byte (link_reader_t *r, uint8_t *o) { return __r_byte (r, o); }
int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); }
int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); }
int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); }
int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); }
int link_r_done (const link_reader_t *r) { return __r_done (r); }
size_t link_r_pos (const link_reader_t *r) { return r->off; }
/* ---------- dispatch entry point ---------- */
int __dispatch_message(link_connection_t *conn, const struct link_msg *m)
{
struct link_object *o;
struct link_vtable_entry *e = NULL;
const link_method_t *meth;
struct link_call call;
ssize_t blen;
int rc;
if (m->type != LINK_MSG_METHOD_CALL) {
/* Signals and replies from a client to PID 1 are nonsense;
* silently drop. */
return 0;
}
if (!m->path || !m->member) {
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Method call without path or member");
}
/* Built-in DBus interfaces (Hello, Ping, Introspect, Properties)
* are handled here before object-tree lookup, which means they
* also run before the LINK_METHOD_PRIVILEGED authz gate further
* down. The current set is read-only; do NOT introduce a
* state-changing built-in without first adding equivalent
* authorisation inside __handle_builtin. */
rc = __handle_builtin(conn, m);
if (rc >= 0)
return rc; /* 0 = handled OK, 1 = built-in but failed; <0 = not a built-in */
o = find_object(conn->server, m->path);
if (!o) {
return __send_error(conn, m,
"org.freedesktop.DBus.Error.UnknownObject",
"No such object");
}
meth = resolve(o, m->interface, m->member, &e);
if (!meth) {
return __send_error(conn, m,
"org.freedesktop.DBus.Error.UnknownMethod",
"No such method on this object");
}
/* Validate signature: client must match the declared in_sig. */
{
const char *got = m->signature ? m->signature : "";
const char *want = meth->in_sig ? meth->in_sig : "";
if (strcmp(got, want) != 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Argument signature mismatch");
}
/* Per-method authorization. PRIVILEGED methods require uid 0;
* the peer's uid was captured via SO_PEERCRED at accept time
* and verified against the AUTH EXTERNAL claim, so we can trust
* conn->peer_uid here. */
if ((meth->flags & LINK_METHOD_PRIVILEGED) && conn->peer_uid != 0) {
return __send_error(conn, m,
"org.freedesktop.DBus.Error.AccessDenied",
"Method requires root privileges");
}
memset(&call, 0, sizeof(call));
call.conn = conn;
call.incoming = *m;
__r_init(&call.read_cursor, m->body, m->body_avail);
rc = meth->handler(&call, e->userdata);
if (rc < 0 && !call.reply_consumed && !call.error_sent) {
/* Handler returned an error without sending one. */
__send_error(conn, m,
"org.freedesktop.DBus.Error.Failed",
"Handler failed");
return 0;
}
if (!call.reply_consumed && !call.error_sent) {
/* Handler returned 0 but never produced a reply; treat as
* empty reply with out_sig "". */
__send_method_return(conn, m, NULL, NULL, 0);
return 0;
}
if (call.reply_consumed && !call.error_sent) {
blen = __w_finish(&call.reply_writer);
if (blen < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.Failed",
"Reply marshalling overflow");
return __send_method_return(conn, m, meth->out_sig,
conn->txbuf, (size_t)blen);
}
return 0;
}
+141
View File
@@ -0,0 +1,141 @@
/* libink internal types — not for external consumers.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#ifndef LIBINK_INTERNAL_H_
#define LIBINK_INTERNAL_H_
#include <stdint.h>
#include <sys/queue.h>
#include "link.h"
#include "marshal.h"
#include "proto.h"
typedef enum {
LINK_AUTH_NUL = 0,
LINK_AUTH_LINE,
LINK_AUTH_DONE,
LINK_AUTH_FAILED,
} link_auth_state_t;
#define LINK_PATH_MAX 108
#define LINK_AUTH_LINEBUF_SIZE 256
#define LINK_RX_BUF_SIZE (64 * 1024)
#define LINK_TX_BUF_SIZE (16 * 1024)
#define LINK_UNIQUE_NAME_LEN 16
#define LINK_MATCH_RULE_MAX 256 /* per-peer match rule cap */
#define LINK_MATCH_PEER_CAP 16 /* max active match rules per peer */
/* Per-vtable record attached to an object's interface list. */
struct link_vtable_entry {
const link_vtable_t *vt;
void *userdata;
TAILQ_ENTRY(link_vtable_entry) link;
};
TAILQ_HEAD(link_vtable_list, link_vtable_entry);
/* An object exposed at one path. */
struct link_object {
char path[LINK_PATH_MAX];
struct link_vtable_list vtables;
TAILQ_ENTRY(link_object) link;
};
TAILQ_HEAD(link_object_list, link_object);
struct link_server {
int fd;
char path[LINK_PATH_MAX];
struct link_object_list objects;
uint32_t next_unique_id; /* for ":1.N" names */
};
/* The reply being assembled inside a method handler.
*
* The reply body lives in conn->txbuf, not on this struct, so a
* stack-allocated link_call (in dispatch) stays small. Sharing the
* connection's txbuf is safe: the event loop is single-threaded and
* a connection only ever has one in-flight method call at a time. */
struct link_call {
link_connection_t *conn;
struct link_msg incoming;
struct link_reader read_cursor;
struct link_writer reply_writer; /* writes into conn->txbuf */
int reply_consumed;
int error_sent;
};
/* A parsed AddMatch rule. Fields are NULL when the rule omits the
* key, meaning "match anything"; non-NULL means "must equal". */
struct link_match {
char *raw; /* original string, for RemoveMatch */
char *type; /* "signal", or NULL */
char *interface;
char *member;
char *path;
};
struct link_connection {
int fd;
uid_t peer_uid;
char guid[33];
char unique_name[LINK_UNIQUE_NAME_LEN]; /* ":1.N" */
link_auth_state_t auth;
char linebuf[LINK_AUTH_LINEBUF_SIZE];
size_t linelen;
/* Match rules registered via org.freedesktop.DBus.AddMatch.
* Bounded for PID 1 hygiene; a peer that exceeds the cap gets
* a LimitsExceeded error reply. */
struct link_match *matches[LINK_MATCH_PEER_CAP];
size_t matches_count;
uint8_t rxbuf[LINK_RX_BUF_SIZE];
size_t rxlen;
/* Scratch for outgoing reply bodies. Shared by the dispatch
* path (writes through call.reply_writer) and built-in handlers
* (send_string_reply). Lifetime ends with each send_method_*
* call. */
uint8_t txbuf[LINK_TX_BUF_SIZE];
uint32_t next_serial;
struct link_server *server; /* back-pointer for dispatch */
};
/* io.c — shared EINTR-resilient I/O loops. */
int __io_write_all(int fd, const void *buf, size_t len);
int __io_read_full(int fd, void *buf, size_t len);
/* auth.c */
int __auth_process(link_connection_t *conn);
void __auth_generate_guid(char out[33]);
int __auth_client(int fd, uid_t uid);
/* dispatch.c */
int __dispatch_message(link_connection_t *conn, const struct link_msg *m);
int __send_error(link_connection_t *conn, const struct link_msg *req,
const char *error_name, const char *text);
int __send_method_return(link_connection_t *conn, const struct link_msg *req,
const char *out_sig,
const uint8_t *body, size_t body_len);
/* builtin.c */
int __handle_builtin(link_connection_t *conn, const struct link_msg *m);
/* match.c */
struct link_match *__match_parse (const char *rule);
void __match_free (struct link_match *m);
int __match_matches(const struct link_match *m,
const char *path, const char *iface,
const char *member);
int __match_add (link_connection_t *conn, const char *rule);
int __match_remove (link_connection_t *conn, const char *rule);
#endif /* LIBINK_INTERNAL_H_ */
+53
View File
@@ -0,0 +1,53 @@
/* libink — shared I/O helpers.
*
* Server send paths (libink/dispatch.c, libink/auth.c) and the
* client (libink/client.c) all need EINTR-resilient write_all /
* read_full. On any other error they return -1 with an unknown
* number of bytes already transferred.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <unistd.h>
#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;
}
+10
View File
@@ -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}
+295
View File
@@ -0,0 +1,295 @@
/* libink — brokerless D-Bus server library, born inside Finit
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef LIBINK_LINK_H_
#define LIBINK_LINK_H_
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct link_server link_server_t;
typedef struct link_connection link_connection_t;
typedef struct link_call link_call_t;
typedef struct link_client link_client_t;
/* D-Bus message type codes -- see link_reply_t.type. */
#define LINK_MSG_INVALID 0
#define LINK_MSG_METHOD_CALL 1
#define LINK_MSG_METHOD_RETURN 2
#define LINK_MSG_ERROR 3
#define LINK_MSG_SIGNAL 4
/* Writer is exposed so callers can stack-allocate one for marshalling
* signal/reply bodies. Treat the fields as opaque; use link_writer_init
* + the link_w_* helpers + link_writer_finish. Sized for typical D-Bus
* messages -- the array stack supports up to 8 levels of nesting. */
#define LINK_WRITER_MAX_NESTING 8
typedef struct link_writer {
uint8_t *buf;
size_t cap;
size_t off;
int err;
struct {
size_t lenpos;
size_t elemstart;
} arrays[LINK_WRITER_MAX_NESTING];
size_t array_depth;
} link_writer_t;
/* Reader is exposed so callers can stack-allocate one for decoding
* reply or signal bodies received from a peer. Treat fields as
* opaque; use link_reader_init + the link_r_* helpers. */
typedef struct link_reader {
const uint8_t *base;
size_t off;
size_t cap;
int err; /* sticky */
} link_reader_t;
/* View of an inbound message (method-return, error, or signal),
* populated by link_client_call(_v) and link_client_wait(), and
* returned by link_client_reply(). All pointers reference internal
* client storage and are invalidated by the next call or wait on the
* same client, or by link_client_close(). `body` is NULL iff
* body_len==0; `error_name` is non-NULL only when type == LINK_MSG_ERROR;
* `path`/`interface`/`member` are non-NULL on signals. */
typedef struct {
uint8_t type; /* LINK_MSG_METHOD_RETURN, _ERROR, or _SIGNAL */
const char *signature;
const char *error_name;
const char *path;
const char *interface;
const char *member;
const uint8_t *body;
size_t body_len;
} link_reply_t;
/* ---------- server / connection lifecycle ---------- */
int link_server_new (link_server_t **server, const char *path);
void link_server_free (link_server_t *server);
int link_server_get_fd(const link_server_t *server);
int link_server_accept(link_server_t *server, link_connection_t **conn);
int link_connection_get_fd (const link_connection_t *conn);
uid_t link_connection_get_uid (const link_connection_t *conn);
int link_connection_process (link_connection_t *conn);
void link_connection_close (link_connection_t *conn);
/* ---------- object registration ---------- */
typedef int (*link_method_fn)(link_call_t *call, void *userdata);
/* Method flags for link_method_t.flags */
#define LINK_METHOD_PRIVILEGED (1u << 0) /* peer must be uid 0 (root) */
typedef struct {
const char *name; /* member name */
const char *in_sig; /* input signature (D-Bus, e.g. "" or "s") */
const char *out_sig; /* output signature */
unsigned flags; /* OR of LINK_METHOD_* */
link_method_fn handler;
} link_method_t;
typedef struct {
const char *interface; /* e.g. "org.finit.Manager1" */
const link_method_t *methods; /* terminated by {NULL, ...} */
} link_vtable_t;
/* Register one (interface, methods) at `path`. Calling repeatedly
* with the same path and different vtables adds more interfaces at
* that object. The vtable pointer must outlive the server (typically
* a static table). */
int link_server_add_object(link_server_t *server, const char *path,
const link_vtable_t *vt, void *userdata);
/* Remove every vtable registered at `path` and free the object.
* Returns 0 if the object existed, -1 (errno=ENOENT) otherwise. */
int link_server_remove_object(link_server_t *server, const char *path);
/* ---------- call accessors ---------- */
const char *link_call_path (const link_call_t *call);
const char *link_call_interface(const link_call_t *call);
const char *link_call_member (const link_call_t *call);
uid_t link_call_uid (const link_call_t *call);
/* ---------- reading method-call arguments ----------
*
* Cursor starts at the beginning of the request body. Each
* function returns 0 on success and advances the cursor; on
* failure it returns -1 and leaves the cursor in an error state
* (subsequent reads also fail). Strings reference the
* connection's rx buffer and are valid for the duration of the
* method handler. */
int link_call_read_byte (link_call_t *call, uint8_t *out);
int link_call_read_bool (link_call_t *call, int *out);
int link_call_read_u32 (link_call_t *call, uint32_t *out);
int link_call_read_string(link_call_t *call, const char **out); /* "s" */
int link_call_read_path (link_call_t *call, const char **out); /* "o" */
/* ---------- reply construction ---------- */
/* Get the writer for the reply body, write args into it, return 0
* from the handler. Dispatch finalizes and sends the reply with
* the out_sig declared on the vtable. May be called once per
* call. */
link_writer_t *link_call_reply(link_call_t *call);
/* Send a D-Bus error reply. `name` must be a valid D-Bus error
* name (e.g. "org.freedesktop.DBus.Error.UnknownMethod"); `message`
* may be NULL. */
int link_call_reply_error(link_call_t *call, const char *name, const char *message);
/* ---------- signal emission ----------
*
* Send a signal to a single peer if its AddMatch rules accept it.
* Callers marshal the body separately and pass the resulting bytes.
* Returns 0 on success (or "filtered out, nothing sent"), -1 with
* errno set on failure: EMSGSIZE and EINVAL mean nothing hit the
* wire and the connection is still usable; anything else is a
* transport failure that may have left a partial frame -- the
* caller must drop the peer. */
int link_connection_emit_signal(link_connection_t *conn,
const char *path,
const char *interface,
const char *member,
const char *signature,
const uint8_t *body, size_t body_len);
/* ---------- client (outgoing method calls) ----------
*
* Connect, authenticate as the current effective uid, send BEGIN.
* Returns NULL on any failure (caller can fall back to another
* transport if it has one). */
link_client_t *link_client_open(const char *path);
void link_client_close(link_client_t *c);
/* Status codes returned by link_client_call(_v). */
#define LINK_CALL_OK 0 /* method-return received */
#define LINK_CALL_ERROR 1 /* server replied with an error */
#define LINK_CALL_FAIL (-1) /* transport, parse, or invalid-arg failure */
/* Send a METHOD_CALL and read the reply synchronously.
*
* `signature` and `body`/`body_len` describe the outgoing body --
* marshal it yourself with link_writer_init + the link_w_* helpers
* + link_writer_finish. Pass signature=NULL and body=NULL for
* methods that take no arguments.
*
* After the call, inspect the reply via link_client_reply() -- it
* exposes the body bytes (for callers that want to decode them with
* link_reader_init + link_r_*) and the error name on LINK_CALL_ERROR.
* The reply view is invalidated by the next call on the same client
* or by link_client_close(). */
int link_client_call(link_client_t *c,
const char *obj_path,
const char *interface,
const char *member,
const char *signature,
const uint8_t *body, size_t body_len);
/* Convenience wrapper that marshals the outgoing body from varargs
* matching `signature`. Supported type codes (one per arg):
* 'y' -> int (promoted uint8_t)
* 'b' -> int (0/non-zero)
* 'u' -> uint32_t
* 's' -> const char *
* 'o' -> const char * (object path)
*
* Pass signature=NULL or "" for void calls. Return value matches
* link_client_call; an unsupported type code returns LINK_CALL_FAIL
* with no message sent. */
int link_client_call_v(link_client_t *c,
const char *obj_path,
const char *interface,
const char *member,
const char *signature, ...);
const link_reply_t *link_client_reply(link_client_t *c);
/* Wait up to `timeout_ms` milliseconds for the next inbound message
* (typically a SIGNAL delivered after an AddMatch subscription), and
* populate the same view returned by link_client_reply().
* timeout_ms < 0 : block forever
* timeout_ms == 0 : non-blocking (returns 1 immediately if no data)
* timeout_ms > 0 : wait that long
* Returns 0 on success, 1 on timeout, -1 on transport/parse error.
*
* Note: the timeout gates only the wait for the first byte of the
* next frame. Once data starts arriving the rest of the message is
* read blockingly; callers that need a hard upper bound should pass
* a positive timeout AND have a watchdog at a higher level. */
int link_client_wait(link_client_t *c, int timeout_ms);
/* ---------- standalone writer ----------
*
* For marshalling bodies outside a method-call handler (signals,
* pre-computed replies). Initialise on a caller-owned buffer,
* write args via link_w_*, then call link_writer_finish which
* returns the body length or -1 on overflow. */
void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap);
ssize_t link_writer_finish(link_writer_t *w);
/* ---------- writer (mirrors the internal marshaller) ---------- */
void link_w_byte (link_writer_t *w, uint8_t v);
void link_w_bool (link_writer_t *w, int v);
void link_w_u32 (link_writer_t *w, uint32_t v);
void link_w_string (link_writer_t *w, const char *s); /* "s" */
void link_w_path (link_writer_t *w, const char *s); /* "o" */
void link_w_array_begin (link_writer_t *w, char element_sig);
void link_w_array_end (link_writer_t *w);
void link_w_struct_begin(link_writer_t *w);
void link_w_struct_end (link_writer_t *w);
/* ---------- standalone reader ----------
*
* For decoding bodies received off the wire (reply or signal).
* Initialise on the body pointer + length, read via link_r_*,
* check link_r_done() to confirm everything was consumed. */
void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len);
int link_r_byte (link_reader_t *r, uint8_t *out);
int link_r_bool (link_reader_t *r, int *out);
int link_r_u32 (link_reader_t *r, uint32_t *out);
int link_r_string (link_reader_t *r, const char **out); /* "s" */
int link_r_path (link_reader_t *r, const char **out); /* "o" */
int link_r_done (const link_reader_t *r);
/* Byte offset of the next read inside the original body buffer. Used
* to detect end-of-array when walking "a<T>" payloads: read the array
* byte-length prefix with link_r_u32 first, record (pos+length) as the
* end, then loop while link_r_pos < end. */
size_t link_r_pos (const link_reader_t *r);
#ifdef __cplusplus
}
#endif
#endif /* LIBINK_LINK_H_ */
+260
View File
@@ -0,0 +1,260 @@
/* libink — D-Bus body marshalling (writer side).
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <string.h>
#include <sys/types.h>
#include "marshal.h"
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
void __w_init(struct link_writer *w, uint8_t *buf, size_t cap)
{
w->buf = buf;
w->cap = cap;
w->off = 0;
w->err = 0;
w->array_depth = 0;
}
ssize_t __w_finish(struct link_writer *w)
{
if (w->err || w->array_depth != 0)
return -1;
return (ssize_t)w->off;
}
static int reserve(struct link_writer *w, size_t align, size_t bytes)
{
size_t pad;
if (w->err)
return -1;
pad = ALIGN_UP(w->off, align) - w->off;
if (w->off + pad + bytes > w->cap) {
w->err = 1;
return -1;
}
while (pad-- > 0)
w->buf[w->off++] = 0;
return 0;
}
static void put_u32_at(struct link_writer *w, size_t pos, uint32_t v)
{
w->buf[pos] = (uint8_t)(v & 0xff);
w->buf[pos + 1] = (uint8_t)((v >> 8) & 0xff);
w->buf[pos + 2] = (uint8_t)((v >> 16) & 0xff);
w->buf[pos + 3] = (uint8_t)((v >> 24) & 0xff);
}
static void put_u32(struct link_writer *w, uint32_t v)
{
put_u32_at(w, w->off, v);
w->off += 4;
}
void __w_byte(struct link_writer *w, uint8_t v)
{
if (reserve(w, 1, 1) < 0)
return;
w->buf[w->off++] = v;
}
void __w_bool(struct link_writer *w, int v)
{
if (reserve(w, 4, 4) < 0)
return;
put_u32(w, v ? 1u : 0u);
}
void __w_u32(struct link_writer *w, uint32_t v)
{
if (reserve(w, 4, 4) < 0)
return;
put_u32(w, v);
}
static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_len)
{
size_t len = s ? strlen(s) : 0;
if (onebyte_len) {
if (reserve(w, 1, 1 + len + 1) < 0)
return;
w->buf[w->off++] = (uint8_t)len;
} else {
if (reserve(w, 4, 4 + len + 1) < 0)
return;
put_u32(w, (uint32_t)len);
}
if (s && len)
memcpy(w->buf + w->off, s, len);
w->off += len;
w->buf[w->off++] = 0;
}
void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); }
void __w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); }
void __w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); }
static size_t element_align(char c)
{
switch (c) {
case 'y': case 'g': case 'v': return 1;
case 'n': case 'q': return 2;
case 'b': case 'i': case 'u':
case 's': case 'o': case 'h': case 'a': return 4;
case 'x': case 't': case 'd':
case '(': case '{': return 8;
default: return 1;
}
}
void __w_array_begin(struct link_writer *w, char element_sig_first_char)
{
size_t lenpos;
if (w->err)
return;
if (w->array_depth >= LINK_WRITER_MAX_NESTING) {
w->err = 1;
return;
}
if (reserve(w, 4, 4) < 0)
return;
lenpos = w->off;
put_u32(w, 0); /* placeholder */
/* Pad to the element's alignment. These pad bytes are NOT
* counted in the array length per the D-Bus spec. */
if (reserve(w, element_align(element_sig_first_char), 0) < 0)
return;
w->arrays[w->array_depth].lenpos = lenpos;
w->arrays[w->array_depth].elemstart = w->off;
w->array_depth++;
}
void __w_array_end(struct link_writer *w)
{
size_t elemstart, lenpos;
uint32_t actual;
if (w->err || w->array_depth == 0) {
w->err = 1;
return;
}
w->array_depth--;
lenpos = w->arrays[w->array_depth].lenpos;
elemstart = w->arrays[w->array_depth].elemstart;
actual = (uint32_t)(w->off - elemstart);
put_u32_at(w, lenpos, actual);
}
void __w_struct_begin(struct link_writer *w)
{
reserve(w, 8, 0);
}
void __w_struct_end(struct link_writer *w)
{
(void)w;
}
/* ---- reader ---- */
void __r_init(struct link_reader *r, const uint8_t *body, size_t len)
{
r->base = body;
r->off = 0;
r->cap = len;
r->err = 0;
}
static int r_skip_align(struct link_reader *r, size_t align)
{
size_t pad;
if (r->err)
return -1;
pad = ALIGN_UP(r->off, align) - r->off;
if (r->off + pad > r->cap) {
r->err = 1;
return -1;
}
r->off += pad;
return 0;
}
static uint32_t rd_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
int __r_byte(struct link_reader *r, uint8_t *out)
{
if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) {
r->err = 1;
return -1;
}
*out = r->base[r->off++];
return 0;
}
int __r_u32(struct link_reader *r, uint32_t *out)
{
if (r_skip_align(r, 4) < 0 || r->off + 4 > r->cap) {
r->err = 1;
return -1;
}
*out = rd_u32(r->base + r->off);
r->off += 4;
return 0;
}
int __r_bool(struct link_reader *r, int *out)
{
uint32_t v;
if (__r_u32(r, &v) < 0)
return -1;
*out = v ? 1 : 0;
return 0;
}
static int read_string_like(struct link_reader *r, const char **out)
{
uint32_t len;
if (__r_u32(r, &len) < 0)
return -1;
if (r->off + (size_t)len + 1 > r->cap) {
r->err = 1;
return -1;
}
/* Spec requires nul terminator at base[off + len]. */
if (r->base[r->off + len] != 0) {
r->err = 1;
return -1;
}
*out = (const char *)(r->base + r->off);
r->off += (size_t)len + 1;
return 0;
}
int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); }
int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); }
int __r_done(const struct link_reader *r)
{
return !r->err && r->off == r->cap;
}
+48
View File
@@ -0,0 +1,48 @@
/* libink — D-Bus body marshalling (writer side).
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#ifndef LIBINK_MARSHAL_H_
#define LIBINK_MARSHAL_H_
#include <stddef.h>
#include <stdint.h>
/* struct link_writer is defined in ink.h (public). Field layout is
* "opaque" per the public contract; this file's helpers manipulate
* the fields directly. */
#include "link.h"
void __w_init (struct link_writer *w, uint8_t *buf, size_t cap);
ssize_t __w_finish(struct link_writer *w);
void __w_byte (struct link_writer *w, uint8_t v);
void __w_bool (struct link_writer *w, int v);
void __w_u32 (struct link_writer *w, uint32_t v);
void __w_string (struct link_writer *w, const char *s); /* "s" */
void __w_path (struct link_writer *w, const char *s); /* "o" */
void __w_sig (struct link_writer *w, const char *s); /* "g" */
/* element_sig_first_char drives the alignment padding inserted
* between the array length prefix and the first element. */
void __w_array_begin (struct link_writer *w, char element_sig_first_char);
void __w_array_end (struct link_writer *w);
void __w_struct_begin(struct link_writer *w);
void __w_struct_end (struct link_writer *w);
/* ---- reader ----
*
* Reads from a message body pointer + length, advancing a cursor.
* struct link_reader is defined in link.h (public, opaque); the
* helpers here manipulate the fields directly. */
void __r_init (struct link_reader *r, const uint8_t *body, size_t len);
int __r_byte (struct link_reader *r, uint8_t *out);
int __r_bool (struct link_reader *r, int *out);
int __r_u32 (struct link_reader *r, uint32_t *out);
int __r_string(struct link_reader *r, const char **out); /* "s" */
int __r_path (struct link_reader *r, const char **out); /* "o" */
int __r_done (const struct link_reader *r);
#endif /* LIBINK_MARSHAL_H_ */
+199
View File
@@ -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 <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "internal.h"
static char *dup_range(const char *p, size_t n)
{
char *s = malloc(n + 1);
if (!s)
return NULL;
memcpy(s, p, n);
s[n] = '\0';
return s;
}
/* Parse one key='value' entry starting at *p. On success advances
* *p past the trailing quote and any comma, returns 0. On malformed
* input, returns -1. */
static int parse_kv(const char **p, char **out_key, char **out_value)
{
const char *q = *p;
const char *key_start, *val_start;
while (*q == ' ' || *q == '\t')
q++;
key_start = q;
while ((*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z') || *q == '_')
q++;
if (q == key_start || *q != '=')
return -1;
*out_key = dup_range(key_start, (size_t)(q - key_start));
if (!*out_key)
return -1;
q++;
if (*q != '\'') {
free(*out_key);
return -1;
}
q++;
val_start = q;
while (*q && *q != '\'')
q++;
if (*q != '\'') {
free(*out_key);
return -1;
}
*out_value = dup_range(val_start, (size_t)(q - val_start));
if (!*out_value) {
free(*out_key);
return -1;
}
q++;
while (*q == ' ' || *q == '\t' || *q == ',')
q++;
*p = q;
return 0;
}
struct link_match *__match_parse(const char *rule)
{
struct link_match *m;
const char *p;
if (!rule || strlen(rule) >= LINK_MATCH_RULE_MAX) {
errno = EINVAL;
return NULL;
}
m = calloc(1, sizeof(*m));
if (!m)
return NULL;
m->raw = strdup(rule);
if (!m->raw) {
free(m);
return NULL;
}
for (p = rule; *p; ) {
char *key = NULL, *value = NULL;
char **slot = NULL;
if (parse_kv(&p, &key, &value) < 0)
goto bad;
if (!strcmp(key, "type")) slot = &m->type;
else if (!strcmp(key, "interface")) slot = &m->interface;
else if (!strcmp(key, "member")) slot = &m->member;
else if (!strcmp(key, "path")) slot = &m->path;
else {
free(key);
free(value);
goto bad;
}
if (*slot) {
/* Duplicate key. */
free(key);
free(value);
goto bad;
}
*slot = value;
free(key);
}
/* No need to default m->type: when it's NULL the matcher below
* treats it as "match any", and the only thing libink emits via
* the match table is signals, so the effective filter is
* already "signal" without the explicit assignment. */
return m;
bad:
__match_free(m);
errno = EINVAL;
return NULL;
}
void __match_free(struct link_match *m)
{
if (!m)
return;
free(m->raw);
free(m->type);
free(m->interface);
free(m->member);
free(m->path);
free(m);
}
static int field_matches(const char *want, const char *got)
{
if (!want)
return 1; /* no filter on this field */
if (!got)
return 0;
return strcmp(want, got) == 0;
}
int __match_matches(const struct link_match *m,
const char *path, const char *iface,
const char *member)
{
/* Type filter: only signals get delivered through this path. */
if (m->type && strcmp(m->type, "signal") != 0)
return 0;
return field_matches(m->path, path)
&& field_matches(m->interface, iface)
&& field_matches(m->member, member);
}
int __match_add(link_connection_t *conn, const char *rule)
{
struct link_match *m;
if (conn->matches_count >= LINK_MATCH_PEER_CAP) {
errno = ENOSPC;
return -1;
}
m = __match_parse(rule);
if (!m)
return -1;
conn->matches[conn->matches_count++] = m;
return 0;
}
int __match_remove(link_connection_t *conn, const char *rule)
{
size_t i;
for (i = 0; i < conn->matches_count; i++) {
if (strcmp(conn->matches[i]->raw, rule) == 0) {
__match_free(conn->matches[i]);
conn->matches[i] = conn->matches[conn->matches_count - 1];
conn->matches_count--;
return 0;
}
}
errno = ENOENT;
return -1;
}
+43
View File
@@ -0,0 +1,43 @@
/* libink — D-Bus object-path encoding for arbitrary identifiers.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <stddef.h>
#include "path.h"
static int is_safe(unsigned char c)
{
return (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9');
}
int link_path_encode(const char *in, char *out, size_t outsz)
{
static const char hex[] = "0123456789abcdef";
size_t off = 0;
if (!in || !out || outsz == 0)
return -1;
for (; *in; in++) {
unsigned char c = (unsigned char)*in;
if (is_safe(c)) {
if (off + 1 >= outsz)
return -1;
out[off++] = (char)c;
} else {
if (off + 3 >= outsz)
return -1;
out[off++] = '_';
out[off++] = hex[c >> 4];
out[off++] = hex[c & 0xf];
}
}
out[off] = '\0';
return (int)off;
}
+21
View File
@@ -0,0 +1,21 @@
/* libink — D-Bus object-path encoding for arbitrary identifiers.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#ifndef LIBINK_PATH_H_
#define LIBINK_PATH_H_
#include <stddef.h>
/* Encode `in` as a D-Bus path segment. Bytes in [A-Za-z0-9] pass
* through unchanged; everything else (including '_' itself) becomes
* "_HH" with lowercase hex, mirroring systemd's escape_path.
*
* Returns the encoded length (excluding the terminating nul), or -1
* if the output buffer cannot fit the result. `outsz` must
* accommodate the encoded bytes plus a trailing nul; the worst-case
* size for an N-byte input is 3*N + 1. */
int link_path_encode(const char *in, char *out, size_t outsz);
#endif /* LIBINK_PATH_H_ */
+387
View File
@@ -0,0 +1,387 @@
/* libink — D-Bus wire protocol: message header parsing and building.
*
* Implements the binary message header format described in the
* D-Bus specification, sections "Message Format" and "Header Fields".
* Bodies are deliberately not parsed here — that's the marshaller's
* job (marshal.c).
*
* Native byte order is assumed to be little-endian; messages with the
* 'B' endianness flag are rejected for now (every conforming client
* on the platforms Finit targets sends 'l').
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <sys/types.h>
#include "proto.h"
#define HDR_FIXED_SIZE 16
#define MAX_MSG_SIZE (128 * 1024) /* sanity limit for PID 1 */
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
static inline uint32_t rd_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
static inline void wr_u32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v & 0xff);
p[1] = (uint8_t)((v >> 8) & 0xff);
p[2] = (uint8_t)((v >> 16) & 0xff);
p[3] = (uint8_t)((v >> 24) & 0xff);
}
/* Parse a (length-prefixed, nul-terminated) DBus STRING or PATH from
* the header field array. Returns a pointer into buf or NULL on
* malformed input. *consumed receives the bytes used including the
* nul. */
static const char *parse_string(const uint8_t *buf, size_t avail, size_t *consumed)
{
uint32_t len;
if (avail < 4)
return NULL;
len = rd_u32(buf);
if (len >= avail - 4) /* need room for len bytes + nul */
return NULL;
if (buf[4 + len] != 0)
return NULL;
*consumed = 4 + len + 1;
return (const char *)(buf + 4);
}
/* Parse a SIGNATURE (1-byte length, nul-terminated). */
static const char *parse_signature(const uint8_t *buf, size_t avail, size_t *consumed)
{
uint32_t len;
if (avail < 1)
return NULL;
len = buf[0];
if (len + 2 > avail)
return NULL;
if (buf[1 + len] != 0)
return NULL;
*consumed = 1 + len + 1;
return (const char *)(buf + 1);
}
ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out)
{
uint32_t fields_len, total_hdr, body_off, total;
const uint8_t *fp, *fend;
memset(out, 0, sizeof(*out));
if (len < HDR_FIXED_SIZE)
return 0;
if (buf[0] != 'l') {
errno = EPROTO;
return -1;
}
if (buf[3] != LINK_PROTOCOL_VERSION) {
errno = EPROTONOSUPPORT;
return -1;
}
out->endian = buf[0];
out->type = buf[1];
out->flags = buf[2];
out->body_len = rd_u32(buf + 4);
out->serial = rd_u32(buf + 8);
fields_len = rd_u32(buf + 12);
if (fields_len > MAX_MSG_SIZE || out->body_len > MAX_MSG_SIZE) {
errno = E2BIG;
return -1;
}
total_hdr = HDR_FIXED_SIZE + fields_len;
body_off = (uint32_t)ALIGN_UP(total_hdr, 8);
total = body_off + out->body_len;
if (len < total)
return 0; /* need more bytes */
/* Walk the array of (byte field-code, variant). */
fp = buf + HDR_FIXED_SIZE;
fend = fp + fields_len;
while (fp < fend) {
uint8_t code;
const char *vsig;
size_t used;
fp = buf + ALIGN_UP((size_t)(fp - buf), 8);
if (fp >= fend)
break;
code = *fp++;
vsig = parse_signature(fp, (size_t)(fend - fp), &used);
if (!vsig) {
errno = EPROTO;
return -1;
}
fp += used;
if (vsig[0] == 's' || vsig[0] == 'o') {
fp = buf + ALIGN_UP((size_t)(fp - buf), 4);
if (fp >= fend) { errno = EPROTO; return -1; }
const char *s = parse_string(fp, (size_t)(fend - fp), &used);
if (!s) { errno = EPROTO; return -1; }
switch (code) {
case LINK_HDR_PATH: out->path = s; break;
case LINK_HDR_INTERFACE: out->interface = s; break;
case LINK_HDR_MEMBER: out->member = s; break;
case LINK_HDR_ERROR_NAME: out->error_name = s; break;
case LINK_HDR_DESTINATION: out->destination = s; break;
case LINK_HDR_SENDER: out->sender = s; break;
}
fp += used;
} else if (vsig[0] == 'g') {
const char *s = parse_signature(fp, (size_t)(fend - fp), &used);
if (!s) { errno = EPROTO; return -1; }
if (code == LINK_HDR_SIGNATURE)
out->signature = s;
fp += used;
} else if (vsig[0] == 'u') {
fp = buf + ALIGN_UP((size_t)(fp - buf), 4);
if (fp + 4 > fend) { errno = EPROTO; return -1; }
uint32_t v = rd_u32(fp);
if (code == LINK_HDR_REPLY_SERIAL)
out->reply_serial = v;
fp += 4;
} else {
/* Unknown field type — skip whole message. */
errno = EPROTO;
return -1;
}
}
out->body = buf + body_off;
out->body_avail = out->body_len;
return (ssize_t)total;
}
/* ---------- builders ---------- */
/* Append a (byte field-code, variant) entry to a header-fields array,
* with the entry pre-aligned to 8 bytes. */
static int put_field_string(uint8_t *buf, size_t cap, size_t *off,
uint8_t code, char vsig_char,
const char *value)
{
size_t o = *off;
size_t pad = ALIGN_UP(o, 8) - o;
size_t len = strlen(value);
/* Padding for struct alignment */
while (pad-- > 0) {
if (o >= cap) return -1;
buf[o++] = 0;
}
/* code, variant signature (1B len + 1B char + 1B nul) */
if (o + 4 > cap) return -1;
buf[o++] = code;
buf[o++] = 1;
buf[o++] = (uint8_t)vsig_char;
buf[o++] = 0;
if (vsig_char == 's' || vsig_char == 'o') {
/* 4-byte align for u32 length */
while (o & 3) {
if (o >= cap) return -1;
buf[o++] = 0;
}
if (o + 4 + len + 1 > cap) return -1;
wr_u32(buf + o, (uint32_t)len);
o += 4;
memcpy(buf + o, value, len);
o += len;
buf[o++] = 0;
} else if (vsig_char == 'g') {
if (o + 1 + len + 1 > cap) return -1;
buf[o++] = (uint8_t)len;
memcpy(buf + o, value, len);
o += len;
buf[o++] = 0;
} else {
return -1;
}
*off = o;
return 0;
}
static int put_field_u32(uint8_t *buf, size_t cap, size_t *off,
uint8_t code, uint32_t value)
{
size_t o = *off;
size_t pad = ALIGN_UP(o, 8) - o;
while (pad-- > 0) {
if (o >= cap) return -1;
buf[o++] = 0;
}
if (o + 8 > cap) return -1;
buf[o++] = code;
buf[o++] = 1;
buf[o++] = 'u';
buf[o++] = 0;
while (o & 3) {
if (o >= cap) return -1;
buf[o++] = 0;
}
if (o + 4 > cap) return -1;
wr_u32(buf + o, value);
o += 4;
*off = o;
return 0;
}
static ssize_t finalize_header(uint8_t *buf, size_t cap,
uint8_t type, uint8_t flags,
uint32_t body_len, uint32_t serial,
size_t fields_end)
{
size_t hdr_end = fields_end;
size_t pad = ALIGN_UP(hdr_end, 8) - hdr_end;
buf[0] = 'l';
buf[1] = type;
buf[2] = flags;
buf[3] = LINK_PROTOCOL_VERSION;
wr_u32(buf + 4, body_len);
wr_u32(buf + 8, serial);
wr_u32(buf + 12, (uint32_t)(hdr_end - HDR_FIXED_SIZE));
while (pad-- > 0) {
if (hdr_end >= cap) return -1;
buf[hdr_end++] = 0;
}
return (ssize_t)hdr_end;
}
ssize_t __msg_build_return(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t reply_serial,
const char *destination,
const char *signature, uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE)
return -1;
if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0)
return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_METHOD_RETURN,
LINK_FLAG_NO_REPLY_EXPECTED,
body_len, serial, off);
}
ssize_t __msg_build_error(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t reply_serial,
const char *destination,
const char *error_name,
const char *signature, uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE || !error_name)
return -1;
if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_ERROR_NAME, 's', error_name) < 0)
return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_ERROR,
LINK_FLAG_NO_REPLY_EXPECTED,
body_len, serial, off);
}
ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *signature, uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE || !path || !interface || !member)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_SIGNAL,
LINK_FLAG_NO_REPLY_EXPECTED,
body_len, serial, off);
}
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *signature,
uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE || !path || !member)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0)
return -1;
if (interface &&
put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_METHOD_CALL,
/* flags=0: we expect a reply */
0,
body_len, serial, off);
}
size_t __msg_header_size(const struct link_msg *m)
{
(void)m;
/* Generous upper bound used by callers to size send buffers. */
return 512;
}
+101
View File
@@ -0,0 +1,101 @@
/* libink — D-Bus wire protocol: message header parsing and building.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#ifndef LIBINK_PROTO_H_
#define LIBINK_PROTO_H_
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include "link.h" /* LINK_MSG_* type codes */
/* Message flags. */
#define LINK_FLAG_NO_REPLY_EXPECTED 0x01
#define LINK_FLAG_NO_AUTO_START 0x02
#define LINK_FLAG_ALLOW_INTERACTIVE_AUTHORIZATION 0x04
/* Header field codes. */
#define LINK_HDR_PATH 1
#define LINK_HDR_INTERFACE 2
#define LINK_HDR_MEMBER 3
#define LINK_HDR_ERROR_NAME 4
#define LINK_HDR_REPLY_SERIAL 5
#define LINK_HDR_DESTINATION 6
#define LINK_HDR_SENDER 7
#define LINK_HDR_SIGNATURE 8
#define LINK_HDR_UNIX_FDS 9
#define LINK_PROTOCOL_VERSION 1
/* Parsed view of an incoming message. Pointers reference bytes
* inside the receiver's own rx buffer; treat as borrowed and short-
* lived (until the next read of the same connection). */
struct link_msg {
uint8_t type;
uint8_t flags;
uint8_t endian; /* 'l' or 'B' */
uint32_t body_len;
uint32_t serial;
uint32_t reply_serial;
const char *path; /* object path, or NULL */
const char *interface; /* may be NULL on method calls */
const char *member;
const char *error_name;
const char *destination;
const char *sender;
const char *signature; /* may be NULL if body is empty */
/* Pointer into the rx buffer and length, after header padding. */
const uint8_t *body;
uint32_t body_avail;
};
/* Parse a complete D-Bus message from `buf` of size `len`. On
* success returns the total number of bytes consumed (header +
* padding + body) and fills *out. Returns 0 if more bytes are
* needed, -1 on malformed input. */
ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out);
/* Compute the on-wire size of a future message header given the
* fields we'd populate. Used to size send buffers. */
size_t __msg_header_size(const struct link_msg *m);
/* Build a method-return header into `buf` (capacity `cap`).
* `reply_serial`/`destination` come from the call being replied to.
* `signature` is the body signature ("" if no args). `body_len`
* is the length of the body that will follow the header padding.
* Returns the number of bytes written, or -1 on overflow. */
ssize_t __msg_build_return(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t reply_serial,
const char *destination,
const char *signature, uint32_t body_len);
/* Build an error reply header. */
ssize_t __msg_build_error(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t reply_serial,
const char *destination,
const char *error_name,
const char *signature, uint32_t body_len);
/* Build a signal header (no reply expected, no destination). */
ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *signature, uint32_t body_len);
/* Build a method-call header (client side). */
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *signature,
uint32_t body_len);
#endif /* LIBINK_PROTO_H_ */
+162
View File
@@ -0,0 +1,162 @@
/* libink — listening socket, accept, peer-credential capture
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>
#include "internal.h"
static void close_save_errno(int fd)
{
int saved = errno;
close(fd);
errno = saved;
}
int link_server_new(link_server_t **out, const char *path)
{
struct sockaddr_un sun = { .sun_family = AF_UNIX };
link_server_t *srv;
size_t plen;
int fd;
if (!out || !path || !*path) {
errno = EINVAL;
return -1;
}
plen = strlen(path);
if (plen >= sizeof(sun.sun_path) || plen >= LINK_PATH_MAX) {
errno = ENAMETOOLONG;
return -1;
}
srv = calloc(1, sizeof(*srv));
if (!srv)
return -1;
TAILQ_INIT(&srv->objects);
fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0)
goto err_free;
memcpy(sun.sun_path, path, plen + 1);
(void)unlink(path);
/* fchmod() on a Unix-domain socket fd is a silent no-op on Linux:
* the file mode is fixed at bind() time as (0777 & ~umask). Set
* umask around the bind() so the socket appears with mode 0666
* atomically, no race window. World-accessible by design;
* per-method authorization happens later in dispatch via
* SO_PEERCRED. */
{
mode_t oldmask = umask(0111);
int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun));
int saved = errno;
umask(oldmask);
if (rc < 0) {
errno = saved;
goto err_close;
}
}
if (listen(fd, 16) < 0)
goto err_unlink;
srv->fd = fd;
memcpy(srv->path, path, plen + 1);
*out = srv;
return 0;
err_unlink:
(void)unlink(path);
err_close:
close_save_errno(fd);
err_free:
free(srv);
return -1;
}
void link_server_free(link_server_t *srv)
{
struct link_object *o;
if (!srv)
return;
o = TAILQ_FIRST(&srv->objects);
while (o) {
struct link_object *next_o = TAILQ_NEXT(o, link);
struct link_vtable_entry *e = TAILQ_FIRST(&o->vtables);
while (e) {
struct link_vtable_entry *next_e = TAILQ_NEXT(e, link);
free(e);
e = next_e;
}
free(o);
o = next_o;
}
if (srv->fd >= 0)
close(srv->fd);
if (srv->path[0])
(void)unlink(srv->path);
free(srv);
}
int link_server_get_fd(const link_server_t *srv)
{
if (!srv)
return -1;
return srv->fd;
}
int link_server_accept(link_server_t *srv, link_connection_t **out)
{
struct ucred cred = { 0 };
socklen_t credlen = sizeof(cred);
link_connection_t *conn;
int cfd;
if (!srv || !out) {
errno = EINVAL;
return -1;
}
cfd = accept4(srv->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (cfd < 0)
return -1;
conn = calloc(1, sizeof(*conn));
if (!conn) {
close_save_errno(cfd);
return -1;
}
conn->fd = cfd;
conn->auth = LINK_AUTH_NUL;
conn->server = srv;
if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0)
conn->peer_uid = cred.uid;
else
conn->peer_uid = (uid_t)-1;
__auth_generate_guid(conn->guid);
*out = conn;
return 0;
}
+11
View File
@@ -81,6 +81,9 @@ finit_SOURCES = api.c cgroup.c cgroup.h \
if LOGROTATE
finit_SOURCES += logrotate.c
endif
if DBUS
finit_SOURCES += dbus.c
endif
pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \
plugin.h svc.h service.h
@@ -94,6 +97,10 @@ finit_LDADD += ../plugins/libplug.la
else
finit_LDADD += -ldl
endif
if DBUS
finit_CPPFLAGS += -I$(top_srcdir)/libink
finit_LDADD += $(top_builddir)/libink/libink.la
endif
initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \
client.c client.h cond.c cond.h reboot.c \
@@ -101,6 +108,10 @@ initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \
initctl_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
initctl_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS)
initctl_LDADD = $(lite_LIBS) $(uev_LIBS)
if DBUS
initctl_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/libink
initctl_LDADD += $(top_builddir)/libink/libink.la
endif
INIT_LNKS = init telinit
REBOOT_LNKS = reboot shutdown halt poweroff suspend
+1 -32
View File
@@ -113,38 +113,7 @@ static int restart(svc_t *svc, void *user_data)
static int reload(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
if (svc_is_blocked(svc))
svc_start(svc);
else
service_timeout_cancel(svc);
/*
* Clear conditions before reload to ensure dependent services
* are properly updated. Only needed when the service does NOT
* support SIGHUP (noreload), because then it will be stopped
* and restarted, so conditions genuinely go away. When the
* service handles SIGHUP, its PID and pidfile persist, so the
* condition stays valid and dependents should not be disrupted.
*
* Note: only clear 'ready' for services where the pidfile
* inotify handler reasserts it (pid/none). For s6/systemd
* services readiness relies on their respective notification
* mechanism which may not re-trigger on SIGHUP.
*/
if (svc_is_noreload(svc)) {
svc_cond_clear(svc);
if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE)
service_ready(svc, 0);
}
svc_mark_dirty(svc);
service_step(svc);
return 0;
return service_reload(svc);
}
static int do_stop (char *buf, size_t len) { return call(stop, buf, len); }
+16
View File
@@ -37,6 +37,13 @@
#include "service.h"
#include "sm.h"
#ifdef HAVE_DBUS
/* Forward-declared locally to keep cond-w.c independent of the
* daemon's private.h (which pulls in svc/plugin headers). The full
* prototype lives in private.h for callers in finit's main loop. */
void dbus_notify_condition_change(const char *name, const char *state);
#endif
struct cond_boot {
TAILQ_ENTRY(cond_boot) link;
char *name;
@@ -353,6 +360,9 @@ void cond_set(const char *name)
if (cond_set_noupdate(name))
return;
#ifdef HAVE_DBUS
dbus_notify_condition_change(name, "on");
#endif
cond_update(name);
}
@@ -383,6 +393,9 @@ void cond_set_oneshot(const char *name)
if (cond_set_oneshot_noupdate(name))
return;
#ifdef HAVE_DBUS
dbus_notify_condition_change(name, "on");
#endif
cond_update(name);
}
@@ -404,6 +417,9 @@ void cond_clear(const char *name)
if (cond_clear_noupdate(name))
return;
#ifdef HAVE_DBUS
dbus_notify_condition_change(name, "off");
#endif
cond_update(name);
}
+869
View File
@@ -0,0 +1,869 @@
/* Finit-side glue between the event loop and libink.
*
* Owns the libink server, accepts new peers, drives each peer's
* state machine, and registers the Finit-specific D-Bus object
* tree (org.finit.Manager1 et al). Nothing in libink/ depends on
* finit-internal types: the boundary lives in this file, by design.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
*
* 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 <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include <uev/uev.h>
#include <ftw.h>
#include "link.h"
#include "path.h"
#include "finit.h"
#include "cond.h"
#include "conf.h"
#include "log.h"
#include "private.h"
#include "service.h"
#include "sig.h"
#include "sm.h"
#include "svc.h"
#define DBUS_MAX_PEERS 64
struct peer {
uev_t watcher;
link_connection_t *conn;
TAILQ_ENTRY(peer) link;
};
static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers);
static link_server_t *server;
static uev_t accept_watcher;
static size_t peer_count;
static void peer_drop(struct peer *p)
{
uev_io_stop(&p->watcher);
link_connection_close(p->conn);
TAILQ_REMOVE(&peers, p, link);
peer_count--;
free(p);
}
static void peer_cb(uev_t *w, void *arg, int events)
{
struct peer *p = arg;
(void)w;
if (UEV_ERROR == events) {
peer_drop(p);
return;
}
if (link_connection_process(p->conn) < 0)
peer_drop(p);
}
static void accept_cb(uev_t *w, void *arg, int events)
{
(void)arg;
if (UEV_ERROR == events) {
err(1, "D-Bus accept watcher error");
return;
}
for (;;) {
link_connection_t *conn = NULL;
struct peer *p;
if (link_server_accept(server, &conn) < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK)
err(1, "Failed accepting D-Bus client");
break;
}
if (peer_count >= DBUS_MAX_PEERS) {
logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping",
peer_count);
link_connection_close(conn);
continue;
}
p = calloc(1, sizeof(*p));
if (!p) {
link_connection_close(conn);
err(1, "Out of memory accepting D-Bus client");
break;
}
p->conn = conn;
TAILQ_INSERT_TAIL(&peers, p, link);
peer_count++;
if (uev_io_init(w->ctx, &p->watcher, peer_cb, p,
link_connection_get_fd(conn), UEV_READ)) {
err(1, "Failed registering D-Bus peer watcher");
peer_drop(p);
}
}
}
/* ---------- org.finit.Manager1 ---------- */
/* Forward decl + buffer-size constant — both consumed by Manager1
* handlers below, defined in the Service1 block further down. */
#define SERVICE_PATH_PREFIX "/org/finit/service/"
#define SERVICE_PATH_PREFIX_LEN (sizeof(SERVICE_PATH_PREFIX) - 1)
#define FINIT_SVC_PATH_MAX 512
static int service_path_for(svc_t *svc, char *buf, size_t bufsz);
static int manager_list_services(link_call_t *call, void *userdata)
{
link_writer_t *w;
svc_t *iter = NULL;
svc_t *svc;
(void)userdata;
w = link_call_reply(call);
if (!w)
return -1;
link_w_array_begin(w, 's');
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
char ident[MAX_IDENT_LEN];
svc_ident(svc, ident, sizeof(ident));
link_w_string(w, ident);
}
link_w_array_end(w);
return 0;
}
static int manager_get_service(link_call_t *call, void *userdata)
{
const char *ident;
svc_t *svc;
char path[FINIT_SVC_PATH_MAX];
link_writer_t *w;
(void)userdata;
if (link_call_read_string(call, &ident) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (s)");
svc = svc_find_by_str(ident);
if (!svc)
return link_call_reply_error(call,
"org.finit.Error.NoSuchService", ident);
if (service_path_for(svc, path, sizeof(path)) < 0)
return link_call_reply_error(call,
"org.finit.Error.Failed",
"Path encoding overflow");
w = link_call_reply(call);
if (!w)
return -1;
link_w_path(w, path);
return 0;
}
/* Service-control helpers used by Start/Stop/Restart/Reload. These
* mirror the static helpers in api.c — kept private here so api.c
* stays untouched in this increment. */
static int dbus_apply_stop(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_stop(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
}
static int dbus_apply_start(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_start(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
}
static int dbus_apply_restart(svc_t *svc, void *user_data)
{
if (!svc)
return 1;
if (!svc_is_running(svc))
return dbus_apply_start(svc, user_data);
service_timeout_cancel(svc);
service_stop(svc);
service_step(svc);
return 0;
}
struct dispatch_ctx {
int (*action)(svc_t *, void *);
int matched;
};
static int dispatch_found(svc_t *svc, void *udata)
{
struct dispatch_ctx *ctx = udata;
ctx->matched++;
return ctx->action(svc, NULL);
}
static int dispatch_missing(char *job, char *id, void *udata)
{
(void)job; (void)id; (void)udata;
return 0; /* don't penalise the return; we'll check ->matched */
}
/* Apply `action` to every service matched by `ident`. Returns 0 if
* at least one service matched and the action succeeded on all;
* -1 if no service matched the identity (caller sends NoSuchService). */
static int dispatch_action(const char *ident,
int (*action)(svc_t *, void *))
{
char buf[MAX_IDENT_LEN];
struct dispatch_ctx ctx = { .action = action };
int rc;
if (!ident || !*ident || strlen(ident) >= sizeof(buf))
return -1;
memcpy(buf, ident, strlen(ident) + 1);
rc = svc_parse_jobstr(buf, sizeof(buf), &ctx,
dispatch_found, dispatch_missing);
if (ctx.matched == 0)
return -1;
return rc;
}
static int manager_take_string_method(link_call_t *call,
int (*action)(svc_t *, void *))
{
const char *ident;
if (link_call_read_string(call, &ident) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (s)");
if (dispatch_action(ident, action) != 0)
return link_call_reply_error(call,
"org.finit.Error.NoSuchService", ident);
(void)link_call_reply(call); /* empty reply */
return 0;
}
static int manager_start (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); }
static int manager_stop (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); }
static int manager_restart(link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); }
static int manager_reload(link_call_t *call, void *userdata)
{
(void)userdata;
/*
* Same semantics as api.c: harmless no-op during bootstrap
* and shutdown, the client still sees success.
*/
if (IS_RESERVED_RUNLEVEL(runlevel))
warnx("Ignoring reload in runlevel S and 6/0.");
else
sm_reload();
(void)link_call_reply(call);
return 0;
}
static int manager_set_runlevel(link_call_t *call, void *userdata)
{
uint32_t lvl;
(void)userdata;
if (link_call_read_u32(call, &lvl) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (u)");
if (lvl > 9 || lvl == INIT_LEVEL)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"runlevel must be 0-9 (excluding internal levels)");
if (lvl == 0) halt = SHUT_OFF;
if (lvl == 6) halt = SHUT_REBOOT;
sm_runlevel((int)lvl);
(void)link_call_reply(call);
return 0;
}
static int dbus_shutdown(link_call_t *call, shutop_t target, int level)
{
if (IS_RESERVED_RUNLEVEL(runlevel))
return link_call_reply_error(call,
"org.finit.Error.WrongRunlevel",
"Already in shutdown");
halt = target;
sm_runlevel(level);
(void)link_call_reply(call);
return 0;
}
static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); }
static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); }
static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); }
static const link_method_t manager_methods[] = {
{ .name = "ListServices", .in_sig = "", .out_sig = "as",
.handler = manager_list_services },
{ .name = "GetService", .in_sig = "s", .out_sig = "o",
.handler = manager_get_service },
{ .name = "Start", .in_sig = "s", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_start },
{ .name = "Stop", .in_sig = "s", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_stop },
{ .name = "Restart", .in_sig = "s", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_restart },
{ .name = "Reload", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload },
{ .name = "SetRunlevel", .in_sig = "u", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel },
{ .name = "Reboot", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot },
{ .name = "Poweroff", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff },
{ .name = "Halt", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt },
{ NULL, NULL, NULL, 0, NULL }
};
static const link_vtable_t manager_vtable = {
.interface = "org.finit.Manager1",
.methods = manager_methods,
};
/* ---------- org.finit.Service1 (one object per service) ----------
*
* Per-service object at /org/finit/service/<encoded-identity>.
* The vtable's `userdata` is the svc_t * for the specific service.
* Registration is driven dynamically from svc_new()/svc_del() via
* dbus_register_service() / dbus_unregister_service() below. */
/* SERVICE_PATH_PREFIX / SERVICE_PATH_PREFIX_LEN / FINIT_SVC_PATH_MAX
* defined near the top of the file so Manager1.GetService can refer
* to them. */
static int service_action_method(link_call_t *call, void *userdata,
int (*action)(svc_t *, void *))
{
svc_t *svc = userdata;
if (!svc)
return link_call_reply_error(call,
"org.finit.Error.NoSuchService",
"Service object no longer valid");
action(svc, NULL);
(void)link_call_reply(call);
return 0;
}
static int service1_start (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); }
static int service1_stop (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); }
static int service1_restart(link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); }
static int service1_reload(link_call_t *call, void *userdata)
{
svc_t *svc = userdata;
if (!svc)
return link_call_reply_error(call,
"org.finit.Error.NoSuchService",
"Service object no longer valid");
service_reload(svc);
(void)link_call_reply(call);
return 0;
}
static const link_method_t service_methods[] = {
{ .name = "Start", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_start },
{ .name = "Stop", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_stop },
{ .name = "Restart", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_restart },
{ .name = "Reload", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_reload },
{ NULL, NULL, NULL, 0, NULL }
};
static const link_vtable_t service_vtable = {
.interface = "org.finit.Service1",
.methods = service_methods,
};
/* Build the canonical object path for a service. Identity is
* "name" for single-instance services, "name:id" otherwise. */
static int service_path_for(svc_t *svc, char *buf, size_t bufsz)
{
char ident[MAX_IDENT_LEN];
size_t plen = SERVICE_PATH_PREFIX_LEN;
int enc;
if (bufsz <= plen)
return -1;
memcpy(buf, SERVICE_PATH_PREFIX, plen);
svc_ident(svc, ident, sizeof(ident));
enc = link_path_encode(ident, buf + plen, bufsz - plen);
if (enc < 0)
return -1;
return (int)plen + enc;
}
void dbus_register_service(svc_t *svc)
{
char path[FINIT_SVC_PATH_MAX];
if (!server || !svc)
return;
if (service_path_for(svc, path, sizeof(path)) < 0)
return;
if (link_server_add_object(server, path, &service_vtable, svc) < 0)
logit(LOG_WARNING, "dbus: failed registering %s", path);
}
void dbus_unregister_service(svc_t *svc)
{
char path[FINIT_SVC_PATH_MAX];
if (!server || !svc)
return;
if (service_path_for(svc, path, sizeof(path)) < 0)
return;
(void)link_server_remove_object(server, path);
}
/* ---------- signal emission: ServiceStateChanged ---------- */
/*
* Coarse svc_state_t -> string. svc_status() in svc.h returns a
* richer string that also considers svc->block, but emitting just
* the state-machine state is enough for clients to track lifecycle
* transitions. Keep the strings stable -- they're a wire-API
* commitment once shipped.
*
* No `default:` on purpose: a new SVC_*_STATE added to svc.h must
* also pick a wire name here, and -Wall (-Wswitch) flags the
* missing case.
*/
static const char *state_name(svc_state_t s)
{
switch (s) {
case SVC_HALTED_STATE: return "halted";
case SVC_DONE_STATE: return "done";
case SVC_DEAD_STATE: return "dead";
case SVC_CLEANUP_STATE: return "cleanup";
case SVC_TEARDOWN_STATE: return "teardown";
case SVC_STOPPING_STATE: return "stopping";
case SVC_SETUP_STATE: return "setup";
case SVC_PAUSED_STATE: return "paused";
case SVC_WAITING_STATE: return "waiting";
case SVC_STARTING_STATE: return "starting";
case SVC_RUNNING_STATE: return "running";
}
return "unknown";
}
void dbus_notify_service_state(svc_t *svc, int old_state, int new_state)
{
uint8_t body[256];
link_writer_t w;
struct peer *p;
char ident[MAX_IDENT_LEN];
ssize_t blen;
svc_state_t o = (svc_state_t)old_state;
svc_state_t n = (svc_state_t)new_state;
if (!server || !svc)
return;
if (TAILQ_EMPTY(&peers))
return; /* nobody could possibly be listening */
svc_ident(svc, ident, sizeof(ident));
link_writer_init(&w, body, sizeof(body));
link_w_string(&w, ident);
link_w_string(&w, state_name(o));
link_w_string(&w, state_name(n));
blen = link_writer_finish(&w);
if (blen < 0)
return;
TAILQ_FOREACH(p, &peers, link)
(void)link_connection_emit_signal(p->conn,
"/org/finit/manager",
"org.finit.Manager1",
"ServiceStateChanged",
"sss",
body, (size_t)blen);
}
/* ---------- org.finit.Cond1 ---------- */
#define COND_PATH_OBJECT "/org/finit/cond"
#define COND_INTERFACE "org.finit.Cond1"
/* Cond1.Set/Clear refuse anything that isn't a usr/ condition --
* pid/, sys/, hook/ are owned by Finit's state machine and giving
* clients write access there would let them corrupt service state.
* Bare names ("foo") are normalised to "usr/foo" the same way
* initctl does. The returned pointer is valid for the duration
* of the caller's stack frame (`buf` must be at least 128 bytes). */
static const char *normalise_usr_cond(const char *name, char *buf, size_t bufsz)
{
const char *tail;
if (!name || !*name)
return NULL;
if (strchr(name, '.'))
return NULL;
if (strchr(name, '/')) {
if (strncmp(name, "usr/", 4) != 0)
return NULL;
tail = name + 4;
/* Match initctl's policy: no further slashes in the tail,
* and no empty tail ("usr/" alone). */
if (!*tail || strchr(tail, '/'))
return NULL;
if (strlen(name) >= bufsz)
return NULL;
memcpy(buf, name, strlen(name) + 1);
return buf;
}
if ((size_t)snprintf(buf, bufsz, "usr/%s", name) >= bufsz)
return NULL;
return buf;
}
/* Reject names that would escape /run/finit/cond/. cond_get(name)
* boils down to fopen(_PATH_COND + name), so without this check any
* caller can make PID 1 open arbitrary files -- a path traversal
* primitive that also stalls PID 1 if pointed at a FIFO or a slow
* device. Legal cond names look like "usr/foo", "pid/sshd",
* "service/keventd/ready"; no leading slash, no ".." segment. */
static int cond_name_valid(const char *name)
{
const char *p;
if (!name || !*name || *name == '/')
return 0;
for (p = name; *p; p++) {
if (*p == '.' && p[1] == '.' &&
(p[2] == '\0' || p[2] == '/'))
return 0;
}
return 1;
}
static int cond1_get(link_call_t *call, void *userdata)
{
const char *name;
link_writer_t *w;
(void)userdata;
if (link_call_read_string(call, &name) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (s)");
if (!cond_name_valid(name))
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"invalid condition name");
w = link_call_reply(call);
if (!w)
return -1;
link_w_string(w, condstr(cond_get(name)));
return 0;
}
static int cond1_set_or_clear(link_call_t *call, int do_set)
{
const char *name;
char buf[128];
const char *full;
if (link_call_read_string(call, &name) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (s)");
full = normalise_usr_cond(name, buf, sizeof(buf));
if (!full)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"Set/Clear is restricted to usr/* conditions");
if (do_set)
/* cond_set_oneshot, not cond_set: a user-asserted condition
* is a symlink to _PATH_RECONF, so it tracks the reconf
* generation automatically and stays "on" across reloads
* and runlevel switches. cond_set() writes a fixed
* generation that goes "flux" on the next reload -- wrong
* semantics for user conditions, and what initctl cond set
* has done forever via the filesystem path. */
cond_set_oneshot(full);
else
cond_clear(full);
(void)link_call_reply(call);
return 0;
}
static int cond1_set (link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); }
static int cond1_clear(link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 0); }
/* nftw() can't pass user data so a single static handle ferries the
* writer into the callback. Safe because dispatch is single-threaded. */
static link_writer_t *cond_walk_writer;
static int cond_walk_dump;
static int cond_walk_cb(const char *fpath, const struct stat *sb,
int tflag, struct FTW *ftwbuf)
{
const char *name;
const char *state;
size_t prefix_len;
(void)sb;
(void)ftwbuf;
if (tflag != FTW_F)
return 0;
if (!strcmp(fpath, _PATH_RECONF))
return 0;
prefix_len = strlen(_PATH_COND);
if (strlen(fpath) <= prefix_len)
return 0;
name = fpath + prefix_len;
if (cond_walk_dump) {
state = condstr(cond_get_path(fpath));
link_w_struct_begin(cond_walk_writer);
link_w_string(cond_walk_writer, name);
link_w_string(cond_walk_writer, state);
link_w_struct_end(cond_walk_writer);
} else {
link_w_string(cond_walk_writer, name);
}
return 0;
}
static int cond1_list(link_call_t *call, void *userdata)
{
link_writer_t *w;
(void)userdata;
w = link_call_reply(call);
if (!w)
return -1;
link_w_array_begin(w, 's');
cond_walk_writer = w;
cond_walk_dump = 0;
(void)nftw(_PATH_COND, cond_walk_cb, 20, 0);
cond_walk_writer = NULL;
link_w_array_end(w);
return 0;
}
static int cond1_dump(link_call_t *call, void *userdata)
{
link_writer_t *w;
(void)userdata;
w = link_call_reply(call);
if (!w)
return -1;
link_w_array_begin(w, '(');
cond_walk_writer = w;
cond_walk_dump = 1;
(void)nftw(_PATH_COND, cond_walk_cb, 20, 0);
cond_walk_writer = NULL;
link_w_array_end(w);
return 0;
}
static const link_method_t cond_methods[] = {
{ .name = "Get", .in_sig = "s", .out_sig = "s",
.handler = cond1_get },
{ .name = "Set", .in_sig = "s", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = cond1_set },
{ .name = "Clear", .in_sig = "s", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = cond1_clear },
{ .name = "List", .in_sig = "", .out_sig = "as",
.handler = cond1_list },
{ .name = "Dump", .in_sig = "", .out_sig = "a(ss)",
.handler = cond1_dump },
{ NULL, NULL, NULL, 0, NULL }
};
static const link_vtable_t cond_vtable = {
.interface = COND_INTERFACE,
.methods = cond_methods,
};
/* ---------- signal emission: ConditionChanged ---------- */
void dbus_notify_condition_change(const char *name, const char *state)
{
uint8_t body[256];
link_writer_t w;
struct peer *p;
ssize_t blen;
if (!server || !name || !state)
return;
if (TAILQ_EMPTY(&peers))
return;
link_writer_init(&w, body, sizeof(body));
link_w_string(&w, name);
link_w_string(&w, state);
blen = link_writer_finish(&w);
if (blen < 0)
return;
TAILQ_FOREACH(p, &peers, link)
(void)link_connection_emit_signal(p->conn,
COND_PATH_OBJECT,
COND_INTERFACE,
"ConditionChanged",
"ss",
body, (size_t)blen);
}
/* ---------- init / exit ---------- */
int dbus_init(uev_ctx_t *ctx)
{
dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET);
if (link_server_new(&server, FINIT_BUS_SOCKET) < 0) {
err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET);
return 1;
}
if (link_server_add_object(server, "/org/finit/manager",
&manager_vtable, NULL) < 0) {
err(1, "Failed registering Manager1 object");
link_server_free(server);
server = NULL;
return 1;
}
if (link_server_add_object(server, COND_PATH_OBJECT,
&cond_vtable, NULL) < 0) {
err(1, "Failed registering Cond1 object");
link_server_free(server);
server = NULL;
return 1;
}
if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL,
link_server_get_fd(server), UEV_READ)) {
err(1, "Failed registering D-Bus accept watcher");
link_server_free(server);
server = NULL;
return 1;
}
/* Register Service1 objects for every service already loaded.
* Subsequent svc_new()/svc_del() calls into
* dbus_register_service()/dbus_unregister_service(). */
{
svc_t *iter = NULL;
svc_t *svc;
for (svc = svc_iterator(&iter, 1); svc;
svc = svc_iterator(&iter, 0))
dbus_register_service(svc);
}
return 0;
}
int dbus_exit(void)
{
struct peer *p;
uev_io_stop(&accept_watcher);
while ((p = TAILQ_FIRST(&peers)))
peer_drop(p);
if (server) {
link_server_free(server);
server = NULL;
}
return 0;
}
#endif /* HAVE_DBUS */
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+5
View File
@@ -780,6 +780,11 @@ int main(int argc, char *argv[])
dbg("Starting initctl API responder ...");
api_init(&loop);
#ifdef HAVE_DBUS
dbg("Starting D-Bus listener ...");
dbus_init(&loop);
#endif
dbg("Starting service interval monitor ...");
service_init(&loop);
+1
View File
@@ -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
+130 -6
View File
@@ -268,19 +268,105 @@ static int do_startstop(int cmd, char *arg)
return do_svc(cmd, arg);
}
static int do_start (char *arg) { return do_startstop(INIT_CMD_START_SVC, arg); }
static int do_stop (char *arg) { return do_startstop(INIT_CMD_STOP_SVC, arg); }
#ifdef HAVE_DBUS
#include "link.h"
/* Try the D-Bus path for a Manager1 method. Returns:
* 0 succeeded via D-Bus
* 1 D-Bus replied with an error -- callers should error out
* -1 D-Bus not reachable -- callers should fall back to the
* legacy INIT_SOCKET transport
*
* On D-Bus error replies the function maps the org.* error name to
* the same exit code initctl historically printed for that case
* (e.g. NoSuchService -> 69 with the legacy message). */
static int try_dbus_manager(const char *method, const char *arg_sig,
const char *arg)
{
link_client_t *c;
const char *err;
int rc;
c = link_client_open(FINIT_BUS_SOCKET);
if (!c)
return -1;
/* "s" methods take the service identity (arg may be NULL ->
* empty string); void methods pass no body. */
if (arg_sig && !strcmp(arg_sig, "s"))
rc = link_client_call_v(c, "/org/finit/manager",
"org.finit.Manager1", method,
"s", arg ? arg : "");
else if (!arg_sig || !*arg_sig)
rc = link_client_call_v(c, "/org/finit/manager",
"org.finit.Manager1", method, NULL);
else
rc = LINK_CALL_FAIL;
if (rc == LINK_CALL_ERROR) {
const link_reply_t *r = link_client_reply(c);
err = (r && r->error_name) ? r->error_name : "";
/* Exact match on the fully-qualified error name; substring
* matching would misfire on a future name that contains
* one of these as a substring. */
if (!strcmp(err, "org.finit.Error.NoSuchService")) {
link_client_close(c);
ERRX(noerr ? 0 : 69, "no such task or service(s): %s",
arg ? arg : "");
}
if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) {
link_client_close(c);
ERRX(1, "permission denied: %s requires root", method);
}
link_client_close(c);
ERRX(1, "%s: %s", method, *err ? err : "D-Bus error");
}
link_client_close(c);
if (rc == LINK_CALL_OK)
return 0;
return -1; /* LINK_CALL_FAIL or anything else: fall back */
}
#endif /* HAVE_DBUS */
static int do_start (char *arg)
{
#ifdef HAVE_DBUS
int rc = try_dbus_manager("Start", "s", arg);
if (rc >= 0) return rc;
#endif
return do_startstop(INIT_CMD_START_SVC, arg);
}
static int do_stop (char *arg)
{
#ifdef HAVE_DBUS
int rc = try_dbus_manager("Stop", "s", arg);
if (rc >= 0) return rc;
#endif
return do_startstop(INIT_CMD_STOP_SVC, arg);
}
static int do_reload (char *arg)
{
if (!arg || !arg[0])
if (!arg || !arg[0]) {
#ifdef HAVE_DBUS
int rc = try_dbus_manager("Reload", "", NULL);
if (rc >= 0) return rc;
#endif
return do_svc(INIT_CMD_RELOAD, NULL);
}
return do_startstop(INIT_CMD_RELOAD_SVC, arg);
}
static int do_restart(char *arg)
{
#ifdef HAVE_DBUS
int rc = try_dbus_manager("Restart", "s", arg);
if (rc == 0) return 0;
if (rc == 1) ERRX(noerr ? 0 : 7, "failed restarting %s", arg);
#endif
if (do_startstop(INIT_CMD_RESTART_SVC, arg))
ERRX(noerr ? 0 : 7, "failed restarting %s", arg);
@@ -643,9 +729,47 @@ static int do_cmd(int cmd)
return 0;
}
int do_reboot (char *arg) { return do_cmd(INIT_CMD_REBOOT); }
int do_halt (char *arg) { return do_cmd(INIT_CMD_HALT); }
int do_poweroff(char *arg) { return do_cmd(INIT_CMD_POWEROFF); }
#ifdef HAVE_DBUS
static int do_reboot_dbus(const char *method)
{
int rc = try_dbus_manager(method, "", NULL);
if (rc == 0) {
sleep(5); /* match legacy: wait for finit to shut down */
return 0;
}
return rc; /* 1 = error, -1 = fall back */
}
#endif
int do_reboot (char *arg)
{
#ifdef HAVE_DBUS
int rc = do_reboot_dbus("Reboot");
if (rc >= 0) return rc;
#endif
return do_cmd(INIT_CMD_REBOOT);
}
int do_halt (char *arg)
{
#ifdef HAVE_DBUS
int rc = do_reboot_dbus("Halt");
if (rc >= 0) return rc;
#endif
return do_cmd(INIT_CMD_HALT);
}
int do_poweroff(char *arg)
{
#ifdef HAVE_DBUS
int rc = do_reboot_dbus("Poweroff");
if (rc >= 0) return rc;
#endif
return do_cmd(INIT_CMD_POWEROFF);
}
/* Suspend has no Manager1 equivalent yet; uses the legacy IPC. */
int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); }
/**
+9
View File
@@ -45,6 +45,15 @@ extern uev_ctx_t *ctx;
int api_init (uev_ctx_t *ctx);
int api_exit (void);
#ifdef HAVE_DBUS
int dbus_init (uev_ctx_t *ctx);
int dbus_exit (void);
void dbus_register_service (svc_t *svc);
void dbus_unregister_service (svc_t *svc);
void dbus_notify_service_state (svc_t *svc, int old_state, int new_state);
void dbus_notify_condition_change(const char *name, const char *state);
#endif
void conf_flush_events(void);
void service_monitor (pid_t lost, int status);
+59 -7
View File
@@ -1443,17 +1443,65 @@ int service_stop(svc_t *svc)
}
/**
* service_reload - Reload a service
* service_reload - Request reload of a service, driven by the state machine
* @svc: Service to reload
*
* This function does some basic checks of the runtime state of Finit
* and a sanity check of the @svc before sending %SIGHUP or calling
* the reload:script command.
* Mark a service dirty so the state machine will (re-)apply its
* configuration, then advance the state machine. For services that
* don't handle SIGHUP this also clears the readiness condition and
* the pidfile/none-notify readiness flag so dependents are properly
* notified once the service comes back.
*
* Returns:
* POSIX OK(0) on success, non-zero if @svc is NULL.
*/
int service_reload(svc_t *svc)
{
if (!svc)
return 1;
if (svc_is_blocked(svc))
svc_start(svc);
else
service_timeout_cancel(svc);
/*
* Clear conditions before reload to ensure dependent services
* are properly updated. Only needed when the service does NOT
* support SIGHUP (noreload), because then it will be stopped
* and restarted, so conditions genuinely go away. When the
* service handles SIGHUP, its PID and pidfile persist, so the
* condition stays valid and dependents should not be disrupted.
*
* Note: only clear 'ready' for services where the pidfile
* inotify handler reasserts it (pid/none). For s6/systemd
* services readiness relies on their respective notification
* mechanism which may not re-trigger on SIGHUP.
*/
if (svc_is_noreload(svc)) {
svc_cond_clear(svc);
if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE)
service_ready(svc, 0);
}
svc_mark_dirty(svc);
service_step(svc);
return 0;
}
/**
* service_reload_apply - Perform the actual reload of a running service
* @svc: Service to reload
*
* Low-level reload step: sends %SIGHUP or runs the reload:script
* command. Called by the state machine when a service marked
* reload-pending by service_reload() reaches the right state.
*
* Returns:
* POSIX OK(0) or non-zero on error.
*/
static int service_reload(svc_t *svc)
static int service_reload_apply(svc_t *svc)
{
const char *id = svc_ident(svc, NULL, 0);
int do_progress = 1;
@@ -3015,6 +3063,10 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state)
return;
*state = new_state;
#ifdef HAVE_DBUS
dbus_notify_service_state(svc, old_state, new_state);
#endif
/*
* The unit has stopped: HALTED comes after any post:/cleanup:
* script, DONE is a completed run/task, where remain-after-exit
@@ -3384,7 +3436,7 @@ restart:
if (sm_in_reload())
break;
service_reload(svc);
service_reload_apply(svc);
}
svc_mark_clean(svc);
@@ -3422,7 +3474,7 @@ restart:
if (svc_is_noreload(svc))
service_stop(svc);
else
service_reload(svc);
service_reload_apply(svc);
break;
}
+1
View File
@@ -59,6 +59,7 @@ void service_forked (svc_t *svc);
void service_ready (svc_t *svc, int ready);
int service_stop (svc_t *svc);
int service_reload (svc_t *svc);
int service_step (svc_t *svc);
void service_step_all (int types);
void service_worker (void *unused);
+9
View File
@@ -38,6 +38,7 @@
#include "finit.h"
#include "conf.h"
#include "private.h"
#include "svc.h"
#include "helpers.h"
#include "pid.h"
@@ -153,6 +154,10 @@ svc_t *svc_new(char *cmd, char *name, char *id, int type)
TAILQ_INSERT_TAIL(&svc_list, svc, link);
#ifdef HAVE_DBUS
dbus_register_service(svc);
#endif
return svc;
}
@@ -170,6 +175,10 @@ static struct wq work = {
*/
int svc_del(svc_t *svc)
{
#ifdef HAVE_DBUS
dbus_unregister_service(svc);
#endif
TAILQ_REMOVE(&svc_list, svc, link);
TAILQ_INSERT_TAIL(&gc_list, svc, link);
+4
View File
@@ -71,6 +71,7 @@ EXTRA_DIST += start-stop-serv.sh
EXTRA_DIST += signal-service.sh
EXTRA_DIST += testserv.sh
EXTRA_DIST += unexpected-restart.sh
EXTRA_DIST += dbus-auth.sh
AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/';
AM_TESTS_ENVIRONMENT += export SYSROOT;
@@ -126,6 +127,9 @@ if TESTSERV
TESTS += testserv.sh
endif
TESTS += unexpected-restart.sh
if DBUS
TESTS += dbus-auth.sh
endif
check-recursive: setup-chroot
+2 -2
View File
@@ -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
+315
View File
@@ -0,0 +1,315 @@
#!/bin/sh
# End-to-end smoke test for libink:
# - AUTH EXTERNAL handshake (happy and wrong-uid paths)
# - org.freedesktop.DBus.Hello
# - org.freedesktop.DBus.Introspectable.Introspect (root, manager)
# - org.finit.Manager1.ListServices
# - Error reply for an unknown method.
set -eu
TEST_DIR=$(dirname "$0")
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
CLIENT=/sbin/dbus-auth-client
BUS=/run/finit/bus
if ! texec test -x "$CLIENT"; then
skip "dbus-auth-client not built (configured with --disable-dbus?)"
fi
say "Wait for $BUS to appear"
retry "texec test -S $BUS"
say "Socket mode is 0666"
mode=$(texec stat -c %a "$BUS")
assert "Socket mode is 666 (got $mode)" "$mode" = "666"
# ---------- AUTH ----------
say "AUTH EXTERNAL: claim correct UID (root = 0)"
reply=$(texec "$CLIENT" auth "$BUS" 0)
assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK"
guid=${reply#OK }
assert "GUID is 32 hex chars (got: $guid)" \
"$(printf '%s' "$guid" | tr -d '0-9a-f' | wc -c)" -eq 0
assert "GUID length is 32 (got: ${#guid})" "${#guid}" -eq 32
say "AUTH EXTERNAL: wrong UID is rejected"
set +e
wrong_reply=$(texec "$CLIENT" auth "$BUS" 1)
wrong_rc=$?
set -e
assert "Wrong UID rejected (rc=$wrong_rc, reply: $wrong_reply)" \
"$wrong_rc" -eq 1
say "Two sequential AUTH connections get different GUIDs"
r1=$(texec "$CLIENT" auth "$BUS" 0)
r2=$(texec "$CLIENT" auth "$BUS" 0)
g1=${r1#OK }
g2=${r2#OK }
assert "Per-connection GUIDs differ ($g1 vs $g2)" "$g1" != "$g2"
# ---------- Built-in interfaces ----------
say "Hello() returns a unique name beginning with ':1.'"
name=$(texec "$CLIENT" hello "$BUS")
case "$name" in
:1.*) assert "Hello returned a :1.N name (got $name)" 0 -eq 0 ;;
*) fail "Hello returned unexpected name: $name" ;;
esac
say "Two Hello() calls produce different unique names"
n1=$(texec "$CLIENT" hello "$BUS")
n2=$(texec "$CLIENT" hello "$BUS")
assert "Unique names increment ($n1 vs $n2)" "$n1" != "$n2"
say "Introspect on root path returns valid XML referencing /manager"
xml=$(texec "$CLIENT" introspect "$BUS" /)
case "$xml" in
*'<node'*) assert "XML has <node> root (good)" 0 -eq 0 ;;
*) fail "Root introspect missing <node>: $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
+14 -1
View File
@@ -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 <wrapper>` 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
+1
View File
@@ -3,3 +3,4 @@
/.libs/
/.deps/
/serv
/dbus-auth-client
+5
View File
@@ -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
+861
View File
@@ -0,0 +1,861 @@
/* Minimal D-Bus client used by the libink smoke tests.
*
* Modes:
* dbus-auth-client auth <sock> <uid>
* Send the SASL handshake claiming <uid>; print server reply line.
* Exit 0 if reply begins "OK ", 1 if "REJECTED ", 2 otherwise.
*
* dbus-auth-client hello <sock>
* Auth as own uid; call org.freedesktop.DBus.Hello on
* /org/freedesktop/DBus. Print the assigned unique name.
*
* dbus-auth-client introspect <sock> <object-path>
* Auth + org.freedesktop.DBus.Introspectable.Introspect.
* Print the XML reply.
*
* dbus-auth-client liststrings <sock> <object-path> <interface> <method>
* Auth + method call expecting reply signature "as"; print one
* string per line.
*
* dbus-auth-client unknown <sock>
* 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 <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdint.h>
#include <poll.h>
static const char hex[] = "0123456789abcdef";
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
/* ---------- low level I/O ---------- */
static int write_all(int fd, const void *buf, size_t len)
{
const char *p = buf;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
p += n;
len -= (size_t)n;
}
return 0;
}
static int read_full(int fd, void *buf, size_t len)
{
char *p = buf;
while (len > 0) {
ssize_t n = read(fd, p, len);
if (n == 0) return -1;
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
p += n;
len -= (size_t)n;
}
return 0;
}
static int read_with_timeout(int fd, void *buf, size_t len, int timeout_ms)
{
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int rc;
for (;;) {
rc = poll(&pfd, 1, timeout_ms);
if (rc < 0) {
if (errno == EINTR)
continue;
return -1;
}
if (rc == 0)
return 0; /* timed out */
break;
}
return (int)read(fd, buf, len);
}
static ssize_t read_line(int fd, char *buf, size_t bufsz)
{
size_t off = 0;
while (off + 1 < bufsz) {
ssize_t n = read(fd, buf + off, 1);
if (n == 0) return -1;
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (buf[off] == '\n') {
buf[off] = '\0';
if (off > 0 && buf[off - 1] == '\r')
buf[--off] = '\0';
return (ssize_t)off;
}
off++;
}
return -1;
}
/* ---------- connect + AUTH ---------- */
static int connect_and_auth(const char *path, uid_t claimed_uid)
{
struct sockaddr_un sun = { .sun_family = AF_UNIX };
char uidstr[16];
char hexuid[32];
char line[64];
char reply[256];
size_t i, n;
int fd, rc;
if (strlen(path) >= sizeof(sun.sun_path))
return -1;
memcpy(sun.sun_path, path, strlen(path) + 1);
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
close(fd);
return -1;
}
n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)claimed_uid);
for (i = 0; i < n; i++) {
unsigned c = (unsigned char)uidstr[i];
hexuid[i * 2] = hex[c >> 4];
hexuid[i * 2 + 1] = hex[c & 0xf];
}
hexuid[n * 2] = '\0';
if (write_all(fd, "\0", 1) < 0) goto io;
rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid);
if (rc < 0 || (size_t)rc >= sizeof(line)) goto io;
if (write_all(fd, line, (size_t)rc) < 0) goto io;
if (read_line(fd, reply, sizeof(reply)) < 0) goto io;
if (strncmp(reply, "OK ", 3) != 0) {
fprintf(stderr, "auth failed: %s\n", reply);
close(fd);
return -1;
}
if (write_all(fd, "BEGIN\r\n", 7) < 0) goto io;
return fd;
io:
perror("auth handshake");
close(fd);
return -1;
}
/* ---------- D-Bus message build / parse ---------- */
struct buf {
uint8_t *p;
size_t cap;
size_t off;
int err;
};
static int b_reserve(struct buf *b, size_t align, size_t bytes)
{
size_t pad = ALIGN_UP(b->off, align) - b->off;
if (b->err || b->off + pad + bytes > b->cap) {
b->err = 1;
return -1;
}
while (pad--) b->p[b->off++] = 0;
return 0;
}
static void b_put_u32(struct buf *b, uint32_t v)
{
if (b_reserve(b, 4, 4) < 0) return;
b->p[b->off++] = (uint8_t)(v & 0xff);
b->p[b->off++] = (uint8_t)((v >> 8) & 0xff);
b->p[b->off++] = (uint8_t)((v >> 16) & 0xff);
b->p[b->off++] = (uint8_t)((v >> 24) & 0xff);
}
static void b_put_byte(struct buf *b, uint8_t v)
{
if (b_reserve(b, 1, 1) < 0) return;
b->p[b->off++] = v;
}
static void b_put_string(struct buf *b, const char *s)
{
size_t len = strlen(s);
if (b_reserve(b, 4, 4 + len + 1) < 0) return;
b_put_u32(b, (uint32_t)len);
memcpy(b->p + b->off, s, len);
b->off += len;
b->p[b->off++] = 0;
}
static void b_put_signature(struct buf *b, const char *s)
{
size_t len = strlen(s);
if (b_reserve(b, 1, 1 + len + 1) < 0) return;
b->p[b->off++] = (uint8_t)len;
memcpy(b->p + b->off, s, len);
b->off += len;
b->p[b->off++] = 0;
}
/* Send a method call with an optional argument.
* arg_sig == NULL or "" -> no body
* arg_sig == "s" -> arg_string used
* arg_sig == "u" -> arg_u32 used
*/
static int send_method_call_with_arg(int fd,
const char *path,
const char *interface,
const char *member,
const char *arg_sig,
const char *arg_string,
uint32_t arg_u32);
static int send_method_call(int fd,
const char *path,
const char *interface,
const char *member)
{
return send_method_call_with_arg(fd, path, interface, member,
NULL, NULL, 0);
}
static int send_method_call_with_arg(int fd,
const char *path,
const char *interface,
const char *member,
const char *arg_sig,
const char *arg_string,
uint32_t arg_u32)
{
uint8_t hdr[2048];
uint8_t body[1024];
struct buf b = { .p = hdr, .cap = sizeof(hdr) };
struct buf bb = { .p = body, .cap = sizeof(body) };
size_t fields_start, fields_end, padded_end;
uint32_t body_len = 0;
/* Build body first so its length and the signature are known
* before we write the header. */
if (arg_sig && *arg_sig) {
if (strcmp(arg_sig, "s") == 0) {
b_put_string(&bb, arg_string ? arg_string : "");
} else if (strcmp(arg_sig, "u") == 0) {
b_put_u32(&bb, arg_u32);
} else {
return -1;
}
if (bb.err) return -1;
body_len = (uint32_t)bb.off;
}
/* Fixed header */
memset(hdr, 0, 16);
hdr[0] = 'l';
hdr[1] = 1; /* METHOD_CALL */
hdr[2] = 0; /* flags */
hdr[3] = 1; /* protocol */
hdr[4] = (uint8_t)( body_len & 0xff);
hdr[5] = (uint8_t)((body_len >> 8) & 0xff);
hdr[6] = (uint8_t)((body_len >> 16) & 0xff);
hdr[7] = (uint8_t)((body_len >> 24) & 0xff);
hdr[8] = 1; /* serial */
b.off = 16;
fields_start = b.off;
/* PATH */
b_reserve(&b, 8, 0);
b_put_byte(&b, 1);
b_put_signature(&b, "o");
b_put_string(&b, path);
if (interface) {
b_reserve(&b, 8, 0);
b_put_byte(&b, 2);
b_put_signature(&b, "s");
b_put_string(&b, interface);
}
b_reserve(&b, 8, 0);
b_put_byte(&b, 3);
b_put_signature(&b, "s");
b_put_string(&b, member);
if (arg_sig && *arg_sig) {
b_reserve(&b, 8, 0);
b_put_byte(&b, 8);
b_put_signature(&b, "g");
/* SIGNATURE wire form: 1-byte len, bytes, nul */
b_put_byte(&b, (uint8_t)strlen(arg_sig));
if (b.off + strlen(arg_sig) + 1 > b.cap) return -1;
memcpy(b.p + b.off, arg_sig, strlen(arg_sig));
b.off += strlen(arg_sig);
b.p[b.off++] = 0;
}
fields_end = b.off;
{
uint32_t flen = (uint32_t)(fields_end - fields_start);
hdr[12] = (uint8_t)( flen & 0xff);
hdr[13] = (uint8_t)((flen >> 8) & 0xff);
hdr[14] = (uint8_t)((flen >> 16) & 0xff);
hdr[15] = (uint8_t)((flen >> 24) & 0xff);
}
padded_end = ALIGN_UP(fields_end, 8);
while (b.off < padded_end) hdr[b.off++] = 0;
if (b.err) return -1;
if (write_all(fd, hdr, b.off) < 0) return -1;
if (body_len > 0 && write_all(fd, body, body_len) < 0) return -1;
return 0;
}
/* Read one D-Bus message header + body into msg/body buffers.
* Returns 0 on success. Caller-supplied buffers must be large
* enough; we set them generously. */
struct reply {
uint8_t type;
uint32_t serial;
uint32_t body_len;
char signature[64];
char error_name[128];
char interface[128];
char member[128];
uint8_t body[8192];
};
/* Read one D-Bus message into *r.
* timeout_ms == 0 -> block forever waiting for the header byte
* timeout_ms > 0 -> wait that long for the header to start; once
* bytes arrive, the remainder of the frame is
* read without a timeout (it's "in flight").
* Returns 0 on success, -1 on EOF / parse error / timeout. */
static int read_reply(int fd, struct reply *r, int timeout_ms)
{
uint8_t hdr_fixed[16];
uint8_t hdr_fields[2048];
uint32_t fields_len;
size_t body_off;
size_t pos;
size_t off = 0;
memset(r, 0, sizeof(*r));
if (timeout_ms > 0) {
int n = read_with_timeout(fd, hdr_fixed, 1, timeout_ms);
if (n <= 0) return -1;
off = 1;
}
if (off < 16 && read_full(fd, hdr_fixed + off, 16 - off) < 0)
return -1;
if (hdr_fixed[0] != 'l') return -1;
r->type = hdr_fixed[1];
r->body_len = (uint32_t)hdr_fixed[4]
| ((uint32_t)hdr_fixed[5] << 8)
| ((uint32_t)hdr_fixed[6] << 16)
| ((uint32_t)hdr_fixed[7] << 24);
r->serial = (uint32_t)hdr_fixed[8]
| ((uint32_t)hdr_fixed[9] << 8)
| ((uint32_t)hdr_fixed[10] << 16)
| ((uint32_t)hdr_fixed[11] << 24);
fields_len = (uint32_t)hdr_fixed[12]
| ((uint32_t)hdr_fixed[13] << 8)
| ((uint32_t)hdr_fixed[14] << 16)
| ((uint32_t)hdr_fixed[15] << 24);
if (fields_len > sizeof(hdr_fields)) return -1;
if (read_full(fd, hdr_fields, fields_len) < 0) return -1;
body_off = (size_t)ALIGN_UP(16 + fields_len, 8);
if (body_off > 16 + fields_len) {
uint8_t pad[8];
if (read_full(fd, pad, body_off - 16 - fields_len) < 0)
return -1;
}
pos = 0;
while (pos < fields_len) {
uint8_t code;
size_t vsig_len;
const char *vsig;
pos = ALIGN_UP(pos, 8);
if (pos >= fields_len) break;
code = hdr_fields[pos++];
vsig_len = hdr_fields[pos++];
if (pos + vsig_len + 1 > fields_len) return -1;
vsig = (const char *)(hdr_fields + pos);
pos += vsig_len + 1;
if (vsig[0] == 's' || vsig[0] == 'o') {
uint32_t slen;
char *dst = NULL;
size_t dst_sz = 0;
pos = ALIGN_UP(pos, 4);
if (pos + 4 > fields_len) return -1;
slen = (uint32_t)hdr_fields[pos]
| ((uint32_t)hdr_fields[pos + 1] << 8)
| ((uint32_t)hdr_fields[pos + 2] << 16)
| ((uint32_t)hdr_fields[pos + 3] << 24);
pos += 4;
if (pos + slen + 1 > fields_len) return -1;
switch (code) {
case 2: dst = r->interface; dst_sz = sizeof(r->interface); break;
case 3: dst = r->member; dst_sz = sizeof(r->member); break;
case 4: dst = r->error_name; dst_sz = sizeof(r->error_name); break;
default: break;
}
if (dst && slen < dst_sz) {
memcpy(dst, hdr_fields + pos, slen);
dst[slen] = '\0';
}
pos += slen + 1;
} else if (vsig[0] == 'g') {
uint32_t slen = hdr_fields[pos++];
if (pos + slen + 1 > fields_len) return -1;
if (code == 8 && slen < sizeof(r->signature)) {
memcpy(r->signature, hdr_fields + pos, slen);
r->signature[slen] = '\0';
}
pos += slen + 1;
} else if (vsig[0] == 'u') {
pos = ALIGN_UP(pos, 4);
pos += 4;
} else {
return -1;
}
}
if (r->body_len > sizeof(r->body)) return -1;
if (r->body_len > 0 && read_full(fd, r->body, r->body_len) < 0)
return -1;
return 0;
}
/* Decode a body containing exactly one "s" or "o" -- the wire form
* is identical for both (u32 length + bytes + nul). */
static int decode_string(struct reply *r, char *out, size_t outsz)
{
uint32_t len;
if (r->body_len < 5)
return -1;
if (strcmp(r->signature, "s") != 0 && strcmp(r->signature, "o") != 0)
return -1;
len = (uint32_t)r->body[0]
| ((uint32_t)r->body[1] << 8)
| ((uint32_t)r->body[2] << 16)
| ((uint32_t)r->body[3] << 24);
if (4 + len + 1 > r->body_len) return -1;
if (len + 1 > outsz) return -1;
memcpy(out, r->body + 4, len);
out[len] = '\0';
return 0;
}
/* Decode a body with signature "as", print one string per line. */
static int decode_array_of_strings(struct reply *r)
{
uint32_t array_len;
size_t pos;
if (strcmp(r->signature, "as") != 0 || r->body_len < 4)
return -1;
array_len = (uint32_t)r->body[0]
| ((uint32_t)r->body[1] << 8)
| ((uint32_t)r->body[2] << 16)
| ((uint32_t)r->body[3] << 24);
pos = ALIGN_UP(4, 4);
if (pos + array_len > r->body_len) return -1;
while (pos < 4 + array_len) {
uint32_t slen;
pos = ALIGN_UP(pos, 4);
if (pos + 4 > r->body_len) return -1;
slen = (uint32_t)r->body[pos]
| ((uint32_t)r->body[pos + 1] << 8)
| ((uint32_t)r->body[pos + 2] << 16)
| ((uint32_t)r->body[pos + 3] << 24);
pos += 4;
if (pos + slen + 1 > r->body_len) return -1;
printf("%.*s\n", (int)slen, r->body + pos);
pos += slen + 1;
}
return 0;
}
/* ---------- modes ---------- */
static int mode_auth(int argc, char *argv[])
{
struct sockaddr_un sun = { .sun_family = AF_UNIX };
char hexuid[32], line[64], reply[256];
const char *path, *claimed;
size_t i, claimed_len, plen;
int fd, rc;
if (argc != 4) return 2;
path = argv[2];
claimed = argv[3];
plen = strlen(path);
if (plen >= sizeof(sun.sun_path)) return 2;
claimed_len = strlen(claimed);
if (claimed_len * 2 >= sizeof(hexuid)) return 2;
for (i = 0; i < claimed_len; i++) {
unsigned c = (unsigned char)claimed[i];
hexuid[i * 2] = hex[c >> 4];
hexuid[i * 2 + 1] = hex[c & 0xf];
}
hexuid[claimed_len * 2] = '\0';
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return 2; }
memcpy(sun.sun_path, path, plen + 1);
if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
perror("connect"); close(fd); return 2;
}
if (write_all(fd, "\0", 1) < 0) { close(fd); return 2; }
rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid);
if (rc < 0 || (size_t)rc >= sizeof(line)) { close(fd); return 2; }
if (write_all(fd, line, (size_t)rc) < 0) { close(fd); return 2; }
if (read_line(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; }
printf("%s\n", reply);
close(fd);
if (strncmp(reply, "OK ", 3) == 0) return 0;
if (strncmp(reply, "REJECTED ", 9) == 0) return 1;
return 2;
}
static int do_call_arg(const char *path, const char *obj_path,
const char *iface, const char *method,
const char *arg_sig, const char *arg_string,
uint32_t arg_u32, struct reply *r)
{
int fd = connect_and_auth(path, getuid());
if (fd < 0) return 2;
if (send_method_call_with_arg(fd, obj_path, iface, method,
arg_sig, arg_string, arg_u32) < 0) {
fprintf(stderr, "send: %s\n", strerror(errno));
close(fd);
return 2;
}
if (read_reply(fd, r, 0) < 0) {
fprintf(stderr, "read_reply\n");
close(fd);
return 2;
}
close(fd);
if (r->type == 3) {
fprintf(stderr, "ERROR: %s\n", r->error_name);
return 1;
}
return 0;
}
static int do_call(const char *path, const char *obj_path,
const char *iface, const char *method,
struct reply *r)
{
return do_call_arg(path, obj_path, iface, method, NULL, NULL, 0, r);
}
static int mode_hello(int argc, char *argv[])
{
struct reply r;
char name[256];
int rc;
if (argc != 3) return 2;
rc = do_call(argv[2], "/org/freedesktop/DBus",
"org.freedesktop.DBus", "Hello", &r);
if (rc != 0) return rc;
if (decode_string(&r, name, sizeof(name)) < 0) return 2;
printf("%s\n", name);
return 0;
}
static int mode_introspect(int argc, char *argv[])
{
struct reply r;
char xml[8192];
int rc;
if (argc != 4) return 2;
rc = do_call(argv[2], argv[3],
"org.freedesktop.DBus.Introspectable", "Introspect", &r);
if (rc != 0) return rc;
if (decode_string(&r, xml, sizeof(xml)) < 0) return 2;
printf("%s\n", xml);
return 0;
}
static int mode_liststrings(int argc, char *argv[])
{
struct reply r;
int rc;
if (argc != 6) return 2;
rc = do_call(argv[2], argv[3], argv[4], argv[5], &r);
if (rc != 0) return rc;
if (decode_array_of_strings(&r) < 0) return 2;
return 0;
}
/* call-s: method taking one string arg, void/error reply.
* call-void: method taking no args, void/error reply. */
static int mode_call_s(int argc, char *argv[])
{
struct reply r;
int rc;
if (argc != 7) return 2;
rc = do_call_arg(argv[2], argv[3], argv[4], argv[5],
"s", argv[6], 0, &r);
if (rc == 0)
printf("OK\n");
return rc;
}
static int mode_call_void(int argc, char *argv[])
{
struct reply r;
int rc;
if (argc != 6) return 2;
rc = do_call_arg(argv[2], argv[3], argv[4], argv[5],
NULL, NULL, 0, &r);
if (rc == 0)
printf("OK\n");
return rc;
}
/* get-service <sock> <identity>
*
* Calls Manager1.GetService(identity) and prints the returned
* object path. Exit 0 on success, 1 on server error, 2 transport. */
static int mode_get_service(int argc, char *argv[])
{
struct reply r;
char path[256];
int rc;
if (argc != 4) return 2;
rc = do_call_arg(argv[2], "/org/finit/manager",
"org.finit.Manager1", "GetService",
"s", argv[3], 0, &r);
if (rc != 0) return rc;
/* decode_string accepts both "s" and "o" — wire form is
* identical; no need to pre-check the signature here. */
if (decode_string(&r, path, sizeof(path)) < 0)
return 2;
printf("%s\n", path);
return 0;
}
/* Drop effective uid to argv[2], parsed as decimal. Returns 0 on
* success, 2 (the program's "transport error" code) on failure. */
static int drop_uid_from_arg(const char *uid_arg, const char *progname)
{
uid_t drop_to;
char *ep = NULL;
long v;
errno = 0;
v = strtol(uid_arg, &ep, 10);
if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) {
fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg);
return 2;
}
drop_to = (uid_t)v;
if (setuid(drop_to) < 0) {
perror("setuid");
return 2;
}
return 0;
}
/* monitor-signal <sock> <match-rule> <timeout-ms>
*
* 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 <iface> <member>"
* 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 <uid> <sock> <obj> <iface> <method> <arg>
*
* Drops effective uid to <uid> (must work inside the test
* namespace where additional uids are mapped) before connecting,
* so AUTH EXTERNAL captures <uid> 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 <uid> <sock> <obj> <iface> <method> */
static int mode_call_void_as_uid(int argc, char *argv[])
{
struct reply r;
int rc;
if (argc != 7) return 2;
if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0)
return rc;
rc = do_call_arg(argv[3], argv[4], argv[5], argv[6],
NULL, NULL, 0, &r);
if (rc == 0)
printf("OK\n");
return rc;
}
static int mode_unknown(int argc, char *argv[])
{
struct reply r;
int rc;
if (argc != 3) return 2;
rc = do_call(argv[2], "/org/finit/manager",
"org.finit.Manager1", "NotARealMethod", &r);
if (rc == 1 && strstr(r.error_name, "org.freedesktop.DBus.Error.") == r.error_name)
return 0;
if (rc == 1)
return 1;
return 2;
}
int main(int argc, char *argv[])
{
if (argc < 2) return 2;
if (strcmp(argv[1], "auth") == 0) return mode_auth(argc, argv);
if (strcmp(argv[1], "hello") == 0) return mode_hello(argc, argv);
if (strcmp(argv[1], "introspect") == 0) return mode_introspect(argc, argv);
if (strcmp(argv[1], "liststrings") == 0) return mode_liststrings(argc, argv);
if (strcmp(argv[1], "call-s") == 0) return mode_call_s(argc, argv);
if (strcmp(argv[1], "call-void") == 0) return mode_call_void(argc, argv);
if (strcmp(argv[1], "monitor-signal") == 0) return mode_monitor_signal(argc, argv);
if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv);
if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv);
if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv);
if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv);
fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]);
return 2;
}