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

+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;
}