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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
gotd: fork repo children on demand
To:
gameoftrees@openbsd.org
Date:
Mon, 26 Dec 2022 18:22:06 +0100

Download raw body.

Thread
Fork gotd repo children on demand, instead of just once at startup.
This is another step on the road towards better privsep.

For now, we need to add the "exec" pledge promise back to the parent process.
It will be removed again in a later step.

ok?

diff 9430e858d160f9d3dbd02b218b5c0a95e266a199 deb4055e1245cfbff24d9e826ce9d07cd0ea6767
commit - 9430e858d160f9d3dbd02b218b5c0a95e266a199
commit + deb4055e1245cfbff24d9e826ce9d07cd0ea6767
blob - 97731f1e9b245bfea3cec151f3d56b6c81205390
blob + 34b475dc44e1368b499b914af45b89401b37568b
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -96,6 +96,9 @@ static void gotd_shutdown(void);
 
 void gotd_sighdlr(int sig, short event, void *arg);
 static void gotd_shutdown(void);
+static const struct got_error *start_repo_child(struct gotd_client *,
+    enum gotd_procid, struct gotd_repo *, char *, const char *, int, int);
+static void kill_proc(struct gotd_child_proc *, int);
 
 __dead static void
 usage()
@@ -234,6 +237,23 @@ static int
 	return NULL;
 }
 
+static struct gotd_client *
+find_client_by_proc_fd(int fd)
+{
+	uint64_t slot;
+
+	for (slot = 0; slot < nitems(gotd_clients); slot++) {
+		struct gotd_client *c;
+		STAILQ_FOREACH(c, &gotd_clients[slot], entry) {
+			struct gotd_child_proc *proc = get_client_proc(c);
+			if (proc && proc->iev.ibuf.fd == fd)
+				return c;
+		}
+	}
+
+	return NULL;
+}
+
 static int
 client_is_reading(struct gotd_client *client)
 {
@@ -299,7 +319,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];
+	struct gotd_child_proc *listen_proc = &gotd.listen_proc;
 	uint64_t slot;
 
 	log_debug("uid %d: disconnecting", client->euid);
@@ -310,6 +330,10 @@ disconnect(struct gotd_client *client)
 		    GOTD_IMSG_DISCONNECT, PROC_GOTD, -1,
 		    &idisconnect, sizeof(idisconnect)) == -1)
 			log_warn("imsg compose DISCONNECT");
