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>
This commit is contained in:
Joachim Wiberg committed 2026-08-16 08:57:38 +02:00
1 parent e62b463852
commit 3231ade38a
24 files changed
+266 -108

No files matched your search

+27 -40
View File
@@ -31,9 +31,6 @@
#ifdef HAVE_DBUS
#include <errno.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
@@ -138,9 +135,12 @@ static void expire_sweep(void *arg)
(void)arg;
expire_armed = 0;
/* _SAFE because expiring a call runs its callback, and a
* callback that ends up writing to a peer can drop it, which
* unlinks it from this very list. */
/* Expiring a call runs its callback, which may drop the peer it
* belongs to; _SAFE keeps this loop walking when that peer is
* `p`. It does NOT cover a callback that drops a *different*
* peer, which would unlink our saved `tmp` -- no path does that
* today (the only callback here denies on the same connection),
* and if one ever does it must not free from under the sweep. */
TAILQ_FOREACH_SAFE(p, &peers, link, tmp) {
if (!p->dead)
live += link_connection_expire(p->conn, DBUS_CALL_TIMEOUT_MS);
@@ -1292,49 +1292,33 @@ void dbus_notify_condition_change(const char *name, const char *state)
* the same thing, rather than the socket admitting the wheel group and
* every method turning it away.
*
* On the local bus the kernel already made this decision at connect(),
* supplementary groups and all, so the lookup only confirms it. The
* system bus has no socket mode to lean on, which is why we ask here
* rather than trusting the connection.
* The group set is the caller's own, captured by libink from the kernel
* (SO_PEERCRED + SO_PEERGROUPS) at connect, so this never asks NSS --
* getpwuid/getgrouplist can block on a slow LDAP/SSSD backend, and PID 1
* must never block. The owning group's gid is resolved once, at init.
*
* Deliberately uncached: /etc/group changes while Finit runs, and a
* privileged call is an operator action, not a hot path.
* A caller reaching us through a broker carries no group set (libink
* passes ngroups 0), so system-bus privileged methods are root-only.
* The wheel group acts over the local bus, which is where initctl and
* operators connect. See libink/README.md for lifting that limit.
*/
static int caller_is_privileged(uid_t uid, void *userdata)
static gid_t privileged_gid = (gid_t)-1; /* DEFGROUP, resolved at init */
static int caller_is_privileged(uid_t uid, const gid_t *groups, int ngroups,
void *userdata)
{
(void)userdata;
if (uid == 0)
return 1;
/* Group membership comes from NSS, which the C library loads
* with dlopen(), so a build meant to link statically cannot
* count on it. Compiled out rather than left to fail open:
* root only there, see doc/dbus.md. */
#ifndef ENABLE_STATIC
{
gid_t groups[NGROUPS_MAX];
int ngroups = NGROUPS_MAX;
struct passwd *pw;
int gid, i;
if (privileged_gid == (gid_t)-1)
return 0; /* no owning group to match against */
gid = getgroup(DEFGROUP);
if (gid < 0)
return 0;
pw = getpwuid(uid);
if (!pw)
return 0;
if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) < 0)
return 0;
for (i = 0; i < ngroups; i++) {
if (groups[i] == (gid_t)gid)
return 1;
}
for (int i = 0; i < ngroups; i++) {
if (groups[i] == privileged_gid)
return 1;
}
#endif
return 0;
}
@@ -1641,7 +1625,10 @@ int dbus_init(uev_ctx_t *ctx)
return 1;
}
if (chown(FINIT_BUS_SOCKET, geteuid(), getgroup(DEFGROUP)))
/* Resolve the owning group once: the authorizer matches callers
* against it on every privileged call and must not hit NSS then. */
privileged_gid = getgroup(DEFGROUP);
if (chown(FINIT_BUS_SOCKET, geteuid(), privileged_gid))
err(1, "Failed setting group %s on %s", DEFGROUP, FINIT_BUS_SOCKET);
link_server_set_authorizer(server, caller_is_privileged, NULL);
+18 -11
View File
@@ -386,18 +386,25 @@ static int do_startstop(int cmd, char *arg)
* `c` is closed before exit either way. Use exact-match on the
* fully-qualified error name; a substring match would misfire on a
* future name that contained one of these as a prefix. */
static void map_dbus_err(link_client_t *c, const char *method, const char *ident)
/* The reply view points into the client's rxbuf, which link_client_close()
* frees, so anyone who wants the error name past the close has to copy it
* out first. Both D-Bus error paths in this file do; keep the one copy
* here so a third caller cannot reintroduce the use-after-free. */
static void take_dbus_err(link_client_t *c, char *buf, size_t len)
{
const link_reply_t *r = link_client_reply(c);
if (r && r->error_name)
strlcpy(buf, r->error_name, len);
else if (len)
buf[0] = '\0';
}
static void map_dbus_err(link_client_t *c, const char *method, const char *ident)
{
char err[128];
/* The reply view points into c->rxbuf; copy the error name out
* before link_client_close() frees the client. Otherwise the
* strcmps below read freed memory. */
if (r && r->error_name)
strlcpy(err, r->error_name, sizeof(err));
else
err[0] = '\0';
take_dbus_err(c, err, sizeof(err));
link_client_close(c);
if (!strcmp(err, "org.finit.Error.NoSuchService"))
@@ -514,16 +521,16 @@ static int cond_dbus_call(link_client_t **bus, condop_t op,
return 1;
}
if (rc == LINK_CALL_ERROR) {
const link_reply_t *r = link_client_reply(*bus);
const char *err = (r && r->error_name) ? r->error_name : "";
char err[128];
take_dbus_err(*bus, err, sizeof(err));
link_client_close(*bus);
*bus = NULL;
if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied"))
ERRX(1, "permission denied: cond %s requires root", method);
ERRX(73, "Failed %s condition <%s>: %s",
op == COND_SET ? "asserting" : "deasserting",
arg, *err ? err : "D-Bus error");
arg, err[0] ? err : "D-Bus error");
}
/* LINK_CALL_FAIL */
link_client_close(*bus);
+5
View File
@@ -338,6 +338,11 @@ void do_shutdown(shutop_t op)
/* We can no longer act on conditions, activate script runner */
cond_exit();
#ifdef HAVE_DBUS
/* Close the bus: drop peers, free the server, unlink the socket. */
dbus_exit();
#endif
/* Reap 'em */
while (waitpid(-1, NULL, WNOHANG) > 0)
;