mirror of
https://github.com/troglobit/finit.git
synced 2026-10-18 10:59:34 +00:00
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:
1 parent
ae9a24f44f
commit
0a269f3298
39 files changed
+5530
-49
No files matched your search
+12
-1
@@ -1,5 +1,16 @@
|
||||
ACLOCAL_AMFLAGS = -I m4
|
||||
SUBDIRS = man plugins src system tmpfiles.d
|
||||
|
||||
# libink must precede src in SUBDIRS because finit links against
|
||||
# libink at build time. Automake recurses subdirs strictly in
|
||||
# declaration order, so the typical "explicit dependency" pattern
|
||||
# (foo: bar) doesn't help here — ordering is the only thing that
|
||||
# does. libsystemd is consumed by test/serv only, so its position
|
||||
# after src is fine.
|
||||
SUBDIRS = man plugins
|
||||
if DBUS
|
||||
SUBDIRS += libink
|
||||
endif
|
||||
SUBDIRS += src system tmpfiles.d
|
||||
dist_doc_DATA = README.md LICENSE contrib/finit.conf
|
||||
|
||||
if CONTRIB
|
||||
|
||||
@@ -12,6 +12,7 @@ AC_CONFIG_FILES([Makefile
|
||||
contrib/debian/Makefile contrib/debian/finit.d/Makefile contrib/debian/finit.d/available/Makefile
|
||||
contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile
|
||||
doc/Makefile doc/config/Makefile
|
||||
libink/Makefile libink/libink.pc
|
||||
libsystemd/Makefile libsystemd/libsystemd.pc
|
||||
man/Makefile
|
||||
plugins/Makefile
|
||||
@@ -94,6 +95,10 @@ AC_ARG_ENABLE(logrotate,
|
||||
AS_HELP_STRING([--disable-logrotate], [Disable built-in rotation of /var/log/wtmp]),,[
|
||||
enable_logrotate=yes])
|
||||
|
||||
AC_ARG_ENABLE(dbus,
|
||||
AS_HELP_STRING([--disable-dbus], [Disable D-Bus support (libink + Finit object tree)]),,[
|
||||
enable_dbus=yes])
|
||||
|
||||
AC_ARG_ENABLE(doc,
|
||||
AS_HELP_STRING([--disable-doc], [Disable build and install of doc/ section]),,[
|
||||
enable_doc=yes])
|
||||
@@ -243,6 +248,10 @@ AS_IF([test "x$enable_rescue" != "xno"], [
|
||||
|
||||
AM_CONDITIONAL(LOGROTATE, [test "x$enable_logrotate" = "xyes"])
|
||||
|
||||
AS_IF([test "x$enable_dbus" = "xyes"], [
|
||||
AC_DEFINE(HAVE_DBUS, 1, [Build D-Bus support via libink])])
|
||||
AM_CONDITIONAL(DBUS, [test "x$enable_dbus" = "xyes"])
|
||||
|
||||
### With features ##############################################################################
|
||||
AS_IF([test "x$bash_dir" = "xyes"], [
|
||||
PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0],
|
||||
@@ -438,6 +447,7 @@ Optional features:
|
||||
Built-in sulogin......: $with_sulogin $sulogin
|
||||
Built-in watchdogd....: $with_watchdog $watchdog
|
||||
Built-in logrotate....: $enable_logrotate
|
||||
D-Bus support (libink): $enable_dbus
|
||||
Replacement libsystemd: $with_libsystemd
|
||||
Use cgroup v2.........: $enable_cgroup
|
||||
Use libcap............: $enable_libcap
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.deps/*
|
||||
.libs/*
|
||||
.dirstamp
|
||||
*.lo
|
||||
libink.*
|
||||
!libink.pc.in
|
||||
Makefile
|
||||
Makefile.in
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -81,6 +81,9 @@ finit_SOURCES = api.c cgroup.c cgroup.h \
|
||||
if LOGROTATE
|
||||
finit_SOURCES += logrotate.c
|
||||
endif
|
||||
if DBUS
|
||||
finit_SOURCES += dbus.c
|
||||
endif
|
||||
|
||||
pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \
|
||||
plugin.h svc.h service.h
|
||||
@@ -94,6 +97,10 @@ finit_LDADD += ../plugins/libplug.la
|
||||
else
|
||||
finit_LDADD += -ldl
|
||||
endif
|
||||
if DBUS
|
||||
finit_CPPFLAGS += -I$(top_srcdir)/libink
|
||||
finit_LDADD += $(top_builddir)/libink/libink.la
|
||||
endif
|
||||
|
||||
initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \
|
||||
client.c client.h cond.c cond.h reboot.c \
|
||||
@@ -101,6 +108,10 @@ initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \
|
||||
initctl_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
|
||||
initctl_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS)
|
||||
initctl_LDADD = $(lite_LIBS) $(uev_LIBS)
|
||||
if DBUS
|
||||
initctl_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/libink
|
||||
initctl_LDADD += $(top_builddir)/libink/libink.la
|
||||
endif
|
||||
|
||||
INIT_LNKS = init telinit
|
||||
REBOOT_LNKS = reboot shutdown halt poweroff suspend
|
||||
|
||||
@@ -113,38 +113,7 @@ static int restart(svc_t *svc, void *user_data)
|
||||
static int reload(svc_t *svc, void *user_data)
|
||||
{
|
||||
(void)user_data;
|
||||
|
||||
if (!svc)
|
||||
return 1;
|
||||
|
||||
if (svc_is_blocked(svc))
|
||||
svc_start(svc);
|
||||
else
|
||||
service_timeout_cancel(svc);
|
||||
|
||||
/*
|
||||
* Clear conditions before reload to ensure dependent services
|
||||
* are properly updated. Only needed when the service does NOT
|
||||
* support SIGHUP (noreload), because then it will be stopped
|
||||
* and restarted, so conditions genuinely go away. When the
|
||||
* service handles SIGHUP, its PID and pidfile persist, so the
|
||||
* condition stays valid and dependents should not be disrupted.
|
||||
*
|
||||
* Note: only clear 'ready' for services where the pidfile
|
||||
* inotify handler reasserts it (pid/none). For s6/systemd
|
||||
* services readiness relies on their respective notification
|
||||
* mechanism which may not re-trigger on SIGHUP.
|
||||
*/
|
||||
if (svc_is_noreload(svc)) {
|
||||
svc_cond_clear(svc);
|
||||
if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE)
|
||||
service_ready(svc, 0);
|
||||
}
|
||||
|
||||
svc_mark_dirty(svc);
|
||||
service_step(svc);
|
||||
|
||||
return 0;
|
||||
return service_reload(svc);
|
||||
}
|
||||
|
||||
static int do_stop (char *buf, size_t len) { return call(stop, buf, len); }
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
#include "service.h"
|
||||
#include "sm.h"
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
/* Forward-declared locally to keep cond-w.c independent of the
|
||||
* daemon's private.h (which pulls in svc/plugin headers). The full
|
||||
* prototype lives in private.h for callers in finit's main loop. */
|
||||
void dbus_notify_condition_change(const char *name, const char *state);
|
||||
#endif
|
||||
|
||||
struct cond_boot {
|
||||
TAILQ_ENTRY(cond_boot) link;
|
||||
char *name;
|
||||
@@ -353,6 +360,9 @@ void cond_set(const char *name)
|
||||
if (cond_set_noupdate(name))
|
||||
return;
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_notify_condition_change(name, "on");
|
||||
#endif
|
||||
cond_update(name);
|
||||
}
|
||||
|
||||
@@ -383,6 +393,9 @@ void cond_set_oneshot(const char *name)
|
||||
if (cond_set_oneshot_noupdate(name))
|
||||
return;
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_notify_condition_change(name, "on");
|
||||
#endif
|
||||
cond_update(name);
|
||||
}
|
||||
|
||||
@@ -404,6 +417,9 @@ void cond_clear(const char *name)
|
||||
if (cond_clear_noupdate(name))
|
||||
return;
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_notify_condition_change(name, "off");
|
||||
#endif
|
||||
cond_update(name);
|
||||
}
|
||||
|
||||
|
||||
+869
@@ -0,0 +1,869 @@
|
||||
/* Finit-side glue between the event loop and libink.
|
||||
*
|
||||
* Owns the libink server, accepts new peers, drives each peer's
|
||||
* state machine, and registers the Finit-specific D-Bus object
|
||||
* tree (org.finit.Manager1 et al). Nothing in libink/ depends on
|
||||
* finit-internal types: the boundary lives in this file, by design.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/queue.h>
|
||||
#include <uev/uev.h>
|
||||
|
||||
#include <ftw.h>
|
||||
|
||||
#include "link.h"
|
||||
#include "path.h"
|
||||
|
||||
#include "finit.h"
|
||||
#include "cond.h"
|
||||
#include "conf.h"
|
||||
#include "log.h"
|
||||
#include "private.h"
|
||||
#include "service.h"
|
||||
#include "sig.h"
|
||||
#include "sm.h"
|
||||
#include "svc.h"
|
||||
|
||||
#define DBUS_MAX_PEERS 64
|
||||
|
||||
struct peer {
|
||||
uev_t watcher;
|
||||
link_connection_t *conn;
|
||||
TAILQ_ENTRY(peer) link;
|
||||
};
|
||||
|
||||
static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers);
|
||||
static link_server_t *server;
|
||||
static uev_t accept_watcher;
|
||||
static size_t peer_count;
|
||||
|
||||
static void peer_drop(struct peer *p)
|
||||
{
|
||||
uev_io_stop(&p->watcher);
|
||||
link_connection_close(p->conn);
|
||||
TAILQ_REMOVE(&peers, p, link);
|
||||
peer_count--;
|
||||
free(p);
|
||||
}
|
||||
|
||||
static void peer_cb(uev_t *w, void *arg, int events)
|
||||
{
|
||||
struct peer *p = arg;
|
||||
|
||||
(void)w;
|
||||
|
||||
if (UEV_ERROR == events) {
|
||||
peer_drop(p);
|
||||
return;
|
||||
}
|
||||
|
||||
if (link_connection_process(p->conn) < 0)
|
||||
peer_drop(p);
|
||||
}
|
||||
|
||||
static void accept_cb(uev_t *w, void *arg, int events)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
if (UEV_ERROR == events) {
|
||||
err(1, "D-Bus accept watcher error");
|
||||
return;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
link_connection_t *conn = NULL;
|
||||
struct peer *p;
|
||||
|
||||
if (link_server_accept(server, &conn) < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
err(1, "Failed accepting D-Bus client");
|
||||
break;
|
||||
}
|
||||
|
||||
if (peer_count >= DBUS_MAX_PEERS) {
|
||||
logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping",
|
||||
peer_count);
|
||||
link_connection_close(conn);
|
||||
continue;
|
||||
}
|
||||
|
||||
p = calloc(1, sizeof(*p));
|
||||
if (!p) {
|
||||
link_connection_close(conn);
|
||||
err(1, "Out of memory accepting D-Bus client");
|
||||
break;
|
||||
}
|
||||
|
||||
p->conn = conn;
|
||||
TAILQ_INSERT_TAIL(&peers, p, link);
|
||||
peer_count++;
|
||||
|
||||
if (uev_io_init(w->ctx, &p->watcher, peer_cb, p,
|
||||
link_connection_get_fd(conn), UEV_READ)) {
|
||||
err(1, "Failed registering D-Bus peer watcher");
|
||||
peer_drop(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- org.finit.Manager1 ---------- */
|
||||
|
||||
/* Forward decl + buffer-size constant — both consumed by Manager1
|
||||
* handlers below, defined in the Service1 block further down. */
|
||||
#define SERVICE_PATH_PREFIX "/org/finit/service/"
|
||||
#define SERVICE_PATH_PREFIX_LEN (sizeof(SERVICE_PATH_PREFIX) - 1)
|
||||
#define FINIT_SVC_PATH_MAX 512
|
||||
static int service_path_for(svc_t *svc, char *buf, size_t bufsz);
|
||||
|
||||
static int manager_list_services(link_call_t *call, void *userdata)
|
||||
{
|
||||
link_writer_t *w;
|
||||
svc_t *iter = NULL;
|
||||
svc_t *svc;
|
||||
|
||||
(void)userdata;
|
||||
|
||||
w = link_call_reply(call);
|
||||
if (!w)
|
||||
return -1;
|
||||
|
||||
link_w_array_begin(w, 's');
|
||||
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
|
||||
char ident[MAX_IDENT_LEN];
|
||||
|
||||
svc_ident(svc, ident, sizeof(ident));
|
||||
link_w_string(w, ident);
|
||||
}
|
||||
link_w_array_end(w);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int manager_get_service(link_call_t *call, void *userdata)
|
||||
{
|
||||
const char *ident;
|
||||
svc_t *svc;
|
||||
char path[FINIT_SVC_PATH_MAX];
|
||||
link_writer_t *w;
|
||||
|
||||
(void)userdata;
|
||||
|
||||
if (link_call_read_string(call, &ident) < 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"expected (s)");
|
||||
|
||||
svc = svc_find_by_str(ident);
|
||||
if (!svc)
|
||||
return link_call_reply_error(call,
|
||||
"org.finit.Error.NoSuchService", ident);
|
||||
|
||||
if (service_path_for(svc, path, sizeof(path)) < 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.finit.Error.Failed",
|
||||
"Path encoding overflow");
|
||||
|
||||
w = link_call_reply(call);
|
||||
if (!w)
|
||||
return -1;
|
||||
link_w_path(w, path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Service-control helpers used by Start/Stop/Restart/Reload. These
|
||||
* mirror the static helpers in api.c — kept private here so api.c
|
||||
* stays untouched in this increment. */
|
||||
|
||||
static int dbus_apply_stop(svc_t *svc, void *user_data)
|
||||
{
|
||||
(void)user_data;
|
||||
if (!svc)
|
||||
return 1;
|
||||
service_timeout_cancel(svc);
|
||||
svc_stop(svc);
|
||||
service_step(svc);
|
||||
if (!IS_RESERVED_RUNLEVEL(runlevel))
|
||||
service_step_all(SVC_TYPE_ANY);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dbus_apply_start(svc_t *svc, void *user_data)
|
||||
{
|
||||
(void)user_data;
|
||||
if (!svc)
|
||||
return 1;
|
||||
service_timeout_cancel(svc);
|
||||
svc_start(svc);
|
||||
service_step(svc);
|
||||
if (!IS_RESERVED_RUNLEVEL(runlevel))
|
||||
service_step_all(SVC_TYPE_ANY);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dbus_apply_restart(svc_t *svc, void *user_data)
|
||||
{
|
||||
if (!svc)
|
||||
return 1;
|
||||
if (!svc_is_running(svc))
|
||||
return dbus_apply_start(svc, user_data);
|
||||
service_timeout_cancel(svc);
|
||||
service_stop(svc);
|
||||
service_step(svc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct dispatch_ctx {
|
||||
int (*action)(svc_t *, void *);
|
||||
int matched;
|
||||
};
|
||||
|
||||
static int dispatch_found(svc_t *svc, void *udata)
|
||||
{
|
||||
struct dispatch_ctx *ctx = udata;
|
||||
|
||||
ctx->matched++;
|
||||
return ctx->action(svc, NULL);
|
||||
}
|
||||
|
||||
static int dispatch_missing(char *job, char *id, void *udata)
|
||||
{
|
||||
(void)job; (void)id; (void)udata;
|
||||
return 0; /* don't penalise the return; we'll check ->matched */
|
||||
}
|
||||
|
||||
/* Apply `action` to every service matched by `ident`. Returns 0 if
|
||||
* at least one service matched and the action succeeded on all;
|
||||
* -1 if no service matched the identity (caller sends NoSuchService). */
|
||||
static int dispatch_action(const char *ident,
|
||||
int (*action)(svc_t *, void *))
|
||||
{
|
||||
char buf[MAX_IDENT_LEN];
|
||||
struct dispatch_ctx ctx = { .action = action };
|
||||
int rc;
|
||||
|
||||
if (!ident || !*ident || strlen(ident) >= sizeof(buf))
|
||||
return -1;
|
||||
memcpy(buf, ident, strlen(ident) + 1);
|
||||
rc = svc_parse_jobstr(buf, sizeof(buf), &ctx,
|
||||
dispatch_found, dispatch_missing);
|
||||
if (ctx.matched == 0)
|
||||
return -1;
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int manager_take_string_method(link_call_t *call,
|
||||
int (*action)(svc_t *, void *))
|
||||
{
|
||||
const char *ident;
|
||||
|
||||
if (link_call_read_string(call, &ident) < 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"expected (s)");
|
||||
if (dispatch_action(ident, action) != 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.finit.Error.NoSuchService", ident);
|
||||
|
||||
(void)link_call_reply(call); /* empty reply */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int manager_start (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); }
|
||||
static int manager_stop (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); }
|
||||
static int manager_restart(link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); }
|
||||
|
||||
static int manager_reload(link_call_t *call, void *userdata)
|
||||
{
|
||||
(void)userdata;
|
||||
/*
|
||||
* Same semantics as api.c: harmless no-op during bootstrap
|
||||
* and shutdown, the client still sees success.
|
||||
*/
|
||||
if (IS_RESERVED_RUNLEVEL(runlevel))
|
||||
warnx("Ignoring reload in runlevel S and 6/0.");
|
||||
else
|
||||
sm_reload();
|
||||
(void)link_call_reply(call);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int manager_set_runlevel(link_call_t *call, void *userdata)
|
||||
{
|
||||
uint32_t lvl;
|
||||
|
||||
(void)userdata;
|
||||
if (link_call_read_u32(call, &lvl) < 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"expected (u)");
|
||||
if (lvl > 9 || lvl == INIT_LEVEL)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"runlevel must be 0-9 (excluding internal levels)");
|
||||
|
||||
if (lvl == 0) halt = SHUT_OFF;
|
||||
if (lvl == 6) halt = SHUT_REBOOT;
|
||||
sm_runlevel((int)lvl);
|
||||
|
||||
(void)link_call_reply(call);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dbus_shutdown(link_call_t *call, shutop_t target, int level)
|
||||
{
|
||||
if (IS_RESERVED_RUNLEVEL(runlevel))
|
||||
return link_call_reply_error(call,
|
||||
"org.finit.Error.WrongRunlevel",
|
||||
"Already in shutdown");
|
||||
halt = target;
|
||||
sm_runlevel(level);
|
||||
(void)link_call_reply(call);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); }
|
||||
static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); }
|
||||
static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); }
|
||||
|
||||
static const link_method_t manager_methods[] = {
|
||||
{ .name = "ListServices", .in_sig = "", .out_sig = "as",
|
||||
.handler = manager_list_services },
|
||||
{ .name = "GetService", .in_sig = "s", .out_sig = "o",
|
||||
.handler = manager_get_service },
|
||||
{ .name = "Start", .in_sig = "s", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_start },
|
||||
{ .name = "Stop", .in_sig = "s", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_stop },
|
||||
{ .name = "Restart", .in_sig = "s", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_restart },
|
||||
{ .name = "Reload", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload },
|
||||
{ .name = "SetRunlevel", .in_sig = "u", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel },
|
||||
{ .name = "Reboot", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot },
|
||||
{ .name = "Poweroff", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff },
|
||||
{ .name = "Halt", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt },
|
||||
{ NULL, NULL, NULL, 0, NULL }
|
||||
};
|
||||
|
||||
static const link_vtable_t manager_vtable = {
|
||||
.interface = "org.finit.Manager1",
|
||||
.methods = manager_methods,
|
||||
};
|
||||
|
||||
/* ---------- org.finit.Service1 (one object per service) ----------
|
||||
*
|
||||
* Per-service object at /org/finit/service/<encoded-identity>.
|
||||
* The vtable's `userdata` is the svc_t * for the specific service.
|
||||
* Registration is driven dynamically from svc_new()/svc_del() via
|
||||
* dbus_register_service() / dbus_unregister_service() below. */
|
||||
|
||||
/* SERVICE_PATH_PREFIX / SERVICE_PATH_PREFIX_LEN / FINIT_SVC_PATH_MAX
|
||||
* defined near the top of the file so Manager1.GetService can refer
|
||||
* to them. */
|
||||
|
||||
static int service_action_method(link_call_t *call, void *userdata,
|
||||
int (*action)(svc_t *, void *))
|
||||
{
|
||||
svc_t *svc = userdata;
|
||||
|
||||
if (!svc)
|
||||
return link_call_reply_error(call,
|
||||
"org.finit.Error.NoSuchService",
|
||||
"Service object no longer valid");
|
||||
|
||||
action(svc, NULL);
|
||||
(void)link_call_reply(call);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int service1_start (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); }
|
||||
static int service1_stop (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); }
|
||||
static int service1_restart(link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); }
|
||||
|
||||
static int service1_reload(link_call_t *call, void *userdata)
|
||||
{
|
||||
svc_t *svc = userdata;
|
||||
|
||||
if (!svc)
|
||||
return link_call_reply_error(call,
|
||||
"org.finit.Error.NoSuchService",
|
||||
"Service object no longer valid");
|
||||
|
||||
service_reload(svc);
|
||||
(void)link_call_reply(call);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const link_method_t service_methods[] = {
|
||||
{ .name = "Start", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_start },
|
||||
{ .name = "Stop", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_stop },
|
||||
{ .name = "Restart", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_restart },
|
||||
{ .name = "Reload", .in_sig = "", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = service1_reload },
|
||||
{ NULL, NULL, NULL, 0, NULL }
|
||||
};
|
||||
|
||||
static const link_vtable_t service_vtable = {
|
||||
.interface = "org.finit.Service1",
|
||||
.methods = service_methods,
|
||||
};
|
||||
|
||||
/* Build the canonical object path for a service. Identity is
|
||||
* "name" for single-instance services, "name:id" otherwise. */
|
||||
static int service_path_for(svc_t *svc, char *buf, size_t bufsz)
|
||||
{
|
||||
char ident[MAX_IDENT_LEN];
|
||||
size_t plen = SERVICE_PATH_PREFIX_LEN;
|
||||
int enc;
|
||||
|
||||
if (bufsz <= plen)
|
||||
return -1;
|
||||
memcpy(buf, SERVICE_PATH_PREFIX, plen);
|
||||
|
||||
svc_ident(svc, ident, sizeof(ident));
|
||||
enc = link_path_encode(ident, buf + plen, bufsz - plen);
|
||||
if (enc < 0)
|
||||
return -1;
|
||||
return (int)plen + enc;
|
||||
}
|
||||
|
||||
void dbus_register_service(svc_t *svc)
|
||||
{
|
||||
char path[FINIT_SVC_PATH_MAX];
|
||||
|
||||
if (!server || !svc)
|
||||
return;
|
||||
if (service_path_for(svc, path, sizeof(path)) < 0)
|
||||
return;
|
||||
|
||||
if (link_server_add_object(server, path, &service_vtable, svc) < 0)
|
||||
logit(LOG_WARNING, "dbus: failed registering %s", path);
|
||||
}
|
||||
|
||||
void dbus_unregister_service(svc_t *svc)
|
||||
{
|
||||
char path[FINIT_SVC_PATH_MAX];
|
||||
|
||||
if (!server || !svc)
|
||||
return;
|
||||
if (service_path_for(svc, path, sizeof(path)) < 0)
|
||||
return;
|
||||
|
||||
(void)link_server_remove_object(server, path);
|
||||
}
|
||||
|
||||
/* ---------- signal emission: ServiceStateChanged ---------- */
|
||||
|
||||
/*
|
||||
* Coarse svc_state_t -> string. svc_status() in svc.h returns a
|
||||
* richer string that also considers svc->block, but emitting just
|
||||
* the state-machine state is enough for clients to track lifecycle
|
||||
* transitions. Keep the strings stable -- they're a wire-API
|
||||
* commitment once shipped.
|
||||
*
|
||||
* No `default:` on purpose: a new SVC_*_STATE added to svc.h must
|
||||
* also pick a wire name here, and -Wall (-Wswitch) flags the
|
||||
* missing case.
|
||||
*/
|
||||
static const char *state_name(svc_state_t s)
|
||||
{
|
||||
switch (s) {
|
||||
case SVC_HALTED_STATE: return "halted";
|
||||
case SVC_DONE_STATE: return "done";
|
||||
case SVC_DEAD_STATE: return "dead";
|
||||
case SVC_CLEANUP_STATE: return "cleanup";
|
||||
case SVC_TEARDOWN_STATE: return "teardown";
|
||||
case SVC_STOPPING_STATE: return "stopping";
|
||||
case SVC_SETUP_STATE: return "setup";
|
||||
case SVC_PAUSED_STATE: return "paused";
|
||||
case SVC_WAITING_STATE: return "waiting";
|
||||
case SVC_STARTING_STATE: return "starting";
|
||||
case SVC_RUNNING_STATE: return "running";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
void dbus_notify_service_state(svc_t *svc, int old_state, int new_state)
|
||||
{
|
||||
uint8_t body[256];
|
||||
link_writer_t w;
|
||||
struct peer *p;
|
||||
char ident[MAX_IDENT_LEN];
|
||||
ssize_t blen;
|
||||
svc_state_t o = (svc_state_t)old_state;
|
||||
svc_state_t n = (svc_state_t)new_state;
|
||||
|
||||
if (!server || !svc)
|
||||
return;
|
||||
if (TAILQ_EMPTY(&peers))
|
||||
return; /* nobody could possibly be listening */
|
||||
|
||||
svc_ident(svc, ident, sizeof(ident));
|
||||
|
||||
link_writer_init(&w, body, sizeof(body));
|
||||
link_w_string(&w, ident);
|
||||
link_w_string(&w, state_name(o));
|
||||
link_w_string(&w, state_name(n));
|
||||
blen = link_writer_finish(&w);
|
||||
if (blen < 0)
|
||||
return;
|
||||
|
||||
TAILQ_FOREACH(p, &peers, link)
|
||||
(void)link_connection_emit_signal(p->conn,
|
||||
"/org/finit/manager",
|
||||
"org.finit.Manager1",
|
||||
"ServiceStateChanged",
|
||||
"sss",
|
||||
body, (size_t)blen);
|
||||
}
|
||||
|
||||
/* ---------- org.finit.Cond1 ---------- */
|
||||
|
||||
#define COND_PATH_OBJECT "/org/finit/cond"
|
||||
#define COND_INTERFACE "org.finit.Cond1"
|
||||
|
||||
/* Cond1.Set/Clear refuse anything that isn't a usr/ condition --
|
||||
* pid/, sys/, hook/ are owned by Finit's state machine and giving
|
||||
* clients write access there would let them corrupt service state.
|
||||
* Bare names ("foo") are normalised to "usr/foo" the same way
|
||||
* initctl does. The returned pointer is valid for the duration
|
||||
* of the caller's stack frame (`buf` must be at least 128 bytes). */
|
||||
static const char *normalise_usr_cond(const char *name, char *buf, size_t bufsz)
|
||||
{
|
||||
const char *tail;
|
||||
|
||||
if (!name || !*name)
|
||||
return NULL;
|
||||
if (strchr(name, '.'))
|
||||
return NULL;
|
||||
|
||||
if (strchr(name, '/')) {
|
||||
if (strncmp(name, "usr/", 4) != 0)
|
||||
return NULL;
|
||||
tail = name + 4;
|
||||
/* Match initctl's policy: no further slashes in the tail,
|
||||
* and no empty tail ("usr/" alone). */
|
||||
if (!*tail || strchr(tail, '/'))
|
||||
return NULL;
|
||||
if (strlen(name) >= bufsz)
|
||||
return NULL;
|
||||
memcpy(buf, name, strlen(name) + 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
if ((size_t)snprintf(buf, bufsz, "usr/%s", name) >= bufsz)
|
||||
return NULL;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Reject names that would escape /run/finit/cond/. cond_get(name)
|
||||
* boils down to fopen(_PATH_COND + name), so without this check any
|
||||
* caller can make PID 1 open arbitrary files -- a path traversal
|
||||
* primitive that also stalls PID 1 if pointed at a FIFO or a slow
|
||||
* device. Legal cond names look like "usr/foo", "pid/sshd",
|
||||
* "service/keventd/ready"; no leading slash, no ".." segment. */
|
||||
static int cond_name_valid(const char *name)
|
||||
{
|
||||
const char *p;
|
||||
|
||||
if (!name || !*name || *name == '/')
|
||||
return 0;
|
||||
for (p = name; *p; p++) {
|
||||
if (*p == '.' && p[1] == '.' &&
|
||||
(p[2] == '\0' || p[2] == '/'))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cond1_get(link_call_t *call, void *userdata)
|
||||
{
|
||||
const char *name;
|
||||
link_writer_t *w;
|
||||
|
||||
(void)userdata;
|
||||
|
||||
if (link_call_read_string(call, &name) < 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"expected (s)");
|
||||
if (!cond_name_valid(name))
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"invalid condition name");
|
||||
|
||||
w = link_call_reply(call);
|
||||
if (!w)
|
||||
return -1;
|
||||
link_w_string(w, condstr(cond_get(name)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cond1_set_or_clear(link_call_t *call, int do_set)
|
||||
{
|
||||
const char *name;
|
||||
char buf[128];
|
||||
const char *full;
|
||||
|
||||
if (link_call_read_string(call, &name) < 0)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"expected (s)");
|
||||
|
||||
full = normalise_usr_cond(name, buf, sizeof(buf));
|
||||
if (!full)
|
||||
return link_call_reply_error(call,
|
||||
"org.freedesktop.DBus.Error.InvalidArgs",
|
||||
"Set/Clear is restricted to usr/* conditions");
|
||||
|
||||
if (do_set)
|
||||
/* cond_set_oneshot, not cond_set: a user-asserted condition
|
||||
* is a symlink to _PATH_RECONF, so it tracks the reconf
|
||||
* generation automatically and stays "on" across reloads
|
||||
* and runlevel switches. cond_set() writes a fixed
|
||||
* generation that goes "flux" on the next reload -- wrong
|
||||
* semantics for user conditions, and what initctl cond set
|
||||
* has done forever via the filesystem path. */
|
||||
cond_set_oneshot(full);
|
||||
else
|
||||
cond_clear(full);
|
||||
|
||||
(void)link_call_reply(call);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cond1_set (link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); }
|
||||
static int cond1_clear(link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 0); }
|
||||
|
||||
/* nftw() can't pass user data so a single static handle ferries the
|
||||
* writer into the callback. Safe because dispatch is single-threaded. */
|
||||
static link_writer_t *cond_walk_writer;
|
||||
static int cond_walk_dump;
|
||||
|
||||
static int cond_walk_cb(const char *fpath, const struct stat *sb,
|
||||
int tflag, struct FTW *ftwbuf)
|
||||
{
|
||||
const char *name;
|
||||
const char *state;
|
||||
size_t prefix_len;
|
||||
|
||||
(void)sb;
|
||||
(void)ftwbuf;
|
||||
|
||||
if (tflag != FTW_F)
|
||||
return 0;
|
||||
if (!strcmp(fpath, _PATH_RECONF))
|
||||
return 0;
|
||||
|
||||
prefix_len = strlen(_PATH_COND);
|
||||
if (strlen(fpath) <= prefix_len)
|
||||
return 0;
|
||||
name = fpath + prefix_len;
|
||||
|
||||
if (cond_walk_dump) {
|
||||
state = condstr(cond_get_path(fpath));
|
||||
link_w_struct_begin(cond_walk_writer);
|
||||
link_w_string(cond_walk_writer, name);
|
||||
link_w_string(cond_walk_writer, state);
|
||||
link_w_struct_end(cond_walk_writer);
|
||||
} else {
|
||||
link_w_string(cond_walk_writer, name);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cond1_list(link_call_t *call, void *userdata)
|
||||
{
|
||||
link_writer_t *w;
|
||||
|
||||
(void)userdata;
|
||||
|
||||
w = link_call_reply(call);
|
||||
if (!w)
|
||||
return -1;
|
||||
|
||||
link_w_array_begin(w, 's');
|
||||
cond_walk_writer = w;
|
||||
cond_walk_dump = 0;
|
||||
(void)nftw(_PATH_COND, cond_walk_cb, 20, 0);
|
||||
cond_walk_writer = NULL;
|
||||
link_w_array_end(w);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cond1_dump(link_call_t *call, void *userdata)
|
||||
{
|
||||
link_writer_t *w;
|
||||
|
||||
(void)userdata;
|
||||
|
||||
w = link_call_reply(call);
|
||||
if (!w)
|
||||
return -1;
|
||||
|
||||
link_w_array_begin(w, '(');
|
||||
cond_walk_writer = w;
|
||||
cond_walk_dump = 1;
|
||||
(void)nftw(_PATH_COND, cond_walk_cb, 20, 0);
|
||||
cond_walk_writer = NULL;
|
||||
link_w_array_end(w);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const link_method_t cond_methods[] = {
|
||||
{ .name = "Get", .in_sig = "s", .out_sig = "s",
|
||||
.handler = cond1_get },
|
||||
{ .name = "Set", .in_sig = "s", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = cond1_set },
|
||||
{ .name = "Clear", .in_sig = "s", .out_sig = "",
|
||||
.flags = LINK_METHOD_PRIVILEGED, .handler = cond1_clear },
|
||||
{ .name = "List", .in_sig = "", .out_sig = "as",
|
||||
.handler = cond1_list },
|
||||
{ .name = "Dump", .in_sig = "", .out_sig = "a(ss)",
|
||||
.handler = cond1_dump },
|
||||
{ NULL, NULL, NULL, 0, NULL }
|
||||
};
|
||||
|
||||
static const link_vtable_t cond_vtable = {
|
||||
.interface = COND_INTERFACE,
|
||||
.methods = cond_methods,
|
||||
};
|
||||
|
||||
/* ---------- signal emission: ConditionChanged ---------- */
|
||||
|
||||
void dbus_notify_condition_change(const char *name, const char *state)
|
||||
{
|
||||
uint8_t body[256];
|
||||
link_writer_t w;
|
||||
struct peer *p;
|
||||
ssize_t blen;
|
||||
|
||||
if (!server || !name || !state)
|
||||
return;
|
||||
if (TAILQ_EMPTY(&peers))
|
||||
return;
|
||||
|
||||
link_writer_init(&w, body, sizeof(body));
|
||||
link_w_string(&w, name);
|
||||
link_w_string(&w, state);
|
||||
blen = link_writer_finish(&w);
|
||||
if (blen < 0)
|
||||
return;
|
||||
|
||||
TAILQ_FOREACH(p, &peers, link)
|
||||
(void)link_connection_emit_signal(p->conn,
|
||||
COND_PATH_OBJECT,
|
||||
COND_INTERFACE,
|
||||
"ConditionChanged",
|
||||
"ss",
|
||||
body, (size_t)blen);
|
||||
}
|
||||
|
||||
/* ---------- init / exit ---------- */
|
||||
|
||||
int dbus_init(uev_ctx_t *ctx)
|
||||
{
|
||||
dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET);
|
||||
|
||||
if (link_server_new(&server, FINIT_BUS_SOCKET) < 0) {
|
||||
err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (link_server_add_object(server, "/org/finit/manager",
|
||||
&manager_vtable, NULL) < 0) {
|
||||
err(1, "Failed registering Manager1 object");
|
||||
link_server_free(server);
|
||||
server = NULL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (link_server_add_object(server, COND_PATH_OBJECT,
|
||||
&cond_vtable, NULL) < 0) {
|
||||
err(1, "Failed registering Cond1 object");
|
||||
link_server_free(server);
|
||||
server = NULL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL,
|
||||
link_server_get_fd(server), UEV_READ)) {
|
||||
err(1, "Failed registering D-Bus accept watcher");
|
||||
link_server_free(server);
|
||||
server = NULL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Register Service1 objects for every service already loaded.
|
||||
* Subsequent svc_new()/svc_del() calls into
|
||||
* dbus_register_service()/dbus_unregister_service(). */
|
||||
{
|
||||
svc_t *iter = NULL;
|
||||
svc_t *svc;
|
||||
|
||||
for (svc = svc_iterator(&iter, 1); svc;
|
||||
svc = svc_iterator(&iter, 0))
|
||||
dbus_register_service(svc);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dbus_exit(void)
|
||||
{
|
||||
struct peer *p;
|
||||
|
||||
uev_io_stop(&accept_watcher);
|
||||
|
||||
while ((p = TAILQ_FIRST(&peers)))
|
||||
peer_drop(p);
|
||||
|
||||
if (server) {
|
||||
link_server_free(server);
|
||||
server = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* HAVE_DBUS */
|
||||
|
||||
/**
|
||||
* Local Variables:
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "linux"
|
||||
* End:
|
||||
*/
|
||||
@@ -780,6 +780,11 @@ int main(int argc, char *argv[])
|
||||
dbg("Starting initctl API responder ...");
|
||||
api_init(&loop);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
dbg("Starting D-Bus listener ...");
|
||||
dbus_init(&loop);
|
||||
#endif
|
||||
|
||||
dbg("Starting service interval monitor ...");
|
||||
service_init(&loop);
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
#define BUF_SIZE 4096
|
||||
|
||||
#define INIT_SOCKET _PATH_VARRUN "finit/socket"
|
||||
#define FINIT_BUS_SOCKET _PATH_VARRUN "finit/bus"
|
||||
#define INIT_MAGIC 0x03091969
|
||||
#define INIT_LEVEL 10
|
||||
|
||||
|
||||
+130
-6
@@ -268,19 +268,105 @@ static int do_startstop(int cmd, char *arg)
|
||||
return do_svc(cmd, arg);
|
||||
}
|
||||
|
||||
static int do_start (char *arg) { return do_startstop(INIT_CMD_START_SVC, arg); }
|
||||
static int do_stop (char *arg) { return do_startstop(INIT_CMD_STOP_SVC, arg); }
|
||||
#ifdef HAVE_DBUS
|
||||
#include "link.h"
|
||||
|
||||
/* Try the D-Bus path for a Manager1 method. Returns:
|
||||
* 0 succeeded via D-Bus
|
||||
* 1 D-Bus replied with an error -- callers should error out
|
||||
* -1 D-Bus not reachable -- callers should fall back to the
|
||||
* legacy INIT_SOCKET transport
|
||||
*
|
||||
* On D-Bus error replies the function maps the org.* error name to
|
||||
* the same exit code initctl historically printed for that case
|
||||
* (e.g. NoSuchService -> 69 with the legacy message). */
|
||||
static int try_dbus_manager(const char *method, const char *arg_sig,
|
||||
const char *arg)
|
||||
{
|
||||
link_client_t *c;
|
||||
const char *err;
|
||||
int rc;
|
||||
|
||||
c = link_client_open(FINIT_BUS_SOCKET);
|
||||
if (!c)
|
||||
return -1;
|
||||
|
||||
/* "s" methods take the service identity (arg may be NULL ->
|
||||
* empty string); void methods pass no body. */
|
||||
if (arg_sig && !strcmp(arg_sig, "s"))
|
||||
rc = link_client_call_v(c, "/org/finit/manager",
|
||||
"org.finit.Manager1", method,
|
||||
"s", arg ? arg : "");
|
||||
else if (!arg_sig || !*arg_sig)
|
||||
rc = link_client_call_v(c, "/org/finit/manager",
|
||||
"org.finit.Manager1", method, NULL);
|
||||
else
|
||||
rc = LINK_CALL_FAIL;
|
||||
|
||||
if (rc == LINK_CALL_ERROR) {
|
||||
const link_reply_t *r = link_client_reply(c);
|
||||
|
||||
err = (r && r->error_name) ? r->error_name : "";
|
||||
/* Exact match on the fully-qualified error name; substring
|
||||
* matching would misfire on a future name that contains
|
||||
* one of these as a substring. */
|
||||
if (!strcmp(err, "org.finit.Error.NoSuchService")) {
|
||||
link_client_close(c);
|
||||
ERRX(noerr ? 0 : 69, "no such task or service(s): %s",
|
||||
arg ? arg : "");
|
||||
}
|
||||
if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) {
|
||||
link_client_close(c);
|
||||
ERRX(1, "permission denied: %s requires root", method);
|
||||
}
|
||||
link_client_close(c);
|
||||
ERRX(1, "%s: %s", method, *err ? err : "D-Bus error");
|
||||
}
|
||||
link_client_close(c);
|
||||
if (rc == LINK_CALL_OK)
|
||||
return 0;
|
||||
return -1; /* LINK_CALL_FAIL or anything else: fall back */
|
||||
}
|
||||
#endif /* HAVE_DBUS */
|
||||
|
||||
static int do_start (char *arg)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = try_dbus_manager("Start", "s", arg);
|
||||
if (rc >= 0) return rc;
|
||||
#endif
|
||||
return do_startstop(INIT_CMD_START_SVC, arg);
|
||||
}
|
||||
|
||||
static int do_stop (char *arg)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = try_dbus_manager("Stop", "s", arg);
|
||||
if (rc >= 0) return rc;
|
||||
#endif
|
||||
return do_startstop(INIT_CMD_STOP_SVC, arg);
|
||||
}
|
||||
|
||||
static int do_reload (char *arg)
|
||||
{
|
||||
if (!arg || !arg[0])
|
||||
if (!arg || !arg[0]) {
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = try_dbus_manager("Reload", "", NULL);
|
||||
if (rc >= 0) return rc;
|
||||
#endif
|
||||
return do_svc(INIT_CMD_RELOAD, NULL);
|
||||
}
|
||||
|
||||
return do_startstop(INIT_CMD_RELOAD_SVC, arg);
|
||||
}
|
||||
|
||||
static int do_restart(char *arg)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = try_dbus_manager("Restart", "s", arg);
|
||||
if (rc == 0) return 0;
|
||||
if (rc == 1) ERRX(noerr ? 0 : 7, "failed restarting %s", arg);
|
||||
#endif
|
||||
if (do_startstop(INIT_CMD_RESTART_SVC, arg))
|
||||
ERRX(noerr ? 0 : 7, "failed restarting %s", arg);
|
||||
|
||||
@@ -643,9 +729,47 @@ static int do_cmd(int cmd)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int do_reboot (char *arg) { return do_cmd(INIT_CMD_REBOOT); }
|
||||
int do_halt (char *arg) { return do_cmd(INIT_CMD_HALT); }
|
||||
int do_poweroff(char *arg) { return do_cmd(INIT_CMD_POWEROFF); }
|
||||
#ifdef HAVE_DBUS
|
||||
static int do_reboot_dbus(const char *method)
|
||||
{
|
||||
int rc = try_dbus_manager(method, "", NULL);
|
||||
|
||||
if (rc == 0) {
|
||||
sleep(5); /* match legacy: wait for finit to shut down */
|
||||
return 0;
|
||||
}
|
||||
return rc; /* 1 = error, -1 = fall back */
|
||||
}
|
||||
#endif
|
||||
|
||||
int do_reboot (char *arg)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = do_reboot_dbus("Reboot");
|
||||
if (rc >= 0) return rc;
|
||||
#endif
|
||||
return do_cmd(INIT_CMD_REBOOT);
|
||||
}
|
||||
|
||||
int do_halt (char *arg)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = do_reboot_dbus("Halt");
|
||||
if (rc >= 0) return rc;
|
||||
#endif
|
||||
return do_cmd(INIT_CMD_HALT);
|
||||
}
|
||||
|
||||
int do_poweroff(char *arg)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
int rc = do_reboot_dbus("Poweroff");
|
||||
if (rc >= 0) return rc;
|
||||
#endif
|
||||
return do_cmd(INIT_CMD_POWEROFF);
|
||||
}
|
||||
|
||||
/* Suspend has no Manager1 equivalent yet; uses the legacy IPC. */
|
||||
int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); }
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,6 +45,15 @@ extern uev_ctx_t *ctx;
|
||||
|
||||
int api_init (uev_ctx_t *ctx);
|
||||
int api_exit (void);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
int dbus_init (uev_ctx_t *ctx);
|
||||
int dbus_exit (void);
|
||||
void dbus_register_service (svc_t *svc);
|
||||
void dbus_unregister_service (svc_t *svc);
|
||||
void dbus_notify_service_state (svc_t *svc, int old_state, int new_state);
|
||||
void dbus_notify_condition_change(const char *name, const char *state);
|
||||
#endif
|
||||
void conf_flush_events(void);
|
||||
|
||||
void service_monitor (pid_t lost, int status);
|
||||
|
||||
+59
-7
@@ -1443,17 +1443,65 @@ int service_stop(svc_t *svc)
|
||||
}
|
||||
|
||||
/**
|
||||
* service_reload - Reload a service
|
||||
* service_reload - Request reload of a service, driven by the state machine
|
||||
* @svc: Service to reload
|
||||
*
|
||||
* This function does some basic checks of the runtime state of Finit
|
||||
* and a sanity check of the @svc before sending %SIGHUP or calling
|
||||
* the reload:script command.
|
||||
* Mark a service dirty so the state machine will (re-)apply its
|
||||
* configuration, then advance the state machine. For services that
|
||||
* don't handle SIGHUP this also clears the readiness condition and
|
||||
* the pidfile/none-notify readiness flag so dependents are properly
|
||||
* notified once the service comes back.
|
||||
*
|
||||
* Returns:
|
||||
* POSIX OK(0) on success, non-zero if @svc is NULL.
|
||||
*/
|
||||
int service_reload(svc_t *svc)
|
||||
{
|
||||
if (!svc)
|
||||
return 1;
|
||||
|
||||
if (svc_is_blocked(svc))
|
||||
svc_start(svc);
|
||||
else
|
||||
service_timeout_cancel(svc);
|
||||
|
||||
/*
|
||||
* Clear conditions before reload to ensure dependent services
|
||||
* are properly updated. Only needed when the service does NOT
|
||||
* support SIGHUP (noreload), because then it will be stopped
|
||||
* and restarted, so conditions genuinely go away. When the
|
||||
* service handles SIGHUP, its PID and pidfile persist, so the
|
||||
* condition stays valid and dependents should not be disrupted.
|
||||
*
|
||||
* Note: only clear 'ready' for services where the pidfile
|
||||
* inotify handler reasserts it (pid/none). For s6/systemd
|
||||
* services readiness relies on their respective notification
|
||||
* mechanism which may not re-trigger on SIGHUP.
|
||||
*/
|
||||
if (svc_is_noreload(svc)) {
|
||||
svc_cond_clear(svc);
|
||||
if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE)
|
||||
service_ready(svc, 0);
|
||||
}
|
||||
|
||||
svc_mark_dirty(svc);
|
||||
service_step(svc);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* service_reload_apply - Perform the actual reload of a running service
|
||||
* @svc: Service to reload
|
||||
*
|
||||
* Low-level reload step: sends %SIGHUP or runs the reload:script
|
||||
* command. Called by the state machine when a service marked
|
||||
* reload-pending by service_reload() reaches the right state.
|
||||
*
|
||||
* Returns:
|
||||
* POSIX OK(0) or non-zero on error.
|
||||
*/
|
||||
static int service_reload(svc_t *svc)
|
||||
static int service_reload_apply(svc_t *svc)
|
||||
{
|
||||
const char *id = svc_ident(svc, NULL, 0);
|
||||
int do_progress = 1;
|
||||
@@ -3015,6 +3063,10 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state)
|
||||
return;
|
||||
*state = new_state;
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_notify_service_state(svc, old_state, new_state);
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The unit has stopped: HALTED comes after any post:/cleanup:
|
||||
* script, DONE is a completed run/task, where remain-after-exit
|
||||
@@ -3384,7 +3436,7 @@ restart:
|
||||
if (sm_in_reload())
|
||||
break;
|
||||
|
||||
service_reload(svc);
|
||||
service_reload_apply(svc);
|
||||
}
|
||||
|
||||
svc_mark_clean(svc);
|
||||
@@ -3422,7 +3474,7 @@ restart:
|
||||
if (svc_is_noreload(svc))
|
||||
service_stop(svc);
|
||||
else
|
||||
service_reload(svc);
|
||||
service_reload_apply(svc);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ void service_forked (svc_t *svc);
|
||||
void service_ready (svc_t *svc, int ready);
|
||||
|
||||
int service_stop (svc_t *svc);
|
||||
int service_reload (svc_t *svc);
|
||||
int service_step (svc_t *svc);
|
||||
void service_step_all (int types);
|
||||
void service_worker (void *unused);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
#include "finit.h"
|
||||
#include "conf.h"
|
||||
#include "private.h"
|
||||
#include "svc.h"
|
||||
#include "helpers.h"
|
||||
#include "pid.h"
|
||||
@@ -153,6 +154,10 @@ svc_t *svc_new(char *cmd, char *name, char *id, int type)
|
||||
|
||||
TAILQ_INSERT_TAIL(&svc_list, svc, link);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_register_service(svc);
|
||||
#endif
|
||||
|
||||
return svc;
|
||||
}
|
||||
|
||||
@@ -170,6 +175,10 @@ static struct wq work = {
|
||||
*/
|
||||
int svc_del(svc_t *svc)
|
||||
{
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_unregister_service(svc);
|
||||
#endif
|
||||
|
||||
TAILQ_REMOVE(&svc_list, svc, link);
|
||||
TAILQ_INSERT_TAIL(&gc_list, svc, link);
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ EXTRA_DIST += start-stop-serv.sh
|
||||
EXTRA_DIST += signal-service.sh
|
||||
EXTRA_DIST += testserv.sh
|
||||
EXTRA_DIST += unexpected-restart.sh
|
||||
EXTRA_DIST += dbus-auth.sh
|
||||
|
||||
AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/';
|
||||
AM_TESTS_ENVIRONMENT += export SYSROOT;
|
||||
@@ -126,6 +127,9 @@ if TESTSERV
|
||||
TESTS += testserv.sh
|
||||
endif
|
||||
TESTS += unexpected-restart.sh
|
||||
if DBUS
|
||||
TESTS += dbus-auth.sh
|
||||
endif
|
||||
|
||||
check-recursive: setup-chroot
|
||||
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ if [ "$run_make" -eq 1 ]; then
|
||||
fi
|
||||
|
||||
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \
|
||||
--enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \
|
||||
--with-keventd --with-libsystemd \
|
||||
--enable-dbus --enable-x11-common-plugin --enable-testserv-plugin \
|
||||
--with-watchdog --with-keventd --with-libsystemd \
|
||||
CFLAGS='-fsanitize=address -ggdb'
|
||||
|
||||
if [ "$run_make" -eq 1 ]; then
|
||||
|
||||
Executable
+315
@@ -0,0 +1,315 @@
|
||||
#!/bin/sh
|
||||
# End-to-end smoke test for libink:
|
||||
# - AUTH EXTERNAL handshake (happy and wrong-uid paths)
|
||||
# - org.freedesktop.DBus.Hello
|
||||
# - org.freedesktop.DBus.Introspectable.Introspect (root, manager)
|
||||
# - org.finit.Manager1.ListServices
|
||||
# - Error reply for an unknown method.
|
||||
|
||||
set -eu
|
||||
|
||||
TEST_DIR=$(dirname "$0")
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$TEST_DIR/lib/setup.sh"
|
||||
|
||||
CLIENT=/sbin/dbus-auth-client
|
||||
BUS=/run/finit/bus
|
||||
|
||||
if ! texec test -x "$CLIENT"; then
|
||||
skip "dbus-auth-client not built (configured with --disable-dbus?)"
|
||||
fi
|
||||
|
||||
say "Wait for $BUS to appear"
|
||||
retry "texec test -S $BUS"
|
||||
|
||||
say "Socket mode is 0666"
|
||||
mode=$(texec stat -c %a "$BUS")
|
||||
assert "Socket mode is 666 (got $mode)" "$mode" = "666"
|
||||
|
||||
# ---------- AUTH ----------
|
||||
|
||||
say "AUTH EXTERNAL: claim correct UID (root = 0)"
|
||||
reply=$(texec "$CLIENT" auth "$BUS" 0)
|
||||
assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK"
|
||||
|
||||
guid=${reply#OK }
|
||||
assert "GUID is 32 hex chars (got: $guid)" \
|
||||
"$(printf '%s' "$guid" | tr -d '0-9a-f' | wc -c)" -eq 0
|
||||
assert "GUID length is 32 (got: ${#guid})" "${#guid}" -eq 32
|
||||
|
||||
say "AUTH EXTERNAL: wrong UID is rejected"
|
||||
set +e
|
||||
wrong_reply=$(texec "$CLIENT" auth "$BUS" 1)
|
||||
wrong_rc=$?
|
||||
set -e
|
||||
assert "Wrong UID rejected (rc=$wrong_rc, reply: $wrong_reply)" \
|
||||
"$wrong_rc" -eq 1
|
||||
|
||||
say "Two sequential AUTH connections get different GUIDs"
|
||||
r1=$(texec "$CLIENT" auth "$BUS" 0)
|
||||
r2=$(texec "$CLIENT" auth "$BUS" 0)
|
||||
g1=${r1#OK }
|
||||
g2=${r2#OK }
|
||||
assert "Per-connection GUIDs differ ($g1 vs $g2)" "$g1" != "$g2"
|
||||
|
||||
# ---------- Built-in interfaces ----------
|
||||
|
||||
say "Hello() returns a unique name beginning with ':1.'"
|
||||
name=$(texec "$CLIENT" hello "$BUS")
|
||||
case "$name" in
|
||||
:1.*) assert "Hello returned a :1.N name (got $name)" 0 -eq 0 ;;
|
||||
*) fail "Hello returned unexpected name: $name" ;;
|
||||
esac
|
||||
|
||||
say "Two Hello() calls produce different unique names"
|
||||
n1=$(texec "$CLIENT" hello "$BUS")
|
||||
n2=$(texec "$CLIENT" hello "$BUS")
|
||||
assert "Unique names increment ($n1 vs $n2)" "$n1" != "$n2"
|
||||
|
||||
say "Introspect on root path returns valid XML referencing /manager"
|
||||
xml=$(texec "$CLIENT" introspect "$BUS" /)
|
||||
case "$xml" in
|
||||
*'<node'*) assert "XML has <node> root (good)" 0 -eq 0 ;;
|
||||
*) fail "Root introspect missing <node>: $xml" ;;
|
||||
esac
|
||||
|
||||
say "Introspect on /org/finit/manager exposes Manager1.ListServices"
|
||||
xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager)
|
||||
case "$xml" in
|
||||
*'org.finit.Manager1'*'ListServices'*)
|
||||
assert "Manager1 and ListServices visible in XML" 0 -eq 0 ;;
|
||||
*)
|
||||
fail "Manager1 XML missing; got: $xml" ;;
|
||||
esac
|
||||
|
||||
# ---------- Real method call ----------
|
||||
|
||||
say "Manager1.ListServices returns the running services"
|
||||
list=$(texec "$CLIENT" liststrings "$BUS" /org/finit/manager \
|
||||
org.finit.Manager1 ListServices)
|
||||
assert "ListServices returned at least one service" \
|
||||
"$(printf '%s' "$list" | wc -l | tr -d ' ')" -ge 1
|
||||
echo "$list"
|
||||
|
||||
# ---------- Method with arguments ----------
|
||||
|
||||
say "Manager1.Reload (void) succeeds"
|
||||
texec "$CLIENT" call-void "$BUS" /org/finit/manager \
|
||||
org.finit.Manager1 Reload >/dev/null \
|
||||
|| fail "Reload returned non-zero"
|
||||
assert "Reload void method ok" 0 -eq 0
|
||||
|
||||
say "Manager1.Stop with bogus identity returns NoSuchService error"
|
||||
set +e
|
||||
texec "$CLIENT" call-s "$BUS" /org/finit/manager \
|
||||
org.finit.Manager1 Stop "no-such-service-here" >/tmp/dbus-stop.out 2>&1
|
||||
stop_rc=$?
|
||||
set -e
|
||||
assert "Bogus service rejected (rc=$stop_rc)" "$stop_rc" -eq 1
|
||||
case "$(cat /tmp/dbus-stop.out)" in
|
||||
*NoSuchService*) assert "Error is NoSuchService" 0 -eq 0 ;;
|
||||
*) fail "Unexpected error reply: $(cat /tmp/dbus-stop.out)" ;;
|
||||
esac
|
||||
|
||||
# ---------- Authorization ----------
|
||||
|
||||
say "Manager1.Restart from non-root is rejected with AccessDenied"
|
||||
set +e
|
||||
texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \
|
||||
org.finit.Manager1 Restart "testserv" >/tmp/dbus-authz.out 2>&1
|
||||
authz_rc=$?
|
||||
set -e
|
||||
assert "Non-root Restart rejected (rc=$authz_rc)" "$authz_rc" -eq 1
|
||||
case "$(cat /tmp/dbus-authz.out)" in
|
||||
*AccessDenied*) assert "Error is AccessDenied" 0 -eq 0 ;;
|
||||
*) fail "Unexpected error: $(cat /tmp/dbus-authz.out)" ;;
|
||||
esac
|
||||
|
||||
say "Manager1.ListServices is reachable as non-root (not blocked by authz)"
|
||||
# call-s-as-uid sends an "s" body; ListServices expects "", so the
|
||||
# server must reply with org.freedesktop.DBus.Error.InvalidArgs.
|
||||
# Asserting that *positive* marker (not just "no AccessDenied")
|
||||
# ensures we don't silently pass if setuid() failed or the client
|
||||
# never reached the server (e.g. a transport error would print
|
||||
# neither AccessDenied nor InvalidArgs).
|
||||
set +e
|
||||
result=$(texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \
|
||||
org.finit.Manager1 ListServices "" 2>&1)
|
||||
set -e
|
||||
case "$result" in
|
||||
*AccessDenied*) fail "Non-root ListServices rejected by authz: $result" ;;
|
||||
*InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;;
|
||||
*) fail "Unexpected reply from non-root ListServices: $result" ;;
|
||||
esac
|
||||
|
||||
# ---------- Per-service objects (Service1) ----------
|
||||
|
||||
say "Manager1.GetService(keventd) returns the encoded object path"
|
||||
path=$(texec "$CLIENT" get-service "$BUS" keventd)
|
||||
expected="/org/finit/service/keventd"
|
||||
assert "GetService returned expected path (got: $path)" "$path" = "$expected"
|
||||
|
||||
say "Introspect on the service object exposes Service1 methods"
|
||||
xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/service/keventd)
|
||||
case "$xml" in
|
||||
*'org.finit.Service1'*'Restart'*)
|
||||
assert "Service1.Restart visible in service-object XML" 0 -eq 0 ;;
|
||||
*)
|
||||
fail "Service1 not visible on /org/finit/service/keventd: $xml" ;;
|
||||
esac
|
||||
|
||||
say "Service1.Restart on /org/finit/service/keventd succeeds"
|
||||
texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \
|
||||
org.finit.Service1 Restart >/dev/null \
|
||||
|| fail "Service1.Restart returned non-zero"
|
||||
assert "Per-service Restart ok" 0 -eq 0
|
||||
|
||||
say "Service1.Restart from non-root is rejected with AccessDenied"
|
||||
set +e
|
||||
texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/service/keventd \
|
||||
org.finit.Service1 Restart >/tmp/dbus-svcauthz.out 2>&1
|
||||
svc_authz_rc=$?
|
||||
set -e
|
||||
assert "Non-root Service1.Restart rejected (rc=$svc_authz_rc)" \
|
||||
"$svc_authz_rc" -eq 1
|
||||
case "$(cat /tmp/dbus-svcauthz.out)" in
|
||||
*AccessDenied*) assert "Service1 authz fires" 0 -eq 0 ;;
|
||||
*) fail "Expected AccessDenied, got: $(cat /tmp/dbus-svcauthz.out)" ;;
|
||||
esac
|
||||
|
||||
# ---------- Signals ----------
|
||||
|
||||
say "Service1.Restart fires Manager1.ServiceStateChanged"
|
||||
rm -f /tmp/dbus-sig.out
|
||||
( texec "$CLIENT" monitor-signal "$BUS" \
|
||||
"type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \
|
||||
5000 > /tmp/dbus-sig.out 2>&1 ) &
|
||||
mon_pid=$!
|
||||
sleep 0.5
|
||||
texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \
|
||||
org.finit.Service1 Restart >/dev/null \
|
||||
|| fail "Restart trigger returned non-zero"
|
||||
set +e
|
||||
wait "$mon_pid"
|
||||
mon_rc=$?
|
||||
set -e
|
||||
assert "monitor saw a signal (rc=$mon_rc)" "$mon_rc" -eq 0
|
||||
case "$(cat /tmp/dbus-sig.out)" in
|
||||
*"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*)
|
||||
assert "Signal payload contains the keventd identity" 0 -eq 0 ;;
|
||||
*)
|
||||
fail "Unexpected signal output: $(cat /tmp/dbus-sig.out)" ;;
|
||||
esac
|
||||
|
||||
# ---------- Cond1 ----------
|
||||
|
||||
say "Cond1.Get returns 'off' for an unset condition"
|
||||
result=$(texec "$CLIENT" call-s "$BUS" /org/finit/cond \
|
||||
org.finit.Cond1 Get "no-such-cond")
|
||||
case "$result" in
|
||||
OK*) : ;; # ok, the cond reports a state, fall through
|
||||
*) fail "Cond1.Get failed: $result" ;;
|
||||
esac
|
||||
|
||||
say "Cond1.Set fires Cond1.ConditionChanged and Get reflects the change"
|
||||
rm -f /tmp/dbus-cond.out
|
||||
( texec "$CLIENT" monitor-signal "$BUS" \
|
||||
"type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \
|
||||
5000 > /tmp/dbus-cond.out 2>&1 ) &
|
||||
cond_mon_pid=$!
|
||||
sleep 0.5
|
||||
texec "$CLIENT" call-s "$BUS" /org/finit/cond \
|
||||
org.finit.Cond1 Set "dbus-test-cond" >/dev/null \
|
||||
|| fail "Cond1.Set returned non-zero"
|
||||
set +e
|
||||
wait "$cond_mon_pid"
|
||||
cond_mon_rc=$?
|
||||
set -e
|
||||
assert "Cond1 monitor saw a signal (rc=$cond_mon_rc)" "$cond_mon_rc" -eq 0
|
||||
case "$(cat /tmp/dbus-cond.out)" in
|
||||
*"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/dbus-test-cond"*on*)
|
||||
assert "ConditionChanged carries usr/dbus-test-cond and 'on'" 0 -eq 0 ;;
|
||||
*)
|
||||
fail "Unexpected Cond1 signal: $(cat /tmp/dbus-cond.out)" ;;
|
||||
esac
|
||||
|
||||
say "Cond1.Set/Clear on non-usr/* is rejected"
|
||||
set +e
|
||||
texec "$CLIENT" call-s "$BUS" /org/finit/cond \
|
||||
org.finit.Cond1 Set "pid/sshd" >/tmp/dbus-condrej.out 2>&1
|
||||
condrej_rc=$?
|
||||
set -e
|
||||
assert "pid/* rejected (rc=$condrej_rc)" "$condrej_rc" -eq 1
|
||||
case "$(cat /tmp/dbus-condrej.out)" in
|
||||
*InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;;
|
||||
*) fail "Unexpected reply: $(cat /tmp/dbus-condrej.out)" ;;
|
||||
esac
|
||||
|
||||
say "Cond1.Set from non-root is rejected with AccessDenied"
|
||||
set +e
|
||||
texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/cond \
|
||||
org.finit.Cond1 Set "would-be-cond" >/tmp/dbus-condauthz.out 2>&1
|
||||
ca_rc=$?
|
||||
set -e
|
||||
assert "Non-root Cond1.Set rejected (rc=$ca_rc)" "$ca_rc" -eq 1
|
||||
case "$(cat /tmp/dbus-condauthz.out)" in
|
||||
*AccessDenied*) assert "Cond1 authz fires" 0 -eq 0 ;;
|
||||
*) fail "Unexpected reply: $(cat /tmp/dbus-condauthz.out)" ;;
|
||||
esac
|
||||
|
||||
say "AddMatch with a bogus key is rejected"
|
||||
set +e
|
||||
texec "$CLIENT" call-s "$BUS" /org/freedesktop/DBus \
|
||||
org.freedesktop.DBus AddMatch "bogus='whatever'" >/tmp/dbus-match.out 2>&1
|
||||
am_rc=$?
|
||||
set -e
|
||||
assert "Bad rule rejected (rc=$am_rc)" "$am_rc" -eq 1
|
||||
case "$(cat /tmp/dbus-match.out)" in
|
||||
*MatchRuleInvalid*) assert "Error is MatchRuleInvalid" 0 -eq 0 ;;
|
||||
*) fail "Unexpected reply: $(cat /tmp/dbus-match.out)" ;;
|
||||
esac
|
||||
|
||||
# ---------- initctl port ----------
|
||||
|
||||
# initctl now talks to /run/finit/bus when available. Verify by
|
||||
# subscribing to ServiceStateChanged on a background monitor and
|
||||
# then running initctl restart -- if D-Bus is in use, the signal
|
||||
# fires. If the legacy socket were still in use, the dbus subscriber
|
||||
# would see nothing.
|
||||
|
||||
say "initctl restart drives D-Bus (signal observed via dbus-auth-client)"
|
||||
rm -f /tmp/dbus-initctl-sig.out
|
||||
( texec "$CLIENT" monitor-signal "$BUS" \
|
||||
"type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \
|
||||
5000 > /tmp/dbus-initctl-sig.out 2>&1 ) &
|
||||
ic_pid=$!
|
||||
sleep 0.5
|
||||
texec initctl restart keventd >/dev/null \
|
||||
|| fail "initctl restart returned non-zero"
|
||||
set +e
|
||||
wait "$ic_pid"
|
||||
ic_rc=$?
|
||||
set -e
|
||||
assert "ServiceStateChanged fired from initctl restart (rc=$ic_rc)" \
|
||||
"$ic_rc" -eq 0
|
||||
case "$(cat /tmp/dbus-initctl-sig.out)" in
|
||||
*"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*)
|
||||
assert "initctl restart routed through D-Bus" 0 -eq 0 ;;
|
||||
*)
|
||||
fail "initctl restart didn't produce expected signal: $(cat /tmp/dbus-initctl-sig.out)" ;;
|
||||
esac
|
||||
|
||||
say "initctl reload (no args) routes through Manager1.Reload"
|
||||
texec initctl reload >/dev/null \
|
||||
|| fail "initctl reload returned non-zero"
|
||||
assert "initctl reload ok" 0 -eq 0
|
||||
|
||||
# ---------- Error reply ----------
|
||||
|
||||
say "Unknown method gets an org.freedesktop.DBus.Error.* reply"
|
||||
set +e
|
||||
texec "$CLIENT" unknown "$BUS"
|
||||
unknown_rc=$?
|
||||
set -e
|
||||
assert "Unknown method returned an error (rc=$unknown_rc)" "$unknown_rc" -eq 0
|
||||
+14
-1
@@ -15,9 +15,22 @@ make -C "$top_builddir" DESTDIR="$SYSROOT" install
|
||||
|
||||
mkdir -p "$SYSROOT/sbin/"
|
||||
cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/"
|
||||
if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then
|
||||
cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2154
|
||||
FINITBIN="$(pwd)/$top_builddir/src/finit" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk"
|
||||
# Prefer the real ELF in .libs/ over the libtool wrapper script at
|
||||
# $top_builddir/src/finit. Libtool generates a shell wrapper when
|
||||
# the binary depends on an in-tree convenience library (e.g. libink),
|
||||
# and `ldd <wrapper>` returns "not a dynamic executable", which
|
||||
# silently makes sysroot.mk copy zero host libs into the sysroot.
|
||||
if [ -f "$top_builddir/src/.libs/finit" ]; then
|
||||
finitbin_for_ldd="$(pwd)/$top_builddir/src/.libs/finit"
|
||||
else
|
||||
finitbin_for_ldd="$(pwd)/$top_builddir/src/finit"
|
||||
fi
|
||||
FINITBIN="$finitbin_for_ldd" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk"
|
||||
|
||||
# Drop plugins we don't need in test, only causes confusing FAIL in logs.
|
||||
for plugin in tty.so urandom.so rtc.so modprobe.so; do
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
/.libs/
|
||||
/.deps/
|
||||
/serv
|
||||
/dbus-auth-client
|
||||
@@ -7,3 +7,8 @@ serv_CPPFLAGS += -I$(top_srcdir)/libsystemd $(lite_CFLAGS)
|
||||
serv_SOURCES += $(top_srcdir)/libsystemd/sd-daemon.c
|
||||
serv_LDADD = $(lite_LIBS)
|
||||
endif
|
||||
|
||||
if DBUS
|
||||
noinst_PROGRAMS += dbus-auth-client
|
||||
dbus_auth_client_SOURCES = dbus-auth-client.c
|
||||
endif
|
||||
@@ -0,0 +1,861 @@
|
||||
/* Minimal D-Bus client used by the libink smoke tests.
|
||||
*
|
||||
* Modes:
|
||||
* dbus-auth-client auth <sock> <uid>
|
||||
* Send the SASL handshake claiming <uid>; print server reply line.
|
||||
* Exit 0 if reply begins "OK ", 1 if "REJECTED ", 2 otherwise.
|
||||
*
|
||||
* dbus-auth-client hello <sock>
|
||||
* Auth as own uid; call org.freedesktop.DBus.Hello on
|
||||
* /org/freedesktop/DBus. Print the assigned unique name.
|
||||
*
|
||||
* dbus-auth-client introspect <sock> <object-path>
|
||||
* Auth + org.freedesktop.DBus.Introspectable.Introspect.
|
||||
* Print the XML reply.
|
||||
*
|
||||
* dbus-auth-client liststrings <sock> <object-path> <interface> <method>
|
||||
* Auth + method call expecting reply signature "as"; print one
|
||||
* string per line.
|
||||
*
|
||||
* dbus-auth-client unknown <sock>
|
||||
* Auth + call a bogus method; exits 0 only if the server replies
|
||||
* with an "org.freedesktop.DBus.Error.*" error.
|
||||
*
|
||||
* In every non-auth mode the program exits 0 on a successful method
|
||||
* reply, 1 on a server-side error reply, 2 on transport / parse error.
|
||||
*
|
||||
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <poll.h>
|
||||
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
|
||||
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
|
||||
|
||||
/* ---------- low level I/O ---------- */
|
||||
|
||||
static int write_all(int fd, const void *buf, size_t len)
|
||||
{
|
||||
const char *p = buf;
|
||||
|
||||
while (len > 0) {
|
||||
ssize_t n = write(fd, p, len);
|
||||
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
p += n;
|
||||
len -= (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_full(int fd, void *buf, size_t len)
|
||||
{
|
||||
char *p = buf;
|
||||
|
||||
while (len > 0) {
|
||||
ssize_t n = read(fd, p, len);
|
||||
|
||||
if (n == 0) return -1;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
p += n;
|
||||
len -= (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_with_timeout(int fd, void *buf, size_t len, int timeout_ms)
|
||||
{
|
||||
struct pollfd pfd = { .fd = fd, .events = POLLIN };
|
||||
int rc;
|
||||
|
||||
for (;;) {
|
||||
rc = poll(&pfd, 1, timeout_ms);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (rc == 0)
|
||||
return 0; /* timed out */
|
||||
break;
|
||||
}
|
||||
return (int)read(fd, buf, len);
|
||||
}
|
||||
|
||||
static ssize_t read_line(int fd, char *buf, size_t bufsz)
|
||||
{
|
||||
size_t off = 0;
|
||||
|
||||
while (off + 1 < bufsz) {
|
||||
ssize_t n = read(fd, buf + off, 1);
|
||||
|
||||
if (n == 0) return -1;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
if (buf[off] == '\n') {
|
||||
buf[off] = '\0';
|
||||
if (off > 0 && buf[off - 1] == '\r')
|
||||
buf[--off] = '\0';
|
||||
return (ssize_t)off;
|
||||
}
|
||||
off++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---------- connect + AUTH ---------- */
|
||||
|
||||
static int connect_and_auth(const char *path, uid_t claimed_uid)
|
||||
{
|
||||
struct sockaddr_un sun = { .sun_family = AF_UNIX };
|
||||
char uidstr[16];
|
||||
char hexuid[32];
|
||||
char line[64];
|
||||
char reply[256];
|
||||
size_t i, n;
|
||||
int fd, rc;
|
||||
|
||||
if (strlen(path) >= sizeof(sun.sun_path))
|
||||
return -1;
|
||||
memcpy(sun.sun_path, path, strlen(path) + 1);
|
||||
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)claimed_uid);
|
||||
for (i = 0; i < n; i++) {
|
||||
unsigned c = (unsigned char)uidstr[i];
|
||||
|
||||
hexuid[i * 2] = hex[c >> 4];
|
||||
hexuid[i * 2 + 1] = hex[c & 0xf];
|
||||
}
|
||||
hexuid[n * 2] = '\0';
|
||||
|
||||
if (write_all(fd, "\0", 1) < 0) goto io;
|
||||
|
||||
rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid);
|
||||
if (rc < 0 || (size_t)rc >= sizeof(line)) goto io;
|
||||
if (write_all(fd, line, (size_t)rc) < 0) goto io;
|
||||
|
||||
if (read_line(fd, reply, sizeof(reply)) < 0) goto io;
|
||||
if (strncmp(reply, "OK ", 3) != 0) {
|
||||
fprintf(stderr, "auth failed: %s\n", reply);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_all(fd, "BEGIN\r\n", 7) < 0) goto io;
|
||||
return fd;
|
||||
|
||||
io:
|
||||
perror("auth handshake");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---------- D-Bus message build / parse ---------- */
|
||||
|
||||
struct buf {
|
||||
uint8_t *p;
|
||||
size_t cap;
|
||||
size_t off;
|
||||
int err;
|
||||
};
|
||||
|
||||
static int b_reserve(struct buf *b, size_t align, size_t bytes)
|
||||
{
|
||||
size_t pad = ALIGN_UP(b->off, align) - b->off;
|
||||
|
||||
if (b->err || b->off + pad + bytes > b->cap) {
|
||||
b->err = 1;
|
||||
return -1;
|
||||
}
|
||||
while (pad--) b->p[b->off++] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void b_put_u32(struct buf *b, uint32_t v)
|
||||
{
|
||||
if (b_reserve(b, 4, 4) < 0) return;
|
||||
b->p[b->off++] = (uint8_t)(v & 0xff);
|
||||
b->p[b->off++] = (uint8_t)((v >> 8) & 0xff);
|
||||
b->p[b->off++] = (uint8_t)((v >> 16) & 0xff);
|
||||
b->p[b->off++] = (uint8_t)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static void b_put_byte(struct buf *b, uint8_t v)
|
||||
{
|
||||
if (b_reserve(b, 1, 1) < 0) return;
|
||||
b->p[b->off++] = v;
|
||||
}
|
||||
|
||||
static void b_put_string(struct buf *b, const char *s)
|
||||
{
|
||||
size_t len = strlen(s);
|
||||
|
||||
if (b_reserve(b, 4, 4 + len + 1) < 0) return;
|
||||
b_put_u32(b, (uint32_t)len);
|
||||
memcpy(b->p + b->off, s, len);
|
||||
b->off += len;
|
||||
b->p[b->off++] = 0;
|
||||
}
|
||||
|
||||
static void b_put_signature(struct buf *b, const char *s)
|
||||
{
|
||||
size_t len = strlen(s);
|
||||
|
||||
if (b_reserve(b, 1, 1 + len + 1) < 0) return;
|
||||
b->p[b->off++] = (uint8_t)len;
|
||||
memcpy(b->p + b->off, s, len);
|
||||
b->off += len;
|
||||
b->p[b->off++] = 0;
|
||||
}
|
||||
|
||||
/* Send a method call with an optional argument.
|
||||
* arg_sig == NULL or "" -> no body
|
||||
* arg_sig == "s" -> arg_string used
|
||||
* arg_sig == "u" -> arg_u32 used
|
||||
*/
|
||||
static int send_method_call_with_arg(int fd,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *arg_sig,
|
||||
const char *arg_string,
|
||||
uint32_t arg_u32);
|
||||
|
||||
static int send_method_call(int fd,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member)
|
||||
{
|
||||
return send_method_call_with_arg(fd, path, interface, member,
|
||||
NULL, NULL, 0);
|
||||
}
|
||||
|
||||
static int send_method_call_with_arg(int fd,
|
||||
const char *path,
|
||||
const char *interface,
|
||||
const char *member,
|
||||
const char *arg_sig,
|
||||
const char *arg_string,
|
||||
uint32_t arg_u32)
|
||||
{
|
||||
uint8_t hdr[2048];
|
||||
uint8_t body[1024];
|
||||
struct buf b = { .p = hdr, .cap = sizeof(hdr) };
|
||||
struct buf bb = { .p = body, .cap = sizeof(body) };
|
||||
size_t fields_start, fields_end, padded_end;
|
||||
uint32_t body_len = 0;
|
||||
|
||||
/* Build body first so its length and the signature are known
|
||||
* before we write the header. */
|
||||
if (arg_sig && *arg_sig) {
|
||||
if (strcmp(arg_sig, "s") == 0) {
|
||||
b_put_string(&bb, arg_string ? arg_string : "");
|
||||
} else if (strcmp(arg_sig, "u") == 0) {
|
||||
b_put_u32(&bb, arg_u32);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
if (bb.err) return -1;
|
||||
body_len = (uint32_t)bb.off;
|
||||
}
|
||||
|
||||
/* Fixed header */
|
||||
memset(hdr, 0, 16);
|
||||
hdr[0] = 'l';
|
||||
hdr[1] = 1; /* METHOD_CALL */
|
||||
hdr[2] = 0; /* flags */
|
||||
hdr[3] = 1; /* protocol */
|
||||
hdr[4] = (uint8_t)( body_len & 0xff);
|
||||
hdr[5] = (uint8_t)((body_len >> 8) & 0xff);
|
||||
hdr[6] = (uint8_t)((body_len >> 16) & 0xff);
|
||||
hdr[7] = (uint8_t)((body_len >> 24) & 0xff);
|
||||
hdr[8] = 1; /* serial */
|
||||
b.off = 16;
|
||||
fields_start = b.off;
|
||||
|
||||
/* PATH */
|
||||
b_reserve(&b, 8, 0);
|
||||
b_put_byte(&b, 1);
|
||||
b_put_signature(&b, "o");
|
||||
b_put_string(&b, path);
|
||||
|
||||
if (interface) {
|
||||
b_reserve(&b, 8, 0);
|
||||
b_put_byte(&b, 2);
|
||||
b_put_signature(&b, "s");
|
||||
b_put_string(&b, interface);
|
||||
}
|
||||
|
||||
b_reserve(&b, 8, 0);
|
||||
b_put_byte(&b, 3);
|
||||
b_put_signature(&b, "s");
|
||||
b_put_string(&b, member);
|
||||
|
||||
if (arg_sig && *arg_sig) {
|
||||
b_reserve(&b, 8, 0);
|
||||
b_put_byte(&b, 8);
|
||||
b_put_signature(&b, "g");
|
||||
/* SIGNATURE wire form: 1-byte len, bytes, nul */
|
||||
b_put_byte(&b, (uint8_t)strlen(arg_sig));
|
||||
if (b.off + strlen(arg_sig) + 1 > b.cap) return -1;
|
||||
memcpy(b.p + b.off, arg_sig, strlen(arg_sig));
|
||||
b.off += strlen(arg_sig);
|
||||
b.p[b.off++] = 0;
|
||||
}
|
||||
|
||||
fields_end = b.off;
|
||||
{
|
||||
uint32_t flen = (uint32_t)(fields_end - fields_start);
|
||||
hdr[12] = (uint8_t)( flen & 0xff);
|
||||
hdr[13] = (uint8_t)((flen >> 8) & 0xff);
|
||||
hdr[14] = (uint8_t)((flen >> 16) & 0xff);
|
||||
hdr[15] = (uint8_t)((flen >> 24) & 0xff);
|
||||
}
|
||||
|
||||
padded_end = ALIGN_UP(fields_end, 8);
|
||||
while (b.off < padded_end) hdr[b.off++] = 0;
|
||||
|
||||
if (b.err) return -1;
|
||||
|
||||
if (write_all(fd, hdr, b.off) < 0) return -1;
|
||||
if (body_len > 0 && write_all(fd, body, body_len) < 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Read one D-Bus message header + body into msg/body buffers.
|
||||
* Returns 0 on success. Caller-supplied buffers must be large
|
||||
* enough; we set them generously. */
|
||||
struct reply {
|
||||
uint8_t type;
|
||||
uint32_t serial;
|
||||
uint32_t body_len;
|
||||
char signature[64];
|
||||
char error_name[128];
|
||||
char interface[128];
|
||||
char member[128];
|
||||
uint8_t body[8192];
|
||||
};
|
||||
|
||||
/* Read one D-Bus message into *r.
|
||||
* timeout_ms == 0 -> block forever waiting for the header byte
|
||||
* timeout_ms > 0 -> wait that long for the header to start; once
|
||||
* bytes arrive, the remainder of the frame is
|
||||
* read without a timeout (it's "in flight").
|
||||
* Returns 0 on success, -1 on EOF / parse error / timeout. */
|
||||
static int read_reply(int fd, struct reply *r, int timeout_ms)
|
||||
{
|
||||
uint8_t hdr_fixed[16];
|
||||
uint8_t hdr_fields[2048];
|
||||
uint32_t fields_len;
|
||||
size_t body_off;
|
||||
size_t pos;
|
||||
size_t off = 0;
|
||||
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
if (timeout_ms > 0) {
|
||||
int n = read_with_timeout(fd, hdr_fixed, 1, timeout_ms);
|
||||
if (n <= 0) return -1;
|
||||
off = 1;
|
||||
}
|
||||
if (off < 16 && read_full(fd, hdr_fixed + off, 16 - off) < 0)
|
||||
return -1;
|
||||
|
||||
if (hdr_fixed[0] != 'l') return -1;
|
||||
r->type = hdr_fixed[1];
|
||||
r->body_len = (uint32_t)hdr_fixed[4]
|
||||
| ((uint32_t)hdr_fixed[5] << 8)
|
||||
| ((uint32_t)hdr_fixed[6] << 16)
|
||||
| ((uint32_t)hdr_fixed[7] << 24);
|
||||
r->serial = (uint32_t)hdr_fixed[8]
|
||||
| ((uint32_t)hdr_fixed[9] << 8)
|
||||
| ((uint32_t)hdr_fixed[10] << 16)
|
||||
| ((uint32_t)hdr_fixed[11] << 24);
|
||||
fields_len = (uint32_t)hdr_fixed[12]
|
||||
| ((uint32_t)hdr_fixed[13] << 8)
|
||||
| ((uint32_t)hdr_fixed[14] << 16)
|
||||
| ((uint32_t)hdr_fixed[15] << 24);
|
||||
if (fields_len > sizeof(hdr_fields)) return -1;
|
||||
if (read_full(fd, hdr_fields, fields_len) < 0) return -1;
|
||||
|
||||
body_off = (size_t)ALIGN_UP(16 + fields_len, 8);
|
||||
if (body_off > 16 + fields_len) {
|
||||
uint8_t pad[8];
|
||||
if (read_full(fd, pad, body_off - 16 - fields_len) < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
pos = 0;
|
||||
while (pos < fields_len) {
|
||||
uint8_t code;
|
||||
size_t vsig_len;
|
||||
const char *vsig;
|
||||
|
||||
pos = ALIGN_UP(pos, 8);
|
||||
if (pos >= fields_len) break;
|
||||
code = hdr_fields[pos++];
|
||||
vsig_len = hdr_fields[pos++];
|
||||
if (pos + vsig_len + 1 > fields_len) return -1;
|
||||
vsig = (const char *)(hdr_fields + pos);
|
||||
pos += vsig_len + 1;
|
||||
|
||||
if (vsig[0] == 's' || vsig[0] == 'o') {
|
||||
uint32_t slen;
|
||||
char *dst = NULL;
|
||||
size_t dst_sz = 0;
|
||||
|
||||
pos = ALIGN_UP(pos, 4);
|
||||
if (pos + 4 > fields_len) return -1;
|
||||
slen = (uint32_t)hdr_fields[pos]
|
||||
| ((uint32_t)hdr_fields[pos + 1] << 8)
|
||||
| ((uint32_t)hdr_fields[pos + 2] << 16)
|
||||
| ((uint32_t)hdr_fields[pos + 3] << 24);
|
||||
pos += 4;
|
||||
if (pos + slen + 1 > fields_len) return -1;
|
||||
|
||||
switch (code) {
|
||||
case 2: dst = r->interface; dst_sz = sizeof(r->interface); break;
|
||||
case 3: dst = r->member; dst_sz = sizeof(r->member); break;
|
||||
case 4: dst = r->error_name; dst_sz = sizeof(r->error_name); break;
|
||||
default: break;
|
||||
}
|
||||
if (dst && slen < dst_sz) {
|
||||
memcpy(dst, hdr_fields + pos, slen);
|
||||
dst[slen] = '\0';
|
||||
}
|
||||
pos += slen + 1;
|
||||
} else if (vsig[0] == 'g') {
|
||||
uint32_t slen = hdr_fields[pos++];
|
||||
if (pos + slen + 1 > fields_len) return -1;
|
||||
if (code == 8 && slen < sizeof(r->signature)) {
|
||||
memcpy(r->signature, hdr_fields + pos, slen);
|
||||
r->signature[slen] = '\0';
|
||||
}
|
||||
pos += slen + 1;
|
||||
} else if (vsig[0] == 'u') {
|
||||
pos = ALIGN_UP(pos, 4);
|
||||
pos += 4;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (r->body_len > sizeof(r->body)) return -1;
|
||||
if (r->body_len > 0 && read_full(fd, r->body, r->body_len) < 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Decode a body containing exactly one "s" or "o" -- the wire form
|
||||
* is identical for both (u32 length + bytes + nul). */
|
||||
static int decode_string(struct reply *r, char *out, size_t outsz)
|
||||
{
|
||||
uint32_t len;
|
||||
|
||||
if (r->body_len < 5)
|
||||
return -1;
|
||||
if (strcmp(r->signature, "s") != 0 && strcmp(r->signature, "o") != 0)
|
||||
return -1;
|
||||
len = (uint32_t)r->body[0]
|
||||
| ((uint32_t)r->body[1] << 8)
|
||||
| ((uint32_t)r->body[2] << 16)
|
||||
| ((uint32_t)r->body[3] << 24);
|
||||
if (4 + len + 1 > r->body_len) return -1;
|
||||
if (len + 1 > outsz) return -1;
|
||||
memcpy(out, r->body + 4, len);
|
||||
out[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Decode a body with signature "as", print one string per line. */
|
||||
static int decode_array_of_strings(struct reply *r)
|
||||
{
|
||||
uint32_t array_len;
|
||||
size_t pos;
|
||||
|
||||
if (strcmp(r->signature, "as") != 0 || r->body_len < 4)
|
||||
return -1;
|
||||
array_len = (uint32_t)r->body[0]
|
||||
| ((uint32_t)r->body[1] << 8)
|
||||
| ((uint32_t)r->body[2] << 16)
|
||||
| ((uint32_t)r->body[3] << 24);
|
||||
pos = ALIGN_UP(4, 4);
|
||||
if (pos + array_len > r->body_len) return -1;
|
||||
|
||||
while (pos < 4 + array_len) {
|
||||
uint32_t slen;
|
||||
pos = ALIGN_UP(pos, 4);
|
||||
if (pos + 4 > r->body_len) return -1;
|
||||
slen = (uint32_t)r->body[pos]
|
||||
| ((uint32_t)r->body[pos + 1] << 8)
|
||||
| ((uint32_t)r->body[pos + 2] << 16)
|
||||
| ((uint32_t)r->body[pos + 3] << 24);
|
||||
pos += 4;
|
||||
if (pos + slen + 1 > r->body_len) return -1;
|
||||
printf("%.*s\n", (int)slen, r->body + pos);
|
||||
pos += slen + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------- modes ---------- */
|
||||
|
||||
static int mode_auth(int argc, char *argv[])
|
||||
{
|
||||
struct sockaddr_un sun = { .sun_family = AF_UNIX };
|
||||
char hexuid[32], line[64], reply[256];
|
||||
const char *path, *claimed;
|
||||
size_t i, claimed_len, plen;
|
||||
int fd, rc;
|
||||
|
||||
if (argc != 4) return 2;
|
||||
path = argv[2];
|
||||
claimed = argv[3];
|
||||
|
||||
plen = strlen(path);
|
||||
if (plen >= sizeof(sun.sun_path)) return 2;
|
||||
claimed_len = strlen(claimed);
|
||||
if (claimed_len * 2 >= sizeof(hexuid)) return 2;
|
||||
for (i = 0; i < claimed_len; i++) {
|
||||
unsigned c = (unsigned char)claimed[i];
|
||||
|
||||
hexuid[i * 2] = hex[c >> 4];
|
||||
hexuid[i * 2 + 1] = hex[c & 0xf];
|
||||
}
|
||||
hexuid[claimed_len * 2] = '\0';
|
||||
|
||||
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) { perror("socket"); return 2; }
|
||||
memcpy(sun.sun_path, path, plen + 1);
|
||||
if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
|
||||
perror("connect"); close(fd); return 2;
|
||||
}
|
||||
if (write_all(fd, "\0", 1) < 0) { close(fd); return 2; }
|
||||
rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid);
|
||||
if (rc < 0 || (size_t)rc >= sizeof(line)) { close(fd); return 2; }
|
||||
if (write_all(fd, line, (size_t)rc) < 0) { close(fd); return 2; }
|
||||
if (read_line(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; }
|
||||
printf("%s\n", reply);
|
||||
close(fd);
|
||||
if (strncmp(reply, "OK ", 3) == 0) return 0;
|
||||
if (strncmp(reply, "REJECTED ", 9) == 0) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int do_call_arg(const char *path, const char *obj_path,
|
||||
const char *iface, const char *method,
|
||||
const char *arg_sig, const char *arg_string,
|
||||
uint32_t arg_u32, struct reply *r)
|
||||
{
|
||||
int fd = connect_and_auth(path, getuid());
|
||||
|
||||
if (fd < 0) return 2;
|
||||
if (send_method_call_with_arg(fd, obj_path, iface, method,
|
||||
arg_sig, arg_string, arg_u32) < 0) {
|
||||
fprintf(stderr, "send: %s\n", strerror(errno));
|
||||
close(fd);
|
||||
return 2;
|
||||
}
|
||||
if (read_reply(fd, r, 0) < 0) {
|
||||
fprintf(stderr, "read_reply\n");
|
||||
close(fd);
|
||||
return 2;
|
||||
}
|
||||
close(fd);
|
||||
if (r->type == 3) {
|
||||
fprintf(stderr, "ERROR: %s\n", r->error_name);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int do_call(const char *path, const char *obj_path,
|
||||
const char *iface, const char *method,
|
||||
struct reply *r)
|
||||
{
|
||||
return do_call_arg(path, obj_path, iface, method, NULL, NULL, 0, r);
|
||||
}
|
||||
|
||||
static int mode_hello(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
char name[256];
|
||||
int rc;
|
||||
|
||||
if (argc != 3) return 2;
|
||||
rc = do_call(argv[2], "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "Hello", &r);
|
||||
if (rc != 0) return rc;
|
||||
if (decode_string(&r, name, sizeof(name)) < 0) return 2;
|
||||
printf("%s\n", name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mode_introspect(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
char xml[8192];
|
||||
int rc;
|
||||
|
||||
if (argc != 4) return 2;
|
||||
rc = do_call(argv[2], argv[3],
|
||||
"org.freedesktop.DBus.Introspectable", "Introspect", &r);
|
||||
if (rc != 0) return rc;
|
||||
if (decode_string(&r, xml, sizeof(xml)) < 0) return 2;
|
||||
printf("%s\n", xml);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mode_liststrings(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
int rc;
|
||||
|
||||
if (argc != 6) return 2;
|
||||
rc = do_call(argv[2], argv[3], argv[4], argv[5], &r);
|
||||
if (rc != 0) return rc;
|
||||
if (decode_array_of_strings(&r) < 0) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* call-s: method taking one string arg, void/error reply.
|
||||
* call-void: method taking no args, void/error reply. */
|
||||
static int mode_call_s(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
int rc;
|
||||
|
||||
if (argc != 7) return 2;
|
||||
rc = do_call_arg(argv[2], argv[3], argv[4], argv[5],
|
||||
"s", argv[6], 0, &r);
|
||||
if (rc == 0)
|
||||
printf("OK\n");
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int mode_call_void(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
int rc;
|
||||
|
||||
if (argc != 6) return 2;
|
||||
rc = do_call_arg(argv[2], argv[3], argv[4], argv[5],
|
||||
NULL, NULL, 0, &r);
|
||||
if (rc == 0)
|
||||
printf("OK\n");
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* get-service <sock> <identity>
|
||||
*
|
||||
* Calls Manager1.GetService(identity) and prints the returned
|
||||
* object path. Exit 0 on success, 1 on server error, 2 transport. */
|
||||
static int mode_get_service(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
char path[256];
|
||||
int rc;
|
||||
|
||||
if (argc != 4) return 2;
|
||||
rc = do_call_arg(argv[2], "/org/finit/manager",
|
||||
"org.finit.Manager1", "GetService",
|
||||
"s", argv[3], 0, &r);
|
||||
if (rc != 0) return rc;
|
||||
/* decode_string accepts both "s" and "o" — wire form is
|
||||
* identical; no need to pre-check the signature here. */
|
||||
if (decode_string(&r, path, sizeof(path)) < 0)
|
||||
return 2;
|
||||
printf("%s\n", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Drop effective uid to argv[2], parsed as decimal. Returns 0 on
|
||||
* success, 2 (the program's "transport error" code) on failure. */
|
||||
static int drop_uid_from_arg(const char *uid_arg, const char *progname)
|
||||
{
|
||||
uid_t drop_to;
|
||||
char *ep = NULL;
|
||||
long v;
|
||||
|
||||
errno = 0;
|
||||
v = strtol(uid_arg, &ep, 10);
|
||||
if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) {
|
||||
fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg);
|
||||
return 2;
|
||||
}
|
||||
drop_to = (uid_t)v;
|
||||
|
||||
if (setuid(drop_to) < 0) {
|
||||
perror("setuid");
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* monitor-signal <sock> <match-rule> <timeout-ms>
|
||||
*
|
||||
* Subscribes via org.freedesktop.DBus.AddMatch, then reads
|
||||
* incoming messages until either a SIGNAL is received or the
|
||||
* timeout elapses. On a signal: prints "SIGNAL <iface> <member>"
|
||||
* followed by any "s" args, one per line. Exit 0 on signal, 1 on
|
||||
* timeout, 2 on transport error. */
|
||||
static int mode_monitor_signal(int argc, char *argv[])
|
||||
{
|
||||
int fd;
|
||||
int timeout_ms;
|
||||
struct reply r;
|
||||
char *ep = NULL;
|
||||
long v;
|
||||
|
||||
if (argc != 5) return 2;
|
||||
|
||||
errno = 0;
|
||||
v = strtol(argv[4], &ep, 10);
|
||||
if (errno || !ep || *ep != '\0' || v <= 0 || v > 600000) {
|
||||
fprintf(stderr, "%s: bad timeout: %s\n", argv[0], argv[4]);
|
||||
return 2;
|
||||
}
|
||||
timeout_ms = (int)v;
|
||||
|
||||
fd = connect_and_auth(argv[2], getuid());
|
||||
if (fd < 0) return 2;
|
||||
|
||||
/* AddMatch on org.freedesktop.DBus */
|
||||
if (send_method_call_with_arg(fd, "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "AddMatch",
|
||||
"s", argv[3], 0) < 0) {
|
||||
close(fd); return 2;
|
||||
}
|
||||
if (read_reply(fd, &r, 0) < 0) { close(fd); return 2; }
|
||||
if (r.type == 3) {
|
||||
fprintf(stderr, "AddMatch ERROR: %s\n", r.error_name);
|
||||
close(fd); return 2;
|
||||
}
|
||||
|
||||
/* Now read messages until a signal or timeout. */
|
||||
for (;;) {
|
||||
if (read_reply(fd, &r, timeout_ms) < 0) {
|
||||
close(fd);
|
||||
return 1; /* timeout / transport */
|
||||
}
|
||||
if (r.type != 4) /* not a SIGNAL */
|
||||
continue;
|
||||
printf("SIGNAL %s %s\n", r.interface, r.member);
|
||||
/* Decode body as a sequence of strings; print one per line. */
|
||||
{
|
||||
size_t pos = 0;
|
||||
while (pos + 4 <= r.body_len) {
|
||||
uint32_t slen;
|
||||
pos = ALIGN_UP(pos, 4);
|
||||
if (pos + 4 > r.body_len) break;
|
||||
slen = (uint32_t)r.body[pos]
|
||||
| ((uint32_t)r.body[pos + 1] << 8)
|
||||
| ((uint32_t)r.body[pos + 2] << 16)
|
||||
| ((uint32_t)r.body[pos + 3] << 24);
|
||||
pos += 4;
|
||||
if (pos + slen + 1 > r.body_len) break;
|
||||
printf("%.*s\n", (int)slen, r.body + pos);
|
||||
pos += slen + 1;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* call-s-as-uid <uid> <sock> <obj> <iface> <method> <arg>
|
||||
*
|
||||
* Drops effective uid to <uid> (must work inside the test
|
||||
* namespace where additional uids are mapped) before connecting,
|
||||
* so AUTH EXTERNAL captures <uid> as the peer's real identity.
|
||||
* Used to verify per-method authorization gating. */
|
||||
static int mode_call_s_as_uid(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
int rc;
|
||||
|
||||
if (argc != 8) return 2;
|
||||
if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0)
|
||||
return rc;
|
||||
rc = do_call_arg(argv[3], argv[4], argv[5], argv[6],
|
||||
"s", argv[7], 0, &r);
|
||||
if (rc == 0)
|
||||
printf("OK\n");
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* call-void-as-uid <uid> <sock> <obj> <iface> <method> */
|
||||
static int mode_call_void_as_uid(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
int rc;
|
||||
|
||||
if (argc != 7) return 2;
|
||||
if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0)
|
||||
return rc;
|
||||
rc = do_call_arg(argv[3], argv[4], argv[5], argv[6],
|
||||
NULL, NULL, 0, &r);
|
||||
if (rc == 0)
|
||||
printf("OK\n");
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int mode_unknown(int argc, char *argv[])
|
||||
{
|
||||
struct reply r;
|
||||
int rc;
|
||||
|
||||
if (argc != 3) return 2;
|
||||
rc = do_call(argv[2], "/org/finit/manager",
|
||||
"org.finit.Manager1", "NotARealMethod", &r);
|
||||
if (rc == 1 && strstr(r.error_name, "org.freedesktop.DBus.Error.") == r.error_name)
|
||||
return 0;
|
||||
if (rc == 1)
|
||||
return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (argc < 2) return 2;
|
||||
if (strcmp(argv[1], "auth") == 0) return mode_auth(argc, argv);
|
||||
if (strcmp(argv[1], "hello") == 0) return mode_hello(argc, argv);
|
||||
if (strcmp(argv[1], "introspect") == 0) return mode_introspect(argc, argv);
|
||||
if (strcmp(argv[1], "liststrings") == 0) return mode_liststrings(argc, argv);
|
||||
if (strcmp(argv[1], "call-s") == 0) return mode_call_s(argc, argv);
|
||||
if (strcmp(argv[1], "call-void") == 0) return mode_call_void(argc, argv);
|
||||
if (strcmp(argv[1], "monitor-signal") == 0) return mode_monitor_signal(argc, argv);
|
||||
if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv);
|
||||
if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv);
|
||||
if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv);
|
||||
if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv);
|
||||
fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]);
|
||||
return 2;
|
||||
}
|
||||
Reference in new issue
Block a user