"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
gotd listen process
To:
gameoftrees@openbsd.org
Date:
Fri, 9 Dec 2022 11:26:14 +0100

Download raw body.

Thread
  • Stefan Sperling:

    gotd listen process

Add a gotd "listen" process which watches the unix socket and
passes new client connections to the parent.

This is a first step towards better privsep. By itself this doesn't
buy us much. A lot more code remains to be written in order to
untangle things properly.

ok?
 
diff 30eb1ad6b8cdffd307215e60396bdcd58e7ccf17 ecdc4b4ea93107606601c584c90cb821d32d76a3
commit - 30eb1ad6b8cdffd307215e60396bdcd58e7ccf17
commit + ecdc4b4ea93107606601c584c90cb821d32d76a3
blob - c10b6a5df87a4c719d3bb926aff2316341052ac3
blob + 150c4e0f6fded4edfa8704bb486d56939d38c114
--- gotd/Makefile
+++ gotd/Makefile
@@ -8,7 +8,7 @@ SRCS=		gotd.c auth.c repo_read.c repo_write.c log.c pr
 
 PROG=		gotd
 SRCS=		gotd.c auth.c repo_read.c repo_write.c log.c privsep_stub.c \
-		imsg.c parse.y pack_create.c ratelimit.c deltify.c \
+		listen.c imsg.c parse.y pack_create.c ratelimit.c deltify.c \
 		bloom.c buf.c date.c deflate.c delta.c delta_cache.c error.c \
 		gitconfig.c gotconfig.c inflate.c lockfile.c murmurhash2.c \
 		object.c object_cache.c object_create.c object_idset.c \
blob - e18e72315e0f9d45caece8de0bbff6a0b31fadf9
blob + 3c62987cb615e5a1580f36670da87e9eecf8ea1c
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -58,6 +58,7 @@
 
 #include "gotd.h"
 #include "log.h"
+#include "listen.h"
 #include "auth.h"
 #include "repo_read.h"
 #include "repo_write.h"
@@ -91,7 +92,6 @@ static int inflight;
 static SIPHASH_KEY clients_hash_key;
 volatile int client_cnt;
 static struct timeval timeout = { 3600, 0 };
-static int inflight;
 static struct gotd gotd;
 
 void gotd_sighdlr(int sig, short event, void *arg);
@@ -188,27 +188,6 @@ static int
 	return NULL;
 }
 
-static int
-accept_reserve(int fd, struct sockaddr *addr, socklen_t *addrlen,
-    int reserve, volatile int *counter)
-{
-	int ret;
-
-	if (getdtablecount() + reserve +
-	    ((*counter + 1) * GOTD_FD_NEEDED) >= getdtablesize()) {
-		log_debug("inflight fds exceeded");
-		errno = EMFILE;
-		return -1;
-	}
-
-	if ((ret = accept4(fd, addr, addrlen,
-	    SOCK_NONBLOCK | SOCK_CLOEXEC)) > -1) {
-		(*counter)++;
-	}
-
-	return ret;
-}
-
 static uint64_t
 client_hash(uint32_t client_id)
 {
@@ -238,20 +217,6 @@ static uint32_t
 	return NULL;
 }
 
-static uint32_t
-get_client_id(void)
-{
-	int duplicate = 0;
-	uint32_t id;
-
-	do {
-		id = arc4random();
-		duplicate = (find_client(id) != NULL);
-	} while (duplicate || id == 0);
-
-	return id;
-}
-
 static struct gotd_child_proc *
 get_client_proc(struct gotd_client *client)
 {
@@ -334,6 +299,7 @@ disconnect(struct gotd_client *client)
 {
 	struct gotd_imsg_disconnect idisconnect;
 	struct gotd_child_proc *proc = get_client_proc(client);
+	struct gotd_child_proc *listen_proc = &gotd.procs[0];
 	uint64_t slot;
 
 	log_debug("uid %d: disconnecting", client->euid);
@@ -345,6 +311,12 @@ disconnect(struct gotd_client *client)
 		    &idisconnect, sizeof(idisconnect)) == -1)
 			log_warn("imsg compose DISCONNECT");
 	}
+
+	if (gotd_imsg_compose_event(&listen_proc->iev,
+	    GOTD_IMSG_DISCONNECT, PROC_GOTD, -1,
+	    &idisconnect, sizeof(idisconnect)) == -1)
+		log_warn("imsg compose DISCONNECT");
+
 	slot = client_hash(client->id) % nitems(gotd_clients);
 	STAILQ_REMOVE(&gotd_clients[slot], client, gotd_client, entry);
 	imsg_clear(&client->iev.ibuf);
