libink: a brokerless D-Bus implementation for Finit

Finit had no way to answer the question every service manager gets
asked: what is running, and change it.  D-Bus is how the rest of
userspace asks, but linking libdbus, sd-bus or GIO into PID 1 buys a
dependency, an allocator and a main loop we do not control.

So libink: the wire format, an object tree, and a bus of Finit's own
at /run/finit/bus, gated like INIT_SOCKET.  It speaks the standard
org.freedesktop.DBus, .Peer, .Introspectable interfaces, and Finit's
own Manager1, Service1 and Cond1 on top.  Methods that change
something are marked privileged and answered only for a caller the
kernel vouched for, via SO_PEERCRED.

Server and client both, since initctl is the first thing that needs
to talk to it, and its Start/Stop/Restart/Reload now go over the bus
rather than the legacy socket.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-08-13 09:28:14 +02:00
parent ae9a24f44f
commit 0a269f3298
39 changed files with 5530 additions and 49 deletions
+4
View File
@@ -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
View File
@@ -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
+315
View File
@@ -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
View File
@@ -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
+1
View File
@@ -3,3 +3,4 @@
/.libs/
/.deps/
/serv
/dbus-auth-client
+5
View File
@@ -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
+861
View File
@@ -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;
}