PKGBUILDS/dinit-userservd/dinit-userservd.cc

1161 lines
38 KiB
C++

/* 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 <q66@chimera-linux.org>
* License: BSD-2-Clause
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* accept4 */
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstddef>
#include <cerrno>
#include <cassert>
#include <climits>
#include <ctime>
#include <limits>
#include <vector>
#include <algorithm>
#include <poll.h>
#include <fcntl.h>
#include <signal.h>
#include <dirent.h>
#include <unistd.h>
#include <spawn.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#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<int> 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<session> sessions;
static std::vector<pending_conn> pending_conns;
/* file descriptors for poll */
static std::vector<pollfd> fds;
/* control IPC socket */
static int ctl_sock;
/* requests for new FIFOs; picked up by the event loop and cleared */
static std::vector<pollfd> fifos;
/* timer list */
static std::vector<session_timer> timers;
#define print_dbg(...) if (debug) { printf(__VA_ARGS__); }
static constexpr int const UID_DIGITS = \
std::numeric_limits<unsigned int>::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<char **>(argp), const_cast<char **>(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<char *>(
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<char *>(
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<sockaddr const *>(&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<unsigned int>(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;
}