@@ -365,7 +337,6 @@ disconnect(struct gotd_client *client)
 	}
 	free(client->capabilities);
 	free(client);
-	inflight--;
 	client_cnt--;
 }
 
@@ -1231,65 +1202,50 @@ static void
 	disconnect(client);
 }
 
-static void
-gotd_accept(int fd, short event, void *arg)
+static const struct got_error *
+recv_connect(uint32_t *client_id, struct imsg *imsg)
 {
-	struct sockaddr_storage ss;
-	struct timeval backoff;
-	socklen_t len;
+	const struct got_error *err = NULL;
+	struct gotd_imsg_connect iconnect;
+	size_t datalen;
 	int s = -1;
 	struct gotd_client *client = NULL;
 	uid_t euid;
 	gid_t egid;
 
-	backoff.tv_sec = 1;
-	backoff.tv_usec = 0;
+	*client_id = 0;
 
-	if (event_add(&gotd.ev, NULL) == -1) {
-		log_warn("event_add");
-		return;
-	}
-	if (event & EV_TIMEOUT)
-		return;
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(iconnect))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&iconnect, imsg->data, sizeof(iconnect));
 
-	len = sizeof(ss);
-
-	s = accept_reserve(fd, (struct sockaddr *)&ss, &len, GOTD_FD_RESERVE,
-	    &inflight);
-
+	s = imsg->fd;
 	if (s == -1) {
-		switch (errno) {
-		case EINTR:
-		case EWOULDBLOCK:
-		case ECONNABORTED:
-			return;
-		case EMFILE:
-		case ENFILE:
-			event_del(&gotd.ev);
-			evtimer_add(&gotd.pause, &backoff);
-			return;
-		default:
-			log_warn("%s: accept", __func__);
-			return;
-		}
+		err = got_error(GOT_ERR_PRIVSEP_NO_FD);
+		goto done;
 	}
 
-	if (client_cnt >= GOTD_MAXCLIENTS)
-		goto err;
+	if (find_client(iconnect.client_id)) {
+		err = got_error_msg(GOT_ERR_CLIENT_ID, "duplicate client ID");
+		goto done;
+	}
 
 	if (getpeereid(s, &euid, &egid) == -1) {
-		log_warn("%s: getpeereid", __func__);
-		goto err;
+		err = got_error_from_errno("getpeerid");
+		goto done;
 	}
 
 	client = calloc(1, sizeof(*client));
 	if (client == NULL) {
-		log_warn("%s: calloc", __func__);
-		goto err;
+		err = got_error_from_errno("calloc");
+		goto done;
 	}
 
+	*client_id = iconnect.client_id;
+
 	client->state = GOTD_STATE_EXPECT_LIST_REFS;
-	client->id = get_client_id();
+	client->id = iconnect.client_id;
 	client->fd = s;
 	s = -1;
 	client->delta_cache_fd = -1;
@@ -1311,22 +1267,27 @@ gotd_accept(int fd, short event, void *arg)
 	add_client(client);
 	log_debug("%s: new client uid %d connected on fd %d", __func__,
 	    client->euid, client->fd);
-	return;
-err:
-	inflight--;
-	if (s != -1)
-		close(s);
-	free(client);
-}
+done:
+	if (err) {
+		struct gotd_child_proc *listen_proc = &gotd.procs[0];
+		struct gotd_imsg_disconnect idisconnect;
 
-static void
-gotd_accept_paused(int fd, short event, void *arg)
-{
-	event_add(&gotd.ev, NULL);
+		idisconnect.client_id = client->id;
+		if (gotd_imsg_compose_event(&listen_proc->iev,
+		    GOTD_IMSG_DISCONNECT, PROC_GOTD, -1,
+		    &idisconnect, sizeof(idisconnect)) == -1)
+			log_warn("imsg compose DISCONNECT");
+
+		if (s != -1)
+			close(s);
+	}
+
+	return err;
 }
 
 static const char *gotd_proc_names[PROC_MAX] = {
 	"parent",
+	"listen",
 	"repo_read",
 	"repo_write"
 };
