libink: a message bus is not a peer

libink was written against the only bus it had, its own, where the
peer on the other end is the client.  A broker is not: it routes for
senders it names itself, expects a DESTINATION on anything addressed
through it, and answers on its own schedule rather than next.

Runlevels go on the wire as S and N rather than the digits Finit
keeps internally, since that is what a caller outside Finit means by
one.

The library stays a convenience library, linked into finit and
initctl and installed nowhere: the ABI promise waits until libink is
its own project.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-08-13 09:28:26 +02:00
1 parent 6310d9e760
commit b55dada80b
16 files changed
+340 -250

No files matched your search

-1
View File
@@ -3,6 +3,5 @@
.dirstamp
*.lo
libink.*
!libink.pc.in
Makefile
Makefile.in
+7 -11
View File
@@ -1,5 +1,10 @@
# libink — brokerless D-Bus library (server + client), born inside Finit
lib_LTLIBRARIES = libink.la
#
# Internal convenience library: linked statically into finit and
# initctl, nothing installed. Public install (and the ABI promise
# that comes with it) is deferred until libink is extracted into its
# own project.
noinst_LTLIBRARIES = libink.la
libink_la_SOURCES = server.c auth.c connection.c \
proto.c proto.h \
marshal.c marshal.h \
@@ -7,16 +12,7 @@ libink_la_SOURCES = server.c auth.c connection.c \
match.c \
path.c \
client.c io.c \
internal.h
link.h path.h internal.h
libink_la_LDFLAGS = -version-info 0:0:0
libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE
libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
# pkg-config support
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libink.pc
# Public headers install to $(includedir)/ink/
inkdir = $(includedir)/ink
ink_HEADERS = link.h path.h
+64
View File
@@ -0,0 +1,64 @@
libink — brokerless D-Bus for Finit
===================================
libink is a small C library implementing the [D-Bus wire protocol][spec],
both the server and the client side, without a broker and without any
dependency on `libdbus`, `sd-bus`, or GIO. It was born inside Finit to
let PID 1 be a bus of its own: clients connect straight to the listening
socket, authenticate with the standard SASL EXTERNAL handshake, and get
kernel-authenticated credentials via `SO_PEERCRED`.
For what the bus exposes and how to talk to it, see the User Guide,
[D-Bus Integration](../doc/dbus.md). This file covers the library
itself.
Status
------
libink is an internal implementation detail of Finit: built as a libtool
convenience library, linked statically into `finit` and `initctl`,
nothing installed. There is deliberately no ABI promise yet — that
comes if/when libink is extracted into a project of its own. External
D-Bus clients need none of this; the wire protocol is the compatibility
surface, any standard D-Bus library works.
Layout
------
| File | Contents |
|-----------------|-------------------------------------------------------|
| `server.c` | Listening socket, accept, peer credential capture |
| `auth.c` | SASL EXTERNAL handshake, uid verification |
| `connection.c` | Per-peer state machine, message framing |
| `proto.c` | Wire header parse/build |
| `marshal.c` | Body (de)marshalling: basic types, arrays, variants |
| `dispatch.c` | Object tree, vtable registration, method dispatch |
| `builtin.c` | `org.freedesktop.DBus.*` stock interfaces |
| `match.c` | AddMatch/RemoveMatch rule parsing and signal filter |
| `path.c` | systemd-style `_HH` object path encoding |
| `client.c` | Outgoing connections, method calls, reply/signal wait |
| `io.c` | Shared EINTR-resilient read/write loops |
Public API symbols carry the `link_*` prefix (`link.h`), internal ones
`__*` (`internal.h`). Method handlers are registered as vtables of
`link_method_t`/`link_property_t`; the framework emits variant
signatures from the property table so the declared type is the single
source of truth.
The boundary to Finit is deliberate: nothing under `libink/` includes a
Finit header. All glue lives in `src/dbus.c` — object registration,
signal emission from the service/condition/runlevel hook points, and
the uev event loop bridge. `initctl` uses the client half of the same
library, so one wire-format implementation serves both ends. If libink
is ever spun out, that file is the cut line.
Testing
-------
The `test/dbus-*.sh` suite exercises the library end to end against a
live Finit in a namespace, driven by `test/src/dbus-auth-client.c`.
Wire-format conformance against third-party tools (`dbus-send`,
`dbus-monitor`) and fuzzing of the parsers are tracked as pre-merge
work — this is PID 1's attack surface.
[spec]: https://dbus.freedesktop.org/doc/dbus-specification.html
+44 -1
View File
@@ -24,6 +24,7 @@
struct link_client {
int fd;
uint32_t next_serial;
const char *destination; /* not owned; NULL when brokerless */
link_reply_t reply; /* most recent reply view (points into rxbuf) */
/* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which
* is a wire-valid (if malformed) type, so we need an out-of-band
@@ -87,6 +88,12 @@ link_client_t *link_client_open(const char *path)
return link_client_open_timeout(path, 0);
}
void link_client_set_destination(link_client_t *c, const char *destination)
{
if (c)
c->destination = destination;
}
void link_client_close(link_client_t *c)
{
if (!c)
@@ -187,6 +194,41 @@ static void clear_reply(link_client_t *c)
c->have_reply = 0;
}
/* A broker interleaves traffic of its own with our replies: claiming a
* name makes it emit NameAcquired, and it arrives before the reply to
* the call that caused it. Read past anything that is not the reply
* we are waiting for. On a brokerless link nothing is interleaved and
* the first message read is always the one we want.
*
* Bounded so a chatty or hostile broker cannot stall PID 1 here; each
* read is bounded in turn by SO_RCVTIMEO when the caller asked for a
* timeout at open. */
#define LINK_CALL_MAX_SKIP 16
static int read_reply(link_client_t *c, uint32_t serial)
{
int i;
for (i = 0; i < LINK_CALL_MAX_SKIP; i++) {
struct link_msg msg;
if (read_one(c, &msg) < 0)
return -1;
/* Not a reply at all, or a reply to something else. */
if (msg.type != LINK_MSG_METHOD_RETURN && msg.type != LINK_MSG_ERROR)
continue;
if (msg.reply_serial != serial)
continue;
publish_reply(c, &msg);
return 0;
}
errno = EPROTO;
return -1;
}
/* Wait up to timeout_ms (-1 = forever) for one full inbound frame
* and publish it. Returns 0 on success, 1 on timeout, -1 on error. */
static int read_and_publish(link_client_t *c, int timeout_ms)
@@ -235,6 +277,7 @@ int link_client_call(link_client_t *c,
serial = c->next_serial++;
hlen = __msg_build_method_call(hdr, sizeof(hdr), serial,
obj_path, interface, member,
c->destination,
signature, (uint32_t)body_len);
if (hlen < 0)
return LINK_CALL_FAIL;
@@ -244,7 +287,7 @@ int link_client_call(link_client_t *c,
if (body_len > 0 && send_all(c->fd, body, body_len) < 0)
return LINK_CALL_FAIL;
if (read_and_publish(c, -1) != 0)
if (read_reply(c, serial) < 0)
return LINK_CALL_FAIL;
if (c->reply.type == LINK_MSG_METHOD_RETURN)
-10
View File
@@ -1,10 +0,0 @@
prefix=@prefix@
exec_prefix=@exec_prefix@
libdir=@libdir@
includedir=@includedir@
Name: libink
Description: Brokerless D-Bus server library, born inside Finit
Version: @PACKAGE_VERSION@
Libs: -L${libdir} -link
Cflags: -I${includedir}
+7
View File
@@ -226,6 +226,13 @@ link_client_t *link_client_open_timeout(const char *path, int timeout_ms);
void link_client_close(link_client_t *c);
/* Address subsequent calls on `c` to a well-known name. Needed when
* a broker routes the message, e.g. "org.freedesktop.DBus" to reach
* the bus driver itself; a brokerless link has a single peer and
* needs no destination, which is the default. `destination` is not
* copied, so it must outlive the client. */
void link_client_set_destination(link_client_t *c, const char *destination);
/* Detach the authenticated socket from the client and return the raw
* fd; subsequent link_client_close on `c` is invalid because the
* structure has already been freed. Used by callers (e.g. system-bus
+4
View File
@@ -354,6 +354,7 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
const char *path,
const char *interface,
const char *member,
const char *destination,
const char *signature,
uint32_t body_len)
{
@@ -369,6 +370,9 @@ ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
+3 -1
View File
@@ -89,12 +89,14 @@ ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
const char *member,
const char *signature, uint32_t body_len);
/* Build a method-call header (client side). */
/* Build a method-call header (client side). `destination` is NULL
* when no broker routes the message. */
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *destination,
const char *signature,
uint32_t body_len);