mirror of
https://github.com/troglobit/finit.git
synced 2026-10-28 12:30:44 +00:00
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:
1 parent
e62b463852
commit
3231ade38a
24 files changed
+266
-108
No files matched your search
+27
-40
@@ -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
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
;
|
||||
|
||||
Reference in new issue
Block a user