@@ -1452,22 +1413,32 @@ verify_imsg_src(struct gotd_client *client, struct got
 	struct gotd_child_proc *client_proc;
 	int ret = 0;
 
-	client_proc = get_client_proc(client);
-	if (client_proc == NULL)
-		fatalx("no process found for uid %d", client->euid);
-
-	if (proc->pid != client_proc->pid) {
-		kill_proc(proc, 1);
-		log_warnx("received message from PID %d for uid %d, while "
-		    "PID %d is the process serving this user",
-		    proc->pid, client->euid, client_proc->pid);
-		return 0;
+	if (proc->type == PROC_REPO_READ || proc->type == PROC_REPO_WRITE) {
+		client_proc = get_client_proc(client);
+		if (client_proc == NULL)
+			fatalx("no process found for uid %d", client->euid);
+		if (proc->pid != client_proc->pid) {
+			kill_proc(proc, 1);
+			log_warnx("received message from PID %d for uid %d, "
+			    "while PID %d is the process serving this user",
+			    proc->pid, client->euid, client_proc->pid);
+			return 0;
+		}
 	}
 
 	switch (imsg->hdr.type) {
 	case GOTD_IMSG_ERROR:
 		ret = 1;
 		break;
+	case GOTD_IMSG_CONNECT:
+		if (proc->type != PROC_LISTEN) {
+			err = got_error_fmt(GOT_ERR_BAD_PACKET,
+			    "new connection for uid %d from PID %d "
+			    "which is not the listen process",
+			    proc->pid, client->euid);
+		} else
+			ret = 1;
+		break;
 	case GOTD_IMSG_PACKFILE_DONE:
 		err = ensure_proc_is_reading(client, proc);
 		if (err)
@@ -1897,6 +1868,9 @@ gotd_dispatch(int fd, short event, void *arg)
 			do_disconnect = 1;
 			err = gotd_imsg_recv_error(&client_id, &imsg);
 			break;
+		case GOTD_IMSG_CONNECT:
+			err = recv_connect(&client_id, &imsg);
+			break;
 		case GOTD_IMSG_PACKFILE_DONE:
 			do_disconnect = 1;
 			err = recv_packfile_done(&client_id, &imsg);
@@ -1992,6 +1966,9 @@ start_child(enum gotd_procid proc_id, const char *chro
 
 	argv[argc++] = argv0;
 	switch (proc_id) {
+	case PROC_LISTEN:
+		argv[argc++] = (char *)"-L";
+		break;
 	case PROC_REPO_READ:
 		argv[argc++] = (char *)"-R";
 		break;
@@ -2005,8 +1982,10 @@ start_child(enum gotd_procid proc_id, const char *chro
 	argv[argc++] = (char *)"-f";
 	argv[argc++] = (char *)confpath;
 
-	argv[argc++] = (char *)"-P";
-	argv[argc++] = (char *)chroot_path;
+	if (chroot_path) {
+		argv[argc++] = (char *)"-P";
+		argv[argc++] = (char *)chroot_path;
+	}
 
 	if (!daemonize)
 		argv[argc++] = (char *)"-d";
@@ -2021,6 +2000,25 @@ start_repo_children(struct gotd *gotd, char *argv0, co
 }
 
 static void
+start_listener(char *argv0, const char *confpath, int daemonize, int verbosity)
+{
+	struct gotd_child_proc *proc = &gotd.procs[0];
+
+	proc->type = PROC_LISTEN;
+
+	if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+	    PF_UNSPEC, proc->pipe) == -1)
+		fatal("socketpair");
+
+	proc->pid = start_child(proc->type, NULL, argv0, confpath,
+	    proc->pipe[1], daemonize, verbosity);
+	imsg_init(&proc->iev.ibuf, proc->pipe[0]);
+	proc->iev.handler = gotd_dispatch;
+	proc->iev.events = EV_READ;
+	proc->iev.handler_arg = NULL;
+}
+
+static void
 start_repo_children(struct gotd *gotd, char *argv0, const char *confpath,
     int daemonize, int verbosity)
 {
@@ -2028,19 +2026,11 @@ start_repo_children(struct gotd *gotd, char *argv0, co
 	struct gotd_child_proc *proc;
 	int i;
 
-	/*
-	 * XXX For now, use one reader and one writer per repository.
-	 * This should be changed to N readers + M writers.
-	 */
-	gotd->nprocs = gotd->nrepos * 2;
-	gotd->procs = calloc(gotd->nprocs, sizeof(*gotd->procs));
-	if (gotd->procs == NULL)
-		fatal("calloc");
-	for (i = 0; i < gotd->nprocs; i++) {
+	for (i = 1; i < gotd->nprocs; i++) {
 		if (repo == NULL)
 			repo = TAILQ_FIRST(&gotd->repos);
 		proc = &gotd->procs[i];
-		if (i < gotd->nrepos)
+		if (i - 1 < gotd->nrepos)
 			proc->type = PROC_REPO_READ;
 		else
 			proc->type = PROC_REPO_WRITE;
@@ -2105,7 +2095,7 @@ main(int argc, char **argv)
 
 	log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
 
-	while ((ch = getopt(argc, argv, "df:nP:RvW")) != -1) {
+	while ((ch = getopt(argc, argv, "df:LnP:RvW")) != -1) {
 		switch (ch) {
 		case 'd':
 			daemonize = 0;
@@ -2113,6 +2103,9 @@ main(int argc, char **argv)
 		case 'f':
 			confpath = optarg;
 			break;
+		case 'L':
+			proc_id = PROC_LISTEN;
+			break;
 		case 'n':
 			noaction = 1;
 			break;
@@ -2182,7 +2175,7 @@ main(int argc, char **argv)
 		    gotd.unix_group_name);
 	}
 
-	if (proc_id == PROC_GOTD &&
+	if (proc_id == PROC_LISTEN &&
 	    !got_path_is_absolute(gotd.unix_socket_path))
 		fatalx("bad unix socket path \"%s\": must be an absolute path",
 		    gotd.unix_socket_path);
@@ -2191,6 +2184,25 @@ main(int argc, char **argv)
 		return 0;
 
 	if (proc_id == PROC_GOTD) {
+		gotd.pid = getpid();
+		snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]);
+		/*
+		 * Start a listener and repository readers/writers.
+		 * XXX For now, use one reader and one writer per repository.
+		 * This should be changed to N readers + M writers.
+		 */
+		gotd.nprocs = 1 + gotd.nrepos * 2;
+		gotd.procs = calloc(gotd.nprocs, sizeof(*gotd.procs));
+		if (gotd.procs == NULL)
+			fatal("calloc");
+		start_listener(argv0, confpath, daemonize, verbosity);
+		start_repo_children(&gotd, argv0, confpath, daemonize,
+		    verbosity);
+		arc4random_buf(&clients_hash_key, sizeof(clients_hash_key));
+		if (daemonize && daemon(1, 0) == -1)
+			fatal("daemon");
+	} else if (proc_id == PROC_LISTEN) {
+		snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]);
 		if (verbosity) {
 			log_info("socket: %s", gotd.unix_socket_path);
 			log_info("user: %s", pw->pw_name);
@@ -2203,15 +2215,11 @@ main(int argc, char **argv)
 			fatal("cannot listen on unix socket %s",
 			    gotd.unix_socket_path);
 		}
-	}
-
-	if (proc_id == PROC_GOTD) {
-		gotd.pid = getpid();
-		snprintf(title, sizeof(title), "%s", gotd_proc_names[proc_id]);
-		start_repo_children(&gotd, argv0, confpath, daemonize,
-		    verbosity);
-		arc4random_buf(&clients_hash_key, sizeof(clients_hash_key));
-		if (daemonize && daemon(0, 0) == -1)
+		if (chroot(GOTD_EMPTY_PATH) == -1)
+			fatal("chroot");
+		if (chdir("/") == -1)
+			fatal("chdir(\"/\")");
+		if (daemonize && daemon(1, 0) == -1)
 			fatal("daemon");
 	} else if (proc_id == PROC_REPO_READ || proc_id == PROC_REPO_WRITE) {
 		error = got_repo_pack_fds_open(&pack_fds);
@@ -2252,6 +2260,14 @@ main(int argc, char **argv)
 			err(1, "pledge");
 #endif
 		break;
+	case PROC_LISTEN:
+#ifndef PROFILE
+		if (pledge("stdio sendfd unix", NULL) == -1)
+			err(1, "pledge");
+#endif
+		listen_main(title, fd);
+		/* NOTREACHED */
+		break;
 	case PROC_REPO_READ:
 #ifndef PROFILE
 		if (pledge("stdio rpath recvfd", NULL) == -1)
@@ -2288,15 +2304,10 @@ main(int argc, char **argv)
 	signal_add(&evsighup, NULL);
 	signal_add(&evsigusr1, NULL);
 
-	event_set(&gotd.ev, fd, EV_READ | EV_PERSIST, gotd_accept, NULL);
-	if (event_add(&gotd.ev, NULL))
-		fatalx("event add");
-	evtimer_set(&gotd.pause, gotd_accept_paused, NULL);
+	gotd_imsg_event_add(&gotd.procs[0].iev);
 
 	event_dispatch();
 
-	if (fd != -1)
-		close(fd);
 	if (pack_fds)
 		got_repo_pack_fds_close(pack_fds);
 	free(repo_path);
blob - 8773697218bbd2c0f2f0b4320d20311c0ff48300
blob + 9ec3939ca07f41efabccdf15c8ed41e0a606fe86
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -20,6 +20,7 @@
 #define GOTD_UNIX_GROUP	"_gotsh"
 #define GOTD_USER	"_gotd"
 #define GOTD_CONF_PATH	"/etc/gotd.conf"
+#define GOTD_EMPTY_PATH	"/var/empty"
 
 #define GOTD_MAXCLIENTS		1024
 #define GOTD_FD_RESERVE		5
@@ -31,6 +32,7 @@ enum gotd_procid {
 
 enum gotd_procid {
 	PROC_GOTD	= 0,
+	PROC_LISTEN,
 	PROC_REPO_READ,
 	PROC_REPO_WRITE,
 	PROC_MAX,
@@ -113,8 +115,6 @@ struct gotd {
 	struct gotd_repolist repos;
 	int nrepos;
 	int verbosity;
-	struct event ev;
-	struct event pause;
 	struct gotd_child_proc *procs;
 	int nprocs;
 };
@@ -169,8 +169,9 @@ enum gotd_imsg_type {
 	GOTD_IMSG_REF_UPDATE_NG, /* Update was not good. */
 	GOTD_IMSG_REFS_UPDATED, /* The server proccessed all ref updates. */
 
-	/* Client is disconnecting. */
+	/* Client connections. */
 	GOTD_IMSG_DISCONNECT,
+	GOTD_IMSG_CONNECT,
 };
 
 /* Structure for GOTD_IMSG_ERROR. */
@@ -399,6 +400,11 @@ int parse_config(const char *, enum gotd_procid, struc
 	uint32_t client_id;
 };
 
+/* Structure for GOTD_IMSG_CONNECT. */
+struct gotd_imsg_connect {
+	uint32_t client_id;
+};
+
 int parse_config(const char *, enum gotd_procid, struct gotd *);
 
 /* imsg.c */
blob - /dev/null
blob + 96427e857a4b693de2dec2665df312e682a43f73 (mode 644)
--- /dev/null
+++ gotd/listen.c
@@ -0,0 +1,388 @@
+/*
+ * Copyright (c) 2022 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+#include <errno.h>
+#include <event.h>
+#include <siphash.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <imsg.h>
+#include <limits.h>
+#include <sha1.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include "got_error.h"
+
+#include "gotd.h"
+#include "log.h"
+#include "listen.h"
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+struct gotd_listen_client {
+	STAILQ_ENTRY(gotd_listen_client)	 entry;
+	uint32_t			 id;
+	int				 fd;
+};
+STAILQ_HEAD(gotd_listen_clients, gotd_listen_client);
+
+static struct gotd_listen_clients gotd_listen_clients[GOTD_CLIENT_TABLE_SIZE];
+static SIPHASH_KEY clients_hash_key;
+static volatile int listen_client_cnt;
+static int inflight;
+
+static struct {
+	pid_t pid;
+	const char *title;
+	int fd;
+	struct gotd_imsgev iev;
+	struct gotd_imsgev pause;
+} gotd_listen;
+
+static int inflight;
+
+static void listen_shutdown(void);
+
+static void
+listen_sighdlr(int sig, short event, void *arg)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGHUP:
+		break;
+	case SIGUSR1:
+		break;
+	case SIGTERM:
+	case SIGINT:
+		listen_shutdown();
+		/* NOTREACHED */
+		break;
+	default:
+		fatalx("unexpected signal");
+	}
+}
+
+static uint64_t
+client_hash(uint32_t client_id)
+{
+	return SipHash24(&clients_hash_key, &client_id, sizeof(client_id));
+}
+
+static void
+add_client(struct gotd_listen_client *client)
+{
+	uint64_t slot = client_hash(client->id) % nitems(gotd_listen_clients);
+	STAILQ_INSERT_HEAD(&gotd_listen_clients[slot], client, entry);
+	listen_client_cnt++;
+}
+
+static struct gotd_listen_client *
+find_client(uint32_t client_id)
+{
+	uint64_t slot;
+	struct gotd_listen_client *c;
+
+	slot = client_hash(client_id) % nitems(gotd_listen_clients);
+	STAILQ_FOREACH(c, &gotd_listen_clients[slot], entry) {
+		if (c->id == client_id)
+			return c;
+	}
+
+	return NULL;
+}
+
+static uint32_t
+get_client_id(void)
+{
+	int duplicate = 0;
+	uint32_t id;
+
+	do {
+		id = arc4random();
+		duplicate = (find_client(id) != NULL);
+	} while (duplicate || id == 0);
+
+	return id;
+}
+
+static const struct got_error *
+disconnect(struct gotd_listen_client *client)
+{
+	uint64_t slot;
+	int client_fd;
+
+	log_debug("client on fd %d disconnecting", client->fd);
+
+	slot = client_hash(client->id) % nitems(gotd_listen_clients);
+	STAILQ_REMOVE(&gotd_listen_clients[slot], client,
+	    gotd_listen_client, entry);
+	client_fd = client->fd;
+	free(client);
+	inflight--;
+	listen_client_cnt--;
+	if (close(client_fd) == -1)
+		return got_error_from_errno("close");
+
+	return NULL;
+}
+
+static int
+accept_reserve(int fd, struct sockaddr *addr, socklen_t *addrlen,
+    int reserve, volatile int *counter)
+{
+	int ret;
+
+	if (getdtablecount() + reserve +
+	    ((*counter + 1) * GOTD_FD_NEEDED) >= getdtablesize()) {
+		log_debug("inflight fds exceeded");
+		errno = EMFILE;
+		return -1;
+	}
+
+	if ((ret = accept4(fd, addr, addrlen,
+	    SOCK_NONBLOCK | SOCK_CLOEXEC)) > -1) {
+		(*counter)++;
+	}
+
+	return ret;
+}
+
+static void
+gotd_accept_paused(int fd, short event, void *arg)
+{
+	event_add(&gotd_listen.iev.ev, NULL);
+}
+
+static void
+gotd_accept(int fd, short event, void *arg)
+{
+	struct gotd_imsgev *iev = arg;
+	struct sockaddr_storage ss;
+	struct timeval backoff;
+	socklen_t len;
+	int s = -1;
+	struct gotd_listen_client *client = NULL;
+	struct gotd_imsg_connect iconn;
+
+	backoff.tv_sec = 1;
+	backoff.tv_usec = 0;
+
+	if (event_add(&gotd_listen.iev.ev, NULL) == -1) {
+		log_warn("event_add");
+		return;
+	}
+	if (event & EV_TIMEOUT)
+		return;
+
+	len = sizeof(ss);
+
+	/* Other backoff conditions apart from EMFILE/ENFILE? */
+	s = accept_reserve(fd, (struct sockaddr *)&ss, &len, GOTD_FD_RESERVE,
+	    &inflight);
+	if (s == -1) {
+		switch (errno) {
+		case EINTR:
+		case EWOULDBLOCK:
+		case ECONNABORTED:
+			return;
+		case EMFILE:
+		case ENFILE:
+			event_del(&gotd_listen.iev.ev);
+			evtimer_add(&gotd_listen.pause.ev, &backoff);
+			return;
+		default:
+			log_warn("accept");
+			return;
+		}
+	}
+
+	if (listen_client_cnt >= GOTD_MAXCLIENTS)
+		goto err;
+
+	client = calloc(1, sizeof(*client));
+	if (client == NULL) {
+		log_warn("%s: calloc", __func__);
+		goto err;
+	}
+	client->id = get_client_id();
+	client->fd = s;
+	s = -1;
+	add_client(client);
+	log_debug("%s: new client connected on fd %d", __func__, client->fd);
+
+	memset(&iconn, 0, sizeof(iconn));
+	iconn.client_id = client->id;
+	s = dup(client->fd);
+	if (s == -1) {
+		log_warn("%s: dup", __func__);
+		goto err;
+	}
+	if (gotd_imsg_compose_event(iev, GOTD_IMSG_CONNECT, PROC_LISTEN, s,
+	    &iconn, sizeof(iconn)) == -1) {
+		log_warn("imsg compose CONNECT");
+		goto err;
+	}
+
+	return;
+err:
+	inflight--;
+	if (client)
+		disconnect(client);
+	if (s != -1)
+		close(s);
+}
+
+static const struct got_error *
+recv_disconnect(struct imsg *imsg)
+{
+	struct gotd_imsg_disconnect idisconnect;
+	size_t datalen;
+	struct gotd_listen_client *client = NULL;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(idisconnect))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&idisconnect, imsg->data, sizeof(idisconnect));
+
+	log_debug("client disconnecting");
+
+	client = find_client(idisconnect.client_id);
+	if (client == NULL)
+		return got_error(GOT_ERR_CLIENT_ID);
+
+	return disconnect(client);
+}
+
+static void
+listen_dispatch(int fd, short event, void *arg)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct imsg imsg;
+	ssize_t n;
+	int shut = 0;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+
+	if (event & EV_WRITE) {
+		n = msgbuf_write(&ibuf->w);
+		if (n == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("%s: imsg_get", __func__);
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_DISCONNECT:
+			err = recv_disconnect(&imsg);
+			if (err)
+				log_warnx("%s: disconnect: %s",
+				    gotd_listen.title, err->msg);
+			break;
+		default:
+			log_debug("%s: unexpected imsg %d", gotd_listen.title,
+			    imsg.hdr.type);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut) {
+		gotd_imsg_event_add(iev);
+	} else {
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+void
+listen_main(const char *title, int gotd_socket)
+{
+	struct gotd_imsgev iev;
+	struct event evsigint, evsigterm, evsighup, evsigusr1;
+
+	gotd_listen.title = title;
+	gotd_listen.pid = getpid();
+	gotd_listen.fd = gotd_socket;
+
+	signal_set(&evsigint, SIGINT, listen_sighdlr, NULL);
+	signal_set(&evsigterm, SIGTERM, listen_sighdlr, NULL);
+	signal_set(&evsighup, SIGHUP, listen_sighdlr, NULL);
+	signal_set(&evsigusr1, SIGUSR1, listen_sighdlr, NULL);
+	signal(SIGPIPE, SIG_IGN);
+
+	signal_add(&evsigint, NULL);
+	signal_add(&evsigterm, NULL);
+	signal_add(&evsighup, NULL);
+	signal_add(&evsigusr1, NULL);
+
+	imsg_init(&iev.ibuf, GOTD_FILENO_MSG_PIPE);
+	iev.handler = listen_dispatch;
+	iev.events = EV_READ;
+	iev.handler_arg = NULL;
+	event_set(&iev.ev, iev.ibuf.fd, EV_READ, listen_dispatch, &iev);
+	if (event_add(&iev.ev, NULL) == -1)
+		fatalx("event add");
+
+	event_set(&gotd_listen.iev.ev, gotd_listen.fd, EV_READ | EV_PERSIST,
+	    gotd_accept, &iev);
+	if (event_add(&gotd_listen.iev.ev, NULL))
+		fatalx("event add");
+	evtimer_set(&gotd_listen.pause.ev, gotd_accept_paused, NULL);
+
+	event_dispatch();
+
+	listen_shutdown();
+}
+
+static void
+listen_shutdown(void)
+{
+	log_debug("%s: shutting down", gotd_listen.title);
+
+	if (gotd_listen.fd != -1)
+		close(gotd_listen.fd);
+
+	exit(0);
+}
blob - /dev/null
blob + 5ecdb0084e77470530899068dc16cb0f553ead7b (mode 644)
--- /dev/null
+++ gotd/listen.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2022 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+void listen_main(const char *title, int gotd_socket);
blob - 4fd5767663f9fecdb247c6ed37f3865d7bb9a480
blob + f3ea139f65f7d8cf2b35c0d63a1ba9fc62aef542
--- gotd/parse.y
+++ gotd/parse.y
@@ -136,7 +136,7 @@ main		: UNIX_SOCKET STRING {
 		;
 
 main		: UNIX_SOCKET STRING {
-			if (gotd_proc_id == PROC_GOTD) {
+			if (gotd_proc_id == PROC_LISTEN) {
 				if (strlcpy(gotd->unix_socket_path, $2,
 				    sizeof(gotd->unix_socket_path)) >=
 				    sizeof(gotd->unix_socket_path)) {