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

407 lines
11 KiB
C

/* libink — D-Bus wire protocol: message header parsing and building.
*
* Implements the binary message header format described in the
* D-Bus specification, sections "Message Format" and "Header Fields".
* Bodies are deliberately not parsed here — that's the marshaller's
* job (marshal.c).
*
* Native byte order is assumed to be little-endian; messages with the
* 'B' endianness flag are rejected for now (every conforming client
* on the platforms Finit targets sends 'l').
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <sys/types.h>
#include "proto.h"
#define HDR_FIXED_SIZE LINK_HDR_FIXED_SIZE
#define MAX_MSG_SIZE (128 * 1024) /* sanity limit for PID 1 */
#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1)))
static inline uint32_t rd_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
static inline void wr_u32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v & 0xff);
p[1] = (uint8_t)((v >> 8) & 0xff);
p[2] = (uint8_t)((v >> 16) & 0xff);
p[3] = (uint8_t)((v >> 24) & 0xff);
}
/* Parse a (length-prefixed, nul-terminated) DBus STRING or PATH from
* the header field array. Returns a pointer into buf or NULL on
* malformed input. *consumed receives the bytes used including the
* nul. */
static const char *parse_string(const uint8_t *buf, size_t avail, size_t *consumed)
{
uint32_t len;
if (avail < 4)
return NULL;
len = rd_u32(buf);
if (len >= avail - 4) /* need room for len bytes + nul */
return NULL;
if (buf[4 + len] != 0)
return NULL;
*consumed = 4 + len + 1;
return (const char *)(buf + 4);
}
/* Parse a SIGNATURE (1-byte length, nul-terminated). */
static const char *parse_signature(const uint8_t *buf, size_t avail, size_t *consumed)
{
uint32_t len;
if (avail < 1)
return NULL;
len = buf[0];
if (len + 2 > avail)
return NULL;
if (buf[1 + len] != 0)
return NULL;
*consumed = 1 + len + 1;
return (const char *)(buf + 1);
}
ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out)
{
uint32_t fields_len, total_hdr, body_off, total;
const uint8_t *fp, *fend;
memset(out, 0, sizeof(*out));
if (len < HDR_FIXED_SIZE)
return 0;
/* XXX: a big-endian sender is refused rather than byte-swapped.
* Every conforming client on the platforms Finit targets sends
* 'l', so the swap paths have not been worth writing. */
if (buf[0] != 'l') {
errno = EPROTO;
return -1;
}
if (buf[3] != LINK_PROTOCOL_VERSION) {
errno = EPROTONOSUPPORT;
return -1;
}
out->endian = buf[0];
out->type = buf[1];
out->flags = buf[2];
out->body_len = rd_u32(buf + 4);
out->serial = rd_u32(buf + 8);
fields_len = rd_u32(buf + 12);
if (fields_len > MAX_MSG_SIZE || out->body_len > MAX_MSG_SIZE) {
errno = E2BIG;
return -1;
}
total_hdr = HDR_FIXED_SIZE + fields_len;
body_off = (uint32_t)ALIGN_UP(total_hdr, 8);
total = body_off + out->body_len;
if (len < total)
return 0; /* need more bytes */
/* Walk the array of (byte field-code, variant). */
fp = buf + HDR_FIXED_SIZE;
fend = fp + fields_len;
while (fp < fend) {
uint8_t code;
const char *vsig;
size_t used;
fp = buf + ALIGN_UP((size_t)(fp - buf), 8);
if (fp >= fend)
break;
code = *fp++;
vsig = parse_signature(fp, (size_t)(fend - fp), &used);
if (!vsig) {
errno = EPROTO;
return -1;
}
fp += used;
if (vsig[0] == 's' || vsig[0] == 'o') {
fp = buf + ALIGN_UP((size_t)(fp - buf), 4);
if (fp >= fend) { errno = EPROTO; return -1; }
const char *s = parse_string(fp, (size_t)(fend - fp), &used);
if (!s) { errno = EPROTO; return -1; }
switch (code) {
case LINK_HDR_PATH: out->path = s; break;
case LINK_HDR_INTERFACE: out->interface = s; break;
case LINK_HDR_MEMBER: out->member = s; break;
case LINK_HDR_ERROR_NAME: out->error_name = s; break;
case LINK_HDR_DESTINATION: out->destination = s; break;
case LINK_HDR_SENDER: out->sender = s; break;
}
fp += used;
} else if (vsig[0] == 'g') {
const char *s = parse_signature(fp, (size_t)(fend - fp), &used);
if (!s) { errno = EPROTO; return -1; }
if (code == LINK_HDR_SIGNATURE)
out->signature = s;
fp += used;
} else if (vsig[0] == 'u') {
fp = buf + ALIGN_UP((size_t)(fp - buf), 4);
if (fp + 4 > fend) { errno = EPROTO; return -1; }
uint32_t v = rd_u32(fp);
if (code == LINK_HDR_REPLY_SERIAL)
out->reply_serial = v;
fp += 4;
} else {
/* Unknown field type — skip whole message. */
errno = EPROTO;
return -1;
}
}
out->body = buf + body_off;
out->body_avail = out->body_len;
return (ssize_t)total;
}
/* ---------- builders ---------- */
/* Append a (byte field-code, variant) entry to a header-fields array,
* with the entry pre-aligned to 8 bytes. */
static int put_field_string(uint8_t *buf, size_t cap, size_t *off,
uint8_t code, char vsig_char,
const char *value)
{
size_t o = *off;
size_t pad = ALIGN_UP(o, 8) - o;
size_t len = strlen(value);
/* Padding for struct alignment */
while (pad-- > 0) {
if (o >= cap) return -1;
buf[o++] = 0;
}
/* code, variant signature (1B len + 1B char + 1B nul) */
if (o + 4 > cap) return -1;
buf[o++] = code;
buf[o++] = 1;
buf[o++] = (uint8_t)vsig_char;
buf[o++] = 0;
if (vsig_char == 's' || vsig_char == 'o') {
/* 4-byte align for u32 length */
while (o & 3) {
if (o >= cap) return -1;
buf[o++] = 0;
}
if (o + 4 + len + 1 > cap) return -1;
wr_u32(buf + o, (uint32_t)len);
o += 4;
memcpy(buf + o, value, len);
o += len;
buf[o++] = 0;
} else if (vsig_char == 'g') {
if (o + 1 + len + 1 > cap) return -1;
buf[o++] = (uint8_t)len;
memcpy(buf + o, value, len);
o += len;
buf[o++] = 0;
} else {
return -1;
}
*off = o;
return 0;
}
static int put_field_u32(uint8_t *buf, size_t cap, size_t *off,
uint8_t code, uint32_t value)
{
size_t o = *off;
size_t pad = ALIGN_UP(o, 8) - o;
while (pad-- > 0) {
if (o >= cap) return -1;
buf[o++] = 0;
}
if (o + 8 > cap) return -1;
buf[o++] = code;
buf[o++] = 1;
buf[o++] = 'u';
buf[o++] = 0;
while (o & 3) {
if (o >= cap) return -1;
buf[o++] = 0;
}
if (o + 4 > cap) return -1;
wr_u32(buf + o, value);
o += 4;
*off = o;
return 0;
}
static ssize_t finalize_header(uint8_t *buf, size_t cap,
uint8_t type, uint8_t flags,
uint32_t body_len, uint32_t serial,
size_t fields_end)
{
size_t hdr_end = fields_end;
size_t pad = ALIGN_UP(hdr_end, 8) - hdr_end;
buf[0] = 'l';
buf[1] = type;
buf[2] = flags;
buf[3] = LINK_PROTOCOL_VERSION;
wr_u32(buf + 4, body_len);
wr_u32(buf + 8, serial);
wr_u32(buf + 12, (uint32_t)(hdr_end - HDR_FIXED_SIZE));
while (pad-- > 0) {
if (hdr_end >= cap) return -1;
buf[hdr_end++] = 0;
}
return (ssize_t)hdr_end;
}
ssize_t __msg_build_return(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t reply_serial,
const char *destination,
const char *signature, uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE)
return -1;
if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0)
return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_METHOD_RETURN,
LINK_FLAG_NO_REPLY_EXPECTED,
body_len, serial, off);
}
ssize_t __msg_build_error(uint8_t *buf, size_t cap,
uint32_t serial, uint32_t reply_serial,
const char *destination,
const char *error_name,
const char *signature, uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE || !error_name)
return -1;
if (put_field_u32(buf, cap, &off, LINK_HDR_REPLY_SERIAL, reply_serial) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_ERROR_NAME, 's', error_name) < 0)
return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_ERROR,
LINK_FLAG_NO_REPLY_EXPECTED,
body_len, serial, off);
}
ssize_t __msg_build_signal(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *signature, uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE || !path || !interface || !member)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_SIGNAL,
LINK_FLAG_NO_REPLY_EXPECTED,
body_len, serial, off);
}
ssize_t __msg_build_method_call(uint8_t *buf, size_t cap,
uint32_t serial,
const char *path,
const char *interface,
const char *member,
const char *destination,
const char *signature,
uint32_t body_len)
{
size_t off = HDR_FIXED_SIZE;
if (cap < HDR_FIXED_SIZE || !path || !member)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_PATH, 'o', path) < 0)
return -1;
if (interface &&
put_field_string(buf, cap, &off, LINK_HDR_INTERFACE, 's', interface) < 0)
return -1;
if (put_field_string(buf, cap, &off, LINK_HDR_MEMBER, 's', member) < 0)
return -1;
if (destination &&
put_field_string(buf, cap, &off, LINK_HDR_DESTINATION, 's', destination) < 0)
return -1;
if (signature && *signature &&
put_field_string(buf, cap, &off, LINK_HDR_SIGNATURE, 'g', signature) < 0)
return -1;
return finalize_header(buf, cap, LINK_MSG_METHOD_CALL,
/* flags=0: we expect a reply */
0,
body_len, serial, off);
}
size_t __msg_header_size(const struct link_msg *m)
{
(void)m;
/* Generous upper bound used by callers to size send buffers. */
return 512;
}
void __msg_to_reply(link_reply_t *r, const struct link_msg *m)
{
r->type = m->type;
r->signature = m->signature;
r->error_name = m->error_name;
r->path = m->path;
r->interface = m->interface;
r->member = m->member;
r->body = m->body_avail ? m->body : NULL;
r->body_len = m->body_avail;
}