/* libink — built-in implementations of the well-known * org.freedesktop.DBus.* interfaces (Hello, Peer, Introspectable). * * These run before object-tree lookup in the dispatcher; returning * 0 means "handled, reply sent"; <0 means "not a built-in, fall * through to user-registered handlers". * * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ #include #include #include #include #include #include "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 stanza for one method definition. */ static void emit_method(struct xbuf *x, const link_method_t *m) { const char *p; xprintf(x, " \n", m->name); for (p = m->in_sig ? m->in_sig : ""; *p; p++) xprintf(x, " \n", *p); for (p = m->out_sig ? m->out_sig : ""; *p; p++) xprintf(x, " \n", *p); xprintf(x, " \n"); } /* Introspection limitation: emit_method prints one per * character of the signature, which is wrong for compound types * (an "a(ss)" arg appears as four args). Good enough for the * "s", "u", "as" signatures we expose today; replace with a * signature parser when the first compound argument lands. */ static const char STANDARD_INTERFACES_XML[] = " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n"; /* Is `child` a path under `parent`? If so, write the first segment * of the relative remainder into out (max outsz) and return 1. */ static int child_segment(const char *parent, const char *child, char *out, size_t outsz) { size_t plen = strlen(parent); const char *rest, *slash; size_t seglen; if (strncmp(parent, child, plen) != 0) return 0; /* Special case for "/" */ if (plen == 1 && parent[0] == '/') rest = child + 1; else if (child[plen] != '/') return 0; else rest = child + plen + 1; if (!*rest) return 0; slash = strchr(rest, '/'); seglen = slash ? (size_t)(slash - rest) : strlen(rest); if (seglen + 1 > outsz) return 0; memcpy(out, rest, seglen); out[seglen] = '\0'; return 1; } static int handle_introspect(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, "\n" "\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, " \n", e->vt->interface); if (e->vt->methods) for (meth = e->vt->methods; meth->name; meth++) emit_method(&x, meth); xprintf(&x, " \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, " \n", seg); memcpy(prev_seg, seg, sizeof(prev_seg)); } } xprintf(&x, "\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 */ }