Files
finit/test/dbus-auth.sh
T
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

316 lines
11 KiB
Bash
Executable File

#!/bin/sh
# End-to-end smoke test for libink:
# - AUTH EXTERNAL handshake (happy and wrong-uid paths)
# - org.freedesktop.DBus.Hello
# - org.freedesktop.DBus.Introspectable.Introspect (root, manager)
# - org.finit.Manager1.ListServices
# - Error reply for an unknown method.
set -eu
TEST_DIR=$(dirname "$0")
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
CLIENT=/sbin/dbus-auth-client
BUS=/run/finit/bus
if ! texec test -x "$CLIENT"; then
skip "dbus-auth-client not built (configured with --disable-dbus?)"
fi
say "Wait for $BUS to appear"
retry "texec test -S $BUS"
say "Socket mode is 0666"
mode=$(texec stat -c %a "$BUS")
assert "Socket mode is 666 (got $mode)" "$mode" = "666"
# ---------- AUTH ----------
say "AUTH EXTERNAL: claim correct UID (root = 0)"
reply=$(texec "$CLIENT" auth "$BUS" 0)
assert "Reply starts with OK (got: $reply)" "${reply%% *}" = "OK"
guid=${reply#OK }
assert "GUID is 32 hex chars (got: $guid)" \
"$(printf '%s' "$guid" | tr -d '0-9a-f' | wc -c)" -eq 0
assert "GUID length is 32 (got: ${#guid})" "${#guid}" -eq 32
say "AUTH EXTERNAL: wrong UID is rejected"
set +e
wrong_reply=$(texec "$CLIENT" auth "$BUS" 1)
wrong_rc=$?
set -e
assert "Wrong UID rejected (rc=$wrong_rc, reply: $wrong_reply)" \
"$wrong_rc" -eq 1
say "Two sequential AUTH connections get different GUIDs"
r1=$(texec "$CLIENT" auth "$BUS" 0)
r2=$(texec "$CLIENT" auth "$BUS" 0)
g1=${r1#OK }
g2=${r2#OK }
assert "Per-connection GUIDs differ ($g1 vs $g2)" "$g1" != "$g2"
# ---------- Built-in interfaces ----------
say "Hello() returns a unique name beginning with ':1.'"
name=$(texec "$CLIENT" hello "$BUS")
case "$name" in
:1.*) assert "Hello returned a :1.N name (got $name)" 0 -eq 0 ;;
*) fail "Hello returned unexpected name: $name" ;;
esac
say "Two Hello() calls produce different unique names"
n1=$(texec "$CLIENT" hello "$BUS")
n2=$(texec "$CLIENT" hello "$BUS")
assert "Unique names increment ($n1 vs $n2)" "$n1" != "$n2"
say "Introspect on root path returns valid XML referencing /manager"
xml=$(texec "$CLIENT" introspect "$BUS" /)
case "$xml" in
*'<node'*) assert "XML has <node> root (good)" 0 -eq 0 ;;
*) fail "Root introspect missing <node>: $xml" ;;
esac
say "Introspect on /org/finit/manager exposes Manager1.ListServices"
xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager)
case "$xml" in
*'org.finit.Manager1'*'ListServices'*)
assert "Manager1 and ListServices visible in XML" 0 -eq 0 ;;
*)
fail "Manager1 XML missing; got: $xml" ;;
esac
# ---------- Real method call ----------
say "Manager1.ListServices returns the running services"
list=$(texec "$CLIENT" liststrings "$BUS" /org/finit/manager \
org.finit.Manager1 ListServices)
assert "ListServices returned at least one service" \
"$(printf '%s' "$list" | wc -l | tr -d ' ')" -ge 1
echo "$list"
# ---------- Method with arguments ----------
say "Manager1.Reload (void) succeeds"
texec "$CLIENT" call-void "$BUS" /org/finit/manager \
org.finit.Manager1 Reload >/dev/null \
|| fail "Reload returned non-zero"
assert "Reload void method ok" 0 -eq 0
say "Manager1.Stop with bogus identity returns NoSuchService error"
set +e
texec "$CLIENT" call-s "$BUS" /org/finit/manager \
org.finit.Manager1 Stop "no-such-service-here" >/tmp/dbus-stop.out 2>&1
stop_rc=$?
set -e
assert "Bogus service rejected (rc=$stop_rc)" "$stop_rc" -eq 1
case "$(cat /tmp/dbus-stop.out)" in
*NoSuchService*) assert "Error is NoSuchService" 0 -eq 0 ;;
*) fail "Unexpected error reply: $(cat /tmp/dbus-stop.out)" ;;
esac
# ---------- Authorization ----------
say "Manager1.Restart from non-root is rejected with AccessDenied"
set +e
texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \
org.finit.Manager1 Restart "testserv" >/tmp/dbus-authz.out 2>&1
authz_rc=$?
set -e
assert "Non-root Restart rejected (rc=$authz_rc)" "$authz_rc" -eq 1
case "$(cat /tmp/dbus-authz.out)" in
*AccessDenied*) assert "Error is AccessDenied" 0 -eq 0 ;;
*) fail "Unexpected error: $(cat /tmp/dbus-authz.out)" ;;
esac
say "Manager1.ListServices is reachable as non-root (not blocked by authz)"
# call-s-as-uid sends an "s" body; ListServices expects "", so the
# server must reply with org.freedesktop.DBus.Error.InvalidArgs.
# Asserting that *positive* marker (not just "no AccessDenied")
# ensures we don't silently pass if setuid() failed or the client
# never reached the server (e.g. a transport error would print
# neither AccessDenied nor InvalidArgs).
set +e
result=$(texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/manager \
org.finit.Manager1 ListServices "" 2>&1)
set -e
case "$result" in
*AccessDenied*) fail "Non-root ListServices rejected by authz: $result" ;;
*InvalidArgs*) assert "Non-root reached signature check (InvalidArgs, not AccessDenied)" 0 -eq 0 ;;
*) fail "Unexpected reply from non-root ListServices: $result" ;;
esac
# ---------- Per-service objects (Service1) ----------
say "Manager1.GetService(keventd) returns the encoded object path"
path=$(texec "$CLIENT" get-service "$BUS" keventd)
expected="/org/finit/service/keventd"
assert "GetService returned expected path (got: $path)" "$path" = "$expected"
say "Introspect on the service object exposes Service1 methods"
xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/service/keventd)
case "$xml" in
*'org.finit.Service1'*'Restart'*)
assert "Service1.Restart visible in service-object XML" 0 -eq 0 ;;
*)
fail "Service1 not visible on /org/finit/service/keventd: $xml" ;;
esac
say "Service1.Restart on /org/finit/service/keventd succeeds"
texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \
org.finit.Service1 Restart >/dev/null \
|| fail "Service1.Restart returned non-zero"
assert "Per-service Restart ok" 0 -eq 0
say "Service1.Restart from non-root is rejected with AccessDenied"
set +e
texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/service/keventd \
org.finit.Service1 Restart >/tmp/dbus-svcauthz.out 2>&1
svc_authz_rc=$?
set -e
assert "Non-root Service1.Restart rejected (rc=$svc_authz_rc)" \
"$svc_authz_rc" -eq 1
case "$(cat /tmp/dbus-svcauthz.out)" in
*AccessDenied*) assert "Service1 authz fires" 0 -eq 0 ;;
*) fail "Expected AccessDenied, got: $(cat /tmp/dbus-svcauthz.out)" ;;
esac
# ---------- Signals ----------
say "Service1.Restart fires Manager1.ServiceStateChanged"
rm -f /tmp/dbus-sig.out
( texec "$CLIENT" monitor-signal "$BUS" \
"type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \
5000 > /tmp/dbus-sig.out 2>&1 ) &
mon_pid=$!
sleep 0.5
texec "$CLIENT" call-void "$BUS" /org/finit/service/keventd \
org.finit.Service1 Restart >/dev/null \
|| fail "Restart trigger returned non-zero"
set +e
wait "$mon_pid"
mon_rc=$?
set -e
assert "monitor saw a signal (rc=$mon_rc)" "$mon_rc" -eq 0
case "$(cat /tmp/dbus-sig.out)" in
*"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*)
assert "Signal payload contains the keventd identity" 0 -eq 0 ;;
*)
fail "Unexpected signal output: $(cat /tmp/dbus-sig.out)" ;;
esac
# ---------- Cond1 ----------
say "Cond1.Get returns 'off' for an unset condition"
result=$(texec "$CLIENT" call-s "$BUS" /org/finit/cond \
org.finit.Cond1 Get "no-such-cond")
case "$result" in
OK*) : ;; # ok, the cond reports a state, fall through
*) fail "Cond1.Get failed: $result" ;;
esac
say "Cond1.Set fires Cond1.ConditionChanged and Get reflects the change"
rm -f /tmp/dbus-cond.out
( texec "$CLIENT" monitor-signal "$BUS" \
"type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \
5000 > /tmp/dbus-cond.out 2>&1 ) &
cond_mon_pid=$!
sleep 0.5
texec "$CLIENT" call-s "$BUS" /org/finit/cond \
org.finit.Cond1 Set "dbus-test-cond" >/dev/null \
|| fail "Cond1.Set returned non-zero"
set +e
wait "$cond_mon_pid"
cond_mon_rc=$?
set -e
assert "Cond1 monitor saw a signal (rc=$cond_mon_rc)" "$cond_mon_rc" -eq 0
case "$(cat /tmp/dbus-cond.out)" in
*"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/dbus-test-cond"*on*)
assert "ConditionChanged carries usr/dbus-test-cond and 'on'" 0 -eq 0 ;;
*)
fail "Unexpected Cond1 signal: $(cat /tmp/dbus-cond.out)" ;;
esac
say "Cond1.Set/Clear on non-usr/* is rejected"
set +e
texec "$CLIENT" call-s "$BUS" /org/finit/cond \
org.finit.Cond1 Set "pid/sshd" >/tmp/dbus-condrej.out 2>&1
condrej_rc=$?
set -e
assert "pid/* rejected (rc=$condrej_rc)" "$condrej_rc" -eq 1
case "$(cat /tmp/dbus-condrej.out)" in
*InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-condrej.out)" ;;
esac
say "Cond1.Set from non-root is rejected with AccessDenied"
set +e
texec "$CLIENT" call-s-as-uid 1 "$BUS" /org/finit/cond \
org.finit.Cond1 Set "would-be-cond" >/tmp/dbus-condauthz.out 2>&1
ca_rc=$?
set -e
assert "Non-root Cond1.Set rejected (rc=$ca_rc)" "$ca_rc" -eq 1
case "$(cat /tmp/dbus-condauthz.out)" in
*AccessDenied*) assert "Cond1 authz fires" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-condauthz.out)" ;;
esac
say "AddMatch with a bogus key is rejected"
set +e
texec "$CLIENT" call-s "$BUS" /org/freedesktop/DBus \
org.freedesktop.DBus AddMatch "bogus='whatever'" >/tmp/dbus-match.out 2>&1
am_rc=$?
set -e
assert "Bad rule rejected (rc=$am_rc)" "$am_rc" -eq 1
case "$(cat /tmp/dbus-match.out)" in
*MatchRuleInvalid*) assert "Error is MatchRuleInvalid" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-match.out)" ;;
esac
# ---------- initctl port ----------
# initctl now talks to /run/finit/bus when available. Verify by
# subscribing to ServiceStateChanged on a background monitor and
# then running initctl restart -- if D-Bus is in use, the signal
# fires. If the legacy socket were still in use, the dbus subscriber
# would see nothing.
say "initctl restart drives D-Bus (signal observed via dbus-auth-client)"
rm -f /tmp/dbus-initctl-sig.out
( texec "$CLIENT" monitor-signal "$BUS" \
"type='signal',interface='org.finit.Manager1',member='ServiceStateChanged'" \
5000 > /tmp/dbus-initctl-sig.out 2>&1 ) &
ic_pid=$!
sleep 0.5
texec initctl restart keventd >/dev/null \
|| fail "initctl restart returned non-zero"
set +e
wait "$ic_pid"
ic_rc=$?
set -e
assert "ServiceStateChanged fired from initctl restart (rc=$ic_rc)" \
"$ic_rc" -eq 0
case "$(cat /tmp/dbus-initctl-sig.out)" in
*"SIGNAL org.finit.Manager1 ServiceStateChanged"*keventd*)
assert "initctl restart routed through D-Bus" 0 -eq 0 ;;
*)
fail "initctl restart didn't produce expected signal: $(cat /tmp/dbus-initctl-sig.out)" ;;
esac
say "initctl reload (no args) routes through Manager1.Reload"
texec initctl reload >/dev/null \
|| fail "initctl reload returned non-zero"
assert "initctl reload ok" 0 -eq 0
# ---------- Error reply ----------
say "Unknown method gets an org.freedesktop.DBus.Error.* reply"
set +e
texec "$CLIENT" unknown "$BUS"
unknown_rc=$?
set -e
assert "Unknown method returned an error (rc=$unknown_rc)" "$unknown_rc" -eq 0