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

libink parser and dispatch:

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

initctl:

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

Authorization:

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

Shutdown:

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

Tests, CI, docs:

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

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

526 lines
14 KiB
C

/* 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.
* (Manual SASL: this mode exists *to* test AUTH itself.)
*
* dbus-auth-client hello <sock>
* Call org.freedesktop.DBus.Hello, print the assigned unique name.
*
* dbus-auth-client introspect <sock> <object-path>
* Call org.freedesktop.DBus.Introspectable.Introspect, print XML.
*
* dbus-auth-client liststrings <sock> <object-path> <interface> <method>
* Call method expecting reply signature "as", print one per line.
*
* dbus-auth-client call-s <sock> <obj> <iface> <method> <string>
* dbus-auth-client call-void <sock> <obj> <iface> <method>
* Issue method call with the given (or no) argument; "OK" or
* "ERROR: <name>" on stderr.
*
* dbus-auth-client call-{s,void}-as-uid <uid> <sock> <obj> <iface> ...
* As above, but setuid(<uid>) first so AUTH EXTERNAL claims <uid>.
*
* dbus-auth-client get-service <sock> <identity>
* Manager1.GetService(identity) -> print the encoded path.
*
* dbus-auth-client monitor-signal <sock> <match-rule> <timeout-ms>
* AddMatch + wait for one SIGNAL. Print "SIGNAL <iface> <member>"
* then any string-typed body args. Exit 0 on signal, 1 on timeout.
*
* dbus-auth-client unknown <sock>
* Call a bogus method, exit 0 iff the server replies with an
* org.freedesktop.DBus.Error.* error.
*
* Exit codes for the non-auth modes: 0 on success, 1 on server-side
* error reply, 2 on transport / parse / arg error.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <grp.h>
#include <pwd.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 "link.h"
/* The -as-uid modes drop all privileges before exiting, and LeakSanitizer
* cannot run under that (it needs ptrace on itself). Turn leak detection
* off for this test client via ASan's startup hook; it is a short-lived
* tool, and finit -- the process whose leaks actually matter -- is checked
* on its own. Harmless when the build has no sanitizer. */
const char *__asan_default_options(void);
const char *__asan_default_options(void)
{
return "detect_leaks=0";
}
/* ---------- manual SASL: only mode_auth uses this ---------- */
static const char hex[] = "0123456789abcdef";
static int write_all_fd(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 ssize_t read_line_fd(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;
}
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(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(fd, line, (size_t)rc) < 0) { close(fd); return 2; }
if (read_line_fd(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;
}
/* ---------- libink-driven modes ---------- */
/* Convert link_client_call rc to the test client's 0/1/2 convention,
* printing the error name on stderr for ERROR replies. */
static int report_rc(link_client_t *c, int rc)
{
if (rc == LINK_CALL_OK)
return 0;
if (rc == LINK_CALL_ERROR) {
const link_reply_t *r = link_client_reply(c);
fprintf(stderr, "ERROR: %s\n",
(r && r->error_name) ? r->error_name : "<no-name>");
return 1;
}
return 2;
}
/* Become the argv-supplied uid (decimal), the way a login does: reset
* the group set from the target user's /etc/group entry before setuid,
* so the connection carries that user's real credentials. A bare
* setuid() would keep root's groups, and SO_PEERGROUPS would then hand
* the server a caller who still holds root's privileges -- masking the
* very authorization the -as-uid tests check. */
static int drop_uid(const char *uid_arg, const char *progname)
{
struct passwd *pw;
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;
}
pw = getpwuid((uid_t)v);
if (pw) {
if (initgroups(pw->pw_name, pw->pw_gid) < 0) {
perror("initgroups");
return 2;
}
if (setgid(pw->pw_gid) < 0) {
perror("setgid");
return 2;
}
} else if (setgroups(0, NULL) < 0) { /* no entry: drop all groups */
perror("setgroups");
return 2;
}
if (setuid((uid_t)v) < 0) {
perror("setuid");
return 2;
}
return 0;
}
static int mode_hello(int argc, char *argv[])
{
link_client_t *c;
const char *name;
int rc;
if (argc != 3) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, "/org/freedesktop/DBus",
"org.freedesktop.DBus", "Hello", NULL);
rc = report_rc(c, rc);
if (rc == 0) {
if (link_reply_get_string(link_client_reply(c), &name) == 0)
printf("%s\n", name);
else
rc = 2;
}
link_client_close(c);
return rc;
}
static int mode_introspect(int argc, char *argv[])
{
link_client_t *c;
const char *xml;
int rc;
if (argc != 4) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, argv[3],
"org.freedesktop.DBus.Introspectable",
"Introspect", NULL);
rc = report_rc(c, rc);
if (rc == 0) {
if (link_reply_get_string(link_client_reply(c), &xml) == 0)
printf("%s\n", xml);
else
rc = 2;
}
link_client_close(c);
return rc;
}
/* Decode body with signature "as", print one string per line. */
static int print_string_array(const link_reply_t *r)
{
link_reader_t reader;
size_t end;
if (!r || !r->signature || strcmp(r->signature, "as") != 0)
return -1;
link_reader_init(&reader, r->body, r->body_len);
if (link_r_array_begin(&reader, &end) < 0)
return -1;
while (link_r_pos(&reader) < end) {
const char *s;
if (link_r_string(&reader, &s) < 0)
return -1;
printf("%s\n", s);
}
return 0;
}
static int mode_liststrings(int argc, char *argv[])
{
link_client_t *c;
int rc;
if (argc != 6) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, argv[3], argv[4], argv[5], NULL);
rc = report_rc(c, rc);
if (rc == 0 && print_string_array(link_client_reply(c)) < 0)
rc = 2;
link_client_close(c);
return rc;
}
/* Shared call helpers used by both the plain and the -as-uid modes.
* `arg` may be NULL (void method); when non-NULL the call signature
* is "s" with `arg` as the single string argument. */
static int do_call(const char *sock, const char *obj, const char *iface,
const char *method, const char *arg)
{
link_client_t *c;
int rc;
c = link_client_open(sock);
if (!c) return 2;
rc = arg
? link_client_call_v(c, obj, iface, method, "s", arg)
: link_client_call_v(c, obj, iface, method, NULL);
rc = report_rc(c, rc);
if (rc == 0)
printf("OK\n");
link_client_close(c);
return rc;
}
static int mode_call_s(int argc, char *argv[])
{
if (argc != 7) return 2;
return do_call(argv[2], argv[3], argv[4], argv[5], argv[6]);
}
static int mode_call_void(int argc, char *argv[])
{
if (argc != 6) return 2;
return do_call(argv[2], argv[3], argv[4], argv[5], NULL);
}
static int mode_call_s_as_uid(int argc, char *argv[])
{
int rc;
if (argc != 8) return 2;
if ((rc = drop_uid(argv[2], argv[0])) != 0)
return rc;
return do_call(argv[3], argv[4], argv[5], argv[6], argv[7]);
}
static int mode_call_void_as_uid(int argc, char *argv[])
{
int rc;
if (argc != 7) return 2;
if ((rc = drop_uid(argv[2], argv[0])) != 0)
return rc;
return do_call(argv[3], argv[4], argv[5], argv[6], NULL);
}
static int mode_get_service(int argc, char *argv[])
{
link_client_t *c;
const char *path;
int rc;
if (argc != 4) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, "/org/finit/manager",
"org.finit.Manager1", "GetService",
"s", argv[3]);
rc = report_rc(c, rc);
if (rc == 0) {
/* Reply sig is "o", same wire form as "s". */
if (link_reply_get_string(link_client_reply(c), &path) == 0)
printf("%s\n", path);
else
rc = 2;
}
link_client_close(c);
return rc;
}
/*
* getprop BUS PATH IFACE PROP -- Properties.Get, prints the variant
* value; dispatches on the wire signature, "s" and "u" supported.
*/
static int mode_getprop(int argc, char *argv[])
{
const link_reply_t *r;
link_client_t *c;
link_reader_t reader;
const char *s;
uint32_t u;
int rc;
if (argc != 6) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, argv[3],
"org.freedesktop.DBus.Properties", "Get",
"ss", argv[4], argv[5]);
rc = report_rc(c, rc);
if (rc == 0) {
char type = 0;
r = link_client_reply(c);
link_reader_init(&reader, r->body, r->body_len);
if (link_r_variant_begin(&reader, &type) != 0)
rc = 2;
else if (type == 's' && link_r_string(&reader, &s) == 0)
printf("%s\n", s);
else if (type == 'u' && link_r_u32(&reader, &u) == 0)
printf("%u\n", u);
else {
fprintf(stderr, "unsupported variant type '%c'\n", type);
rc = 2;
}
}
link_client_close(c);
return rc;
}
static int mode_monitor_signal(int argc, char *argv[])
{
link_client_t *c;
const link_reply_t *r;
link_reader_t reader;
char *ep = NULL;
long v;
int timeout_ms;
int rc;
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;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, "/org/freedesktop/DBus",
"org.freedesktop.DBus", "AddMatch",
"s", argv[3]);
if (rc != LINK_CALL_OK) {
rc = report_rc(c, rc);
link_client_close(c);
return rc;
}
for (;;) {
rc = link_client_wait(c, timeout_ms);
if (rc != 0) {
link_client_close(c);
return 1; /* timeout or transport */
}
r = link_client_reply(c);
if (!r || r->type != LINK_MSG_SIGNAL)
continue;
printf("SIGNAL %s %s\n",
r->interface ? r->interface : "<no-iface>",
r->member ? r->member : "<no-member>");
/* Print any leading "s" args (other types are silently
* skipped -- callers test for the strings only). */
link_reader_init(&reader, r->body, r->body_len);
if (r->signature) {
const char *s;
const char *p;
for (p = r->signature; *p == 's'; p++) {
if (link_r_string(&reader, &s) < 0)
break;
printf("%s\n", s);
}
}
link_client_close(c);
return 0;
}
}
static int mode_unknown(int argc, char *argv[])
{
link_client_t *c;
const link_reply_t *r;
int rc;
if (argc != 3) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, "/org/finit/manager",
"org.finit.Manager1", "NotARealMethod", NULL);
if (rc != LINK_CALL_ERROR) {
link_client_close(c);
return rc == LINK_CALL_OK ? 1 : 2;
}
r = link_client_reply(c);
{
static const char prefix[] = "org.freedesktop.DBus.Error.";
rc = (r && r->error_name &&
strncmp(r->error_name, prefix, sizeof(prefix) - 1) == 0)
? 0 : 1;
}
link_client_close(c);
return rc;
}
int main(int argc, char *argv[])
{
if (argc < 2) return 2;
if (!strcmp(argv[1], "auth")) return mode_auth (argc, argv);
if (!strcmp(argv[1], "hello")) return mode_hello (argc, argv);
if (!strcmp(argv[1], "introspect")) return mode_introspect (argc, argv);
if (!strcmp(argv[1], "liststrings")) return mode_liststrings (argc, argv);
if (!strcmp(argv[1], "call-s")) return mode_call_s (argc, argv);
if (!strcmp(argv[1], "call-void")) return mode_call_void (argc, argv);
if (!strcmp(argv[1], "call-s-as-uid")) return mode_call_s_as_uid (argc, argv);
if (!strcmp(argv[1], "call-void-as-uid")) return mode_call_void_as_uid(argc, argv);
if (!strcmp(argv[1], "get-service")) return mode_get_service (argc, argv);
if (!strcmp(argv[1], "getprop")) return mode_getprop (argc, argv);
if (!strcmp(argv[1], "monitor-signal")) return mode_monitor_signal (argc, argv);
if (!strcmp(argv[1], "unknown")) return mode_unknown (argc, argv);
fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]);
return 2;
}