+
+		msgbuf_clear(&proc->iev.ibuf.w);
+		close(proc->iev.ibuf.fd);
+		kill_proc(proc, 0);
 	}
 
 	if (gotd_imsg_compose_event(&listen_proc->iev,
@@ -533,53 +557,13 @@ static struct gotd_child_proc *
 	return NULL;
 }
 
-static struct gotd_child_proc *
-find_proc_by_repo_name(enum gotd_procid proc_id, const char *repo_name)
-{
-	struct gotd_child_proc *proc;
-	int i;
-	size_t namelen;
-
-	for (i = 0; i < gotd.nprocs; i++) {
-		proc = &gotd.procs[i];
-		if (proc->type != proc_id)
-			continue;
-		namelen = strlen(proc->repo_name);
-		if (strncmp(proc->repo_name, repo_name, namelen) != 0)
-			continue;
-		if (repo_name[namelen] == '\0' ||
-		    strcmp(&repo_name[namelen], ".git") == 0)
-			return proc;
-	}
-
-	return NULL;
-}
-
-static struct gotd_child_proc *
-find_proc_by_fd(int fd)
-{
-	struct gotd_child_proc *proc;
-	int i;
-
-	for (i = 0; i < gotd.nprocs; i++) {
-		proc = &gotd.procs[i];
-		if (proc->iev.ibuf.fd == fd)
-			return proc;
-	}
-
-	return NULL;
-}
-
 static const struct got_error *
-forward_list_refs_request(struct gotd_client *client, struct imsg *imsg)
+start_client_session(struct gotd_client *client, struct imsg *imsg)
 {
 	const struct got_error *err;
 	struct gotd_imsg_list_refs ireq;
-	struct gotd_imsg_list_refs_internal ilref;
 	struct gotd_repo *repo = NULL;
-	struct gotd_child_proc *proc = NULL;
 	size_t datalen;
-	int fd = -1;
 
 	log_debug("list-refs request from uid %d", client->euid);
 
@@ -589,9 +573,6 @@ forward_list_refs_request(struct gotd_client *client, 
 
 	memcpy(&ireq, imsg->data, datalen);
 
-	memset(&ilref, 0, sizeof(ilref));
-	ilref.client_id = client->id;
-
 	if (ireq.client_is_reading) {
 		err = ensure_client_is_not_writing(client);
 		if (err)
@@ -603,10 +584,11 @@ forward_list_refs_request(struct gotd_client *client, 
 		    client->euid, client->egid, GOTD_AUTH_READ);
 		if (err)
 			return err;
-		client->repo_read = find_proc_by_repo_name(PROC_REPO_READ,
-		    ireq.repo_name);
-		if (client->repo_read == NULL)
-			return got_error(GOT_ERR_NOT_GIT_REPO);
+		err = start_repo_child(client, PROC_REPO_READ, repo,
+			gotd.argv0, gotd.confpath, gotd.daemonize,
+			gotd.verbosity);
+		if (err)
+			return err;
 	} else {
 		err = ensure_client_is_not_reading(client);
 		if (err)
@@ -618,27 +600,14 @@ forward_list_refs_request(struct gotd_client *client, 
 		    client->egid, GOTD_AUTH_READ | GOTD_AUTH_WRITE);
 		if (err)
 			return err;
-		client->repo_write = find_proc_by_repo_name(PROC_REPO_WRITE,
-		    ireq.repo_name);
-		if (client->repo_write == NULL)
-			return got_error(GOT_ERR_NOT_GIT_REPO);
+		err = start_repo_child(client, PROC_REPO_WRITE, repo,
+			gotd.argv0, gotd.confpath, gotd.daemonize,
+			gotd.verbosity);
+		if (err)
+			return err;
 	}
 
-	fd = dup(client->fd);
-	if (fd == -1)
-		return got_error_from_errno("dup");
-
-	proc = get_client_proc(client);
-	if (proc == NULL)
-		fatalx("no process found for uid %d", client->euid);
-	if (gotd_imsg_compose_event(&proc->iev,
-	    GOTD_IMSG_LIST_REFS_INTERNAL, PROC_GOTD, fd,
-	    &ilref, sizeof(ilref)) == -1) {
-		err = got_error_from_errno("imsg compose WANT");
-		close(fd);
-		return err;
-	}
-
+	/* List-refs request will be forwarded once the child is ready. */
 	return NULL;
 }
 
@@ -1040,12 +1009,9 @@ gotd_request(int fd, short events, void *arg)
 				    "unexpected list-refs request received");
 				break;
 			}
-			err = forward_list_refs_request(client, &imsg);
+			err = start_client_session(client, &imsg);
 			if (err)
 				break;
