Commit Graph
8 Commits
Author SHA1 Message Date
Joachim Wiberg 1e508f9168 libink: trace connections, calls, and authorization decisions
The legacy socket logs a line per command under initctl debug; the bus
logged nothing, so the transport that now carries most of initctl was
the one you could not watch.

libink gets a logger hook rather than a dependency on Finit's: it
passes the emitting function and a formatted message, and dbus.c hands
both to logit() so the two sources read alike.  Trace points cover the
connection lifecycle, every inbound call, and why a call was refused.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:06 +02:00
Joachim Wiberg 63aabaa6df 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>
2026-08-13 10:15:06 +02:00
Joachim Wiberg d710a23513 dbus: gate the bus socket like INIT_SOCKET
The D-Bus socket was bound world read/write, on the reasoning that
SO_PEERCRED authorizes each method anyway.  That leaves the read-only
surface open to every local user, and it quietly ignores --with-group:
a system that restricts initctl to the wheel group still handed the
same service state to anyone who asked over the bus.

Bind it 0660 and chown it to the configured group, the same gate the
fallback socket has always had.  libink takes the mode as an argument
rather than assuming one, since who may connect is the embedder's
policy, not the library's.

The mode is applied at bind(), so there is no window where the socket
is more permissive than intended.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:14:48 +02:00
Joachim Wiberg c71ccce742 libink: a broker peer is not an ordinary client
libink was written peer to peer, where one connection is one client
and one principal.  Attaching to a message bus breaks both halves of
that, and two things followed from it.

Signals never reached the system bus.  Fan-out is gated on the peer
having sent AddMatch, but a broker subscribes for its own clients and
never sends us one, so every ServiceStateChanged was dropped on the
floor.  A connection attached with LINK_ATTACH_BROKER gets them all.

Hello, AddMatch and RemoveMatch write per-connection state.  Shared by
every caller behind a broker, that lets one sender exhaust the match
cap or drop another's rule, so we leave all three to the bus, whose
job they are.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:14:48 +02:00
Joachim Wiberg b55dada80b libink: a message bus is not a peer
libink was written against the only bus it had, its own, where the
peer on the other end is the client.  A broker is not: it routes for
senders it names itself, expects a DESTINATION on anything addressed
through it, and answers on its own schedule rather than next.

Runlevels go on the wire as S and N rather than the digits Finit
keeps internally, since that is what a caller outside Finit means by
one.

The library stays a convenience library, linked into finit and
initctl and installed nowhere: the ABI promise waits until libink is
its own project.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:26 +02:00
Joachim Wiberg 6310d9e760 initctl: the status views over D-Bus
The summary table, the per-service detail, JSON and the quiet and
ident forms all read state Finit already publishes, so they read it
from the bus like everything else rather than through a second path
that has to be kept in step.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:26 +02:00
Joachim Wiberg ebc0ef62e6 libink/finit: properties, and org.finit on the system bus
Runlevel and version are state, not actions, so they belong behind
org.freedesktop.DBus.Properties rather than another method each.

Finit also claims org.finit on the system bus when it finds one, so
ordinary D-Bus clients can reach it without knowing about
/run/finit/bus.  Opportunistic on purpose: no dbus-daemon is a normal
state for the systems Finit runs on, not an error to report.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:14 +02:00
Joachim Wiberg 0a269f3298 libink: a brokerless D-Bus implementation for Finit
Finit had no way to answer the question every service manager gets
asked: what is running, and change it.  D-Bus is how the rest of
userspace asks, but linking libdbus, sd-bus or GIO into PID 1 buys a
dependency, an allocator and a main loop we do not control.

So libink: the wire format, an object tree, and a bus of Finit's own
at /run/finit/bus, gated like INIT_SOCKET.  It speaks the standard
org.freedesktop.DBus, .Peer, .Introspectable interfaces, and Finit's
own Manager1, Service1 and Cond1 on top.  Methods that change
something are marked privileged and answered only for a caller the
kernel vouched for, via SO_PEERCRED.

Server and client both, since initctl is the first thing that needs
to talk to it, and its Start/Stop/Restart/Reload now go over the bus
rather than the legacy socket.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 09:28:14 +02:00