Files
finit/libink/builtin.c
T
Joachim Wiberg 3231ade38a dbus: fixes from a code review of the branch
A pass over the whole branch before merge, mostly in libink since
that is the new code and the part exposed to the wire.  Grouped here
rather than scattered so the review is easy to read in one place.

libink parser and dispatch:

 - Bound reader lengths so a 32-bit size_t can't wrap a wire length
   past the guard and read out of bounds.  Reachable pre-auth on any
   bus, so it matters on the 32-bit targets Finit runs on.
 - Drop a peer when a reply send fails instead of limping on with a
   half-written frame; a built-in whose send failed used to fall
   through and put a second frame on the wire.

initctl:

 - Copy a D-Bus error name out of the reply before closing the client;
   the reply points into memory the close frees.  Both error paths now
   share one helper so this can't creep back.

Authorization:

 - Take the caller's groups from the kernel (SO_PEERCRED plus
   SO_PEERGROUPS) rather than getpwuid()/getgrouplist(), which go
   through NSS and can block PID 1 on a slow LDAP or SSSD backend.
   The check is now a lookup against the group resolved once at init,
   with no NSS and no 256 KiB array on the stack.  A caller reaching
   us through a broker carries no group set, so system-bus privileged
   methods are root-only; the local bus keeps group support.  See
   libink/README.md for the note on lifting that.

Shutdown:

 - Call dbus_exit() from the shutdown path so the server, its peers,
   and the socket are let go cleanly.  The teardown existed but nobody
   called it.

Tests, CI, docs:

 - A fuzz target for the message parser, run as a quick sweep in the
   suite and properly under libFuzzer in CI, with the corpus carried
   between runs.  The -as-uid tests drop groups the way a login does
   so SO_PEERGROUPS sees the right set, and widen the test socket to
   reach the per-method check behind the 0660 gate.  Bring the GitHub
   actions up to versions that run on Node 24, and tidy a few small
   things a /simplify pass turned up.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-16 08:57:38 +02:00

512 lines
14 KiB
C

