Files
Joachim Wiberg 0a269f3298 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>
2026-08-13 09:28:14 +02:00

301 lines
7.3 KiB
C

/* 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;
}