mirror of
https://github.com/troglobit/finit.git
synced 2026-10-01 05:22:48 +07:00
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>
722 lines
21 KiB
C
722 lines
21 KiB
C
/* libink — object tree, vtable registration, and method dispatch.
|
|
*
|
|
* 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 <unistd.h>
|
|
|
|
#include "internal.h"
|
|
|
|
/* ---------- object/vtable registration ---------- */
|
|
|
|
static struct link_object *find_object(link_server_t *srv, const char *path)
|
|
{
|
|
struct link_object *o;
|
|
|
|
TAILQ_FOREACH(o, &srv->objects, link)
|
|
if (strcmp(o->path, path) == 0)
|
|
return o;
|
|
return NULL;
|
|
}
|
|
|
|
int link_server_remove_object(link_server_t *srv, const char *path)
|
|
{
|
|
struct link_object *o;
|
|
struct link_vtable_entry *e;
|
|
|
|
if (!srv || !path) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
o = find_object(srv, path);
|
|
if (!o) {
|
|
errno = ENOENT;
|
|
return -1;
|
|
}
|
|
|
|
while ((e = TAILQ_FIRST(&o->vtables))) {
|
|
TAILQ_REMOVE(&o->vtables, e, link);
|
|
free(e);
|
|
}
|
|
TAILQ_REMOVE(&srv->objects, o, link);
|
|
free(o);
|
|
return 0;
|
|
}
|
|
|
|
int link_server_add_object(link_server_t *srv, const char *path,
|
|
const link_vtable_t *vt, void *userdata)
|
|
{
|
|
struct link_object *o;
|
|
struct link_vtable_entry *e;
|
|
size_t plen;
|
|
|
|
if (!srv || !path || !*path || !vt || !vt->interface) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
plen = strlen(path);
|
|
if (plen >= LINK_PATH_MAX) {
|
|
errno = ENAMETOOLONG;
|
|
return -1;
|
|
}
|
|
|
|
o = find_object(srv, path);
|
|
if (!o) {
|
|
o = calloc(1, sizeof(*o));
|
|
if (!o)
|
|
return -1;
|
|
memcpy(o->path, path, plen + 1);
|
|
TAILQ_INIT(&o->vtables);
|
|
TAILQ_INSERT_TAIL(&srv->objects, o, link);
|
|
}
|
|
|
|
e = calloc(1, sizeof(*e));
|
|
if (!e)
|
|
return -1;
|
|
e->vt = vt;
|
|
e->userdata = userdata;
|
|
TAILQ_INSERT_TAIL(&o->vtables, e, link);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- lookup ---------- */
|
|
|
|
static const link_method_t *find_method(const link_vtable_t *vt, const char *name)
|
|
{
|
|
const link_method_t *m;
|
|
|
|
if (!vt->methods)
|
|
return NULL;
|
|
for (m = vt->methods; m->name; m++)
|
|
if (strcmp(m->name, name) == 0)
|
|
return m;
|
|
return NULL;
|
|
}
|
|
|
|
/* If incoming.interface is NULL, search every interface on the
|
|
* object for a member with this name. Returns the matching method
|
|
* and writes back its vtable_entry in *out_e. */
|
|
static const link_method_t *resolve(struct link_object *o,
|
|
const char *iface, const char *member,
|
|
struct link_vtable_entry **out_e)
|
|
{
|
|
struct link_vtable_entry *e;
|
|
const link_method_t *m;
|
|
|
|
if (iface) {
|
|
TAILQ_FOREACH(e, &o->vtables, link) {
|
|
if (strcmp(e->vt->interface, iface) != 0)
|
|
continue;
|
|
m = find_method(e->vt, member);
|
|
if (m) {
|
|
*out_e = e;
|
|
return m;
|
|
}
|
|
return NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
TAILQ_FOREACH(e, &o->vtables, link) {
|
|
m = find_method(e->vt, member);
|
|
if (m) {
|
|
*out_e = e;
|
|
return m;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ---------- send helpers ---------- */
|
|
|
|
/*
|
|
* Peer fds are non-blocking, so send_all may fail mid-frame (e.g.
|
|
* EAGAIN from a peer that stopped draining its socket). Any failure
|
|
* poisons the peer's stream: never retry on the same connection,
|
|
* drop the peer.
|
|
*/
|
|
#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len))
|
|
|
|
int __send_method_return(link_connection_t *conn, const struct link_msg *req,
|
|
const char *out_sig,
|
|
const uint8_t *body, size_t body_len)
|
|
{
|
|
uint8_t hdr[512];
|
|
ssize_t hlen;
|
|
uint32_t serial = ++conn->next_serial;
|
|
|
|
hlen = __msg_build_return(hdr, sizeof(hdr), serial,
|
|
req->serial,
|
|
req->sender,
|
|
out_sig, (uint32_t)body_len);
|
|
if (hlen < 0) {
|
|
errno = EMSGSIZE;
|
|
return -1;
|
|
}
|
|
|
|
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
|
|
return -1;
|
|
if (body_len > 0 && send_all(conn->fd, body, body_len) < 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
int link_connection_emit_signal(link_connection_t *conn,
|
|
const char *path,
|
|
const char *interface,
|
|
const char *member,
|
|
const char *signature,
|
|
const uint8_t *body, size_t body_len)
|
|
{
|
|
uint8_t hdr[512];
|
|
ssize_t hlen;
|
|
uint32_t serial;
|
|
size_t i;
|
|
int matched = 0;
|
|
|
|
if (!conn || !path || !interface || !member) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
if (conn->auth != LINK_AUTH_DONE)
|
|
return 0; /* peer hasn't finished the SASL phase */
|
|
|
|
/* A broker routes to whoever subscribed with it, so it wants
|
|
* every signal and never sends us AddMatch of its own. */
|
|
matched = conn->broker;
|
|
|
|
for (i = 0; !matched && i < conn->matches_count; i++) {
|
|
if (__match_matches(conn->matches[i], path,
|
|
interface, member))
|
|
matched = 1;
|
|
}
|
|
if (!matched)
|
|
return 0; /* peer didn't subscribe — nothing to do */
|
|
|
|
serial = ++conn->next_serial;
|
|
hlen = __msg_build_signal(hdr, sizeof(hdr), serial,
|
|
path, interface, member,
|
|
signature, (uint32_t)body_len);
|
|
if (hlen < 0) {
|
|
errno = EMSGSIZE;
|
|
return -1;
|
|
}
|
|
|
|
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
|
|
return -1;
|
|
if (body_len > 0 && send_all(conn->fd, body, body_len) < 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
int __send_error(link_connection_t *conn, const struct link_msg *req,
|
|
const char *error_name, const char *text)
|
|
{
|
|
uint8_t hdr[512];
|
|
uint8_t body[256];
|
|
ssize_t hlen;
|
|
size_t blen = 0;
|
|
uint32_t serial = ++conn->next_serial;
|
|
const char *sig = NULL;
|
|
|
|
if (text && *text) {
|
|
struct link_writer w;
|
|
ssize_t n;
|
|
|
|
__w_init(&w, body, sizeof(body));
|
|
__w_string(&w, text);
|
|
n = __w_finish(&w);
|
|
if (n < 0) {
|
|
errno = EMSGSIZE;
|
|
return -1;
|
|
}
|
|
blen = (size_t)n;
|
|
sig = "s";
|
|
}
|
|
|
|
hlen = __msg_build_error(hdr, sizeof(hdr), serial,
|
|
req->serial, req->sender,
|
|
error_name, sig, (uint32_t)blen);
|
|
if (hlen < 0) {
|
|
errno = EMSGSIZE;
|
|
return -1;
|
|
}
|
|
|
|
if (send_all(conn->fd, hdr, (size_t)hlen) < 0)
|
|
return -1;
|
|
if (blen > 0 && send_all(conn->fd, body, blen) < 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* ---------- link_call public surface ---------- */
|
|
|
|
const char *link_call_path (const link_call_t *c) { return c ? c->incoming.path : NULL; }
|
|
const char *link_call_interface(const link_call_t *c) { return c ? c->incoming.interface : NULL; }
|
|
const char *link_call_member (const link_call_t *c) { return c ? c->incoming.member : NULL; }
|
|
uid_t link_call_uid (const link_call_t *c) { return c ? c->uid : LINK_UID_UNKNOWN; }
|
|
|
|
link_writer_t *link_call_reply(link_call_t *call)
|
|
{
|
|
if (!call || call->reply_consumed || call->error_sent)
|
|
return NULL;
|
|
call->reply_consumed = 1;
|
|
__w_init(&call->reply_writer,
|
|
call->conn->txbuf, sizeof(call->conn->txbuf));
|
|
return &call->reply_writer;
|
|
}
|
|
|
|
int link_call_reply_error(link_call_t *call, const char *name, const char *message)
|
|
{
|
|
if (!call || call->error_sent) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
call->error_sent = 1;
|
|
return __send_error(call->conn, &call->incoming, name, message);
|
|
}
|
|
|
|
/* ---------- public reader wrappers ---------- */
|
|
|
|
int link_call_read_byte (link_call_t *c, uint8_t *o) { return __r_byte (&c->read_cursor, o); }
|
|
int link_call_read_bool (link_call_t *c, int *o) { return __r_bool (&c->read_cursor, o); }
|
|
int link_call_read_u32 (link_call_t *c, uint32_t *o) { return __r_u32 (&c->read_cursor, o); }
|
|
int link_call_read_string(link_call_t *c, const char **o) { return __r_string(&c->read_cursor, o); }
|
|
int link_call_read_path (link_call_t *c, const char **o) { return __r_path (&c->read_cursor, o); }
|
|
|
|
/* ---------- public writer wrappers ---------- */
|
|
|
|
void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap) { __w_init(w, buf, cap); }
|
|
ssize_t link_writer_finish(link_writer_t *w) { return __w_finish(w); }
|
|
|
|
void link_w_byte (link_writer_t *w, uint8_t v) { __w_byte(w, v); }
|
|
void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); }
|
|
void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); }
|
|
void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); }
|
|
void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); }
|
|
void link_w_variant_string(link_writer_t *w, const char *s) { __w_variant_string(w, s); }
|
|
void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); }
|
|
void link_w_array_end (link_writer_t *w) { __w_array_end(w); }
|
|
void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); }
|
|
void link_w_struct_end (link_writer_t *w) { __w_struct_end(w); }
|
|
|
|
/* ---------- public reader wrappers ---------- */
|
|
|
|
void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len) { __r_init(r, body, len); }
|
|
int link_r_byte (link_reader_t *r, uint8_t *o) { return __r_byte (r, o); }
|
|
int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); }
|
|
int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); }
|
|
int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); }
|
|
int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); }
|
|
int link_r_variant_begin (link_reader_t *r, char *type) { return __r_variant_begin(r, type); }
|
|
int link_r_skip_basic (link_reader_t *r, char type) { return __r_skip_basic(r, type); }
|
|
int link_r_variant_string(link_reader_t *r, const char **o) { return __r_variant_string(r, o); }
|
|
int link_r_align (link_reader_t *r, size_t n) { return __r_align (r, n); }
|
|
int link_r_array_begin(link_reader_t *r, size_t *e) { return __r_array_begin(r, e); }
|
|
int link_r_done (const link_reader_t *r) { return __r_done (r); }
|
|
size_t link_r_pos (const link_reader_t *r) { return r->off; }
|
|
|
|
/* ---------- dispatch entry point ---------- */
|
|
|
|
/* ---------- replies to our own outbound calls ---------- */
|
|
|
|
/* Hand a reply to whoever issued the matching link_connection_call().
|
|
* Unmatched replies are dropped: a broker is free to send us things we
|
|
* never asked for, and that is not a reason to drop the connection. */
|
|
static void deliver_reply(link_connection_t *conn, const struct link_msg *m)
|
|
{
|
|
link_reply_cb_t cb;
|
|
link_reply_t r;
|
|
void *userdata;
|
|
int i;
|
|
|
|
if (!conn->bus) {
|
|
__dbg("unsolicited reply, serial %u", m->reply_serial);
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < LINK_PENDING_CAP; i++) {
|
|
if (conn->bus->pending[i].used &&
|
|
conn->bus->pending[i].serial == m->reply_serial)
|
|
break;
|
|
}
|
|
if (i == LINK_PENDING_CAP) {
|
|
__dbg("unsolicited reply, serial %u", m->reply_serial);
|
|
return;
|
|
}
|
|
|
|
cb = conn->bus->pending[i].cb;
|
|
userdata = conn->bus->pending[i].userdata;
|
|
conn->bus->pending[i].used = 0;
|
|
|
|
if (!cb)
|
|
return;
|
|
|
|
__msg_to_reply(&r, m);
|
|
cb(conn, &r, userdata);
|
|
}
|
|
|
|
/* ---------- calls parked while their caller is identified ---------- */
|
|
|
|
/* Every park gets a token that is never issued twice, so a resolver
|
|
* answering late, twice, or after its connection went away resumes
|
|
* nothing rather than whatever call has since taken the slot. */
|
|
static struct link_parked *park(link_connection_t *conn, const uint8_t *frame,
|
|
size_t len, link_authz_t *tok)
|
|
{
|
|
struct link_bus *bus;
|
|
int i;
|
|
|
|
if (!frame || !len || len > LINK_PARKED_MSG_MAX)
|
|
return NULL;
|
|
|
|
bus = __bus_get(conn);
|
|
if (!bus)
|
|
return NULL;
|
|
|
|
for (i = 0; i < LINK_PARKED_CAP; i++) {
|
|
if (!bus->parked[i].tok)
|
|
break;
|
|
}
|
|
if (i == LINK_PARKED_CAP)
|
|
return NULL;
|
|
|
|
bus->parked[i].tok = ++bus->next_tok;
|
|
bus->parked[i].conn = conn;
|
|
bus->parked[i].stamp = __now_ms();
|
|
bus->parked[i].len = len;
|
|
memcpy(bus->parked[i].buf, frame, len);
|
|
*tok = bus->parked[i].tok;
|
|
|
|
return &bus->parked[i];
|
|
}
|
|
|
|
static void unpark(struct link_parked *p)
|
|
{
|
|
p->tok = 0;
|
|
p->conn = NULL;
|
|
}
|
|
|
|
/* A resolver that answers late, or never, would otherwise hold both
|
|
* the slot and the caller forever. Returns how many are still parked;
|
|
* link_connection_expire() is what calls this. */
|
|
int __dispatch_expire_parked(link_connection_t *conn, unsigned int age_ms)
|
|
{
|
|
uint64_t now;
|
|
int i, live = 0;
|
|
|
|
if (!conn->bus)
|
|
return 0;
|
|
|
|
now = __now_ms();
|
|
for (i = 0; i < LINK_PARKED_CAP; i++) {
|
|
struct link_parked *p = &conn->bus->parked[i];
|
|
uint8_t buf[LINK_PARKED_MSG_MAX];
|
|
struct link_msg msg;
|
|
size_t len;
|
|
|
|
if (!p->tok)
|
|
continue;
|
|
|
|
if (now - p->stamp < age_ms) {
|
|
live++;
|
|
continue;
|
|
}
|
|
|
|
/* Free the slot before replying, as link_uid_resolved()
|
|
* does: the send path must not find it still parked. */
|
|
len = p->len;
|
|
memcpy(buf, p->buf, len);
|
|
unpark(p);
|
|
|
|
if (__msg_parse(buf, len, &msg) <= 0)
|
|
continue;
|
|
|
|
__dbg("timed out %s, nobody said who the caller was",
|
|
msg.member ? msg.member : "call");
|
|
(void)__send_error(conn, &msg,
|
|
"org.freedesktop.DBus.Error.TimedOut",
|
|
"Timed out identifying the caller");
|
|
}
|
|
|
|
return live;
|
|
}
|
|
|
|
void __dispatch_forget_conn(link_connection_t *conn)
|
|
{
|
|
struct link_bus *bus = conn->bus;
|
|
int i;
|
|
|
|
if (!bus)
|
|
return;
|
|
|
|
/* Drop parked calls first. A pending callback below may try to
|
|
* resolve one, and resuming a dispatch on a connection that is
|
|
* being torn down is no use to anyone; an invalidated slot makes
|
|
* that resolve a no-op instead. */
|
|
for (i = 0; i < LINK_PARKED_CAP; i++)
|
|
unpark(&bus->parked[i]);
|
|
|
|
for (i = 0; i < LINK_PENDING_CAP; i++) {
|
|
if (bus->pending[i].used && bus->pending[i].cb)
|
|
bus->pending[i].cb(conn, NULL, bus->pending[i].userdata);
|
|
bus->pending[i].used = 0;
|
|
}
|
|
|
|
__bus_free(conn);
|
|
}
|
|
|
|
static int dispatch_call(link_connection_t *conn, const struct link_msg *m,
|
|
const uint8_t *frame, size_t framelen,
|
|
const uid_t *known_uid);
|
|
|
|
void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid)
|
|
{
|
|
uint8_t buf[LINK_PARKED_MSG_MAX];
|
|
struct link_parked *p = NULL;
|
|
struct link_msg msg;
|
|
size_t len;
|
|
int i;
|
|
|
|
if (!conn || !conn->bus || !tok)
|
|
return;
|
|
|
|
for (i = 0; i < LINK_PARKED_CAP; i++) {
|
|
if (conn->bus->parked[i].tok == tok) {
|
|
p = &conn->bus->parked[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!p)
|
|
return; /* stale handle, already answered */
|
|
|
|
/* Copy the message out and free the slot before dispatching:
|
|
* the handler may park a call of its own. */
|
|
len = p->len;
|
|
memcpy(buf, p->buf, len);
|
|
unpark(p);
|
|
|
|
if (__msg_parse(buf, len, &msg) <= 0)
|
|
return;
|
|
|
|
__dbg("resumed %s, caller uid %d", msg.member ? msg.member : "call", (int)uid);
|
|
(void)dispatch_call(conn, &msg, NULL, 0, &uid);
|
|
}
|
|
|
|
int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen)
|
|
{
|
|
if (m->type == LINK_MSG_METHOD_RETURN || m->type == LINK_MSG_ERROR) {
|
|
deliver_reply(conn, m);
|
|
return 0;
|
|
}
|
|
|
|
if (m->type != LINK_MSG_METHOD_CALL) {
|
|
/* Signals from a client to PID 1 are nonsense; drop. */
|
|
return 0;
|
|
}
|
|
|
|
return dispatch_call(conn, m, conn->rxbuf, framelen, NULL);
|
|
}
|
|
|
|
/* Who may invoke a privileged method. Without an authorizer, only
|
|
* root, which is what libink can decide on its own. */
|
|
static int caller_may(link_server_t *srv, uid_t uid,
|
|
const gid_t *groups, int ngroups)
|
|
{
|
|
if (uid == LINK_UID_UNKNOWN)
|
|
return 0;
|
|
if (srv && srv->authorizer)
|
|
return srv->authorizer(uid, groups, ngroups, srv->authz_userdata);
|
|
|
|
return uid == 0;
|
|
}
|
|
|
|
/* Ask who is calling on a broker connection, where the message is the
|
|
* only evidence. Returns 0 with *uid set, 1 when the call was parked
|
|
* and will be dispatched again once the resolver answers, -1 when the
|
|
* caller cannot be identified, and -2 when we have no room to ask. */
|
|
#define CALLER_UID_BUSY (-2)
|
|
|
|
static int resolve_caller(link_connection_t *conn, const struct link_msg *m,
|
|
const uint8_t *frame, size_t framelen, uid_t *uid)
|
|
{
|
|
link_server_t *srv = conn->server;
|
|
struct link_parked *p;
|
|
link_authz_t tok;
|
|
int rc;
|
|
|
|
if (!srv || !srv->uid_resolver || !m->sender)
|
|
return -1;
|
|
|
|
/* Enforce the rule link.h states, rather than trusting every
|
|
* resolver to remember it: a truncated sender key would let two
|
|
* callers share one identity. */
|
|
if (strlen(m->sender) >= LINK_SENDER_MAX) {
|
|
__dbg("sender name too long, refusing to identify it");
|
|
return -1;
|
|
}
|
|
|
|
/* Park first so the resolver has somewhere to answer, then let
|
|
* it release the slot immediately if it already knew. */
|
|
p = park(conn, frame, framelen, &tok);
|
|
if (!p)
|
|
return CALLER_UID_BUSY;
|
|
|
|
rc = srv->uid_resolver(conn, m->sender, tok, uid, srv->uid_userdata);
|
|
if (rc != 1)
|
|
unpark(p);
|
|
else
|
|
__dbg("parked %s from %s, awaiting caller uid", m->member, m->sender);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int dispatch_call(link_connection_t *conn, const struct link_msg *m,
|
|
const uint8_t *frame, size_t framelen,
|
|
const uid_t *known_uid)
|
|
{
|
|
struct link_object *o;
|
|
struct link_vtable_entry *e = NULL;
|
|
const link_method_t *meth;
|
|
struct link_call call;
|
|
ssize_t blen;
|
|
uid_t call_uid;
|
|
int rc;
|
|
|
|
/* What a handler sees via link_call_uid(). Unresolved on a broker
|
|
* connection until a privileged method forces the question. */
|
|
call_uid = known_uid ? *known_uid : conn->peer_uid;
|
|
|
|
if (!m->path || !m->member) {
|
|
__dbg("malformed call, no path or member");
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Method call without path or member");
|
|
}
|
|
|
|
__dbg("call %s %s.%s from %s", m->path,
|
|
m->interface ? m->interface : "-", m->member,
|
|
m->sender ? m->sender : "peer");
|
|
|
|
/* Built-in DBus interfaces (Hello, Ping, Introspect, Properties)
|
|
* are handled here before object-tree lookup, which means they
|
|
* also run before the LINK_METHOD_PRIVILEGED authz gate further
|
|
* down. The current set is read-only; do NOT introduce a
|
|
* state-changing built-in without first adding equivalent
|
|
* authorisation inside __handle_builtin. */
|
|
rc = __handle_builtin(conn, m);
|
|
if (rc <= 0)
|
|
return rc; /* 0 = handled & sent, -1 = send failed, drop */
|
|
/* rc == 1: not a built-in, fall through to the object tree. */
|
|
|
|
o = find_object(conn->server, m->path);
|
|
if (!o) {
|
|
__dbg("no such object %s", m->path);
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.UnknownObject",
|
|
"No such object");
|
|
}
|
|
|
|
meth = resolve(o, m->interface, m->member, &e);
|
|
if (!meth) {
|
|
__dbg("no such method %s on %s", m->member, m->path);
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.UnknownMethod",
|
|
"No such method on this object");
|
|
}
|
|
|
|
/* Validate signature: client must match the declared in_sig. */
|
|
{
|
|
const char *got = m->signature ? m->signature : "";
|
|
const char *want = meth->in_sig ? meth->in_sig : "";
|
|
|
|
if (strcmp(got, want) != 0) {
|
|
__dbg("%s takes '%s', caller sent '%s'", m->member, want, got);
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.InvalidArgs",
|
|
"Argument signature mismatch");
|
|
}
|
|
}
|
|
|
|
/* Per-method authorization. PRIVILEGED methods require uid 0.
|
|
* On an ordinary connection the peer's uid was captured via
|
|
* SO_PEERCRED at accept time and verified against the AUTH
|
|
* EXTERNAL claim, so conn->peer_uid is the answer. A broker
|
|
* connection carries every caller at once, so who is asking has
|
|
* to be established per message, which may park the call. */
|
|
if (meth->flags & LINK_METHOD_PRIVILEGED) {
|
|
if (conn->broker && !known_uid) {
|
|
rc = resolve_caller(conn, m, frame, framelen, &call_uid);
|
|
if (rc == 1)
|
|
return 0; /* parked, resumed later */
|
|
|
|
if (rc == CALLER_UID_BUSY) {
|
|
/* Not a permission problem: root may well
|
|
* be asking, we just have no slot to find
|
|
* out in. Say so, it is retryable. */
|
|
__dbg("no free slot to identify %s", m->sender);
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.LimitsExceeded",
|
|
"Too many calls awaiting authorization");
|
|
}
|
|
if (rc < 0)
|
|
call_uid = (uid_t)-1;
|
|
}
|
|
|
|
/* Local peers carry a kernel-captured group set; a call
|
|
* resolved over a broker carries only its uid, so the
|
|
* authorizer sees no groups and falls back to root-only. */
|
|
if (!caller_may(conn->server, call_uid,
|
|
known_uid ? NULL : conn->peer_groups,
|
|
known_uid ? 0 : conn->peer_ngroups)) {
|
|
__dbg("denied %s, caller uid %d is not privileged",
|
|
m->member, (int)call_uid);
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.AccessDenied",
|
|
"Caller is not privileged for this method");
|
|
}
|
|
}
|
|
|
|
memset(&call, 0, sizeof(call));
|
|
call.conn = conn;
|
|
call.uid = call_uid;
|
|
call.incoming = *m;
|
|
__r_init(&call.read_cursor, m->body, m->body_avail);
|
|
|
|
rc = meth->handler(&call, e->userdata);
|
|
|
|
/* Every path returns the status of whatever it put on the wire,
|
|
* so a failed send propagates out and the read loop drops the
|
|
* peer rather than leaving a half-written frame on a live link.
|
|
*
|
|
* The handler sent its own reply or error: its return value is
|
|
* that send's status. */
|
|
if (call.error_sent)
|
|
return rc;
|
|
|
|
if (call.reply_consumed) {
|
|
blen = __w_finish(&call.reply_writer);
|
|
if (blen < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.Failed",
|
|
"Reply marshalling overflow");
|
|
return __send_method_return(conn, m, meth->out_sig,
|
|
conn->txbuf, (size_t)blen);
|
|
}
|
|
|
|
/* Nothing sent yet: a negative return becomes an error reply, a
|
|
* zero return an empty method return. */
|
|
if (rc < 0)
|
|
return __send_error(conn, m,
|
|
"org.freedesktop.DBus.Error.Failed",
|
|
"Handler failed");
|
|
|
|
return __send_method_return(conn, m, NULL, NULL, 0);
|
|
}
|