From dd1390a6c29fe59732ead4e54a2db9e7a95e9fb9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 13 Aug 2026 09:28:14 +0200 Subject: [PATCH] initctl: monitor and condition control over the bus The bus can already answer questions and change services, so give initctl the two things it still did another way: watching signals as they happen, and getting or setting user conditions. The dbus tests move with it, split by area rather than one file that grew every time the library did. Signed-off-by: Joachim Wiberg --- src/initctl.c | 167 +++++- test/Makefile.am | 10 + test/dbus-auth.sh | 286 +---------- test/dbus-bus.sh | 62 +++ test/dbus-cond.sh | 69 +++ test/dbus-initctl.sh | 68 +++ test/dbus-manager.sh | 69 +++ test/dbus-service.sh | 71 +++ test/lib/Makefile.am | 2 +- test/lib/dbus-setup.sh | 20 + test/setup-sysroot.sh | 8 +- test/src/Makefile.am | 6 +- test/src/dbus-auth-client.c | 996 +++++++++++------------------------- 13 files changed, 854 insertions(+), 980 deletions(-) create mode 100755 test/dbus-bus.sh create mode 100755 test/dbus-cond.sh create mode 100755 test/dbus-initctl.sh create mode 100755 test/dbus-manager.sh create mode 100755 test/dbus-service.sh create mode 100644 test/lib/dbus-setup.sh diff --git a/src/initctl.c b/src/initctl.c index 0fcb408f..71f14263 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -42,6 +42,9 @@ #include "cgutil.h" #include "utmp-api.h" +/* Used by both do_cond_act and (with HAVE_DBUS) cond_dbus_call. */ +typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; + struct cmd { char *cmd; struct cmd *ctx; @@ -327,6 +330,139 @@ static int try_dbus_manager(const char *method, const char *arg_sig, return 0; return -1; /* LINK_CALL_FAIL or anything else: fall back */ } + +/* Try one Cond1.{Get,Set,Clear} call. On COND_GET success the helper + * fills *out_exit with the exit code (0 = on, 1 = off, 255 = flux). + * Outcomes: + * 1 call succeeded; for GET the result is in *out_exit, for + * SET/CLR the caller loops to the next arg + * 0 bus not reachable, or LINK_CALL_FAIL -- *bus is closed/NULLed + * and the caller should drop to the legacy filesystem path + * (LINK_CALL_ERROR exits via ERRX inside the helper) + * + * `*bus` is borrowed; the helper closes it (and sets NULL) on every + * exit path that leaves the bus unusable. */ +static int cond_dbus_call(link_client_t **bus, condop_t op, + const char *arg, int *out_exit) +{ + const char *method = (op == COND_GET) ? "Get" + : (op == COND_SET) ? "Set" : "Clear"; + int rc; + + if (!*bus) + return 0; + + rc = link_client_call_v(*bus, "/org/finit/cond", + "org.finit.Cond1", method, + "s", arg); + if (rc == LINK_CALL_OK) { + if (op == COND_GET) { + const link_reply_t *r = link_client_reply(*bus); + link_reader_t reader; + const char *state = NULL; + + if (r && r->body) { + link_reader_init(&reader, r->body, r->body_len); + link_r_string(&reader, &state); + } + if (verbose && state) + puts(state); + *out_exit = (state && !strcmp(state, "on")) ? 0 + : (state && !strcmp(state, "off")) ? 1 : 255; + } + return 1; + } + if (rc == LINK_CALL_ERROR) { + const link_reply_t *r = link_client_reply(*bus); + const char *err = (r && r->error_name) ? r->error_name : ""; + + link_client_close(*bus); + *bus = NULL; + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: cond %s requires root", method); + ERRX(73, "Failed %s condition <%s>: %s", + op == COND_SET ? "asserting" : "deasserting", + arg, *err ? err : "D-Bus error"); + } + /* LINK_CALL_FAIL */ + link_client_close(*bus); + *bus = NULL; + return 0; +} + +/* Subscribe to every signal on the bus and print one line per + * incoming message: + * HH:MM:SS interface.member(arg1, arg2, ...) + * + * Only string-typed leading args are decoded (matches what our two + * current signals -- ServiceStateChanged (sss) and ConditionChanged + * (ss) -- emit). Non-string args are silently skipped. Loops until + * the connection drops or the user hits ^C. */ +static int do_monitor(char *arg) +{ + link_client_t *c; + int rc; + + (void)arg; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + ERRX(1, "monitor requires the D-Bus socket at %s", FINIT_BUS_SOCKET); + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", "type='signal'"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + ERRX(1, "AddMatch failed (rc=%d)", rc); + } + + for (;;) { + const link_reply_t *r; + link_reader_t reader; + char ts[16]; + time_t now; + struct tm tm; + + rc = link_client_wait(c, -1); + if (rc < 0) { + link_client_close(c); + ERRX(1, "bus connection lost"); + } + if (rc > 0) /* impossible with timeout=-1, but harmless */ + continue; + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) + continue; + + now = time(NULL); + localtime_r(&now, &tm); + strftime(ts, sizeof(ts), "%H:%M:%S", &tm); + printf("%s %s.%s(", ts, + r->interface ? r->interface : "?", + r->member ? r->member : "?"); + + link_reader_init(&reader, r->body, r->body_len); + if (r->signature) { + const char *p; + int first = 1; + + for (p = r->signature; *p == 's'; p++) { + const char *s; + + if (link_r_string(&reader, &s) < 0) + break; + printf("%s%s", first ? "" : ", ", s); + first = 0; + } + } + printf(")\n"); + fflush(stdout); + } + + link_client_close(c); + return 0; +} #endif /* HAVE_DBUS */ static int do_start (char *arg) @@ -496,8 +632,6 @@ static int do_cond_dump(char *arg) return 0; } -typedef enum { COND_CLR, COND_SET, COND_GET } condop_t; - static cond_state_t cond_read(char *path) { int now, gen; @@ -527,6 +661,9 @@ static int do_cond_act(char *args, condop_t op) cond_state_t cstate; char path[256]; char *arg; +#ifdef HAVE_DBUS + link_client_t *bus = link_client_open(FINIT_BUS_SOCKET); +#endif if (!args || !args[0]) ERRX(2, "Invalid condition (empty)"); @@ -546,6 +683,22 @@ static int do_cond_act(char *args, condop_t op) ERRX(2, "Invalid condition (periods)"); } +#ifdef HAVE_DBUS + { + int exit_code; + + if (cond_dbus_call(&bus, op, arg, &exit_code)) { + if (op == COND_GET) { + link_client_close(bus); + return exit_code; + } + arg = strtok(NULL, " \t"); + continue; + } + /* bus is NULL now -- drop through to legacy */ + } +#endif + if (strchr(arg, '/')) snprintf(path, sizeof(path), _PATH_COND "%s", arg); else @@ -582,6 +735,10 @@ static int do_cond_act(char *args, condop_t op) arg = strtok(NULL, " \t"); } +#ifdef HAVE_DBUS + if (bus) + link_client_close(bus); +#endif return 0; } @@ -1696,6 +1853,9 @@ static int usage(int rc) " Note: Finit .conf file(s) are *not* reloaded!\n" " restart [:ID] Restart (stop/start) service by name\n" " kill [:ID] Send signal S to service by name, with optional ID\n" +#ifdef HAVE_DBUS + " monitor Stream D-Bus signals (service state, conditions) until ^C\n" +#endif " ident [NAME] Show matching identities for NAME, or all\n" " status [:ID] Show service status, by name\n" " status Show status of services, default command\n"); @@ -1880,6 +2040,9 @@ int main(int argc, char *argv[]) { "start", NULL, do_start, NULL, NULL }, { "stop", NULL, do_stop, NULL, NULL }, { "restart", NULL, do_restart, NULL, NULL }, +#ifdef HAVE_DBUS + { "monitor", NULL, do_monitor, NULL, NULL }, +#endif { "signal", NULL, NULL, NULL, do_signal }, { "kill", NULL, NULL, NULL, do_signal }, /* alias */ diff --git a/test/Makefile.am b/test/Makefile.am index f4b04ee5..e5a3d5ac 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -72,6 +72,11 @@ EXTRA_DIST += signal-service.sh EXTRA_DIST += testserv.sh EXTRA_DIST += unexpected-restart.sh EXTRA_DIST += dbus-auth.sh +EXTRA_DIST += dbus-bus.sh +EXTRA_DIST += dbus-manager.sh +EXTRA_DIST += dbus-service.sh +EXTRA_DIST += dbus-cond.sh +EXTRA_DIST += dbus-initctl.sh AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -129,6 +134,11 @@ endif TESTS += unexpected-restart.sh if DBUS TESTS += dbus-auth.sh +TESTS += dbus-bus.sh +TESTS += dbus-manager.sh +TESTS += dbus-service.sh +TESTS += dbus-cond.sh +TESTS += dbus-initctl.sh endif check-recursive: setup-chroot diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh index 27bf6bea..1e9535d3 100755 --- a/test/dbus-auth.sh +++ b/test/dbus-auth.sh @@ -1,10 +1,9 @@ #!/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. +# libink: D-Bus AUTH EXTERNAL handshake. +# +# Verifies the SASL handshake itself in isolation -- everything else +# the bus does (built-in DBus interface, vtables, signals, initctl +# routing) lives in the other dbus-*.sh tests. set -eu @@ -12,23 +11,13 @@ 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" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" 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" @@ -52,264 +41,3 @@ 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 - *' root (good)" 0 -eq 0 ;; - *) fail "Root introspect missing : $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 diff --git a/test/dbus-bus.sh b/test/dbus-bus.sh new file mode 100755 index 00000000..7bd53704 --- /dev/null +++ b/test/dbus-bus.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# libink: org.freedesktop.DBus built-in interface. +# +# Covers the stock D-Bus interface every conforming bus implements: +# Hello (peer name allocation), Introspect (XML), and AddMatch's +# error-path rule parser. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +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" +xml=$(texec "$CLIENT" introspect "$BUS" /) +case "$xml" in + *' root (good)" 0 -eq 0 ;; + *) fail "Root introspect missing : $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 + +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 + +say "Unknown method on a Finit interface 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 diff --git a/test/dbus-cond.sh b/test/dbus-cond.sh new file mode 100755 index 00000000..f0e55da1 --- /dev/null +++ b/test/dbus-cond.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# libink: org.finit.Cond1 vtable + ConditionChanged signal. +# +# Covers user-condition manipulation: Get, Set (with signal fan-out), +# the usr/* policy guard (non-usr conditions are rejected), and the +# non-root authorization gate. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +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 diff --git a/test/dbus-initctl.sh b/test/dbus-initctl.sh new file mode 100755 index 00000000..c51c1a3e --- /dev/null +++ b/test/dbus-initctl.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# initctl: confirms the legacy CLI now routes through D-Bus. +# +# Subscribes to ServiceStateChanged on a background monitor and then +# runs initctl -- if D-Bus is in use, the signal fires. If the legacy +# socket were still in use, the dbus subscriber would see nothing. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +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 + +# A ConditionChanged signal can only originate from Cond1.Set going +# through finit (the legacy filesystem path doesn't emit signals). +# So if the monitor sees one, we know initctl cond set was routed +# via D-Bus. +say "initctl cond set drives Cond1.Set via D-Bus" +rm -f /tmp/dbus-initctl-cond.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-initctl-cond.out 2>&1 ) & +ic_cond_pid=$! +sleep 0.5 +texec initctl cond set "via-initctl" >/dev/null \ + || fail "initctl cond set returned non-zero" +set +e +wait "$ic_cond_pid" +ic_cond_rc=$? +set -e +assert "ConditionChanged fired from initctl cond set (rc=$ic_cond_rc)" \ + "$ic_cond_rc" -eq 0 +case "$(cat /tmp/dbus-initctl-cond.out)" in + *"SIGNAL org.finit.Cond1 ConditionChanged"*"usr/via-initctl"*on*) + assert "initctl cond set routed through D-Bus" 0 -eq 0 ;; + *) + fail "initctl cond set didn't produce expected signal: $(cat /tmp/dbus-initctl-cond.out)" ;; +esac diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh new file mode 100755 index 00000000..e26b4c11 --- /dev/null +++ b/test/dbus-manager.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# libink: org.finit.Manager1 vtable. +# +# Covers the Manager1 method surface: ListServices, Reload, Stop with +# bogus service, plus per-method authorization (Restart from non-root +# is rejected, ListServices remains reachable as non-root). + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +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" + +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 + +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 + +# Send call-s-as-uid an "s" body where the server expects "" -- 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 (a transport error would print neither +# AccessDenied nor InvalidArgs). +say "Manager1.ListServices is reachable as non-root (not blocked by authz)" +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 diff --git a/test/dbus-service.sh b/test/dbus-service.sh new file mode 100755 index 00000000..9099a637 --- /dev/null +++ b/test/dbus-service.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# libink: org.finit.Service1 vtable + ServiceStateChanged signal. +# +# Covers per-service objects exposed at /org/finit/service/: +# GetService lookup, Introspect on a service object, Service1.Restart, +# authorization (non-root rejected), and the Manager1.ServiceStateChanged +# signal that Service1.Restart triggers. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +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 + +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 diff --git a/test/lib/Makefile.am b/test/lib/Makefile.am index a281a920..ab85f6a4 100644 --- a/test/lib/Makefile.am +++ b/test/lib/Makefile.am @@ -1 +1 @@ -EXTRA_DIST = exec.sh setup.sh start.sh sysroot.mk +EXTRA_DIST = exec.sh setup.sh start.sh sysroot.mk dbus-setup.sh diff --git a/test/lib/dbus-setup.sh b/test/lib/dbus-setup.sh new file mode 100644 index 00000000..5b969da7 --- /dev/null +++ b/test/lib/dbus-setup.sh @@ -0,0 +1,20 @@ +# shellcheck shell=sh +# Shared preamble for the D-Bus smoke tests. Expects test/lib/setup.sh +# to have been sourced already (so texec, skip, retry, say, assert are +# available). Skips the test when the libink-driven client was not +# built, otherwise blocks until the bus socket appears. +# +# Exports: CLIENT, BUS. + +command -v texec >/dev/null \ + || { echo "dbus-setup.sh: source test/lib/setup.sh first" >&2; exit 99; } + +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" diff --git a/test/setup-sysroot.sh b/test/setup-sysroot.sh index 102cbad2..f986125e 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -15,7 +15,13 @@ make -C "$top_builddir" DESTDIR="$SYSROOT" install mkdir -p "$SYSROOT/sbin/" cp "$top_builddir/test/src/serv" "$SYSROOT/sbin/" -if [ -x "$top_builddir/test/src/dbus-auth-client" ]; then +# Prefer the real ELF in .libs/ over the libtool wrapper script -- +# since the test client links libink.la, libtool wraps the top-level +# dbus-auth-client as a shell script that re-execs the real binary +# via its own RPATH, which falls apart inside the test namespace. +if [ -x "$top_builddir/test/src/.libs/dbus-auth-client" ]; then + cp "$top_builddir/test/src/.libs/dbus-auth-client" "$SYSROOT/sbin/" +elif [ -x "$top_builddir/test/src/dbus-auth-client" ]; then cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" fi diff --git a/test/src/Makefile.am b/test/src/Makefile.am index b8d607d1..87d6e362 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -9,6 +9,8 @@ serv_LDADD = $(lite_LIBS) endif if DBUS -noinst_PROGRAMS += dbus-auth-client -dbus_auth_client_SOURCES = dbus-auth-client.c +noinst_PROGRAMS += dbus-auth-client +dbus_auth_client_SOURCES = dbus-auth-client.c +dbus_auth_client_CPPFLAGS = -D_GNU_SOURCE -I$(top_srcdir)/libink +dbus_auth_client_LDADD = $(top_builddir)/libink/libink.la endif diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c index 00e275a4..53f85223 100644 --- a/test/src/dbus-auth-client.c +++ b/test/src/dbus-auth-client.c @@ -4,32 +4,44 @@ * dbus-auth-client auth * Send the SASL handshake claiming ; print server reply line. * Exit 0 if reply begins "OK ", 1 if "REJECTED ", 2 otherwise. + * (Manual SASL: this mode exists *to* test AUTH itself.) * * dbus-auth-client hello - * Auth as own uid; call org.freedesktop.DBus.Hello on - * /org/freedesktop/DBus. Print the assigned unique name. + * Call org.freedesktop.DBus.Hello, print the assigned unique name. * * dbus-auth-client introspect - * Auth + org.freedesktop.DBus.Introspectable.Introspect. - * Print the XML reply. + * Call org.freedesktop.DBus.Introspectable.Introspect, print XML. * * dbus-auth-client liststrings - * Auth + method call expecting reply signature "as"; print one - * string per line. + * Call method expecting reply signature "as", print one per line. + * + * dbus-auth-client call-s + * dbus-auth-client call-void + * Issue method call with the given (or no) argument; "OK" or + * "ERROR: " on stderr. + * + * dbus-auth-client call-{s,void}-as-uid ... + * As above, but setuid() first so AUTH EXTERNAL claims . + * + * dbus-auth-client get-service + * Manager1.GetService(identity) -> print the encoded path. + * + * dbus-auth-client monitor-signal + * AddMatch + wait for one SIGNAL. Print "SIGNAL " + * then any string-typed body args. Exit 0 on signal, 1 on timeout. * * dbus-auth-client unknown - * Auth + call a bogus method; exits 0 only if the server replies - * with an "org.freedesktop.DBus.Error.*" error. + * Call a bogus method, exit 0 iff the server replies with an + * org.freedesktop.DBus.Error.* error. * - * In every non-auth mode the program exits 0 on a successful method - * reply, 1 on a server-side error reply, 2 on transport / parse error. + * Exit codes for the non-auth modes: 0 on success, 1 on server-side + * error reply, 2 on transport / parse / arg error. * * Copyright (c) 2026 Joachim Wiberg * SPDX-License-Identifier: MIT */ #include -#include #include #include #include @@ -37,76 +49,35 @@ #include #include #include -#include -#include + +#include "link.h" + +/* ---------- manual SASL: only mode_auth uses this ---------- */ static const char hex[] = "0123456789abcdef"; -#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) - -/* ---------- low level I/O ---------- */ - -static int write_all(int fd, const void *buf, size_t len) +static int write_all_fd(int fd, const void *buf, size_t len) { const char *p = buf; while (len > 0) { ssize_t n = write(fd, p, len); - if (n < 0) { if (errno == EINTR) continue; return -1; } - p += n; + p += n; len -= (size_t)n; } return 0; } -static int read_full(int fd, void *buf, size_t len) -{ - char *p = buf; - - while (len > 0) { - ssize_t n = read(fd, p, len); - - if (n == 0) return -1; - if (n < 0) { - if (errno == EINTR) continue; - return -1; - } - p += n; - len -= (size_t)n; - } - return 0; -} - -static int read_with_timeout(int fd, void *buf, size_t len, int timeout_ms) -{ - struct pollfd pfd = { .fd = fd, .events = POLLIN }; - int rc; - - for (;;) { - rc = poll(&pfd, 1, timeout_ms); - if (rc < 0) { - if (errno == EINTR) - continue; - return -1; - } - if (rc == 0) - return 0; /* timed out */ - break; - } - return (int)read(fd, buf, len); -} - -static ssize_t read_line(int fd, char *buf, size_t bufsz) +static ssize_t read_line_fd(int fd, char *buf, size_t bufsz) { size_t off = 0; while (off + 1 < bufsz) { ssize_t n = read(fd, buf + off, 1); - if (n == 0) return -1; if (n < 0) { if (errno == EINTR) continue; @@ -123,410 +94,6 @@ static ssize_t read_line(int fd, char *buf, size_t bufsz) return -1; } -/* ---------- connect + AUTH ---------- */ - -static int connect_and_auth(const char *path, uid_t claimed_uid) -{ - struct sockaddr_un sun = { .sun_family = AF_UNIX }; - char uidstr[16]; - char hexuid[32]; - char line[64]; - char reply[256]; - size_t i, n; - int fd, rc; - - if (strlen(path) >= sizeof(sun.sun_path)) - return -1; - memcpy(sun.sun_path, path, strlen(path) + 1); - - fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) return -1; - if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { - close(fd); - return -1; - } - - n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)claimed_uid); - for (i = 0; i < n; i++) { - unsigned c = (unsigned char)uidstr[i]; - - hexuid[i * 2] = hex[c >> 4]; - hexuid[i * 2 + 1] = hex[c & 0xf]; - } - hexuid[n * 2] = '\0'; - - if (write_all(fd, "\0", 1) < 0) goto io; - - rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); - if (rc < 0 || (size_t)rc >= sizeof(line)) goto io; - if (write_all(fd, line, (size_t)rc) < 0) goto io; - - if (read_line(fd, reply, sizeof(reply)) < 0) goto io; - if (strncmp(reply, "OK ", 3) != 0) { - fprintf(stderr, "auth failed: %s\n", reply); - close(fd); - return -1; - } - - if (write_all(fd, "BEGIN\r\n", 7) < 0) goto io; - return fd; - -io: - perror("auth handshake"); - close(fd); - return -1; -} - -/* ---------- D-Bus message build / parse ---------- */ - -struct buf { - uint8_t *p; - size_t cap; - size_t off; - int err; -}; - -static int b_reserve(struct buf *b, size_t align, size_t bytes) -{ - size_t pad = ALIGN_UP(b->off, align) - b->off; - - if (b->err || b->off + pad + bytes > b->cap) { - b->err = 1; - return -1; - } - while (pad--) b->p[b->off++] = 0; - return 0; -} - -static void b_put_u32(struct buf *b, uint32_t v) -{ - if (b_reserve(b, 4, 4) < 0) return; - b->p[b->off++] = (uint8_t)(v & 0xff); - b->p[b->off++] = (uint8_t)((v >> 8) & 0xff); - b->p[b->off++] = (uint8_t)((v >> 16) & 0xff); - b->p[b->off++] = (uint8_t)((v >> 24) & 0xff); -} - -static void b_put_byte(struct buf *b, uint8_t v) -{ - if (b_reserve(b, 1, 1) < 0) return; - b->p[b->off++] = v; -} - -static void b_put_string(struct buf *b, const char *s) -{ - size_t len = strlen(s); - - if (b_reserve(b, 4, 4 + len + 1) < 0) return; - b_put_u32(b, (uint32_t)len); - memcpy(b->p + b->off, s, len); - b->off += len; - b->p[b->off++] = 0; -} - -static void b_put_signature(struct buf *b, const char *s) -{ - size_t len = strlen(s); - - if (b_reserve(b, 1, 1 + len + 1) < 0) return; - b->p[b->off++] = (uint8_t)len; - memcpy(b->p + b->off, s, len); - b->off += len; - b->p[b->off++] = 0; -} - -/* Send a method call with an optional argument. - * arg_sig == NULL or "" -> no body - * arg_sig == "s" -> arg_string used - * arg_sig == "u" -> arg_u32 used - */ -static int send_method_call_with_arg(int fd, - const char *path, - const char *interface, - const char *member, - const char *arg_sig, - const char *arg_string, - uint32_t arg_u32); - -static int send_method_call(int fd, - const char *path, - const char *interface, - const char *member) -{ - return send_method_call_with_arg(fd, path, interface, member, - NULL, NULL, 0); -} - -static int send_method_call_with_arg(int fd, - const char *path, - const char *interface, - const char *member, - const char *arg_sig, - const char *arg_string, - uint32_t arg_u32) -{ - uint8_t hdr[2048]; - uint8_t body[1024]; - struct buf b = { .p = hdr, .cap = sizeof(hdr) }; - struct buf bb = { .p = body, .cap = sizeof(body) }; - size_t fields_start, fields_end, padded_end; - uint32_t body_len = 0; - - /* Build body first so its length and the signature are known - * before we write the header. */ - if (arg_sig && *arg_sig) { - if (strcmp(arg_sig, "s") == 0) { - b_put_string(&bb, arg_string ? arg_string : ""); - } else if (strcmp(arg_sig, "u") == 0) { - b_put_u32(&bb, arg_u32); - } else { - return -1; - } - if (bb.err) return -1; - body_len = (uint32_t)bb.off; - } - - /* Fixed header */ - memset(hdr, 0, 16); - hdr[0] = 'l'; - hdr[1] = 1; /* METHOD_CALL */ - hdr[2] = 0; /* flags */ - hdr[3] = 1; /* protocol */ - hdr[4] = (uint8_t)( body_len & 0xff); - hdr[5] = (uint8_t)((body_len >> 8) & 0xff); - hdr[6] = (uint8_t)((body_len >> 16) & 0xff); - hdr[7] = (uint8_t)((body_len >> 24) & 0xff); - hdr[8] = 1; /* serial */ - b.off = 16; - fields_start = b.off; - - /* PATH */ - b_reserve(&b, 8, 0); - b_put_byte(&b, 1); - b_put_signature(&b, "o"); - b_put_string(&b, path); - - if (interface) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 2); - b_put_signature(&b, "s"); - b_put_string(&b, interface); - } - - b_reserve(&b, 8, 0); - b_put_byte(&b, 3); - b_put_signature(&b, "s"); - b_put_string(&b, member); - - if (arg_sig && *arg_sig) { - b_reserve(&b, 8, 0); - b_put_byte(&b, 8); - b_put_signature(&b, "g"); - /* SIGNATURE wire form: 1-byte len, bytes, nul */ - b_put_byte(&b, (uint8_t)strlen(arg_sig)); - if (b.off + strlen(arg_sig) + 1 > b.cap) return -1; - memcpy(b.p + b.off, arg_sig, strlen(arg_sig)); - b.off += strlen(arg_sig); - b.p[b.off++] = 0; - } - - fields_end = b.off; - { - uint32_t flen = (uint32_t)(fields_end - fields_start); - hdr[12] = (uint8_t)( flen & 0xff); - hdr[13] = (uint8_t)((flen >> 8) & 0xff); - hdr[14] = (uint8_t)((flen >> 16) & 0xff); - hdr[15] = (uint8_t)((flen >> 24) & 0xff); - } - - padded_end = ALIGN_UP(fields_end, 8); - while (b.off < padded_end) hdr[b.off++] = 0; - - if (b.err) return -1; - - if (write_all(fd, hdr, b.off) < 0) return -1; - if (body_len > 0 && write_all(fd, body, body_len) < 0) return -1; - return 0; -} - -/* Read one D-Bus message header + body into msg/body buffers. - * Returns 0 on success. Caller-supplied buffers must be large - * enough; we set them generously. */ -struct reply { - uint8_t type; - uint32_t serial; - uint32_t body_len; - char signature[64]; - char error_name[128]; - char interface[128]; - char member[128]; - uint8_t body[8192]; -}; - -/* Read one D-Bus message into *r. - * timeout_ms == 0 -> block forever waiting for the header byte - * timeout_ms > 0 -> wait that long for the header to start; once - * bytes arrive, the remainder of the frame is - * read without a timeout (it's "in flight"). - * Returns 0 on success, -1 on EOF / parse error / timeout. */ -static int read_reply(int fd, struct reply *r, int timeout_ms) -{ - uint8_t hdr_fixed[16]; - uint8_t hdr_fields[2048]; - uint32_t fields_len; - size_t body_off; - size_t pos; - size_t off = 0; - - memset(r, 0, sizeof(*r)); - - if (timeout_ms > 0) { - int n = read_with_timeout(fd, hdr_fixed, 1, timeout_ms); - if (n <= 0) return -1; - off = 1; - } - if (off < 16 && read_full(fd, hdr_fixed + off, 16 - off) < 0) - return -1; - - if (hdr_fixed[0] != 'l') return -1; - r->type = hdr_fixed[1]; - r->body_len = (uint32_t)hdr_fixed[4] - | ((uint32_t)hdr_fixed[5] << 8) - | ((uint32_t)hdr_fixed[6] << 16) - | ((uint32_t)hdr_fixed[7] << 24); - r->serial = (uint32_t)hdr_fixed[8] - | ((uint32_t)hdr_fixed[9] << 8) - | ((uint32_t)hdr_fixed[10] << 16) - | ((uint32_t)hdr_fixed[11] << 24); - fields_len = (uint32_t)hdr_fixed[12] - | ((uint32_t)hdr_fixed[13] << 8) - | ((uint32_t)hdr_fixed[14] << 16) - | ((uint32_t)hdr_fixed[15] << 24); - if (fields_len > sizeof(hdr_fields)) return -1; - if (read_full(fd, hdr_fields, fields_len) < 0) return -1; - - body_off = (size_t)ALIGN_UP(16 + fields_len, 8); - if (body_off > 16 + fields_len) { - uint8_t pad[8]; - if (read_full(fd, pad, body_off - 16 - fields_len) < 0) - return -1; - } - - pos = 0; - while (pos < fields_len) { - uint8_t code; - size_t vsig_len; - const char *vsig; - - pos = ALIGN_UP(pos, 8); - if (pos >= fields_len) break; - code = hdr_fields[pos++]; - vsig_len = hdr_fields[pos++]; - if (pos + vsig_len + 1 > fields_len) return -1; - vsig = (const char *)(hdr_fields + pos); - pos += vsig_len + 1; - - if (vsig[0] == 's' || vsig[0] == 'o') { - uint32_t slen; - char *dst = NULL; - size_t dst_sz = 0; - - pos = ALIGN_UP(pos, 4); - if (pos + 4 > fields_len) return -1; - slen = (uint32_t)hdr_fields[pos] - | ((uint32_t)hdr_fields[pos + 1] << 8) - | ((uint32_t)hdr_fields[pos + 2] << 16) - | ((uint32_t)hdr_fields[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > fields_len) return -1; - - switch (code) { - case 2: dst = r->interface; dst_sz = sizeof(r->interface); break; - case 3: dst = r->member; dst_sz = sizeof(r->member); break; - case 4: dst = r->error_name; dst_sz = sizeof(r->error_name); break; - default: break; - } - if (dst && slen < dst_sz) { - memcpy(dst, hdr_fields + pos, slen); - dst[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'g') { - uint32_t slen = hdr_fields[pos++]; - if (pos + slen + 1 > fields_len) return -1; - if (code == 8 && slen < sizeof(r->signature)) { - memcpy(r->signature, hdr_fields + pos, slen); - r->signature[slen] = '\0'; - } - pos += slen + 1; - } else if (vsig[0] == 'u') { - pos = ALIGN_UP(pos, 4); - pos += 4; - } else { - return -1; - } - } - - if (r->body_len > sizeof(r->body)) return -1; - if (r->body_len > 0 && read_full(fd, r->body, r->body_len) < 0) - return -1; - return 0; -} - -/* Decode a body containing exactly one "s" or "o" -- the wire form - * is identical for both (u32 length + bytes + nul). */ -static int decode_string(struct reply *r, char *out, size_t outsz) -{ - uint32_t len; - - if (r->body_len < 5) - return -1; - if (strcmp(r->signature, "s") != 0 && strcmp(r->signature, "o") != 0) - return -1; - len = (uint32_t)r->body[0] - | ((uint32_t)r->body[1] << 8) - | ((uint32_t)r->body[2] << 16) - | ((uint32_t)r->body[3] << 24); - if (4 + len + 1 > r->body_len) return -1; - if (len + 1 > outsz) return -1; - memcpy(out, r->body + 4, len); - out[len] = '\0'; - return 0; -} - -/* Decode a body with signature "as", print one string per line. */ -static int decode_array_of_strings(struct reply *r) -{ - uint32_t array_len; - size_t pos; - - if (strcmp(r->signature, "as") != 0 || r->body_len < 4) - return -1; - array_len = (uint32_t)r->body[0] - | ((uint32_t)r->body[1] << 8) - | ((uint32_t)r->body[2] << 16) - | ((uint32_t)r->body[3] << 24); - pos = ALIGN_UP(4, 4); - if (pos + array_len > r->body_len) return -1; - - while (pos < 4 + array_len) { - uint32_t slen; - pos = ALIGN_UP(pos, 4); - if (pos + 4 > r->body_len) return -1; - slen = (uint32_t)r->body[pos] - | ((uint32_t)r->body[pos + 1] << 8) - | ((uint32_t)r->body[pos + 2] << 16) - | ((uint32_t)r->body[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > r->body_len) return -1; - printf("%.*s\n", (int)slen, r->body + pos); - pos += slen + 1; - } - return 0; -} - -/* ---------- modes ---------- */ - static int mode_auth(int argc, char *argv[]) { struct sockaddr_un sun = { .sun_family = AF_UNIX }; @@ -557,11 +124,11 @@ static int mode_auth(int argc, char *argv[]) if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { perror("connect"); close(fd); return 2; } - if (write_all(fd, "\0", 1) < 0) { close(fd); return 2; } + if (write_all_fd(fd, "\0", 1) < 0) { close(fd); return 2; } rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); if (rc < 0 || (size_t)rc >= sizeof(line)) { close(fd); return 2; } - if (write_all(fd, line, (size_t)rc) < 0) { close(fd); return 2; } - if (read_line(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; } + if (write_all_fd(fd, line, (size_t)rc) < 0) { close(fd); return 2; } + if (read_line_fd(fd, reply, sizeof(reply)) < 0) { close(fd); return 2; } printf("%s\n", reply); close(fd); if (strncmp(reply, "OK ", 3) == 0) return 0; @@ -569,138 +136,26 @@ static int mode_auth(int argc, char *argv[]) return 2; } -static int do_call_arg(const char *path, const char *obj_path, - const char *iface, const char *method, - const char *arg_sig, const char *arg_string, - uint32_t arg_u32, struct reply *r) -{ - int fd = connect_and_auth(path, getuid()); +/* ---------- libink-driven modes ---------- */ - if (fd < 0) return 2; - if (send_method_call_with_arg(fd, obj_path, iface, method, - arg_sig, arg_string, arg_u32) < 0) { - fprintf(stderr, "send: %s\n", strerror(errno)); - close(fd); - return 2; - } - if (read_reply(fd, r, 0) < 0) { - fprintf(stderr, "read_reply\n"); - close(fd); - return 2; - } - close(fd); - if (r->type == 3) { - fprintf(stderr, "ERROR: %s\n", r->error_name); +/* Convert link_client_call rc to the test client's 0/1/2 convention, + * printing the error name on stderr for ERROR replies. */ +static int report_rc(link_client_t *c, int rc) +{ + if (rc == LINK_CALL_OK) + return 0; + if (rc == LINK_CALL_ERROR) { + const link_reply_t *r = link_client_reply(c); + fprintf(stderr, "ERROR: %s\n", + (r && r->error_name) ? r->error_name : ""); return 1; } - return 0; + return 2; } -static int do_call(const char *path, const char *obj_path, - const char *iface, const char *method, - struct reply *r) +/* Drop effective uid to argv-supplied value (decimal). */ +static int drop_uid(const char *uid_arg, const char *progname) { - return do_call_arg(path, obj_path, iface, method, NULL, NULL, 0, r); -} - -static int mode_hello(int argc, char *argv[]) -{ - struct reply r; - char name[256]; - int rc; - - if (argc != 3) return 2; - rc = do_call(argv[2], "/org/freedesktop/DBus", - "org.freedesktop.DBus", "Hello", &r); - if (rc != 0) return rc; - if (decode_string(&r, name, sizeof(name)) < 0) return 2; - printf("%s\n", name); - return 0; -} - -static int mode_introspect(int argc, char *argv[]) -{ - struct reply r; - char xml[8192]; - int rc; - - if (argc != 4) return 2; - rc = do_call(argv[2], argv[3], - "org.freedesktop.DBus.Introspectable", "Introspect", &r); - if (rc != 0) return rc; - if (decode_string(&r, xml, sizeof(xml)) < 0) return 2; - printf("%s\n", xml); - return 0; -} - -static int mode_liststrings(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 6) return 2; - rc = do_call(argv[2], argv[3], argv[4], argv[5], &r); - if (rc != 0) return rc; - if (decode_array_of_strings(&r) < 0) return 2; - return 0; -} - -/* call-s: method taking one string arg, void/error reply. - * call-void: method taking no args, void/error reply. */ -static int mode_call_s(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 7) return 2; - rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], - "s", argv[6], 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - -static int mode_call_void(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 6) return 2; - rc = do_call_arg(argv[2], argv[3], argv[4], argv[5], - NULL, NULL, 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - -/* get-service - * - * Calls Manager1.GetService(identity) and prints the returned - * object path. Exit 0 on success, 1 on server error, 2 transport. */ -static int mode_get_service(int argc, char *argv[]) -{ - struct reply r; - char path[256]; - int rc; - - if (argc != 4) return 2; - rc = do_call_arg(argv[2], "/org/finit/manager", - "org.finit.Manager1", "GetService", - "s", argv[3], 0, &r); - if (rc != 0) return rc; - /* decode_string accepts both "s" and "o" — wire form is - * identical; no need to pre-check the signature here. */ - if (decode_string(&r, path, sizeof(path)) < 0) - return 2; - printf("%s\n", path); - return 0; -} - -/* Drop effective uid to argv[2], parsed as decimal. Returns 0 on - * success, 2 (the program's "transport error" code) on failure. */ -static int drop_uid_from_arg(const char *uid_arg, const char *progname) -{ - uid_t drop_to; char *ep = NULL; long v; @@ -710,29 +165,207 @@ static int drop_uid_from_arg(const char *uid_arg, const char *progname) fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg); return 2; } - drop_to = (uid_t)v; - - if (setuid(drop_to) < 0) { + if (setuid((uid_t)v) < 0) { perror("setuid"); return 2; } return 0; } -/* monitor-signal - * - * Subscribes via org.freedesktop.DBus.AddMatch, then reads - * incoming messages until either a SIGNAL is received or the - * timeout elapses. On a signal: prints "SIGNAL " - * followed by any "s" args, one per line. Exit 0 on signal, 1 on - * timeout, 2 on transport error. */ +static int mode_hello(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *name; + int rc; + + if (argc != 3) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + rc = report_rc(c, rc); + if (rc == 0) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_string(&reader, &name) == 0) + printf("%s\n", name); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +static int mode_introspect(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *xml; + int rc; + + if (argc != 4) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], + "org.freedesktop.DBus.Introspectable", + "Introspect", NULL); + rc = report_rc(c, rc); + if (rc == 0) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_string(&reader, &xml) == 0) + printf("%s\n", xml); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +/* Decode body with signature "as" -- u32 array byte-len, then "s" strings. + * libink's public reader doesn't yet have an array helper, so we walk + * the wire form with link_r_u32 + link_r_string + link_r_pos. */ +static int print_string_array(const link_reply_t *r) +{ + link_reader_t reader; + uint32_t array_len; + size_t end; + + if (!r || !r->signature || strcmp(r->signature, "as") != 0) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_u32(&reader, &array_len) < 0) + return -1; + end = link_r_pos(&reader) + array_len; + if (end > r->body_len) + return -1; + + while (link_r_pos(&reader) < end) { + const char *s; + + if (link_r_string(&reader, &s) < 0) + return -1; + printf("%s\n", s); + } + return 0; +} + +static int mode_liststrings(int argc, char *argv[]) +{ + link_client_t *c; + int rc; + + if (argc != 6) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, argv[3], argv[4], argv[5], NULL); + rc = report_rc(c, rc); + if (rc == 0 && print_string_array(link_client_reply(c)) < 0) + rc = 2; + link_client_close(c); + return rc; +} + +/* Shared call helpers used by both the plain and the -as-uid modes. + * `arg` may be NULL (void method); when non-NULL the call signature + * is "s" with `arg` as the single string argument. */ +static int do_call(const char *sock, const char *obj, const char *iface, + const char *method, const char *arg) +{ + link_client_t *c; + int rc; + + c = link_client_open(sock); + if (!c) return 2; + + rc = arg + ? link_client_call_v(c, obj, iface, method, "s", arg) + : link_client_call_v(c, obj, iface, method, NULL); + rc = report_rc(c, rc); + if (rc == 0) + printf("OK\n"); + link_client_close(c); + return rc; +} + +static int mode_call_s(int argc, char *argv[]) +{ + if (argc != 7) return 2; + return do_call(argv[2], argv[3], argv[4], argv[5], argv[6]); +} + +static int mode_call_void(int argc, char *argv[]) +{ + if (argc != 6) return 2; + return do_call(argv[2], argv[3], argv[4], argv[5], NULL); +} + +static int mode_call_s_as_uid(int argc, char *argv[]) +{ + int rc; + + if (argc != 8) return 2; + if ((rc = drop_uid(argv[2], argv[0])) != 0) + return rc; + return do_call(argv[3], argv[4], argv[5], argv[6], argv[7]); +} + +static int mode_call_void_as_uid(int argc, char *argv[]) +{ + int rc; + + if (argc != 7) return 2; + if ((rc = drop_uid(argv[2], argv[0])) != 0) + return rc; + return do_call(argv[3], argv[4], argv[5], argv[6], NULL); +} + +static int mode_get_service(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + const char *path; + int rc; + + if (argc != 4) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "GetService", + "s", argv[3]); + rc = report_rc(c, rc); + if (rc == 0) { + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + /* Reply signature is "o" but link_r_path / link_r_string + * have the same wire form. */ + if (link_r_path(&reader, &path) == 0) + printf("%s\n", path); + else + rc = 2; + } + link_client_close(c); + return rc; +} + static int mode_monitor_signal(int argc, char *argv[]) { - int fd; - int timeout_ms; - struct reply r; + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; char *ep = NULL; long v; + int timeout_ms; + int rc; if (argc != 5) return 2; @@ -744,118 +377,91 @@ static int mode_monitor_signal(int argc, char *argv[]) } timeout_ms = (int)v; - fd = connect_and_auth(argv[2], getuid()); - if (fd < 0) return 2; + c = link_client_open(argv[2]); + if (!c) return 2; - /* AddMatch on org.freedesktop.DBus */ - if (send_method_call_with_arg(fd, "/org/freedesktop/DBus", - "org.freedesktop.DBus", "AddMatch", - "s", argv[3], 0) < 0) { - close(fd); return 2; - } - if (read_reply(fd, &r, 0) < 0) { close(fd); return 2; } - if (r.type == 3) { - fprintf(stderr, "AddMatch ERROR: %s\n", r.error_name); - close(fd); return 2; + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "AddMatch", + "s", argv[3]); + if (rc != LINK_CALL_OK) { + rc = report_rc(c, rc); + link_client_close(c); + return rc; } - /* Now read messages until a signal or timeout. */ for (;;) { - if (read_reply(fd, &r, timeout_ms) < 0) { - close(fd); - return 1; /* timeout / transport */ + rc = link_client_wait(c, timeout_ms); + if (rc != 0) { + link_client_close(c); + return 1; /* timeout or transport */ } - if (r.type != 4) /* not a SIGNAL */ + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) continue; - printf("SIGNAL %s %s\n", r.interface, r.member); - /* Decode body as a sequence of strings; print one per line. */ - { - size_t pos = 0; - while (pos + 4 <= r.body_len) { - uint32_t slen; - pos = ALIGN_UP(pos, 4); - if (pos + 4 > r.body_len) break; - slen = (uint32_t)r.body[pos] - | ((uint32_t)r.body[pos + 1] << 8) - | ((uint32_t)r.body[pos + 2] << 16) - | ((uint32_t)r.body[pos + 3] << 24); - pos += 4; - if (pos + slen + 1 > r.body_len) break; - printf("%.*s\n", (int)slen, r.body + pos); - pos += slen + 1; + + printf("SIGNAL %s %s\n", + r->interface ? r->interface : "", + r->member ? r->member : ""); + /* Print any leading "s" args (other types are silently + * skipped -- callers test for the strings only). */ + link_reader_init(&reader, r->body, r->body_len); + if (r->signature) { + const char *s; + const char *p; + + for (p = r->signature; *p == 's'; p++) { + if (link_r_string(&reader, &s) < 0) + break; + printf("%s\n", s); } } - close(fd); + link_client_close(c); return 0; } } -/* call-s-as-uid - * - * Drops effective uid to (must work inside the test - * namespace where additional uids are mapped) before connecting, - * so AUTH EXTERNAL captures as the peer's real identity. - * Used to verify per-method authorization gating. */ -static int mode_call_s_as_uid(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 8) return 2; - if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) - return rc; - rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], - "s", argv[7], 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - -/* call-void-as-uid */ -static int mode_call_void_as_uid(int argc, char *argv[]) -{ - struct reply r; - int rc; - - if (argc != 7) return 2; - if ((rc = drop_uid_from_arg(argv[2], argv[0])) != 0) - return rc; - rc = do_call_arg(argv[3], argv[4], argv[5], argv[6], - NULL, NULL, 0, &r); - if (rc == 0) - printf("OK\n"); - return rc; -} - static int mode_unknown(int argc, char *argv[]) { - struct reply r; + link_client_t *c; + const link_reply_t *r; int rc; if (argc != 3) return 2; - rc = do_call(argv[2], "/org/finit/manager", - "org.finit.Manager1", "NotARealMethod", &r); - if (rc == 1 && strstr(r.error_name, "org.freedesktop.DBus.Error.") == r.error_name) - return 0; - if (rc == 1) - return 1; - return 2; + c = link_client_open(argv[2]); + if (!c) return 2; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "NotARealMethod", NULL); + if (rc != LINK_CALL_ERROR) { + link_client_close(c); + return rc == LINK_CALL_OK ? 1 : 2; + } + + r = link_client_reply(c); + { + static const char prefix[] = "org.freedesktop.DBus.Error."; + rc = (r && r->error_name && + strncmp(r->error_name, prefix, sizeof(prefix) - 1) == 0) + ? 0 : 1; + } + link_client_close(c); + return rc; } int main(int argc, char *argv[]) { if (argc < 2) return 2; - if (strcmp(argv[1], "auth") == 0) return mode_auth(argc, argv); - if (strcmp(argv[1], "hello") == 0) return mode_hello(argc, argv); - if (strcmp(argv[1], "introspect") == 0) return mode_introspect(argc, argv); - if (strcmp(argv[1], "liststrings") == 0) return mode_liststrings(argc, argv); - if (strcmp(argv[1], "call-s") == 0) return mode_call_s(argc, argv); - if (strcmp(argv[1], "call-void") == 0) return mode_call_void(argc, argv); - if (strcmp(argv[1], "monitor-signal") == 0) return mode_monitor_signal(argc, argv); - if (strcmp(argv[1], "call-s-as-uid") == 0) return mode_call_s_as_uid(argc, argv); - if (strcmp(argv[1], "call-void-as-uid") == 0) return mode_call_void_as_uid(argc, argv); - if (strcmp(argv[1], "get-service") == 0) return mode_get_service(argc, argv); - if (strcmp(argv[1], "unknown") == 0) return mode_unknown(argc, argv); + if (!strcmp(argv[1], "auth")) return mode_auth (argc, argv); + if (!strcmp(argv[1], "hello")) return mode_hello (argc, argv); + if (!strcmp(argv[1], "introspect")) return mode_introspect (argc, argv); + if (!strcmp(argv[1], "liststrings")) return mode_liststrings (argc, argv); + if (!strcmp(argv[1], "call-s")) return mode_call_s (argc, argv); + if (!strcmp(argv[1], "call-void")) return mode_call_void (argc, argv); + if (!strcmp(argv[1], "call-s-as-uid")) return mode_call_s_as_uid (argc, argv); + if (!strcmp(argv[1], "call-void-as-uid")) return mode_call_void_as_uid(argc, argv); + if (!strcmp(argv[1], "get-service")) return mode_get_service (argc, argv); + if (!strcmp(argv[1], "monitor-signal")) return mode_monitor_signal (argc, argv); + if (!strcmp(argv[1], "unknown")) return mode_unknown (argc, argv); fprintf(stderr, "%s: unknown mode '%s'\n", argv[0], argv[1]); return 2; }