-			client->state = GOTD_STATE_EXPECT_CAPABILITIES;
-			log_debug("uid %d: expecting capabilities",
-			    client->euid);
 			break;
 		case GOTD_IMSG_CAPABILITIES:
 			if (client->state != GOTD_STATE_EXPECT_CAPABILITIES) {
@@ -1269,7 +1235,7 @@ done:
 	    client->euid, client->fd);
 done:
 	if (err) {
-		struct gotd_child_proc *listen_proc = &gotd.procs[0];
+		struct gotd_child_proc *listen_proc = &gotd.listen_proc;
 		struct gotd_imsg_disconnect idisconnect;
 
 		idisconnect.client_id = client->id;
@@ -1296,12 +1262,19 @@ get_proc_for_pid(pid_t pid)
 get_proc_for_pid(pid_t pid)
 {
 	struct gotd_child_proc *proc;
-	int i;
+	uint64_t slot;
 
-	for (i = 0; i < gotd.nprocs; i++) {
-		proc = &gotd.procs[i];
-		if (proc->pid == pid)
-			return proc;
+	proc = &gotd.listen_proc;
+	if (proc->pid == pid)
+		return proc;
+
+	for (slot = 0; slot < nitems(gotd_clients); slot++) {
+		struct gotd_client *c;
+		STAILQ_FOREACH(c, &gotd_clients[slot], entry) {
+			proc = get_client_proc(c);
+			if (proc->pid == pid)
+				return proc;
+		}
 	}
 
 	return NULL;
@@ -1321,16 +1294,21 @@ gotd_shutdown(void)
 gotd_shutdown(void)
 {
 	pid_t	 pid;
-	int	 status, i;
+	int	 status;
 	struct gotd_child_proc *proc;
+	uint64_t slot;
 
-	for (i = 0; i < gotd.nprocs; i++) {
-		proc = &gotd.procs[i];
-		msgbuf_clear(&proc->iev.ibuf.w);
-		close(proc->iev.ibuf.fd);
-		kill_proc(proc, 0);
+	for (slot = 0; slot < nitems(gotd_clients); slot++) {
+		struct gotd_client *c, *tmp;
+		STAILQ_FOREACH_SAFE(c, &gotd_clients[slot], entry, tmp)
+			disconnect(c);
 	}
 
+	proc = &gotd.listen_proc;
+	msgbuf_clear(&proc->iev.ibuf.w);
+	close(proc->iev.ibuf.fd);
+	kill_proc(proc, 0);
+
 	log_debug("waiting for children to terminate");
 	do {
 		pid = wait(&status);
@@ -1339,9 +1317,10 @@ gotd_shutdown(void)
 				fatal("wait");
 		} else if (WIFSIGNALED(status)) {
 			proc = get_proc_for_pid(pid);
-			log_warnx("%s %s child process terminated; signal %d",
+			log_warnx("%s %s child PID %d terminated; signal %d",
 			    proc ? gotd_proc_names[proc->type] : "",
-			    proc ? proc->repo_path : "", WTERMSIG(status));
+			    proc ? proc->repo_path : "",
+			    pid, WTERMSIG(status));
 		}	
 	} while (pid != -1 || (pid == -1 && errno == EINTR));
 
@@ -1439,6 +1418,15 @@ verify_imsg_src(struct gotd_client *client, struct got
 		} else
 			ret = 1;
 		break;
+	case GOTD_IMSG_REPO_CHILD_READY:
+		if (proc->type != PROC_REPO_READ &&
+		    proc->type != PROC_REPO_WRITE) {
+			err = got_error_fmt(GOT_ERR_BAD_PACKET,
+			    "unexpected \"ready\" signal from PID %d",
+			    proc->pid);
+		} else
+			ret = 1;
+		break;
 	case GOTD_IMSG_PACKFILE_DONE:
 		err = ensure_proc_is_reading(client, proc);
 		if (err)
@@ -1464,6 +1452,32 @@ recv_packfile_done(uint32_t *client_id, struct imsg *i
 }
 
 static const struct got_error *
+list_refs_request(struct gotd_client *client, struct gotd_imsgev *iev)
+{
+	static const struct got_error *err;
+	struct gotd_imsg_list_refs_internal ilref;
+	int fd;
+
+	memset(&ilref, 0, sizeof(ilref));
+	ilref.client_id = client->id;
+
+	fd = dup(client->fd);
+	if (fd == -1)
+		return got_error_from_errno("dup");
+
+	if (gotd_imsg_compose_event(iev, GOTD_IMSG_LIST_REFS_INTERNAL,
+	    PROC_GOTD, fd, &ilref, sizeof(ilref)) == -1) {
+		err = got_error_from_errno("imsg compose WANT");
+		close(fd);
+		return err;
+	}
+
+	client->state = GOTD_STATE_EXPECT_CAPABILITIES;
+	log_debug("uid %d: expecting capabilities", client->euid);
+	return NULL;
+}
+
+static const struct got_error *
 recv_packfile_done(uint32_t *client_id, struct imsg *imsg)
 {
 	struct gotd_imsg_packfile_done idone;
@@ -1816,15 +1830,18 @@ gotd_dispatch(int fd, short event, void *arg)
 }
 
 static void
-gotd_dispatch(int fd, short event, void *arg)
+gotd_dispatch_listener(int fd, short event, void *arg)
 {
 	struct gotd_imsgev *iev = arg;
 	struct imsgbuf *ibuf = &iev->ibuf;
-	struct gotd_child_proc *proc = NULL;
+	struct gotd_child_proc *proc = &gotd.listen_proc;
 	ssize_t n;
 	int shut = 0;
 	struct imsg imsg;
 
+	if (proc->iev.ibuf.fd != fd)
+		fatalx("%s: unexpected fd %d", __func__, fd);
+
 	if (event & EV_READ) {
 		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
 			fatal("imsg_read error");
@@ -1846,17 +1863,11 @@ gotd_dispatch(int fd, short event, void *arg)
 		}
 	}
 
-	proc = find_proc_by_fd(fd);
-	if (proc == NULL)
-		fatalx("cannot find child process for fd %d", fd);
-
 	for (;;) {
 		const struct got_error *err = NULL;
 		struct gotd_client *client = NULL;
 		uint32_t client_id = 0;
 		int do_disconnect = 0;
-		int do_ref_updates = 0, do_ref_update = 0;
-		int do_packfile_install = 0;
 
 		if ((n = imsg_get(ibuf, &imsg)) == -1)
 			fatal("%s: imsg_get error", __func__);
@@ -1871,6 +1882,100 @@ gotd_dispatch(int fd, short event, void *arg)
 		case GOTD_IMSG_CONNECT:
 			err = recv_connect(&client_id, &imsg);
 			break;
+		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+
+		client = find_client(client_id);
+		if (client == NULL) {
+			log_warnx("%s: client not found", __func__);
+			imsg_free(&imsg);
+			continue;
+		}
+
+		if (err)
+			log_warnx("uid %d: %s", client->euid, err->msg);
+
+		if (do_disconnect) {
+			if (err)
+				disconnect_on_error(client, err);
+			else
+				disconnect(client);
+		}
+
+		imsg_free(&imsg);
+	}
+done:
+	if (!shut) {
+		gotd_imsg_event_add(iev);
+	} else {
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+static void
+gotd_dispatch_repo_child(int fd, short event, void *arg)
+{
+	struct gotd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct gotd_child_proc *proc = NULL;
+	struct gotd_client *client = NULL;
+	ssize_t n;
+	int shut = 0;
+	struct imsg imsg;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0) {
+			/* Connection closed. */
+			shut = 1;
+			goto done;
+		}
+	}
+
+	if (event & EV_WRITE) {
+		n = msgbuf_write(&ibuf->w);
+		if (n == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0) {
+			/* Connection closed. */
+			shut = 1;
+			goto done;
+		}
+	}
+
+	client = find_client_by_proc_fd(fd);
+	if (client == NULL)
+		fatalx("cannot find client for fd %d", fd);
+
+	proc = get_client_proc(client);
+	if (proc == NULL)
+		fatalx("cannot find child process for fd %d", fd);
+
+	for (;;) {
+		const struct got_error *err = NULL;
+		uint32_t client_id = 0;
+		int do_disconnect = 0;
+		int do_list_refs = 0, do_ref_updates = 0, do_ref_update = 0;
+		int do_packfile_install = 0;
+
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("%s: imsg_get error", __func__);
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_ERROR:
+			do_disconnect = 1;
+			err = gotd_imsg_recv_error(&client_id, &imsg);
+			break;
+		case GOTD_IMSG_REPO_CHILD_READY:
+			do_list_refs = 1;
+			break;
 		case GOTD_IMSG_PACKFILE_DONE:
 			do_disconnect = 1;
 			err = recv_packfile_done(&client_id, &imsg);
@@ -1895,13 +2000,6 @@ gotd_dispatch(int fd, short event, void *arg)
 			break;
 		}
 
-		client = find_client(client_id);
-		if (client == NULL) {
-			log_warnx("%s: client not found", __func__);
-			imsg_free(&imsg);
-			continue;
-		}
-
 		if (!verify_imsg_src(client, proc, &imsg)) {
 			log_debug("dropping imsg type %d from PID %d",
 			    imsg.hdr.type, proc->pid);
@@ -1917,7 +2015,9 @@ gotd_dispatch(int fd, short event, void *arg)
 			else
 				disconnect(client);
 		} else {
-			if (do_packfile_install)
+			if (do_list_refs)
+				err = list_refs_request(client, iev);
+			else if (do_packfile_install)
 				err = install_pack(client, proc->repo_path,
 				    &imsg);
 			else if (do_ref_updates)
@@ -2002,7 +2102,7 @@ start_listener(char *argv0, const char *confpath, int 
 static void
 start_listener(char *argv0, const char *confpath, int daemonize, int verbosity)
 {
-	struct gotd_child_proc *proc = &gotd.procs[0];
+	struct gotd_child_proc *proc = &gotd.listen_proc;
 
 	proc->type = PROC_LISTEN;
 
@@ -2013,50 +2113,55 @@ start_listener(char *argv0, const char *confpath, int 
 	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.handler = gotd_dispatch_listener;
 	proc->iev.events = EV_READ;
 	proc->iev.handler_arg = NULL;
 }
 
-static void
-start_repo_children(struct gotd *gotd, char *argv0, const char *confpath,
+static const struct got_error *
+start_repo_child(struct gotd_client *client, enum gotd_procid proc_type,
+    struct gotd_repo *repo, char *argv0, const char *confpath,
     int daemonize, int verbosity)
 {
-	struct gotd_repo *repo = NULL;
 	struct gotd_child_proc *proc;
-	int i;
 
-	for (i = 1; i < gotd->nprocs; i++) {
-		if (repo == NULL)
-			repo = TAILQ_FIRST(&gotd->repos);
-		proc = &gotd->procs[i];
-		if (i - 1 < gotd->nrepos)
-			proc->type = PROC_REPO_READ;
-		else
-			proc->type = PROC_REPO_WRITE;
-		if (strlcpy(proc->repo_name, repo->name,
-		    sizeof(proc->repo_name)) >= sizeof(proc->repo_name))
-			fatalx("repository name too long: %s", repo->name);
-		log_debug("adding repository %s", repo->name);
-		if (realpath(repo->path, proc->repo_path) == NULL)
-			fatal("%s", repo->path);
-		if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
-		    PF_UNSPEC, proc->pipe) == -1)
-			fatal("socketpair");
-		proc->pid = start_child(proc->type, proc->repo_path, argv0,
-		    confpath, proc->pipe[1], daemonize, verbosity);
-		imsg_init(&proc->iev.ibuf, proc->pipe[0]);
-		log_debug("proc %s %s is on fd %d",
-		    gotd_proc_names[proc->type], proc->repo_path,
-		    proc->pipe[0]);
-		proc->iev.handler = gotd_dispatch;
-		proc->iev.events = EV_READ;
-		proc->iev.handler_arg = NULL;
-		event_set(&proc->iev.ev, proc->iev.ibuf.fd, EV_READ,
-		    gotd_dispatch, &proc->iev);
+	if (proc_type != PROC_REPO_READ && proc_type != PROC_REPO_WRITE)
+		return got_error_msg(GOT_ERR_NOT_IMPL, "bad process type");
+		
+	proc = calloc(1, sizeof(*proc));
+	if (proc == NULL)
+		return got_error_from_errno("calloc");
 
-		repo = TAILQ_NEXT(repo, entry);
-	}
+	proc->type = proc_type;
+	if (strlcpy(proc->repo_name, repo->name,
+	    sizeof(proc->repo_name)) >= sizeof(proc->repo_name))
+		fatalx("repository name too long: %s", repo->name);
+	log_debug("starting %s for repository %s",
+	    proc->type == PROC_REPO_READ ? "reader" : "writer", repo->name);
+	if (realpath(repo->path, proc->repo_path) == NULL)
+		fatal("%s", repo->path);
+	if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+	    PF_UNSPEC, proc->pipe) == -1)
+		fatal("socketpair");
+	proc->pid = start_child(proc->type, proc->repo_path, argv0,
+	    confpath, proc->pipe[1], daemonize, verbosity);
+	imsg_init(&proc->iev.ibuf, proc->pipe[0]);
+	log_debug("proc %s %s is on fd %d",
+	    gotd_proc_names[proc->type], proc->repo_path,
+	    proc->pipe[0]);
+	proc->iev.handler = gotd_dispatch_repo_child;
+	proc->iev.events = EV_READ;
+	proc->iev.handler_arg = NULL;
+	event_set(&proc->iev.ev, proc->iev.ibuf.fd, EV_READ,
+	    gotd_dispatch_repo_child, &proc->iev);
+	gotd_imsg_event_add(&proc->iev);
+
+	if (proc->type == PROC_REPO_READ)
+		client->repo_read = proc;
+	else
+		client->repo_write = proc;
+
+	return NULL;
 }
 
 static void
@@ -2074,6 +2179,9 @@ apply_unveil(void)
 {
 	struct gotd_repo *repo;
 
+	if (unveil(gotd.argv0, "x") == -1)
+		fatal("unveil %s", gotd.argv0);
+
 	TAILQ_FOREACH(repo, &gotd.repos, entry) {
 		if (unveil(repo->path, "rwc") == -1)
 			fatal("unveil %s", repo->path);
@@ -2145,7 +2253,11 @@ main(int argc, char **argv)
 	if (argc != 0)
 		usage();
 
-	if (geteuid())
+	/* Require an absolute path in argv[0] for reliable re-exec. */
+	if (!got_path_is_absolute(argv0))
+		fatalx("bad path \"%s\": must be an absolute path", argv0);
+
+	if (geteuid() && (proc_id == PROC_GOTD || proc_id == PROC_LISTEN))
 		fatalx("need root privileges");
 
 	log_init(daemonize ? 0 : 1, LOG_DAEMON);
@@ -2154,6 +2266,11 @@ main(int argc, char **argv)
 	if (parse_config(confpath, proc_id, &gotd) != 0)
 		return 1;
 
+	gotd.argv0 = argv0;
+	gotd.daemonize = daemonize;
+	gotd.verbosity = verbosity;
+	gotd.confpath = confpath;
+
 	if (proc_id == PROC_GOTD &&
 	    (gotd.nrepos == 0 || TAILQ_EMPTY(&gotd.repos)))
 		fatalx("no repository defined in configuration file");
@@ -2196,18 +2313,7 @@ main(int argc, char **argv)
 	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");
@@ -2257,8 +2363,8 @@ main(int argc, char **argv)
 	switch (proc_id) {
 	case PROC_GOTD:
 #ifndef PROFILE
-		if (pledge("stdio rpath wpath cpath proc getpw sendfd recvfd "
-		    "fattr flock unix unveil", NULL) == -1)
+		if (pledge("stdio rpath wpath cpath proc exec getpw "
+		    "sendfd recvfd fattr flock unix unveil", NULL) == -1)
 			err(1, "pledge");
 #endif
 		break;
@@ -2308,7 +2414,7 @@ main(int argc, char **argv)
 	signal_add(&evsighup, NULL);
 	signal_add(&evsigusr1, NULL);
 
-	gotd_imsg_event_add(&gotd.procs[0].iev);
+	gotd_imsg_event_add(&gotd.listen_proc.iev);
 
 	event_dispatch();
 
blob - f7524de7c95a08df6542dacc78d3295c249be091
blob + c9660ad1b2468eb23cf0f8fbd3bb1937ac6e8449
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -114,9 +114,12 @@ struct gotd {
 	char user_name[32];
 	struct gotd_repolist repos;
 	int nrepos;
+	struct gotd_child_proc listen_proc;
+
+	char *argv0;
+	const char *confpath;
+	int daemonize;
 	int verbosity;
-	struct gotd_child_proc *procs;
-	int nprocs;
 };
 
 enum gotd_imsg_type {
@@ -172,6 +175,9 @@ enum gotd_imsg_type {
 	/* Client connections. */
 	GOTD_IMSG_DISCONNECT,
 	GOTD_IMSG_CONNECT,
+
+	/* Child process management. */
+	GOTD_IMSG_REPO_CHILD_READY,
 };
 
 /* Structure for GOTD_IMSG_ERROR. */
blob - 4716f4da690117859980910c5c467ea48b7fa838
blob + 84ad53e727b231bc0df858b1927be8682b139f1a
--- gotd/repo_read.c
+++ gotd/repo_read.c
@@ -892,8 +892,10 @@ repo_read_main(const char *title, const char *repo_pat
 	iev.events = EV_READ;
 	iev.handler_arg = NULL;
 	event_set(&iev.ev, iev.ibuf.fd, EV_READ, repo_read_dispatch, &iev);
-	if (event_add(&iev.ev, NULL) == -1) {
-		err = got_error_from_errno("event_add");
+
+	if (gotd_imsg_compose_event(&iev, GOTD_IMSG_REPO_CHILD_READY,
+	    PROC_REPO_READ, -1, NULL, 0) == -1) {
+		err = got_error_from_errno("imsg compose REPO_CHILD_READY");
 		goto done;
 	}
 
blob - 2b06c2f7956001962fa2df8ef028f9f1ac519b4f
blob + a8b8a48b519156b15cdfed829ea3090d0552fc1a
--- gotd/repo_write.c
+++ gotd/repo_write.c
@@ -1425,8 +1425,9 @@ repo_write_main(const char *title, const char *repo_pa
 	iev.events = EV_READ;
 	iev.handler_arg = NULL;
 	event_set(&iev.ev, iev.ibuf.fd, EV_READ, repo_write_dispatch, &iev);
-	if (event_add(&iev.ev, NULL) == -1) {
-		err = got_error_from_errno("event_add");
+	if (gotd_imsg_compose_event(&iev, GOTD_IMSG_REPO_CHILD_READY,
+	    PROC_REPO_WRITE, -1, NULL, 0) == -1) {
+		err = got_error_from_errno("imsg compose REPO_CHILD_READY");
 		goto done;
 	}
 
blob - 4df499c68e7f0c6fcb75510088da6bca9b323ba7
blob + ad35246ac7343183f2a9d4eedddd349272bb40e1
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -1,3 +1,5 @@
+.include "../../got-version.mk"
+
 REGRESS_TARGETS=test_repo_read test_repo_read_group \
 	test_repo_read_denied_user test_repo_read_denied_group \
 	test_repo_read_bad_user test_repo_read_bad_group \
@@ -26,8 +28,16 @@ GOTD_START_CMD=../../gotd/obj/gotd -vv -f $(PWD)/gotd.
 GOTD_GROUP?=gotsh
 GOTD_SOCK=${GOTD_DEVUSER_HOME}/gotd.sock
 
-GOTD_START_CMD=../../gotd/obj/gotd -vv -f $(PWD)/gotd.conf
-GOTD_STOP_CMD=../../gotctl/obj/gotctl -f $(GOTD_SOCK) stop
+.if "${GOT_RELEASE}" == "Yes"
+PREFIX ?= /usr/local
+BINDIR ?= ${PREFIX}/bin
+.else
+PREFIX ?= ${GOTD_TEST_USER_HOME}
+BINDIR ?= ${PREFIX}/bin
+.endif
+
+GOTD_START_CMD?=$(BINDIR)/gotd -vv -f $(PWD)/gotd.conf
+GOTD_STOP_CMD?=$(BINDIR)/gotctl -f $(GOTD_SOCK) stop
 GOTD_TRAP=trap "$(GOTD_STOP_CMD)" HUP INT QUIT PIPE TERM
 
 GOTD_TEST_ENV=GOTD_TEST_ROOT=$(GOTD_TEST_ROOT) \
blob - 5a81eccc64738c41542ac67a0c0695628e573319
blob + 09b2d5993cc7481df50e85fbda4705700de997bd
--- regress/gotd/README
+++ regress/gotd/README
@@ -30,7 +30,10 @@ Tests will run the locally built gotd binary found in 
 available in a non-standard PATH directory such as ~gotdev/bin, the
 gotdev user's PATH must be set appropriately in sshd_config (see below).
 
-Tests will run the locally built gotd binary found in gotd/obj/gotd.
+By default, tests will run the gotd binary found in ~/bin.
+If sources were unpacked from a Got release tarball then tests will run
+/usr/local/bin/gotd by default instead.
+
 The test suite creates the corresponding gotd socket in ~gotdev/gotd.sock.
 To make this work, the GOTD_UNIX_SOCKET variable must be set by sshd
 when the gotdev user logs in. The following should be added to the file