diff --git a/.braids.json b/.braids.json index d415d0f..c8eff68 100644 --- a/.braids.json +++ b/.braids.json @@ -1,6 +1,11 @@ { "config_version": 1, "mirrors": { + "dinit-userservd": { + "url": "https://github.com/XynonWasTaken/dinit-userservd", + "branch": "master", + "revision": "0745b208d6a742e9b6e8e168a1dfea707f2c11be" + }, "linux-xanmod-edge": { "url": "https://aur.archlinux.org/linux-xanmod-edge.git", "branch": "master", diff --git a/dinit-userservd/COPYING.md b/dinit-userservd/COPYING.md new file mode 100644 index 0000000..e2a2a41 --- /dev/null +++ b/dinit-userservd/COPYING.md @@ -0,0 +1,22 @@ +Copyright 2021 Daniel "q66" Kolesa + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/dinit-userservd/PKGBUILD b/dinit-userservd/PKGBUILD new file mode 100644 index 0000000..e50732d --- /dev/null +++ b/dinit-userservd/PKGBUILD @@ -0,0 +1,33 @@ +pkgname=dinit-userservd +pkgver=0.1.1 +pkgrel=1 +epoch= +pkgdesc="user dinit instance spawner + manager daemon." +arch=('any') +url="https://github.com/chimera-linux/dinit-userservd/" +license=('BSD') +groups=('dinit-system') +depends=('dinit' 'elogind') +makedepends=('meson') +optdepends=() +provides=('dinit-userservd') +conflicts=('dinit-userservd-git') +install='dinit-userservd.install' +changelog= + +prepare() { + cd $startdir + rm -rf pkg + mkdir -p build +} + +build() { + cd $startdir + meson --prefix=/usr --buildtype=plain ./ ./build + meson compile -C build +} + +package() { + cd $startdir + meson install -C build --destdir "$pkgdir" +} diff --git a/dinit-userservd/README.md b/dinit-userservd/README.md new file mode 100644 index 0000000..01e48af --- /dev/null +++ b/dinit-userservd/README.md @@ -0,0 +1,97 @@ +# Artix Specific Info + +PKGBUILDs for dinit-userservd located at: https://github.com/XynonWasTaken/dinit-userservd-PKGBUILD + +Artix services for dinit-userservd located at: https://github.com/XynonWasTaken/dinit-userservd-services + +# dinit-userservd + +This is a daemon and a PAM module to handle user services management with the +`dinit` init system and service manager (https://github.com/davmac314/dinit). + +It was created for the needs of the Chimera Linux project. It is not expected +to work properly anywhere else by default (those use cases are unsupported), +and issues or feature requests specific to other environments will not be +addressed. Patches may be accepted, provided they are not disruptive or +introduce excessive complexity. + +## How it works + +The project consists of a daemon and a PAM module. The PAM module is enabled +for example by adding this in your login path: + +``` +session optional pam_dinit_userservd.so +``` + +The daemon must simply be running in some way. If it is not running, you will +still be able to log in with the above setup, but it will not do anything. + +A recommended way to manage the daemon is using a `dinit` service that is +provided with the project. + +The daemon opens a control socket. The PAM module will make connections to +it upon session start (and close it upon session end). When the daemon +receives a connection, it will negotiate a session with the PAM module +and upon first login of each user, spawn a user `dinit` instance. + +This instance is supervised, if it fails in any way it gets automatically +restarted. It runs outside of the login itself, as only one instance must +exist per user (who can have multiple logins) and it only exists once the +last login has logged out. This means that environment variables of the +login do not exist within the user instance by default, and they must be +exported into it through other means. + +It will register the following service directories: + +* `~/.config/dinit.d` +* `/etc/dinit.d/user` +* `/usr/local/lib/dinit.d/user` +* `/usr/lib/dinit.d/user` + +You do not need to provide a `boot` service (in fact, you should not). +By default, the following path is used for autostarted user services: + +* `~/.config/dinit.d/boot.d` + +Simply drop symlinks to whatever services you want in there and they will +get started with your login. + +The login proceeds once the `dinit` instance has signaled readiness (which +is once it has started its autostart services). It does so via an internal +notification mechanism. + +### XDG_RUNTIME_DIR handling + +**NOTE:** This is problematic for now, so it's disabled at the moment. + +Usually, `XDG_RUNTIME_DIR` is managed by another daemon, typically `elogind` +for Chimera. However, some people may not be running `elogind` or a similar +solution. The PAM module automatically detects this and makes the daemon +manage the runtime directory for you. + +It takes care of both creation and cleanup automatically as sessions are +logged in and as they go away. + +To prevent it from managing rundir, you simply have to have something else +manage it before; that means specifying that earlier in the PAM config file. +Or, if you want to force that off, you can pass the `norundir` extra PAM +argument. + +### Dbus handling + +The daemon also supports handling of D-Bus session bus. If the socket +`/run/user/UID/bus` exists by the time readiness has been signaled, the +variable `DBUS_SESSION_BUS_ADDRESS` will automatically be exported into +the login environment. + +That way it is possible to manage the session bus as a user service without +having to spawn it on-demand. + +User services making use of the bus need to ensure that the variable is +exported in their launch environment in some way, as the service manager +runs outside of the user's login session. + +## TODO + +* Do not hardcode things to make it easier to use for other projects. diff --git a/dinit-userservd/dinit-userservd b/dinit-userservd/dinit-userservd new file mode 100644 index 0000000..03200ca --- /dev/null +++ b/dinit-userservd/dinit-userservd @@ -0,0 +1,7 @@ +# dinit-userservd service + +type = process +command = /usr/bin/dinit-userservd +depends-on = elogind +smooth-recovery = true +logfile = /var/log/dinit-userservd.log diff --git a/dinit-userservd/dinit-userservd.cc b/dinit-userservd/dinit-userservd.cc new file mode 100644 index 0000000..d355d5b --- /dev/null +++ b/dinit-userservd/dinit-userservd.cc @@ -0,0 +1,1161 @@ +/* dinit-userservd: handle incoming session requests and start + * (or stop) dinit user instances as necessary + * + * the daemon should never exit under "normal" circumstances + * + * Copyright 2021 Daniel "q66" Kolesa + * License: BSD-2-Clause + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* accept4 */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "protocol.hh" + +static bool debug = false; +/* timeout in case the dinit --user does not signal readiness + * + * we keep a timer for each waiting session, if no readiness is received + * within that timespan, the service manager is terminated and failure + * is issued to all the connections + */ +static constexpr time_t const dinit_timeout = 60; + +/* session information: contains a list of connections (which also provide + * a way to know when to end the session, as the connection is persistent + * on the PAM side) and some statekeeping info: + * + * - the running service manager instance PID + * - the user and group ID of the session's user + * - a file descriptor for the dinit readiness notification FIFO + * - whether dinit is currently waiting for readiness notification + */ +struct session { + std::vector conns{}; + char *homedir = nullptr; + char *rundir = nullptr; + char dinit_tmp[6]; + pid_t dinit_pid = -1; + unsigned int uid = 0; + unsigned int gid = 0; + int userpipe = -1; + bool dinit_wait = true; + bool manage_rdir = false; + + ~session() { + std::free(homedir); + std::free(rundir); + } +}; + +struct pending_conn { + pending_conn(): + pending_uid{1}, pending_gid{1}, pending_hdir{1}, + pending_rdir{1}, managed_rdir{0} + {} + int conn = -1; + char *homedir = nullptr; + char *rundir = nullptr; + unsigned int uid = 0; + unsigned int gid = 0; + unsigned int dirleft = 0; + unsigned int dirgot = 0; + unsigned int pending_uid: 1; + unsigned int pending_gid: 1; + unsigned int pending_hdir: 1; + unsigned int pending_rdir: 1; + unsigned int managed_rdir: 1; + + ~pending_conn() { + std::free(homedir); + std::free(rundir); + } +}; + +struct session_timer { + timer_t timer{}; + sigevent sev{}; + unsigned int uid = 0; +}; + +static std::vector sessions; +static std::vector pending_conns; + +/* file descriptors for poll */ +static std::vector fds; +/* control IPC socket */ +static int ctl_sock; +/* requests for new FIFOs; picked up by the event loop and cleared */ +static std::vector fifos; +/* timer list */ +static std::vector timers; + +#define print_dbg(...) if (debug) { printf(__VA_ARGS__); } + +static constexpr int const UID_DIGITS = \ + std::numeric_limits::digits10; + +static bool rundir_make(char *rundir, unsigned int uid, unsigned int gid) { + char *sl = std::strchr(rundir + 1, '/'); + struct stat dstat; + print_dbg("rundir: make directory %s\n", rundir); + /* recursively create all parent paths */ + while (sl) { + *sl = '\0'; + print_dbg("rundir: try make parent %s\n", rundir); + if (stat(rundir, &dstat) || !S_ISDIR(dstat.st_mode)) { + print_dbg("rundir: make parent %s\n", rundir); + if (mkdir(rundir, 0755)) { + perror("rundir: mkdir failed for path"); + return false; + } + } + *sl = '/'; + sl = strchr(sl + 1, '/'); + } + /* create rundir with correct permissions */ + if (mkdir(rundir, 0700)) { + perror("rundir: mkdir failed for rundir"); + return false; + } + if (chown(rundir, uid, gid) < 0) { + perror("rundir: chown failed for rundir"); + rmdir(rundir); + return false; + } + return true; +} + +static bool rundir_clear_contents(int dfd) { + DIR *d = fdopendir(dfd); + if (!d) { + perror("rundir: fdopendir failed"); + close(dfd); + return false; + } + + unsigned char buf[offsetof(struct dirent, d_name) + NAME_MAX + 1]; + unsigned char *bufp = buf; + + struct dirent *dentb = nullptr, *dent = nullptr; + std::memcpy(&dentb, &bufp, sizeof(dent)); + + for (;;) { + if (readdir_r(d, dentb, &dent) < 0) { + perror("rundir: readdir_r failed"); + closedir(d); + return false; + } + if (!dent) { + break; + } + if ( + !std::strcmp(dent->d_name, ".") || + !std::strcmp(dent->d_name, "..") + ) { + continue; + } + + print_dbg("rundir: clear %s at %d\n", dent->d_name, dfd); + int efd = openat(dfd, dent->d_name, O_RDONLY); + if (efd < 0) { + perror("rundir: openat failed"); + closedir(d); + return false; + } + + struct stat st; + if (fstat(efd, &st) < 0) { + perror("rundir: fstat failed"); + closedir(d); + return false; + } + + if (S_ISDIR(st.st_mode)) { + if (!rundir_clear_contents(efd)) { + closedir(d); + return false; + } + } else { + close(efd); + } + + if (unlinkat( + dfd, dent->d_name, S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0 + ) < 0) { + perror("rundir: unlinkat failed"); + closedir(d); + return false; + } + } + + closedir(d); + return true; +} + +static void rundir_clear(char *rundir) { + struct stat dstat; + print_dbg("rundir: clear directory %s\n", rundir); + int dfd = open(rundir, O_RDONLY); + /* non-existent */ + if (fstat(dfd, &dstat)) { + return; + } + /* not a directory */ + if (!S_ISDIR(dstat.st_mode)) { + print_dbg("rundir: %s is not a directory\n", rundir); + return; + } + if (rundir_clear_contents(dfd)) { + /* was empty */ + rmdir(rundir); + } else { + print_dbg("rundir: failed to clear contents of %s\n", rundir); + } +} + +static void dinit_clean(session &sess) { + char buf[sizeof(USER_FIFO) + UID_DIGITS]; + print_dbg("dinit: cleanup %u\n", sess.uid); + /* close the fifo */ + if (sess.userpipe != -1) { + std::snprintf(buf, sizeof(buf), USER_FIFO, sess.uid); + print_dbg("dinit: close %s\n", buf); + /* close best we can */ + close(sess.userpipe); + unlink(buf); + std::snprintf(buf, sizeof(buf), USER_PATH, sess.uid); + rmdir(buf); + for (auto &pfd: fds) { + if (pfd.fd == sess.userpipe) { + pfd.fd = -1; + pfd.revents = 0; + break; + } + } + sess.userpipe = -1; + } +} + +/* stop the dinit instance for a session */ +static void dinit_stop(session &sess) { + /* temporary services dir */ + char buf[sizeof(USER_DIR) + UID_DIGITS + 5]; + print_dbg("dinit: stop\n"); + if (sess.dinit_pid != -1) { + print_dbg("dinit: term\n"); + kill(sess.dinit_pid, SIGTERM); + sess.dinit_pid = -1; + sess.dinit_wait = true; + /* remove the generated service directory best we can + * + * it would be pretty harmless to just leave it too + */ + std::snprintf(buf, sizeof(buf), USER_DIR"/boot", sess.uid); + std::memcpy(std::strstr(buf, "XXXXXX"), sess.dinit_tmp, 6); + print_dbg("dinit: remove %s\n", buf); + unlink(buf); + *std::strrchr(buf, '/') = '\0'; + rmdir(buf); + dinit_clean(sess); + } +} + +/* global service directory paths */ +static constexpr char const *servpaths[] = { + "/etc/dinit.d/user", + "/usr/local/lib/dinit.d/user", + "/usr/lib/dinit.d/user", +}; + +/* start the dinit instance for a session */ +static bool dinit_start(session &sess) { + /* user dir */ + char rdir[sizeof(USER_PATH) + UID_DIGITS]; + std::snprintf(rdir, sizeof(rdir), USER_PATH, sess.uid); + /* temporary services dir */ + char tdir[sizeof(USER_DIR) + UID_DIGITS]; + std::snprintf(tdir, sizeof(tdir), USER_DIR, sess.uid); + /* create /run/dinit-userservd/$UID if non-existent */ + { + struct stat pstat; + if (stat(rdir, &pstat) || !S_ISDIR(pstat.st_mode)) { + if (mkdir(rdir, 0700)) { + perror("dinit: mkdir($UID) failed"); + return false; + } + if (chown(rdir, sess.uid, sess.gid) < 0) { + perror("dinit: chown($UID) failed"); + rmdir(rdir); + return false; + } + } + } + /* create temporary services dir */ + if (!mkdtemp(tdir)) { + perror("dinit: mkdtemp failed"); + return false; + } + print_dbg("dinit: created service directory (%s)\n", tdir); + /* store the characters identifying the tempdir */ + std::memcpy(sess.dinit_tmp, tdir + std::strlen(tdir) - 6, 6); + if (chown(tdir, sess.uid, sess.gid) < 0) { + perror("dinit: chown failed"); + rmdir(tdir); + return false; + } + /* user fifo path */ + char ufifo[sizeof(USER_FIFO) + UID_DIGITS]; + std::snprintf(ufifo, sizeof(ufifo), USER_FIFO, sess.uid); + /* user services dir */ + char udir[DIRLEN_MAX + 32]; + std::snprintf(udir, sizeof(udir), "%s/.config/dinit.d", sess.homedir); + /* set up service file */ + { + char uboot[sizeof(tdir) + 5]; + std::snprintf(uboot, sizeof(uboot), "%s/boot", tdir); + auto *f = std::fopen(uboot, "w"); + if (!f) { + perror("dinit: fopen failed"); + return false; + } + /* write boot service */ + std::fprintf(f, "type = scripted\n"); + /* wait for a service directory */ + std::fprintf(f, "waits-for.d = %s/boot.d\n", udir); + /* readiness notification */ + std::fprintf( + f, "command = sh -c \"test -p '%s' && printf 1 > '%s' || :\"\n", + ufifo, ufifo + ); + std::fclose(f); + /* set perms otherwise we would infinite loop */ + if (chown(uboot, sess.uid, sess.gid) < 0) { + perror("dinit: chown failed"); + unlink(uboot); + return false; + } + } + /* lazily set up user fifo */ + if (sess.userpipe == -1) { + /* create a named pipe */ + unlink(ufifo); + if (mkfifo(ufifo, 0600) < 0) { + perror("dinit: mkfifo failed"); + return false; + } + /* user fifo is owned by the user */ + if (chown(ufifo, sess.uid, sess.gid) < 0) { + perror("dinit: chown failed"); + unlink(ufifo); + return false; + } + /* get its file descriptor */ + sess.userpipe = open(ufifo, O_RDONLY | O_NONBLOCK); + if (sess.userpipe < 0) { + perror("dinit: open failed"); + unlink(ufifo); + return false; + } + auto &pfd = fifos.emplace_back(); + pfd.fd = sess.userpipe; + pfd.events = POLLIN | POLLHUP; + } + /* set up the timer, issue SIGLARM when it fires */ + print_dbg("dinit: timer set\n"); + { + auto &tm = timers.emplace_back(); + tm.uid = sess.uid; + tm.sev.sigev_notify = SIGEV_SIGNAL; + tm.sev.sigev_signo = SIGALRM; + /* create timer, drop if it fails */ + if (timer_create(CLOCK_MONOTONIC, &tm.sev, &tm.timer) < 0) { + perror("dinit: timer_create failed"); + timers.pop_back(); + return false; + } + /* arm timer, drop if it fails */ + itimerspec tval{}; + tval.it_value.tv_sec = dinit_timeout; + if (timer_settime(tm.timer, 0, &tval, nullptr) < 0) { + perror("dinit: timer_settime failed"); + timer_delete(tm.timer); + timers.pop_back(); + return false; + } + } + /* launch dinit */ + print_dbg("dinit: launch\n"); + auto pid = fork(); + if (pid == 0) { + if (getuid() == 0) { + if (setgid(sess.gid) != 0) { + perror("dinit: failed to set gid"); + exit(1); + } + if (setuid(sess.uid) != 0) { + perror("dinit: failed to set uid"); + exit(1); + } + } + /* make up an environment */ + char uenv[DIRLEN_MAX + 5]; + char rundir[DIRLEN_MAX + sizeof("XDG_RUNTIME_DIR=")]; + char euid[UID_DIGITS + 5], egid[UID_DIGITS + 5]; + std::snprintf(uenv, sizeof(uenv), "HOME=%s", sess.homedir); + std::snprintf(euid, sizeof(euid), "UID=%u", sess.uid); + std::snprintf(egid, sizeof(egid), "GID=%u", sess.gid); + if (sess.rundir) { + std::snprintf( + rundir, sizeof(rundir), "XDG_RUNTIME_DIR=%s", sess.rundir + ); + } + char const *envp[] = { + uenv, euid, egid, + "PATH=/usr/local/bin:/usr/bin:/bin", + sess.rundir ? rundir : nullptr, nullptr + }; + /* 6 args reserved + whatever service dirs + terminator */ + char const *argp[6 + (sizeof(servpaths) / sizeof(*servpaths)) * 2 + 1]; + std::size_t cidx = 0; + argp[cidx++] = "dinit"; + argp[cidx++] = "--user"; + argp[cidx++] = "--services-dir"; + argp[cidx++] = tdir; + argp[cidx++] = "--services-dir"; + argp[cidx++] = udir; + for ( + std::size_t i = 0; + i < (sizeof(servpaths) / sizeof(*servpaths)); + ++i + ) { + argp[cidx++] = "--services-dir"; + argp[cidx++] = servpaths[i]; + } + argp[cidx] = nullptr; + /* restore umask to user default */ + umask(022); + /* fire */ + execvpe("dinit", const_cast(argp), const_cast(envp)); + } else if (pid < 0) { + perror("dinit: fork failed"); + return false; + } + sess.dinit_pid = pid; + return true; +} + +/* restart callback for a PID: issued upon receiving a SIGCHLD + * + * this way the daemon supervises its session manager instances, + * those that have a matching PID record in some existing session + * will get restarted automatically + * + * also ensures that stopped sessions have their managed rundirs cleared + */ +static bool dinit_restart(pid_t pid) { + print_dbg("dinit: check for restarts\n"); + for (auto &sess: sessions) { + /* clear rundirs that are done */ + if (sess.manage_rdir && (sess.dinit_pid < 0)) { + rundir_clear(sess.rundir); + sess.manage_rdir = false; + } + if (sess.dinit_pid != pid) { + continue; + } + sess.dinit_pid = -1; + if (!sess.dinit_wait) { + /* failed without ever having signaled readiness + * this indicates that we'd probably just loop forever, + * so bail out + */ + std::fprintf(stderr, "dinit: died without notifying readiness\n"); + return false; + } + sess.dinit_wait = true; + return dinit_start(sess); + } + return true; +} + +static session *get_session(int fd) { + for (auto &sess: sessions) { + for (auto c: sess.conns) { + if (fd == c) { + return &sess; + } + } + } + return nullptr; +} + +static bool msg_send(int fd, unsigned int msg) { + if (send(fd, &msg, sizeof(msg), 0) < 0) { + perror("msg: send failed"); + return false; + } + return (msg != MSG_ERR); +} + +static bool handle_read(int fd) { + unsigned int msg; + auto ret = recv(fd, &msg, sizeof(msg), 0); + if (ret != sizeof(msg)) { + if (errno == EAGAIN) { + return true; + } + perror("msg: recv failed"); + return false; + } + print_dbg( + "msg: read %u (%u, %d)\n", msg & MSG_TYPE_MASK, + msg >> MSG_TYPE_BITS, fd + ); + switch (msg & MSG_TYPE_MASK) { + case MSG_START: { + /* new login, register it */ + auto &pc = pending_conns.emplace_back(); + pc.conn = fd; + return msg_send(fd, MSG_OK); + } + case MSG_OK: { + auto *sess = get_session(fd); + if (!sess) { + print_dbg("msg: no session for %u\n", msg); + return msg_send(fd, MSG_ERR); + } + if (!sess->dinit_wait) { + /* already started, reply with ok */ + print_dbg("msg: done\n"); + return msg_send(fd, MSG_OK_DONE); + } else { + if (sess->dinit_pid == -1) { + print_dbg("msg: start service manager\n"); + if (!dinit_start(*sess)) { + return false; + } + } + msg = MSG_OK_WAIT; + print_dbg("msg: wait\n"); + return msg_send(fd, MSG_OK_WAIT); + } + break; + } + case MSG_REQ_RLEN: { + auto *sess = get_session(fd); + /* send rundir length */ + if (!sess->rundir) { + /* send zero length */ + return msg_send(fd, MSG_DATA); + } + return msg_send(fd, MSG_ENCODE(std::strlen(sess->rundir))); + } + case MSG_REQ_RDATA: { + auto *sess = get_session(fd); + msg >>= MSG_TYPE_BITS; + if (msg == 0) { + return msg_send(fd, MSG_ERR); + } + unsigned int v = 0; + auto rlen = sess->rundir ? std::strlen(sess->rundir) : 0; + if (msg > rlen) { + return msg_send(fd, MSG_ERR); + } + auto *rstr = sess->rundir; + std::memcpy(&v, rstr + rlen - msg, MSG_SBYTES(msg)); + return msg_send(fd, MSG_ENCODE(v)); + } + case MSG_DATA: { + msg >>= MSG_TYPE_BITS; + /* can be uid, gid, homedir size, homedir data, + * rundir size or rundir data + */ + for ( + auto it = pending_conns.begin(); + it != pending_conns.end(); ++it + ) { + if (it->conn == fd) { + /* first message after welcome */ + if (it->pending_uid) { + print_dbg("msg: welcome uid %u\n", msg); + it->uid = msg; + it->pending_uid = 0; + return msg_send(fd, MSG_OK); + } + /* first message after uid */ + if (it->pending_gid) { + print_dbg( + "msg: welcome gid %u (uid %u)\n", msg, it->uid + ); + it->gid = msg; + it->pending_gid = 0; + return msg_send(fd, MSG_OK); + } + /* first message after gid */ + if (it->pending_hdir && !it->dirleft) { + print_dbg( + "msg: getting homedir for %u (length: %u)\n", + it->uid, msg + ); + /* no length or too long; reject */ + if (!msg || (msg > DIRLEN_MAX)) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + it->homedir = static_cast( + std::malloc(msg + 1) + ); + if (!it->homedir) { + print_dbg( + "msg: failed to alloc %u bytes for %u\n", + msg, it->uid + ); + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + it->dirleft = msg; + return msg_send(fd, MSG_OK); + } + if (it->pending_hdir && it->dirleft) { + auto pkt = MSG_SBYTES(it->dirleft); + std::memcpy(&it->homedir[it->dirgot], &msg, pkt); + it->dirgot += pkt; + it->dirleft -= pkt; + /* not done receiving homedir yet */ + if (it->dirleft) { + return msg_send(fd, MSG_OK); + } + it->pending_hdir = 0; + /* done receiving, sanitize */ + it->homedir[it->dirgot] = '\0'; + auto hlen = std::strlen(it->homedir); + if (!hlen) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + while (it->homedir[hlen - 1] == '/') { + it->homedir[--hlen] = '\0'; + } + if (!hlen) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + /* must be absolute */ + if (it->homedir[0] != '/') { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + struct stat s; + /* ensure the homedir exists and is a directory, + * this also ensures the path is safe to use in + * unsanitized contexts without escaping + */ + if (stat(it->homedir, &s) || !S_ISDIR(s.st_mode)) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + return msg_send(fd, MSG_OK); + } + /* any of the homedir pieces */ + if (it->pending_rdir) { + /* rundir is handled similarly to homedir */ + char buf[sizeof(RUNDIR_PATH) + 32]; + print_dbg( + "msg: getting rundir for %u (length: %u)\n", + it->uid, msg + ); + /* no length; that means we should make it up */ + if (!msg) { + print_dbg("msg: received zero length rundir\n"); + std::snprintf( + buf, sizeof(buf), RUNDIR_PATH, it->uid + ); + it->rundir = strdup(buf); + if (!it->rundir) { + print_dbg( + "msg: failed to allocate rundir for %u\n", + it->uid + ); + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + print_dbg( + "msg: made up rundir '%s' for %u\n", + it->rundir, it->uid + ); + it->dirgot = std::strlen(it->rundir); + it->dirleft = 0; + it->pending_rdir = 0; + it->managed_rdir = 1; + goto session_ack; + } + /* length too long; we should ignore rundir */ + if (msg > DIRLEN_MAX) { + print_dbg("msg: skipping rundir\n"); + it->rundir = nullptr; + it->dirgot = 0; + it->dirleft = 0; + it->pending_rdir = 0; + goto session_ack; + } + /* else allocate and receive chunks */ + it->rundir = static_cast( + std::malloc(msg + 1) + ); + if (!it->rundir) { + print_dbg( + "msg: failed to alloc %u bytes for %u\n", + msg, it->uid + ); + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + it->dirgot = 0; + it->dirleft = msg; + it->pending_rdir = 0; + return msg_send(fd, MSG_OK); + } + /* any of the rundir pieces */ + if (it->dirleft) { + auto pkt = MSG_SBYTES(it->dirleft); + std::memcpy(&it->rundir[it->dirgot], &msg, pkt); + it->dirgot += pkt; + it->dirleft -= pkt; + } + /* not done receiving rundir yet */ + if (it->dirleft) { + return msg_send(fd, MSG_OK); + } + /* we have received all, sanitize the rundir */ + if (it->rundir) { + it->rundir[it->dirgot] = '\0'; + auto rlen = std::strlen(it->rundir); + if (!rlen) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + while (it->rundir[rlen - 1] == '/') { + it->rundir[--rlen] = '\0'; + } + if (!rlen || (it->rundir[0] != '/')) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + } +session_ack: + /* acknowledge the session */ + print_dbg( + "msg: welcome %u (%s, %s)\n", it->uid, it->homedir, + it->rundir ? it->rundir : "no rundir" + ); + session *sess = nullptr; + for (auto &sessr: sessions) { + if (sessr.uid == it->uid) { + sess = &sessr; + break; + } + } + if (!sess) { + sess = &sessions.emplace_back(); + } + for (auto c: sess->conns) { + if (c == fd) { + print_dbg( + "msg: already have session %u\n", it->uid + ); + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + } + if (it->managed_rdir) { + print_dbg("msg: setup rundir for %u\n", it->uid); + if (!rundir_make(it->rundir, it->uid, it->gid)) { + pending_conns.erase(it); + return msg_send(fd, MSG_ERR); + } + } + print_dbg("msg: setup session %u\n", it->uid); + sess->conns.push_back(fd); + sess->uid = it->uid; + sess->gid = it->gid; + std::free(sess->homedir); + std::free(sess->rundir); + sess->homedir = it->homedir; + sess->rundir = it->rundir; + sess->manage_rdir = it->managed_rdir; + it->homedir = nullptr; + it->rundir = nullptr; + pending_conns.erase(it); + /* reply */ + return msg_send(fd, MSG_OK); + } + } + break; + } + default: + break; + } + /* unexpected message, terminate the connection */ + return false; +} + +static int sigpipe[2] = {-1, -1}; + +static void sighandler(int sign) { + write(sigpipe[1], &sign, sizeof(int)); +} + +static void conn_term(int conn) { + for (auto &sess: sessions) { + auto &conv = sess.conns; + for ( + auto cit = conv.begin(); cit != conv.end(); ++cit + ) { + if (*cit != conn) { + continue; + } + print_dbg( + "conn: close %d for session %u\n", + conn, sess.uid + ); + conv.erase(cit); + /* empty now; shut down session */ + if (conv.empty()) { + dinit_stop(sess); + sess.dinit_pid = -1; + } + close(conn); + return; + } + } + close(conn); +} + +static bool sock_new(char const *path, int &sock) { + sock = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (sock < 0) { + perror("socket failed"); + return false; + } + + print_dbg("socket: created %d for %s\n", sock, path); + + sockaddr_un un; + std::memset(&un, 0, sizeof(un)); + un.sun_family = AF_UNIX; + + auto plen = std::strlen(path); + if (plen >= sizeof(un.sun_path)) { + std::fprintf(stderr, "path name %s too long", path); + close(sock); + return false; + } + + std::memcpy(un.sun_path, path, plen + 1); + /* no need to check this */ + unlink(path); + + if (bind(sock, reinterpret_cast(&un), sizeof(un)) < 0) { + perror("bind failed"); + close(sock); + return false; + } + print_dbg("socket: bound %d for %s\n", sock, path); + + if (chmod(path, 0600) < 0) { + perror("chmod failed"); + goto fail; + } + print_dbg("socket: permissions set\n"); + + if (listen(sock, SOMAXCONN) < 0) { + perror("listen failed"); + goto fail; + } + print_dbg("socket: listen\n"); + + print_dbg("socket: done\n"); + return true; + +fail: + unlink(path); + close(sock); + return false; +} + +int main() { + if (signal(SIGCHLD, sighandler) == SIG_ERR) { + perror("signal failed"); + } + if (signal(SIGALRM, sighandler) == SIG_ERR) { + perror("signal failed"); + } + + /* prealloc a bunch of space */ + pending_conns.reserve(8); + sessions.reserve(16); + timers.reserve(16); + fds.reserve(64); + fifos.reserve(8); + + if (std::getenv("DINIT_USERSERVD_DEBUG")) { + debug = true; + } + + print_dbg("userservd: init signal fd\n"); + + { + struct stat pstat; + if (stat(SOCK_PATH, &pstat) || !S_ISDIR(pstat.st_mode)) { + /* create control directory */ + if (mkdir(SOCK_PATH, 0755)) { + perror("mkdir failed"); + return 1; + } + } + } + + /* use a strict mask */ + umask(077); + + /* signal pipe */ + { + if (pipe(sigpipe) < 0) { + perror("pipe failed"); + return 1; + } + auto &pfd = fds.emplace_back(); + pfd.fd = sigpipe[0]; + pfd.events = POLLIN; + } + + print_dbg("userservd: init control socket\n"); + + /* main control socket */ + { + if (!sock_new(DAEMON_SOCK, ctl_sock)) { + return 1; + } + auto &pfd = fds.emplace_back(); + pfd.fd = ctl_sock; + pfd.events = POLLIN; + } + + print_dbg("userservd: main loop\n"); + + std::size_t i = 0; + + /* main loop */ + for (;;) { + print_dbg("userservd: poll\n"); + auto pret = poll(fds.data(), fds.size(), -1); + if (pret < 0) { + /* interrupted by signal */ + if (errno == EINTR) { + goto do_compact; + } + perror("poll failed"); + return 1; + } else if (pret == 0) { + goto do_compact; + } + /* check signal fd */ + if (fds[0].revents == POLLIN) { + int sign; + if (read(fds[0].fd, &sign, sizeof(int)) != sizeof(int)) { + perror("signal read failed"); + goto do_compact; + } + if (sign == SIGALRM) { + print_dbg("userservd: sigalrm\n"); + /* timer, take the closest one */ + auto &tm = timers.front(); + /* find its session */ + for (auto &sess: sessions) { + if (sess.uid != tm.uid) { + continue; + } + print_dbg("userservd: drop session %u\n", sess.uid); + /* notify errors; this will make clients close their + * connections, and once all of them are gone, the + * server can safely terminate it + */ + for (auto c: sess.conns) { + msg_send(c, MSG_ERR); + } + break; + } + print_dbg("userservd: drop timer\n"); + timer_delete(tm.timer); + timers.erase(timers.begin()); + goto signal_done; + } + /* this is a SIGCHLD */ + pid_t wpid; + int status; + print_dbg("userservd: sigchld\n"); + /* reap */ + while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) { + /* deal with each dinit pid here */ + if (!dinit_restart(wpid)) { + std::fprintf( + stderr, "failed to restart dinit (%u)\n", + static_cast(wpid) + ); + /* this is an unrecoverable condition */ + return 1; + } + } + } +signal_done: + /* check incoming connections on control socket */ + if (fds[1].revents) { + for (;;) { + auto afd = accept4( + fds[1].fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC + ); + if (afd < 0) { + if (errno != EAGAIN) { + /* should not happen? disregard the connection */ + perror("accept4 failed"); + } + break; + } + auto &rfd = fds.emplace_back(); + rfd.fd = afd; + rfd.events = POLLIN | POLLHUP; + print_dbg("conn: accepted %d for %d\n", afd, fds[1].fd); + } + } + /* check on pipes */ + for (i = 2; i < fds.size(); ++i) { + if (fds[i].revents == 0) { + continue; + } + /* find if this is a pipe */ + session *sess = nullptr; + for (auto &sessr: sessions) { + if (fds[i].fd == sessr.userpipe) { + sess = &sessr; + break; + } + } + if (!sess) { + break; + } + if (fds[i].revents & POLLIN) { + /* input on pipe or connection */ + char b; + /* get a byte */ + if (read(fds[i].fd, &b, 1) == 1) { + /* notify session and clear dinit for wait */ + if (sess->dinit_wait) { + print_dbg("dinit: ready notification\n"); + unsigned int msg = MSG_OK_DONE; + for (auto c: sess->conns) { + if (send(c, &msg, sizeof(msg), 0) < 0) { + perror("conn: send failed"); + } + } + /* disarm an associated timer */ + print_dbg("dinit: disarm timer\n"); + for ( + auto it = timers.begin(); it != timers.end(); ++it + ) { + if (it->uid == sess->uid) { + timer_delete(it->timer); + timers.erase(it); + break; + } + } + sess->dinit_wait = false; + } else { + /* spurious, warn and eat it */ + fprintf(stderr, "fifo: got data but not waiting"); + } + } else { + perror("read failed"); + continue; + } + /* eat whatever else is in the pipe */ + while (read(fds[i].fd, &b, 1) == 1) {} + } + if (fds[i].revents & POLLHUP) { + dinit_clean(*sess); + fds[i].fd = -1; + fds[i].revents = 0; + continue; + } + } + /* check on connections */ + for (; i < fds.size(); ++i) { + if (fds[i].revents == 0) { + continue; + } + if (fds[i].revents & POLLHUP) { + conn_term(fds[i].fd); + fds[i].fd = -1; + fds[i].revents = 0; + continue; + } + if (fds[i].revents & POLLIN) { + /* input on connection */ + if (!handle_read(fds[i].fd)) { + fprintf( + stderr, "read: handler failed (terminate connection)\n" + ); + conn_term(fds[i].fd); + fds[i].fd = -1; + fds[i].revents = 0; + continue; + } + } + } +do_compact: + /* compact the descriptor list */ + for (auto it = fds.begin(); it != fds.end();) { + if (it->fd == -1) { + it = fds.erase(it); + } else { + ++it; + } + } + /* queue fifos after control socket */ + if (!fifos.empty()) { + fds.insert(fds.begin() + 2, fifos.begin(), fifos.end()); + fifos.clear(); + } + } + for (auto &fd: fds) { + if (fd.fd >= 0) { + close(fd.fd); + } + } + return 0; +} \ No newline at end of file diff --git a/dinit-userservd/dinit-userservd.install b/dinit-userservd/dinit-userservd.install new file mode 100644 index 0000000..52da455 --- /dev/null +++ b/dinit-userservd/dinit-userservd.install @@ -0,0 +1,15 @@ +post_upgrade() { + echo """ + POST INSTALL INSTRUCTIONS + --------------------------------- + you must add: session optional pam_dinit_userservd.so + to your /etc/pam.d/login or this package WILL NOT WORK + --------------------------------- + please install user service files to one of these locations (symlink to boot.d in config dir to enable): + ~/.config/dinit.d/ + /etc/dinit/user/ + /usr/lib/dinit.d/user/ + /usr/local/lib/dinit.d/user/ + --------------------------------- + """ +} diff --git a/dinit-userservd/meson.build b/dinit-userservd/meson.build new file mode 100644 index 0000000..e08c292 --- /dev/null +++ b/dinit-userservd/meson.build @@ -0,0 +1,37 @@ +project( + 'dinit-userservd', + ['cpp'], + version: '0.1.0', + default_options: [ + 'cpp_std=c++17', 'warning_level=3', 'buildtype=debugoptimized', + 'cpp_eh=none', 'cpp_rtti=false', + ], + license: 'BSD-2-Clause' +) + +cpp = meson.get_compiler('cpp') + +pam_dep = dependency('pam', required: true) +rt_dep = cpp.find_library('rt', required: false) + +daemon = executable( + 'dinit-userservd', 'dinit-userservd.cc', + install: true, + dependencies: [rt_dep], + gnu_symbol_visibility: 'hidden' +) + +pam_mod = shared_module( + 'pam_dinit_userservd', 'pam_dinit_userservd.cc', + install: true, + install_dir: join_paths(get_option('libdir'), 'security'), + name_prefix: '', + dependencies: [pam_dep], + gnu_symbol_visibility: 'hidden' +) + +install_data( + 'dinit-userservd', + install_dir: join_paths(get_option('sysconfdir'), 'dinit.d'), + install_mode: 'rw-r--r--' +) diff --git a/dinit-userservd/pam_dinit_userservd.cc b/dinit-userservd/pam_dinit_userservd.cc new file mode 100644 index 0000000..faeb9ac --- /dev/null +++ b/dinit-userservd/pam_dinit_userservd.cc @@ -0,0 +1,365 @@ +/* pam_dinit_userservd: the client part of dinit-userservd + * + * it connects to its socket and requests logins/logouts, + * communicating over a rudimentary protocol + * + * the PAM session opens a persistent connection, which also + * takes care of tracking when a session needs ending on the + * daemon side (once all connections are gone) + * + * Copyright 2021 Daniel "q66" Kolesa + * License: BSD-2-Clause + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "protocol.hh" + +#define PAMAPI __attribute__((visibility ("default"))) + +static void free_sock(pam_handle_t *, void *data, int) { + int sock = *static_cast(data); + if (sock != -1) { + close(sock); + } + free(data); +} + +static bool open_session( + pam_handle_t *pamh, unsigned int &uid, int argc, char const **argv, + unsigned int &orlen, char *orbuf, bool &set_rundir +) { + int *sock = static_cast(std::malloc(sizeof(int))); + if (!sock) { + return false; + } + +#if 0 + /* FIXME: this is problematic with gdm somehow, figure out why */ + bool do_rundir = true; + + /* overrides */ + for (int i = 0; i < argc; ++i) { + if (!std::strcmp(argv[i], "norundir")) { + do_rundir = false; + } + } +#else + bool do_rundir = false; +#endif + + /* blocking socket and a simple protocol */ + *sock = socket(AF_UNIX, SOCK_SEQPACKET, 0); + if (*sock == -1) { + return false; + } + + /* associate the socket with the session */ + if (pam_set_data( + pamh, "pam_dinit_session", sock, free_sock + ) != PAM_SUCCESS) { + return false; + } + + sockaddr_un saddr; + std::memset(&saddr, 0, sizeof(saddr)); + + saddr.sun_family = AF_UNIX; + std::memcpy(saddr.sun_path, DAEMON_SOCK, sizeof(DAEMON_SOCK)); + + char const *puser; + char const *hdir; + char const *rdir; + passwd *pwd; + int ret, hlen, rlen; + + auto send_msg = [sock](unsigned int msg) { + if (write(*sock, &msg, sizeof(msg)) < 0) { + return false; + } + return true; + }; + + if (pam_get_user(pamh, &puser, nullptr) != PAM_SUCCESS) { + goto err; + } + + pwd = getpwnam(puser); + if (!pwd) { + goto err; + } + uid = pwd->pw_uid; + + hdir = pam_getenv(pamh, "HOME"); + if (!hdir || !hdir[0]) { + hdir = pwd->pw_dir; + } + if (!hdir || !hdir[0]) { + goto err; + } + hlen = strlen(hdir); + if (hlen > DIRLEN_MAX) { + goto err; + } + /* this is verified serverside too but bail out early if needed */ + if (struct stat s; stat(hdir, &s) || !S_ISDIR(s.st_mode)) { + goto err; + } + + /* the other runtime dir manager is expected to ensure that the + * rundir actually exists by this point (logind does ensure it) + */ + rdir = pam_getenv(pamh, "XDG_RUNTIME_DIR"); + if (!rdir) { + rdir = ""; + } + rlen = strlen(rdir); + if (rlen > DIRLEN_MAX) { + goto err; + } else if (rlen == 0) { + set_rundir = do_rundir; + } + + if (connect( + *sock, reinterpret_cast(&saddr), sizeof(saddr) + ) < 0) { + goto err; + } + + if (!send_msg(MSG_START)) { + goto err; + } + /* main message loop */ + { + unsigned int msg; + unsigned int state = 0; + bool sent_uid = false; + bool sent_gid = false; + bool sent_hlen = false; + bool sent_rlen = false; + bool got_rlen = false; + char *rbuf = orbuf; + + auto send_strpkt = [&send_msg](char const *&sdir, int &slen) { + unsigned int pkt = 0; + auto psize = MSG_SBYTES(slen); + std::memcpy(&pkt, sdir, psize); + pkt <<= MSG_TYPE_BITS; + pkt |= MSG_DATA; + if (!send_msg(pkt)) { + return false; + } + sdir += psize; + slen -= psize; + return true; + }; + + for (;;) { + ret = read(*sock, &msg, sizeof(msg)); + if (ret < 0) { + goto err; + } + switch (state) { + case 0: + /* session not established yet */ + if (msg != MSG_OK) { + goto err; + } + /* send uid */ + if (!sent_uid) { + if (!send_msg(MSG_ENCODE(pwd->pw_uid))) { + goto err; + } + sent_uid = true; + break; + } + /* send gid */ + if (!sent_gid) { + if (!send_msg(MSG_ENCODE(pwd->pw_gid))) { + goto err; + } + sent_gid = true; + break; + } + /* send homedir len */ + if (!sent_hlen) { + if (!send_msg(MSG_ENCODE(hlen))) { + goto err; + } + sent_hlen = true; + break; + } + /* send a piece of homedir */ + if (hlen) { + if (!send_strpkt(hdir, hlen)) { + goto err; + } + break; + } + /* send rundir len */ + if (!sent_rlen) { + auto srlen = rlen; + if (!srlen && !do_rundir) { + srlen = DIRLEN_MAX + 1; + } + if (!send_msg(MSG_ENCODE(srlen))) { + goto err; + } + sent_rlen = true; + break; + } + /* send a piece of rundir */ + if (rlen) { + if (!send_strpkt(rdir, rlen)) { + goto err; + } + break; + } + /* send clientside OK */ + state = msg; + if (!send_msg(MSG_OK)) { + goto err; + } + break; + case MSG_OK: + /* if started, get the rundir back; else block */ + if ((msg == MSG_OK_DONE) || (msg == MSG_OK_WAIT)) { + state = msg; + if ((msg == MSG_OK_DONE) && !send_msg(MSG_REQ_RLEN)) { + goto err; + } + continue; + } + /* bad message */ + goto err; + case MSG_OK_WAIT: + /* if we previously waited and now got another message, + * it means either an error or that the system is now + * fully ready + */ + if (msg == MSG_OK_DONE) { + state = msg; + if (!send_msg(MSG_REQ_RLEN)) { + goto err; + } + continue; + } + /* bad message */ + goto err; + case MSG_OK_DONE: { + if ((msg & MSG_TYPE_MASK) != MSG_DATA) { + goto err; + } + /* after MSG_OK_DONE, we should receive the runtime dir + * length first; if zero, it means we are completely done + */ + msg >>= MSG_TYPE_BITS; + if (!got_rlen) { + if (msg == 0) { + orlen = 0; + return true; + } else if (msg > DIRLEN_MAX) { + goto err; + } + got_rlen = true; + rlen = int(msg); + orlen = msg; + if (!send_msg(MSG_ENCODE_AUX(rlen, MSG_REQ_RDATA))) { + goto err; + } + continue; + } + /* we are receiving the string... */ + int pkts = MSG_SBYTES(rlen); + std::memcpy(rbuf, &msg, pkts); + rbuf += pkts; + rlen -= pkts; + if (rlen == 0) { + /* we have received the whole thing, terminate */ + *rbuf = '\0'; + return true; + } + if (!send_msg(MSG_ENCODE_AUX(rlen, MSG_REQ_RDATA))) { + goto err; + } + /* keep receiving pieces */ + continue; + } + default: + goto err; + } + } + } + + return true; + +err: + close(*sock); + *sock = -1; + return false; +} + +extern "C" PAMAPI int pam_sm_open_session( + pam_handle_t *pamh, int, int argc, char const **argv +) { + unsigned int uid, rlen = 0; + bool set_rundir = false; + /* potential rundir we are managing */ + char rdir[DIRLEN_MAX + 1]; + if (!open_session(pamh, uid, argc, argv, rlen, rdir, set_rundir)) { + return PAM_SESSION_ERR; + } + if (rlen) { + char const dpfx[] = "DBUS_SESSION_BUS_ADDRESS=unix:path="; + char buf[sizeof(rdir) + sizeof(dpfx) + 4]; + + /* try exporting a dbus session bus variable */ + std::snprintf(buf, sizeof(buf), "%s%s/bus", dpfx, rdir); + + struct stat sbuf; + if (!lstat(strchr(buf, '/'), &sbuf) && S_ISSOCK(sbuf.st_mode)) { + if (pam_putenv(pamh, buf) != PAM_SUCCESS) { + return PAM_SESSION_ERR; + } + } + + if (!set_rundir) { + return PAM_SUCCESS; + } + + /* set rundir too if needed */ + if (pam_misc_setenv(pamh, "XDG_RUNTIME_DIR", rdir, 1) != PAM_SUCCESS) { + return PAM_SESSION_ERR; + } + } + return PAM_SUCCESS; +} + +extern "C" PAMAPI int pam_sm_close_session( + pam_handle_t *pamh, int, int, char const ** +) { + void const *data; + /* there is nothing we can do here */ + if (pam_get_data(pamh, "pam_dinit_session", &data) != PAM_SUCCESS) { + return PAM_SUCCESS; + } + int sock = *static_cast(data); + if (sock < 0) { + return PAM_SUCCESS; + } + /* close the session */ + close(sock); + return PAM_SUCCESS; +} diff --git a/dinit-userservd/protocol.hh b/dinit-userservd/protocol.hh new file mode 100644 index 0000000..8c16d3c --- /dev/null +++ b/dinit-userservd/protocol.hh @@ -0,0 +1,100 @@ +/* defines the simple protocol between the daemon and the PAM module + * + * Copyright 2021 Daniel "q66" Kolesa + * License: BSD-2-Clause + */ + +#ifndef DINIT_USERSERVD_PROTOCOL_HH +#define DINIT_USERSERVD_PROTOCOL_HH + +#include + +#define RUNDIR_PATH "/run/user/%u" +#define SOCK_PATH "/run/dinit-userservd" +#define DAEMON_SOCK SOCK_PATH"/control.sock" +#define USER_PATH SOCK_PATH"/%u" +#define USER_FIFO USER_PATH"/dinit.fifo" +#define USER_DIR USER_PATH"/dinit.XXXXXX" + +/* sanity check */ +static_assert( + sizeof(DAEMON_SOCK) > sizeof(decltype(sockaddr_un{}.sun_family)) +); + +/* maximum length of a directory path we can receive */ +#define DIRLEN_MAX 1024 + +/* protocol messages + * + * this is a simple protocol consisting of uint-sized messages; each + * message carries the type (4 bits) and optionally auxiliary data + * (only some messages; MSG_DATA and MSG_REQ_RDATA) + * + * dinit-userservd is the server; the pam module is the client + * + * the client connects to DAEMON_SOCK (seqpacket sockets are used) + * + * from there, the following sequence happens: + * + * CLIENT: sends MSG_START and enters a message loop (state machine) + * SERVER: receives it and adds the session into pending connections, + * then responds MSG_OK + * CLIENT: consumes MSG_OK, sends MSG_DATA with user id attached + * SERVER: responds MSG_OK + * CLIENT: consumes MSG_OK, sends MSG_DATA with group id attached + * SERVER: responds MSG_OK + * CLIENT: consumes MSG_OK, sends MSG_DATA with homedir length attached + * SERVER: validates, allocates a data buffer and responds MSG_OK + * loop: + * CLIENT: consumes MSG_OK, if there is any of homedir left unsent, + * it sends it; otherwise loop ends + * SERVER: adds to buffer, responds MSG_OK + * CLIENT: consumes MSG_OK, sends MSG_DATA with rundir length attached; + * if no rundir is set clientside, sends 0 instead and the server + * will make its own; if rundir handling is intentionally skipped, + * DIRLEN_MAX+1 is sent instead and the server will disregard it + * loop: same as above, but for rundir (nothing is sent for 0 length); + * at the end, server acknowledges the session and replies MSG_OK + * CLIENT: sends MSG_OK to confirm everything is ready on its side + * SERVER: if service manager for the user is already running, responds + * with MSG_OK_DONE; else initiates startup and responds with + * MSG_OK_WAIT + * CLIENT: if MSG_OK_WAIT was received, waits for a message + * SERVER: once service manager starts, MSG_OK_DONE is sent + * CLIENT: sends MSG_REQ_RLEN + * SERVER: responds with MSG_DATA with rundir length (0 if not known) + * loop: + * CLIENT: sends MSG_REQ_RDATA with number of remaining bytes of rundir + * that are yet to be received + * SERVER: responds with a MSG_DATA packet until none is left + * CLIENT: finishes startup, exports XDG_RUNTIME_DIR if needed as well + * as DBUS_SESSION_BUS_ADDRESS, and everything is done + */ + +/* this is a regular unsigned int */ +enum { + /* sent by the server as an acknowledgement of a message, and by + * the client once it has sent all the session info + */ + MSG_OK = 0x1, + MSG_OK_WAIT, /* login, wait */ + MSG_OK_DONE, /* ready, proceed */ + MSG_REQ_RLEN, /* rundir length request */ + MSG_REQ_RDATA, /* rundir string request + how much is left */ + MSG_DATA, + MSG_START, + /* sent by server on errors */ + MSG_ERR, + + MSG_TYPE_BITS = 4, + MSG_TYPE_MASK = 0xF, + MSG_DATA_BYTES = sizeof(unsigned int) - 1 +}; + +#define MSG_ENCODE_AUX(v, tp) \ + (tp | (static_cast(v) << MSG_TYPE_BITS)) + +#define MSG_ENCODE(v) MSG_ENCODE_AUX(v, MSG_DATA) +#define MSG_SBYTES(len) std::min(int(MSG_DATA_BYTES), int(len)) + +#endif