libink/dbus: identify the caller behind a broker

On the local bus SO_PEERCRED says who is calling and the kernel is the
one saying it.  Behind a broker one connection carries every caller,
so that credential describes dbus-daemon and nothing else, and every
privileged method was refused there, root included.

Ask the bus driver instead.  libink parks the call and hands us the
sender; we ask GetConnectionUnixUser and answer when the reply lands,
through the same event loop as everything else.  Nothing blocks:
blocking in PID 1 is why libuEv exists.  That needs calls libink can
make on a connection it already has, so it gained those too.

Answers are cached, since a bus never reuses a unique name while it
runs.  Not across a restart though: a new dbus-daemon numbers from
scratch and :1.7 becomes somebody else, so the cache goes when the
broker does.  A sender name too long to key on is refused rather than
truncated, two callers sharing a truncated key would share an
identity.

Privilege is no longer uid 0 alone.  The socket is already owned by
the --with-group group, so refusing its members every method that
changes anything left a wheel user able to open the bus and unable to
reboot.  Both gates now say the same thing.

Group membership needs NSS, which the C library loads with dlopen(),
so the lookup is compiled out where Finit is built to link statically.
That leaves such a build root-only, which is worth saying out loud
rather than leaving to be discovered.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-08-13 10:15:06 +02:00
1 parent d710a23513
commit 63aabaa6df
17 files changed
+774 -90

No files matched your search

+2 -1
View File
@@ -44,7 +44,8 @@ Below are a few of the main switches to configure:
* `--enable-static`: Build Finit statically. The plugins will be
built-ins (.o files) and all external libraries, except the C library
will be linked statically.
will be linked statically. Privileged D-Bus methods then accept only
`root`, see [Authorization](dbus.md#authorization)
* `--enable-kernel-cmdline`: Enable Finit pre-4.1 parsing of init args from
`/proc/cmdline`, this is *not recommended* since Finit may be running as the
+27 -10
View File
@@ -204,20 +204,37 @@ conditions belong to Finit's state machine.
Authorization
-------------
Privileged methods reject any caller whose peer `uid` isn't 0. On the
**local** bus the kernel's `SO_PEERCRED` socket option tells Finit exactly
who's calling, so privilege escalation through the bus is impossible.
Privileged methods accept `root`, and any caller belonging to the group given
to `--with-group` at build time. That is the same set the socket mode already
admits, so the two gates agree instead of the socket letting the group in and
every method turning it away.
On the **system** bus, all incoming traffic is treated as unprivileged: it
arrives through `dbus-daemon` (typically running as root) and Finit cannot yet
ask the daemon for the real requester's uid via `GetConnectionUnixUser`. This
means external tooling can freely `Get`/`Introspect`/`ListServices`, but every
state-changing method returns `org.freedesktop.DBus.Error.AccessDenied`.
Per-sender uid lookup is on the roadmap.
A build configured with `--enable-static` accepts only `root`. Group
membership comes from NSS, which the C library loads with `dlopen()`, and a
build meant to link statically cannot count on that, so the lookup is compiled
out rather than left to fail open. The socket mode is unchanged, so the group
can still connect, it just cannot invoke a privileged method.
On the **local** bus the kernel decides this at `connect()`, supplementary
groups included, and `SO_PEERCRED` tells Finit exactly who is calling, so
privilege escalation through the bus is impossible.
On the **system** bus one connection carries every caller, so `SO_PEERCRED`
describes `dbus-daemon` rather than whoever asked. For a privileged method
Finit asks the bus driver `GetConnectionUnixUser` about the message sender and
holds the call until the answer arrives. Nothing blocks: the reply comes back
through the same event loop as everything else, and the held call is then
dispatched or refused on its merits.
Answers are cached per sender. A bus never reuses a unique name while it
runs, so an answer holds for as long as that bus does; Finit empties the cache
when the broker goes away, since a new one numbers its clients from scratch.
A caller Finit cannot identify is refused, so the failure mode is a denial
rather than an escalation.
When a privileged method is rejected the error name is exactly
`org.freedesktop.DBus.Error.AccessDenied`, and the body carries a short reason
string (e.g. `"permission denied: Start requires root"`).
string.
`initctl` integration
---------------------