Files
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

257 lines
5.7 KiB
C

/* libink — listening socket, accept, peer-credential capture
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <fcntl.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, mode_t mode)
{
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 the mode the
* caller asked for atomically, with no window where it is more
* permissive. Who may connect is the caller's policy to set;
* per-method authorization happens later in dispatch via
* SO_PEERCRED. */
__dbg("binding %s, mode %04o", path, (unsigned)mode);
{
mode_t oldmask = umask(0777 & ~mode);
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;
/* Capture the peer's group set from the kernel so the
* authorizer never has to ask NSS (which can block PID 1).
* Primary gid first, then the supplementary groups.
*
* SO_PEERGROUPS is all-or-nothing: too small a buffer gets
* ERANGE and fills nothing, so a peer with more than fits
* is captured as the primary gid alone. A member of the
* owning group through a supplementary slot beyond the cap
* is then denied -- it fails closed, never open. Needs
* Linux 4.13; on older kernels the primary gid stands. */
conn->peer_groups[0] = cred.gid;
conn->peer_ngroups = 1;
#ifdef SO_PEERGROUPS
{
gid_t sup[LINK_PEER_GROUPS_MAX - 1];
socklen_t len = sizeof(sup);
if (getsockopt(cfd, SOL_SOCKET, SO_PEERGROUPS, sup, &len) == 0) {
int n = (int)(len / sizeof(sup[0]));
for (int i = 0; i < n; i++)
conn->peer_groups[conn->peer_ngroups++] = sup[i];
}
}
#endif
} else {
conn->peer_uid = (uid_t)-1;
}
__auth_generate_guid(conn->guid);
__dbg("new peer on fd %d, uid %d", cfd, (int)conn->peer_uid);
*out = conn;
return 0;
}
void link_server_set_uid_resolver(link_server_t *srv, link_uid_resolver_t cb, void *userdata)
{
if (!srv)
return;
srv->uid_resolver = cb;
srv->uid_userdata = userdata;
}
void link_server_set_authorizer(link_server_t *srv, link_authorizer_t cb, void *userdata)
{
if (!srv)
return;
srv->authorizer = cb;
srv->authz_userdata = userdata;
}
link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid,
unsigned int attach_flags)
{
link_connection_t *conn;
int flags;
/* On entry we always own `fd` -- close it on every failure path
* so callers don't have to track whether we touched fcntl state. */
if (!srv || fd < 0) {
if (fd >= 0)
close_save_errno(fd);
errno = EINVAL;
return NULL;
}
/* Match server_accept's fd setup: CLOEXEC first (so a fork-and-
* exec between the two calls cannot leak the fd), then NONBLOCK
* so process_binary's read loop can drain without hanging. */
flags = fcntl(fd, F_GETFD, 0);
if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0)
goto err_close;
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)
goto err_close;
conn = calloc(1, sizeof(*conn));
if (!conn)
goto err_close;
conn->fd = fd;
conn->auth = LINK_AUTH_DONE; /* caller already handshook */
conn->server = srv;
conn->peer_uid = peer_uid;
conn->broker = !!(attach_flags & LINK_ATTACH_BROKER);
__auth_generate_guid(conn->guid);
__dbg("attached %s peer on fd %d, uid %d",
conn->broker ? "broker" : "external", fd, (int)peer_uid);
return conn;
err_close:
close_save_errno(fd);
return NULL;
}