/* 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;
}
/*
* Advance past one single complete type in a D-Bus signature:
* a basic type code, 'a' + element type, or a bracketed group.
* Signatures come from our own vtables, so trust them; an
* unterminated group just stops at NUL.
*/
static const char *sig_next(const char *p)
{
while (*p == 'a') /* array prefixes, then element type */
p++;
if (*p == '(' || *p == '{') {
char close = *p == '(' ? ')' : '}';
for (p++; *p && *p != close; p = sig_next(p))
;
}
return *p ? p + 1 : p; /* NUL: unterminated group, stop here */
}
static void emit_args(struct xbuf *x, const char *sig, const char *dir)
{
const char *p, *e;
for (p = sig; p && *p; p = e) {
e = sig_next(p);
xprintf(x, " <arg type=\"%.*s\" direction=\"%s\"/>\n",
(int)(e - p), p, dir);
}
}
/* Emit a single <method> stanza for one method definition. */
static void emit_method(struct xbuf *x, const link_method_t *m)
{
xprintf(x, " <method name=\"%s\">\n", m->name);
emit_args(x, m->in_sig, "in");
emit_args(x, m->out_sig, "out");
xprintf(x, " </method>\n");
}
static void emit_property(struct xbuf *x, const link_property_t *p)
{
/* Setters are not implemented, so every property advertises
* access="read" today. When Properties.Set lands, switch on
* a writable flag. */
xprintf(x, " <property name=\"%s\" type=\"%s\" access=\"read\"/>\n",
p->name, p->sig ? p->sig : "s");
}
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"
" <interface name=\"org.freedesktop.DBus.Properties\">\n"
" <method name=\"Get\">\n"
" <arg name=\"interface\" type=\"s\" direction=\"in\"/>\n"
" <arg name=\"property\" type=\"s\" direction=\"in\"/>\n"
" <arg name=\"value\" type=\"v\" direction=\"out\"/>\n"
" </method>\n"
" <method name=\"GetAll\">\n"
" <arg name=\"interface\" type=\"s\" direction=\"in\"/>\n"
" <arg name=\"props\" type=\"a{sv}\" 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) {
const link_property_t *prop;
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);
if (e->vt->properties)
for (prop = e->vt->properties; prop->name; prop++)
emit_property(&x, prop);
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);
}
/* ---------- Properties.Get / GetAll ---------- */
/* Find the (object, vtable-entry) pair matching `path` and `interface`.
* Returns NULL if the path is unknown or the interface isn't exposed
* on it. */
static struct link_vtable_entry *
find_vtable(link_connection_t *conn, const char *path, const char *interface)
{
struct link_object *o;
if (!path || !interface)
return NULL;
TAILQ_FOREACH(o, &conn->server->objects, link) {
struct link_vtable_entry *e;
if (strcmp(o->path, path) != 0)
continue;
TAILQ_FOREACH(e, &o->vtables, link) {
if (strcmp(e->vt->interface, interface) == 0)
return e;
}
}
return NULL;
}
static int handle_properties_get(link_connection_t *conn, const struct link_msg *m)
{
const char *iface, *prop_name;
struct link_reader r;
struct link_writer w;
struct link_vtable_entry *e;
const link_property_t *p;
ssize_t blen;
if (!m->signature || strcmp(m->signature, "ss") != 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Properties.Get takes (interface, property)");
__r_init(&r, m->body, m->body_avail);
if (__r_string(&r, &iface) < 0 || __r_string(&r, &prop_name) < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Malformed argument");
e = find_vtable(conn, m->path, iface);
if (!e || !e->vt->properties)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.UnknownInterface",
"No such interface on this object");
for (p = e->vt->properties; p->name; p++) {
if (strcmp(p->name, prop_name) != 0)
continue;
if (!p->getter)
break;
__w_init(&w, conn->txbuf, sizeof(conn->txbuf));
__w_sig(&w, p->sig ? p->sig : "s");
if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.Failed",
"Property getter failed");
return __send_method_return(conn, m, "v",
conn->txbuf, (size_t)blen);
}
return __send_error(conn, m,
"org.freedesktop.DBus.Error.UnknownProperty",
"No such property on this interface");
}
static int handle_properties_get_all(link_connection_t *conn, const struct link_msg *m)
{
const char *iface;
struct link_reader r;
struct link_writer w;
struct link_vtable_entry *e;
const link_property_t *p;
ssize_t blen;
if (!m->signature || strcmp(m->signature, "s") != 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Properties.GetAll takes one string");
__r_init(&r, m->body, m->body_avail);
if (__r_string(&r, &iface) < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.InvalidArgs",
"Malformed argument");
e = find_vtable(conn, m->path, iface);
if (!e)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.UnknownInterface",
"No such interface on this object");
__w_init(&w, conn->txbuf, sizeof(conn->txbuf));
__w_array_begin(&w, '{');
if (e->vt->properties) {
for (p = e->vt->properties; p->name; p++) {
if (!p->getter)
continue;
__w_struct_begin(&w);
__w_string(&w, p->name);
__w_sig(&w, p->sig ? p->sig : "s");
if (p->getter(&w, e->userdata) != 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.Failed",
"Property getter failed");
__w_struct_end(&w);
}
}
__w_array_end(&w);
blen = __w_finish(&w);
if (blen < 0)
return __send_error(conn, m,
"org.freedesktop.DBus.Error.Failed",
"Reply too large");
return __send_method_return(conn, m, "a{sv}",
conn->txbuf, (size_t)blen);
}
/* ---------- AddMatch / RemoveMatch ---------- */
static int handle_add_match(link_connection_t *conn, const struct link_msg *m)
{
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 ---------- */
/* Returns 0 when a built-in handled the call and its reply went out,
* -1 when a built-in handled it but the send failed (the caller must
* drop the connection, its stream is now half-written), and 1 when
* the member is not a built-in and the caller should try the object
* tree. The three are distinct so a failed send is never mistaken
* for "not mine". */
int __handle_builtin(link_connection_t *conn, const struct link_msg *m)
{
/* Hello and AddMatch/RemoveMatch are per-connection state, and on
* a broker link the connection is shared by every caller: one
* sender could exhaust the match cap or drop another's rule.
* Naming and subscription belong to the broker for its own
* clients, so we do not answer these there. */
if (conn->broker && m->member &&
(!strcmp(m->member, "Hello") ||
!strcmp(m->member, "AddMatch") ||
!strcmp(m->member, "RemoveMatch"))) {
__dbg("%s is the broker's to answer, not ours", m->member);
return __send_error(conn, m,
"org.freedesktop.DBus.Error.AccessDenied",
"Handled by the message bus, not by this peer");
}
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);
if (member_is(m, "org.freedesktop.DBus.Properties", "Get"))
return handle_properties_get(conn, m);
if (member_is(m, "org.freedesktop.DBus.Properties", "GetAll"))
return handle_properties_get_all(conn, m);
return 1; /* not a built-in, let the caller try the object tree */
}