diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec7aeab..e3a148d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,71 @@ concurrency: cancel-in-progress: true jobs: + fuzz: + name: fuzz + runs-on: ubuntu-latest + if: github.event_name != 'push' || github.ref == 'refs/heads/master' + steps: + - name: Install dependencies + run: | + sudo apt-get -y update + sudo apt-get -y install pkg-config libconfuse-dev clang + # clang picks the newest gcc tree it finds and needs the + # matching libstdc++ headers to link the fuzzer runtime + sudo apt-get -y install libstdc++-14-dev || true + wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz + wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz + tar xf libuev-2.4.1.tar.xz + tar xf libite-2.6.2.tar.gz + (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) + (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) + sudo ldconfig + - uses: actions/checkout@v7 + - name: Configure + run: | + ./autogen.sh + ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var + - name: Build fuzz target + run: | + clang -fsanitize=fuzzer,address -DLINK_FUZZ_LIBFUZZER -D_GNU_SOURCE \ + -I libink -I . -o fuzz-msg-parse \ + test/src/fuzz-msg-parse.c libink/*.c + # Restores the newest corpus and saves a fresh one, since a cache + # entry is immutable once written. Caches made on a branch are + # private to it, so the corpus that accumulates on master is what + # pull requests start from, rather than nothing. + - name: Restore corpus + uses: actions/cache@v6 + with: + path: .fuzz-corpus + key: fuzz-corpus-${{ github.run_id }} + restore-keys: fuzz-corpus- + - name: Fuzz + run: | + mkdir -p .fuzz-corpus + ./fuzz-msg-parse .fuzz-corpus -max_total_time=120 -max_len=4096 \ + -print_final_stats=1 + # Without this the corpus only ever grows, and most of what it + # accumulates reaches code some earlier input already reached. + - name: Minimise corpus + if: always() + run: | + mkdir -p .fuzz-corpus-min + ./fuzz-msg-parse -merge=1 .fuzz-corpus-min .fuzz-corpus + rm -rf .fuzz-corpus + mv .fuzz-corpus-min .fuzz-corpus + echo "corpus: $(ls .fuzz-corpus | wc -l) inputs" + - name: Upload crashers + if: failure() + uses: actions/upload-artifact@v7 + with: + name: fuzz-crashers + path: | + crash-* + leak-* + timeout-* + if-no-files-found: ignore + build: # Verify we can build on latest Ubuntu with both gcc and clang name: ${{ matrix.compiler }} @@ -42,7 +107,7 @@ jobs: (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) sudo ldconfig - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Static Finit run: | ./autogen.sh @@ -52,7 +117,7 @@ jobs: run: | ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \ --enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \ - --with-keventd --with-libsystemd \ + --with-keventd \ CFLAGS="-fsanitize=address -ggdb" make -j9 clean make -j9 V=1 @@ -80,7 +145,7 @@ jobs: make -j1 check || (cat test/test-suite.log; false) - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: finit-test-${{ matrix.compiler }} path: test/*.log diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index f9b51781..d3090c99 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -19,7 +19,7 @@ jobs: coverity: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Fetch latest Coverity Scan MD5 id: var env: @@ -29,7 +29,7 @@ jobs: --post-data "token=$TOKEN&project=${COVERITY_PROJ}&md5=1" \ -O coverity-latest.tar.gz.md5 echo "md5=$(cat coverity-latest.tar.gz.md5)" | tee -a $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v6 id: cache with: path: coverity-latest.tar.gz @@ -86,7 +86,7 @@ jobs: --form description="${PROJECT_NAME} $(git rev-parse HEAD)" \ https://scan.coverity.com/builds?project=${COVERITY_PROJ} - name: Upload build.log - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverity-build.log path: cov-int/build-log.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 28e98055..ee3cb619 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 # Needed for git-revision-date-localized plugin @@ -42,7 +42,7 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -58,7 +58,7 @@ jobs: run: mkdocs build --clean - name: Upload site artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: site path: site/ @@ -72,13 +72,13 @@ jobs: steps: - name: Download site artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: site path: site/ - name: Checkout pages repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: finit-project/finit-project.github.io path: pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0bfcbd4..eb22a67e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Installing dependencies ... run: | sudo apt-get -y update @@ -64,7 +64,7 @@ jobs: mv ../*.tar.* artifacts/ - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: finit-release-test-logs path: finit-*/_build/sub/test/*.log diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index d2d8cbe4..b8b31da6 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -16,7 +16,7 @@ jobs: outputs: run: ${{ steps.schedule.outputs.run }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Check for any commits since last week id: schedule run: | @@ -42,7 +42,7 @@ jobs: (cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip) (cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip) sudo ldconfig - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Base run run: | ./autogen.sh @@ -61,7 +61,7 @@ jobs: make -j1 distcheck || (cat $dir/test/test-suite.log; false) - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: finit-weekly-test-logs path: finit-*/_build/sub/test/*.log diff --git a/.gitignore b/.gitignore index d30aefef..5b943e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,8 @@ GTAGS # VS Code user settings .vscode + +# Fuzzing: the target is built by hand with clang, see doc/build.md, +# and the corpus it grows is a local artefact, not something to ship +/fuzz-msg-parse +/.fuzz-corpus/ diff --git a/Makefile.am b/Makefile.am index a5333be6..f5e82762 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,16 @@ ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = man plugins src system tmpfiles.d + +# libink must precede src in SUBDIRS because finit links against +# libink at build time. Automake recurses subdirs strictly in +# declaration order, so the typical "explicit dependency" pattern +# (foo: bar) doesn't help here — ordering is the only thing that +# does. libsystemd is consumed by test/serv only, so its position +# after src is fine. +SUBDIRS = man plugins +if DBUS +SUBDIRS += libink dbus-1 +endif +SUBDIRS += src system tmpfiles.d dist_doc_DATA = README.md LICENSE contrib/finit.conf if CONTRIB @@ -24,6 +35,13 @@ endif install-dev: @make -C src install-pkgincludeHEADERS +# The fuzz target is built by hand with clang (doc/build.md), so it is +# not in any _PROGRAMS and nothing else would clear it or the corpus +# it grows. +distclean-local: + -rm -f $(top_builddir)/fuzz-msg-parse + -rm -rf $(top_builddir)/.fuzz-corpus + # Target to run when building a release release: distcheck @for file in $(DIST_ARCHIVES); do \ diff --git a/configure.ac b/configure.ac index 0b213b53..3a0a971d 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,8 @@ AC_CONFIG_FILES([Makefile contrib/debian/Makefile contrib/debian/finit.d/Makefile contrib/debian/finit.d/available/Makefile contrib/void/Makefile contrib/void/finit.d/Makefile contrib/void/finit.d/available/Makefile doc/Makefile doc/config/Makefile + dbus-1/Makefile + libink/Makefile libsystemd/Makefile libsystemd/libsystemd.pc man/Makefile plugins/Makefile @@ -94,6 +96,10 @@ AC_ARG_ENABLE(logrotate, AS_HELP_STRING([--disable-logrotate], [Disable built-in rotation of /var/log/wtmp]),,[ enable_logrotate=yes]) +AC_ARG_ENABLE(dbus, + AS_HELP_STRING([--disable-dbus], [Disable D-Bus support (libink + Finit object tree)]),,[ + enable_dbus=yes]) + AC_ARG_ENABLE(doc, AS_HELP_STRING([--disable-doc], [Disable build and install of doc/ section]),,[ enable_doc=yes]) @@ -111,7 +117,7 @@ AC_ARG_ENABLE(all_plugins, AS_HELP_STRING([--enable-all-plugins], [Enable all plugins, default: auto]), enable_all_plugins=$enableval, enable_all_plugins=auto) AC_PLUGIN([alsa-utils], [no], [Save and restore ALSA sound settings using alsactl]) -AC_PLUGIN([dbus], [no], [Setup and start system message bus, D-Bus]) +AC_PLUGIN([dbus], [yes], [Setup and start system message bus, D-Bus]) AC_PLUGIN([modules-load], [no], [Scans /etc/modules-load.d for modules to load]) AC_PLUGIN([modprobe], [no], [Coldplug modules using modalias magic]) AC_PLUGIN([resolvconf], [no], [Setup necessary files for resolvconf]) @@ -155,7 +161,7 @@ AC_ARG_WITH(sysconfig, [sysconfig=$withval], [sysconfig=default]) AC_ARG_WITH(group, - AS_HELP_STRING([--with-group=NAME], [Group for /run/finit/socket (initctl), default: root]), + AS_HELP_STRING([--with-group=NAME], [Group for /run/finit/socket and /run/finit/bus, default: root]), [group=$withval], [group=root]) AC_ARG_WITH(hostname, @@ -243,6 +249,10 @@ AS_IF([test "x$enable_rescue" != "xno"], [ AM_CONDITIONAL(LOGROTATE, [test "x$enable_logrotate" = "xyes"]) +AS_IF([test "x$enable_dbus" = "xyes"], [ + AC_DEFINE(HAVE_DBUS, 1, [Build D-Bus support via libink])]) +AM_CONDITIONAL(DBUS, [test "x$enable_dbus" = "xyes"]) + ### With features ############################################################################## AS_IF([test "x$bash_dir" = "xyes"], [ PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0], @@ -288,7 +298,7 @@ AS_IF([test "x$with_heading" != "xno"], [ AS_IF([test "x$with_group" != "xno"], [ AS_IF([test "x$group" = "xyes"], [ group=root])]) -AC_DEFINE_UNQUOTED(DEFGROUP, "$group", [For /run/finit/socket]) +AC_DEFINE_UNQUOTED(DEFGROUP, "$group", [For /run/finit/socket and /run/finit/bus]) AS_IF([test "x$with_hostname" != "xno"], [ AS_IF([test "x$hostname" = "xyes"], [ @@ -438,6 +448,7 @@ Optional features: Built-in sulogin......: $with_sulogin $sulogin Built-in watchdogd....: $with_watchdog $watchdog Built-in logrotate....: $enable_logrotate + D-Bus support (libink): $enable_dbus Replacement libsystemd: $with_libsystemd Use cgroup v2.........: $enable_cgroup Use libcap............: $enable_libcap diff --git a/dbus-1/.gitignore b/dbus-1/.gitignore new file mode 100644 index 00000000..b336cc7c --- /dev/null +++ b/dbus-1/.gitignore @@ -0,0 +1,2 @@ +/Makefile +/Makefile.in diff --git a/dbus-1/Makefile.am b/dbus-1/Makefile.am new file mode 100644 index 00000000..dec4a333 --- /dev/null +++ b/dbus-1/Makefile.am @@ -0,0 +1,6 @@ +EXTRA_DIST = org.finit.conf + +if DBUS +dbuspolicydir = $(sysconfdir)/dbus-1/system.d +dist_dbuspolicy_DATA = org.finit.conf +endif diff --git a/dbus-1/org.finit.conf b/dbus-1/org.finit.conf new file mode 100644 index 00000000..5eb40fc2 --- /dev/null +++ b/dbus-1/org.finit.conf @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 5b25a99b..9a3082cc 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -8,6 +8,14 @@ All relevant changes are documented in this file. ### Changes +- The `dbus.so` plugin, which starts an external `dbus-daemon`, is now + enabled by default. It does nothing on systems without a + `dbus-daemon` installed, and `--disable-dbus-plugin` opts out. The + daemon it used to register from C now lives in `20-dbus.conf`, and + the directories it needs in `tmpfiles.d/dbus.conf`, so both can be + overridden from `/etc` like any other system file. Those + directories are no longer chowned to `messagebus`, matching how + Finit ships directories for other daemons - The `tty` block takes `passenv`, which the line-based format has had since v4.4 (issue #286) and the block format was missing - New `provides` setting for run/task/service/sysv blocks, naming @@ -20,6 +28,55 @@ All relevant changes are documented in this file. same service, `command = { "/lib/systemd/systemd-udevd", "-udevd" }`, and Finit starts the first one it finds. The line-based format could only express this by repeating the whole stanza per candidate +- Finit now ships with a built-in brokerless D-Bus implementation, + **libink**, exposing the running init system as a peer on its own + private bus at `/run/finit/bus`, and -- opportunistically -- + registering `org.finit` on the standard system bus when a + `dbus-daemon` is reachable. No external `libdbus`/`sd-bus`/`GIO` + dependency. + + The bus implements the stock `org.freedesktop.DBus`, + `org.freedesktop.DBus.Peer`, `org.freedesktop.DBus.Introspectable`, + and `org.freedesktop.DBus.Properties` interfaces, plus three + Finit-specific ones: + + * `org.finit.Manager1` at `/org/finit/manager` -- + `ListServices`, `GetService`, `Start`/`Stop`/`Restart`/`Reload`, + `SetRunlevel`, `SetDebug`, `Signal`, `Suspend`, and the + `Reboot`/`Halt`/`Poweroff` triplet. Read-only properties + `Runlevel`, `PrevRunlevel`, `Version`. Signals + `ServiceStateChanged (sss)` and `RunlevelChanged (ss)`. + + * `org.finit.Service1` at `/org/finit/service/` -- one + object per loaded service, with `Start`/`Stop`/`Restart`/`Reload` + for working off an object handle rather than passing the + identity string around. + + * `org.finit.Cond1` at `/org/finit/cond` -- `Get`, `Set`, `Clear`, + `List`, `Dump` for [user-defined conditions](conditions.md), + with a `ConditionChanged (ss)` signal. + + Privileged methods reject non-root callers based on the kernel- + authenticated peer uid (`SO_PEERCRED`); read-only methods are open. + See [D-Bus Integration](dbus.md) for the full surface, build flag, + and `dbus-send`/`dbus-monitor` examples. + +- `initctl` now transparently routes through D-Bus when the bus is + reachable, with the legacy `INIT_SOCKET` transport as a fallback: + `start`, `stop`, `restart`, `reload`, `reload `, `reboot`, + `halt`, `poweroff`, `suspend`, `debug`, `signal`, `runlevel`, and + `cond {get,set,clr}` all use the new path. Two new subcommands + show up that have no legacy equivalent: + + * `initctl monitor` -- streams every signal on the bus to the + terminal, one line per delivery (`HH:MM:SS iface.member(args)`), + until interrupted. Same idea as `dbus-monitor`, but scoped to + Finit and with no address plumbing required. + + * Issuing `initctl cond set/clr` over D-Bus also fires the + `Cond1.ConditionChanged` signal, so observers see user-driven + state changes the same way they see service-driven ones. + - Restart log now spells out the signal name and flags core dumps, e.g. `killed by SIGKILL` or `killed by SIGSEGV, core dumped`, in place of the bare numeric `by signal: N`. Gives operators a much diff --git a/doc/build.md b/doc/build.md index 65bf5365..c018ff47 100644 --- a/doc/build.md +++ b/doc/build.md @@ -44,15 +44,25 @@ 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 init for container apps that can see the host's `/proc` filesystem +* `--disable-dbus`: Opt out of Finit's built-in D-Bus support, enabled by + default. See [D-Bus Integration](dbus.md) for what it provides. Not to + be confused with `--disable-dbus-plugin` below, which only governs + starting an external `dbus-daemon` + * `--enable-alsa-utils-plugin`: Enable the optional `alsa-utils.so` sound plugin. -* `--enable-dbus-plugin`: Enable the optional D-Bus `dbus.so` plugin. +* `--disable-dbus-plugin`: Drop the `dbus.so` plugin, which launches + `dbus-daemon` at boot. Enabled by default; the plugin does nothing on + a system that has no `dbus-daemon` installed, so opting out is only + needed to keep init from starting a bus on a system that has one. + Unrelated to the built-in bus, see `--disable-dbus` above. * `--enable-resolvconf-plugin`: Enable the `resolvconf.so` optional plugin. @@ -116,6 +126,60 @@ Linux config to: > `/etc/fstab`. +Testing +------- + +`make check` runs the test suite in a private namespace, so it is safe +on a running system. It needs `unshare` and, on Ubuntu, unprivileged +user namespaces enabled: + +```shell +sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 +make check +``` + +The D-Bus message parser is the only place in Finit where bytes off a +socket become pointers, so it also has a fuzz target. `make check` +runs it as a fixed sweep, which takes milliseconds and needs nothing +beyond the normal build. To fuzz it properly, build it with clang and +libFuzzer: + +```shell +clang -fsanitize=fuzzer,address -DLINK_FUZZ_LIBFUZZER -D_GNU_SOURCE \ + -I libink -I . -o fuzz-msg-parse \ + test/src/fuzz-msg-parse.c libink/*.c +mkdir -p .fuzz-corpus +./fuzz-msg-parse .fuzz-corpus -max_total_time=300 +``` + +Give it a corpus directory as above and it saves what it learns there, +so the next run picks up where this one left off instead of starting +cold. Nothing writes to it unless you name it: without the argument +libFuzzer keeps everything in memory and the run leaves only crashes +behind. `make distclean` clears the corpus and the target. + +Once a corpus has grown, most of it reaches code some earlier input +already reached. Minimise it: + +```shell +./fuzz-msg-parse -merge=1 .fuzz-corpus-min .fuzz-corpus +rm -rf .fuzz-corpus && mv .fuzz-corpus-min .fuzz-corpus +``` + +Run `./configure` first, the target needs the generated `config.h`. +If the link fails with `cannot find -lstdc++`, install the `libstdc++` +headers matching the newest GCC on the system, not the default one: +clang picks the newest tree it finds, and that is the one that needs +them. CI runs this target on every pull request, carrying its +corpus between runs so it reaches deeper over time. + +Feed a file back to the target to reproduce a find: + +```shell +./fuzz-msg-parse crash-3f2a... +``` + + Running ------- diff --git a/doc/dbus.md b/doc/dbus.md new file mode 100644 index 00000000..e57898a2 --- /dev/null +++ b/doc/dbus.md @@ -0,0 +1,338 @@ +D-Bus Integration +================= + +Finit ships with a built-in, brokerless [D-Bus][] implementation, **libink** +(`-link`), that exposes the running init system as a peer on its own private +bus, and optionally on the system bus when `dbus-daemon` is available. +Everything `initctl` does is also reachable from any generic D-Bus tooling — +`dbus-send`, `dbus-monitor`, `gdbus`, language bindings, dashboards, +monitoring agents, etc. + +> [!NOTE] +> D-Bus support is enabled by default, opt out at build time with +> `--disable-dbus`, see [Building](build.md) for details. When disabled, +> `initctl` keeps using the legacy `INIT_SOCKET` transport and Finit +> exposes no bus. + +Bus address +----------- + +| Bus | Address | +|------------------------|---------------------------------------------| +| Local (always) | `unix:path=/run/finit/bus` | +| System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket` | + +The **local** bus is brokerless: clients connect straight to Finit over a +Unix-domain socket using the standard D-Bus SASL EXTERNAL handshake. No +`dbus-daemon` is required, which makes it suitable for embedded systems that +don't ship one. + +The socket is `0660`, owned by `root` and the group given to +`--with-group` at build time, the same gate as `/run/finit/socket` that +`initctl` falls back on. The bus reaches every operation `initctl` +does, so restricting one and not the other would leave the door open. +Members of that group may use it, see [Authorization](#authorization). + +The **system** bus is best-effort: Finit probes for a running `dbus-daemon` +and, when reachable, claims the well-known name `org.finit` so that standard +tooling sees Finit just like any other system service: + +```sh +dbus-send --system --print-reply --dest=org.finit /org/finit/manager \ + org.finit.Manager1.ListServices + +dbus-monitor --system "sender='org.finit'" +``` + +If no system bus is present (the common case on embedded targets), this step +is silently skipped. + +Object tree +----------- + +``` +/ +├── org/ +│ └── finit/ +│ ├── manager Manager1 +│ ├── cond Cond1 +│ └── service/ +│ ├── keventd Service1 (one per service) +│ ├── sshd +│ └── … +└── org/freedesktop/DBus Standard well-known interfaces +``` + +Every node implements the usual stock interfaces: + +| Interface | Purpose | +|---------------------------------------|-------------------------------------------| +| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` | +| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` | +| `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description | +| `org.freedesktop.DBus.Properties` | `Get`, `GetAll`; nothing is writable | + +Note: `Hello`, `AddMatch`, and `RemoveMatch` are answered on the canonical +`/org/freedesktop/DBus` object only, as per the D-Bus specification. + +`org.finit.Manager1` +-------------------- + +Lives at **`/org/finit/manager`**. Owns the global init operations +and the service registry. + +### Methods + +| Method | In sig | Out sig | Priv. | Notes | +|------------------------------|--------|---------|-------|-----------------------------------------------------------| +| `ListServices` | — | `as` | no | Returns identities (`name`, `name:id`) of loaded services | +| `GetService` | `s` | `o` | no | Resolves an identity to its `Service1` object path | +| `Start` | `s` | — | yes | Start the service(s) matching the identity | +| `Stop` | `s` | — | yes | Stop the service(s) matching the identity | +| `Restart` | `s` | — | yes | Restart (stop + start) the service(s) | +| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes | +| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6) | +| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag | +| `Signal` | `su` | — | yes | Send signal `u` (1–31) to services matching identity `s` | +| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM | +| `Reboot`, `Halt`, `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence | + +### Properties + +All read-only strings; observable via `Properties.Get` and +`Properties.GetAll`. + +| Property | Type | Returns | +|----------------|------|--------------------------------------------------------| +| `Runlevel` | `s` | Current runlevel as a digit (`"2"`, `"3"`, …) or `"S"` | +| `PrevRunlevel` | `s` | Previous runlevel, same encoding, `"N"` when none | +| `Version` | `s` | Finit's version string (`PACKAGE_VERSION`) | + +### Signals + +| Signal | Body | Fires when | +|-----------------------|----------------------------------------|------------------------| +| `ServiceStateChanged` | `sss` — identity, old state, new state | Service transitions | +| `RunlevelChanged` | `ss` — old level, new level | System runlevel change | + +State names emitted by `ServiceStateChanged` are stable wire strings: +`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, +`paused`, `waiting`, `starting`, `running`. `RunlevelChanged` levels +use the same encoding as the `Runlevel` property: digits, `"S"`, `"N"`. + +`org.finit.Service1` (per-service objects) +------------------------------------------ + +Lives at **`/org/finit/service/`**, one object per loaded service. +`` is the service identity (name, or `name:id` for templated +services) put through systemd-style `_HH` hex escaping — ASCII alphanumerics +and `_` pass through, anything else becomes `_HH` where `HH` is the hex byte. +Use `Manager1.GetService(identity)` to look up the exact path rather than +constructing it by hand. + +| Method | In sig | Out sig | Priv. | Notes | +|-----------|--------|---------|-------|-------------------------------------------------------------| +| `Start` | — | — | yes | Equivalent to `Manager1.Start()` for this service | +| `Stop` | — | — | yes | … | +| `Restart` | — | — | yes | … | +| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart) | + +### Properties + +All read-only; observable via `Properties.Get` and `Properties.GetAll`. + +| Property | Type | Returns | +|-----------------|------|----------------------------------------------------------------| +| `Identity` | `s` | Service identity, `name` or `name:id` | +| `Name` | `s` | Program name (basename of the command) | +| `State` | `s` | Current status, same vocabulary as `initctl status` | +| `Pid` | `u` | Current PID, 0 when not running | +| `RestartCount` | `u` | Restarts since the last stable run | +| `Runlevels` | `u` | Allowed runlevels as a bitmask, bit N = runlevel N, bit 10 = S | +| `Description` | `s` | The service's `description` string | +| `Command` | `s` | Full command line, arguments included | +| `Conditions` | `s` | Declared conditions, raw `.conf` form | +| `Type` | `s` | Unit type: `service`, `task`, `run`, `sysv`, `tty`, `free` | +| `Origin` | `s` | Source `.conf` file, empty for built-ins | +| `Environment` | `s` | The service's `env` setting, raw | +| `PidFile` | `s` | Declared PID file, raw (`!` prefix included) | +| `User` | `s` | User the service runs as | +| `Group` | `s` | Group the service runs as | +| `Uptime` | `u` | Seconds since start, 0 when not running | +| `ExitStatus` | `u` | Raw `waitpid(2)` status from the last exit | +| `RestartsTotal` | `u` | Restarts over the service's lifetime | +| `RestartMax` | `u` | Restart limit before the service is blocked | +| `Starts` | `u` | Times started, for `manual-start` units | +| `ManualStart` | `b` | `manual-start` set in the `.conf` | +| `Forking` | `b` | Daemon forks to background | +| `Started` | `b` | Run/task completed successfully | + +On every state transition the object also emits the standard +`org.freedesktop.DBus.Properties.PropertiesChanged` signal: `State` +in the changed dictionary, `Pid` and `RestartCount` invalidated (call +`Get` for fresh values). + +The per-service surface lets generic tooling supply an object handle +once and then invoke methods on it, instead of repeatedly passing the +identity string. + +`org.finit.Cond1` +----------------- + +Lives at **`/org/finit/cond`**. Exposes Finit's +[condition system](conditions.md) to bus clients. + +### Methods + +| Method | In sig | Out sig | Priv. | Notes | +|---------|--------|---------|-------|--------------------------------------------------------------| +| `Get` | `s` | `s` | no | Returns `"on"`, `"off"`, or `"flux"` for the named condition | +| `Set` | `s` | — | yes | Assert a `usr/` condition | +| `Clear` | `s` | — | yes | Deassert a `usr/` condition | +| `List` | — | `as` | no | Names of all known conditions | +| `Dump` | — | `a(ss)` | no | `(name, state)` pairs for everything `List` returns | + +Note: non-`usr/*` paths are rejected with `InvalidArgs` -- system +conditions belong to Finit's state machine. + +### Signals + +| Signal | Body | Fires when | +|--------------------|------------------------|---------------------------------------| +| `ConditionChanged` | `ss` — name, new state | A condition is asserted or deasserted | + +Authorization +------------- + +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. + +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. + +`initctl` integration +--------------------- + +`initctl` transparently routes through D-Bus when the bus socket is present, +and falls back to the legacy `INIT_SOCKET` transport otherwise. Concretely, +the following subcommands use the bus first: + +| Subcommand | Method | +|----------------------------|--------------------------------------------------| +| `initctl start` | `Manager1.Start(svc)` | +| `initctl stop` | `Manager1.Stop(svc)` | +| `initctl restart` | `Manager1.Restart(svc)` | +| `initctl reload` | `Manager1.Reload()` | +| `initctl reload S` | `Service1.Reload()` (per-svc) | +| `initctl reboot` | `Manager1.Reboot()` | +| `initctl halt` | `Manager1.Halt()` | +| `initctl poweroff` | `Manager1.Poweroff()` | +| `initctl suspend` | `Manager1.Suspend()` | +| `initctl debug` | `Manager1.SetDebug()` | +| `initctl signal` | `Manager1.Signal(svc, signo)` | +| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` | +| `initctl cond set/get/clear` | `Cond1.{Set,Get,Clear}`, `clr` is an alias | + +Two `initctl` subcommands are pure D-Bus features without legacy equivalents: + +* `initctl monitor` — subscribes to every signal on the local bus and prints + one line per delivery (with timestamp, interface and member). Same idea as + `dbus-monitor`, but scoped to Finit and with no need to pass `--address`. + +* `initctl cond` emits the standard `Cond1.ConditionChanged` signal as a side + effect, so subscribers observe user-driven state changes the same way they + observe service-driven ones. + +Examples +-------- + +The examples below use `dbus-send` and `dbus-monitor`, which ship as part of +the [dbus][] reference implementation; they're widely packaged and don't pull +in any extra runtime. Any tool that speaks D-Bus over an AF_UNIX socket works +equally well — `gdbus`, Python's `jeepney`/`dasbus`, etc. — substitute their +syntax for setting the bus address. The wire protocol is the compatibility +surface: Finit's own **libink** is an internal implementation detail, external +clients should use any standard D-Bus library. + +When `org.finit` is registered on the system bus you can replace +`--address=unix:path=/run/finit/bus` with `--system` in any example below. + +List the running services: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.finit.Manager1.ListServices +``` + +Read the current runlevel via the Properties interface: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/manager \ + org.freedesktop.DBus.Properties.Get \ + string:org.finit.Manager1 string:Runlevel +``` + +Subscribe to every state change on the manager object: + +```sh +dbus-monitor --address=unix:path=/run/finit/bus \ + "type='signal',interface='org.finit.Manager1'" +``` + +Or use `initctl monitor`, which does the same without any address +plumbing. + +Restart a service by its object path: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/service/sshd \ + org.finit.Service1.Restart +``` + +Trigger a `usr/`-condition assertion that wakes any dependent service: + +```sh +dbus-send --address=unix:path=/run/finit/bus \ + --type=method_call --dest=org.finit \ + /org/finit/cond \ + org.finit.Cond1.Set string:"data-ready" +``` + +The `--dest=org.finit` argument is informational on the local +brokerless bus — Finit accepts any destination because there's no +broker to route by name — but `dbus-send` requires it syntactically. + +[dbus]: https://gitlab.freedesktop.org/dbus/dbus +[D-Bus]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/doc/plugins.md b/doc/plugins.md index 36b6ddde..458544a7 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -35,7 +35,10 @@ For your convenience a set of *optional* plugins are available: > the `/etc/fstab` file and in the Linux kernel config. * *dbus.so*: Setup and start system message bus, D-Bus, at boot. - _Optional plugin._ + Enabled by default, and a no-op on systems without a `dbus-daemon`. + The daemon is declared in `20-dbus.conf` and its directories in + `tmpfiles.d/dbus.conf`, both overridable from `/etc`; the plugin + itself only handles what has to inspect the running system. * *hook-scripts.so*: Trigger the execution of scripts from plugin hook points (see [Hooks](#hooks)). _Optional plugin._ diff --git a/libink/.gitignore b/libink/.gitignore new file mode 100644 index 00000000..ffdcdbf7 --- /dev/null +++ b/libink/.gitignore @@ -0,0 +1,7 @@ +.deps/* +.libs/* +.dirstamp +*.lo +libink.* +Makefile +Makefile.in diff --git a/libink/Makefile.am b/libink/Makefile.am new file mode 100644 index 00000000..e81d921f --- /dev/null +++ b/libink/Makefile.am @@ -0,0 +1,18 @@ +# libink — brokerless D-Bus library (server + client), born inside Finit +# +# Internal convenience library: linked statically into finit and +# initctl, nothing installed. Public install (and the ABI promise +# that comes with it) is deferred until libink is extracted into its +# own project. +noinst_LTLIBRARIES = libink.la +libink_la_SOURCES = server.c auth.c connection.c \ + proto.c proto.h \ + marshal.c marshal.h \ + dispatch.c builtin.c \ + match.c \ + path.c \ + client.c io.c log.c \ + link.h path.h internal.h + +libink_la_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE -D_BSD_SOURCE +libink_la_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 diff --git a/libink/README.md b/libink/README.md new file mode 100644 index 00000000..fbbf64c1 --- /dev/null +++ b/libink/README.md @@ -0,0 +1,64 @@ +libink — brokerless D-Bus for Finit +=================================== + +libink is a small C library implementing the [D-Bus wire protocol][spec], +both the server and the client side, without a broker and without any +dependency on `libdbus`, `sd-bus`, or GIO. It was born inside Finit to +let PID 1 be a bus of its own: clients connect straight to the listening +socket, authenticate with the standard SASL EXTERNAL handshake, and get +kernel-authenticated credentials via `SO_PEERCRED`. + +For what the bus exposes and how to talk to it, see the User Guide, +[D-Bus Integration](../doc/dbus.md). This file covers the library +itself. + +Status +------ + +libink is an internal implementation detail of Finit: built as a libtool +convenience library, linked statically into `finit` and `initctl`, +nothing installed. There is deliberately no ABI promise yet — that +comes if/when libink is extracted into a project of its own. External +D-Bus clients need none of this; the wire protocol is the compatibility +surface, any standard D-Bus library works. + +Layout +------ + +| File | Contents | +|-----------------|-------------------------------------------------------| +| `server.c` | Listening socket, accept, peer credential capture | +| `auth.c` | SASL EXTERNAL handshake, uid verification | +| `connection.c` | Per-peer state machine, message framing | +| `proto.c` | Wire header parse/build | +| `marshal.c` | Body (de)marshalling: basic types, arrays, variants | +| `dispatch.c` | Object tree, vtable registration, method dispatch | +| `builtin.c` | `org.freedesktop.DBus.*` stock interfaces | +| `match.c` | AddMatch/RemoveMatch rule parsing and signal filter | +| `path.c` | systemd-style `_HH` object path encoding | +| `client.c` | Outgoing connections, method calls, reply/signal wait | +| `io.c` | Shared EINTR-resilient read/write loops | + +Public API symbols carry the `link_*` prefix (`link.h`), internal ones +`__*` (`internal.h`). Method handlers are registered as vtables of +`link_method_t`/`link_property_t`; the framework emits variant +signatures from the property table so the declared type is the single +source of truth. + +The boundary to Finit is deliberate: nothing under `libink/` includes a +Finit header. All glue lives in `src/dbus.c` — object registration, +signal emission from the service/condition/runlevel hook points, and +the uev event loop bridge. `initctl` uses the client half of the same +library, so one wire-format implementation serves both ends. If libink +is ever spun out, that file is the cut line. + +Testing +------- + +The `test/dbus-*.sh` suite exercises the library end to end against a +live Finit in a namespace, driven by `test/src/dbus-auth-client.c`. +Wire-format conformance against third-party tools (`dbus-send`, +`dbus-monitor`) and fuzzing of the parsers are tracked as pre-merge +work — this is PID 1's attack surface. + +[spec]: https://dbus.freedesktop.org/doc/dbus-specification.html diff --git a/libink/auth.c b/libink/auth.c new file mode 100644 index 00000000..76baee13 --- /dev/null +++ b/libink/auth.c @@ -0,0 +1,300 @@ +/* libink — D-Bus AUTH EXTERNAL handshake + * + * Implements the line-based SASL-style exchange described in the + * D-Bus specification, section "Authentication Protocol". Only the + * AUTH EXTERNAL mechanism is offered; everything else is rejected. + * + * The exchange: + * + * client --> [nul byte] + * client --> "AUTH EXTERNAL \r\n" + * server <-- "OK \r\n" + * client --> "NEGOTIATE_UNIX_FD\r\n" [optional] + * server <-- "ERROR \r\n" (no fd-passing yet) + * client --> "BEGIN\r\n" + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +static const char rejected_ext[] = "REJECTED EXTERNAL\r\n"; + +#define write_all(fd, buf, len) __io_write_all((fd), (buf), (len)) + +/* Shared by __auth_generate_guid (server) and __auth_client + * (client) for hex-encoding GUIDs and uid claims. */ +static const char hex_digits[] = "0123456789abcdef"; + +static int reply(int fd, const char *line) +{ + return write_all(fd, line, strlen(line)); +} + +static int reject(link_connection_t *conn) +{ + return write_all(conn->fd, rejected_ext, sizeof(rejected_ext) - 1); +} + +void __auth_generate_guid(char out[33]) +{ + uint8_t raw[16]; + size_t i; + + if (getrandom(raw, sizeof(raw), 0) != (ssize_t)sizeof(raw)) { + /* Extraordinarily unlikely; GUID is informational, not a + * security primitive — fall back to something deterministic + * rather than uninitialized memory. */ + for (i = 0; i < sizeof(raw); i++) + raw[i] = (uint8_t)(i ^ 0xa5); + } + + for (i = 0; i < sizeof(raw); i++) { + out[i * 2] = hex_digits[raw[i] >> 4]; + out[i * 2 + 1] = hex_digits[raw[i] & 0xf]; + } + out[32] = '\0'; +} + +static int hexval(int c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* Parse "AUTH EXTERNAL " payload into a uid. The argument is + * an even-length hex string whose decoded form is a decimal uid in + * ASCII. Returns 0 on success, -1 on malformed input. */ +static int parse_external_uid(const char *arg, size_t arglen, uid_t *out) +{ + char decoded[24]; + char *ep = NULL; + unsigned long v; + size_t i, dlen; + + if (arglen == 0 || (arglen & 1) || arglen / 2 >= sizeof(decoded)) + return -1; + + dlen = arglen / 2; + for (i = 0; i < dlen; i++) { + int hi = hexval((unsigned char)arg[i * 2]); + int lo = hexval((unsigned char)arg[i * 2 + 1]); + + if (hi < 0 || lo < 0) + return -1; + decoded[i] = (char)((hi << 4) | lo); + } + decoded[dlen] = '\0'; + + errno = 0; + v = strtoul(decoded, &ep, 10); + if (errno || !ep || *ep != '\0' || v > (unsigned long)((uid_t)-1)) + return -1; + + *out = (uid_t)v; + return 0; +} + +static int handle_line(link_connection_t *conn, const char *line, size_t len) +{ + if (len >= 14 && memcmp(line, "AUTH EXTERNAL ", 14) == 0) { + uid_t claimed; + char ok[64]; + + if (parse_external_uid(line + 14, len - 14, &claimed) < 0) + return reject(conn); + if (conn->peer_uid == (uid_t)-1 || claimed != conn->peer_uid) + return reject(conn); + + snprintf(ok, sizeof(ok), "OK %s\r\n", conn->guid); + return reply(conn->fd, ok); + } + + if (len == 4 && memcmp(line, "AUTH", 4) == 0) + return reject(conn); + + if (len == 17 && memcmp(line, "NEGOTIATE_UNIX_FD", 17) == 0) + return reply(conn->fd, "ERROR fd-passing not supported\r\n"); + + if (len == 5 && memcmp(line, "BEGIN", 5) == 0) { + conn->auth = LINK_AUTH_DONE; + return 0; + } + + if (len == 6 && memcmp(line, "CANCEL", 6) == 0) + return reject(conn); + + if (len >= 5 && memcmp(line, "ERROR", 5) == 0) + return reject(conn); + + return reply(conn->fd, "ERROR Unknown command\r\n"); +} + +/* Pull one CR+LF-terminated line out of conn->linebuf. Returns the + * line length (without the CR+LF), or 0 if no complete line is + * present yet. Consumes the line on success. */ +static size_t take_line(link_connection_t *conn, char *out, size_t outsz) +{ + size_t i; + + for (i = 0; i + 1 < conn->linelen; i++) { + if (conn->linebuf[i] == '\r' && conn->linebuf[i + 1] == '\n') { + size_t linelen = i; + size_t consumed = i + 2; + + if (linelen >= outsz) + linelen = outsz - 1; + + memcpy(out, conn->linebuf, linelen); + out[linelen] = '\0'; + + memmove(conn->linebuf, conn->linebuf + consumed, + conn->linelen - consumed); + conn->linelen -= consumed; + return linelen; + } + } + return 0; +} + +int __auth_process(link_connection_t *conn) +{ + uint8_t buf[256]; + ssize_t n; + size_t off = 0; + + n = read(conn->fd, buf, sizeof(buf)); + if (n == 0) + return -1; /* peer closed */ + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return 0; + return -1; + } + + if (conn->auth == LINK_AUTH_NUL) { + if (buf[0] != 0x00) { + conn->auth = LINK_AUTH_FAILED; + return -1; + } + off = 1; + conn->auth = LINK_AUTH_LINE; + } + + if (conn->auth == LINK_AUTH_LINE) { + size_t take = (size_t)n - off; + char line[LINK_AUTH_LINEBUF_SIZE]; + size_t linelen; + + if (conn->linelen + take > sizeof(conn->linebuf)) { + conn->auth = LINK_AUTH_FAILED; + return -1; + } + memcpy(conn->linebuf + conn->linelen, buf + off, take); + conn->linelen += take; + + while ((linelen = take_line(conn, line, sizeof(line))) > 0) { + if (handle_line(conn, line, linelen) < 0) + return -1; + if (conn->auth != LINK_AUTH_LINE) + break; + } + + /* If BEGIN flipped us to DONE, any remaining linebuf bytes + * are the first bytes of the binary D-Bus stream — move + * them to rxbuf so the dispatcher can pick them up on the + * next process() call. */ + if (conn->auth == LINK_AUTH_DONE && conn->linelen > 0) { + if (conn->linelen > sizeof(conn->rxbuf)) + return -1; + memcpy(conn->rxbuf, conn->linebuf, conn->linelen); + conn->rxlen = conn->linelen; + conn->linelen = 0; + } + } + + return 0; +} + +/* ---- client-side SASL composer ---- */ + +/* Read a single CR+LF (or just LF) terminated line from fd into buf. + * Returns the line length (without the terminator), or -1 on EOF or + * buffer overflow. Blocks until a complete line arrives. + * + * Used only by __auth_client; the server-side parser does its + * own line extraction out of conn->linebuf. */ +static ssize_t client_read_line(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; + return -1; + } + if (buf[off] == '\n') { + buf[off] = '\0'; + if (off > 0 && buf[off - 1] == '\r') + buf[--off] = '\0'; + return (ssize_t)off; + } + off++; + } + return -1; +} + +int __auth_client(int fd, uid_t uid) +{ + + char uidstr[16]; + char hexuid[32]; + char line[64]; + char reply_line[256]; + size_t i, n; + int rc; + + if (write_all(fd, "\0", 1) < 0) + return -1; + + n = (size_t)snprintf(uidstr, sizeof(uidstr), "%u", (unsigned)uid); + if (n * 2 >= sizeof(hexuid)) + return -1; + for (i = 0; i < n; i++) { + unsigned c = (unsigned char)uidstr[i]; + + hexuid[i * 2] = hex_digits[c >> 4]; + hexuid[i * 2 + 1] = hex_digits[c & 0xf]; + } + hexuid[n * 2] = '\0'; + + rc = snprintf(line, sizeof(line), "AUTH EXTERNAL %s\r\n", hexuid); + if (rc < 0 || (size_t)rc >= sizeof(line)) + return -1; + if (write_all(fd, line, (size_t)rc) < 0) + return -1; + + if (client_read_line(fd, reply_line, sizeof(reply_line)) < 0) + return -1; + if (strncmp(reply_line, "OK ", 3) != 0) + return -1; + + if (write_all(fd, "BEGIN\r\n", 7) < 0) + return -1; + return 0; +} diff --git a/libink/builtin.c b/libink/builtin.c new file mode 100644 index 00000000..fa511c1a --- /dev/null +++ b/libink/builtin.c @@ -0,0 +1,505 @@ +/* libink — built-in implementations of the well-known + * org.freedesktop.DBus.* interfaces (Hello, Peer, Introspectable). + * + * These run before object-tree lookup in the dispatcher; returning + * 0 means "handled, reply sent"; <0 means "not a built-in, fall + * through to user-registered handlers". + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +#include "internal.h" + +/* ---------- helpers ---------- */ + +static int member_is(const struct link_msg *m, const char *iface, const char *member) +{ + if (!m->member || strcmp(m->member, member) != 0) + return 0; + if (m->interface && strcmp(m->interface, iface) != 0) + return 0; + return 1; +} + +static int send_string_reply(link_connection_t *conn, const struct link_msg *req, + const char *s) +{ + struct link_writer w; + ssize_t blen; + + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_string(&w, s); + blen = __w_finish(&w); + if (blen < 0) { + errno = EMSGSIZE; + return -1; + } + return __send_method_return(conn, req, "s", conn->txbuf, (size_t)blen); +} + +/* ---------- Hello ---------- */ + +static int handle_hello(link_connection_t *conn, const struct link_msg *m) +{ + if (!conn->unique_name[0]) { + uint32_t n = ++conn->server->next_unique_id; + + snprintf(conn->unique_name, sizeof(conn->unique_name), + ":1.%u", n); + } + return send_string_reply(conn, m, conn->unique_name); +} + +/* ---------- Ping / GetMachineId ---------- */ + +static int handle_ping(link_connection_t *conn, const struct link_msg *m) +{ + return __send_method_return(conn, m, NULL, NULL, 0); +} + +static int handle_get_machine_id(link_connection_t *conn, const struct link_msg *m) +{ + /* D-Bus mandates a 32-char hex machine-id. Use the per-server + * GUID-style identifier we already generate for each connection, + * promoted to a per-server constant on first call. Good enough + * for the brokerless case where clients use this only as a + * sanity hint. */ + static char machine_id[33]; + + if (!machine_id[0]) + __auth_generate_guid(machine_id); + return send_string_reply(conn, m, machine_id); +} + +/* ---------- Introspect ---------- */ + +struct xbuf { + char *buf; + size_t cap; + size_t off; + int err; +}; + +static void xprintf(struct xbuf *x, const char *fmt, ...) +{ + va_list ap; + int n; + + if (x->err) + return; + va_start(ap, fmt); + n = vsnprintf(x->buf + x->off, x->cap - x->off, fmt, ap); + va_end(ap); + if (n < 0 || (size_t)n >= x->cap - x->off) { + x->err = 1; + return; + } + x->off += (size_t)n; +} + +/* + * Advance past one single complete type in a D-Bus signature: + * a basic type code, 'a' + element type, or a bracketed group. + * Signatures come from our own vtables, so trust them; an + * unterminated group just stops at NUL. + */ +static const char *sig_next(const char *p) +{ + while (*p == 'a') /* array prefixes, then element type */ + p++; + if (*p == '(' || *p == '{') { + char close = *p == '(' ? ')' : '}'; + + for (p++; *p && *p != close; p = sig_next(p)) + ; + } + return *p ? p + 1 : p; /* NUL: unterminated group, stop here */ +} + +static void emit_args(struct xbuf *x, const char *sig, const char *dir) +{ + const char *p, *e; + + for (p = sig; p && *p; p = e) { + e = sig_next(p); + xprintf(x, " \n", + (int)(e - p), p, dir); + } +} + +/* Emit a single stanza for one method definition. */ +static void emit_method(struct xbuf *x, const link_method_t *m) +{ + xprintf(x, " \n", m->name); + emit_args(x, m->in_sig, "in"); + emit_args(x, m->out_sig, "out"); + xprintf(x, " \n"); +} + +static void emit_property(struct xbuf *x, const link_property_t *p) +{ + /* Setters are not implemented, so every property advertises + * access="read" today. When Properties.Set lands, switch on + * a writable flag. */ + xprintf(x, " \n", + p->name, p->sig ? p->sig : "s"); +} + +static const char STANDARD_INTERFACES_XML[] = + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n"; + +/* Is `child` a path under `parent`? If so, write the first segment + * of the relative remainder into out (max outsz) and return 1. */ +static int child_segment(const char *parent, const char *child, + char *out, size_t outsz) +{ + size_t plen = strlen(parent); + const char *rest, *slash; + size_t seglen; + + if (strncmp(parent, child, plen) != 0) + return 0; + /* Special case for "/" */ + if (plen == 1 && parent[0] == '/') + rest = child + 1; + else if (child[plen] != '/') + return 0; + else + rest = child + plen + 1; + if (!*rest) + return 0; + + slash = strchr(rest, '/'); + seglen = slash ? (size_t)(slash - rest) : strlen(rest); + if (seglen + 1 > outsz) + return 0; + memcpy(out, rest, seglen); + out[seglen] = '\0'; + return 1; +} + +static int handle_introspect(link_connection_t *conn, const struct link_msg *m) +{ + static char xml[8192]; /* static keeps the stack small in PID 1 */ + struct xbuf x = { .buf = xml, .cap = sizeof(xml) }; + struct link_object *o; + const char *path = m->path; + + xprintf(&x, + "\n" + "\n"); + + xprintf(&x, "%s", STANDARD_INTERFACES_XML); + + o = NULL; + { + struct link_object *p; + + TAILQ_FOREACH(p, &conn->server->objects, link) { + if (strcmp(p->path, path) == 0) { + o = p; + break; + } + } + } + + if (o) { + struct link_vtable_entry *e; + const link_method_t *meth; + + TAILQ_FOREACH(e, &o->vtables, link) { + const link_property_t *prop; + + xprintf(&x, " \n", + e->vt->interface); + if (e->vt->methods) + for (meth = e->vt->methods; meth->name; meth++) + emit_method(&x, meth); + if (e->vt->properties) + for (prop = e->vt->properties; prop->name; prop++) + emit_property(&x, prop); + xprintf(&x, " \n"); + } + } + + { + struct link_object *p; + char prev_seg[LINK_PATH_MAX] = { 0 }; + char seg [LINK_PATH_MAX]; + + TAILQ_FOREACH(p, &conn->server->objects, link) { + if (!child_segment(path, p->path, seg, sizeof(seg))) + continue; + if (strcmp(prev_seg, seg) == 0) + continue; + xprintf(&x, " \n", seg); + memcpy(prev_seg, seg, sizeof(prev_seg)); + } + } + + xprintf(&x, "\n"); + + if (x.err) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Introspection XML overflow"); + + return send_string_reply(conn, m, xml); +} + +/* ---------- Properties.Get / GetAll ---------- */ + +/* Find the (object, vtable-entry) pair matching `path` and `interface`. + * Returns NULL if the path is unknown or the interface isn't exposed + * on it. */ +static struct link_vtable_entry * +find_vtable(link_connection_t *conn, const char *path, const char *interface) +{ + struct link_object *o; + + if (!path || !interface) + return NULL; + TAILQ_FOREACH(o, &conn->server->objects, link) { + struct link_vtable_entry *e; + + if (strcmp(o->path, path) != 0) + continue; + TAILQ_FOREACH(e, &o->vtables, link) { + if (strcmp(e->vt->interface, interface) == 0) + return e; + } + } + return NULL; +} + +static int handle_properties_get(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface, *prop_name; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "ss") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.Get takes (interface, property)"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0 || __r_string(&r, &prop_name) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e || !e->vt->properties) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + for (p = e->vt->properties; p->name; p++) { + if (strcmp(p->name, prop_name) != 0) + continue; + if (!p->getter) + break; + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_sig(&w, p->sig ? p->sig : "s"); + if (p->getter(&w, e->userdata) != 0 || (blen = __w_finish(&w)) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + return __send_method_return(conn, m, "v", + conn->txbuf, (size_t)blen); + } + + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownProperty", + "No such property on this interface"); +} + +static int handle_properties_get_all(link_connection_t *conn, const struct link_msg *m) +{ + const char *iface; + struct link_reader r; + struct link_writer w; + struct link_vtable_entry *e; + const link_property_t *p; + ssize_t blen; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Properties.GetAll takes one string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &iface) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + e = find_vtable(conn, m->path, iface); + if (!e) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownInterface", + "No such interface on this object"); + + __w_init(&w, conn->txbuf, sizeof(conn->txbuf)); + __w_array_begin(&w, '{'); + if (e->vt->properties) { + for (p = e->vt->properties; p->name; p++) { + if (!p->getter) + continue; + __w_struct_begin(&w); + __w_string(&w, p->name); + __w_sig(&w, p->sig ? p->sig : "s"); + if (p->getter(&w, e->userdata) != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Property getter failed"); + __w_struct_end(&w); + } + } + __w_array_end(&w); + + blen = __w_finish(&w); + if (blen < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply too large"); + + return __send_method_return(conn, m, "a{sv}", + conn->txbuf, (size_t)blen); +} + +/* ---------- AddMatch / RemoveMatch ---------- */ + +static int handle_add_match(link_connection_t *conn, const struct link_msg *m) +{ + const char *rule; + struct link_reader r; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "AddMatch takes a single string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &rule) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + if (__match_add(conn, rule) < 0) { + if (errno == ENOSPC) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.LimitsExceeded", + "Too many active match rules"); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.MatchRuleInvalid", + "Unrecognised key or malformed rule"); + } + return __send_method_return(conn, m, NULL, NULL, 0); +} + +static int handle_remove_match(link_connection_t *conn, const struct link_msg *m) +{ + const char *rule; + struct link_reader r; + + if (!m->signature || strcmp(m->signature, "s") != 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "RemoveMatch takes a single string"); + + __r_init(&r, m->body, m->body_avail); + if (__r_string(&r, &rule) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Malformed argument"); + + if (__match_remove(conn, rule) < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.MatchRuleNotFound", + "No such match rule on this connection"); + + return __send_method_return(conn, m, NULL, NULL, 0); +} + +/* ---------- entry point ---------- */ + +int __handle_builtin(link_connection_t *conn, const struct link_msg *m) +{ + /* Hello and AddMatch/RemoveMatch are per-connection state, and on + * a broker link the connection is shared by every caller: one + * sender could exhaust the match cap or drop another's rule. + * Naming and subscription belong to the broker for its own + * clients, so we do not answer these there. */ + if (conn->broker && m->member && + (!strcmp(m->member, "Hello") || + !strcmp(m->member, "AddMatch") || + !strcmp(m->member, "RemoveMatch"))) { + __dbg("%s is the broker's to answer, not ours", m->member); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.AccessDenied", + "Handled by the message bus, not by this peer"); + } + + if (member_is(m, "org.freedesktop.DBus", "Hello") && + m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) + return handle_hello(conn, m); + + if (member_is(m, "org.freedesktop.DBus", "AddMatch") && + m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) + return handle_add_match(conn, m); + + if (member_is(m, "org.freedesktop.DBus", "RemoveMatch") && + m->path && strcmp(m->path, "/org/freedesktop/DBus") == 0) + return handle_remove_match(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Peer", "Ping")) + return handle_ping(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Peer", "GetMachineId")) + return handle_get_machine_id(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Introspectable", "Introspect")) + return handle_introspect(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Properties", "Get")) + return handle_properties_get(conn, m); + + if (member_is(m, "org.freedesktop.DBus.Properties", "GetAll")) + return handle_properties_get_all(conn, m); + + return -1; /* not a built-in */ +} diff --git a/libink/client.c b/libink/client.c new file mode 100644 index 00000000..6a0c60b4 --- /dev/null +++ b/libink/client.c @@ -0,0 +1,348 @@ +/* libink — synchronous client-side D-Bus calls. + * + * Pairs with server.c / connection.c on the receiving end. The + * intent is for short-lived CLI tools (initctl) and tests to use + * libink as their D-Bus client rather than reimplementing the + * wire format. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +struct link_client { + int fd; + uint32_t next_serial; + const char *destination; /* not owned; NULL when brokerless */ + link_reply_t reply; /* most recent reply view (points into rxbuf) */ + /* Distinct from "reply.type == 0": LINK_MSG_INVALID is 0, which + * is a wire-valid (if malformed) type, so we need an out-of-band + * "have we ever produced a reply?" flag. */ + int have_reply; + /* Re-use the server-side rx buffer size for incoming replies. + * Replies to our methods are bounded by the same per-message + * sanity cap as everything else. */ + uint8_t rxbuf[LINK_RX_BUF_SIZE]; + size_t rxlen; +}; + +link_client_t *link_client_open_timeout(const char *path, int timeout_ms) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + link_client_t *c; + int fd; + + if (!path || strlen(path) >= sizeof(sun.sun_path)) + return NULL; + memcpy(sun.sun_path, path, strlen(path) + 1); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + return NULL; + + if (timeout_ms > 0) { + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + /* Cover both directions so the AUTH write and the + * subsequent read both honour the budget. setsockopt + * failure is non-fatal -- the bus may still respond + * quickly enough; we just lose the safety net. */ + (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { + close(fd); + return NULL; + } + if (__auth_client(fd, geteuid()) < 0) { + close(fd); + return NULL; + } + + c = calloc(1, sizeof(*c)); + if (!c) { + close(fd); + return NULL; + } + c->fd = fd; + c->next_serial = 1; + return c; +} + +link_client_t *link_client_open(const char *path) +{ + return link_client_open_timeout(path, 0); +} + +void link_client_set_destination(link_client_t *c, const char *destination) +{ + if (c) + c->destination = destination; +} + +void link_client_close(link_client_t *c) +{ + if (!c) + return; + if (c->fd >= 0) + close(c->fd); + free(c); +} + +int link_client_steal_fd(link_client_t *c) +{ + int fd; + + if (!c) + return -1; + fd = c->fd; + c->fd = -1; + free(c); + return fd; +} + +/* read_full / send_all live in libink/io.c. */ +#define read_full(fd, buf, len) __io_read_full ((fd), (buf), (len)) +#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) + +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +/* Read one complete D-Bus message: the 16-byte fixed header tells + * us fields_len + body_len, so we then issue exactly one more read + * for the remainder. Both lengths are bounded against rxbuf before + * arithmetic so a malformed wire u32 can't wrap into a near-4-GiB + * read. */ +static int read_one(link_client_t *c, struct link_msg *msg) +{ + uint32_t body_len, fields_len, body_off, total; + ssize_t consumed; + + memset(msg, 0, sizeof(*msg)); + + if (read_full(c->fd, c->rxbuf, 16) < 0) + return -1; + if (c->rxbuf[0] != 'l') + return -1; + + body_len = (uint32_t)c->rxbuf[4] + | ((uint32_t)c->rxbuf[5] << 8) + | ((uint32_t)c->rxbuf[6] << 16) + | ((uint32_t)c->rxbuf[7] << 24); + fields_len = (uint32_t)c->rxbuf[12] + | ((uint32_t)c->rxbuf[13] << 8) + | ((uint32_t)c->rxbuf[14] << 16) + | ((uint32_t)c->rxbuf[15] << 24); + + /* Bound the wire-supplied lengths before any arithmetic on + * them. Without this, fields_len = 0xFFFFFFF0 would wrap + * 16u + fields_len to near zero, bypass the total < rxbuf + * check, and trigger an out-of-bounds read. */ + if (fields_len > sizeof(c->rxbuf) || body_len > sizeof(c->rxbuf)) + return -1; + + body_off = (uint32_t)ALIGN_UP(16u + fields_len, 8u); + total = body_off + body_len; + if (total > sizeof(c->rxbuf) || total < 16) + return -1; + + if (read_full(c->fd, c->rxbuf + 16, total - 16) < 0) + return -1; + c->rxlen = total; + + consumed = __msg_parse(c->rxbuf, c->rxlen, msg); + if (consumed <= 0) + return -1; + return 0; +} + +static void publish_reply(link_client_t *c, const struct link_msg *m) +{ + __msg_to_reply(&c->reply, m); + c->have_reply = 1; +} + +/* The reply view in c->reply points into c->rxbuf and is invalidated + * the moment we touch that buffer again -- clear it at every entry, + * even on the bad-args path, so link_client_reply() cannot return + * stale dangling pointers from a previous call. */ +static void clear_reply(link_client_t *c) +{ + if (!c) + return; + memset(&c->reply, 0, sizeof(c->reply)); + c->have_reply = 0; +} + +/* A broker interleaves traffic of its own with our replies: claiming a + * name makes it emit NameAcquired, and it arrives before the reply to + * the call that caused it. Read past anything that is not the reply + * we are waiting for. On a brokerless link nothing is interleaved and + * the first message read is always the one we want. + * + * Bounded so a chatty or hostile broker cannot stall PID 1 here; each + * read is bounded in turn by SO_RCVTIMEO when the caller asked for a + * timeout at open. */ +#define LINK_CALL_MAX_SKIP 16 + +static int read_reply(link_client_t *c, uint32_t serial) +{ + int i; + + for (i = 0; i < LINK_CALL_MAX_SKIP; i++) { + struct link_msg msg; + + if (read_one(c, &msg) < 0) + return -1; + + /* Not a reply at all, or a reply to something else. */ + if (msg.type != LINK_MSG_METHOD_RETURN && msg.type != LINK_MSG_ERROR) + continue; + if (msg.reply_serial != serial) + continue; + + publish_reply(c, &msg); + return 0; + } + + errno = EPROTO; + return -1; +} + +/* Wait up to timeout_ms (-1 = forever) for one full inbound frame + * and publish it. Returns 0 on success, 1 on timeout, -1 on error. */ +static int read_and_publish(link_client_t *c, int timeout_ms) +{ + struct link_msg msg; + + if (timeout_ms >= 0) { + struct pollfd pfd = { .fd = c->fd, .events = POLLIN }; + int rc; + + do { + rc = poll(&pfd, 1, timeout_ms); + } while (rc < 0 && errno == EINTR); + if (rc < 0) + return -1; + if (rc == 0) + return 1; + } + + if (read_one(c, &msg) < 0) + return -1; + publish_reply(c, &msg); + return 0; +} + +int link_client_call(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + uint8_t hdr[LINK_CALL_HDR_MAX]; + ssize_t hlen; + uint32_t serial; + + clear_reply(c); + if (!c || c->fd < 0 || !obj_path || !member) + return LINK_CALL_FAIL; + + serial = c->next_serial++; + hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, + obj_path, interface, member, + c->destination, + signature, (uint32_t)body_len); + if (hlen < 0) + return LINK_CALL_FAIL; + + if (send_all(c->fd, hdr, (size_t)hlen) < 0) + return LINK_CALL_FAIL; + if (body_len > 0 && send_all(c->fd, body, body_len) < 0) + return LINK_CALL_FAIL; + + if (read_reply(c, serial) < 0) + return LINK_CALL_FAIL; + + if (c->reply.type == LINK_MSG_METHOD_RETURN) + return LINK_CALL_OK; + if (c->reply.type == LINK_MSG_ERROR) + return LINK_CALL_ERROR; + return LINK_CALL_FAIL; +} + +const link_reply_t *link_client_reply(link_client_t *c) +{ + if (!c || !c->have_reply) + return NULL; + return &c->reply; +} + +int link_reply_get_string(const link_reply_t *r, const char **out) +{ + link_reader_t reader; + + if (out) + *out = NULL; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_string(&reader, out); +} + +int link_reply_get_u32(const link_reply_t *r, uint32_t *out) +{ + link_reader_t reader; + + if (out) + *out = 0; + if (!r || !r->body || !out) + return -1; + link_reader_init(&reader, r->body, r->body_len); + return link_r_u32(&reader, out); +} + +int link_client_call_v(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, ...) +{ + uint8_t body[LINK_CALL_BODY_MAX]; + ssize_t body_len = 0; + + if (signature && *signature) { + va_list ap; + + va_start(ap, signature); + body_len = __marshal_va(body, sizeof(body), signature, ap); + va_end(ap); + if (body_len < 0) + return LINK_CALL_FAIL; + } + + return link_client_call(c, obj_path, interface, member, + signature, body, (size_t)body_len); +} + +int link_client_wait(link_client_t *c, int timeout_ms) +{ + clear_reply(c); + if (!c || c->fd < 0) + return -1; + return read_and_publish(c, timeout_ms); +} diff --git a/libink/connection.c b/libink/connection.c new file mode 100644 index 00000000..d5cff196 --- /dev/null +++ b/libink/connection.c @@ -0,0 +1,251 @@ +/* libink — per-connection lifecycle and dispatch entry point + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +#include "internal.h" + +int link_connection_get_fd(const link_connection_t *conn) +{ + return conn ? conn->fd : -1; +} + +uid_t link_connection_get_uid(const link_connection_t *conn) +{ + return conn ? conn->peer_uid : (uid_t)-1; +} + +/* Issue a method call and remember the serial so the reply can be + * handed back to `cb` when the read loop picks it up. Nothing here + * waits: this is the counterpart of link_client_call() for a + * connection already owned by the event loop. */ +int link_connection_call(link_connection_t *conn, const char *destination, + const char *path, const char *interface, const char *member, + link_reply_cb_t cb, void *userdata, + const char *signature, ...) +{ + uint8_t body[LINK_CALL_BODY_MAX]; + uint8_t hdr[LINK_CALL_HDR_MAX]; + ssize_t blen = 0; + ssize_t hlen; + struct link_bus *bus; + uint32_t serial; + int i; + + if (!conn || conn->fd < 0 || !path || !member) { + errno = EINVAL; + return -1; + } + + bus = __bus_get(conn); + if (!bus) + return -1; + + for (i = 0; i < LINK_PENDING_CAP; i++) { + if (!bus->pending[i].used) + break; + } + if (i == LINK_PENDING_CAP) { + errno = EBUSY; + return -1; + } + + if (signature && *signature) { + va_list ap; + + va_start(ap, signature); + blen = __marshal_va(body, sizeof(body), signature, ap); + va_end(ap); + if (blen < 0) { + errno = EMSGSIZE; + return -1; + } + } + + serial = ++conn->next_serial; + hlen = __msg_build_method_call(hdr, sizeof(hdr), serial, path, interface, + member, destination, signature, (uint32_t)blen); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (__io_write_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (blen > 0 && __io_write_all(conn->fd, body, (size_t)blen) < 0) + return -1; + + __dbg("calling %s.%s on %s, serial %u", interface ? interface : "-", + member, destination ? destination : "peer", serial); + + bus->pending[i].used = 1; + bus->pending[i].serial = serial; + bus->pending[i].stamp = __now_ms(); + bus->pending[i].cb = cb; + bus->pending[i].userdata = userdata; + + return 0; +} + +struct link_bus *__bus_get(link_connection_t *conn) +{ + if (!conn->bus) + conn->bus = calloc(1, sizeof(*conn->bus)); + + return conn->bus; +} + +void __bus_free(link_connection_t *conn) +{ + free(conn->bus); + conn->bus = NULL; +} + +/* Neither half can time itself out, libink has no event loop, so the + * embedder sweeps. Calls go first: one timing out usually resolves + * the park it was made for, and being told AccessDenied says more to + * that caller than a bare timeout. */ +int link_connection_expire(link_connection_t *conn, unsigned int age_ms) +{ + struct link_bus *bus; + uint64_t now; + int i, live = 0; + + if (!conn || !conn->bus) + return 0; + + bus = conn->bus; + now = __now_ms(); + for (i = 0; i < LINK_PENDING_CAP; i++) { + link_reply_cb_t cb; + void *userdata; + + if (!bus->pending[i].used) + continue; + + if (now - bus->pending[i].stamp < age_ms) { + live++; + continue; + } + + cb = bus->pending[i].cb; + userdata = bus->pending[i].userdata; + bus->pending[i].used = 0; + + __dbg("timed out call serial %u, no reply", bus->pending[i].serial); + if (cb) + cb(conn, NULL, userdata); + } + + return live + __dispatch_expire_parked(conn, age_ms); +} + +void link_connection_close(link_connection_t *conn) +{ + size_t i; + + if (!conn) + return; + + __dbg("closing peer on fd %d", conn->fd); + + /* Anything waiting on this connection has to be told, or a parked + * call sits forever and its caller never hears back. */ + __dispatch_forget_conn(conn); + + for (i = 0; i < conn->matches_count; i++) + __match_free(conn->matches[i]); + + if (conn->fd >= 0) + close(conn->fd); + free(conn); +} + +/* Process buffered binary D-Bus messages, dispatching each complete + * message and shifting consumed bytes out of rxbuf. Returns -1 if + * we should drop the connection (peer closed, protocol error, + * downstream send failure). */ +static int process_binary(link_connection_t *conn) +{ + while (conn->rxlen > 0) { + struct link_msg msg; + ssize_t consumed; + + consumed = __msg_parse(conn->rxbuf, conn->rxlen, &msg); + if (consumed == 0) + break; /* incomplete; wait for more bytes */ + if (consumed < 0) + return -1; + + if (__dispatch_message(conn, &msg, (size_t)consumed) < 0) + return -1; + + memmove(conn->rxbuf, conn->rxbuf + consumed, + conn->rxlen - (size_t)consumed); + conn->rxlen -= (size_t)consumed; + } + return 0; +} + +int link_connection_process(link_connection_t *conn) +{ + if (!conn) { + errno = EINVAL; + return -1; + } + + if (conn->auth == LINK_AUTH_FAILED) + return -1; + + if (conn->auth != LINK_AUTH_DONE) { + if (__auth_process(conn) < 0) + return -1; + + /* Still in SASL phase — wait for more bytes. */ + if (conn->auth != LINK_AUTH_DONE) + return 0; + + /* Fall through: BEGIN may have arrived in the same read + * as the first binary message. auth_process moved those + * bytes into rxbuf; they must be dispatched now, because + * no further wake-up is guaranteed (the kernel has + * already delivered everything that was readable). */ + if (process_binary(conn) < 0) + return -1; + } + + /* Read additional bytes and dispatch any complete messages. + * process_binary is called inside the loop after every + * successful read; no second call after EAGAIN because the + * buffer hasn't changed. */ + for (;;) { + ssize_t n; + size_t room = sizeof(conn->rxbuf) - conn->rxlen; + + if (room == 0) { + errno = E2BIG; + return -1; + } + + n = read(conn->fd, conn->rxbuf + conn->rxlen, room); + if (n == 0) + return -1; /* peer closed */ + if (n < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return 0; + return -1; + } + conn->rxlen += (size_t)n; + if (process_binary(conn) < 0) + return -1; + } +} diff --git a/libink/dispatch.c b/libink/dispatch.c new file mode 100644 index 00000000..5597407d --- /dev/null +++ b/libink/dispatch.c @@ -0,0 +1,712 @@ +/* libink — object tree, vtable registration, and method dispatch. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +#include "internal.h" + +/* ---------- object/vtable registration ---------- */ + +static struct link_object *find_object(link_server_t *srv, const char *path) +{ + struct link_object *o; + + TAILQ_FOREACH(o, &srv->objects, link) + if (strcmp(o->path, path) == 0) + return o; + return NULL; +} + +int link_server_remove_object(link_server_t *srv, const char *path) +{ + struct link_object *o; + struct link_vtable_entry *e; + + if (!srv || !path) { + errno = EINVAL; + return -1; + } + + o = find_object(srv, path); + if (!o) { + errno = ENOENT; + return -1; + } + + while ((e = TAILQ_FIRST(&o->vtables))) { + TAILQ_REMOVE(&o->vtables, e, link); + free(e); + } + TAILQ_REMOVE(&srv->objects, o, link); + free(o); + return 0; +} + +int link_server_add_object(link_server_t *srv, const char *path, + const link_vtable_t *vt, void *userdata) +{ + struct link_object *o; + struct link_vtable_entry *e; + size_t plen; + + if (!srv || !path || !*path || !vt || !vt->interface) { + errno = EINVAL; + return -1; + } + plen = strlen(path); + if (plen >= LINK_PATH_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + o = find_object(srv, path); + if (!o) { + o = calloc(1, sizeof(*o)); + if (!o) + return -1; + memcpy(o->path, path, plen + 1); + TAILQ_INIT(&o->vtables); + TAILQ_INSERT_TAIL(&srv->objects, o, link); + } + + e = calloc(1, sizeof(*e)); + if (!e) + return -1; + e->vt = vt; + e->userdata = userdata; + TAILQ_INSERT_TAIL(&o->vtables, e, link); + return 0; +} + +/* ---------- lookup ---------- */ + +static const link_method_t *find_method(const link_vtable_t *vt, const char *name) +{ + const link_method_t *m; + + if (!vt->methods) + return NULL; + for (m = vt->methods; m->name; m++) + if (strcmp(m->name, name) == 0) + return m; + return NULL; +} + +/* If incoming.interface is NULL, search every interface on the + * object for a member with this name. Returns the matching method + * and writes back its vtable_entry in *out_e. */ +static const link_method_t *resolve(struct link_object *o, + const char *iface, const char *member, + struct link_vtable_entry **out_e) +{ + struct link_vtable_entry *e; + const link_method_t *m; + + if (iface) { + TAILQ_FOREACH(e, &o->vtables, link) { + if (strcmp(e->vt->interface, iface) != 0) + continue; + m = find_method(e->vt, member); + if (m) { + *out_e = e; + return m; + } + return NULL; + } + return NULL; + } + + TAILQ_FOREACH(e, &o->vtables, link) { + m = find_method(e->vt, member); + if (m) { + *out_e = e; + return m; + } + } + return NULL; +} + +/* ---------- send helpers ---------- */ + +/* + * Peer fds are non-blocking, so send_all may fail mid-frame (e.g. + * EAGAIN from a peer that stopped draining its socket). Any failure + * poisons the peer's stream: never retry on the same connection, + * drop the peer. + */ +#define send_all(fd, buf, len) __io_write_all((fd), (buf), (len)) + +int __send_method_return(link_connection_t *conn, const struct link_msg *req, + const char *out_sig, + const uint8_t *body, size_t body_len) +{ + uint8_t hdr[512]; + ssize_t hlen; + uint32_t serial = ++conn->next_serial; + + hlen = __msg_build_return(hdr, sizeof(hdr), serial, + req->serial, + req->sender, + out_sig, (uint32_t)body_len); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (send_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (body_len > 0 && send_all(conn->fd, body, body_len) < 0) + return -1; + return 0; +} + +int link_connection_emit_signal(link_connection_t *conn, + const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + uint8_t hdr[512]; + ssize_t hlen; + uint32_t serial; + size_t i; + int matched = 0; + + if (!conn || !path || !interface || !member) { + errno = EINVAL; + return -1; + } + if (conn->auth != LINK_AUTH_DONE) + return 0; /* peer hasn't finished the SASL phase */ + + /* A broker routes to whoever subscribed with it, so it wants + * every signal and never sends us AddMatch of its own. */ + matched = conn->broker; + + for (i = 0; !matched && i < conn->matches_count; i++) { + if (__match_matches(conn->matches[i], path, + interface, member)) + matched = 1; + } + if (!matched) + return 0; /* peer didn't subscribe — nothing to do */ + + serial = ++conn->next_serial; + hlen = __msg_build_signal(hdr, sizeof(hdr), serial, + path, interface, member, + signature, (uint32_t)body_len); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (send_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (body_len > 0 && send_all(conn->fd, body, body_len) < 0) + return -1; + return 0; +} + +int __send_error(link_connection_t *conn, const struct link_msg *req, + const char *error_name, const char *text) +{ + uint8_t hdr[512]; + uint8_t body[256]; + ssize_t hlen; + size_t blen = 0; + uint32_t serial = ++conn->next_serial; + const char *sig = NULL; + + if (text && *text) { + struct link_writer w; + ssize_t n; + + __w_init(&w, body, sizeof(body)); + __w_string(&w, text); + n = __w_finish(&w); + if (n < 0) { + errno = EMSGSIZE; + return -1; + } + blen = (size_t)n; + sig = "s"; + } + + hlen = __msg_build_error(hdr, sizeof(hdr), serial, + req->serial, req->sender, + error_name, sig, (uint32_t)blen); + if (hlen < 0) { + errno = EMSGSIZE; + return -1; + } + + if (send_all(conn->fd, hdr, (size_t)hlen) < 0) + return -1; + if (blen > 0 && send_all(conn->fd, body, blen) < 0) + return -1; + return 0; +} + +/* ---------- link_call public surface ---------- */ + +const char *link_call_path (const link_call_t *c) { return c ? c->incoming.path : NULL; } +const char *link_call_interface(const link_call_t *c) { return c ? c->incoming.interface : NULL; } +const char *link_call_member (const link_call_t *c) { return c ? c->incoming.member : NULL; } +uid_t link_call_uid (const link_call_t *c) { return c ? c->uid : LINK_UID_UNKNOWN; } + +link_writer_t *link_call_reply(link_call_t *call) +{ + if (!call || call->reply_consumed || call->error_sent) + return NULL; + call->reply_consumed = 1; + __w_init(&call->reply_writer, + call->conn->txbuf, sizeof(call->conn->txbuf)); + return &call->reply_writer; +} + +int link_call_reply_error(link_call_t *call, const char *name, const char *message) +{ + if (!call || call->error_sent) { + errno = EINVAL; + return -1; + } + call->error_sent = 1; + return __send_error(call->conn, &call->incoming, name, message); +} + +/* ---------- public reader wrappers ---------- */ + +int link_call_read_byte (link_call_t *c, uint8_t *o) { return __r_byte (&c->read_cursor, o); } +int link_call_read_bool (link_call_t *c, int *o) { return __r_bool (&c->read_cursor, o); } +int link_call_read_u32 (link_call_t *c, uint32_t *o) { return __r_u32 (&c->read_cursor, o); } +int link_call_read_string(link_call_t *c, const char **o) { return __r_string(&c->read_cursor, o); } +int link_call_read_path (link_call_t *c, const char **o) { return __r_path (&c->read_cursor, o); } + +/* ---------- public writer wrappers ---------- */ + +void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap) { __w_init(w, buf, cap); } +ssize_t link_writer_finish(link_writer_t *w) { return __w_finish(w); } + +void link_w_byte (link_writer_t *w, uint8_t v) { __w_byte(w, v); } +void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); } +void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); } +void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); } +void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); } +void link_w_variant_string(link_writer_t *w, const char *s) { __w_variant_string(w, s); } +void link_w_array_begin (link_writer_t *w, char ec) { __w_array_begin(w, ec); } +void link_w_array_end (link_writer_t *w) { __w_array_end(w); } +void link_w_struct_begin(link_writer_t *w) { __w_struct_begin(w); } +void link_w_struct_end (link_writer_t *w) { __w_struct_end(w); } + +/* ---------- public reader wrappers ---------- */ + +void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len) { __r_init(r, body, len); } +int link_r_byte (link_reader_t *r, uint8_t *o) { return __r_byte (r, o); } +int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); } +int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); } +int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); } +int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); } +int link_r_variant_begin (link_reader_t *r, char *type) { return __r_variant_begin(r, type); } +int link_r_skip_basic (link_reader_t *r, char type) { return __r_skip_basic(r, type); } +int link_r_variant_string(link_reader_t *r, const char **o) { return __r_variant_string(r, o); } +int link_r_align (link_reader_t *r, size_t n) { return __r_align (r, n); } +int link_r_array_begin(link_reader_t *r, size_t *e) { return __r_array_begin(r, e); } +int link_r_done (const link_reader_t *r) { return __r_done (r); } +size_t link_r_pos (const link_reader_t *r) { return r->off; } + +/* ---------- dispatch entry point ---------- */ + +/* ---------- replies to our own outbound calls ---------- */ + +/* Hand a reply to whoever issued the matching link_connection_call(). + * Unmatched replies are dropped: a broker is free to send us things we + * never asked for, and that is not a reason to drop the connection. */ +static void deliver_reply(link_connection_t *conn, const struct link_msg *m) +{ + link_reply_cb_t cb; + link_reply_t r; + void *userdata; + int i; + + if (!conn->bus) { + __dbg("unsolicited reply, serial %u", m->reply_serial); + return; + } + + for (i = 0; i < LINK_PENDING_CAP; i++) { + if (conn->bus->pending[i].used && + conn->bus->pending[i].serial == m->reply_serial) + break; + } + if (i == LINK_PENDING_CAP) { + __dbg("unsolicited reply, serial %u", m->reply_serial); + return; + } + + cb = conn->bus->pending[i].cb; + userdata = conn->bus->pending[i].userdata; + conn->bus->pending[i].used = 0; + + if (!cb) + return; + + __msg_to_reply(&r, m); + cb(conn, &r, userdata); +} + +/* ---------- calls parked while their caller is identified ---------- */ + +/* Every park gets a token that is never issued twice, so a resolver + * answering late, twice, or after its connection went away resumes + * nothing rather than whatever call has since taken the slot. */ +static struct link_parked *park(link_connection_t *conn, const uint8_t *frame, + size_t len, link_authz_t *tok) +{ + struct link_bus *bus; + int i; + + if (!frame || !len || len > LINK_PARKED_MSG_MAX) + return NULL; + + bus = __bus_get(conn); + if (!bus) + return NULL; + + for (i = 0; i < LINK_PARKED_CAP; i++) { + if (!bus->parked[i].tok) + break; + } + if (i == LINK_PARKED_CAP) + return NULL; + + bus->parked[i].tok = ++bus->next_tok; + bus->parked[i].conn = conn; + bus->parked[i].stamp = __now_ms(); + bus->parked[i].len = len; + memcpy(bus->parked[i].buf, frame, len); + *tok = bus->parked[i].tok; + + return &bus->parked[i]; +} + +static void unpark(struct link_parked *p) +{ + p->tok = 0; + p->conn = NULL; +} + +/* A resolver that answers late, or never, would otherwise hold both + * the slot and the caller forever. Returns how many are still parked; + * link_connection_expire() is what calls this. */ +int __dispatch_expire_parked(link_connection_t *conn, unsigned int age_ms) +{ + uint64_t now; + int i, live = 0; + + if (!conn->bus) + return 0; + + now = __now_ms(); + for (i = 0; i < LINK_PARKED_CAP; i++) { + struct link_parked *p = &conn->bus->parked[i]; + uint8_t buf[LINK_PARKED_MSG_MAX]; + struct link_msg msg; + size_t len; + + if (!p->tok) + continue; + + if (now - p->stamp < age_ms) { + live++; + continue; + } + + /* Free the slot before replying, as link_uid_resolved() + * does: the send path must not find it still parked. */ + len = p->len; + memcpy(buf, p->buf, len); + unpark(p); + + if (__msg_parse(buf, len, &msg) <= 0) + continue; + + __dbg("timed out %s, nobody said who the caller was", + msg.member ? msg.member : "call"); + (void)__send_error(conn, &msg, + "org.freedesktop.DBus.Error.TimedOut", + "Timed out identifying the caller"); + } + + return live; +} + +void __dispatch_forget_conn(link_connection_t *conn) +{ + struct link_bus *bus = conn->bus; + int i; + + if (!bus) + return; + + /* Drop parked calls first. A pending callback below may try to + * resolve one, and resuming a dispatch on a connection that is + * being torn down is no use to anyone; an invalidated slot makes + * that resolve a no-op instead. */ + for (i = 0; i < LINK_PARKED_CAP; i++) + unpark(&bus->parked[i]); + + for (i = 0; i < LINK_PENDING_CAP; i++) { + if (bus->pending[i].used && bus->pending[i].cb) + bus->pending[i].cb(conn, NULL, bus->pending[i].userdata); + bus->pending[i].used = 0; + } + + __bus_free(conn); +} + +static int dispatch_call(link_connection_t *conn, const struct link_msg *m, + const uint8_t *frame, size_t framelen, + const uid_t *known_uid); + +void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid) +{ + uint8_t buf[LINK_PARKED_MSG_MAX]; + struct link_parked *p = NULL; + struct link_msg msg; + size_t len; + int i; + + if (!conn || !conn->bus || !tok) + return; + + for (i = 0; i < LINK_PARKED_CAP; i++) { + if (conn->bus->parked[i].tok == tok) { + p = &conn->bus->parked[i]; + break; + } + } + if (!p) + return; /* stale handle, already answered */ + + /* Copy the message out and free the slot before dispatching: + * the handler may park a call of its own. */ + len = p->len; + memcpy(buf, p->buf, len); + unpark(p); + + if (__msg_parse(buf, len, &msg) <= 0) + return; + + __dbg("resumed %s, caller uid %d", msg.member ? msg.member : "call", (int)uid); + (void)dispatch_call(conn, &msg, NULL, 0, &uid); +} + +int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen) +{ + if (m->type == LINK_MSG_METHOD_RETURN || m->type == LINK_MSG_ERROR) { + deliver_reply(conn, m); + return 0; + } + + if (m->type != LINK_MSG_METHOD_CALL) { + /* Signals from a client to PID 1 are nonsense; drop. */ + return 0; + } + + return dispatch_call(conn, m, conn->rxbuf, framelen, NULL); +} + +/* Who may invoke a privileged method. Without an authorizer, only + * root, which is what libink can decide on its own. */ +static int caller_may(link_server_t *srv, uid_t uid) +{ + if (uid == LINK_UID_UNKNOWN) + return 0; + if (srv && srv->authorizer) + return srv->authorizer(uid, srv->authz_userdata); + + return uid == 0; +} + +/* Ask who is calling on a broker connection, where the message is the + * only evidence. Returns 0 with *uid set, 1 when the call was parked + * and will be dispatched again once the resolver answers, -1 when the + * caller cannot be identified, and -2 when we have no room to ask. */ +#define CALLER_UID_BUSY (-2) + +static int resolve_caller(link_connection_t *conn, const struct link_msg *m, + const uint8_t *frame, size_t framelen, uid_t *uid) +{ + link_server_t *srv = conn->server; + struct link_parked *p; + link_authz_t tok; + int rc; + + if (!srv || !srv->uid_resolver || !m->sender) + return -1; + + /* Enforce the rule link.h states, rather than trusting every + * resolver to remember it: a truncated sender key would let two + * callers share one identity. */ + if (strlen(m->sender) >= LINK_SENDER_MAX) { + __dbg("sender name too long, refusing to identify it"); + return -1; + } + + /* Park first so the resolver has somewhere to answer, then let + * it release the slot immediately if it already knew. */ + p = park(conn, frame, framelen, &tok); + if (!p) + return CALLER_UID_BUSY; + + rc = srv->uid_resolver(conn, m->sender, tok, uid, srv->uid_userdata); + if (rc != 1) + unpark(p); + else + __dbg("parked %s from %s, awaiting caller uid", m->member, m->sender); + + return rc; +} + +static int dispatch_call(link_connection_t *conn, const struct link_msg *m, + const uint8_t *frame, size_t framelen, + const uid_t *known_uid) +{ + struct link_object *o; + struct link_vtable_entry *e = NULL; + const link_method_t *meth; + struct link_call call; + ssize_t blen; + uid_t call_uid; + int rc; + + /* What a handler sees via link_call_uid(). Unresolved on a broker + * connection until a privileged method forces the question. */ + call_uid = known_uid ? *known_uid : conn->peer_uid; + + if (!m->path || !m->member) { + __dbg("malformed call, no path or member"); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Method call without path or member"); + } + + __dbg("call %s %s.%s from %s", m->path, + m->interface ? m->interface : "-", m->member, + m->sender ? m->sender : "peer"); + + /* Built-in DBus interfaces (Hello, Ping, Introspect, Properties) + * are handled here before object-tree lookup, which means they + * also run before the LINK_METHOD_PRIVILEGED authz gate further + * down. The current set is read-only; do NOT introduce a + * state-changing built-in without first adding equivalent + * authorisation inside __handle_builtin. */ + rc = __handle_builtin(conn, m); + if (rc >= 0) + return rc; /* 0 = handled OK, 1 = built-in but failed; <0 = not a built-in */ + + o = find_object(conn->server, m->path); + if (!o) { + __dbg("no such object %s", m->path); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownObject", + "No such object"); + } + + meth = resolve(o, m->interface, m->member, &e); + if (!meth) { + __dbg("no such method %s on %s", m->member, m->path); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.UnknownMethod", + "No such method on this object"); + } + + /* Validate signature: client must match the declared in_sig. */ + { + const char *got = m->signature ? m->signature : ""; + const char *want = meth->in_sig ? meth->in_sig : ""; + + if (strcmp(got, want) != 0) { + __dbg("%s takes '%s', caller sent '%s'", m->member, want, got); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.InvalidArgs", + "Argument signature mismatch"); + } + } + + /* Per-method authorization. PRIVILEGED methods require uid 0. + * On an ordinary connection the peer's uid was captured via + * SO_PEERCRED at accept time and verified against the AUTH + * EXTERNAL claim, so conn->peer_uid is the answer. A broker + * connection carries every caller at once, so who is asking has + * to be established per message, which may park the call. */ + if (meth->flags & LINK_METHOD_PRIVILEGED) { + if (conn->broker && !known_uid) { + rc = resolve_caller(conn, m, frame, framelen, &call_uid); + if (rc == 1) + return 0; /* parked, resumed later */ + + if (rc == CALLER_UID_BUSY) { + /* Not a permission problem: root may well + * be asking, we just have no slot to find + * out in. Say so, it is retryable. */ + __dbg("no free slot to identify %s", m->sender); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.LimitsExceeded", + "Too many calls awaiting authorization"); + } + if (rc < 0) + call_uid = (uid_t)-1; + } + + if (!caller_may(conn->server, call_uid)) { + __dbg("denied %s, caller uid %d is not privileged", + m->member, (int)call_uid); + return __send_error(conn, m, + "org.freedesktop.DBus.Error.AccessDenied", + "Caller is not privileged for this method"); + } + } + + memset(&call, 0, sizeof(call)); + call.conn = conn; + call.uid = call_uid; + call.incoming = *m; + __r_init(&call.read_cursor, m->body, m->body_avail); + + rc = meth->handler(&call, e->userdata); + if (rc < 0 && !call.reply_consumed && !call.error_sent) { + /* Handler returned an error without sending one. */ + __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Handler failed"); + return 0; + } + + if (!call.reply_consumed && !call.error_sent) { + /* Handler returned 0 but never produced a reply; treat as + * empty reply with out_sig "". */ + __send_method_return(conn, m, NULL, NULL, 0); + return 0; + } + + if (call.reply_consumed && !call.error_sent) { + blen = __w_finish(&call.reply_writer); + if (blen < 0) + return __send_error(conn, m, + "org.freedesktop.DBus.Error.Failed", + "Reply marshalling overflow"); + return __send_method_return(conn, m, meth->out_sig, + conn->txbuf, (size_t)blen); + } + + return 0; +} diff --git a/libink/internal.h b/libink/internal.h new file mode 100644 index 00000000..e71fcf00 --- /dev/null +++ b/libink/internal.h @@ -0,0 +1,220 @@ +/* libink internal types — not for external consumers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_INTERNAL_H_ +#define LIBINK_INTERNAL_H_ + +#include +#include + +#include "link.h" +#include "marshal.h" +#include "proto.h" + +typedef enum { + LINK_AUTH_NUL = 0, + LINK_AUTH_LINE, + LINK_AUTH_DONE, + LINK_AUTH_FAILED, +} link_auth_state_t; + +#define LINK_PATH_MAX 108 +#define LINK_AUTH_LINEBUF_SIZE 256 +#define LINK_RX_BUF_SIZE (64 * 1024) +#define LINK_TX_BUF_SIZE (16 * 1024) +#define LINK_UNIQUE_NAME_LEN LINK_SENDER_MAX +#define LINK_MATCH_RULE_MAX 256 /* per-peer match rule cap */ +#define LINK_MATCH_PEER_CAP 16 /* max active match rules per peer */ +#define LINK_PENDING_CAP 4 /* outbound calls awaiting a reply */ +/* Staging for an outgoing method call. Generous on purpose: headers + * for the calls libink makes run to ~150 B, and both the synchronous + * and the connection-side path build into these, so one answer rather + * than a number per call site. */ +#define LINK_CALL_HDR_MAX 1024 +#define LINK_CALL_BODY_MAX 1024 +#define LINK_PARKED_CAP 4 /* inbound calls awaiting a uid */ +/* A call parked for authorization is a privileged one: an object path + * and at most a service name. Finit's per-service paths alone run to + * 512 bytes, so leave room for the header around one. Anything that + * does not fit is denied rather than held. */ +#define LINK_PARKED_MSG_MAX 1024 + +/* Per-vtable record attached to an object's interface list. */ +struct link_vtable_entry { + const link_vtable_t *vt; + void *userdata; + TAILQ_ENTRY(link_vtable_entry) link; +}; + +TAILQ_HEAD(link_vtable_list, link_vtable_entry); + +/* An object exposed at one path. */ +struct link_object { + char path[LINK_PATH_MAX]; + struct link_vtable_list vtables; + TAILQ_ENTRY(link_object) link; +}; + +TAILQ_HEAD(link_object_list, link_object); + +/* An inbound method call held while we find out who sent it. The + * message is copied because rxbuf is reused as soon as we return to + * the read loop. `tok` is the handle the resolver answers with, and + * zero when the slot is free. `stamp` is when it was parked, for + * link_connection_expire(). */ +struct link_parked { + link_authz_t tok; + link_connection_t *conn; + uint64_t stamp; + size_t len; + uint8_t buf[LINK_PARKED_MSG_MAX]; +}; + +/* Calls in flight in either direction: inbound ones held while we ask + * who sent them, outbound ones waiting for their reply. Both belong + * to a conversation with a broker, so this hangs off the connection + * and is allocated on first use. An ordinary peer, which only ever + * calls in and is identified by SO_PEERCRED, never gets one. + * + * Tokens are handed out per bus, which is all link_uid_resolved() + * needs: it is told the connection the answer belongs to. */ +struct link_bus { + struct link_parked parked[LINK_PARKED_CAP]; + link_authz_t next_tok; + + /* Outbound calls we made on this connection, awaiting replies. */ + struct { + int used; + uint32_t serial; + uint64_t stamp; /* for link_connection_expire() */ + link_reply_cb_t cb; + void *userdata; + } pending[LINK_PENDING_CAP]; +}; + +struct link_server { + int fd; + char path[LINK_PATH_MAX]; + struct link_object_list objects; + uint32_t next_unique_id; /* for ":1.N" names */ + + /* Set by link_server_set_uid_resolver(); see link.h. */ + link_uid_resolver_t uid_resolver; + void *uid_userdata; + + /* Set by link_server_set_authorizer(); see link.h. */ + link_authorizer_t authorizer; + void *authz_userdata; +}; + +/* The reply being assembled inside a method handler. + * + * The reply body lives in conn->txbuf, not on this struct, so a + * stack-allocated link_call (in dispatch) stays small. Sharing the + * connection's txbuf is safe because a reply is marshalled and sent + * without yielding. Note that parking means several calls can be in + * flight on one connection: what is held is the request, and + * link_uid_resolved() resumes from a copy, so txbuf is still only + * ever used by one reply at a time. An async handler that returned + * before writing its reply would break that. */ +struct link_call { + link_connection_t *conn; + struct link_msg incoming; + struct link_reader read_cursor; + struct link_writer reply_writer; /* writes into conn->txbuf */ + int reply_consumed; + int error_sent; + uid_t uid; /* caller, resolved for a broker peer */ +}; + +/* A parsed AddMatch rule. Fields are NULL when the rule omits the + * key, meaning "match anything"; non-NULL means "must equal". */ +struct link_match { + char *raw; /* original string, for RemoveMatch */ + char *type; /* "signal", or NULL */ + char *interface; + char *member; + char *path; +}; + +struct link_connection { + int fd; + uid_t peer_uid; + + char guid[33]; + char unique_name[LINK_UNIQUE_NAME_LEN]; /* ":1.N" */ + + link_auth_state_t auth; + char linebuf[LINK_AUTH_LINEBUF_SIZE]; + size_t linelen; + + /* Match rules registered via org.freedesktop.DBus.AddMatch. + * Bounded for PID 1 hygiene; a peer that exceeds the cap gets + * a LimitsExceeded error reply. A broker never registers any, + * it matches for its own clients, so `broker` bypasses them. */ + struct link_match *matches[LINK_MATCH_PEER_CAP]; + size_t matches_count; + int broker; + + uint8_t rxbuf[LINK_RX_BUF_SIZE]; + size_t rxlen; + + /* Scratch for outgoing reply bodies. Shared by the dispatch + * path (writes through call.reply_writer) and built-in handlers + * (send_string_reply). Lifetime ends with each send_method_* + * call. */ + uint8_t txbuf[LINK_TX_BUF_SIZE]; + + uint32_t next_serial; + + /* Allocated on the first park or outbound call, see above. */ + struct link_bus *bus; + + struct link_server *server; /* back-pointer for dispatch */ +}; + +/* log.c — tracing, no-op unless the embedder installed a callback. */ +void __log(const char *func, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +#define __dbg(fmt, ...) __log(__func__, fmt, ##__VA_ARGS__) + +/* io.c — shared EINTR-resilient I/O loops, and the clock the expiry + * sweeps measure against. */ +int __io_write_all(int fd, const void *buf, size_t len); +int __io_read_full(int fd, void *buf, size_t len); +uint64_t __now_ms(void); + +/* auth.c */ +int __auth_process(link_connection_t *conn); +void __auth_generate_guid(char out[33]); +int __auth_client(int fd, uid_t uid); + +/* connection.c — the per-connection bus state, made on demand. */ +struct link_bus *__bus_get (link_connection_t *conn); +void __bus_free(link_connection_t *conn); + +/* dispatch.c */ +int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen); +void __dispatch_forget_conn(link_connection_t *conn); +int __dispatch_expire_parked(link_connection_t *conn, unsigned int age_ms); +int __send_error(link_connection_t *conn, const struct link_msg *req, + const char *error_name, const char *text); +int __send_method_return(link_connection_t *conn, const struct link_msg *req, + const char *out_sig, + const uint8_t *body, size_t body_len); + +/* builtin.c */ +int __handle_builtin(link_connection_t *conn, const struct link_msg *m); + +/* match.c */ +struct link_match *__match_parse (const char *rule); +void __match_free (struct link_match *m); +int __match_matches(const struct link_match *m, + const char *path, const char *iface, + const char *member); +int __match_add (link_connection_t *conn, const char *rule); +int __match_remove (link_connection_t *conn, const char *rule); + +#endif /* LIBINK_INTERNAL_H_ */ diff --git a/libink/io.c b/libink/io.c new file mode 100644 index 00000000..2d96045e --- /dev/null +++ b/libink/io.c @@ -0,0 +1,68 @@ +/* libink — shared I/O helpers. + * + * Server send paths (libink/dispatch.c, libink/auth.c) and the + * client (libink/client.c) all need EINTR-resilient write_all / + * read_full. On any other error they return -1 with an unknown + * number of bytes already transferred. + * + * Also the clock the expiry sweeps measure against: monotonic, so a + * step in wall time cannot make a call look older or younger than it + * is. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include "internal.h" + +uint64_t __now_ms(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + return 0; + + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)(ts.tv_nsec / 1000000); +} + +int __io_write_all(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; + len -= (size_t)n; + } + return 0; +} + +int __io_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; +} diff --git a/libink/link.h b/libink/link.h new file mode 100644 index 00000000..40f6f4b9 --- /dev/null +++ b/libink/link.h @@ -0,0 +1,464 @@ +/* libink — brokerless D-Bus server library, born inside Finit + * + * Copyright (c) 2026 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef LIBINK_LINK_H_ +#define LIBINK_LINK_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct link_server link_server_t; +typedef struct link_connection link_connection_t; +typedef struct link_call link_call_t; +typedef struct link_client link_client_t; + +/* D-Bus message type codes -- see link_reply_t.type. */ +#define LINK_MSG_INVALID 0 +#define LINK_MSG_METHOD_CALL 1 +#define LINK_MSG_METHOD_RETURN 2 +#define LINK_MSG_ERROR 3 +#define LINK_MSG_SIGNAL 4 + +/* Writer is exposed so callers can stack-allocate one for marshalling + * signal/reply bodies. Treat the fields as opaque; use link_writer_init + * + the link_w_* helpers + link_writer_finish. Sized for typical D-Bus + * messages -- the array stack supports up to 8 levels of nesting. */ +#define LINK_WRITER_MAX_NESTING 8 +typedef struct link_writer { + uint8_t *buf; + size_t cap; + size_t off; + int err; + struct { + size_t lenpos; + size_t elemstart; + } arrays[LINK_WRITER_MAX_NESTING]; + size_t array_depth; +} link_writer_t; + +/* Reader is exposed so callers can stack-allocate one for decoding + * reply or signal bodies received from a peer. Treat fields as + * opaque; use link_reader_init + the link_r_* helpers. */ +typedef struct link_reader { + const uint8_t *base; + size_t off; + size_t cap; + int err; /* sticky */ +} link_reader_t; + +/* View of an inbound message (method-return, error, or signal), + * populated by link_client_call(_v) and link_client_wait(), and + * returned by link_client_reply(). All pointers reference internal + * client storage and are invalidated by the next call or wait on the + * same client, or by link_client_close(). `body` is NULL iff + * body_len==0; `error_name` is non-NULL only when type == LINK_MSG_ERROR; + * `path`/`interface`/`member` are non-NULL on signals. */ +typedef struct { + uint8_t type; /* LINK_MSG_METHOD_RETURN, _ERROR, or _SIGNAL */ + const char *signature; + const char *error_name; + const char *path; + const char *interface; + const char *member; + const uint8_t *body; + size_t body_len; +} link_reply_t; + +/* ---------- debug tracing ---------- */ + +/* Receives one line per connection, method call, and authorization + * decision. `func` is the libink function that emitted it, so a host + * can format it the way it formats its own traces. + * + * With no logger installed, the default, a trace point costs one NULL + * test. With one installed the message is formatted before the host + * sees it, so a host that only wants tracing sometimes should install + * and remove the callback rather than discard by level. */ +typedef void (*link_log_cb_t)(void *userdata, const char *func, const char *msg); + +void link_set_logger(link_log_cb_t cb, void *userdata); + +/* ---------- caller identity on a broker connection ---------- */ + +/* Handle for a call parked while its caller is identified. Opaque, + * and safe to hold: it encodes a slot and a generation, so resolving + * a stale handle is a no-op rather than a use-after-free. */ +typedef uint64_t link_authz_t; + +/* "Nobody asked yet", distinct from any real uid. link_call_uid() + * returns this on a broker connection until something forces the + * question, which today only a LINK_METHOD_PRIVILEGED method does. */ +#define LINK_UID_UNKNOWN ((uid_t)-1) + +/* A broker sets SENDER to a unique name, ":1.", so this is very + * generous. Callers that key anything on a sender must reject longer + * names rather than truncate: two senders sharing a truncated key + * would share an identity. */ +#define LINK_SENDER_MAX 64 + +/* Answer "which uid is `sender`?" for a privileged call arriving on a + * broker connection, where SO_PEERCRED describes the broker and not + * the caller. + * + * Return 0 with *uid set when the answer is already known, 1 to answer + * later by calling link_uid_resolved() with `tok`, or -1 when it + * cannot be determined, which fails the call closed. Returning 1 + * without ever calling link_uid_resolved() leaks the slot and leaves + * the caller without a reply, so always answer. */ +typedef int (*link_uid_resolver_t)(link_connection_t *conn, const char *sender, + link_authz_t tok, uid_t *uid, void *userdata); + +void link_server_set_uid_resolver(link_server_t *server, link_uid_resolver_t cb, + void *userdata); + +/* May `uid` invoke a LINK_METHOD_PRIVILEGED method? Return non-zero + * to allow. Who counts as privileged is the embedder's policy, not + * the library's; with no authorizer installed only uid 0 may. */ +typedef int (*link_authorizer_t)(uid_t uid, void *userdata); + +void link_server_set_authorizer(link_server_t *server, link_authorizer_t cb, + void *userdata); + +/* Complete a deferred resolve and resume the parked call. Pass + * (uid_t)-1 to say the caller could not be identified, which denies + * it. Resolving a handle twice, or one whose connection has since + * closed, does nothing. The connection is the one the resolver was + * asked about; a reply callback is handed it as its first argument. */ +void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid); + +/* Called with the reply to an outbound link_connection_call(). `reply` + * is NULL if the connection dropped before one arrived. */ +typedef void (*link_reply_cb_t)(link_connection_t *conn, const link_reply_t *reply, + void *userdata); + +/* Issue a method call on an established connection and invoke `cb` + * when the reply lands. Unlike link_client_call() this never blocks: + * the reply is picked up by the normal read loop. Argument marshalling + * matches link_client_call_v(). */ +int link_connection_call(link_connection_t *conn, const char *destination, + const char *path, const char *interface, const char *member, + link_reply_cb_t cb, void *userdata, + const char *signature, ...); + +/* Nothing in libink runs a clock, it has no event loop, so calls that + * go unanswered in either direction are the embedder's to time out. + * This drops anything held longer than `age_ms` on one connection: a + * park whose resolver never answered, which leaves its caller + * org.freedesktop.DBus.Error.TimedOut, and a call whose reply never + * came, whose callback runs once with a NULL reply exactly as a + * dropped connection would. + * + * Returns how many are still outstanding, so a sweep can stop + * rearming once nothing is left. */ +int link_connection_expire(link_connection_t *conn, unsigned int age_ms); + +/* ---------- server / connection lifecycle ---------- */ + +/* Bind a listening socket at `path` with file mode `mode`, e.g. 0660 + * to keep it to root and one group. The mode is applied at bind(), + * so the socket is never briefly more permissive than asked; setting + * the owning group afterwards is the caller's job. */ +int link_server_new (link_server_t **server, const char *path, mode_t mode); +void link_server_free (link_server_t *server); +int link_server_get_fd(const link_server_t *server); + +int link_server_accept(link_server_t *server, link_connection_t **conn); + +/* Insert an externally-authenticated fd into the server's connection + * set. Used to integrate an outbound peer (e.g. a client-side + * handshake against an external dbus-daemon) so the same dispatch + + * signal-fan-out machinery covers it. `peer_uid` becomes what + * privileged-method checks see; pass (uid_t)-1 to make all + * LINK_METHOD_PRIVILEGED methods reject by default. + * + * On success the connection takes ownership of `fd`. On any failure + * `fd` is closed before the function returns NULL, so callers never + * have to track partial state. + * + * LINK_ATTACH_BROKER says the peer is a message bus rather than an + * ordinary client. A broker subscribes on behalf of its own clients + * and never sends us AddMatch, so signals go to it unconditionally + * instead of being filtered by this connection's match rules. */ +#define LINK_ATTACH_BROKER 0x01 + +link_connection_t *link_server_attach(link_server_t *server, int fd, uid_t peer_uid, + unsigned int attach_flags); + +int link_connection_get_fd (const link_connection_t *conn); +uid_t link_connection_get_uid (const link_connection_t *conn); +int link_connection_process (link_connection_t *conn); +void link_connection_close (link_connection_t *conn); + +/* ---------- object registration ---------- */ + +typedef int (*link_method_fn)(link_call_t *call, void *userdata); + +/* Method flags for link_method_t.flags */ +#define LINK_METHOD_PRIVILEGED (1u << 0) /* peer must be uid 0 (root) */ + +typedef struct { + const char *name; /* member name */ + const char *in_sig; /* input signature (D-Bus, e.g. "" or "s") */ + const char *out_sig; /* output signature */ + unsigned flags; /* OR of LINK_METHOD_* */ + link_method_fn handler; +} link_method_t; + +/* A read-only property descriptor. Set via the Properties.Set side + * is not yet implemented; only Get and GetAll are. The framework + * emits the variant signature from `sig`; the getter writes only the + * bare value into the provided writer (link_w_string for "s", + * link_w_u32 for "u", ...), so the declared type is the single + * source of truth. */ +typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); + +typedef struct { + const char *name; /* property name */ + const char *sig; /* D-Bus signature, e.g. "s" */ + link_property_getter_fn getter; +} link_property_t; + +typedef struct { + const char *interface; /* e.g. "org.finit.Manager1" */ + const link_method_t *methods; /* terminated by {NULL, ...}, or NULL */ + const link_property_t *properties; /* terminated by {NULL, ...}, or NULL */ +} link_vtable_t; + +/* Register one (interface, methods) at `path`. Calling repeatedly + * with the same path and different vtables adds more interfaces at + * that object. The vtable pointer must outlive the server (typically + * a static table). */ +int link_server_add_object(link_server_t *server, const char *path, + const link_vtable_t *vt, void *userdata); + +/* Remove every vtable registered at `path` and free the object. + * Returns 0 if the object existed, -1 (errno=ENOENT) otherwise. */ +int link_server_remove_object(link_server_t *server, const char *path); + +/* ---------- call accessors ---------- */ + +const char *link_call_path (const link_call_t *call); +const char *link_call_interface(const link_call_t *call); +const char *link_call_member (const link_call_t *call); +uid_t link_call_uid (const link_call_t *call); + +/* ---------- reading method-call arguments ---------- + * + * Cursor starts at the beginning of the request body. Each + * function returns 0 on success and advances the cursor; on + * failure it returns -1 and leaves the cursor in an error state + * (subsequent reads also fail). Strings reference the + * connection's rx buffer and are valid for the duration of the + * method handler. */ + +int link_call_read_byte (link_call_t *call, uint8_t *out); +int link_call_read_bool (link_call_t *call, int *out); +int link_call_read_u32 (link_call_t *call, uint32_t *out); +int link_call_read_string(link_call_t *call, const char **out); /* "s" */ +int link_call_read_path (link_call_t *call, const char **out); /* "o" */ + +/* ---------- reply construction ---------- */ + +/* Get the writer for the reply body, write args into it, return 0 + * from the handler. Dispatch finalizes and sends the reply with + * the out_sig declared on the vtable. May be called once per + * call. */ +link_writer_t *link_call_reply(link_call_t *call); + +/* Send a D-Bus error reply. `name` must be a valid D-Bus error + * name (e.g. "org.freedesktop.DBus.Error.UnknownMethod"); `message` + * may be NULL. */ +int link_call_reply_error(link_call_t *call, const char *name, const char *message); + +/* ---------- signal emission ---------- + * + * Send a signal to a single peer if its AddMatch rules accept it. + * Callers marshal the body separately and pass the resulting bytes. + * Returns 0 on success (or "filtered out, nothing sent"), -1 with + * errno set on failure: EMSGSIZE and EINVAL mean nothing hit the + * wire and the connection is still usable; anything else is a + * transport failure that may have left a partial frame -- the + * caller must drop the peer. */ +int link_connection_emit_signal(link_connection_t *conn, + const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len); + +/* ---------- client (outgoing method calls) ---------- + * + * Connect, authenticate as the current effective uid, send BEGIN. + * Returns NULL on any failure (caller can fall back to another + * transport if it has one). */ +link_client_t *link_client_open(const char *path); + +/* As link_client_open but applies SO_SNDTIMEO + SO_RCVTIMEO before + * the connect/AUTH handshake. After link_server_attach flips the fd + * to non-blocking the timeout is silently inert; it only protects + * the synchronous open path against a hung peer. timeout_ms == 0 + * disables the budget (same behaviour as link_client_open). */ +link_client_t *link_client_open_timeout(const char *path, int timeout_ms); + +void link_client_close(link_client_t *c); + +/* Address subsequent calls on `c` to a well-known name. Needed when + * a broker routes the message, e.g. "org.freedesktop.DBus" to reach + * the bus driver itself; a brokerless link has a single peer and + * needs no destination, which is the default. `destination` is not + * copied, so it must outlive the client. */ +void link_client_set_destination(link_client_t *c, const char *destination); + +/* Detach the authenticated socket from the client and return the raw + * fd; subsequent link_client_close on `c` is invalid because the + * structure has already been freed. Used by callers (e.g. system-bus + * integration) that want to promote an outbound client connection + * into a server-attached peer via link_server_attach(). */ +int link_client_steal_fd(link_client_t *c); + +/* Status codes returned by link_client_call(_v). */ +#define LINK_CALL_OK 0 /* method-return received */ +#define LINK_CALL_ERROR 1 /* server replied with an error */ +#define LINK_CALL_FAIL (-1) /* transport, parse, or invalid-arg failure */ + +/* Send a METHOD_CALL and read the reply synchronously. + * + * `signature` and `body`/`body_len` describe the outgoing body -- + * marshal it yourself with link_writer_init + the link_w_* helpers + * + link_writer_finish. Pass signature=NULL and body=NULL for + * methods that take no arguments. + * + * After the call, inspect the reply via link_client_reply() -- it + * exposes the body bytes (for callers that want to decode them with + * link_reader_init + link_r_*) and the error name on LINK_CALL_ERROR. + * The reply view is invalidated by the next call on the same client + * or by link_client_close(). */ +int link_client_call(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len); + +/* Convenience wrapper that marshals the outgoing body from varargs + * matching `signature`. Supported type codes (one per arg): + * 'y' -> int (promoted uint8_t) + * 'b' -> int (0/non-zero) + * 'u' -> uint32_t + * 's' -> const char * + * 'o' -> const char * (object path) + * + * Pass signature=NULL or "" for void calls. Return value matches + * link_client_call; an unsupported type code returns LINK_CALL_FAIL + * with no message sent. */ +int link_client_call_v(link_client_t *c, + const char *obj_path, + const char *interface, + const char *member, + const char *signature, ...); + +const link_reply_t *link_client_reply(link_client_t *c); + +/* Convenience accessors for the common case where a reply carries + * exactly one string ("s" or "o") or one u32 ("u"). They wrap the + * link_reader_init + link_r_* pattern; on success return 0 and + * populate *out, on parse failure or missing body return -1. Use + * link_client_reply + link_reader_init directly for richer payloads. */ +int link_reply_get_string(const link_reply_t *r, const char **out); +int link_reply_get_u32 (const link_reply_t *r, uint32_t *out); + +/* Wait up to `timeout_ms` milliseconds for the next inbound message + * (typically a SIGNAL delivered after an AddMatch subscription), and + * populate the same view returned by link_client_reply(). + * timeout_ms < 0 : block forever + * timeout_ms == 0 : non-blocking (returns 1 immediately if no data) + * timeout_ms > 0 : wait that long + * Returns 0 on success, 1 on timeout, -1 on transport/parse error. + * + * Note: the timeout gates only the wait for the first byte of the + * next frame. Once data starts arriving the rest of the message is + * read blockingly; callers that need a hard upper bound should pass + * a positive timeout AND have a watchdog at a higher level. */ +int link_client_wait(link_client_t *c, int timeout_ms); + +/* ---------- standalone writer ---------- + * + * For marshalling bodies outside a method-call handler (signals, + * pre-computed replies). Initialise on a caller-owned buffer, + * write args via link_w_*, then call link_writer_finish which + * returns the body length or -1 on overflow. */ +void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap); +ssize_t link_writer_finish(link_writer_t *w); + +/* ---------- writer (mirrors the internal marshaller) ---------- */ + +void link_w_byte (link_writer_t *w, uint8_t v); +void link_w_bool (link_writer_t *w, int v); +void link_w_u32 (link_writer_t *w, uint32_t v); +void link_w_string (link_writer_t *w, const char *s); /* "s" */ +void link_w_path (link_writer_t *w, const char *s); /* "o" */ +void link_w_variant_string(link_writer_t *w, const char *s); /* "v" containing "s" */ +void link_w_array_begin (link_writer_t *w, char element_sig); +void link_w_array_end (link_writer_t *w); +void link_w_struct_begin(link_writer_t *w); +void link_w_struct_end (link_writer_t *w); + +/* ---------- standalone reader ---------- + * + * For decoding bodies received off the wire (reply or signal). + * Initialise on the body pointer + length, read via link_r_*, + * check link_r_done() to confirm everything was consumed. */ +void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len); +int link_r_byte (link_reader_t *r, uint8_t *out); +int link_r_bool (link_reader_t *r, int *out); +int link_r_u32 (link_reader_t *r, uint32_t *out); +int link_r_string (link_reader_t *r, const char **out); /* "s" */ +int link_r_path (link_reader_t *r, const char **out); /* "o" */ +int link_r_variant_begin (link_reader_t *r, char *type); /* sig header, cursor at value */ +int link_r_skip_basic (link_reader_t *r, char type); /* skip one basic value */ +int link_r_variant_string(link_reader_t *r, const char **out); /* "v" containing "s" */ +int link_r_align (link_reader_t *r, size_t n); /* skip to next n-byte boundary */ +int link_r_done (const link_reader_t *r); + +/* Begin reading an "a" array. On success returns 0 and sets + * *out_end to the absolute reader offset at which the array ends; + * caller loops while link_r_pos < *out_end. For dict-entry arrays + * ("a{T}") call link_r_align(r, 8) at the top of each iteration -- + * the element-alignment skip from the array prefix only covers the + * first entry. */ +int link_r_array_begin(link_reader_t *r, size_t *out_end); + +/* Byte offset of the next read inside the original body buffer. + * Use together with the *out_end returned by link_r_array_begin to + * walk the elements of an "a" payload. */ +size_t link_r_pos (const link_reader_t *r); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBINK_LINK_H_ */ diff --git a/libink/log.c b/libink/log.c new file mode 100644 index 00000000..226e263c --- /dev/null +++ b/libink/log.c @@ -0,0 +1,40 @@ +/* libink — optional debug tracing, routed to whoever embeds us. + * + * libink has no logger of its own by design: it must not depend on the + * host's logging, and PID 1 already has one. The embedder installs a + * callback and gets a line per connection, call, and authorization + * decision. Formatting happens before the callback, so an embedder + * that wants tracing off should uninstall rather than filter. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "internal.h" + +static link_log_cb_t logger; +static void *logger_userdata; + +void link_set_logger(link_log_cb_t cb, void *userdata) +{ + logger = cb; + logger_userdata = userdata; +} + +void __log(const char *func, const char *fmt, ...) +{ + char msg[256]; + va_list ap; + + if (!logger) + return; + + va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + + logger(logger_userdata, func, msg); +} diff --git a/libink/marshal.c b/libink/marshal.c new file mode 100644 index 00000000..ecc41166 --- /dev/null +++ b/libink/marshal.c @@ -0,0 +1,409 @@ +/* libink — D-Bus body marshalling (writer side). + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include "marshal.h" + +#define ALIGN_UP(x, n) (((x) + (n) - 1) & ~((size_t)((n) - 1))) + +void __w_init(struct link_writer *w, uint8_t *buf, size_t cap) +{ + w->buf = buf; + w->cap = cap; + w->off = 0; + w->err = 0; + w->array_depth = 0; +} + +ssize_t __w_finish(struct link_writer *w) +{ + if (w->err || w->array_depth != 0) + return -1; + return (ssize_t)w->off; +} + +static int reserve(struct link_writer *w, size_t align, size_t bytes) +{ + size_t pad; + + if (w->err) + return -1; + + pad = ALIGN_UP(w->off, align) - w->off; + if (w->off + pad + bytes > w->cap) { + w->err = 1; + return -1; + } + while (pad-- > 0) + w->buf[w->off++] = 0; + return 0; +} + +static void put_u32_at(struct link_writer *w, size_t pos, uint32_t v) +{ + w->buf[pos] = (uint8_t)(v & 0xff); + w->buf[pos + 1] = (uint8_t)((v >> 8) & 0xff); + w->buf[pos + 2] = (uint8_t)((v >> 16) & 0xff); + w->buf[pos + 3] = (uint8_t)((v >> 24) & 0xff); +} + +static void put_u32(struct link_writer *w, uint32_t v) +{ + put_u32_at(w, w->off, v); + w->off += 4; +} + +void __w_byte(struct link_writer *w, uint8_t v) +{ + if (reserve(w, 1, 1) < 0) + return; + w->buf[w->off++] = v; +} + +void __w_bool(struct link_writer *w, int v) +{ + if (reserve(w, 4, 4) < 0) + return; + put_u32(w, v ? 1u : 0u); +} + +void __w_u32(struct link_writer *w, uint32_t v) +{ + if (reserve(w, 4, 4) < 0) + return; + put_u32(w, v); +} + +static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_len) +{ + size_t len = s ? strlen(s) : 0; + + if (onebyte_len) { + if (reserve(w, 1, 1 + len + 1) < 0) + return; + w->buf[w->off++] = (uint8_t)len; + } else { + if (reserve(w, 4, 4 + len + 1) < 0) + return; + put_u32(w, (uint32_t)len); + } + if (s && len) + memcpy(w->buf + w->off, s, len); + w->off += len; + w->buf[w->off++] = 0; +} + +void __w_string(struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void __w_path (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 0); } +void __w_sig (struct link_writer *w, const char *s) { write_lenprefixed(w, s, 1); } + +/* Variant "v" containing a string. Wire form: + * 1-byte sig length (1), 's', NUL, then the string per __w_string. */ +void __w_variant_string(struct link_writer *w, const char *s) +{ + __w_sig (w, "s"); + __w_string(w, s); +} + + +static size_t element_align(char c) +{ + switch (c) { + case 'y': case 'g': case 'v': return 1; + case 'n': case 'q': return 2; + case 'b': case 'i': case 'u': + case 's': case 'o': case 'h': case 'a': return 4; + case 'x': case 't': case 'd': + case '(': case '{': return 8; + default: return 1; + } +} + +void __w_array_begin(struct link_writer *w, char element_sig_first_char) +{ + size_t lenpos; + + if (w->err) + return; + if (w->array_depth >= LINK_WRITER_MAX_NESTING) { + w->err = 1; + return; + } + + if (reserve(w, 4, 4) < 0) + return; + lenpos = w->off; + put_u32(w, 0); /* placeholder */ + + /* Pad to the element's alignment. These pad bytes are NOT + * counted in the array length per the D-Bus spec. */ + if (reserve(w, element_align(element_sig_first_char), 0) < 0) + return; + + w->arrays[w->array_depth].lenpos = lenpos; + w->arrays[w->array_depth].elemstart = w->off; + w->array_depth++; +} + +void __w_array_end(struct link_writer *w) +{ + size_t elemstart, lenpos; + uint32_t actual; + + if (w->err || w->array_depth == 0) { + w->err = 1; + return; + } + w->array_depth--; + lenpos = w->arrays[w->array_depth].lenpos; + elemstart = w->arrays[w->array_depth].elemstart; + actual = (uint32_t)(w->off - elemstart); + put_u32_at(w, lenpos, actual); +} + +void __w_struct_begin(struct link_writer *w) +{ + reserve(w, 8, 0); +} + +void __w_struct_end(struct link_writer *w) +{ + (void)w; +} + +/* ---- reader ---- */ + +void __r_init(struct link_reader *r, const uint8_t *body, size_t len) +{ + r->base = body; + r->off = 0; + r->cap = len; + r->err = 0; +} + +static int r_skip_align(struct link_reader *r, size_t align) +{ + size_t pad; + + if (r->err) + return -1; + pad = ALIGN_UP(r->off, align) - r->off; + if (r->off + pad > r->cap) { + r->err = 1; + return -1; + } + r->off += pad; + return 0; +} + +static 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); +} + +int __r_byte(struct link_reader *r, uint8_t *out) +{ + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) { + r->err = 1; + return -1; + } + *out = r->base[r->off++]; + return 0; +} + +int __r_u32(struct link_reader *r, uint32_t *out) +{ + if (r_skip_align(r, 4) < 0 || r->off + 4 > r->cap) { + r->err = 1; + return -1; + } + *out = rd_u32(r->base + r->off); + r->off += 4; + return 0; +} + +int __r_bool(struct link_reader *r, int *out) +{ + uint32_t v; + + if (__r_u32(r, &v) < 0) + return -1; + *out = v ? 1 : 0; + return 0; +} + +static int read_string_like(struct link_reader *r, const char **out) +{ + uint32_t len; + + if (__r_u32(r, &len) < 0) + return -1; + if (r->off + (size_t)len + 1 > r->cap) { + r->err = 1; + return -1; + } + /* Spec requires nul terminator at base[off + len]. */ + if (r->base[r->off + len] != 0) { + r->err = 1; + return -1; + } + *out = (const char *)(r->base + r->off); + r->off += (size_t)len + 1; + return 0; +} + +int __r_string(struct link_reader *r, const char **out) { return read_string_like(r, out); } +int __r_path (struct link_reader *r, const char **out) { return read_string_like(r, out); } + +/* + * Parse a variant's signature header, i.e. "g" wire form: 1-byte + * length, bytes, NUL. Only single-character inner signatures are + * supported. On success the cursor sits at the value and the type + * code is returned in *type. + */ +int __r_variant_begin(struct link_reader *r, char *type) +{ + uint8_t sig_len; + + if (r_skip_align(r, 1) < 0 || r->off + 1 > r->cap) + goto fail; + sig_len = r->base[r->off++]; + if (sig_len != 1 || r->off + 2 > r->cap) + goto fail; + if (r->base[r->off + 1] != 0) + goto fail; + *type = (char)r->base[r->off]; + r->off += 2; + return 0; +fail: + r->err = 1; + return -1; +} + +/* Read a variant "v" expected to contain a string. Fails if the + * inner signature is anything other than "s" (returns -1, *out set + * to NULL). */ +int __r_variant_string(struct link_reader *r, const char **out) +{ + char type; + + *out = NULL; + if (__r_variant_begin(r, &type) < 0) + return -1; + if (type != 's') { + r->err = 1; + return -1; + } + return read_string_like(r, out); +} + +/* + * Skip one basic value of the given type code, as returned by + * __r_variant_begin(). Lets a{sv} consumers tolerate value types + * they don't know. Returns -1 on non-basic types. + */ +int __r_skip_basic(struct link_reader *r, char type) +{ + const char *s; + uint32_t u; + uint8_t y; + + switch (type) { + case 's': + case 'o': + return read_string_like(r, &s); + case 'b': + case 'u': + case 'i': + return __r_u32(r, &u); + case 'y': + return __r_byte(r, &y); + default: + r->err = 1; + return -1; + } +} + +/* Read a variant "v" expected to contain a uint32. */ +int __r_variant_u32(struct link_reader *r, uint32_t *out) +{ + char type; + + if (__r_variant_begin(r, &type) < 0) + return -1; + if (type != 'u') { + r->err = 1; + return -1; + } + + return __r_u32(r, out); +} + +int __r_done(const struct link_reader *r) +{ + return !r->err && r->off == r->cap; +} + +int __r_align(struct link_reader *r, size_t n) +{ + return r_skip_align(r, n); +} + +/* Begin reading an "a" array. Reads the u32 byte-length prefix + * and sets *out_end to the absolute reader offset at which the array + * ends. Caller loops while r->off < *out_end. Returns -1 on a + * truncated or oversized array length. */ +int __r_array_begin(struct link_reader *r, size_t *out_end) +{ + uint32_t array_bytes; + size_t end; + + if (__r_u32(r, &array_bytes) < 0) + return -1; + end = r->off + (size_t)array_bytes; + if (end > r->cap) { + r->err = 1; + return -1; + } + *out_end = end; + return 0; +} + +ssize_t __marshal_va(uint8_t *body, size_t cap, const char *sig, va_list ap) +{ + struct link_writer w; + const char *s; + + __w_init(&w, body, cap); + for (s = sig; *s; s++) { + switch (*s) { + case 'y': + __w_byte(&w, (uint8_t)va_arg(ap, int)); + break; + case 'b': + __w_bool(&w, va_arg(ap, int)); + break; + case 'u': + __w_u32(&w, va_arg(ap, uint32_t)); + break; + case 's': + __w_string(&w, va_arg(ap, const char *)); + break; + case 'o': + __w_path(&w, va_arg(ap, const char *)); + break; + default: + return -1; + } + } + + return __w_finish(&w); +} diff --git a/libink/marshal.h b/libink/marshal.h new file mode 100644 index 00000000..0a210989 --- /dev/null +++ b/libink/marshal.h @@ -0,0 +1,63 @@ +/* libink — D-Bus body marshalling (writer side). + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_MARSHAL_H_ +#define LIBINK_MARSHAL_H_ + +#include +#include +#include +#include + +/* struct link_writer is defined in ink.h (public). Field layout is + * "opaque" per the public contract; this file's helpers manipulate + * the fields directly. */ +#include "link.h" + +void __w_init (struct link_writer *w, uint8_t *buf, size_t cap); +ssize_t __w_finish(struct link_writer *w); + +void __w_byte (struct link_writer *w, uint8_t v); +void __w_bool (struct link_writer *w, int v); +void __w_u32 (struct link_writer *w, uint32_t v); +void __w_string (struct link_writer *w, const char *s); /* "s" */ +void __w_path (struct link_writer *w, const char *s); /* "o" */ +void __w_sig (struct link_writer *w, const char *s); /* "g" */ +void __w_variant_string(struct link_writer *w, const char *s); /* "v" containing "s" */ + +/* element_sig_first_char drives the alignment padding inserted + * between the array length prefix and the first element. */ +void __w_array_begin (struct link_writer *w, char element_sig_first_char); +void __w_array_end (struct link_writer *w); + +void __w_struct_begin(struct link_writer *w); +void __w_struct_end (struct link_writer *w); + +/* ---- reader ---- + * + * Reads from a message body pointer + length, advancing a cursor. + * struct link_reader is defined in link.h (public, opaque); the + * helpers here manipulate the fields directly. */ +void __r_init (struct link_reader *r, const uint8_t *body, size_t len); +int __r_byte (struct link_reader *r, uint8_t *out); +int __r_bool (struct link_reader *r, int *out); +int __r_u32 (struct link_reader *r, uint32_t *out); +int __r_string(struct link_reader *r, const char **out); /* "s" */ +int __r_path (struct link_reader *r, const char **out); /* "o" */ +int __r_variant_begin (struct link_reader *r, char *type); /* sig header, cursor at value */ +int __r_skip_basic (struct link_reader *r, char type); /* skip one basic value */ +int __r_variant_string(struct link_reader *r, const char **out); /* "v" containing "s" */ +int __r_variant_u32 (struct link_reader *r, uint32_t *out); /* "v" containing "u" */ +int __r_align (struct link_reader *r, size_t n); /* skip to n-byte boundary */ +int __r_array_begin(struct link_reader *r, size_t *out_end); +int __r_done (const struct link_reader *r); + +/* Marshal varargs into `body` (capacity `cap`) according to `sig`. + * Returns the marshalled length, or -1 on overflow or an unsupported + * type code. Shared by the synchronous client and the asynchronous + * connection-side call. */ +ssize_t __marshal_va(uint8_t *body, size_t cap, const char *sig, va_list ap); + +#endif /* LIBINK_MARSHAL_H_ */ diff --git a/libink/match.c b/libink/match.c new file mode 100644 index 00000000..eaaa87f5 --- /dev/null +++ b/libink/match.c @@ -0,0 +1,218 @@ +/* libink — D-Bus AddMatch / RemoveMatch rule parsing and matching. + * + * Subset of the spec: type, interface, member, path. Each entry is + * a key='value' pair with single-quoted value, separated by commas. + * Backslash escapes inside values (\\ and \') are not interpreted — + * a peer needing them will get unexpected literal content. Unknown + * keys cause the whole rule to be rejected so a peer learns its + * filter didn't take, rather than silently receiving everything. + * + * The exceptions are sender, destination, and eavesdrop, which are + * accepted and then ignored. Clients send them as a matter of + * course, and refusing the rule leaves such a peer with no signals at + * all, which serves it far worse than a filter wider than it asked + * for. Widening is safe here: Finit is the only sender on this bus, + * and everything it emits through the match table is state any peer + * that reached the bus may already read. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include "internal.h" + +static char *dup_range(const char *p, size_t n) +{ + char *s = malloc(n + 1); + + if (!s) + return NULL; + memcpy(s, p, n); + s[n] = '\0'; + return s; +} + +/* Parse one key='value' entry starting at *p. On success advances + * *p past the trailing quote and any comma, returns 0. On malformed + * input, returns -1. */ +static int parse_kv(const char **p, char **out_key, char **out_value) +{ + const char *q = *p; + const char *key_start, *val_start; + + while (*q == ' ' || *q == '\t') + q++; + key_start = q; + while ((*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z') || *q == '_') + q++; + if (q == key_start || *q != '=') + return -1; + *out_key = dup_range(key_start, (size_t)(q - key_start)); + if (!*out_key) + return -1; + q++; + + if (*q != '\'') { + free(*out_key); + return -1; + } + q++; + val_start = q; + while (*q && *q != '\'') + q++; + if (*q != '\'') { + free(*out_key); + return -1; + } + *out_value = dup_range(val_start, (size_t)(q - val_start)); + if (!*out_value) { + free(*out_key); + return -1; + } + q++; + + while (*q == ' ' || *q == '\t' || *q == ',') + q++; + *p = q; + return 0; +} + +struct link_match *__match_parse(const char *rule) +{ + struct link_match *m; + const char *p; + + if (!rule || strlen(rule) >= LINK_MATCH_RULE_MAX) { + errno = EINVAL; + return NULL; + } + + m = calloc(1, sizeof(*m)); + if (!m) + return NULL; + m->raw = strdup(rule); + if (!m->raw) { + free(m); + return NULL; + } + + for (p = rule; *p; ) { + char *key = NULL, *value = NULL; + char **slot = NULL; + + if (parse_kv(&p, &key, &value) < 0) + goto bad; + + if (!strcmp(key, "type")) slot = &m->type; + else if (!strcmp(key, "interface")) slot = &m->interface; + else if (!strcmp(key, "member")) slot = &m->member; + else if (!strcmp(key, "path")) slot = &m->path; + else if (!strcmp(key, "sender") || + !strcmp(key, "destination") || + !strcmp(key, "eavesdrop")) { + /* Understood well enough to accept, see above. */ + free(key); + free(value); + continue; + } else { + /* XXX: argN and argNpath land here, so a rule + * using them takes nothing rather than too + * much. They narrow on body contents, which + * means parsing the body to honour them. */ + free(key); + free(value); + goto bad; + } + + if (*slot) { + /* Duplicate key. */ + free(key); + free(value); + goto bad; + } + *slot = value; + free(key); + } + + /* No need to default m->type: when it's NULL the matcher below + * treats it as "match any", and the only thing libink emits via + * the match table is signals, so the effective filter is + * already "signal" without the explicit assignment. */ + return m; + +bad: + __match_free(m); + errno = EINVAL; + return NULL; +} + +void __match_free(struct link_match *m) +{ + if (!m) + return; + free(m->raw); + free(m->type); + free(m->interface); + free(m->member); + free(m->path); + free(m); +} + +static int field_matches(const char *want, const char *got) +{ + if (!want) + return 1; /* no filter on this field */ + if (!got) + return 0; + return strcmp(want, got) == 0; +} + +int __match_matches(const struct link_match *m, + const char *path, const char *iface, + const char *member) +{ + /* Type filter: only signals get delivered through this path. */ + if (m->type && strcmp(m->type, "signal") != 0) + return 0; + + return field_matches(m->path, path) + && field_matches(m->interface, iface) + && field_matches(m->member, member); +} + +int __match_add(link_connection_t *conn, const char *rule) +{ + struct link_match *m; + + if (conn->matches_count >= LINK_MATCH_PEER_CAP) { + errno = ENOSPC; + return -1; + } + + m = __match_parse(rule); + if (!m) + return -1; + + conn->matches[conn->matches_count++] = m; + return 0; +} + +int __match_remove(link_connection_t *conn, const char *rule) +{ + size_t i; + + for (i = 0; i < conn->matches_count; i++) { + if (strcmp(conn->matches[i]->raw, rule) == 0) { + __match_free(conn->matches[i]); + conn->matches[i] = conn->matches[conn->matches_count - 1]; + conn->matches_count--; + return 0; + } + } + errno = ENOENT; + return -1; +} diff --git a/libink/path.c b/libink/path.c new file mode 100644 index 00000000..80f97963 --- /dev/null +++ b/libink/path.c @@ -0,0 +1,43 @@ +/* libink — D-Bus object-path encoding for arbitrary identifiers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include + +#include "path.h" + +static int is_safe(unsigned char c) +{ + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9'); +} + +int link_path_encode(const char *in, char *out, size_t outsz) +{ + static const char hex[] = "0123456789abcdef"; + size_t off = 0; + + if (!in || !out || outsz == 0) + return -1; + + for (; *in; in++) { + unsigned char c = (unsigned char)*in; + + if (is_safe(c)) { + if (off + 1 >= outsz) + return -1; + out[off++] = (char)c; + } else { + if (off + 3 >= outsz) + return -1; + out[off++] = '_'; + out[off++] = hex[c >> 4]; + out[off++] = hex[c & 0xf]; + } + } + out[off] = '\0'; + return (int)off; +} diff --git a/libink/path.h b/libink/path.h new file mode 100644 index 00000000..784c634e --- /dev/null +++ b/libink/path.h @@ -0,0 +1,21 @@ +/* libink — D-Bus object-path encoding for arbitrary identifiers. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_PATH_H_ +#define LIBINK_PATH_H_ + +#include + +/* Encode `in` as a D-Bus path segment. Bytes in [A-Za-z0-9] pass + * through unchanged; everything else (including '_' itself) becomes + * "_HH" with lowercase hex, mirroring systemd's escape_path. + * + * Returns the encoded length (excluding the terminating nul), or -1 + * if the output buffer cannot fit the result. `outsz` must + * accommodate the encoded bytes plus a trailing nul; the worst-case + * size for an N-byte input is 3*N + 1. */ +int link_path_encode(const char *in, char *out, size_t outsz); + +#endif /* LIBINK_PATH_H_ */ diff --git a/libink/proto.c b/libink/proto.c new file mode 100644 index 00000000..15068f14 --- /dev/null +++ b/libink/proto.c @@ -0,0 +1,406 @@ +/* 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 + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include "proto.h" + +#define HDR_FIXED_SIZE 16 +#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; +} diff --git a/libink/proto.h b/libink/proto.h new file mode 100644 index 00000000..0ea345b1 --- /dev/null +++ b/libink/proto.h @@ -0,0 +1,108 @@ +/* libink — D-Bus wire protocol: message header parsing and building. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ +#ifndef LIBINK_PROTO_H_ +#define LIBINK_PROTO_H_ + +#include +#include +#include + +#include "link.h" /* LINK_MSG_* type codes */ + +/* Message flags. */ +#define LINK_FLAG_NO_REPLY_EXPECTED 0x01 +#define LINK_FLAG_NO_AUTO_START 0x02 +#define LINK_FLAG_ALLOW_INTERACTIVE_AUTHORIZATION 0x04 + +/* Header field codes. */ +#define LINK_HDR_PATH 1 +#define LINK_HDR_INTERFACE 2 +#define LINK_HDR_MEMBER 3 +#define LINK_HDR_ERROR_NAME 4 +#define LINK_HDR_REPLY_SERIAL 5 +#define LINK_HDR_DESTINATION 6 +#define LINK_HDR_SENDER 7 +#define LINK_HDR_SIGNATURE 8 +#define LINK_HDR_UNIX_FDS 9 + +#define LINK_PROTOCOL_VERSION 1 + +/* Parsed view of an incoming message. Pointers reference bytes + * inside the receiver's own rx buffer; treat as borrowed and short- + * lived (until the next read of the same connection). */ +struct link_msg { + uint8_t type; + uint8_t flags; + uint8_t endian; /* 'l' or 'B' */ + uint32_t body_len; + uint32_t serial; + uint32_t reply_serial; + + const char *path; /* object path, or NULL */ + const char *interface; /* may be NULL on method calls */ + const char *member; + const char *error_name; + const char *destination; + const char *sender; + const char *signature; /* may be NULL if body is empty */ + + /* Pointer into the rx buffer and length, after header padding. */ + const uint8_t *body; + uint32_t body_avail; +}; + +/* Parse a complete D-Bus message from `buf` of size `len`. On + * success returns the total number of bytes consumed (header + + * padding + body) and fills *out. Returns 0 if more bytes are + * needed, -1 on malformed input. */ +ssize_t __msg_parse(const uint8_t *buf, size_t len, struct link_msg *out); + +/* Project a parsed message onto the public reply view. Shared by the + * synchronous client and the connection-side reply routing so the two + * cannot drift as link_reply_t grows. */ +void __msg_to_reply(link_reply_t *r, const struct link_msg *m); + +/* Compute the on-wire size of a future message header given the + * fields we'd populate. Used to size send buffers. */ +size_t __msg_header_size(const struct link_msg *m); + +/* Build a method-return header into `buf` (capacity `cap`). + * `reply_serial`/`destination` come from the call being replied to. + * `signature` is the body signature ("" if no args). `body_len` + * is the length of the body that will follow the header padding. + * Returns the number of bytes written, or -1 on overflow. */ +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); + +/* Build an error reply header. */ +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); + +/* Build a signal header (no reply expected, no destination). */ +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); + +/* Build a method-call header (client side). `destination` is NULL + * when no broker routes the message. */ +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); + +#endif /* LIBINK_PROTO_H_ */ diff --git a/libink/server.c b/libink/server.c new file mode 100644 index 00000000..9c32c5ce --- /dev/null +++ b/libink/server.c @@ -0,0 +1,229 @@ +/* libink — listening socket, accept, peer-credential capture + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +static void close_save_errno(int fd) +{ + int saved = errno; + close(fd); + errno = saved; +} + +int link_server_new(link_server_t **out, const char *path, mode_t mode) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + link_server_t *srv; + size_t plen; + int fd; + + if (!out || !path || !*path) { + errno = EINVAL; + return -1; + } + + plen = strlen(path); + if (plen >= sizeof(sun.sun_path) || plen >= LINK_PATH_MAX) { + errno = ENAMETOOLONG; + return -1; + } + + srv = calloc(1, sizeof(*srv)); + if (!srv) + return -1; + TAILQ_INIT(&srv->objects); + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) + goto err_free; + + memcpy(sun.sun_path, path, plen + 1); + + (void)unlink(path); + + /* fchmod() on a Unix-domain socket fd is a silent no-op on Linux: + * the file mode is fixed at bind() time as (0777 & ~umask). Set + * umask around the bind() so the socket appears with the mode the + * caller asked for atomically, with no window where it is more + * permissive. Who may connect is the caller's policy to set; + * per-method authorization happens later in dispatch via + * SO_PEERCRED. */ + __dbg("binding %s, mode %04o", path, (unsigned)mode); + { + mode_t oldmask = umask(0777 & ~mode); + int rc = bind(fd, (struct sockaddr *)&sun, sizeof(sun)); + int saved = errno; + + umask(oldmask); + if (rc < 0) { + errno = saved; + goto err_close; + } + } + + if (listen(fd, 16) < 0) + goto err_unlink; + + srv->fd = fd; + memcpy(srv->path, path, plen + 1); + *out = srv; + return 0; + +err_unlink: + (void)unlink(path); +err_close: + close_save_errno(fd); +err_free: + free(srv); + return -1; +} + +void link_server_free(link_server_t *srv) +{ + struct link_object *o; + + if (!srv) + return; + + o = TAILQ_FIRST(&srv->objects); + while (o) { + struct link_object *next_o = TAILQ_NEXT(o, link); + struct link_vtable_entry *e = TAILQ_FIRST(&o->vtables); + + while (e) { + struct link_vtable_entry *next_e = TAILQ_NEXT(e, link); + + free(e); + e = next_e; + } + free(o); + o = next_o; + } + + if (srv->fd >= 0) + close(srv->fd); + if (srv->path[0]) + (void)unlink(srv->path); + free(srv); +} + +int link_server_get_fd(const link_server_t *srv) +{ + if (!srv) + return -1; + + return srv->fd; +} + +int link_server_accept(link_server_t *srv, link_connection_t **out) +{ + struct ucred cred = { 0 }; + socklen_t credlen = sizeof(cred); + link_connection_t *conn; + int cfd; + + if (!srv || !out) { + errno = EINVAL; + return -1; + } + + cfd = accept4(srv->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (cfd < 0) + return -1; + + conn = calloc(1, sizeof(*conn)); + if (!conn) { + close_save_errno(cfd); + return -1; + } + + conn->fd = cfd; + conn->auth = LINK_AUTH_NUL; + conn->server = srv; + + if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &credlen) == 0) + conn->peer_uid = cred.uid; + else + conn->peer_uid = (uid_t)-1; + + __auth_generate_guid(conn->guid); + + __dbg("new peer on fd %d, uid %d", cfd, (int)conn->peer_uid); + + *out = conn; + return 0; +} + +void link_server_set_uid_resolver(link_server_t *srv, link_uid_resolver_t cb, void *userdata) +{ + if (!srv) + return; + srv->uid_resolver = cb; + srv->uid_userdata = userdata; +} + +void link_server_set_authorizer(link_server_t *srv, link_authorizer_t cb, void *userdata) +{ + if (!srv) + return; + srv->authorizer = cb; + srv->authz_userdata = userdata; +} + +link_connection_t *link_server_attach(link_server_t *srv, int fd, uid_t peer_uid, + unsigned int attach_flags) +{ + link_connection_t *conn; + int flags; + + /* On entry we always own `fd` -- close it on every failure path + * so callers don't have to track whether we touched fcntl state. */ + if (!srv || fd < 0) { + if (fd >= 0) + close_save_errno(fd); + errno = EINVAL; + return NULL; + } + + /* Match server_accept's fd setup: CLOEXEC first (so a fork-and- + * exec between the two calls cannot leak the fd), then NONBLOCK + * so process_binary's read loop can drain without hanging. */ + flags = fcntl(fd, F_GETFD, 0); + if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + goto err_close; + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + goto err_close; + + conn = calloc(1, sizeof(*conn)); + if (!conn) + goto err_close; + + conn->fd = fd; + conn->auth = LINK_AUTH_DONE; /* caller already handshook */ + conn->server = srv; + conn->peer_uid = peer_uid; + conn->broker = !!(attach_flags & LINK_ATTACH_BROKER); + __auth_generate_guid(conn->guid); + + __dbg("attached %s peer on fd %d, uid %d", + conn->broker ? "broker" : "external", fd, (int)peer_uid); + + return conn; + +err_close: + close_save_errno(fd); + return NULL; +} diff --git a/mkdocs.yml b/mkdocs.yml index 2ccce539..674a554a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - Plugins: plugins.md - Watchdog: watchdog.md - keventd: keventd.md + - D-Bus Integration: dbus.md - Service State Machine: state-machine.md - Distributions: distro.md - Requirements: requirements.md diff --git a/plugins/dbus.c b/plugins/dbus.c index 80c653e9..39b6ec02 100644 --- a/plugins/dbus.c +++ b/plugins/dbus.c @@ -32,22 +32,11 @@ #include "config.h" #include "helpers.h" #include "plugin.h" -#include "service.h" -#include "util.h" #include "conf.h" +#include "util.h" #include "log.h" #define DBUS_DAEMON "dbus-daemon" -#define DBUS_ARGS "--nofork --system --syslog-only" -#define DBUS_DESC "D-Bus message bus daemon" - -#ifndef DBUS_DAEMONUSER -#define DBUS_DAEMONUSER "messagebus" -#endif - -#ifndef DBUS_DAEMONGROUP -#define DBUS_DAEMONGROUP "messagebus" -#endif /* * Dumnpster diving for the D-Bus main configuration file @@ -96,79 +85,35 @@ static char *dbus_pidfn(void) return NULL; } +/* + * The directories live in tmpfiles.d/dbus.conf and the service in + * system/20-dbus.conf, both of which an administrator can override. + * What is left needs to look at the running system, so it stays here. + */ static void setup(void *arg) { - char *group = DBUS_DAEMONGROUP; - char *user = DBUS_DAEMONUSER; - char esccmd[256]; - char pid[300]; char *pidfn; - mode_t prev; - char *cmd; if (rescue) { dbg("Skipping %s plugin in rescue mode.", "dbus"); return; } - cmd = which(DBUS_DAEMON); - if (!cmd) { + if (!whichp(DBUS_DAEMON)) { dbg("Skipping plugin, %s is not installed.", DBUS_DAEMON); return; } - if (getuser(user, NULL) == -1) { - if (getuser("dbus", NULL) == -1) - user = "root"; /* fallback */ - else - user = "dbus"; /* e.g., Buildroot */ - } - - if (getgroup(group) == -1) { - if (getgroup("dbus") == -1) - group = "root"; /* fallback */ - else - group = "dbus"; /* e.g., Buildroot */ - } - /* Clean up from any previous pre-bootstrap run */ pidfn = dbus_pidfn(); - if (pidfn) + if (pidfn) { remove(pidfn); - - dbg("Creating D-Bus Required Directories ..."); - prev = umask(0); - mksubsys("/var/run/dbus", 0755, user, group); - mksubsys("/var/run/lock/subsys", 0755, user, group); - mksubsys("/var/lib/dbus", 0755, user, group); - mksubsys("/tmp/dbus", 0755, user, group); - umask(prev); + free(pidfn); + } /* Generate machine id for dbus */ if (whichp("dbus-uuidgen")) run_interactive("dbus-uuidgen --ensure", "Verifying D-Bus machine UUID"); - - /* - * Register service with Finit - * Note: dbus drops privs after starting up. - */ - pid[0] = 0; - if (pidfn) { - char esc[280]; - - snprintf(pid, sizeof(pid), "\tpidfile = \"%s\"\n", - conf_escape(pidfn, esc, sizeof(esc))); - free(pidfn); - } - conf_save_service(SVC_TYPE_SERVICE, "dbus", "dbus.conf", - "\tdescription = \"" DBUS_DESC "\"\n" - "\trunlevel = \"S123456789\"\n" - "\tnotify = \"none\"\n" - "\tcgroup system {}\n" - "%s" - "\tcommand = \"%s " DBUS_ARGS "\"\n", - pid, conf_escape(cmd, esccmd, sizeof(esccmd))); - free(cmd); } static plugin_t plugin = { diff --git a/src/Makefile.am b/src/Makefile.am index df8a8dfd..0c065e6a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -81,6 +81,9 @@ finit_SOURCES = api.c cgroup.c cgroup.h \ if LOGROTATE finit_SOURCES += logrotate.c endif +if DBUS +finit_SOURCES += dbus.c +endif pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \ plugin.h svc.h service.h @@ -94,6 +97,10 @@ finit_LDADD += ../plugins/libplug.la else finit_LDADD += -ldl endif +if DBUS +finit_CPPFLAGS += -I$(top_srcdir)/libink +finit_LDADD += $(top_builddir)/libink/libink.la +endif initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \ client.c client.h cond.c cond.h reboot.c \ @@ -101,6 +108,10 @@ initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \ initctl_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 initctl_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS) initctl_LDADD = $(lite_LIBS) $(uev_LIBS) +if DBUS +initctl_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/libink +initctl_LDADD += $(top_builddir)/libink/libink.la +endif INIT_LNKS = init telinit REBOOT_LNKS = reboot shutdown halt poweroff suspend diff --git a/src/api.c b/src/api.c index b60a58d9..be14e215 100644 --- a/src/api.c +++ b/src/api.c @@ -113,38 +113,7 @@ static int restart(svc_t *svc, void *user_data) static int reload(svc_t *svc, void *user_data) { (void)user_data; - - if (!svc) - return 1; - - if (svc_is_blocked(svc)) - svc_start(svc); - else - service_timeout_cancel(svc); - - /* - * Clear conditions before reload to ensure dependent services - * are properly updated. Only needed when the service does NOT - * support SIGHUP (noreload), because then it will be stopped - * and restarted, so conditions genuinely go away. When the - * service handles SIGHUP, its PID and pidfile persist, so the - * condition stays valid and dependents should not be disrupted. - * - * Note: only clear 'ready' for services where the pidfile - * inotify handler reasserts it (pid/none). For s6/systemd - * services readiness relies on their respective notification - * mechanism which may not re-trigger on SIGHUP. - */ - if (svc_is_noreload(svc)) { - svc_cond_clear(svc); - if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) - service_ready(svc, 0); - } - - svc_mark_dirty(svc); - service_step(svc); - - return 0; + return service_reload(svc); } static int do_stop (char *buf, size_t len) { return call(stop, buf, len); } diff --git a/src/cond-w.c b/src/cond-w.c index b3662d6e..fc3d2150 100644 --- a/src/cond-w.c +++ b/src/cond-w.c @@ -37,6 +37,13 @@ #include "service.h" #include "sm.h" +#ifdef HAVE_DBUS +/* Forward-declared locally to keep cond-w.c independent of the + * daemon's private.h (which pulls in svc/plugin headers). The full + * prototype lives in private.h for callers in finit's main loop. */ +void dbus_notify_condition_change(const char *name, const char *state); +#endif + struct cond_boot { TAILQ_ENTRY(cond_boot) link; char *name; @@ -353,6 +360,9 @@ void cond_set(const char *name) if (cond_set_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "on"); +#endif cond_update(name); } @@ -383,6 +393,9 @@ void cond_set_oneshot(const char *name) if (cond_set_oneshot_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "on"); +#endif cond_update(name); } @@ -404,6 +417,9 @@ void cond_clear(const char *name) if (cond_clear_noupdate(name)) return; +#ifdef HAVE_DBUS + dbus_notify_condition_change(name, "off"); +#endif cond_update(name); } diff --git a/src/dbus.c b/src/dbus.c new file mode 100644 index 00000000..8f18f1f0 --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,1721 @@ +/* Finit-side glue between the event loop and libink. + * + * Owns the libink server, accepts new peers, drives each peer's + * state machine, and registers the Finit-specific D-Bus object + * tree (org.finit.Manager1 et al). Nothing in libink/ depends on + * finit-internal types: the boundary lives in this file, by design. + * + * Copyright (c) 2026 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#ifdef HAVE_DBUS + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "link.h" +#include "path.h" + +#include "finit.h" +#include "cond.h" +#include "conf.h" +#include "log.h" +#include "private.h" +#include "schedule.h" +#include "service.h" +#include "sig.h" +#include "sm.h" +#include "svc.h" +#include "util.h" + +#define DBUS_MAX_PEERS 64 + +struct peer { + uev_t watcher; + link_connection_t *conn; + int dead; + TAILQ_ENTRY(peer) link; +}; + +static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); +static TAILQ_HEAD(, peer) reapq = TAILQ_HEAD_INITIALIZER(reapq); +static link_server_t *server; +static uev_t accept_watcher; +static size_t peer_count; +static struct peer *sysbus_peer; + +static void sysbus_probe(void); +static void sender_cache_flush(void); + +/* libink has no logger of its own, so give it ours. The func name it + * passes takes the place of dbg()'s __func__, which would otherwise + * always read "link_log_cb". */ +static void link_log_cb(void *userdata, const char *func, const char *msg) +{ + (void)userdata; + logit(LOG_DEBUG, "%s():%s", func, msg); +} + +/* Close and free everything dropped since the last time round the + * event loop. Safe here because no connection's read loop is on the + * stack; see peer_drop(). */ +static void peer_reap(void *arg) +{ + struct peer *p; + + (void)arg; + while ((p = TAILQ_FIRST(&reapq))) { + TAILQ_REMOVE(&reapq, p, link); + link_connection_close(p->conn); + free(p); + } +} + +/* Not zero: a uev timer armed with a zero timeout is a disarmed + * timer, so the work would never run and the connections would leak. + * Any short delay does, the peer is already unlinked and its watcher + * stopped, so nothing touches it in the meantime. */ +static struct wq reap_work = { .cb = peer_reap, .delay = 10 }; + +/* + * Nothing in libink can time itself out, it has no event loop, so the + * deadline for a parked call and for a call we made on the broker is + * ours to keep. The sweep only runs while something is outstanding: + * expire_arm() starts it, and it stops rearming as soon as nothing is + * left, so a system that never talks to a broker never wakes up for + * this. + */ +#define DBUS_CALL_TIMEOUT_MS 5000 +#define DBUS_SWEEP_MS 1000 + +static void expire_sweep(void *arg); +static struct wq expire_work = { .cb = expire_sweep, .delay = DBUS_SWEEP_MS }; +static int expire_armed; + +/* Idempotent: several parks in one turn of the loop share one sweep. */ +static void expire_arm(void) +{ + if (expire_armed) + return; + if (!schedule_work(&expire_work)) + expire_armed = 1; +} + +static void expire_sweep(void *arg) +{ + struct peer *p, *tmp; + int live = 0; + + (void)arg; + expire_armed = 0; + + /* _SAFE because expiring a call runs its callback, and a + * callback that ends up writing to a peer can drop it, which + * unlinks it from this very list. */ + TAILQ_FOREACH_SAFE(p, &peers, link, tmp) { + if (!p->dead) + live += link_connection_expire(p->conn, DBUS_CALL_TIMEOUT_MS); + } + + if (live) + expire_arm(); +} + +/* + * A peer can be dropped from inside its own read loop: a handler emits + * a signal, the write to this very peer fails, and dbus_emit_signal() + * lands here while link_connection_process() still holds the + * connection and will touch its rx buffer on the way out. Freeing now + * would pull that out from under it, so unlink the peer and let the + * event loop free it once the stack has unwound. + */ +static void peer_drop(struct peer *p) +{ + if (p->dead) + return; /* already on its way out */ + p->dead = 1; + + uev_io_stop(&p->watcher); + TAILQ_REMOVE(&peers, p, link); + peer_count--; + + /* broker gone; the notify paths probe for its return. Its unique + * names die with it, so nothing we learned about them is safe to + * carry over to whatever takes its place. */ + if (p == sysbus_peer) { + sysbus_peer = NULL; + sender_cache_flush(); + } + + TAILQ_INSERT_TAIL(&reapq, p, link); + schedule_work(&reap_work); +} + +static void peer_cb(uev_t *w, void *arg, int events) +{ + struct peer *p = arg; + + (void)w; + + if (UEV_ERROR == events) { + peer_drop(p); + return; + } + + if (link_connection_process(p->conn) < 0) + peer_drop(p); +} + +/* Wrap an authenticated connection in a struct peer, insert into the + * peer list, and register an event-loop watcher. Enforces + * DBUS_MAX_PEERS. Closes the connection and returns NULL on failure. + * Used by both the accept path and the system-bus attach path. */ +static struct peer *peer_register(uev_ctx_t *ctx, link_connection_t *conn) +{ + struct peer *p; + + if (peer_count >= DBUS_MAX_PEERS) { + logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", + peer_count); + link_connection_close(conn); + return NULL; + } + + p = calloc(1, sizeof(*p)); + if (!p) { + link_connection_close(conn); + return NULL; + } + + p->conn = conn; + TAILQ_INSERT_TAIL(&peers, p, link); + peer_count++; + + if (uev_io_init(ctx, &p->watcher, peer_cb, p, + link_connection_get_fd(conn), UEV_READ)) { + peer_drop(p); + return NULL; + } + return p; +} + +static void accept_cb(uev_t *w, void *arg, int events) +{ + (void)arg; + + if (UEV_ERROR == events) { + err(1, "D-Bus accept watcher error"); + return; + } + + for (;;) { + link_connection_t *conn = NULL; + + if (link_server_accept(server, &conn) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + err(1, "Failed accepting D-Bus client"); + break; + } + + if (!peer_register(w->ctx, conn)) + continue; /* logged inside */ + } +} + +/* ---------- org.finit.Manager1 ---------- */ + +/* Forward decl + buffer-size constant — both consumed by Manager1 + * handlers below, defined in the Service1 block further down. */ +#define SERVICE_PATH_PREFIX "/org/finit/service/" +#define SERVICE_PATH_PREFIX_LEN (sizeof(SERVICE_PATH_PREFIX) - 1) +#define FINIT_SVC_PATH_MAX 512 +static int service_path_for(svc_t *svc, char *buf, size_t bufsz); + +static int manager_list_services(link_call_t *call, void *userdata) +{ + link_writer_t *w; + svc_t *iter = NULL; + svc_t *svc; + + (void)userdata; + + w = link_call_reply(call); + if (!w) + return -1; + + link_w_array_begin(w, 's'); + for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { + char ident[MAX_IDENT_LEN]; + + svc_ident(svc, ident, sizeof(ident)); + link_w_string(w, ident); + } + link_w_array_end(w); + return 0; +} + +static int manager_get_service(link_call_t *call, void *userdata) +{ + const char *ident; + svc_t *svc; + char path[FINIT_SVC_PATH_MAX]; + link_writer_t *w; + + (void)userdata; + + if (link_call_read_string(call, &ident) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + svc = svc_find_by_str(ident); + if (!svc) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + + if (service_path_for(svc, path, sizeof(path)) < 0) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "Path encoding overflow"); + + w = link_call_reply(call); + if (!w) + return -1; + link_w_path(w, path); + return 0; +} + +/* Service-control helpers used by Start/Stop/Restart/Reload. These + * mirror the static helpers in api.c — kept private here so api.c + * stays untouched in this increment. */ + +static int dbus_apply_stop(svc_t *svc, void *user_data) +{ + (void)user_data; + if (!svc) + return 1; + service_timeout_cancel(svc); + svc_stop(svc); + service_step(svc); + if (!IS_RESERVED_RUNLEVEL(runlevel)) + service_step_all(SVC_TYPE_ANY); + return 0; +} + +static int dbus_apply_start(svc_t *svc, void *user_data) +{ + (void)user_data; + if (!svc) + return 1; + service_timeout_cancel(svc); + svc_start(svc); + service_step(svc); + if (!IS_RESERVED_RUNLEVEL(runlevel)) + service_step_all(SVC_TYPE_ANY); + return 0; +} + +static int dbus_apply_restart(svc_t *svc, void *user_data) +{ + if (!svc) + return 1; + if (!svc_is_running(svc)) + return dbus_apply_start(svc, user_data); + service_timeout_cancel(svc); + service_stop(svc); + service_step(svc); + return 0; +} + +struct dispatch_ctx { + int (*action)(svc_t *, void *); + void *udata; + int matched; +}; + +static int dispatch_found(svc_t *svc, void *udata) +{ + struct dispatch_ctx *ctx = udata; + + ctx->matched++; + return ctx->action(svc, ctx->udata); +} + +static int dispatch_missing(char *job, char *id, void *udata) +{ + (void)job; (void)id; (void)udata; + return 0; /* don't penalise the return; we'll check ->matched */ +} + +/* Apply `action(svc, udata)` to every service matched by `ident`. + * Returns 0 if at least one service matched and the action succeeded + * on all; -1 if no service matched the identity (caller sends + * NoSuchService). */ +static int dispatch_action(const char *ident, + int (*action)(svc_t *, void *), void *udata) +{ + char buf[MAX_IDENT_LEN]; + struct dispatch_ctx ctx = { .action = action, .udata = udata }; + int rc; + + if (!ident || !*ident || strlen(ident) >= sizeof(buf)) + return -1; + memcpy(buf, ident, strlen(ident) + 1); + rc = svc_parse_jobstr(buf, sizeof(buf), &ctx, + dispatch_found, dispatch_missing); + if (ctx.matched == 0) + return -1; + return rc; +} + +static int manager_take_string_method(link_call_t *call, + int (*action)(svc_t *, void *)) +{ + const char *ident; + int rc; + + if (link_call_read_string(call, &ident) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + rc = dispatch_action(ident, action, NULL); + if (rc < 0) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + if (rc) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "failed on matched service(s)"); + + (void)link_call_reply(call); /* empty reply */ + return 0; +} + +static int manager_start (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_start); } +static int manager_stop (link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_stop); } +static int manager_restart(link_call_t *call, void *u) { (void)u; return manager_take_string_method(call, dbus_apply_restart); } + +static int manager_reload(link_call_t *call, void *userdata) +{ + (void)userdata; + /* + * Same semantics as api.c: harmless no-op during bootstrap + * and shutdown, the client still sees success. + */ + if (IS_RESERVED_RUNLEVEL(runlevel)) + warnx("Ignoring reload in runlevel S and 6/0."); + else + sm_reload(); + (void)link_call_reply(call); + return 0; +} + +static int manager_set_runlevel(link_call_t *call, void *userdata) +{ + uint32_t lvl; + + (void)userdata; + if (link_call_read_u32(call, &lvl) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (u)"); + if (lvl > 9 || lvl == INIT_LEVEL) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "runlevel must be 0-9 (excluding internal levels)"); + + if (lvl == 0) halt = SHUT_OFF; + if (lvl == 6) halt = SHUT_REBOOT; + sm_runlevel((int)lvl); + + (void)link_call_reply(call); + return 0; +} + +static int dbus_shutdown(link_call_t *call, shutop_t target, int level) +{ + if (IS_RESERVED_RUNLEVEL(runlevel)) + return link_call_reply_error(call, + "org.finit.Error.WrongRunlevel", + "Already in shutdown"); + halt = target; + sm_runlevel(level); + (void)link_call_reply(call); + return 0; +} + +static int manager_reboot (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_REBOOT, 6); } +static int manager_poweroff(link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_OFF, 0); } +static int manager_halt (link_call_t *c, void *u) { (void)u; return dbus_shutdown(c, SHUT_HALT, 0); } + +static int manager_set_debug(link_call_t *call, void *u) +{ + (void)u; + log_debug(); + (void)link_call_reply(call); + return 0; +} + +static int signal_one(svc_t *svc, void *udata) +{ + int signo = *(int *)udata; + + /* Silently skip stopped services -- a multi-match ident + * (e.g. "sshd:*") should not fail the whole call just because + * one of the matches happens to be in a halted state. */ + if (!svc_is_running(svc)) + return 0; + return !!kill(svc->pid, signo); +} + +static int manager_signal(link_call_t *call, void *u) +{ + const char *ident; + uint32_t signo; + int sig, rc; + + (void)u; + if (link_call_read_string(call, &ident) < 0 || + link_call_read_u32 (call, &signo) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s, u)"); + /* Match the upper bound `initctl signal` allows (1..31). RT + * signals are a future story; keep both sides in lockstep so + * users see the same range regardless of transport. */ + if (signo == 0 || signo > 31) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "signal out of range (1..31)"); + + sig = (int)signo; + rc = dispatch_action(ident, signal_one, &sig); + if (rc < 0) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", ident); + if (rc) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "failed signalling matched service(s)"); + + (void)link_call_reply(call); + return 0; +} + +static int manager_suspend(link_call_t *call, void *u) +{ + (void)u; + sync(); + if (suspend() < 0) { + const char *msg = (errno == EINVAL) + ? "Kernel does not support suspend to RAM" + : strerror(errno); + return link_call_reply_error(call, + "org.finit.Error.Failed", msg); + } + (void)link_call_reply(call); + return 0; +} + +/* ---------- Manager1 properties ---------- + * + * Read-only string properties exposed via the standard + * org.freedesktop.DBus.Properties interface. Getters write a + * variant containing a single string. */ + +/* + * Two distinct getters because the property table is static const -- + * we can't bind &runlevel/&prevlevel through userdata. The values + * use the same encoding as the runlevel(8) command and `initctl + * runlevel`: "S" for single-user, "N" for no previous runlevel -- + * the internal digit is not a wire format. + */ +static const char *runlevel_encode(int level, char *buf, size_t len) +{ + if (level == INIT_LEVEL) + strlcpy(buf, "S", len); + else if (level >= 0 && level <= 9) + snprintf(buf, len, "%d", level); + else + strlcpy(buf, "N", len); + + return buf; +} + +static int prop_runlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + link_w_string(w, runlevel_encode(runlevel, buf, sizeof(buf))); + return 0; +} + +static int prop_prevrunlevel(link_writer_t *w, void *u) +{ + char buf[8]; + + (void)u; + if (prevlevel <= 0 || prevlevel > 9) + strlcpy(buf, "N", sizeof(buf)); + else + snprintf(buf, sizeof(buf), "%d", prevlevel); + link_w_string(w, buf); + return 0; +} + +static int prop_version(link_writer_t *w, void *u) +{ + (void)u; + link_w_string(w, PACKAGE_VERSION); + return 0; +} + +static const link_property_t manager_properties[] = { + { .name = "Runlevel", .sig = "s", .getter = prop_runlevel }, + { .name = "PrevRunlevel", .sig = "s", .getter = prop_prevrunlevel }, + { .name = "Version", .sig = "s", .getter = prop_version }, + { NULL, NULL, NULL } +}; + +static const link_method_t manager_methods[] = { + { .name = "ListServices", .in_sig = "", .out_sig = "as", + .handler = manager_list_services }, + { .name = "GetService", .in_sig = "s", .out_sig = "o", + .handler = manager_get_service }, + { .name = "Start", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_start }, + { .name = "Stop", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_stop }, + { .name = "Restart", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload }, + { .name = "SetRunlevel", .in_sig = "u", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel }, + { .name = "Reboot", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot }, + { .name = "Poweroff", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff }, + { .name = "Halt", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt }, + { .name = "Suspend", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_suspend }, + { .name = "SetDebug", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_debug }, + { .name = "Signal", .in_sig = "su", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = manager_signal }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_vtable_t manager_vtable = { + .interface = "org.finit.Manager1", + .methods = manager_methods, + .properties = manager_properties, +}; + +/* ---------- org.finit.Service1 (one object per service) ---------- + * + * Per-service object at /org/finit/service/. + * The vtable's `userdata` is the svc_t * for the specific service. + * Registration is driven dynamically from svc_new()/svc_del() via + * dbus_register_service() / dbus_unregister_service() below. */ + +/* SERVICE_PATH_PREFIX / SERVICE_PATH_PREFIX_LEN / FINIT_SVC_PATH_MAX + * defined near the top of the file so Manager1.GetService can refer + * to them. */ + +static int service_action_method(link_call_t *call, void *userdata, + int (*action)(svc_t *, void *)) +{ + svc_t *svc = userdata; + + if (!svc) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", + "Service object no longer valid"); + + action(svc, NULL); + (void)link_call_reply(call); + return 0; +} + +static int service1_start (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_start); } +static int service1_stop (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); } +static int service1_restart(link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); } + +static int service1_reload(link_call_t *call, void *userdata) +{ + svc_t *svc = userdata; + + if (!svc) + return link_call_reply_error(call, + "org.finit.Error.NoSuchService", + "Service object no longer valid"); + + service_reload(svc); + (void)link_call_reply(call); + return 0; +} + +static const link_method_t service_methods[] = { + { .name = "Start", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_start }, + { .name = "Stop", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_stop }, + { .name = "Restart", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_restart }, + { .name = "Reload", .in_sig = "", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = service1_reload }, + { NULL, NULL, NULL, 0, NULL } +}; + +/* + * Getters write the bare value; the framework emits the variant + * signature from the table below. `State` deliberately uses the + * initctl status vocabulary from svc_status(), not the coarser + * ServiceStateChanged strings -- a client that only tracks edges + * has the signal, a client that asks gets the full story. + */ +#define SVC_PROP_STR(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_string(w, ((svc_t *)arg)->field); \ + return 0; \ +} + +#define SVC_PROP_U32(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_u32(w, (uint32_t)((svc_t *)arg)->field); \ + return 0; \ +} + +#define SVC_PROP_BOOL(fn, field) \ +static int fn(link_writer_t *w, void *arg) \ +{ \ + link_w_bool(w, ((svc_t *)arg)->field); \ + return 0; \ +} + +static int svc_prop_identity(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_ident(arg, NULL, 0)); + return 0; +} + +static int svc_prop_state(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_status(arg)); + return 0; +} + +static int svc_prop_type(link_writer_t *w, void *arg) +{ + link_w_string(w, svc_typestr((svc_t *)arg)); + return 0; +} + +static int svc_prop_command(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + char buf[512]; + + compose_cmdline(svc, buf, sizeof(buf)); + if (svc_is_sysv(svc)) { + strlcat(buf, " ", sizeof(buf)); + strlcat(buf, svc->state == SVC_HALTED_STATE + ? "stop" : "start", sizeof(buf)); + } + + link_w_string(w, buf); + return 0; +} + +static int svc_prop_pid(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + + link_w_u32(w, svc->pid > 0 ? (uint32_t)svc->pid : 0); + return 0; +} + +static int svc_prop_restarts(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + + link_w_u32(w, svc->restart_cnt > 0 ? (uint32_t)svc->restart_cnt : 0); + return 0; +} + +static int svc_prop_uptime(link_writer_t *w, void *arg) +{ + svc_t *svc = arg; + long up = 0; + + if (svc->pid > 0) { + up = jiffies() - svc->start_time; + if (up < 0) + up = 0; + } + link_w_u32(w, (uint32_t)up); + return 0; +} + +SVC_PROP_STR (svc_prop_name, name) +SVC_PROP_STR (svc_prop_desc, desc) +SVC_PROP_STR (svc_prop_conditions, cond) +SVC_PROP_STR (svc_prop_origin, file) +SVC_PROP_STR (svc_prop_environ, env) +SVC_PROP_STR (svc_prop_pidfile, pidfile) +SVC_PROP_STR (svc_prop_user, username) +SVC_PROP_STR (svc_prop_group, group) +SVC_PROP_U32 (svc_prop_runlevels, runlevels) +SVC_PROP_U32 (svc_prop_exitstatus, status) +SVC_PROP_U32 (svc_prop_restarts_tot, restart_tot) +SVC_PROP_U32 (svc_prop_restart_max, restart_max) +SVC_PROP_U32 (svc_prop_starts, once) +SVC_PROP_BOOL(svc_prop_manual, manual) +SVC_PROP_BOOL(svc_prop_forking, forking) +SVC_PROP_BOOL(svc_prop_started, started) + +static const link_property_t service_properties[] = { + { .name = "Identity", .sig = "s", .getter = svc_prop_identity }, + { .name = "Name", .sig = "s", .getter = svc_prop_name }, + { .name = "State", .sig = "s", .getter = svc_prop_state }, + { .name = "Pid", .sig = "u", .getter = svc_prop_pid }, + { .name = "RestartCount", .sig = "u", .getter = svc_prop_restarts }, + { .name = "Runlevels", .sig = "u", .getter = svc_prop_runlevels }, + { .name = "Description", .sig = "s", .getter = svc_prop_desc }, + { .name = "Command", .sig = "s", .getter = svc_prop_command }, + { .name = "Conditions", .sig = "s", .getter = svc_prop_conditions }, + { .name = "Type", .sig = "s", .getter = svc_prop_type }, + { .name = "Origin", .sig = "s", .getter = svc_prop_origin }, + { .name = "Environment", .sig = "s", .getter = svc_prop_environ }, + { .name = "PidFile", .sig = "s", .getter = svc_prop_pidfile }, + { .name = "User", .sig = "s", .getter = svc_prop_user }, + { .name = "Group", .sig = "s", .getter = svc_prop_group }, + { .name = "Uptime", .sig = "u", .getter = svc_prop_uptime }, + { .name = "ExitStatus", .sig = "u", .getter = svc_prop_exitstatus }, + { .name = "RestartsTotal",.sig = "u", .getter = svc_prop_restarts_tot }, + { .name = "RestartMax", .sig = "u", .getter = svc_prop_restart_max }, + { .name = "Starts", .sig = "u", .getter = svc_prop_starts }, + { .name = "ManualStart", .sig = "b", .getter = svc_prop_manual }, + { .name = "Forking", .sig = "b", .getter = svc_prop_forking }, + { .name = "Started", .sig = "b", .getter = svc_prop_started }, + { NULL, NULL, NULL } +}; + +static const link_vtable_t service_vtable = { + .interface = "org.finit.Service1", + .methods = service_methods, + .properties = service_properties, +}; + +/* Build the canonical object path for a service. Identity is + * "name" for single-instance services, "name:id" otherwise. */ +static int service_path_for(svc_t *svc, char *buf, size_t bufsz) +{ + char ident[MAX_IDENT_LEN]; + size_t plen = SERVICE_PATH_PREFIX_LEN; + int enc; + + if (bufsz <= plen) + return -1; + memcpy(buf, SERVICE_PATH_PREFIX, plen); + + svc_ident(svc, ident, sizeof(ident)); + enc = link_path_encode(ident, buf + plen, bufsz - plen); + if (enc < 0) + return -1; + return (int)plen + enc; +} + +void dbus_register_service(svc_t *svc) +{ + char path[FINIT_SVC_PATH_MAX]; + + if (!server || !svc) + return; + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + if (link_server_add_object(server, path, &service_vtable, svc) < 0) + logit(LOG_WARNING, "dbus: failed registering %s", path); +} + +void dbus_unregister_service(svc_t *svc) +{ + char path[FINIT_SVC_PATH_MAX]; + + if (!server || !svc) + return; + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + (void)link_server_remove_object(server, path); +} + +/* ---------- signal fan-out helper ---------- + * + * Fan out a pre-marshalled signal body to every connected peer, + * letting each connection apply its AddMatch filter. Short-circuits + * when no peers are connected so dbus_notify_* callers don't have + * to inspect that state themselves. */ +/* A peer that has gone away is routine rather than a fault: initctl + * calls and exits, and on the way down every peer goes at once, which + * is not something an operator watching the shutdown needs to read + * about. Keep the warning for a write that failed for some other + * reason, where something really is wrong. */ +static int quiet_drop(int err) +{ + if (err == EPIPE || err == ECONNRESET || err == ENOTCONN) + return 1; + + return runlevel == 0 || runlevel == 6; +} + +static void dbus_emit_signal(const char *path, + const char *interface, + const char *member, + const char *signature, + const uint8_t *body, size_t body_len) +{ + struct peer *p, *tmp; + + if (!server || TAILQ_EMPTY(&peers)) + return; + TAILQ_FOREACH_SAFE(p, &peers, link, tmp) { + int err; + + if (link_connection_emit_signal(p->conn, + path, interface, member, + signature, body, body_len) >= 0) + continue; + + err = errno; + + /* nothing hit the wire, and same for every peer */ + if (err == EMSGSIZE || err == EINVAL) + break; + + logit(quiet_drop(err) ? LOG_DEBUG : LOG_WARNING, + "D-Bus peer fd %d write failed: %s, dropping", + link_connection_get_fd(p->conn), strerror(err)); + peer_drop(p); + } +} + +/* ---------- signal emission: ServiceStateChanged ---------- */ + +/* + * Coarse svc_state_t -> string. svc_status() in svc.h returns a + * richer string that also considers svc->block, but emitting just + * the state-machine state is enough for clients to track lifecycle + * transitions. Keep the strings stable -- they're a wire-API + * commitment once shipped. + * + * No `default:` on purpose: a new SVC_*_STATE added to svc.h must + * also pick a wire name here, and -Wall (-Wswitch) flags the + * missing case. + */ +static const char *state_name(svc_state_t s) +{ + switch (s) { + case SVC_HALTED_STATE: return "halted"; + case SVC_DONE_STATE: return "done"; + case SVC_DEAD_STATE: return "dead"; + case SVC_CLEANUP_STATE: return "cleanup"; + case SVC_TEARDOWN_STATE: return "teardown"; + case SVC_STOPPING_STATE: return "stopping"; + case SVC_SETUP_STATE: return "setup"; + case SVC_PAUSED_STATE: return "paused"; + case SVC_WAITING_STATE: return "waiting"; + case SVC_STARTING_STATE: return "starting"; + case SVC_RUNNING_STATE: return "running"; + } + return "unknown"; +} + +void dbus_notify_service_state(svc_t *svc, int old_state, int new_state) +{ + uint8_t body[256]; + link_writer_t w; + char ident[MAX_IDENT_LEN]; + char path[FINIT_SVC_PATH_MAX]; + ssize_t blen; + + if (!svc) + return; + + sysbus_probe(); + + svc_ident(svc, ident, sizeof(ident)); + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, ident); + link_w_string(&w, state_name((svc_state_t)old_state)); + link_w_string(&w, state_name((svc_state_t)new_state)); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "ServiceStateChanged", "sss", body, (size_t)blen); + + /* + * Dual emission: Properties-aware clients track one object via + * the standard PropertiesChanged instead of filtering the + * manager-wide signal. Volatile numerics are invalidated, not + * marshalled -- interested clients re-Get. + */ + if (service_path_for(svc, path, sizeof(path)) < 0) + return; + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, "org.finit.Service1"); + link_w_array_begin(&w, '{'); + link_w_struct_begin(&w); + link_w_string(&w, "State"); + link_w_variant_string(&w, svc_status(svc)); + link_w_struct_end(&w); + link_w_array_end(&w); + link_w_array_begin(&w, 's'); + link_w_string(&w, "Pid"); + link_w_string(&w, "RestartCount"); + link_w_array_end(&w); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal(path, "org.freedesktop.DBus.Properties", + "PropertiesChanged", "sa{sv}as", + body, (size_t)blen); +} + +/* ---------- signal emission: RunlevelChanged ---------- + * + * Fired by sm.c right after the runlevel global flips. Body is + * (old, new) in the same runlevel(8) encoding as the Manager1 + * Runlevel property: digits, "S", or "N". */ +void dbus_notify_runlevel_change(int old_level, int new_level) +{ + uint8_t body[64]; + link_writer_t w; + char old_s[8], new_s[8]; + ssize_t blen; + + runlevel_encode(old_level, old_s, sizeof(old_s)); + runlevel_encode(new_level, new_s, sizeof(new_s)); + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, old_s); + link_w_string(&w, new_s); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal("/org/finit/manager", "org.finit.Manager1", + "RunlevelChanged", "ss", body, (size_t)blen); +} + +/* ---------- org.finit.Cond1 ---------- */ + +#define COND_PATH_OBJECT "/org/finit/cond" +#define COND_INTERFACE "org.finit.Cond1" + +/* Cond1.Set/Clear refuse anything that isn't a usr/ condition -- + * pid/, sys/, hook/ are owned by Finit's state machine and giving + * clients write access there would let them corrupt service state. + * Bare names ("foo") are normalised to "usr/foo" the same way + * initctl does. The returned pointer is valid for the duration + * of the caller's stack frame (`buf` must be at least 128 bytes). */ +static const char *normalise_usr_cond(const char *name, char *buf, size_t bufsz) +{ + const char *tail; + + if (!name || !*name) + return NULL; + if (strchr(name, '.')) + return NULL; + + if (strchr(name, '/')) { + if (strncmp(name, "usr/", 4) != 0) + return NULL; + tail = name + 4; + /* Match initctl's policy: no further slashes in the tail, + * and no empty tail ("usr/" alone). */ + if (!*tail || strchr(tail, '/')) + return NULL; + if (strlen(name) >= bufsz) + return NULL; + memcpy(buf, name, strlen(name) + 1); + return buf; + } + + if ((size_t)snprintf(buf, bufsz, "usr/%s", name) >= bufsz) + return NULL; + return buf; +} + +/* Reject names that would escape /run/finit/cond/. cond_get(name) + * boils down to fopen(_PATH_COND + name), so without this check any + * caller can make PID 1 open arbitrary files -- a path traversal + * primitive that also stalls PID 1 if pointed at a FIFO or a slow + * device. Legal cond names look like "usr/foo", "pid/sshd", + * "service/keventd/ready"; no leading slash, no ".." segment. */ +static int cond_name_valid(const char *name) +{ + const char *p; + + if (!name || !*name || *name == '/') + return 0; + for (p = name; *p; p++) { + if (*p == '.' && p[1] == '.' && + (p[2] == '\0' || p[2] == '/')) + return 0; + } + return 1; +} + +static int cond1_get(link_call_t *call, void *userdata) +{ + const char *name; + link_writer_t *w; + + (void)userdata; + + if (link_call_read_string(call, &name) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + if (!cond_name_valid(name)) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "invalid condition name"); + + w = link_call_reply(call); + if (!w) + return -1; + link_w_string(w, condstr(cond_get(name))); + return 0; +} + +static int cond1_set_or_clear(link_call_t *call, int do_set) +{ + const char *name; + char buf[128]; + const char *full; + + if (link_call_read_string(call, &name) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s)"); + + full = normalise_usr_cond(name, buf, sizeof(buf)); + if (!full) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "Set/Clear is restricted to usr/* conditions"); + + if (do_set) + /* cond_set_oneshot, not cond_set: a user-asserted condition + * is a symlink to _PATH_RECONF, so it tracks the reconf + * generation automatically and stays "on" across reloads + * and runlevel switches. cond_set() writes a fixed + * generation that goes "flux" on the next reload -- wrong + * semantics for user conditions, and what initctl cond set + * has done forever via the filesystem path. */ + cond_set_oneshot(full); + else + cond_clear(full); + + (void)link_call_reply(call); + return 0; +} + +static int cond1_set (link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 1); } +static int cond1_clear(link_call_t *c, void *u) { (void)u; return cond1_set_or_clear(c, 0); } + +/* nftw() can't pass user data so a single static handle ferries the + * writer into the callback. Safe because dispatch is single-threaded. */ +static link_writer_t *cond_walk_writer; +static int cond_walk_dump; + +static int cond_walk_cb(const char *fpath, const struct stat *sb, + int tflag, struct FTW *ftwbuf) +{ + const char *name; + const char *state; + size_t prefix_len; + + (void)sb; + (void)ftwbuf; + + if (tflag != FTW_F) + return 0; + if (!strcmp(fpath, _PATH_RECONF)) + return 0; + + prefix_len = strlen(_PATH_COND); + if (strlen(fpath) <= prefix_len) + return 0; + name = fpath + prefix_len; + + if (cond_walk_dump) { + state = condstr(cond_get_path(fpath)); + link_w_struct_begin(cond_walk_writer); + link_w_string(cond_walk_writer, name); + link_w_string(cond_walk_writer, state); + link_w_struct_end(cond_walk_writer); + } else { + link_w_string(cond_walk_writer, name); + } + return 0; +} + +static int cond1_list(link_call_t *call, void *userdata) +{ + link_writer_t *w; + + (void)userdata; + + w = link_call_reply(call); + if (!w) + return -1; + + link_w_array_begin(w, 's'); + cond_walk_writer = w; + cond_walk_dump = 0; + (void)nftw(_PATH_COND, cond_walk_cb, 20, 0); + cond_walk_writer = NULL; + link_w_array_end(w); + return 0; +} + +static int cond1_dump(link_call_t *call, void *userdata) +{ + link_writer_t *w; + + (void)userdata; + + w = link_call_reply(call); + if (!w) + return -1; + + link_w_array_begin(w, '('); + cond_walk_writer = w; + cond_walk_dump = 1; + (void)nftw(_PATH_COND, cond_walk_cb, 20, 0); + cond_walk_writer = NULL; + link_w_array_end(w); + return 0; +} + +static const link_method_t cond_methods[] = { + { .name = "Get", .in_sig = "s", .out_sig = "s", + .handler = cond1_get }, + { .name = "Set", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = cond1_set }, + { .name = "Clear", .in_sig = "s", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = cond1_clear }, + { .name = "List", .in_sig = "", .out_sig = "as", + .handler = cond1_list }, + { .name = "Dump", .in_sig = "", .out_sig = "a(ss)", + .handler = cond1_dump }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_vtable_t cond_vtable = { + .interface = COND_INTERFACE, + .methods = cond_methods, +}; + +/* ---------- signal emission: ConditionChanged ---------- */ + +void dbus_notify_condition_change(const char *name, const char *state) +{ + uint8_t body[256]; + link_writer_t w; + ssize_t blen; + + if (!name || !state) + return; + + sysbus_probe(); + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, name); + link_w_string(&w, state); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + dbus_emit_signal(COND_PATH_OBJECT, COND_INTERFACE, + "ConditionChanged", "ss", body, (size_t)blen); +} + +/* ---------- who may change things ---------- + * + * Root, or a member of the group the bus socket is owned by, which is + * the same set --with-group already lets connect. Both gates then say + * the same thing, rather than the socket admitting the wheel group and + * every method turning it away. + * + * On the local bus the kernel already made this decision at connect(), + * supplementary groups and all, so the lookup only confirms it. The + * system bus has no socket mode to lean on, which is why we ask here + * rather than trusting the connection. + * + * Deliberately uncached: /etc/group changes while Finit runs, and a + * privileged call is an operator action, not a hot path. + */ +static int caller_is_privileged(uid_t uid, void *userdata) +{ + (void)userdata; + + if (uid == 0) + return 1; + + /* Group membership comes from NSS, which the C library loads + * with dlopen(), so a build meant to link statically cannot + * count on it. Compiled out rather than left to fail open: + * root only there, see doc/dbus.md. */ +#ifndef ENABLE_STATIC + { + gid_t groups[NGROUPS_MAX]; + int ngroups = NGROUPS_MAX; + struct passwd *pw; + int gid, i; + + gid = getgroup(DEFGROUP); + if (gid < 0) + return 0; + + pw = getpwuid(uid); + if (!pw) + return 0; + + if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) < 0) + return 0; + + for (i = 0; i < ngroups; i++) { + if (groups[i] == (gid_t)gid) + return 1; + } + } +#endif + + return 0; +} + +/* ---------- caller identity on the system bus ---------- + * + * libink parks a privileged call and asks us who sent it; we ask the + * bus driver with GetConnectionUnixUser and answer when the reply + * lands, through the same event loop as everything else. + * + * A bus never reuses a unique name, so an answer holds for as long as + * that bus runs. It does not survive the bus restarting, though: + * a new dbus-daemon numbers from scratch and :1.7 becomes somebody + * else, so peer_drop() empties the cache when the broker goes. + * + * It is a ring: the oldest entry loses on overflow, and losing one + * only costs another round trip. + */ +#define SENDER_CACHE_LEN 16 + +struct sender_uid { + char name[LINK_SENDER_MAX]; + uid_t uid; +}; + +static struct sender_uid sender_cache[SENDER_CACHE_LEN]; +static unsigned sender_next; + +static void sender_cache_flush(void) +{ + memset(sender_cache, 0, sizeof(sender_cache)); + sender_next = 0; +} + +static int sender_cached(const char *sender, uid_t *uid) +{ + int i; + + for (i = 0; i < SENDER_CACHE_LEN; i++) { + if (sender_cache[i].name[0] && !strcmp(sender_cache[i].name, sender)) { + *uid = sender_cache[i].uid; + return 1; + } + } + + return 0; +} + +static void sender_remember(const char *sender, uid_t uid) +{ + unsigned i = sender_next++ % SENDER_CACHE_LEN; + + strlcpy(sender_cache[i].name, sender, sizeof(sender_cache[i].name)); + sender_cache[i].uid = uid; +} + +/* One outstanding GetConnectionUnixUser. Freed by the reply callback, + * which libink guarantees to run exactly once, with a NULL reply if + * the connection drops first. */ +struct uid_query { + link_authz_t tok; + char sender[LINK_SENDER_MAX]; +}; + +static void uid_reply_cb(link_connection_t *conn, const link_reply_t *reply, void *userdata) +{ + struct uid_query *q = userdata; + uid_t uid = (uid_t)-1; + uint32_t val; + + if (!reply) { + dbg("connection dropped before %s was identified", q->sender); + } else if (reply->type == LINK_MSG_METHOD_RETURN && + link_reply_get_u32(reply, &val) == 0) { + uid = (uid_t)val; + sender_remember(q->sender, uid); + dbg("sender %s is uid %d", q->sender, (int)uid); + } else { + dbg("GetConnectionUnixUser(%s) failed: %s", q->sender, + reply->error_name ? reply->error_name : "unexpected reply"); + } + + link_uid_resolved(conn, q->tok, uid); + free(q); +} + +static int sysbus_uid_resolver(link_connection_t *conn, const char *sender, + link_authz_t tok, uid_t *uid, void *userdata) +{ + struct uid_query *q; + + (void)userdata; + + /* Never truncate: a shortened key could match a different + * sender and hand it someone else's privileges. */ + if (strlen(sender) >= LINK_SENDER_MAX) + return -1; + + if (sender_cached(sender, uid)) { + dbg("sender %s is uid %d, from cache", sender, (int)*uid); + return 0; + } + + dbg("asking the bus driver who %s is ...", sender); + + q = calloc(1, sizeof(*q)); + if (!q) + return -1; + q->tok = tok; + strlcpy(q->sender, sender, sizeof(q->sender)); + + if (link_connection_call(conn, "org.freedesktop.DBus", "/org/freedesktop/DBus", + "org.freedesktop.DBus", "GetConnectionUnixUser", + uid_reply_cb, q, "s", sender) < 0) { + dbg("Failed asking the bus driver about %s: %s", sender, strerror(errno)); + free(q); + return -1; + } + + /* Both the call and the park it belongs to now have a deadline + * to answer by, so make sure something is watching the clock. */ + expire_arm(); + + return 1; /* parked; uid_reply_cb() answers */ +} + +/* ---------- system-bus attach (opportunistic) ---------- + * + * If /var/run/dbus/system_bus_socket is reachable, libink connects to + * the system bus as a regular client, claims org.finit as a well-known + * name, then promotes the authenticated fd into a server-attached + * peer so the same vtables serve incoming method calls and outgoing + * signal fan-out reaches the system bus. + * + * peer_uid is (uid_t)-1 because the connection has no single owner; + * who is calling is established per message by sysbus_uid_resolver() + * below. + * + * A bounded SO_SNDTIMEO/SO_RCVTIMEO budget is applied via + * link_client_open_timeout so a hung dbus-daemon can't stall boot; + * once the connection is attached and flipped to non-blocking, those + * timeouts are silently inert. */ + +#define SYSTEM_BUS_PATH "/var/run/dbus/system_bus_socket" +#define FINIT_BUS_NAME "org.finit" +/* Budget for the synchronous AUTH + Hello + RequestName round-trips. + * If the system bus is alive but the daemon is wedged we'd rather + * give up after a couple of seconds than stall the rest of dbus_init + * (and through it, boot). */ +#define SYSTEM_BUS_TIMEOUT_MS 2000 +/* DBUS_NAME_FLAG_DO_NOT_QUEUE: fail fast if the name is taken + * (something else owns org.finit -- shouldn't happen and we'd + * rather log than silently sit in the queue). */ +#define DBUS_NAME_FLAG_DO_NOT_QUEUE 0x04 + +/* sysbus_probe() re-runs on every service and condition change, so a + * broker that keeps refusing would repeat itself for every event, and + * before syslog is up each line is an open/write/close on /dev/kmsg. + * Say it once, then trace, until an attach succeeds. */ +static int sysbus_warned; + +#define sysbus_level() (sysbus_warned ? LOG_DEBUG : LOG_WARNING) + +/* What the broker said, for the log. A refused call carries an error + * name; anything that failed below that has nothing to quote. */ +static const char *sysbus_errstr(link_client_t *c) +{ + const link_reply_t *r = link_client_reply(c); + + return (r && r->error_name) ? r->error_name : "transport or parse failure"; +} + +static int sysbus_request_name(link_client_t *c) +{ + const char *reason; + uint32_t result; + int rc; + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "RequestName", + "su", FINIT_BUS_NAME, + (uint32_t)DBUS_NAME_FLAG_DO_NOT_QUEUE); + if (rc != LINK_CALL_OK) + reason = sysbus_errstr(c); + else if (link_reply_get_u32(link_client_reply(c), &result) < 0) + reason = "malformed RequestName reply"; + else if (result != 1) /* 2/3/4 mean we did not get the name */ + reason = "name already owned"; + else + return 0; + + logit(sysbus_level(), "Failed to claim %s on system bus: %s", + FINIT_BUS_NAME, reason); + sysbus_warned = 1; + + return -1; +} + +static int try_attach_system_bus(uev_ctx_t *ctx) +{ + link_client_t *c; + link_connection_t *conn; + struct peer *p; + int rc; + + c = link_client_open_timeout(SYSTEM_BUS_PATH, SYSTEM_BUS_TIMEOUT_MS); + if (!c) { + dbg("System bus unavailable at %s; skipping registration", + SYSTEM_BUS_PATH); + return -1; + } + + /* Unlike the local bus, a broker routes by destination, and it + * drops anything not addressed to the driver before Hello. */ + link_client_set_destination(c, "org.freedesktop.DBus"); + + rc = link_client_call_v(c, "/org/freedesktop/DBus", + "org.freedesktop.DBus", "Hello", NULL); + if (rc != LINK_CALL_OK) { + logit(sysbus_level(), "System-bus Hello failed: %s", + sysbus_errstr(c)); + sysbus_warned = 1; + link_client_close(c); + return -1; + } + + if (sysbus_request_name(c) < 0) { + link_client_close(c); + return -1; + } + + /* link_server_attach owns the fd from this point on whether it + * succeeds or fails, so the steal-then-attach pair has no leak + * window. */ + conn = link_server_attach(server, link_client_steal_fd(c), (uid_t)-1, + LINK_ATTACH_BROKER); + if (!conn) + return -1; + + p = peer_register(ctx, conn); + if (!p) { + logit(LOG_WARNING, "Failed registering system-bus peer"); + return -1; + } + + sysbus_peer = p; + link_server_set_uid_resolver(server, sysbus_uid_resolver, NULL); + sysbus_warned = 0; /* arm the warning for a later broker restart */ + logit(LOG_NOTICE, "Registered %s on system bus", FINIT_BUS_NAME); + return 0; +} + +/* + * The broker is usually not up yet when dbus_init() runs -- it is + * typically a finit service itself -- and it may restart at any + * time. The service and condition notify paths call sysbus_probe() + * on every event: when org.finit is unclaimed and the broker's + * socket exists, one coalesced attach attempt is scheduled. This + * stays daemon-agnostic -- only the socket is probed, never a + * service name -- and the broker's own service transitions are what + * trigger it. + */ +static uev_t sysbus_tmr; +static int sysbus_tmr_up; + +static void sysbus_probe_cb(uev_t *w, void *arg, int events) +{ + (void)arg; + (void)events; + + if (!sysbus_peer) + (void)try_attach_system_bus(w->ctx); +} + +static void sysbus_probe(void) +{ + if (sysbus_peer || !server) + return; + if (access(SYSTEM_BUS_PATH, F_OK)) + return; + + /* coalesce bursts to a single probe */ + if (!sysbus_tmr_up) + sysbus_tmr_up = !uev_timer_init(ctx, &sysbus_tmr, + sysbus_probe_cb, NULL, + 200, 0); + else + uev_timer_set(&sysbus_tmr, 200, 0); +} + +/* ---------- init / exit ---------- */ + +int dbus_init(uev_ctx_t *ctx) +{ + dbg("Setting up D-Bus listening socket at %s ...", FINIT_BUS_SOCKET); + + /* Same access policy as INIT_SOCKET: the bus reaches every + * service operation initctl does, so --with-group has to gate + * both or it gates neither. */ + link_set_logger(link_log_cb, NULL); + + if (link_server_new(&server, FINIT_BUS_SOCKET, 0660) < 0) { + err(1, "Failed binding D-Bus socket %s", FINIT_BUS_SOCKET); + return 1; + } + + if (chown(FINIT_BUS_SOCKET, geteuid(), getgroup(DEFGROUP))) + err(1, "Failed setting group %s on %s", DEFGROUP, FINIT_BUS_SOCKET); + + link_server_set_authorizer(server, caller_is_privileged, NULL); + + if (link_server_add_object(server, "/org/finit/manager", + &manager_vtable, NULL) < 0) { + err(1, "Failed registering Manager1 object"); + link_server_free(server); + server = NULL; + return 1; + } + + if (link_server_add_object(server, COND_PATH_OBJECT, + &cond_vtable, NULL) < 0) { + err(1, "Failed registering Cond1 object"); + link_server_free(server); + server = NULL; + return 1; + } + + if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, + link_server_get_fd(server), UEV_READ)) { + err(1, "Failed registering D-Bus accept watcher"); + link_server_free(server); + server = NULL; + return 1; + } + + /* Register Service1 objects for every service already loaded. + * Subsequent svc_new()/svc_del() calls into + * dbus_register_service()/dbus_unregister_service(). */ + { + svc_t *iter = NULL; + svc_t *svc; + + for (svc = svc_iterator(&iter, 1); svc; + svc = svc_iterator(&iter, 0)) + dbus_register_service(svc); + } + + (void)try_attach_system_bus(ctx); + + return 0; +} + +int dbus_exit(void) +{ + struct peer *p; + + if (sysbus_tmr_up) { + uev_timer_stop(&sysbus_tmr); + sysbus_tmr_up = 0; + } + uev_io_stop(&accept_watcher); + + while ((p = TAILQ_FIRST(&peers))) + peer_drop(p); + + /* No read loop is running now, and the timer never will again. */ + peer_reap(NULL); + + if (server) { + link_server_free(server); + server = NULL; + } + + return 0; +} + +#endif /* HAVE_DBUS */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/src/finit.c b/src/finit.c index 96061dd7..8c0e3dc8 100644 --- a/src/finit.c +++ b/src/finit.c @@ -780,6 +780,11 @@ int main(int argc, char *argv[]) dbg("Starting initctl API responder ..."); api_init(&loop); +#ifdef HAVE_DBUS + dbg("Starting D-Bus listener ..."); + dbus_init(&loop); +#endif + dbg("Starting service interval monitor ..."); service_init(&loop); diff --git a/src/finit.h b/src/finit.h index 5bef212b..30657537 100644 --- a/src/finit.h +++ b/src/finit.h @@ -71,6 +71,7 @@ #define BUF_SIZE 4096 #define INIT_SOCKET _PATH_VARRUN "finit/socket" +#define FINIT_BUS_SOCKET _PATH_VARRUN "finit/bus" #define INIT_MAGIC 0x03091969 #define INIT_LEVEL 10 diff --git a/src/initctl.c b/src/initctl.c index f0dbcf86..e5f0df70 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -41,6 +41,13 @@ #include "service.h" #include "cgutil.h" #include "utmp-api.h" +#ifdef HAVE_DBUS +#include "link.h" +#include "path.h" +#endif + +/* 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; @@ -72,7 +79,6 @@ int iw, pw; extern int reboot_main(int argc, char *argv[]); - /* figure ut width of IDENT and PID columns */ static void col_widths(void) { @@ -145,6 +151,11 @@ static int runlevel_get(int *prevlevel) return rc; } +#ifdef HAVE_DBUS +static int try_dbus_manager(const char *method, const char *arg_sig, + const char *arg); +#endif + static int toggle_debug(char *arg) { struct init_request rq = { @@ -152,24 +163,22 @@ static int toggle_debug(char *arg) .cmd = INIT_CMD_DEBUG, }; + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("SetDebug", "", NULL); + if (rc >= 0) return rc; + } +#endif return client_send(&rq, sizeof(rq)); } -static int do_log(svc_t *svc, char *tail) +static int do_log_named(const char *nm, pid_t pid, char *tail) { const char *logfile = "/var/log/syslog"; - pid_t pid; - char *nm; - if (svc) { - nm = svc_ident(svc, NULL, 0); - pid = svc->pid; - if (!pid) - return 0; /* not running */ - } else { - nm = "finit"; - pid = 1; - } + if (!pid) + return 0; /* not running */ if (!fexist(logfile)) { logfile = "/var/log/messages"; @@ -180,6 +189,13 @@ static int do_log(svc_t *svc, char *tail) return systemf("cat %s | grep '\\[%d\\]\\|%s' %s", logfile, pid, nm, tail); } +static int do_log(svc_t *svc, char *tail) +{ + if (svc) + return do_log_named(svc_ident(svc, NULL, 0), svc->pid, tail); + return do_log_named("finit", 1, tail); +} + static int show_log(char *arg) { svc_t *svc = NULL; @@ -193,6 +209,86 @@ static int show_log(char *arg) return do_log(svc, ""); } +#ifdef HAVE_DBUS +/* + * Advance one a{sv} dict entry: key + variant type; the caller reads + * the value with the link_r_* matching `type`. + */ +static int dbus_dict_next(link_reader_t *r, const char **key, char *type) +{ + if (link_r_align(r, 8) < 0) + return -1; + if (link_r_string(r, key) < 0) + return -1; + return link_r_variant_begin(r, type); +} + +/* Fetch all org.finit.Manager1 string properties in one Properties.GetAll + * round-trip, then pick out a subset. `wanted` is a NULL-terminated array + * of property names; `out` parallel-receives the values (each entry left + * untouched if its property wasn't returned). Returns 0 on transport + * success (even if some properties weren't present), -1 on transport or + * parse failure. */ +static int dbus_get_manager_props(const char *const *wanted, char **out, size_t out_sz) +{ + link_client_t *c; + const link_reply_t *r; + link_reader_t reader; + size_t end; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.freedesktop.DBus.Properties", "GetAll", + "s", "org.finit.Manager1"); + if (rc != LINK_CALL_OK) { + link_client_close(c); + return -1; + } + + r = link_client_reply(c); + if (!r || !r->body) { + link_client_close(c); + return -1; + } + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) { + link_client_close(c); + return -1; + } + + while (link_r_pos(&reader) < end) { + const char *key = NULL; + const char *val = NULL; + char type; + size_t i; + + if (dbus_dict_next(&reader, &key, &type) < 0) + goto fail; + if (type != 's' || link_r_string(&reader, &val) < 0) + goto fail; + + for (i = 0; wanted[i]; i++) { + if (!strcmp(key, wanted[i])) { + strlcpy(out[i], val, out_sz); + break; + } + } + } + + link_client_close(c); + return 0; +fail: + link_client_close(c); + return -1; +} + +#endif + static int do_runlevel(char *arg) { struct init_request rq = { @@ -205,6 +301,19 @@ static int do_runlevel(char *arg) int currlevel; char prev, curr; +#ifdef HAVE_DBUS + char curr_buf[16] = { 0 }, prev_buf[16] = { 0 }; + const char *const wanted[] = { "Runlevel", "PrevRunlevel", NULL }; + char *out[] = { curr_buf, prev_buf }; + + if (dbus_get_manager_props(wanted, out, sizeof(curr_buf)) == 0 && + curr_buf[0] && prev_buf[0]) { + /* already in runlevel(8) encoding: digits, S, N */ + printf("%s %s\n", prev_buf, curr_buf); + return 0; + } +#endif + currlevel = runlevel_get(&prevlevel); switch (currlevel) { case 255: @@ -268,19 +377,279 @@ static int do_startstop(int cmd, char *arg) return do_svc(cmd, arg); } -static int do_start (char *arg) { return do_startstop(INIT_CMD_START_SVC, arg); } -static int do_stop (char *arg) { return do_startstop(INIT_CMD_STOP_SVC, arg); } +#ifdef HAVE_DBUS + +/* Map a LINK_CALL_ERROR reply on `c` to the appropriate ERRX exit: + * org.finit.Error.NoSuchService -> exit 69 (legacy "no such svc") + * org.freedesktop.DBus.Error.AccessDenied -> exit 1 (permission denied) + * anything else -> exit 1 (method: err) + * `c` is closed before exit either way. Use exact-match on the + * fully-qualified error name; a substring match would misfire on a + * future name that contained one of these as a prefix. */ +static void map_dbus_err(link_client_t *c, const char *method, const char *ident) +{ + const link_reply_t *r = link_client_reply(c); + char err[128]; + + /* The reply view points into c->rxbuf; copy the error name out + * before link_client_close() frees the client. Otherwise the + * strcmps below read freed memory. */ + if (r && r->error_name) + strlcpy(err, r->error_name, sizeof(err)); + else + err[0] = '\0'; + link_client_close(c); + + if (!strcmp(err, "org.finit.Error.NoSuchService")) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", + ident ? ident : ""); + if (!strcmp(err, "org.freedesktop.DBus.Error.AccessDenied")) + ERRX(1, "permission denied: %s requires root", method); + ERRX(1, "%s: %s", method, *err ? err : "D-Bus error"); +} + +/* Try the D-Bus path for a Manager1 method. Returns: + * 0 succeeded via D-Bus + * -1 D-Bus not reachable -- callers should fall back to the + * legacy INIT_SOCKET transport + * LINK_CALL_ERROR is handled internally via map_dbus_err (does not + * return). */ +static int try_dbus_manager(const char *method, const char *arg_sig, + const char *arg) +{ + link_client_t *c; + int rc; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + /* "s" methods take the service identity (arg may be NULL -> + * empty string); void methods pass no body. */ + if (arg_sig && !strcmp(arg_sig, "s")) + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", method, + "s", arg ? arg : ""); + else if (!arg_sig || !*arg_sig) + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", method, NULL); + else + rc = LINK_CALL_FAIL; + + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, arg); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; +} + +/* Build the object path for a service identity, same encoding as the + * server side; use instead of a Manager1.GetService round-trip. */ +static int dbus_svc_path(const char *ident, char *path, size_t len) +{ + const char *prefix = "/org/finit/service/"; + size_t plen = strlen(prefix); + + if (!ident || !*ident || plen >= len) + return -1; + memcpy(path, prefix, plen); + return link_path_encode(ident, path + plen, len - plen); +} + +/* Call a void-arg method on Service1 at /org/finit/service/. + * Same return convention as try_dbus_manager. */ +static int try_dbus_service(const char *method, const char *ident) +{ + char path[256]; + link_client_t *c; + int rc; + + if (dbus_svc_path(ident, path, sizeof(path)) < 0) + return -1; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, path, "org.finit.Service1", method, NULL); + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, method, ident); /* exits */ + link_client_close(c); + return (rc == LINK_CALL_OK) ? 0 : -1; +} + +/* 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 char *state = NULL; + + link_reply_get_string(link_client_reply(*bus), &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) +{ +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Start", "s", arg); + if (rc >= 0) return rc; +#endif + return do_startstop(INIT_CMD_START_SVC, arg); +} + +static int do_stop (char *arg) +{ +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Stop", "s", arg); + if (rc >= 0) return rc; +#endif + return do_startstop(INIT_CMD_STOP_SVC, arg); +} static int do_reload (char *arg) { - if (!arg || !arg[0]) + if (!arg || !arg[0]) { +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Reload", "", NULL); + if (rc >= 0) return rc; +#endif return do_svc(INIT_CMD_RELOAD, NULL); + } +#ifdef HAVE_DBUS + { + int rc = try_dbus_service("Reload", arg); + if (rc >= 0) return rc; + } +#endif return do_startstop(INIT_CMD_RELOAD_SVC, arg); } static int do_restart(char *arg) { +#ifdef HAVE_DBUS + int rc = try_dbus_manager("Restart", "s", arg); + if (rc == 0) return 0; + if (rc == 1) ERRX(noerr ? 0 : 7, "failed restarting %s", arg); +#endif if (do_startstop(INIT_CMD_RESTART_SVC, arg)) ERRX(noerr ? 0 : 7, "failed restarting %s", arg); @@ -308,10 +677,6 @@ int do_signal(int argc, char *argv[]) if (argc != 2) ERRX(2, "invalid number of arguments to signal"); - strlcpy(rq.data, argv[0], sizeof(rq.data)); - if (client_send(&rq, sizeof(rq))) - ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); - signo = str2sig(argv[1]); if (signo == -1) { const char *errstr = NULL; @@ -321,6 +686,29 @@ int do_signal(int argc, char *argv[]) ERRX(65, "%s signal: %s", errstr, argv[1]); } +#ifdef HAVE_DBUS + { + link_client_t *c = link_client_open(FINIT_BUS_SOCKET); + + if (c) { + int rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "Signal", + "su", argv[0], (uint32_t)signo); + + if (rc == LINK_CALL_ERROR) + map_dbus_err(c, "Signal", argv[0]); /* exits */ + link_client_close(c); + if (rc == LINK_CALL_OK) + return 0; + /* LINK_CALL_FAIL: drop to legacy */ + } + } +#endif + + strlcpy(rq.data, argv[0], sizeof(rq.data)); + if (client_send(&rq, sizeof(rq))) + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", argv[0]); + /* Reuse runlevel for signal number. */ rq.magic = INIT_MAGIC; rq.cmd = INIT_CMD_SIGNAL; @@ -410,8 +798,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; @@ -441,6 +827,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)"); @@ -460,6 +849,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 @@ -496,6 +901,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; } @@ -503,16 +912,16 @@ static int do_cond_get(char *arg) { return do_cond_act(arg, COND_GET); } static int do_cond_set(char *arg) { return do_cond_act(arg, COND_SET); } static int do_cond_clr(char *arg) { return do_cond_act(arg, COND_CLR); } -static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) +static char *cond_string(const char *condstr, char *buf, size_t len, int ansi) { char *cond, *conds; buf[0] = 0; - if (!svc->cond[0]) + if (!condstr || !condstr[0]) return buf; - conds = strdupa(svc->cond); + conds = strdupa(condstr); if (!conds) return buf; @@ -564,6 +973,11 @@ static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) return buf; } +static char *svc_cond(svc_t *svc, char *buf, size_t len, int ansi) +{ + return cond_string(svc->cond, buf, len, ansi); +} + static int do_cond_show(char *arg) { enum cond_state cond; @@ -643,10 +1057,57 @@ static int do_cmd(int cmd) return 0; } -int do_reboot (char *arg) { return do_cmd(INIT_CMD_REBOOT); } -int do_halt (char *arg) { return do_cmd(INIT_CMD_HALT); } -int do_poweroff(char *arg) { return do_cmd(INIT_CMD_POWEROFF); } -int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); } +#ifdef HAVE_DBUS +static int do_reboot_dbus(const char *method) +{ + int rc = try_dbus_manager(method, "", NULL); + + if (rc == 0) { + sleep(5); /* match legacy: wait for finit to shut down */ + return 0; + } + return rc; /* 1 = error, -1 = fall back */ +} +#endif + +int do_reboot (char *arg) +{ +#ifdef HAVE_DBUS + int rc = do_reboot_dbus("Reboot"); + if (rc >= 0) return rc; +#endif + return do_cmd(INIT_CMD_REBOOT); +} + +int do_halt (char *arg) +{ +#ifdef HAVE_DBUS + int rc = do_reboot_dbus("Halt"); + if (rc >= 0) return rc; +#endif + return do_cmd(INIT_CMD_HALT); +} + +int do_poweroff(char *arg) +{ +#ifdef HAVE_DBUS + int rc = do_reboot_dbus("Poweroff"); + if (rc >= 0) return rc; +#endif + return do_cmd(INIT_CMD_POWEROFF); +} + +int do_suspend(char *arg) +{ + (void)arg; +#ifdef HAVE_DBUS + { + int rc = try_dbus_manager("Suspend", "", NULL); + if (rc >= 0) return rc; + } +#endif + return do_cmd(INIT_CMD_SUSPEND); +} /** * do_switch_root - Switch to a new root filesystem (initramfs only) @@ -830,7 +1291,6 @@ char *runlevel_string(int currlevel, int levels) strlcat(lvl, i == INIT_LEVEL ? "S" : l, sizeof(lvl)); - if (!plain && currlevel == i) strlcat(lvl, "\e[0m", sizeof(lvl)); } else { @@ -844,7 +1304,6 @@ char *runlevel_string(int currlevel, int levels) i++; } while (i < INIT_LEVEL); - strlcat(lvl, "]", sizeof(lvl)); return lvl; @@ -876,23 +1335,9 @@ char *runlevel_arr(int levels) return lvl; } -static int missing(svc_t *svc) -{ - if (svc->state == SVC_HALTED_STATE && svc_is_missing(svc)) - return 1; - - return 0; -} - -static char *svc_command(svc_t *svc, char *buf, size_t len, int ansi) +static char *svc_command(svc_t *svc, char *buf, size_t len) { - int bold = missing(svc) && ansi; - - if (whichp(svc->cmd)) - bold = 0; - - strlcpy(buf, bold ? "\e[1m" : "", len); - strlcat(buf, svc->cmd, len); + strlcpy(buf, svc->cmd, len); for (int i = 1; i < MAX_NUM_SVC_ARGS; i++) { if (!svc->args[i][0]) @@ -909,40 +1354,24 @@ static char *svc_command(svc_t *svc, char *buf, size_t len, int ansi) strlcat(buf, cmd, len); } - strlcat(buf, bold ? "\e[0m" : "", len); - return buf; } -static char *svc_environ(svc_t *svc, char *buf, size_t len, int ansi) -{ - int bold = missing(svc); - - if (!ansi || svc_checkenv(svc)) - bold = 0; - - strlcpy(buf, bold ? "\e[1m" : "", len); - strlcat(buf, svc->env, len); - strlcat(buf, bold ? "\e[0m" : "", len); - - return buf; -} - -static char *exit_status(svc_t *svc, char *buf, size_t len) +static char *exit_status_raw(int status, int manual, char *buf, size_t len) { int rc, sig; char *str; - rc = WEXITSTATUS(svc->status); - sig = WTERMSIG(svc->status); + rc = WEXITSTATUS(status); + sig = WTERMSIG(status); - if (WIFEXITED(svc->status)) { + if (WIFEXITED(status)) { str = code2str(rc); snprintf(buf, len, " (code=exited, status=%d%s%s%s)", rc, str[0] ? "/" : "", str, - svc->manual ? ", manual=yes" : ""); + manual ? ", manual=yes" : ""); } - else if (WIFSIGNALED(svc->status)) { + else if (WIFSIGNALED(status)) { str = sig2str(sig); snprintf(buf, len, " (code=signal, status=%d%s%s)", sig, str[0] ? "/" : "", str); } @@ -950,57 +1379,6 @@ static char *exit_status(svc_t *svc, char *buf, size_t len) return buf; } -static char *status(svc_t *svc, int full) -{ - static char buf[96]; - const char *color; - char ok[48] = {0}; - char *s; - - s = svc_status(svc); - switch (svc->state) { - case SVC_HALTED_STATE: - exit_status(svc, ok, sizeof(ok)); - color = "\e[1m"; - break; - - case SVC_RUNNING_STATE: - color = "\e[1;32m"; - break; - - case SVC_DONE_STATE: - exit_status(svc, ok, sizeof(ok)); - if (WIFEXITED(svc->status)) { - if (WEXITSTATUS(svc->status)) - color = "\e[1;31m"; - else - color = "\e[1;32m"; - } else { - if (full && WIFSIGNALED(svc->status)) - color = "\e[1;31m"; - else - color = "\e[1;33m"; - } - break; - - default: - exit_status(svc, ok, sizeof(ok)); - color = "\e[1;33m"; - break; - } - - if (!full || plain) - color = NULL; - - if (!full) - snprintf(buf, sizeof(buf), "%-8.8s", s); - else - snprintf(buf, sizeof(buf), "%s%s%s%s", - color ? color : "", s, ok, color ? "\e[0m" : ""); - - return buf; -} - static void show_cgroup_tree(char *group, char *pfx) { char path[256]; @@ -1038,6 +1416,176 @@ static int svc_compare(svc_t *svc, char *arg) return 0; } +/* + * All fields the status views need, transport-independent: filled + * from a raw svc_t on the legacy socket path or from Service1 + * properties on the D-Bus path, rendered by the same printers. + */ +struct svc_row { + char ident[MAX_IDENT_LEN]; + char state[16]; + char type[16]; + char desc[MAX_STR_LEN]; + char cmdline[512]; + char env[MAX_CMD_LEN]; + char cond[MAX_COND_LEN]; + char pidfile[MAX_CMD_LEN]; + char user[MAX_USER_LEN]; + char grp[MAX_USER_LEN]; + char origin[MAX_ARG_LEN]; + uint32_t pid; + uint32_t runlevels; + uint32_t uptime; /* seconds, 0 when not running */ + uint32_t exitstatus; /* raw waitpid(2) status */ + uint32_t starts; + int32_t restart_tot; + int32_t restart_cnt; + int32_t restart_max; /* -1 = restart:always */ + int manual; + int forking; + int started; +}; + +static void fill_row_from_svc(struct svc_row *r, svc_t *svc) +{ + long now = jiffies(); + + memset(r, 0, sizeof(*r)); + svc_ident(svc, r->ident, sizeof(r->ident)); + strlcpy(r->state, svc_status(svc), sizeof(r->state)); + strlcpy(r->type, svc_typestr(svc), sizeof(r->type)); + strlcpy(r->desc, svc->desc, sizeof(r->desc)); + svc_command(svc, r->cmdline, sizeof(r->cmdline)); + strlcpy(r->env, svc->env, sizeof(r->env)); + strlcpy(r->cond, svc->cond, sizeof(r->cond)); + strlcpy(r->pidfile, svc->pidfile, sizeof(r->pidfile)); + strlcpy(r->user, svc->username, sizeof(r->user)); + strlcpy(r->grp, svc->group, sizeof(r->grp)); + strlcpy(r->origin, svc->file, sizeof(r->origin)); + r->pid = svc->pid > 0 ? (uint32_t)svc->pid : 0; + r->runlevels = (uint32_t)svc->runlevels; + if (svc->pid && now > svc->start_time) + r->uptime = (uint32_t)(now - svc->start_time); + r->exitstatus = (uint32_t)svc->status; + r->starts = (uint32_t)svc->once; + r->restart_tot = (int32_t)svc->restart_tot; + r->restart_cnt = (int32_t)svc->restart_cnt; + r->restart_max = (int32_t)svc->restart_max; + r->manual = svc->manual; + r->forking = svc->forking; + r->started = svc->started; +} + +static const char *pidfile_str(struct svc_row *r) +{ + const char *pidfn = r->pidfile; + + if (pidfn[0] == '!') + pidfn++; + else if (pidfn[0] == 0) + pidfn = "none"; + + return pidfn; +} + +/* string-only twin of svc_checkenv() */ +static int checkenv_str(const char *env) +{ + if (!env || !env[0] || env[0] == '-') + return 1; + + return fexist(env); +} + +/* same matching rules as svc_compare() */ +static int row_compare(struct svc_row *r, char *arg) +{ + char ident[MAX_IDENT_LEN]; + char *ptr; + + strlcpy(ident, r->ident, sizeof(ident)); + ptr = strchr(ident, ':'); + if (ptr && !strchr(arg, ':')) + *ptr = 0; + + return !strcmp(ident, arg); +} + +static int row_missing(struct svc_row *r) +{ + char argv0[512]; + char *sep; + + if (strcmp(r->state, "missing")) + return 0; + + strlcpy(argv0, r->cmdline, sizeof(argv0)); + sep = strchr(argv0, ' '); + if (sep) + *sep = 0; + + return !whichp(argv0); +} + +/* the words svc_status() emits for a blocked/halted service */ +static const char *halted_words[] = { + "halted", "missing", "crashed", "stopped", "busy", + "restart", "conflict", "unknown", NULL +}; + +/* transport-independent status(svc, full) */ +static char *row_status(struct svc_row *r, int full) +{ + static char buf[96]; + const char *color; + char ok[48] = {0}; + int st = (int)r->exitstatus; + size_t i; + + if (!full) { + snprintf(buf, sizeof(buf), "%-8.8s", r->state); + return buf; + } + + if (!strcmp(r->state, "running")) { + color = "\e[1;32m"; + } else if (!strcmp(r->state, "done") || !strcmp(r->state, "failed")) { + exit_status_raw(st, r->manual, ok, sizeof(ok)); + if (WIFEXITED(st)) + color = WEXITSTATUS(st) ? "\e[1;31m" : "\e[1;32m"; + else if (WIFSIGNALED(st)) + color = "\e[1;31m"; + else + color = "\e[1;33m"; + } else { + exit_status_raw(st, r->manual, ok, sizeof(ok)); + color = "\e[1;33m"; + for (i = 0; halted_words[i]; i++) { + if (!strcmp(r->state, halted_words[i])) { + color = "\e[1m"; + break; + } + } + } + + if (plain) + color = NULL; + + snprintf(buf, sizeof(buf), "%s%s%s%s", + color ? color : "", r->state, ok, color ? "\e[0m" : ""); + + return buf; +} + +/* scripting mode: exit code only, same rules as the legacy path */ +static int quiet_row(struct svc_row *r) +{ + if (!strcmp(r->type, "run") || !strcmp(r->type, "task")) + return r->started ? 0 : 1; + + return strcmp(r->state, "running") != 0; +} + /* * Escape a string for safe JSON output. Handles quotes, backslashes, * and control characters. Returns pointer to static buffer. @@ -1094,46 +1642,40 @@ static char *json_escape(const char *str) return buf; } -static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) +static int json_status_one(FILE *fp, struct svc_row *r, char *indent, int prev) { - long now = jiffies(); - char *pidfn = NULL; + const char *pidfn = pidfile_str(r); char buf[512]; - pidfn = svc->pidfile; - if (pidfn[0] == '!') - pidfn++; - else if (pidfn[0] == 0) - pidfn = "none"; - fprintf(fp, "%s" "%s{\n" "%s \"identity\": \"%s\",\n", prev ? ",\n" : indent, prev ? indent : "", - indent, svc_ident(svc, NULL, 0)); + indent, r->ident); fprintf(fp, "%s \"description\": \"%s\",\n", - indent, json_escape(svc->desc)); + indent, json_escape(r->desc)); fprintf(fp, "%s \"type\": \"%s\",\n" "%s \"forking\": %s,\n" "%s \"status\": \"%s\",\n", - indent, svc_typestr(svc), - indent, svc->forking ? "true" : "false", - indent, svc_status(svc)); + indent, r->type, + indent, r->forking ? "true" : "false", + indent, r->state); - if (svc->state != SVC_RUNNING_STATE) { + if (strcmp(r->state, "running")) { + int st = (int)r->exitstatus; int rc, sig; - rc = WEXITSTATUS(svc->status); - sig = WTERMSIG(svc->status); + rc = WEXITSTATUS(st); + sig = WTERMSIG(st); - if (WIFEXITED(svc->status)) + if (WIFEXITED(st)) fprintf(fp, "%s \"exit\": { \"%s\": %d },\n", indent, "code", rc); - else if (WIFSIGNALED(svc->status)) + else if (WIFSIGNALED(st)) fprintf(fp, "%s \"exit\": { \"%s\": %d },\n", indent, "signal", sig); @@ -1141,32 +1683,30 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) fprintf(fp, "%s \"origin\": \"%s\",\n", - indent, svc->file[0] ? svc->file : "built-in"); - svc_command(svc, buf, sizeof(buf), 0); + indent, r->origin[0] ? r->origin : "built-in"); fprintf(fp, "%s \"command\": \"%s\",\n", - indent, json_escape(buf)); + indent, json_escape(r->cmdline)); - svc_environ(svc, buf, sizeof(buf), 0); - if (buf[0]) + if (r->env[0]) fprintf(fp, - "%s \"environment\": \"%s\",\n", indent, json_escape(buf)); + "%s \"environment\": \"%s\",\n", indent, json_escape(r->env)); - svc_cond(svc, buf, sizeof(buf), 0); + cond_string(r->cond, buf, sizeof(buf), 0); if (buf[0]) fprintf(fp, "%s \"condition\": %s,\n", indent, buf); - if (svc->manual) + if (r->manual) fprintf(fp, - "%s \"starts\": %d,\n", indent, svc->once); + "%s \"starts\": %u,\n", indent, r->starts); fprintf(fp, - "%s \"restarts\": %d,\n", indent, svc->restart_tot); /* XXX: add restart_cnt and restart_max */ + "%s \"restarts\": %d,\n", indent, r->restart_tot); /* XXX: add restart_cnt and restart_max */ /* Add memory and CPU information if cgroup support is available */ - if (cgrp && svc->pid > 1) { - char *group = pid_cgroup(svc->pid); + if (cgrp && r->pid > 1) { + char *group = pid_cgroup(r->pid); if (group) { uint64_t throttled_usec = 0; @@ -1270,37 +1810,475 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev) fprintf(fp, "%s \"pidfile\": \"%s\",\n" - "%s \"pid\": %d,\n" + "%s \"pid\": %u,\n" "%s \"user\": \"%s\",\n" "%s \"group\": \"%s\",\n" "%s \"uptime\": %ld,\n" "%s \"runlevels\": %s\n" "%s}", indent, pidfn, - indent, svc->pid, - indent, svc->username, - indent, svc->group, - indent, svc->pid ? now - svc->start_time : 0, - indent, runlevel_arr(svc->runlevels), + indent, r->pid, + indent, r->user, + indent, r->grp, + indent, (long)r->uptime, + indent, runlevel_arr((int)r->runlevels), indent); return 0; } -static int show_status(char *arg) +static void status_heading(int pidw, int identw) { - char ident[MAX_IDENT_LEN]; + char title[80]; + + snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", + pidw, "PID", identw, "IDENT", "STATUS", "RUNLEVELS"); + strlcat(title, !verbose ? "DESCRIPTION" : "COMMAND", sizeof(title)); + print_header("%s", title); +} + +static void print_runlevels(const char *lvls) +{ + /* ANSI escapes for the active level eat into the field width */ + if (strchr(lvls, '\e')) + printf("%-21.21s ", lvls); + else + printf("%-13.13s ", lvls); +} + +/* + * The detail view, shared by the legacy and D-Bus paths; the caller + * fills a svc_row from its transport first. Cgroup statistics and + * the log tail are always local operations, they only need the PID. + */ +static int show_one_row(struct svc_row *r) +{ + char uptm[42] = "N/A"; char buf[512]; + int bold; + + printf(" Status : %s\n", row_status(r, 1)); + printf(" Identity : %s\n", r->ident); + printf("Description : %s\n", r->desc); + printf(" Origin : %s\n", r->origin[0] ? r->origin : "built-in"); + + if (r->env[0]) { + bold = !plain && !strcmp(r->state, "missing") && + !checkenv_str(r->env); + printf("Environment : %s%s%s\n", bold ? "\e[1m" : "", + r->env, bold ? "\e[0m" : ""); + } + + cond_string(r->cond, buf, sizeof(buf), !plain); + if (buf[0]) + printf("Condition(s): <%s>\n", buf); + + bold = !plain && row_missing(r); + printf(" Command : %s%s%s\n", bold ? "\e[1m" : "", + r->cmdline, bold ? "\e[0m" : ""); + printf(" PID file : %s\n", pidfile_str(r)); + printf(" PID : %u\n", r->pid); + printf(" User : %s\n", r->user); + printf(" Group : %s\n", r->grp); + printf(" Uptime : %s\n", r->pid + ? uptime((long)r->uptime, uptm, sizeof(uptm)) : uptm); + if (r->manual) + printf(" Starts : %u\n", r->starts); + printf(" Restarts : %d (%d/%d)\n", + r->restart_tot, r->restart_cnt, r->restart_max); + printf(" Runlevels : %s\n", + runlevel_string(runlevel, (int)r->runlevels)); + + if (cgrp && r->pid > 1) { + const struct cg *cg; + uint64_t throttled_usec = 0; + uint64_t nr_throttled = 0; + char path[256]; + char *group; + + group = pid_cgroup(r->pid); + if (!group) + goto no_cgroup; /* ... or PID doesn't exist (anymore) */ + + snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group); + cg = cg_conf(path); + + printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm))); + + if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) { + printf("CPU Throttle : %lu usec (%lu times)\n", + throttled_usec, nr_throttled); + } + + printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n", + group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max, + cg->cg_mem.min, cg->cg_mem.max); + show_cgroup_tree(group, " "); + + free(group); + } +no_cgroup: + printf("\n"); + + return do_log_named(r->ident, r->pid, "| tail -10"); +} + +static void render_table(struct svc_row *rows, int n, char *filter) +{ + int i, identw = 0, pidw = 0; + + for (i = 0; i < n; i++) { + int w; + + w = (int)strlen(rows[i].ident); + if (w > identw) + identw = w; + w = snprintf(NULL, 0, "%u", rows[i].pid); + if (w > pidw) + pidw = w; + } + if (identw < 6) + identw = 6; + if (pidw < 3) + pidw = 3; + + if (heading) + status_heading(pidw, identw); + + for (i = 0; i < n; i++) { + struct svc_row *r = &rows[i]; + + if (filter && !row_compare(r, filter)) + continue; + + printf("%-*u ", pidw, r->pid); + printf("%-*s %s ", identw, r->ident, row_status(r, 0)); + print_runlevels(runlevel_string(runlevel, (int)r->runlevels)); + + if (!verbose) + puts(r->desc); + else { + int bold = !plain && row_missing(r); + + printf("%s%s%s\n", bold ? "\e[1m" : "", + r->cmdline, bold ? "\e[0m" : ""); + } + } +} + +#ifdef HAVE_DBUS +/* + * Service1 wire properties -> svc_row fields. Unknown keys and + * value types are skipped so newer finit keeps working with older + * initctl. + */ +#define ROW_STR(k, f) { k, 's', offsetof(struct svc_row, f), \ + sizeof(((struct svc_row *)0)->f) } +#define ROW_U32(k, f) { k, 'u', offsetof(struct svc_row, f), 0 } +#define ROW_BOOL(k, f) { k, 'b', offsetof(struct svc_row, f), 0 } + +static const struct row_field { + const char *key; + char type; + size_t off; + size_t len; +} row_fields[] = { + ROW_STR ("State", state), + ROW_STR ("Type", type), + ROW_STR ("Description", desc), + ROW_STR ("Command", cmdline), + ROW_STR ("Environment", env), + ROW_STR ("Conditions", cond), + ROW_STR ("PidFile", pidfile), + ROW_STR ("User", user), + ROW_STR ("Group", grp), + ROW_STR ("Origin", origin), + ROW_U32 ("Pid", pid), + ROW_U32 ("Runlevels", runlevels), + ROW_U32 ("Uptime", uptime), + ROW_U32 ("ExitStatus", exitstatus), + ROW_U32 ("Starts", starts), + ROW_U32 ("RestartsTotal", restart_tot), + ROW_U32 ("RestartCount", restart_cnt), + ROW_U32 ("RestartMax", restart_max), + ROW_BOOL("ManualStart", manual), + ROW_BOOL("Forking", forking), + ROW_BOOL("Started", started), + { NULL, 0, 0, 0 } +}; + +/* + * One service's Properties.GetAll into a row. Returns 0 on success, + * 1 when the object vanished since ListServices (bus error reply), + * -1 on transport failure. + */ +static int dbus_fill_row(link_client_t *c, struct svc_row *row) +{ + const link_reply_t *r; + link_reader_t reader; + char path[256]; + size_t end; + int rc; + + if (dbus_svc_path(row->ident, path, sizeof(path)) < 0) + return -1; + + rc = link_client_call_v(c, path, + "org.freedesktop.DBus.Properties", + "GetAll", "s", "org.finit.Service1"); + if (rc == LINK_CALL_ERROR) + return 1; /* gone since ListServices */ + if (rc != LINK_CALL_OK) + return -1; + r = link_client_reply(c); + if (!r || !r->body) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) + return -1; + + while (link_r_pos(&reader) < end) { + const struct row_field *f; + const char *key, *val; + uint32_t u; + int b; + char type; + + if (dbus_dict_next(&reader, &key, &type) < 0) + return -1; + + for (f = row_fields; f->key; f++) + if (f->type == type && !strcmp(f->key, key)) + break; + + if (!f->key) { + if (link_r_skip_basic(&reader, type) < 0) + return -1; + continue; + } + + switch (type) { + case 's': + if (link_r_string(&reader, &val) < 0) + return -1; + strlcpy((char *)row + f->off, val, f->len); + break; + case 'u': + if (link_r_u32(&reader, &u) < 0) + return -1; + memcpy((char *)row + f->off, &u, sizeof(u)); + break; + case 'b': + if (link_r_bool(&reader, &b) < 0) + return -1; + memcpy((char *)row + f->off, &b, sizeof(b)); + break; + } + } + + return 0; +} + +/* + * ListServices into a caller-freed ident array. One round trip; + * show_ident() needs nothing more. Returns count or -1. + */ +static int dbus_list_idents(link_client_t *c, char (**out)[MAX_IDENT_LEN]) +{ + char (*idents)[MAX_IDENT_LEN] = NULL, (*tmp)[MAX_IDENT_LEN]; + const link_reply_t *r; + link_reader_t reader; + size_t n = 0, end; + int rc; + + rc = link_client_call_v(c, "/org/finit/manager", + "org.finit.Manager1", "ListServices", NULL); + if (rc != LINK_CALL_OK) + return -1; + + r = link_client_reply(c); + if (!r || !r->body) + return -1; + + link_reader_init(&reader, r->body, r->body_len); + if (link_r_array_begin(&reader, &end) < 0) + return -1; + + while (link_r_pos(&reader) < end) { + const char *ident; + + if (link_r_string(&reader, &ident) < 0) + goto fail; + tmp = realloc(idents, (n + 1) * sizeof(*idents)); + if (!tmp) + goto fail; + idents = tmp; + /* copied: the next call clobbers the client rx buffer */ + strlcpy(idents[n], ident, sizeof(idents[n])); + n++; + } + + *out = idents; + return (int)n; +fail: + free(idents); + return -1; +} + +/* + * Rows for every service matching `arg` (all when NULL), fetched over + * D-Bus. Returns the row count (caller frees *out), or -1 to fall + * back to the legacy socket. + */ +static int dbus_fetch_svc_rows(char *arg, struct svc_row **out) +{ + char (*idents)[MAX_IDENT_LEN] = NULL; + struct svc_row *rows = NULL; + link_client_t *c; + int n, i, m = 0; + + c = link_client_open(FINIT_BUS_SOCKET); + if (!c) + return -1; + + n = dbus_list_idents(c, &idents); + if (n < 0) + goto fail; + + rows = calloc(n ? n : 1, sizeof(*rows)); + if (!rows) + goto fail; + + for (i = 0; i < n; i++) { + struct svc_row *row = &rows[m]; + int rc; + + strlcpy(row->ident, idents[i], sizeof(row->ident)); + if (arg && arg[0] && !row_compare(row, arg)) + continue; + + rc = dbus_fill_row(c, row); + if (rc < 0) + goto fail; + if (rc > 0) + continue; /* vanished, skip */ + m++; + } + + free(idents); + link_client_close(c); + *out = rows; + return m; +fail: + free(idents); + free(rows); + link_client_close(c); + return -1; +} + +/* wire encoding is runlevel(8) style: digits, S, N */ +static int runlevel_from_str(const char *s) +{ + if (!strcmp(s, "S")) + return INIT_LEVEL; + + return atoi(s); +} + +/* + * All show_status() views over D-Bus, self-contained (the current + * runlevel comes from Manager1, not the legacy socket). Returns 0 + * with the command exit code in *retval, or -1 to fall back. + */ +static int dbus_show_status(char *arg, int *retval) +{ + static const char *const wanted[] = { "Runlevel", NULL }; + char curr[16] = "0"; + char *outv[] = { curr }; + struct svc_row *rows = NULL; + char *filter; + int n, i; + + n = dbus_fetch_svc_rows(arg, &rows); + if (n < 0) + return -1; + + *retval = 0; + + if (arg && arg[0]) { + if (!n) { + free(rows); + /* exits, exactly like the legacy path */ + ERRX(noerr ? 0 : 69, "no such task or service(s): %s", arg); + } + if (n == 1) { + if (quiet) { + *retval = quiet_row(&rows[0]); + free(rows); + return 0; + } + /* runlevel feeds the detail view's Runlevels line */ + if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) + goto fail; + runlevel = runlevel_from_str(curr); + if (json) { + *retval = json_status_one(stdout, &rows[0], "", 0); + puts(""); + } else + *retval = show_one_row(&rows[0]); + free(rows); + return 0; + } + /* several matches: filtered table below */ + } + + if (dbus_get_manager_props(wanted, outv, sizeof(curr)) < 0) + goto fail; + runlevel = runlevel_from_str(curr); + + filter = (arg && arg[0]) ? arg : NULL; + if (json) { + int prev = 0; + + for (i = 0; i < n; i++) { + if (filter && !row_compare(&rows[i], filter)) + continue; + if (!prev) + fputs("[\n", stdout); + json_status_one(stdout, &rows[i], " ", prev++); + } + if (prev) + fputs("\n]\n", stdout); + } else + render_table(rows, n, filter); + + free(rows); + return 0; +fail: + free(rows); + return -1; +} +#endif /* HAVE_DBUS */ + +static int show_status(char *arg) +{ + struct svc_row row; int num = 0; svc_t *svc; +#ifdef HAVE_DBUS + { + int rc; + + if (dbus_show_status(arg, &rc) == 0) + return rc; + } +#endif + runlevel = runlevel_get(NULL); while (arg && arg[0]) { - long now = jiffies(); - char uptm[42] = "N/A"; - char *pidfn = NULL; - for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) num += svc_compare(svc, arg); @@ -1311,95 +2289,32 @@ static int show_status(char *arg) if (!svc) ERRX(noerr ? 0 : 69, "no such task or service(s): %s", arg); - if (quiet) { - if (svc_is_runtask(svc)) { - if (svc->started) - return 0; - return 1; - } - return svc->state != SVC_RUNNING_STATE; - } + fill_row_from_svc(&row, svc); + if (quiet) + return quiet_row(&row); if (json) { int rc; - rc = json_status_one(stdout, svc, "", 0); + rc = json_status_one(stdout, &row, "", 0); puts(""); return rc; } - pidfn = svc->pidfile; - if (pidfn[0] == '!') - pidfn++; - else if (pidfn[0] == 0) - pidfn = "none"; - - printf(" Status : %s\n", status(svc, 1)); - printf(" Identity : %s\n", svc_ident(svc, ident, sizeof(ident))); - printf("Description : %s\n", svc->desc); - printf(" Origin : %s\n", svc->file[0] ? svc->file : "built-in"); - svc_environ(svc, buf, sizeof(buf), !plain); - if (buf[0]) - printf("Environment : %s\n", buf); - svc_cond(svc, buf, sizeof(buf), !plain); - if (buf[0]) - printf("Condition(s): <%s>\n", buf); - printf(" Command : %s\n", svc_command(svc, buf, sizeof(buf), !plain)); - printf(" PID file : %s\n", pidfn); - printf(" PID : %d\n", svc->pid); - printf(" User : %s\n", svc->username); - printf(" Group : %s\n", svc->group); - printf(" Uptime : %s\n", svc->pid ? uptime(now - svc->start_time, uptm, sizeof(uptm)) : uptm); - if (svc->manual) - printf(" Starts : %d\n", svc->once); - printf(" Restarts : %d (%d/%d)\n", svc->restart_tot, svc->restart_cnt, svc->restart_max); - printf(" Runlevels : %s\n", runlevel_string(runlevel, svc->runlevels)); - - if (cgrp && svc->pid > 1) { - const struct cg *cg; - uint64_t throttled_usec = 0; - uint64_t nr_throttled = 0; - char path[256]; - char *group; - - group = pid_cgroup(svc->pid); - if (!group) - goto no_cgroup; /* ... or PID doesn't exist (anymore) */ - - snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group); - cg = cg_conf(path); - - printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm))); - - if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) { - printf("CPU Throttle : %lu usec (%lu times)\n", - throttled_usec, nr_throttled); - } - - printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n", - group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max, - cg->cg_mem.min, cg->cg_mem.max); - show_cgroup_tree(group, " "); - - free(group); - } - no_cgroup: - printf("\n"); - - return do_log(svc, "| tail -10"); + return show_one_row(&row); } if (json) { int prev = 0; for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { - svc_ident(svc, ident, sizeof(ident)); if (num && !svc_compare(svc, arg)) continue; if (!prev) fputs("[\n", stdout); - json_status_one(stdout, svc, " ", prev++); + fill_row_from_svc(&row, svc); + json_status_one(stdout, &row, " ", prev++); } if (prev) fputs("\n]\n", stdout); @@ -1407,64 +2322,71 @@ static int show_status(char *arg) return 0; } - col_widths(); - if (heading) { - char title[80]; + { + struct svc_row *rows = NULL, *tmp; + int n = 0; - snprintf(title, sizeof(title), "%-*s %-*s %-8s %-13s ", - pw, "PID", iw, "IDENT", "STATUS", "RUNLEVELS"); - if (!verbose) - strlcat(title, "DESCRIPTION", sizeof(title)); - else - strlcat(title, "COMMAND", sizeof(title)); - - print_header("%s", title); + for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { + tmp = realloc(rows, (n + 1) * sizeof(*rows)); + if (!tmp) { + free(rows); + return 1; + } + rows = tmp; + fill_row_from_svc(&rows[n++], svc); + } + render_table(rows, n, num > 1 ? arg : NULL); + free(rows); } - for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { - char *lvls; - - svc_ident(svc, ident, sizeof(ident)); - if (num && !svc_compare(svc, arg)) - continue; - - printf("%-*d ", pw, svc->pid); - printf("%-*s %s ", iw, ident, status(svc, 0)); + return 0; +} - lvls = runlevel_string(runlevel, svc->runlevels); - if (strchr(lvls, '\e')) - printf("%-21.21s ", lvls); - else - printf("%-13.13s ", lvls); +static void ident_line(const char *identity, char *arg) +{ + size_t len; + char *pos; - if (!verbose) - puts(svc->desc); - else - puts(svc_command(svc, buf, sizeof(buf), !plain)); - } + pos = strchr(identity, ':'); + if (pos) + len = (size_t)(pos - identity); + else + len = strlen(identity); + if (arg && arg[0] && strncasecmp(identity, arg, len)) + return; - return 0; + puts(identity); } static int show_ident(char *arg) { svc_t *svc; +#ifdef HAVE_DBUS + { + char (*idents)[MAX_IDENT_LEN]; + link_client_t *c; + int n = -1, i; + + c = link_client_open(FINIT_BUS_SOCKET); + if (c) { + n = dbus_list_idents(c, &idents); + link_client_close(c); + } + if (n >= 0) { + for (i = 0; i < n; i++) + ident_line(idents[i], arg); + free(idents); + return 0; + } + } +#endif + for (svc = client_svc_iterator(1); svc; svc = client_svc_iterator(0)) { char ident[MAX_IDENT_LEN]; - size_t len; - char *pos; svc_ident(svc, ident, sizeof(ident)); - pos = strchr(ident, ':'); - if (pos) - len = pos - ident; - else - len = strlen(ident); - if (arg && arg[0] && strncasecmp(ident, arg, len)) - continue; - - puts(ident); + ident_line(ident, arg); } return 0; @@ -1572,6 +2494,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"); @@ -1756,6 +2681,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/src/private.h b/src/private.h index 1612803b..ae501161 100644 --- a/src/private.h +++ b/src/private.h @@ -45,6 +45,16 @@ extern uev_ctx_t *ctx; int api_init (uev_ctx_t *ctx); int api_exit (void); + +#ifdef HAVE_DBUS +int dbus_init (uev_ctx_t *ctx); +int dbus_exit (void); +void dbus_register_service (svc_t *svc); +void dbus_unregister_service (svc_t *svc); +void dbus_notify_service_state (svc_t *svc, int old_state, int new_state); +void dbus_notify_condition_change(const char *name, const char *state); +void dbus_notify_runlevel_change(int old_level, int new_level); +#endif void conf_flush_events(void); void service_monitor (pid_t lost, int status); diff --git a/src/service.c b/src/service.c index 55f3b858..3c2e05f4 100644 --- a/src/service.c +++ b/src/service.c @@ -520,7 +520,7 @@ static int is_norespawn(void) fexist("/tmp/norespawn"); } -static void compose_cmdline(svc_t *svc, char *buf, size_t len) +void compose_cmdline(svc_t *svc, char *buf, size_t len) { size_t i; @@ -1443,17 +1443,65 @@ int service_stop(svc_t *svc) } /** - * service_reload - Reload a service + * service_reload - Request reload of a service, driven by the state machine * @svc: Service to reload * - * This function does some basic checks of the runtime state of Finit - * and a sanity check of the @svc before sending %SIGHUP or calling - * the reload:script command. + * Mark a service dirty so the state machine will (re-)apply its + * configuration, then advance the state machine. For services that + * don't handle SIGHUP this also clears the readiness condition and + * the pidfile/none-notify readiness flag so dependents are properly + * notified once the service comes back. + * + * Returns: + * POSIX OK(0) on success, non-zero if @svc is NULL. + */ +int service_reload(svc_t *svc) +{ + if (!svc) + return 1; + + if (svc_is_blocked(svc)) + svc_start(svc); + else + service_timeout_cancel(svc); + + /* + * Clear conditions before reload to ensure dependent services + * are properly updated. Only needed when the service does NOT + * support SIGHUP (noreload), because then it will be stopped + * and restarted, so conditions genuinely go away. When the + * service handles SIGHUP, its PID and pidfile persist, so the + * condition stays valid and dependents should not be disrupted. + * + * Note: only clear 'ready' for services where the pidfile + * inotify handler reasserts it (pid/none). For s6/systemd + * services readiness relies on their respective notification + * mechanism which may not re-trigger on SIGHUP. + */ + if (svc_is_noreload(svc)) { + svc_cond_clear(svc); + if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) + service_ready(svc, 0); + } + + svc_mark_dirty(svc); + service_step(svc); + + return 0; +} + +/** + * service_reload_apply - Perform the actual reload of a running service + * @svc: Service to reload + * + * Low-level reload step: sends %SIGHUP or runs the reload:script + * command. Called by the state machine when a service marked + * reload-pending by service_reload() reaches the right state. * * Returns: * POSIX OK(0) or non-zero on error. */ -static int service_reload(svc_t *svc) +static int service_reload_apply(svc_t *svc) { const char *id = svc_ident(svc, NULL, 0); int do_progress = 1; @@ -3015,6 +3063,10 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state) return; *state = new_state; +#ifdef HAVE_DBUS + dbus_notify_service_state(svc, old_state, new_state); +#endif + /* * The unit has stopped: HALTED comes after any post:/cleanup: * script, DONE is a completed run/task, where remain-after-exit @@ -3384,7 +3436,7 @@ int service_step(svc_t *svc) if (sm_in_reload()) break; - service_reload(svc); + service_reload_apply(svc); } svc_mark_clean(svc); @@ -3422,7 +3474,7 @@ int service_step(svc_t *svc) if (svc_is_noreload(svc)) service_stop(svc); else - service_reload(svc); + service_reload_apply(svc); break; } diff --git a/src/service.h b/src/service.h index f8b94fe5..59699f3f 100644 --- a/src/service.h +++ b/src/service.h @@ -54,11 +54,13 @@ void service_ready_script (svc_t *svc); /* XXX: only for pidfile plugin b int service_timeout_after (svc_t *svc, int timeout, void (*cb)(svc_t *svc)); int service_timeout_cancel (svc_t *svc); +void compose_cmdline (svc_t *svc, char *buf, size_t len); void service_forked (svc_t *svc); void service_ready (svc_t *svc, int ready); int service_stop (svc_t *svc); +int service_reload (svc_t *svc); int service_step (svc_t *svc); void service_step_all (int types); void service_worker (void *unused); diff --git a/src/sm.c b/src/sm.c index ab3abdb9..44d0972d 100644 --- a/src/sm.c +++ b/src/sm.c @@ -388,6 +388,9 @@ void sm_step(void) prevlevel = runlevel; runlevel = sm.newlevel; sm.newlevel = -1; +#ifdef HAVE_DBUS + dbus_notify_runlevel_change(prevlevel, runlevel); +#endif /* Restore terse mode and run hooks before shutdown */ if (runlevel == 0 || runlevel == 6) { diff --git a/src/svc.c b/src/svc.c index cbe29cb5..a1fe9c99 100644 --- a/src/svc.c +++ b/src/svc.c @@ -38,6 +38,7 @@ #include "finit.h" #include "conf.h" +#include "private.h" #include "svc.h" #include "helpers.h" #include "pid.h" @@ -153,6 +154,10 @@ svc_t *svc_new(char *cmd, char *name, char *id, int type) TAILQ_INSERT_TAIL(&svc_list, svc, link); +#ifdef HAVE_DBUS + dbus_register_service(svc); +#endif + return svc; } @@ -170,6 +175,10 @@ static struct wq work = { */ int svc_del(svc_t *svc) { +#ifdef HAVE_DBUS + dbus_unregister_service(svc); +#endif + TAILQ_REMOVE(&svc_list, svc, link); TAILQ_INSERT_TAIL(&gc_list, svc, link); diff --git a/system/20-dbus.conf b/system/20-dbus.conf new file mode 100644 index 00000000..f68f23ab --- /dev/null +++ b/system/20-dbus.conf @@ -0,0 +1,19 @@ +# System message bus, D-Bus +# +# The dbus.so plugin creates the directories dbus-daemon needs and makes +# sure the machine has a UUID, but the daemon itself is declared here so +# you can change it. The leading '-' says it is fine if dbus-daemon is +# not installed; the service is then skipped without a warning. +# +# Finit registers org.finit on this bus once it is up, see docs/dbus.md. +# +# Override this file by copying it to /etc/finit.d/, using the same name, +# then change the contents any way you like, it can even be empty. + +service dbus { + description = "D-Bus message bus daemon" + runlevel = "S123456789" + notify = "none" + cgroup system {} + command = "-dbus-daemon --nofork --system --syslog-only" +} diff --git a/system/Makefile.am b/system/Makefile.am index 5dac235b..284aebf9 100644 --- a/system/Makefile.am +++ b/system/Makefile.am @@ -7,6 +7,10 @@ if BUILD_HOTPLUG_PLUGIN system_DATA += 10-hotplug.conf endif +if BUILD_DBUS_PLUGIN +dist_system_DATA += 20-dbus.conf +endif + if BUILD_TESTSERV_PLUGIN dist_system_DATA += 90-testserv.conf endif diff --git a/test/Makefile.am b/test/Makefile.am index a12c296b..82f80bb7 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -10,6 +10,7 @@ EXTRA_DIST = skel/bin/busybox-x86_64.sha256 skel/sbin/service.sh skel/etc/env skel/etc/rcS.d/S01abc.sh skel/etc/rcS.d/S02def.sh \ skel/cdrom/.empty skel/dev/shm/.empty skel/dev/pts/.empty \ skel/etc/inittab skel/etc/hostname skel/etc/fstab \ + skel/usr/share/dbus-1/system.conf skel/etc/machine-id \ skel/etc/passwd skel/etc/group skel/etc/ld.so.conf \ skel/etc/init.d/rcS skel/etc/init.d/rcK skel/tmp/.empty \ skel/etc/finit.d/.empty skel/etc/finit.d/available/.empty \ @@ -71,6 +72,16 @@ EXTRA_DIST += start-stop-serv.sh EXTRA_DIST += signal-service.sh EXTRA_DIST += testserv.sh EXTRA_DIST += unexpected-restart.sh +EXTRA_DIST += dbus-auth.sh +EXTRA_DIST += dbus-authz.sh +EXTRA_DIST += dbus-broker.sh +EXTRA_DIST += fuzz-msg-parse.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 +EXTRA_DIST += dbus-introspect.sh AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/'; AM_TESTS_ENVIRONMENT += export SYSROOT; @@ -126,6 +137,21 @@ if TESTSERV TESTS += testserv.sh endif TESTS += unexpected-restart.sh +if DBUS +TESTS += dbus-auth.sh +TESTS += dbus-authz.sh +TESTS += dbus-bus.sh +TESTS += dbus-manager.sh +TESTS += dbus-service.sh +TESTS += dbus-cond.sh +TESTS += dbus-initctl.sh +TESTS += dbus-introspect.sh +TESTS += fuzz-msg-parse.sh +# Needs the plugin to bring up the bus, not just the built-in one +if BUILD_DBUS_PLUGIN +TESTS += dbus-broker.sh +endif +endif check-recursive: setup-chroot diff --git a/test/check.sh b/test/check.sh index 3c847f56..e76d4af7 100755 --- a/test/check.sh +++ b/test/check.sh @@ -25,8 +25,8 @@ if [ "$run_make" -eq 1 ]; then fi ./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \ - --enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \ - --with-keventd --with-libsystemd \ + --enable-dbus --enable-x11-common-plugin --enable-testserv-plugin \ + --with-watchdog --with-keventd \ CFLAGS='-fsanitize=address -ggdb' if [ "$run_make" -eq 1 ]; then diff --git a/test/dbus-auth.sh b/test/dbus-auth.sh new file mode 100755 index 00000000..ce12bd48 --- /dev/null +++ b/test/dbus-auth.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# 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 + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" +# shellcheck source=/dev/null +. "$TEST_DIR/lib/dbus-setup.sh" + +# The bus reaches every service operation initctl does, so it has to +# be gated like INIT_SOCKET: 0660, owned by root and the --with-group +# group. Only the mode is asserted here, the test namespace does not +# enforce it -- a setuid() client still connects to a 0660 socket. +say "Socket is gated like INIT_SOCKET, not world-accessible" +mode=$(texec stat -c %a "$BUS") +sock=$(texec stat -c %a /run/finit/socket) +assert "Socket mode is 660 (got $mode)" "$mode" = "660" +assert "Bus and INIT_SOCKET agree ($mode vs $sock)" "$mode" = "$sock" + +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" diff --git a/test/dbus-authz.sh b/test/dbus-authz.sh new file mode 100755 index 00000000..66badd54 --- /dev/null +++ b/test/dbus-authz.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Who may invoke a privileged method. Root always, and anyone in the +# group the bus socket is owned by, which is DEFGROUP from +# --with-group, 'root' in a test build. Everyone else is refused. +# +# A caller that gets past the check still has to name a service that +# exists, so NoSuchService is how we tell "allowed, then failed" apart +# from "not allowed at all". + +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" + +# uid 1000 is 'wheelie', a member of group root in the test sysroot. +# uid 2 is 'bin', a member of nothing that matters here. +say "A member of the group may call a privileged method" +set +e +allowed=$(texec "$CLIENT" call-s-as-uid 1000 "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart nosuchservice 2>&1) +set -e +case "$allowed" in + *AccessDenied*) fail "Group member was refused: $allowed" ;; + *NoSuchService*) assert "Group member passed authorization" 0 -eq 0 ;; + *) fail "Unexpected reply for group member: $allowed" ;; +esac + +say "A caller outside the group may not" +set +e +denied=$(texec "$CLIENT" call-s-as-uid 2 "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart nosuchservice 2>&1) +set -e +case "$denied" in + *AccessDenied*) assert "Non-member refused" 0 -eq 0 ;; + *NoSuchService*) fail "Non-member passed authorization: $denied" ;; + *) fail "Unexpected reply for non-member: $denied" ;; +esac + +say "Root is still allowed" +set +e +asroot=$(texec "$CLIENT" call-s "$BUS" /org/finit/manager \ + org.finit.Manager1 Restart nosuchservice 2>&1) +set -e +case "$asroot" in + *AccessDenied*) fail "Root was refused: $asroot" ;; + *NoSuchService*) assert "Root passed authorization" 0 -eq 0 ;; + *) fail "Unexpected reply for root: $asroot" ;; +esac diff --git a/test/dbus-broker.sh b/test/dbus-broker.sh new file mode 100755 index 00000000..e82897f5 --- /dev/null +++ b/test/dbus-broker.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Finit against a real message bus. +# +# Every other dbus-*.sh test drives libink's own client, so the wire +# format is only ever checked against the implementation that produced +# it. Here the dbus plugin brings up a real dbus-daemon, Finit finds +# it and claims org.finit, and then dbus-send talks to Finit: a client +# that shares no code with us. +# +# It also covers the only path the brokerless bus cannot reach, where +# one connection carries every caller and Finit has to ask the broker +# who sent each privileged call. +# +# Skipped when the host has no dbus-daemon or dbus-send to stage. + +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +# Staged by lib/sysroot.mk from the host, when it has them. +for prog in dbus-daemon dbus-send; do + texec sh -c "command -v $prog >/dev/null" \ + || skip "no $prog in the test root, need it on the host" +done + +say 'The dbus plugin started a system bus' +retry 'assert_file_exists /var/run/dbus/system_bus_socket' + +say 'Finit claims org.finit on the system bus' +retry "texec dbus-send --system --print-reply --dest=org.freedesktop.DBus \ + /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.finit" 60 0.5 + +say 'A read-only method answers through the broker' +list=$(texec dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager org.finit.Manager1.ListServices) +assert "ListServices returned the dbus service" \ + "$(printf '%s' "$list" | grep -c '"dbus"')" -ge 1 + +say 'Properties.Get answers through the broker' +rl=$(texec dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager org.freedesktop.DBus.Properties.Get \ + string:org.finit.Manager1 string:Runlevel) +assert "Runlevel property is 2" "$(printf '%s' "$rl" | grep -c '"2"')" -eq 1 + +# The point of the exercise: a privileged call over the broker means +# Finit parks it, asks the driver who the sender is, and dispatches on +# the answer. Root is allowed, so reaching NoSuchService proves the +# whole round trip worked rather than a blanket denial. The repeat +# call goes the same way, only answered from the sender cache. +for pass in first repeat; do + say "A privileged method resolves the caller, $pass call" + priv=$(texec dbus-send --system --print-reply --dest=org.finit \ + /org/finit/manager org.finit.Manager1.Restart string:nosuchservice 2>&1 || true) + case "$priv" in + *NoSuchService*) assert "Caller identified on the $pass call" 0 -eq 0 ;; + *) fail "Unexpected reply to the $pass privileged call: $priv" ;; + esac +done diff --git a/test/dbus-bus.sh b/test/dbus-bus.sh new file mode 100755 index 00000000..57a21da8 --- /dev/null +++ b/test/dbus-bus.sh @@ -0,0 +1,82 @@ +#!/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 + +# A client that sends these must still get its signals: refusing the +# rule would leave it with none at all. The sender here is deliberately +# wrong, so a delivered signal proves the key was ignored and not +# quietly honoured. +say "AddMatch accepts, and ignores, sender/destination/eavesdrop" +rm -f /tmp/dbus-ignored.out +( texec "$CLIENT" monitor-signal "$BUS" \ + "type='signal',sender='org.freedesktop.DBus',destination=':1.99',eavesdrop='false',interface='org.finit.Cond1',member='ConditionChanged'" \ + 5000 > /tmp/dbus-ignored.out 2>&1 ) & +ign_pid=$! +sleep 0.5 +texec "$CLIENT" call-s "$BUS" /org/finit/cond \ + org.finit.Cond1 Set "dbus-ignored-keys" >/dev/null \ + || fail "Cond1.Set returned non-zero" +set +e +wait "$ign_pid" +ign_rc=$? +set -e +assert "Signal still delivered through the wider rule (rc=$ign_rc)" "$ign_rc" -eq 0 + +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..f903f0cf --- /dev/null +++ b/test/dbus-initctl.sh @@ -0,0 +1,144 @@ +#!/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 + +# ---------- arc B: signal + reload routing ---------- + +# initctl signal exits 0 iff the bus call succeeded. We send SIGCONT +# (no observable side-effect on a healthy daemon) so the test stays +# benign regardless of how keventd handles it. +say "initctl signal routes through Manager1.Signal" +texec initctl signal keventd CONT >/dev/null \ + || fail "initctl signal returned non-zero" +assert "initctl signal ok" 0 -eq 0 + +say "initctl signal on a bogus identity reports NoSuchService" +set +e +texec initctl signal no-such-svc-anywhere CONT >/tmp/dbus-sig-bad.out 2>&1 +sigbad_rc=$? +set -e +assert "Bogus signal target rejected (rc=$sigbad_rc)" "$sigbad_rc" -ne 0 +case "$(cat /tmp/dbus-sig-bad.out)" in + *"no such task or service"*) + assert "Error message mentions missing service" 0 -eq 0 ;; + *) + fail "Unexpected initctl signal output: $(cat /tmp/dbus-sig-bad.out)" ;; +esac + +say "initctl reload routes through Service1.Reload" +texec initctl reload keventd >/dev/null \ + || fail "initctl reload keventd returned non-zero" +assert "Per-service reload ok" 0 -eq 0 + +say "initctl status views render over D-Bus alone" +# hide the legacy socket: everything below must come over the bus +texec mv /run/finit/socket /run/finit/socket.hidden + +set +e +out=$(texec initctl -p status) +status_rc=$? +one=$(texec initctl -p status keventd) +one_rc=$? +texec initctl -q status keventd +quiet_rc=$? +jout=$(texec initctl -j status) +json_rc=$? +idout=$(texec initctl ident) +ident_rc=$? +set -e + +texec mv /run/finit/socket.hidden /run/finit/socket + +assert "status table ok without legacy socket (rc=$status_rc)" \ + "$status_rc" -eq 0 +case "$out" in + *keventd*running*) assert "keventd running in table" 0 -eq 0 ;; + *) fail "keventd/running missing from status table" ;; +esac + +assert "status detail view ok (rc=$one_rc)" "$one_rc" -eq 0 +case "$one" in + *"Status : running"*"Identity : keventd"*"Restarts :"*) + assert "detail view fields present" 0 -eq 0 ;; + *) fail "detail view incomplete: $one" ;; +esac + +assert "quiet mode exit code (rc=$quiet_rc)" "$quiet_rc" -eq 0 + +assert "json mode ok (rc=$json_rc)" "$json_rc" -eq 0 +case "$jout" in + *'"identity": "keventd"'*'"status": "running"'*) + assert "json fields present" 0 -eq 0 ;; + *) fail "json output incomplete: $jout" ;; +esac + +assert "ident ok (rc=$ident_rc)" "$ident_rc" -eq 0 +case "$idout" in + *keventd*) assert "ident lists keventd" 0 -eq 0 ;; + *) fail "ident missing keventd" ;; +esac diff --git a/test/dbus-introspect.sh b/test/dbus-introspect.sh new file mode 100755 index 00000000..08e4927c --- /dev/null +++ b/test/dbus-introspect.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# libink: introspection XML well-formedness. +# +# Covers compound type signatures in generated introspection XML: +# Cond1.Dump declares out_sig "a(ss)", which must appear as a single +# , not one per signature character. + +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 "Introspect on /org/finit/cond" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/cond) + +say "Cond1.Dump advertises a single a(ss) out arg" +case "$xml" in + *''*) assert "a(ss) intact" 0 -eq 0 ;; + *) fail "a(ss) not found as a single arg" ;; +esac + +say "No per-character type fragments in the XML" +case "$xml" in + *'type="("'* | *'type=")"'* | *'type="{"'* | *'type="}"'* | *'type="a"'*) + fail "per-character fragment leaked" ;; + *) assert "no bracket/array fragments" 0 -eq 0 ;; +esac diff --git a/test/dbus-manager.sh b/test/dbus-manager.sh new file mode 100755 index 00000000..c3bdce33 --- /dev/null +++ b/test/dbus-manager.sh @@ -0,0 +1,135 @@ +#!/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 + +# ---------- Properties ---------- + +say "Introspect on /org/finit/manager advertises org.freedesktop.DBus.Properties" +xml=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager) +case "$xml" in + *'org.freedesktop.DBus.Properties'*) assert "Properties interface in XML" 0 -eq 0 ;; + *) fail "Properties interface missing from XML" ;; +esac + +say "Manager1 declares Runlevel + Version as in introspection XML" +case "$xml" in + *'/dev/null \ + || fail "SetDebug returned non-zero" +# Toggle back so this test leaves debug in the same state we found it +texec "$CLIENT" call-void "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/dev/null || true +assert "SetDebug round-trip ok" 0 -eq 0 + +say "Manager1.SetDebug from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 SetDebug >/tmp/dbus-setdbg.out 2>&1 +sdbg_rc=$? +set -e +assert "Non-root SetDebug rejected (rc=$sdbg_rc)" "$sdbg_rc" -eq 1 +case "$(cat /tmp/dbus-setdbg.out)" in + *AccessDenied*) assert "SetDebug authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-setdbg.out)" ;; +esac + +# Suspend would actually suspend the test sysroot if it succeeded -- so +# we only test the non-root rejection path, which fails before suspend() +# is called. +say "Manager1.Suspend from non-root is rejected with AccessDenied" +set +e +texec "$CLIENT" call-void-as-uid 1 "$BUS" /org/finit/manager \ + org.finit.Manager1 Suspend >/tmp/dbus-susp.out 2>&1 +susp_rc=$? +set -e +assert "Non-root Suspend rejected (rc=$susp_rc)" "$susp_rc" -eq 1 +case "$(cat /tmp/dbus-susp.out)" in + *AccessDenied*) assert "Suspend authz fires" 0 -eq 0 ;; + *) fail "Unexpected reply: $(cat /tmp/dbus-susp.out)" ;; +esac + +say "initctl runlevel reads via Properties.Get when D-Bus available" +# runlevel output format is " ", e.g. "N 2". Just check +# we get a sensible two-token line. +rl=$(texec initctl runlevel) +case "$rl" in + [N0-9S]\ [0-9S]) + assert "initctl runlevel returned '$rl'" 0 -eq 0 ;; + *) + fail "Unexpected initctl runlevel output: $rl" ;; +esac diff --git a/test/dbus-service.sh b/test/dbus-service.sh new file mode 100755 index 00000000..e812651b --- /dev/null +++ b/test/dbus-service.sh @@ -0,0 +1,93 @@ +#!/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 +# properties via Properties.Get, 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 properties: Identity, State, Pid" +ident=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Identity) +assert "Identity is keventd (got: $ident)" "$ident" = "keventd" + +state=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 State) +assert "State is running (got: $state)" "$state" = "running" + +pid=$(texec "$CLIENT" getprop "$BUS" /org/finit/service/keventd \ + org.finit.Service1 Pid) +assert "Pid is non-zero (got: $pid)" "$pid" -gt 0 + +say "Service1 properties are advertised in introspection XML" +case "$xml" in + *'/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/depserv.sh b/test/depserv.sh index 0d0eaa1d..396afae2 100755 --- a/test/depserv.sh +++ b/test/depserv.sh @@ -66,7 +66,9 @@ test_one() run "initctl stop foo" run "initctl status" run "initctl status bar" - assert_status "bar" "waiting" + # bar passes through stopped on its way to waiting, so this has to + # wait for the end of the transition rather than sample it once + retry 'assert_status "bar" "waiting"' } run "initctl debug" diff --git a/test/fuzz-msg-parse.sh b/test/fuzz-msg-parse.sh new file mode 100755 index 00000000..6e18850e --- /dev/null +++ b/test/fuzz-msg-parse.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# libink: __msg_parse() against truncation, corruption, and garbage. +# +# Runs the fuzz target's fixed sweep, which is the same contract check +# libFuzzer drives, so the suite covers it on every build without +# needing clang. Anything it finds aborts, and the sanitizers CI +# builds with turn a stray read into a failure here rather than a +# puzzle on a target. + +set -eu + +TEST_DIR=$(dirname "$0") +DRIVER="$TEST_DIR/src/fuzz-msg-parse" + +[ -x "$DRIVER" ] || { + echo "fuzz-msg-parse not built, D-Bus support is off" + exit 77 +} + +exec "$DRIVER" 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/lib/setup.sh b/test/lib/setup.sh index 18b90138..41065998 100755 --- a/test/lib/setup.sh +++ b/test/lib/setup.sh @@ -330,7 +330,6 @@ export SYSROOT top_builddir="${top_builddir:-$TEST_DIR/..}" sysroot_finit="$SYSROOT/sbin/finit" built_finit="$top_builddir/src/finit" -[ -x "$top_builddir/src/.libs/finit" ] && built_finit="$top_builddir/src/.libs/finit" if [ -x "$built_finit" ] && [ -e "$sysroot_finit" ] && ! cmp -s "$built_finit" "$sysroot_finit"; then fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'" diff --git a/test/lib/sysroot.mk b/test/lib/sysroot.mk index ea2d8c7c..ae71e7f2 100644 --- a/test/lib/sysroot.mk +++ b/test/lib/sysroot.mk @@ -36,11 +36,23 @@ BBURL ?= $(BBHOME)/$(BBVER)/$(BBBIN) # glibc dlopen()s NSS modules at runtime, so ldd does not list them, but # without libnss_files getpwnam() cannot resolve users inside the chroot -_libs_nss = $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ +_libs_nss := $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ /usr/lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ /lib64/libnss_files.so.2 /lib/libnss_files.so.2)) -_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) -libs = $(foreach path,$(_libs_src),$(abspath $(DEST))$(path)) +# A real broker and a real client, staged when the host has them, so +# one test can check Finit against dbus-daemon instead of only against +# libink's own client. Absent is fine, dbus-broker.sh skips. +dbus_bins := $(foreach b,dbus-daemon dbus-send,$(firstword $(wildcard /usr/bin/$(b) /bin/$(b)))) +# Given several binaries ldd prefixes each with a 'path:' header, and +# that trailing colon would land in a make target. Excluding it here +# is enough, no need for one ldd per binary. +_libs_dbus := $(if $(dbus_bins),$(shell ldd $(dbus_bins) | grep -Eo '/[^ :]+')) + +# The binaries stage exactly like the libraries: same host path, same +# path under DEST, copied by the rule below. +_libs_src := $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) \ + $(_libs_dbus) $(dbus_bins) +libs := $(foreach path,$(sort $(_libs_src)),$(abspath $(DEST))$(path)) all: $(libs) $(DEST)/bin/$(BBBIN) @(cd $(DEST); \ diff --git a/test/setup-sysroot.sh b/test/setup-sysroot.sh index d6787f4d..59af2272 100755 --- a/test/setup-sysroot.sh +++ b/test/setup-sysroot.sh @@ -2,19 +2,14 @@ set -eu -echo "=== Finit Test Sysroot Setup ===" -echo "Date: $(date)" -echo "SYSROOT: $SYSROOT" -echo "top_builddir: $top_builddir" -echo "srcdir: $srcdir" -echo "================================" -echo - # shellcheck disable=SC2154 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 + cp "$top_builddir/test/src/dbus-auth-client" "$SYSROOT/sbin/" +fi # shellcheck disable=SC2154 FINITBIN="$(pwd)/$top_builddir/src/finit" DEST="$SYSROOT" make -f "$srcdir/lib/sysroot.mk" @@ -31,11 +26,4 @@ for conf in 10-hotplug.conf; do done # Update dynamic linker cache for /usr/local/lib libraries -echo "Running ldconfig in sysroot: $SYSROOT" -echo "Contents of $SYSROOT/etc/ld.so.conf:" -cat "$SYSROOT/etc/ld.so.conf" || echo "Warning: ld.so.conf not found" -echo "Libraries in $SYSROOT/usr/local/lib:" -ls -la "$SYSROOT/usr/local/lib/" 2>/dev/null || echo "Warning: /usr/local/lib not found in sysroot" -ldconfig -v -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?" -echo "Verifying ldconfig cache was created:" -ls -la "$SYSROOT/etc/ld.so.cache" || echo "Warning: ld.so.cache not created" +ldconfig -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?" diff --git a/test/skel/etc/group b/test/skel/etc/group index 696ac862..197977f1 100644 --- a/test/skel/etc/group +++ b/test/skel/etc/group @@ -1,4 +1,4 @@ -root:x:0: +root:x:0:wheelie daemon:x:1: bin:x:2: sys:x:3: @@ -7,3 +7,4 @@ tty:x:5: disk:x:6: dialout:x:20: nogroup:x:65534: +wheelie:x:1000: diff --git a/test/skel/etc/machine-id b/test/skel/etc/machine-id new file mode 100644 index 00000000..cc671894 --- /dev/null +++ b/test/skel/etc/machine-id @@ -0,0 +1 @@ +0123456789abcdef0123456789abcdef diff --git a/test/skel/etc/passwd b/test/skel/etc/passwd index 2933891f..bede8cf6 100644 --- a/test/skel/etc/passwd +++ b/test/skel/etc/passwd @@ -3,3 +3,4 @@ daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +wheelie:x:1000:1000:wheelie:/home/wheelie:/bin/sh diff --git a/test/skel/usr/share/dbus-1/system.conf b/test/skel/usr/share/dbus-1/system.conf new file mode 100644 index 00000000..5d256837 --- /dev/null +++ b/test/skel/usr/share/dbus-1/system.conf @@ -0,0 +1,36 @@ + + + + + system + + + /etc/dbus-1/system.d + unix:path=/var/run/dbus/system_bus_socket + EXTERNAL + + + + + + + + + + + + + + diff --git a/test/src/.gitignore b/test/src/.gitignore index fdb16e7f..14824ddc 100644 --- a/test/src/.gitignore +++ b/test/src/.gitignore @@ -3,3 +3,4 @@ /.libs/ /.deps/ /serv +/dbus-auth-client diff --git a/test/src/Makefile.am b/test/src/Makefile.am index 38b26490..d907ff86 100644 --- a/test/src/Makefile.am +++ b/test/src/Makefile.am @@ -2,8 +2,21 @@ noinst_PROGRAMS = serv serv_SOURCES = serv.c serv_CPPFLAGS = -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_GNU_SOURCE -D_DEFAULT_SOURCE -I$(top_builddir) -if LIBSYSTEMD -serv_CPPFLAGS += -I$(top_srcdir)/libsystemd $(lite_CFLAGS) +# serv is what notify.sh drives to test notify:systemd, so it always +# needs sd_notify(). The source is in-tree and it links straight in, +# unrelated to whether --with-libsystemd installs the shared library. +serv_CPPFLAGS += -DHAVE_LIBSYSTEMD=1 -I$(top_srcdir)/libsystemd $(lite_CFLAGS) serv_SOURCES += $(top_srcdir)/libsystemd/sd-daemon.c serv_LDADD = $(lite_LIBS) + +if DBUS +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 + +noinst_PROGRAMS += fuzz-msg-parse +fuzz_msg_parse_SOURCES = fuzz-msg-parse.c +fuzz_msg_parse_CPPFLAGS = -D_GNU_SOURCE -I$(top_srcdir)/libink -I$(top_builddir) +fuzz_msg_parse_LDADD = $(top_builddir)/libink/libink.la endif diff --git a/test/src/dbus-auth-client.c b/test/src/dbus-auth-client.c new file mode 100644 index 00000000..14796647 --- /dev/null +++ b/test/src/dbus-auth-client.c @@ -0,0 +1,490 @@ +/* Minimal D-Bus client used by the libink smoke tests. + * + * Modes: + * 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 + * Call org.freedesktop.DBus.Hello, print the assigned unique name. + * + * dbus-auth-client introspect + * Call org.freedesktop.DBus.Introspectable.Introspect, print XML. + * + * dbus-auth-client liststrings + * 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 + * Call a bogus method, exit 0 iff the server replies with an + * org.freedesktop.DBus.Error.* 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 +#include +#include +#include + +#include "link.h" + +/* ---------- manual SASL: only mode_auth uses this ---------- */ + +static const char hex[] = "0123456789abcdef"; + +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; + len -= (size_t)n; + } + return 0; +} + +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; + return -1; + } + if (buf[off] == '\n') { + buf[off] = '\0'; + if (off > 0 && buf[off - 1] == '\r') + buf[--off] = '\0'; + return (ssize_t)off; + } + off++; + } + return -1; +} + +static int mode_auth(int argc, char *argv[]) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + char hexuid[32], line[64], reply[256]; + const char *path, *claimed; + size_t i, claimed_len, plen; + int fd, rc; + + if (argc != 4) return 2; + path = argv[2]; + claimed = argv[3]; + + plen = strlen(path); + if (plen >= sizeof(sun.sun_path)) return 2; + claimed_len = strlen(claimed); + if (claimed_len * 2 >= sizeof(hexuid)) return 2; + for (i = 0; i < claimed_len; i++) { + unsigned c = (unsigned char)claimed[i]; + + hexuid[i * 2] = hex[c >> 4]; + hexuid[i * 2 + 1] = hex[c & 0xf]; + } + hexuid[claimed_len * 2] = '\0'; + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { perror("socket"); return 2; } + memcpy(sun.sun_path, path, plen + 1); + if (connect(fd, (struct sockaddr *)&sun, sizeof(sun)) < 0) { + perror("connect"); 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(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; + if (strncmp(reply, "REJECTED ", 9) == 0) return 1; + return 2; +} + +/* ---------- libink-driven modes ---------- */ + +/* 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 2; +} + +/* Drop effective uid to argv-supplied value (decimal). */ +static int drop_uid(const char *uid_arg, const char *progname) +{ + char *ep = NULL; + long v; + + errno = 0; + v = strtol(uid_arg, &ep, 10); + if (errno || !ep || *ep != '\0' || v < 0 || v > 65535) { + fprintf(stderr, "%s: bad uid: %s\n", progname, uid_arg); + return 2; + } + if (setuid((uid_t)v) < 0) { + perror("setuid"); + return 2; + } + return 0; +} + +static int mode_hello(int argc, char *argv[]) +{ + link_client_t *c; + 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) { + if (link_reply_get_string(link_client_reply(c), &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 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) { + if (link_reply_get_string(link_client_reply(c), &xml) == 0) + printf("%s\n", xml); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +/* Decode body with signature "as", print one string per line. */ +static int print_string_array(const link_reply_t *r) +{ + link_reader_t reader; + 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_array_begin(&reader, &end) < 0) + 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 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) { + /* Reply sig is "o", same wire form as "s". */ + if (link_reply_get_string(link_client_reply(c), &path) == 0) + printf("%s\n", path); + else + rc = 2; + } + link_client_close(c); + return rc; +} + +/* + * getprop BUS PATH IFACE PROP -- Properties.Get, prints the variant + * value; dispatches on the wire signature, "s" and "u" supported. + */ +static int mode_getprop(int argc, char *argv[]) +{ + const link_reply_t *r; + link_client_t *c; + link_reader_t reader; + const char *s; + uint32_t u; + 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], + "org.freedesktop.DBus.Properties", "Get", + "ss", argv[4], argv[5]); + rc = report_rc(c, rc); + if (rc == 0) { + char type = 0; + + r = link_client_reply(c); + link_reader_init(&reader, r->body, r->body_len); + if (link_r_variant_begin(&reader, &type) != 0) + rc = 2; + else if (type == 's' && link_r_string(&reader, &s) == 0) + printf("%s\n", s); + else if (type == 'u' && link_r_u32(&reader, &u) == 0) + printf("%u\n", u); + else { + fprintf(stderr, "unsupported variant type '%c'\n", type); + rc = 2; + } + } + link_client_close(c); + return rc; +} + +static int mode_monitor_signal(int argc, char *argv[]) +{ + 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; + + errno = 0; + v = strtol(argv[4], &ep, 10); + if (errno || !ep || *ep != '\0' || v <= 0 || v > 600000) { + fprintf(stderr, "%s: bad timeout: %s\n", argv[0], argv[4]); + return 2; + } + timeout_ms = (int)v; + + c = link_client_open(argv[2]); + if (!c) 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; + } + + for (;;) { + rc = link_client_wait(c, timeout_ms); + if (rc != 0) { + link_client_close(c); + return 1; /* timeout or transport */ + } + r = link_client_reply(c); + if (!r || r->type != LINK_MSG_SIGNAL) + continue; + + 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); + } + } + link_client_close(c); + return 0; + } +} + +static int mode_unknown(int argc, char *argv[]) +{ + link_client_t *c; + const link_reply_t *r; + int rc; + + if (argc != 3) 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")) 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], "getprop")) return mode_getprop (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; +} diff --git a/test/src/fuzz-msg-parse.c b/test/src/fuzz-msg-parse.c new file mode 100644 index 00000000..e9cb02c9 --- /dev/null +++ b/test/src/fuzz-msg-parse.c @@ -0,0 +1,232 @@ +/* libink — fuzz target for __msg_parse(). + * + * __msg_parse() is the one place where bytes off a socket become + * pointers, before any authentication has vouched for the peer. It + * hands back borrowed pointers into the caller's buffer, so "did not + * crash" is too weak a pass: a field that points outside the header it + * was supposed to come from, or a string with no terminator inside it, + * is a bug the caller hits later and somewhere else. Every input is + * checked against that contract here. + * + * Built two ways. With libFuzzer (clang -fsanitize=fuzzer,address + * -DLINK_FUZZ_LIBFUZZER) it is a normal fuzz target. Otherwise it + * gets the driver below: named files are replayed, which is how a + * crash found by the fuzzer is reproduced, and with no arguments it + * runs a fixed sweep so the suite exercises the same contract on + * every build without needing clang or a corpus in the tree. + * + * Copyright (c) 2026 Joachim Wiberg + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include "internal.h" + +#define FRAME_MAX 512 +#define HDR_FIXED_SIZE 16 /* must agree with proto.c */ + +static void fail(const char *what, size_t size) +{ + fprintf(stderr, "fuzz-msg-parse: %s, on a %zu byte input\n", what, size); + abort(); +} + +/* A header field is read out of the header field array and nowhere + * else. Checking it against the whole buffer would be too generous: + * a parser that walked off the end of the fields and into the body + * would still be pointing at bytes it was handed, and pass. The + * bound is derived from the raw header here rather than taken from + * the parser, so the two have to agree independently. + * + * The string must also terminate inside that region, or whoever + * borrows it reads past what it was given. */ +static void check_str(const char *s, const uint8_t *base, size_t len, + size_t hdr_end, const char *what) +{ + const char *first = (const char *)base + HDR_FIXED_SIZE; + const char *last = (const char *)base + hdr_end; + size_t room; + + if (!s) + return; + if (s < first || s >= last) + fail(what, len); + + room = (size_t)(last - s); + if (strnlen(s, room) == room) + fail(what, len); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct link_msg m; + size_t hdr_end, body_off; + uint32_t fields_len; + ssize_t rc; + + rc = __msg_parse(data, size, &m); + if (rc <= 0) + return 0; /* need more, or refused: both fine */ + + /* Consuming more than it was handed would desynchronise the + * read loop and make it skip into the next message. */ + if ((size_t)rc > size) + fail("claimed more bytes than it was given", size); + + /* Only 'l' messages parse, so reading the length this way is + * safe, and rc > 0 means the fixed header was all there. */ + memcpy(&fields_len, data + 12, sizeof(fields_len)); + hdr_end = HDR_FIXED_SIZE + fields_len; + body_off = (hdr_end + 7) & ~(size_t)7; + if (hdr_end > size || body_off > size) + fail("accepted a header longer than the message", size); + + check_str(m.path, data, size, hdr_end, "path outside the header"); + check_str(m.interface, data, size, hdr_end, "interface outside the header"); + check_str(m.member, data, size, hdr_end, "member outside the header"); + check_str(m.error_name, data, size, hdr_end, "error name outside the header"); + check_str(m.destination, data, size, hdr_end, "destination outside the header"); + check_str(m.sender, data, size, hdr_end, "sender outside the header"); + check_str(m.signature, data, size, hdr_end, "signature outside the header"); + + if (m.body_avail) { + if (m.body != data + body_off) + fail("body does not start where the header ends", size); + if (m.body_avail > size - body_off) + fail("body runs past the buffer", size); + } + + return 0; +} + +#ifndef LINK_FUZZ_LIBFUZZER +/* Hand the parser a buffer sized to the input and nothing more. + * Reading past the end of a roomy array stays inside the allocation + * and the sanitizer never sees it; against an exact allocation the + * same read is a fault. This is what libFuzzer does, and the reason + * it finds things a fixed buffer cannot. */ +static void run(const uint8_t *data, size_t size) +{ + uint8_t *exact = malloc(size ? size : 1); + + if (!exact) { + fprintf(stderr, "fuzz-msg-parse: out of memory\n"); + abort(); + } + memcpy(exact, data, size); + LLVMFuzzerTestOneInput(exact, size); + free(exact); +} + +/* Deterministic, so a failure reproduces from the same build. */ +static uint32_t prng(uint32_t *state) +{ + uint32_t x = *state; + + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + + return *state = x; +} + +static int sweep(void) +{ + uint8_t frame[FRAME_MAX], copy[FRAME_MAX]; + static const uint8_t poke[] = { 0x00, 0x01, 0x7f, 0x80, 0xff }; + uint32_t state = 0x1234abcd; + ssize_t len; + size_t i, j, n; + + len = __msg_build_method_call(frame, sizeof(frame), 1, + "/org/finit/manager", "org.finit.Manager1", + "ListServices", NULL, NULL, 0); + if (len <= 0) { + fprintf(stderr, "fuzz-msg-parse: cannot build a reference frame\n"); + return 1; + } + + /* Every prefix: the read loop hands over whatever arrived, and + * a short read must come back "need more", never a parse. */ + for (i = 0; i <= (size_t)len; i++) + run(frame, i); + + /* One byte wrong, everywhere, with the values that flip a + * length or an offset furthest. */ + for (i = 0; i < (size_t)len; i++) { + for (j = 0; j < sizeof(poke); j++) { + memcpy(copy, frame, (size_t)len); + copy[i] = poke[j]; + run(copy, (size_t)len); + } + } + + /* fields_len decides where the header walk stops, so it is the + * byte that decides whether a field is read from the header or + * from somewhere else. Walk it across the whole frame, and + * past it, at every truncation the read loop could hand over. */ + for (i = 0; i <= (size_t)len + 8; i++) { + uint32_t fl = (uint32_t)i; + + memcpy(copy, frame, (size_t)len); + memcpy(copy + 12, &fl, sizeof(fl)); + for (j = 0; j <= (size_t)len; j++) + run(copy, j); + } + + /* And input that was never a message to begin with. */ + for (n = 0; n < 20000; n++) { + size_t sz = prng(&state) % (FRAME_MAX + 1); + + for (i = 0; i < sz; i++) + copy[i] = (uint8_t)prng(&state); + + /* Half of them keep a plausible header, so the parser + * gets past its first checks and into field walking. */ + if (sz >= 16 && (n & 1)) { + copy[0] = 'l'; + copy[3] = LINK_PROTOCOL_VERSION; + } + run(copy, sz); + } + + return 0; +} + +static int replay(const char *path) +{ + uint8_t buf[64 * 1024]; + size_t len; + FILE *fp; + + fp = fopen(path, "rb"); + if (!fp) { + perror(path); + return 1; + } + len = fread(buf, 1, sizeof(buf), fp); + fclose(fp); + + run(buf, len); + return 0; +} + +int main(int argc, char *argv[]) +{ + int i, rc = 0; + + if (argc < 2) + return sweep(); + + for (i = 1; i < argc; i++) + rc |= replay(argv[i]); + + return rc; +} +#endif /* !LINK_FUZZ_LIBFUZZER */ diff --git a/tmpfiles.d/Makefile.am b/tmpfiles.d/Makefile.am index 5155277c..facb9722 100644 --- a/tmpfiles.d/Makefile.am +++ b/tmpfiles.d/Makefile.am @@ -6,6 +6,10 @@ dist_tmpfiles_DATA = etc.conf legacy.conf var.conf dist_tmpfiles_DATA += dnsmasq.conf frr.conf lldpd.conf openswan.conf \ quagga.conf sshd.conf uuidd.conf +if BUILD_DBUS_PLUGIN +dist_tmpfiles_DATA += dbus.conf +endif + if BUILD_TESTSERV_PLUGIN dist_tmpfiles_DATA += testserv.conf endif diff --git a/tmpfiles.d/dbus.conf b/tmpfiles.d/dbus.conf new file mode 100644 index 00000000..f98e0252 --- /dev/null +++ b/tmpfiles.d/dbus.conf @@ -0,0 +1,4 @@ +d /var/run/dbus 0755 - - - +d /var/run/lock/subsys 0755 - - - +d /var/lib/dbus 0755 - - - +d /tmp/dbus 0755 - - -