Commit Diff


commit - 75b17c2a7d14fc0476cba0375a5a031cf0c13a00
commit + ae7c1b785440d9b4e2289d935ca85725f45680f1
blob - 898aca45a6b8febe42a7d4946fbe6a9d5a51ad55
blob + 447c95850f26e58bc93149f6531707c99bf1691b
--- gotctl/gotctl.c
+++ gotctl/gotctl.c
@@ -147,6 +147,10 @@ show_client_info(struct imsg *imsg)
 
 	printf("client UID %d, GID %d, protocol state '%s', ",
 	    info.euid, info.egid, get_state_name(info.state));
+	if (info.session_child_pid)
+		printf("session PID %ld, ", (long)info.session_child_pid);
+	if (info.repo_child_pid)
+		printf("repo PID %ld, ", (long)info.repo_child_pid);
 	if (info.is_writing)
 		printf("writing to %s\n", info.repo_name);
 	else
blob - 150c4e0f6fded4edfa8704bb486d56939d38c114
blob + 4ba33eed625e9340f8987306ffe6ead0dcedd209
--- gotd/Makefile
+++ gotd/Makefile
@@ -15,7 +15,7 @@ SRCS=		gotd.c auth.c repo_read.c repo_write.c log.c pr
 		object_open_io.c object_parse.c opentemp.c pack.c path.c \
 		read_gitconfig.c read_gotconfig.c reference.c repository.c  \
 		sha1.c sigs.c pack_create_io.c pollfd.c reference_parse.c \
-		repo_imsg.c pack_index.c
+		repo_imsg.c pack_index.c session.c
 
 MAN =		${PROG}.conf.5 ${PROG}.8
 
blob - ef8285e2c0dea56c703592e647a3268f64478a82
blob + de1e260cf720e4827802724eb7d7dc9215167201
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -59,6 +59,7 @@
 #include "log.h"
 #include "listen.h"
 #include "auth.h"
+#include "session.h"
 #include "repo_read.h"
 #include "repo_write.h"
 
@@ -82,10 +83,10 @@ struct gotd_client {
 	struct gotd_child_proc		*repo_read;
 	struct gotd_child_proc		*repo_write;
 	struct gotd_child_proc		*auth;
+	struct gotd_child_proc		*session;
 	int				 required_auth;
 	char				*packfile_path;
 	char				*packidx_path;
-	int				 nref_updates;
 };
 STAILQ_HEAD(gotd_clients, gotd_client);
 
@@ -97,6 +98,8 @@ static struct gotd gotd;
 
 void gotd_sighdlr(int sig, short event, void *arg);
 static void gotd_shutdown(void);
+static const struct got_error *start_session_child(struct gotd_client *,
+    struct gotd_repo *, char *, const char *, int, int);
 static const struct got_error *start_repo_child(struct gotd_client *,
     enum gotd_procid, struct gotd_repo *, char *, const char *, int, int);
 static const struct got_error *start_auth_child(struct gotd_client *, int,
@@ -205,7 +208,7 @@ find_client(uint32_t client_id)
 }
 
 static struct gotd_child_proc *
-get_client_proc(struct gotd_client *client)
+get_client_repo_proc(struct gotd_client *client)
 {
 	if (client->repo_read && client->repo_write) {
 		fatalx("uid %d is reading and writing in the same session",
@@ -230,10 +233,12 @@ find_client_by_proc_fd(int fd)
 		struct gotd_client *c;
 
 		STAILQ_FOREACH(c, &gotd_clients[slot], entry) {
-			struct gotd_child_proc *proc = get_client_proc(c);
+			struct gotd_child_proc *proc = get_client_repo_proc(c);
 			if (proc && proc->iev.ibuf.fd == fd)
 				return c;
 			if (c->auth && c->auth->iev.ibuf.fd == fd)
+				return c;
+			if (c->session && c->session->iev.ibuf.fd == fd)
 				return c;
 		}
 	}
@@ -254,30 +259,6 @@ client_is_writing(struct gotd_client *client)
 }
 
 static const struct got_error *
-ensure_client_is_reading(struct gotd_client *client)
-{
-	if (!client_is_reading(client)) {
-		return got_error_fmt(GOT_ERR_BAD_PACKET,
-		    "uid %d made a read-request but is not reading from "
-		    "a repository", client->euid);
-	}
-
-	return NULL;
-}
-
-static const struct got_error *
-ensure_client_is_writing(struct gotd_client *client)
-{
-	if (!client_is_writing(client)) {
-		return got_error_fmt(GOT_ERR_BAD_PACKET,
-		    "uid %d made a write-request but is not writing to "
-		    "a repository", client->euid);
-	}
-
-	return NULL;
-}
-
-static const struct got_error *
 ensure_client_is_not_writing(struct gotd_client *client)
 {
 	if (client_is_writing(client)) {
@@ -320,6 +301,17 @@ wait_for_child(pid_t child_pid)
 			    (long)pid, WTERMSIG(status));
 		}
 	} while (pid != -1 || (pid == -1 && errno == EINTR));
+}
+
+static void
+proc_done(struct gotd_child_proc *proc)
+{
+	event_del(&proc->iev.ev);
+	msgbuf_clear(&proc->iev.ibuf.w);
+	close(proc->iev.ibuf.fd);
+	kill_proc(proc, 0);
+	wait_for_child(proc->pid);
+	free(proc);
 }
 
 static void
@@ -333,25 +325,35 @@ kill_auth_proc(struct gotd_client *client)
 	proc = client->auth;
 	client->auth = NULL;
 
-	event_del(&proc->iev.ev);
-	msgbuf_clear(&proc->iev.ibuf.w);
-	close(proc->iev.ibuf.fd);
-	kill_proc(proc, 0);
-	wait_for_child(proc->pid);
-	free(proc);
+	proc_done(proc);
 }
 
 static void
+kill_session_proc(struct gotd_client *client)
+{
+	struct gotd_child_proc *proc;
+
+	if (client->session == NULL)
+		return;
+
+	proc = client->session;
+	client->session = NULL;
+
+	proc_done(proc);
+}
+
+static void
 disconnect(struct gotd_client *client)
 {
 	struct gotd_imsg_disconnect idisconnect;
-	struct gotd_child_proc *proc = get_client_proc(client);
+	struct gotd_child_proc *proc = get_client_repo_proc(client);
 	struct gotd_child_proc *listen_proc = &gotd.listen_proc;
 	uint64_t slot;
 
 	log_debug("uid %d: disconnecting", client->euid);
 
 	kill_auth_proc(client);
+	kill_session_proc(client);
 
 	idisconnect.client_id = client->id;
 	if (proc) {
@@ -378,7 +380,10 @@ disconnect(struct gotd_client *client)
 	imsg_clear(&client->iev.ibuf);
 	event_del(&client->iev.ev);
 	evtimer_del(&client->tmo);
-	close(client->fd);
+	if (client->fd != -1)
+		close(client->fd);
+	else if (client->iev.ibuf.fd != -1)
+		close(client->iev.ibuf.fd);
 	if (client->delta_cache_fd != -1)
 		close(client->delta_cache_fd);
 	if (client->packfile_path) {
@@ -402,7 +407,7 @@ disconnect_on_error(struct gotd_client *client, const 
 	struct imsgbuf ibuf;
 
 	log_warnx("uid %d: %s", client->euid, err->msg);
-	if (err->code != GOT_ERR_EOF) {
+	if (err->code != GOT_ERR_EOF && client->fd != -1) {
 		imsg_init(&ibuf, client->fd);
 		gotd_imsg_send_error(&ibuf, 0, PROC_GOTD, err);
 		imsg_clear(&ibuf);
@@ -487,7 +492,7 @@ send_client_info(struct gotd_imsgev *iev, struct gotd_
 	iclient.euid = client->euid;
 	iclient.egid = client->egid;
 
-	proc = get_client_proc(client);
+	proc = get_client_repo_proc(client);
 	if (proc) {
 		if (strlcpy(iclient.repo_name, proc->repo_path,
 		    sizeof(iclient.repo_name)) >= sizeof(iclient.repo_name)) {
@@ -496,9 +501,13 @@ send_client_info(struct gotd_imsgev *iev, struct gotd_
 		}
 		if (client_is_writing(client))
 			iclient.is_writing = 1;
+
+		iclient.repo_child_pid = proc->pid;
 	}
 
 	iclient.state = client->state;
+	if (client->session)
+		iclient.session_child_pid = client->session->pid;
 	iclient.ncapabilities = client->ncapabilities;
 
 	if (gotd_imsg_compose_event(iev, GOTD_IMSG_INFO_CLIENT, PROC_GOTD, -1,
@@ -593,7 +602,7 @@ find_repo_by_name(const char *repo_name)
 }
 
 static const struct got_error *
-start_client_session(struct gotd_client *client, struct imsg *imsg)
+start_client_authentication(struct gotd_client *client, struct imsg *imsg)
 {
 	const struct got_error *err;
 	struct gotd_imsg_list_refs ireq;
@@ -602,6 +611,10 @@ start_client_session(struct gotd_client *client, struc
 
 	log_debug("list-refs request from uid %d", client->euid);
 
+	if (client->state != GOTD_STATE_EXPECT_LIST_REFS)
+		return got_error_msg(GOT_ERR_BAD_REQUEST,
+		    "unexpected list-refs request received");
+
 	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
 	if (datalen != sizeof(ireq))
 		return got_error(GOT_ERR_PRIVSEP_LEN);
@@ -635,339 +648,12 @@ start_client_session(struct gotd_client *client, struc
 			return err;
 	}
 
-	/* Flow continues upon authentication successs/failure or timeout. */
-	return NULL;
-}
-
-static const struct got_error *
-forward_want(struct gotd_client *client, struct imsg *imsg)
-{
-	struct gotd_imsg_want ireq;
-	struct gotd_imsg_want iwant;
-	size_t datalen;
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(ireq))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-
-	memcpy(&ireq, imsg->data, datalen);
-
-	memset(&iwant, 0, sizeof(iwant));
-	memcpy(iwant.object_id, ireq.object_id, SHA1_DIGEST_LENGTH);
-	iwant.client_id = client->id;
-
-	if (gotd_imsg_compose_event(&client->repo_read->iev, GOTD_IMSG_WANT,
-	    PROC_GOTD, -1, &iwant, sizeof(iwant)) == -1)
-		return got_error_from_errno("imsg compose WANT");
-
-	return NULL;
-}
-
-static const struct got_error *
-forward_ref_update(struct gotd_client *client, struct imsg *imsg)
-{
-	const struct got_error *err = NULL;
-	struct gotd_imsg_ref_update ireq;
-	struct gotd_imsg_ref_update *iref = NULL;
-	size_t datalen;
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen < sizeof(ireq))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&ireq, imsg->data, sizeof(ireq));
-	if (datalen != sizeof(ireq) + ireq.name_len)
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-
-	iref = malloc(datalen);
-	if (iref == NULL)
-		return got_error_from_errno("malloc");
-	memcpy(iref, imsg->data, datalen);
-
-	iref->client_id = client->id;
-	if (gotd_imsg_compose_event(&client->repo_write->iev,
-	    GOTD_IMSG_REF_UPDATE, PROC_GOTD, -1, iref, datalen) == -1)
-		err = got_error_from_errno("imsg compose REF_UPDATE");
-	free(iref);
-	return err;
-}
-
-static const struct got_error *
-forward_have(struct gotd_client *client, struct imsg *imsg)
-{
-	struct gotd_imsg_have ireq;
-	struct gotd_imsg_have ihave;
-	size_t datalen;
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(ireq))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-
-	memcpy(&ireq, imsg->data, datalen);
-
-	memset(&ihave, 0, sizeof(ihave));
-	memcpy(ihave.object_id, ireq.object_id, SHA1_DIGEST_LENGTH);
-	ihave.client_id = client->id;
-
-	if (gotd_imsg_compose_event(&client->repo_read->iev, GOTD_IMSG_HAVE,
-	    PROC_GOTD, -1, &ihave, sizeof(ihave)) == -1)
-		return got_error_from_errno("imsg compose HAVE");
+	evtimer_add(&client->tmo, &auth_timeout);
 
+	/* Flow continues upon authentication successs/failure or timeout. */
 	return NULL;
 }
 
-static int
-client_has_capability(struct gotd_client *client, const char *capastr)
-{
-	struct gotd_client_capability *capa;
-	size_t i;
-
-	if (client->ncapabilities == 0)
-		return 0;
-
-	for (i = 0; i < client->ncapabilities; i++) {
-		capa = &client->capabilities[i];
-		if (strcmp(capa->key, capastr) == 0)
-			return 1;
-	}
-
-	return 0;
-}
-
-static const struct got_error *
-recv_capabilities(struct gotd_client *client, struct imsg *imsg)
-{
-	struct gotd_imsg_capabilities icapas;
-	size_t datalen;
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(icapas))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&icapas, imsg->data, sizeof(icapas));
-
-	client->ncapa_alloc = icapas.ncapabilities;
-	client->capabilities = calloc(client->ncapa_alloc,
-	    sizeof(*client->capabilities));
-	if (client->capabilities == NULL) {
-		client->ncapa_alloc = 0;
-		return got_error_from_errno("calloc");
-	}
-
-	log_debug("expecting %zu capabilities from uid %d",
-	    client->ncapa_alloc, client->euid);
-	return NULL;
-}
-
-static const struct got_error *
-recv_capability(struct gotd_client *client, struct imsg *imsg)
-{
-	struct gotd_imsg_capability icapa;
-	struct gotd_client_capability *capa;
-	size_t datalen;
-	char *key, *value = NULL;
-
-	if (client->capabilities == NULL ||
-	    client->ncapabilities >= client->ncapa_alloc) {
-		return got_error_msg(GOT_ERR_BAD_REQUEST,
-		    "unexpected capability received");
-	}
-
-	memset(&icapa, 0, sizeof(icapa));
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen < sizeof(icapa))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&icapa, imsg->data, sizeof(icapa));
-
-	if (datalen != sizeof(icapa) + icapa.key_len + icapa.value_len)
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-
-	key = malloc(icapa.key_len + 1);
-	if (key == NULL)
-		return got_error_from_errno("malloc");
-	if (icapa.value_len > 0) {
-		value = malloc(icapa.value_len + 1);
-		if (value == NULL) {
-			free(key);
-			return got_error_from_errno("malloc");
-		}
-	}
-
-	memcpy(key, imsg->data + sizeof(icapa), icapa.key_len);
-	key[icapa.key_len] = '\0';
-	if (value) {
-		memcpy(value, imsg->data + sizeof(icapa) + icapa.key_len,
-		    icapa.value_len);
-		value[icapa.value_len] = '\0';
-	}
-
-	capa = &client->capabilities[client->ncapabilities++];
-	capa->key = key;
-	capa->value = value;
-
-	if (value)
-		log_debug("uid %d: capability %s=%s", client->euid, key, value);
-	else
-		log_debug("uid %d: capability %s", client->euid, key);
-
-	return NULL;
-}
-
-static const struct got_error *
-send_packfile(struct gotd_client *client)
-{
-	const struct got_error *err = NULL;
-	struct gotd_imsg_send_packfile ipack;
-	struct gotd_imsg_packfile_pipe ipipe;
-	int pipe[2];
-
-	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1)
-		return got_error_from_errno("socketpair");
-
-	memset(&ipack, 0, sizeof(ipack));
-	memset(&ipipe, 0, sizeof(ipipe));
-
-	ipack.client_id = client->id;
-	if (client_has_capability(client, GOT_CAPA_SIDE_BAND_64K))
-		ipack.report_progress = 1;
-
-	client->delta_cache_fd = got_opentempfd();
-	if (client->delta_cache_fd == -1)
-		return got_error_from_errno("got_opentempfd");
-
-	if (gotd_imsg_compose_event(&client->repo_read->iev,
-	    GOTD_IMSG_SEND_PACKFILE, PROC_GOTD, client->delta_cache_fd,
-	    &ipack, sizeof(ipack)) == -1) {
-		err = got_error_from_errno("imsg compose SEND_PACKFILE");
-		close(pipe[0]);
-		close(pipe[1]);
-		return err;
-	}
-
-	ipipe.client_id = client->id;
-
-	/* Send pack pipe end 0 to repo_read. */
-	if (gotd_imsg_compose_event(&client->repo_read->iev,
-	    GOTD_IMSG_PACKFILE_PIPE, PROC_GOTD, pipe[0],
-	        &ipipe, sizeof(ipipe)) == -1) {
-		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
-		close(pipe[1]);
-		return err;
-	}
-
-	/* Send pack pipe end 1 to gotsh(1) (expects just an fd, no data). */
-	if (gotd_imsg_compose_event(&client->iev,
-	    GOTD_IMSG_PACKFILE_PIPE, PROC_GOTD, pipe[1], NULL, 0) == -1)
-		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
-
-	return err;
-}
-
-static const struct got_error *
-recv_packfile(struct gotd_client *client)
-{
-	const struct got_error *err = NULL;
-	struct gotd_imsg_recv_packfile ipack;
-	struct gotd_imsg_packfile_pipe ipipe;
-	struct gotd_imsg_packidx_file ifile;
-	char *basepath = NULL, *pack_path = NULL, *idx_path = NULL;
-	int packfd = -1, idxfd = -1;
-	int pipe[2] = { -1, -1 };
-
-	if (client->packfile_path) {
-		return got_error_fmt(GOT_ERR_PRIVSEP_MSG,
-		    "uid %d already has a pack file", client->euid);
-	}
-
-	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1)
-		return got_error_from_errno("socketpair");
-
-	memset(&ipipe, 0, sizeof(ipipe));
-	ipipe.client_id = client->id;
-
-	/* Send pack pipe end 0 to repo_write. */
-	if (gotd_imsg_compose_event(&client->repo_write->iev,
-	    GOTD_IMSG_PACKFILE_PIPE, PROC_GOTD, pipe[0],
-	        &ipipe, sizeof(ipipe)) == -1) {
-		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
-		pipe[0] = -1;
-		goto done;
-	}
-	pipe[0] = -1;
-
-	/* Send pack pipe end 1 to gotsh(1) (expects just an fd, no data). */
-	if (gotd_imsg_compose_event(&client->iev,
-	    GOTD_IMSG_PACKFILE_PIPE, PROC_GOTD, pipe[1], NULL, 0) == -1)
-		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
-	pipe[1] = -1;
-
-	if (asprintf(&basepath, "%s/%s/receiving-from-uid-%d.pack",
-	    client->repo_write->repo_path, GOT_OBJECTS_PACK_DIR,
-	    client->euid) == -1) {
-		err = got_error_from_errno("asprintf");
-		goto done;
-	}
-
-	err = got_opentemp_named_fd(&pack_path, &packfd, basepath, "");
-	if (err)
-		goto done;
-
-	free(basepath);
-	if (asprintf(&basepath, "%s/%s/receiving-from-uid-%d.idx",
-	    client->repo_write->repo_path, GOT_OBJECTS_PACK_DIR,
-	    client->euid) == -1) {
-		err = got_error_from_errno("asprintf");
-		basepath = NULL;
-		goto done;
-	}
-	err = got_opentemp_named_fd(&idx_path, &idxfd, basepath, "");
-	if (err)
-		goto done;
-
-	memset(&ifile, 0, sizeof(ifile));
-	ifile.client_id = client->id;
-	if (gotd_imsg_compose_event(&client->repo_write->iev,
-	    GOTD_IMSG_PACKIDX_FILE, PROC_GOTD, idxfd,
-	    &ifile, sizeof(ifile)) == -1) {
-		err = got_error_from_errno("imsg compose PACKIDX_FILE");
-		idxfd = -1;
-		goto done;
-	}
-	idxfd = -1;
-
-	memset(&ipack, 0, sizeof(ipack));
-	ipack.client_id = client->id;
-	if (client_has_capability(client, GOT_CAPA_REPORT_STATUS))
-		ipack.report_status = 1;
-
-	if (gotd_imsg_compose_event(&client->repo_write->iev,
-	    GOTD_IMSG_RECV_PACKFILE, PROC_GOTD, packfd,
-	    &ipack, sizeof(ipack)) == -1) {
-		err = got_error_from_errno("imsg compose RECV_PACKFILE");
-		packfd = -1;
-		goto done;
-	}
-	packfd = -1;
-
-done:
-	free(basepath);
-	if (pipe[0] != -1 && close(pipe[0]) == -1 && err == NULL)
-		err = got_error_from_errno("close");
-	if (pipe[1] != -1 && close(pipe[1]) == -1 && err == NULL)
-		err = got_error_from_errno("close");
-	if (packfd != -1 && close(packfd) == -1 && err == NULL)
-		err = got_error_from_errno("close");
-	if (idxfd != -1 && close(idxfd) == -1 && err == NULL)
-		err = got_error_from_errno("close");
-	if (err) {
-		free(pack_path);
-		free(idx_path);
-	} else {
-		client->packfile_path = pack_path;
-		client->packidx_path = idx_path;
-	}
-	return err;
-}
-
 static void
 gotd_request(int fd, short events, void *arg)
 {
@@ -1032,144 +718,10 @@ gotd_request(int fd, short events, void *arg)
 			err = stop_gotd(client);
 			break;
 		case GOTD_IMSG_LIST_REFS:
-			if (client->state != GOTD_STATE_EXPECT_LIST_REFS) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected list-refs request received");
-				break;
-			}
-			err = start_client_session(client, &imsg);
-			if (err)
-				break;
-			break;
-		case GOTD_IMSG_CAPABILITIES:
-			if (client->state != GOTD_STATE_EXPECT_CAPABILITIES) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected capabilities received");
-				break;
-			}
-			log_debug("receiving capabilities from uid %d",
-			    client->euid);
-			err = recv_capabilities(client, &imsg);
-			break;
-		case GOTD_IMSG_CAPABILITY:
-			if (client->state != GOTD_STATE_EXPECT_CAPABILITIES) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected capability received");
-				break;
-			}
-			err = recv_capability(client, &imsg);
-			if (err || client->ncapabilities < client->ncapa_alloc)
-				break;
-			if (client_is_reading(client)) {
-				client->state = GOTD_STATE_EXPECT_WANT;
-				log_debug("uid %d: expecting want-lines",
-				    client->euid);
-			} else if (client_is_writing(client)) {
-				client->state = GOTD_STATE_EXPECT_REF_UPDATE;
-				log_debug("uid %d: expecting ref-update-lines",
-				    client->euid);
-			} else
-				fatalx("client %d is both reading and writing",
-				    client->euid);
-			break;
-		case GOTD_IMSG_WANT:
-			if (client->state != GOTD_STATE_EXPECT_WANT) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected want-line received");
-				break;
-			}
-			log_debug("received want-line from uid %d",
-			    client->euid);
-			err = ensure_client_is_reading(client);
-			if (err)
-				break;
-			err = forward_want(client, &imsg);
-			break;
-		case GOTD_IMSG_REF_UPDATE:
-			if (client->state != GOTD_STATE_EXPECT_REF_UPDATE) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected ref-update-line received");
-				break;
-			}
-			log_debug("received ref-update-line from uid %d",
-			    client->euid);
-			err = ensure_client_is_writing(client);
-			if (err)
-				break;
-			err = forward_ref_update(client, &imsg);
-			if (err)
-				break;
-			client->state = GOTD_STATE_EXPECT_MORE_REF_UPDATES;
-			break;
-		case GOTD_IMSG_HAVE:
-			if (client->state != GOTD_STATE_EXPECT_HAVE) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected have-line received");
-				break;
-			}
-			log_debug("received have-line from uid %d",
-			    client->euid);
-			err = ensure_client_is_reading(client);
-			if (err)
-				break;
-			err = forward_have(client, &imsg);
-			if (err)
-				break;
+			err = start_client_authentication(client, &imsg);
 			break;
-		case GOTD_IMSG_FLUSH:
-			if (client->state == GOTD_STATE_EXPECT_WANT ||
-			    client->state == GOTD_STATE_EXPECT_HAVE) {
-				err = ensure_client_is_reading(client);
-				if (err)
-					break;
-			} else if (client->state ==
-			    GOTD_STATE_EXPECT_MORE_REF_UPDATES) {
-				err = ensure_client_is_writing(client);
-				if (err)
-					break;
-			} else {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected flush-pkt received");
-				break;
-			}
-			log_debug("received flush-pkt from uid %d",
-			    client->euid);
-			if (client->state == GOTD_STATE_EXPECT_WANT) {
-				client->state = GOTD_STATE_EXPECT_HAVE;
-				log_debug("uid %d: expecting have-lines",
-				    client->euid);
-			} else if (client->state == GOTD_STATE_EXPECT_HAVE) {
-				client->state = GOTD_STATE_EXPECT_DONE;
-				log_debug("uid %d: expecting 'done'",
-				    client->euid);
-			} else if (client->state ==
-			    GOTD_STATE_EXPECT_MORE_REF_UPDATES) {
-				client->state = GOTD_STATE_EXPECT_PACKFILE;
-				log_debug("uid %d: expecting packfile",
-				    client->euid);
-				err = recv_packfile(client);
-			} else {
-				/* should not happen, see above */
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected client state");
-				break;
-			}
-			break;
-		case GOTD_IMSG_DONE:
-			if (client->state != GOTD_STATE_EXPECT_HAVE &&
-			    client->state != GOTD_STATE_EXPECT_DONE) {
-				err = got_error_msg(GOT_ERR_BAD_REQUEST,
-				    "unexpected flush-pkt received");
-				break;
-			}
-			log_debug("received 'done' from uid %d", client->euid);
-			err = ensure_client_is_reading(client);
-			if (err)
-				break;
-			client->state = GOTD_STATE_DONE;
-			err = send_packfile(client);
-			break;
 		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
 			err = got_error(GOT_ERR_PRIVSEP_MSG);
 			break;
 		}
@@ -1183,19 +735,16 @@ gotd_request(int fd, short events, void *arg)
 			disconnect_on_error(client, err);
 	} else {
 		gotd_imsg_event_add(&client->iev);
-		if (client->state == GOTD_STATE_EXPECT_LIST_REFS)
-			evtimer_add(&client->tmo, &auth_timeout);
-		else
-			evtimer_add(&client->tmo, &gotd.request_timeout);
 	}
 }
 
 static void
-gotd_request_timeout(int fd, short events, void *arg)
+gotd_auth_timeout(int fd, short events, void *arg)
 {
 	struct gotd_client *client = arg;
 
-	log_debug("disconnecting uid %d due to timeout", client->euid);
+	log_debug("disconnecting uid %d due to authentication timeout",
+	    client->euid);
 	disconnect(client);
 }
 
@@ -1242,7 +791,6 @@ recv_connect(uint32_t *client_id, struct imsg *imsg)
 	/* The auth process will verify UID/GID for us. */
 	client->euid = iconnect.euid;
 	client->egid = iconnect.egid;
-	client->nref_updates = -1;
 
 	imsg_init(&client->iev.ibuf, client->fd);
 	client->iev.handler = gotd_request;
@@ -1253,7 +801,7 @@ recv_connect(uint32_t *client_id, struct imsg *imsg)
 	    &client->iev);
 	gotd_imsg_event_add(&client->iev);
 
-	evtimer_set(&client->tmo, gotd_request_timeout, client);
+	evtimer_set(&client->tmo, gotd_auth_timeout, client);
 
 	add_client(client);
 	log_debug("%s: new client uid %d connected on fd %d", __func__,
@@ -1280,6 +828,7 @@ static const char *gotd_proc_names[PROC_MAX] = {
 	"parent",
 	"listen",
 	"auth",
+	"session",
 	"repo_read",
 	"repo_write"
 };
@@ -1300,6 +849,7 @@ gotd_shutdown(void)
 	struct gotd_child_proc *proc;
 	uint64_t slot;
 
+	log_debug("shutting down");
 	for (slot = 0; slot < nitems(gotd_clients); slot++) {
 		struct gotd_client *c, *tmp;
 
@@ -1380,7 +930,7 @@ verify_imsg_src(struct gotd_client *client, struct got
 	int ret = 0;
 
 	if (proc->type == PROC_REPO_READ || proc->type == PROC_REPO_WRITE) {
-		client_proc = get_client_proc(client);
+		client_proc = get_client_repo_proc(client);
 		if (client_proc == NULL)
 			fatalx("no process found for uid %d", client->euid);
 		if (proc->pid != client_proc->pid) {
@@ -1388,8 +938,21 @@ verify_imsg_src(struct gotd_client *client, struct got
 			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_SESSION) {
+		if (client->session == NULL) {
+			log_warnx("no session found for uid %d", client->euid);
 			return 0;
 		}
+		if (proc->pid != client->session->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->session->pid);
+			return 0;
+		}
 	}
 
 	switch (imsg->hdr.type) {
@@ -1414,6 +977,14 @@ verify_imsg_src(struct gotd_client *client, struct got
 		} else
 			ret = 1;
 		break;
+	case GOTD_IMSG_CLIENT_SESSION_READY:
+		if (proc->type != PROC_SESSION) {
+			err = got_error_fmt(GOT_ERR_BAD_PACKET,
+			    "unexpected \"ready\" signal from PID %d",
+			    proc->pid);
+		} else
+			ret = 1;
+		break;
 	case GOTD_IMSG_REPO_CHILD_READY:
 		if (proc->type != PROC_REPO_READ &&
 		    proc->type != PROC_REPO_WRITE) {
@@ -1448,383 +1019,46 @@ verify_imsg_src(struct gotd_client *client, struct got
 }
 
 static const struct got_error *
-list_refs_request(struct gotd_client *client, struct gotd_imsgev *iev)
+connect_repo_child(struct gotd_client *client,
+    struct gotd_child_proc *repo_proc)
 {
 	static const struct got_error *err;
-	struct gotd_imsg_list_refs_internal ilref;
-	int fd;
+	struct gotd_imsgev *session_iev = &client->session->iev;
+	struct gotd_imsg_connect_repo_child ireq;
+	int pipe[2];
 
-	memset(&ilref, 0, sizeof(ilref));
-	ilref.client_id = client->id;
+	if (client->state != GOTD_STATE_EXPECT_LIST_REFS)
+		return got_error_msg(GOT_ERR_BAD_REQUEST,
+		    "unexpected repo child ready signal received");
 
-	fd = dup(client->fd);
-	if (fd == -1)
-		return got_error_from_errno("dup");
+	if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+	    PF_UNSPEC, pipe) == -1)
+		fatal("socketpair");
 
-	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;
-	size_t datalen;
-
-	log_debug("packfile-done received");
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(idone))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&idone, imsg->data, sizeof(idone));
-
-	*client_id = idone.client_id;
-	return NULL;
-}
-
-static const struct got_error *
-recv_packfile_install(uint32_t *client_id, struct imsg *imsg)
-{
-	struct gotd_imsg_packfile_install inst;
-	size_t datalen;
-
-	log_debug("packfile-install received");
+	memset(&ireq, 0, sizeof(ireq));
+	ireq.client_id = client->id;
+	ireq.proc_id = repo_proc->type;
 
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(inst))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&inst, imsg->data, sizeof(inst));
-
-	*client_id = inst.client_id;
-	return NULL;
-}
-
-static const struct got_error *
-recv_ref_updates_start(uint32_t *client_id, struct imsg *imsg)
-{
-	struct gotd_imsg_ref_updates_start istart;
-	size_t datalen;
-
-	log_debug("ref-updates-start received");
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(istart))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&istart, imsg->data, sizeof(istart));
-
-	*client_id = istart.client_id;
-	return NULL;
-}
-
-static const struct got_error *
-recv_ref_update(uint32_t *client_id, struct imsg *imsg)
-{
-	struct gotd_imsg_ref_update iref;
-	size_t datalen;
-
-	log_debug("ref-update received");
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen < sizeof(iref))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&iref, imsg->data, sizeof(iref));
-
-	*client_id = iref.client_id;
-	return NULL;
-}
-
-static const struct got_error *
-send_ref_update_ok(struct gotd_client *client,
-    struct gotd_imsg_ref_update *iref, const char *refname)
-{
-	struct gotd_imsg_ref_update_ok iok;
-	struct ibuf *wbuf;
-	size_t len;
-
-	memset(&iok, 0, sizeof(iok));
-	iok.client_id = client->id;
-	memcpy(iok.old_id, iref->old_id, SHA1_DIGEST_LENGTH);
-	memcpy(iok.new_id, iref->new_id, SHA1_DIGEST_LENGTH);
-	iok.name_len = strlen(refname);
-
-	len = sizeof(iok) + iok.name_len;
-	wbuf = imsg_create(&client->iev.ibuf, GOTD_IMSG_REF_UPDATE_OK,
-	    PROC_GOTD, gotd.pid, len);
-	if (wbuf == NULL)
-		return got_error_from_errno("imsg_create REF_UPDATE_OK");
-
-	if (imsg_add(wbuf, &iok, sizeof(iok)) == -1)
-		return got_error_from_errno("imsg_add REF_UPDATE_OK");
-	if (imsg_add(wbuf, refname, iok.name_len) == -1)
-		return got_error_from_errno("imsg_add REF_UPDATE_OK");
-
-	wbuf->fd = -1;
-	imsg_close(&client->iev.ibuf, wbuf);
-	gotd_imsg_event_add(&client->iev);
-	return NULL;
-}
-
-static void
-send_refs_updated(struct gotd_client *client)
-{
-	if (gotd_imsg_compose_event(&client->iev,
-	    GOTD_IMSG_REFS_UPDATED, PROC_GOTD, -1, NULL, 0) == -1)
-		log_warn("imsg compose REFS_UPDATED");
-}
-
-static const struct got_error *
-send_ref_update_ng(struct gotd_client *client,
-    struct gotd_imsg_ref_update *iref, const char *refname,
-    const char *reason)
-{
-	const struct got_error *ng_err;
-	struct gotd_imsg_ref_update_ng ing;
-	struct ibuf *wbuf;
-	size_t len;
-
-	memset(&ing, 0, sizeof(ing));
-	ing.client_id = client->id;
-	memcpy(ing.old_id, iref->old_id, SHA1_DIGEST_LENGTH);
-	memcpy(ing.new_id, iref->new_id, SHA1_DIGEST_LENGTH);
-	ing.name_len = strlen(refname);
-
-	ng_err = got_error_fmt(GOT_ERR_REF_BUSY, "%s", reason);
-	ing.reason_len = strlen(ng_err->msg);
-
-	len = sizeof(ing) + ing.name_len + ing.reason_len;
-	wbuf = imsg_create(&client->iev.ibuf, GOTD_IMSG_REF_UPDATE_NG,
-	    PROC_GOTD, gotd.pid, len);
-	if (wbuf == NULL)
-		return got_error_from_errno("imsg_create REF_UPDATE_NG");
-
-	if (imsg_add(wbuf, &ing, sizeof(ing)) == -1)
-		return got_error_from_errno("imsg_add REF_UPDATE_NG");
-	if (imsg_add(wbuf, refname, ing.name_len) == -1)
-		return got_error_from_errno("imsg_add REF_UPDATE_NG");
-	if (imsg_add(wbuf, ng_err->msg, ing.reason_len) == -1)
-		return got_error_from_errno("imsg_add REF_UPDATE_NG");
-
-	wbuf->fd = -1;
-	imsg_close(&client->iev.ibuf, wbuf);
-	gotd_imsg_event_add(&client->iev);
-	return NULL;
-}
-
-static const struct got_error *
-install_pack(struct gotd_client *client, const char *repo_path,
-    struct imsg *imsg)
-{
-	const struct got_error *err = NULL;
-	struct gotd_imsg_packfile_install inst;
-	char hex[SHA1_DIGEST_STRING_LENGTH];
-	size_t datalen;
-	char *packfile_path = NULL, *packidx_path = NULL;
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(inst))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&inst, imsg->data, sizeof(inst));
-
-	if (client->packfile_path == NULL)
-		return got_error_msg(GOT_ERR_BAD_REQUEST,
-		    "client has no pack file");
-	if (client->packidx_path == NULL)
-		return got_error_msg(GOT_ERR_BAD_REQUEST,
-		    "client has no pack file index");
-
-	if (got_sha1_digest_to_str(inst.pack_sha1, hex, sizeof(hex)) == NULL)
-		return got_error_msg(GOT_ERR_NO_SPACE,
-		    "could not convert pack file SHA1 to hex");
-
-	if (asprintf(&packfile_path, "/%s/%s/pack-%s.pack",
-	    repo_path, GOT_OBJECTS_PACK_DIR, hex) == -1) {
-		err = got_error_from_errno("asprintf");
-		goto done;
+	/* Pass repo child pipe to session child process. */
+	if (gotd_imsg_compose_event(session_iev, GOTD_IMSG_CONNECT_REPO_CHILD,
+	    PROC_GOTD, pipe[0], &ireq, sizeof(ireq)) == -1) {
+		err = got_error_from_errno("imsg compose CONNECT_REPO_CHILD");
+		close(pipe[0]);
+		close(pipe[1]);
+		return err;
 	}
 
-	if (asprintf(&packidx_path, "/%s/%s/pack-%s.idx",
-	    repo_path, GOT_OBJECTS_PACK_DIR, hex) == -1) {
-		err = got_error_from_errno("asprintf");
-		goto done;
+	/* Pass session child pipe to repo child process. */
+	if (gotd_imsg_compose_event(&repo_proc->iev,
+	    GOTD_IMSG_CONNECT_REPO_CHILD, PROC_GOTD, pipe[1], NULL, 0) == -1) {
+		err = got_error_from_errno("imsg compose CONNECT_REPO_CHILD");
+		close(pipe[1]);
+		return err;
 	}
 
-	if (rename(client->packfile_path, packfile_path) == -1) {
-		err = got_error_from_errno3("rename", client->packfile_path,
-		    packfile_path);
-		goto done;
-	}
-
-	free(client->packfile_path);
-	client->packfile_path = NULL;
-
-	if (rename(client->packidx_path, packidx_path) == -1) {
-		err = got_error_from_errno3("rename", client->packidx_path,
-		    packidx_path);
-		goto done;
-	}
-
-	free(client->packidx_path);
-	client->packidx_path = NULL;
-done:
-	free(packfile_path);
-	free(packidx_path);
-	return err;
-}
-
-static const struct got_error *
-begin_ref_updates(struct gotd_client *client, struct imsg *imsg)
-{
-	struct gotd_imsg_ref_updates_start istart;
-	size_t datalen;
-
-	if (client->nref_updates != -1)
-		return got_error(GOT_ERR_PRIVSEP_MSG);
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen != sizeof(istart))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&istart, imsg->data, sizeof(istart));
-
-	if (istart.nref_updates <= 0)
-		return got_error(GOT_ERR_PRIVSEP_MSG);
-
-	client->nref_updates = istart.nref_updates;
 	return NULL;
 }
 
-static const struct got_error *
-update_ref(struct gotd_client *client, const char *repo_path,
-    struct imsg *imsg)
-{
-	const struct got_error *err = NULL;
-	struct got_repository *repo = NULL;
-	struct got_reference *ref = NULL;
-	struct gotd_imsg_ref_update iref;
-	struct got_object_id old_id, new_id;
-	struct got_object_id *id = NULL;
-	struct got_object *obj = NULL;
-	char *refname = NULL;
-	size_t datalen;
-	int locked = 0;
-
-	log_debug("update-ref from uid %d", client->euid);
-
-	if (client->nref_updates <= 0)
-		return got_error(GOT_ERR_PRIVSEP_MSG);
-
-	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
-	if (datalen < sizeof(iref))
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	memcpy(&iref, imsg->data, sizeof(iref));
-	if (datalen != sizeof(iref) + iref.name_len)
-		return got_error(GOT_ERR_PRIVSEP_LEN);
-	refname = malloc(iref.name_len + 1);
-	if (refname == NULL)
-		return got_error_from_errno("malloc");
-	memcpy(refname, imsg->data + sizeof(iref), iref.name_len);
-	refname[iref.name_len] = '\0';
-
-	log_debug("updating ref %s for uid %d", refname, client->euid);
-
-	err = got_repo_open(&repo, repo_path, NULL, NULL);
-	if (err)
-		goto done;
-
-	memcpy(old_id.sha1, iref.old_id, SHA1_DIGEST_LENGTH);
-	memcpy(new_id.sha1, iref.new_id, SHA1_DIGEST_LENGTH);
-	err = got_object_open(&obj, repo, &new_id);
-	if (err)
-		goto done;
-
-	if (iref.ref_is_new) {
-		err = got_ref_open(&ref, repo, refname, 0);
-		if (err) {
-			if (err->code != GOT_ERR_NOT_REF)
-				goto done;
-			err = got_ref_alloc(&ref, refname, &new_id);
-			if (err)
-				goto done;
-			err = got_ref_write(ref, repo); /* will lock/unlock */
-			if (err)
-				goto done;
-		} else {
-			err = got_error_fmt(GOT_ERR_REF_BUSY,
-			    "%s has been created by someone else "
-			    "while transaction was in progress",
-			    got_ref_get_name(ref));
-			goto done;
-		}
-	} else {
-		err = got_ref_open(&ref, repo, refname, 1 /* lock */);
-		if (err)
-			goto done;
-		locked = 1;
-
-		err = got_ref_resolve(&id, repo, ref);
-		if (err)
-			goto done;
-
-		if (got_object_id_cmp(id, &old_id) != 0) {
-			err = got_error_fmt(GOT_ERR_REF_BUSY,
-			    "%s has been modified by someone else "
-			    "while transaction was in progress",
-			    got_ref_get_name(ref));
-			goto done;
-		}
-
-		err = got_ref_change_ref(ref, &new_id);
-		if (err)
-			goto done;
-
-		err = got_ref_write(ref, repo);
-		if (err)
-			goto done;
-
-		free(id);
-		id = NULL;
-	}
-done:
-	if (err) {
-		if (err->code == GOT_ERR_LOCKFILE_TIMEOUT) {
-			err = got_error_fmt(GOT_ERR_LOCKFILE_TIMEOUT,
-			    "could not acquire exclusive file lock for %s",
-			    refname);
-		}
-		send_ref_update_ng(client, &iref, refname, err->msg);
-	} else
-		send_ref_update_ok(client, &iref, refname);
-
-	if (client->nref_updates > 0) {
-		client->nref_updates--;
-		if (client->nref_updates == 0)
-			send_refs_updated(client);
-
-	}
-	if (locked) {
-		const struct got_error *unlock_err;
-		unlock_err = got_ref_unlock(ref);
-		if (unlock_err && err == NULL)
-			err = unlock_err;
-	}
-	if (ref)
-		got_ref_close(ref);
-	if (obj)
-		got_object_close(obj);
-	if (repo)
-		got_repo_close(repo);
-	free(refname);
-	free(id);
-	return err;
-}
-
 static void
 gotd_dispatch_listener(int fd, short event, void *arg)
 {
@@ -1925,7 +1159,6 @@ gotd_dispatch_auth_child(int fd, short event, void *ar
 	struct imsg imsg;
 	uint32_t client_id = 0;
 	int do_disconnect = 0;
-	enum gotd_procid proc_type;
 
 	client = find_client_by_proc_fd(fd);
 	if (client == NULL)
@@ -2003,13 +1236,10 @@ gotd_dispatch_auth_child(int fd, short event, void *ar
 	log_info("authenticated uid %d for repository %s\n",
 	    client->euid, repo->name);
 
-	if (client->required_auth & GOTD_AUTH_WRITE)
-		proc_type = PROC_REPO_WRITE;
-	else
-		proc_type = PROC_REPO_READ;
-
-	err = start_repo_child(client, proc_type, repo, gotd.argv0,
+	err = start_session_child(client, repo, gotd.argv0,
 	    gotd.confpath, gotd.daemonize, gotd.verbosity);
+	if (err)
+		goto done;
 done:
 	if (err)
 		log_warnx("uid %d: %s", client->euid, err->msg);
@@ -2025,8 +1255,44 @@ done:
 	}
 }
 
+static const struct got_error *
+connect_session(struct gotd_client *client)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_connect iconnect;
+	int s;
+
+	memset(&iconnect, 0, sizeof(iconnect));
+
+	s = dup(client->fd);
+	if (s == -1)
+		return got_error_from_errno("dup");
+
+	iconnect.client_id = client->id;
+	iconnect.euid = client->euid;
+	iconnect.egid = client->egid;
+
+	if (gotd_imsg_compose_event(&client->session->iev, GOTD_IMSG_CONNECT,
+	    PROC_GOTD, s, &iconnect, sizeof(iconnect)) == -1) {
+		err = got_error_from_errno("imsg compose CONNECT");
+		close(s);
+		return err;
+	}
+
+	/*
+	 * We are no longer interested in messages from this client.
+	 * Further client requests will be handled by the session process.
+	 */
+	msgbuf_clear(&client->iev.ibuf.w);
+	imsg_clear(&client->iev.ibuf);
+	event_del(&client->iev.ev);
+	client->fd = -1; /* will be closed via copy in client->iev.ibuf.fd */
+
+	return NULL;
+}
+
 static void
-gotd_dispatch_repo_child(int fd, short event, void *arg)
+gotd_dispatch_client_session(int fd, short event, void *arg)
 {
 	struct gotd_imsgev *iev = arg;
 	struct imsgbuf *ibuf = &iev->ibuf;
@@ -2036,6 +1302,10 @@ gotd_dispatch_repo_child(int fd, short event, void *ar
 	int shut = 0;
 	struct imsg imsg;
 
+	client = find_client_by_proc_fd(fd);
+	if (client == NULL)
+		fatalx("cannot find client for fd %d", fd);
+
 	if (event & EV_READ) {
 		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
 			fatal("imsg_read error");
@@ -2054,14 +1324,132 @@ gotd_dispatch_repo_child(int fd, short event, void *ar
 			/* Connection closed. */
 			shut = 1;
 			goto done;
+		}
+	}
+
+	proc = client->session;
+	if (proc == NULL)
+		fatalx("cannot find session child process for fd %d", fd);
+
+	for (;;) {
+		const struct got_error *err = NULL;
+		uint32_t client_id = 0;
+		int do_disconnect = 0, do_start_repo_child = 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_CLIENT_SESSION_READY:
+			if (client->state != GOTD_STATE_EXPECT_LIST_REFS) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				break;
+			}
+			do_start_repo_child = 1;
+			break;
+		case GOTD_IMSG_DISCONNECT:
+			do_disconnect = 1;
+			break;
+		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
+			break;
 		}
+
+		if (!verify_imsg_src(client, proc, &imsg)) {
+			log_debug("dropping imsg type %d from PID %d",
+			    imsg.hdr.type, proc->pid);
+			imsg_free(&imsg);
+			continue;
+		}
+		if (err)
+			log_warnx("uid %d: %s", client->euid, err->msg);
+
+		if (do_start_repo_child) {
+			struct gotd_repo *repo;
+
+			repo = find_repo_by_name(client->session->repo_name);
+			if (repo != NULL) {
+				enum gotd_procid proc_type;
+
+				if (client->required_auth & GOTD_AUTH_WRITE)
+					proc_type = PROC_REPO_WRITE;
+				else
+					proc_type = PROC_REPO_READ;
+
+				err = start_repo_child(client, proc_type, repo,
+				    gotd.argv0, gotd.confpath, gotd.daemonize,
+				    gotd.verbosity);
+			} else
+				err = got_error(GOT_ERR_NOT_GIT_REPO);
+
+			if (err) {
+				log_warnx("uid %d: %s", client->euid, err->msg);
+				do_disconnect = 1;
+			}
+		}
+
+		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);
+		disconnect(client);
+	}
+}
 
+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;
+	ssize_t n;
+	int shut = 0;
+	struct imsg imsg;
+
 	client = find_client_by_proc_fd(fd);
 	if (client == NULL)
 		fatalx("cannot find client for fd %d", fd);
 
-	proc = get_client_proc(client);
+	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;
+		}
+	}
+
+	proc = get_client_repo_proc(client);
 	if (proc == NULL)
 		fatalx("cannot find child process for fd %d", fd);
 
@@ -2069,8 +1457,6 @@ gotd_dispatch_repo_child(int fd, short event, void *ar
 		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__);
@@ -2083,27 +1469,11 @@ gotd_dispatch_repo_child(int fd, short event, void *ar
 			err = gotd_imsg_recv_error(&client_id, &imsg);
 			break;
 		case GOTD_IMSG_REPO_CHILD_READY:
-			do_list_refs = 1;
+			err = connect_session(client);
+			if (err)
+				break;
+			err = connect_repo_child(client, proc);
 			break;
-		case GOTD_IMSG_PACKFILE_DONE:
-			do_disconnect = 1;
-			err = recv_packfile_done(&client_id, &imsg);
-			break;
-		case GOTD_IMSG_PACKFILE_INSTALL:
-			err = recv_packfile_install(&client_id, &imsg);
-			if (err == NULL)
-				do_packfile_install = 1;
-			break;
-		case GOTD_IMSG_REF_UPDATES_START:
-			err = recv_ref_updates_start(&client_id, &imsg);
-			if (err == NULL)
-				do_ref_updates = 1;
-			break;
-		case GOTD_IMSG_REF_UPDATE:
-			err = recv_ref_update(&client_id, &imsg);
-			if (err == NULL)
-				do_ref_update = 1;
-			break;
 		default:
 			log_debug("unexpected imsg %d", imsg.hdr.type);
 			break;
@@ -2123,20 +1493,8 @@ gotd_dispatch_repo_child(int fd, short event, void *ar
 				disconnect_on_error(client, err);
 			else
 				disconnect(client);
-		} else {
-			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)
-				err = begin_ref_updates(client, &imsg);
-			else if (do_ref_update)
-				err = update_ref(client, proc->repo_path,
-				    &imsg);
-			if (err)
-				log_warnx("uid %d: %s", client->euid, err->msg);
 		}
+
 		imsg_free(&imsg);
 	}
 done:
@@ -2145,7 +1503,7 @@ done:
 	} else {
 		/* This pipe is dead. Remove its event handler */
 		event_del(&iev->ev);
-		event_loopexit(NULL);
+		disconnect(client);
 	}
 }
 
@@ -2181,6 +1539,9 @@ start_child(enum gotd_procid proc_id, const char *repo
 	case PROC_AUTH:
 		argv[argc++] = (char *)"-A";
 		break;
+	case PROC_SESSION:
+		argv[argc++] = (char *)"-S";
+		break;
 	case PROC_REPO_READ:
 		argv[argc++] = (char *)"-R";
 		break;
@@ -2231,6 +1592,45 @@ start_listener(char *argv0, const char *confpath, int 
 }
 
 static const struct got_error *
+start_session_child(struct gotd_client *client, struct gotd_repo *repo,
+    char *argv0, const char *confpath, int daemonize, int verbosity)
+{
+	struct gotd_child_proc *proc;
+
+	proc = calloc(1, sizeof(*proc));
+	if (proc == NULL)
+		return got_error_from_errno("calloc");
+
+	proc->type = PROC_SESSION;
+	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 client uid %d session for repository %s",
+	    client->euid, repo->name);
+	if (strlcpy(proc->repo_path, repo->path, sizeof(proc->repo_path)) >=
+	    sizeof(proc->repo_path))
+		fatalx("repository path too long: %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_client_session;
+	proc->iev.events = EV_READ;
+	proc->iev.handler_arg = NULL;
+	event_set(&proc->iev.ev, proc->iev.ibuf.fd, EV_READ,
+	    gotd_dispatch_client_session, &proc->iev);
+	gotd_imsg_event_add(&proc->iev);
+
+	client->session = proc;
+	return NULL;
+}
+
+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)
@@ -2352,6 +1752,19 @@ apply_unveil_repo_readonly(const char *repo_path)
 }
 
 static void
+apply_unveil_repo_readwrite(const char *repo_path)
+{
+	if (unveil(repo_path, "rwc") == -1)
+		fatal("unveil %s", repo_path);
+
+	if (unveil(GOT_TMPDIR_STR, "rwc") == -1)
+		fatal("unveil %s", GOT_TMPDIR_STR);
+
+	if (unveil(NULL, NULL) == -1)
+		fatal("unveil");
+}
+
+static void
 apply_unveil_none(void)
 {
 	if (unveil("/", "") == -1)
@@ -2362,21 +1775,11 @@ apply_unveil_none(void)
 }
 
 static void
-apply_unveil(void)
+apply_unveil_selfexec(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);
-	}
-
-	if (unveil(GOT_TMPDIR_STR, "rwc") == -1)
-		fatal("unveil %s", GOT_TMPDIR_STR);
-
 	if (unveil(NULL, NULL) == -1)
 		fatal("unveil");
 }
@@ -2397,7 +1800,7 @@ main(int argc, char **argv)
 
 	log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
 
-	while ((ch = getopt(argc, argv, "Adf:LnP:RvW")) != -1) {
+	while ((ch = getopt(argc, argv, "Adf:LnP:RSvW")) != -1) {
 		switch (ch) {
 		case 'A':
 			proc_id = PROC_AUTH;
@@ -2422,6 +1825,9 @@ main(int argc, char **argv)
 		case 'R':
 			proc_id = PROC_REPO_READ;
 			break;
+		case 'S':
+			proc_id = PROC_SESSION;
+			break;
 		case 'v':
 			if (verbosity < 3)
 				verbosity++;
@@ -2507,7 +1913,8 @@ main(int argc, char **argv)
 		    gotd_proc_names[proc_id], repo_path);
 		if (daemonize && daemon(0, 0) == -1)
 			fatal("daemon");
-	} else if (proc_id == PROC_REPO_READ || proc_id == PROC_REPO_WRITE) {
+	} else if (proc_id == PROC_REPO_READ || proc_id == PROC_REPO_WRITE ||
+	    proc_id == PROC_SESSION) {
 		error = got_repo_pack_fds_open(&pack_fds);
 		if (error != NULL)
 			fatalx("cannot open pack tempfiles: %s", error->msg);
@@ -2537,8 +1944,8 @@ main(int argc, char **argv)
 	switch (proc_id) {
 	case PROC_GOTD:
 #ifndef PROFILE
-		if (pledge("stdio rpath wpath cpath proc exec "
-		    "sendfd recvfd fattr flock unveil", NULL) == -1)
+		/* "exec" promise will be limited to argv[0] via unveil(2). */
+		if (pledge("stdio proc exec sendfd recvfd unveil", NULL) == -1)
 			err(1, "pledge");
 #endif
 		break;
@@ -2573,6 +1980,21 @@ main(int argc, char **argv)
 		auth_main(title, &gotd.repos, repo_path);
 		/* NOTREACHED */
 		break;
+	case PROC_SESSION:
+#ifndef PROFILE
+		/*
+		 * The "recvfd" promise is only needed during setup and
+		 * will be removed in a later pledge(2) call.
+		 */
+		if (pledge("stdio rpath wpath cpath recvfd sendfd fattr flock "
+		    "unveil", NULL) == -1)
+			err(1, "pledge");
+#endif
+		apply_unveil_repo_readwrite(repo_path);
+		session_main(title, repo_path, pack_fds, temp_fds,
+		    &gotd.request_timeout);
+		/* NOTREACHED */
+		break;
 	case PROC_REPO_READ:
 #ifndef PROFILE
 		if (pledge("stdio rpath recvfd unveil", NULL) == -1)
@@ -2598,7 +2020,7 @@ main(int argc, char **argv)
 	if (proc_id != PROC_GOTD)
 		fatal("invalid process id %d", proc_id);
 
-	apply_unveil();
+	apply_unveil_selfexec();
 
 	signal_set(&evsigint, SIGINT, gotd_sighdlr, NULL);
 	signal_set(&evsigterm, SIGTERM, gotd_sighdlr, NULL);
@@ -2615,8 +2037,8 @@ main(int argc, char **argv)
 
 	event_dispatch();
 
-	if (pack_fds)
-		got_repo_pack_fds_close(pack_fds);
 	free(repo_path);
+	gotd_shutdown();
+
 	return 0;
 }
blob - 266fba3b38a61177a81510a5a8753084f64b1667
blob + 3f6ddfc81056dd0933a199322c90fe6b5e8296b0
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -36,6 +36,7 @@ enum gotd_procid {
 	PROC_GOTD	= 0,
 	PROC_LISTEN,
 	PROC_AUTH,
+	PROC_SESSION,
 	PROC_REPO_READ,
 	PROC_REPO_WRITE,
 	PROC_MAX,
@@ -188,7 +189,9 @@ enum gotd_imsg_type {
 	GOTD_IMSG_CONNECT,
 
 	/* Child process management. */
+	GOTD_IMSG_CLIENT_SESSION_READY,
 	GOTD_IMSG_REPO_CHILD_READY,
+	GOTD_IMSG_CONNECT_REPO_CHILD,
 
 	/* Auth child process. */
 	GOTD_IMSG_AUTHENTICATE,
@@ -227,6 +230,8 @@ struct gotd_imsg_info_client {
 	char repo_name[NAME_MAX];
 	int is_writing;
 	enum gotd_client_state state;
+	pid_t session_child_pid;
+	pid_t repo_child_pid;
 	size_t ncapabilities;
 
 	/* Followed by ncapabilities GOTD_IMSG_CAPABILITY. */
@@ -428,6 +433,14 @@ struct gotd_imsg_connect {
 	gid_t egid;
 };
 
+/* Structure for GOTD_IMSG_CONNECT_REPO_CHILD. */
+struct gotd_imsg_connect_repo_child {
+	uint32_t client_id;
+	enum gotd_procid proc_id;
+
+	/* repo child imsg pipe is passed via imsg fd */
+};
+
 /* Structure for GOTD_IMSG_AUTHENTICATE. */
 struct gotd_imsg_auth {
 	uid_t euid;
blob - 70f72ff14ea0d93944e8b9e766316bd5cd5cb1bc
blob + d9723d2768de50fbc762a863432cae47b27511ce
--- gotd/repo_read.c
+++ gotd/repo_read.c
@@ -60,6 +60,8 @@ static struct repo_read {
 	struct got_repository *repo;
 	int *pack_fds;
 	int *temp_fds;
+	int session_fd;
+	struct gotd_imsgev session_iev;
 } repo_read;
 
 static struct repo_read_client {
@@ -698,7 +700,7 @@ recv_disconnect(struct imsg *imsg)
 }
 
 static void
-repo_read_dispatch(int fd, short event, void *arg)
+repo_read_dispatch_session(int fd, short event, void *arg)
 {
 	const struct got_error *err = NULL;
 	struct gotd_imsgev *iev = arg;
@@ -777,8 +779,96 @@ repo_read_dispatch(int fd, short event, void *arg)
 			if (err)
 				log_warnx("%s: disconnect: %s",
 				    repo_read.title, err->msg);
+			shut = 1;
+			break;
+		default:
+			log_debug("%s: unexpected imsg %d", repo_read.title,
+			    imsg.hdr.type);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut && check_cancelled(NULL) == NULL) {
+		if (err &&
+		    gotd_imsg_send_error_event(iev, PROC_REPO_READ,
+		        client->id, err) == -1) {
+			log_warnx("could not send error to parent: %s",
+			    err->msg);
+		}
+		gotd_imsg_event_add(iev);
+	} else {
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+static const struct got_error *
+recv_connect(struct imsg *imsg)
+{
+	struct gotd_imsgev *iev = &repo_read.session_iev;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != 0)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	if (imsg->fd == -1)
+		return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+	if (repo_read.session_fd != -1)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	repo_read.session_fd = imsg->fd;
+
+	imsg_init(&iev->ibuf, repo_read.session_fd);
+	iev->handler = repo_read_dispatch_session;
+	iev->events = EV_READ;
+	iev->handler_arg = NULL;
+	event_set(&iev->ev, iev->ibuf.fd, EV_READ,
+	    repo_read_dispatch_session, iev);
+	gotd_imsg_event_add(iev);
+
+	return NULL;
+}
+
+static void
+repo_read_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;
+	struct repo_read_client *client = &repo_read_client;
+
+	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;
+	}
+
+	while (err == NULL && check_cancelled(NULL) == NULL) {
+		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_CONNECT_REPO_CHILD:
+			err = recv_connect(&imsg);
+			break;
 		default:
 			log_debug("%s: unexpected imsg %d", repo_read.title,
 			    imsg.hdr.type);
@@ -814,6 +904,8 @@ repo_read_main(const char *title, const char *repo_pat
 	repo_read.pid = getpid();
 	repo_read.pack_fds = pack_fds;
 	repo_read.temp_fds = temp_fds;
+	repo_read.session_fd = -1;
+	repo_read.session_iev.ibuf.fd = -1;
 
 	err = got_repo_open(&repo_read.repo, repo_path, NULL, pack_fds);
 	if (err)
@@ -858,5 +950,7 @@ repo_read_shutdown(void)
 		got_repo_close(repo_read.repo);
 	got_repo_pack_fds_close(repo_read.pack_fds);
 	got_repo_temp_fds_close(repo_read.temp_fds);
+	if (repo_read.session_fd != -1)
+		close(repo_read.session_fd);
 	exit(0);
 }
blob - 965572537276de7c92606cb33b97d87e09190553
blob + 92c95bf4a867f80378b7cb3e6f94566e95ae26b1
--- gotd/repo_write.c
+++ gotd/repo_write.c
@@ -67,6 +67,8 @@ static struct repo_write {
 	struct got_repository *repo;
 	int *pack_fds;
 	int *temp_fds;
+	int session_fd;
+	struct gotd_imsgev session_iev;
 } repo_write;
 
 struct gotd_ref_update {
@@ -1218,7 +1220,7 @@ receive_pack_idx(struct imsg *imsg, struct gotd_imsgev
 }
 
 static void
-repo_write_dispatch(int fd, short event, void *arg)
+repo_write_dispatch_session(int fd, short event, void *arg)
 {
 	const struct got_error *err = NULL;
 	struct gotd_imsgev *iev = arg;
@@ -1314,8 +1316,96 @@ repo_write_dispatch(int fd, short event, void *arg)
 			if (err)
 				log_warnx("%s: disconnect: %s",
 				    repo_write.title, err->msg);
+			shut = 1;
+			break;
+		default:
+			log_debug("%s: unexpected imsg %d", repo_write.title,
+			    imsg.hdr.type);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut && check_cancelled(NULL) == NULL) {
+		if (err &&
+		    gotd_imsg_send_error_event(iev, PROC_REPO_WRITE,
+		        client->id, err) == -1) {
+			log_warnx("could not send error to parent: %s",
+			    err->msg);
+		}
+		gotd_imsg_event_add(iev);
+	} else {
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+static const struct got_error *
+recv_connect(struct imsg *imsg)
+{
+	struct gotd_imsgev *iev = &repo_write.session_iev;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != 0)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	if (imsg->fd == -1)
+		return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+	if (repo_write.session_fd != -1)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	repo_write.session_fd = imsg->fd;
+
+	imsg_init(&iev->ibuf, repo_write.session_fd);
+	iev->handler = repo_write_dispatch_session;
+	iev->events = EV_READ;
+	iev->handler_arg = NULL;
+	event_set(&iev->ev, iev->ibuf.fd, EV_READ,
+	    repo_write_dispatch_session, iev);
+	gotd_imsg_event_add(iev);
+
+	return NULL;
+}
+
+static void
+repo_write_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;
+	struct repo_write_client *client = &repo_write_client;
+
+	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;
+	}
+
+	while (err == NULL && check_cancelled(NULL) == NULL) {
+		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_CONNECT_REPO_CHILD:
+			err = recv_connect(&imsg);
+			break;
 		default:
 			log_debug("%s: unexpected imsg %d", repo_write.title,
 			    imsg.hdr.type);
@@ -1351,6 +1441,8 @@ repo_write_main(const char *title, const char *repo_pa
 	repo_write.pid = getpid();
 	repo_write.pack_fds = pack_fds;
 	repo_write.temp_fds = temp_fds;
+	repo_write.session_fd = -1;
+	repo_write.session_iev.ibuf.fd = -1;
 
 	STAILQ_INIT(&repo_write_client.ref_updates);
 
@@ -1396,5 +1488,7 @@ repo_write_shutdown(void)
 		got_repo_close(repo_write.repo);
 	got_repo_pack_fds_close(repo_write.pack_fds);
 	got_repo_temp_fds_close(repo_write.temp_fds);
+	if (repo_write.session_fd != -1)
+		close(repo_write.session_fd);
 	exit(0);
 }
blob - /dev/null
blob + a82805ac538caaec6b52853abee4b175a8462653 (mode 644)
--- /dev/null
+++ gotd/session.c
@@ -0,0 +1,1438 @@
+/*
+ * Copyright (c) 2022, 2023 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/tree.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+#include <errno.h>
+#include <event.h>
+#include <limits.h>
+#include <sha1.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <imsg.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_repository.h"
+#include "got_object.h"
+#include "got_path.h"
+#include "got_reference.h"
+#include "got_opentemp.h"
+
+#include "got_lib_sha1.h"
+#include "got_lib_delta.h"
+#include "got_lib_object.h"
+#include "got_lib_object_cache.h"
+#include "got_lib_pack.h"
+#include "got_lib_repository.h"
+#include "got_lib_gitproto.h"
+
+#include "gotd.h"
+#include "log.h"
+#include "session.h"
+
+
+static struct gotd_session {
+	pid_t pid;
+	const char *title;
+	struct got_repository *repo;
+	int *pack_fds;
+	int *temp_fds;
+	struct gotd_imsgev parent_iev;
+	struct timeval request_timeout;
+} gotd_session;
+
+static struct gotd_session_client {
+	enum gotd_client_state		 state;
+	int				 is_writing;
+	struct gotd_client_capability	*capabilities;
+	size_t				 ncapa_alloc;
+	size_t				 ncapabilities;
+	uint32_t			 id;
+	int				 fd;
+	int				 delta_cache_fd;
+	struct gotd_imsgev		 iev;
+	struct gotd_imsgev		 repo_child_iev;
+	struct event			 tmo;
+	uid_t				 euid;
+	gid_t				 egid;
+	char				*packfile_path;
+	char				*packidx_path;
+	int				 nref_updates;
+} gotd_session_client;
+
+void gotd_session_sighdlr(int sig, short event, void *arg);
+static void gotd_session_shutdown(void);
+
+static void
+disconnect(struct gotd_session_client *client)
+{
+	log_debug("uid %d: disconnecting", client->euid);
+
+	if (gotd_imsg_compose_event(&gotd_session.parent_iev,
+	    GOTD_IMSG_DISCONNECT, PROC_SESSION, -1, NULL, 0) == -1)
+		log_warn("imsg compose DISCONNECT");
+
+	imsg_clear(&client->repo_child_iev.ibuf);
+	event_del(&client->repo_child_iev.ev);
+	evtimer_del(&client->tmo);
+	close(client->fd);
+	if (client->delta_cache_fd != -1)
+		close(client->delta_cache_fd);
+	if (client->packfile_path) {
+		if (unlink(client->packfile_path) == -1 && errno != ENOENT)
+			log_warn("unlink %s: ", client->packfile_path);
+		free(client->packfile_path);
+	}
+	if (client->packidx_path) {
+		if (unlink(client->packidx_path) == -1 && errno != ENOENT)
+			log_warn("unlink %s: ", client->packidx_path);
+		free(client->packidx_path);
+	}
+	free(client->capabilities);
+
+	gotd_session_shutdown();
+}
+
+static void
+disconnect_on_error(struct gotd_session_client *client,
+    const struct got_error *err)
+{
+	struct imsgbuf ibuf;
+
+	log_warnx("uid %d: %s", client->euid, err->msg);
+	if (err->code != GOT_ERR_EOF) {
+		imsg_init(&ibuf, client->fd);
+		gotd_imsg_send_error(&ibuf, 0, PROC_SESSION, err);
+		imsg_clear(&ibuf);
+	}
+
+	disconnect(client);
+}
+
+static void
+gotd_request_timeout(int fd, short events, void *arg)
+{
+	struct gotd_session_client *client = arg;
+
+	log_debug("disconnecting uid %d due to timeout", client->euid);
+	disconnect(client);
+}
+
+void
+gotd_session_sighdlr(int sig, short event, void *arg)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGHUP:
+		log_info("%s: ignoring SIGHUP", __func__);
+		break;
+	case SIGUSR1:
+		log_info("%s: ignoring SIGUSR1", __func__);
+		break;
+	case SIGTERM:
+	case SIGINT:
+		gotd_session_shutdown();
+		/* NOTREACHED */
+		break;
+	default:
+		fatalx("unexpected signal");
+	}
+}
+
+static const struct got_error *
+recv_packfile_done(uint32_t *client_id, struct imsg *imsg)
+{
+	struct gotd_imsg_packfile_done idone;
+	size_t datalen;
+
+	log_debug("packfile-done received");
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(idone))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&idone, imsg->data, sizeof(idone));
+
+	*client_id = idone.client_id;
+	return NULL;
+}
+
+static const struct got_error *
+recv_packfile_install(uint32_t *client_id, struct imsg *imsg)
+{
+	struct gotd_imsg_packfile_install inst;
+	size_t datalen;
+
+	log_debug("packfile-install received");
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(inst))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&inst, imsg->data, sizeof(inst));
+
+	*client_id = inst.client_id;
+	return NULL;
+}
+
+static const struct got_error *
+recv_ref_updates_start(uint32_t *client_id, struct imsg *imsg)
+{
+	struct gotd_imsg_ref_updates_start istart;
+	size_t datalen;
+
+	log_debug("ref-updates-start received");
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(istart))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&istart, imsg->data, sizeof(istart));
+
+	*client_id = istart.client_id;
+	return NULL;
+}
+
+static const struct got_error *
+recv_ref_update(uint32_t *client_id, struct imsg *imsg)
+{
+	struct gotd_imsg_ref_update iref;
+	size_t datalen;
+
+	log_debug("ref-update received");
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(iref))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&iref, imsg->data, sizeof(iref));
+
+	*client_id = iref.client_id;
+	return NULL;
+}
+
+static const struct got_error *
+send_ref_update_ok(struct gotd_session_client *client,
+    struct gotd_imsg_ref_update *iref, const char *refname)
+{
+	struct gotd_imsg_ref_update_ok iok;
+	struct gotd_imsgev *iev = &client->iev;
+	struct ibuf *wbuf;
+	size_t len;
+
+	memset(&iok, 0, sizeof(iok));
+	iok.client_id = client->id;
+	memcpy(iok.old_id, iref->old_id, SHA1_DIGEST_LENGTH);
+	memcpy(iok.new_id, iref->new_id, SHA1_DIGEST_LENGTH);
+	iok.name_len = strlen(refname);
+
+	len = sizeof(iok) + iok.name_len;
+	wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_REF_UPDATE_OK,
+	    PROC_SESSION, gotd_session.pid, len);
+	if (wbuf == NULL)
+		return got_error_from_errno("imsg_create REF_UPDATE_OK");
+
+	if (imsg_add(wbuf, &iok, sizeof(iok)) == -1)
+		return got_error_from_errno("imsg_add REF_UPDATE_OK");
+	if (imsg_add(wbuf, refname, iok.name_len) == -1)
+		return got_error_from_errno("imsg_add REF_UPDATE_OK");
+
+	wbuf->fd = -1;
+	imsg_close(&iev->ibuf, wbuf);
+	gotd_imsg_event_add(iev);
+	return NULL;
+}
+
+static void
+send_refs_updated(struct gotd_session_client *client)
+{
+	if (gotd_imsg_compose_event(&client->iev, GOTD_IMSG_REFS_UPDATED,
+	    PROC_SESSION, -1, NULL, 0) == -1)
+		log_warn("imsg compose REFS_UPDATED");
+}
+
+static const struct got_error *
+send_ref_update_ng(struct gotd_session_client *client,
+    struct gotd_imsg_ref_update *iref, const char *refname,
+    const char *reason)
+{
+	const struct got_error *ng_err;
+	struct gotd_imsg_ref_update_ng ing;
+	struct gotd_imsgev *iev = &client->iev;
+	struct ibuf *wbuf;
+	size_t len;
+
+	memset(&ing, 0, sizeof(ing));
+	ing.client_id = client->id;
+	memcpy(ing.old_id, iref->old_id, SHA1_DIGEST_LENGTH);
+	memcpy(ing.new_id, iref->new_id, SHA1_DIGEST_LENGTH);
+	ing.name_len = strlen(refname);
+
+	ng_err = got_error_fmt(GOT_ERR_REF_BUSY, "%s", reason);
+	ing.reason_len = strlen(ng_err->msg);
+
+	len = sizeof(ing) + ing.name_len + ing.reason_len;
+	wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_REF_UPDATE_NG,
+	    PROC_SESSION, gotd_session.pid, len);
+	if (wbuf == NULL)
+		return got_error_from_errno("imsg_create REF_UPDATE_NG");
+
+	if (imsg_add(wbuf, &ing, sizeof(ing)) == -1)
+		return got_error_from_errno("imsg_add REF_UPDATE_NG");
+	if (imsg_add(wbuf, refname, ing.name_len) == -1)
+		return got_error_from_errno("imsg_add REF_UPDATE_NG");
+	if (imsg_add(wbuf, ng_err->msg, ing.reason_len) == -1)
+		return got_error_from_errno("imsg_add REF_UPDATE_NG");
+
+	wbuf->fd = -1;
+	imsg_close(&iev->ibuf, wbuf);
+	gotd_imsg_event_add(iev);
+	return NULL;
+}
+
+static const struct got_error *
+install_pack(struct gotd_session_client *client, const char *repo_path,
+    struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_packfile_install inst;
+	char hex[SHA1_DIGEST_STRING_LENGTH];
+	size_t datalen;
+	char *packfile_path = NULL, *packidx_path = NULL;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(inst))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&inst, imsg->data, sizeof(inst));
+
+	if (client->packfile_path == NULL)
+		return got_error_msg(GOT_ERR_BAD_REQUEST,
+		    "client has no pack file");
+	if (client->packidx_path == NULL)
+		return got_error_msg(GOT_ERR_BAD_REQUEST,
+		    "client has no pack file index");
+
+	if (got_sha1_digest_to_str(inst.pack_sha1, hex, sizeof(hex)) == NULL)
+		return got_error_msg(GOT_ERR_NO_SPACE,
+		    "could not convert pack file SHA1 to hex");
+
+	if (asprintf(&packfile_path, "/%s/%s/pack-%s.pack",
+	    repo_path, GOT_OBJECTS_PACK_DIR, hex) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	if (asprintf(&packidx_path, "/%s/%s/pack-%s.idx",
+	    repo_path, GOT_OBJECTS_PACK_DIR, hex) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	if (rename(client->packfile_path, packfile_path) == -1) {
+		err = got_error_from_errno3("rename", client->packfile_path,
+		    packfile_path);
+		goto done;
+	}
+
+	free(client->packfile_path);
+	client->packfile_path = NULL;
+
+	if (rename(client->packidx_path, packidx_path) == -1) {
+		err = got_error_from_errno3("rename", client->packidx_path,
+		    packidx_path);
+		goto done;
+	}
+
+	free(client->packidx_path);
+	client->packidx_path = NULL;
+done:
+	free(packfile_path);
+	free(packidx_path);
+	return err;
+}
+
+static const struct got_error *
+begin_ref_updates(struct gotd_session_client *client, struct imsg *imsg)
+{
+	struct gotd_imsg_ref_updates_start istart;
+	size_t datalen;
+
+	if (client->nref_updates != -1)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(istart))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&istart, imsg->data, sizeof(istart));
+
+	if (istart.nref_updates <= 0)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	client->nref_updates = istart.nref_updates;
+	return NULL;
+}
+
+static const struct got_error *
+update_ref(struct gotd_session_client *client, const char *repo_path,
+    struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct got_repository *repo = NULL;
+	struct got_reference *ref = NULL;
+	struct gotd_imsg_ref_update iref;
+	struct got_object_id old_id, new_id;
+	struct got_object_id *id = NULL;
+	struct got_object *obj = NULL;
+	char *refname = NULL;
+	size_t datalen;
+	int locked = 0;
+
+	log_debug("update-ref from uid %d", client->euid);
+
+	if (client->nref_updates <= 0)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(iref))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&iref, imsg->data, sizeof(iref));
+	if (datalen != sizeof(iref) + iref.name_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	refname = malloc(iref.name_len + 1);
+	if (refname == NULL)
+		return got_error_from_errno("malloc");
+	memcpy(refname, imsg->data + sizeof(iref), iref.name_len);
+	refname[iref.name_len] = '\0';
+
+	log_debug("updating ref %s for uid %d", refname, client->euid);
+
+	err = got_repo_open(&repo, repo_path, NULL, NULL);
+	if (err)
+		goto done;
+
+	memcpy(old_id.sha1, iref.old_id, SHA1_DIGEST_LENGTH);
+	memcpy(new_id.sha1, iref.new_id, SHA1_DIGEST_LENGTH);
+	err = got_object_open(&obj, repo, &new_id);
+	if (err)
+		goto done;
+
+	if (iref.ref_is_new) {
+		err = got_ref_open(&ref, repo, refname, 0);
+		if (err) {
+			if (err->code != GOT_ERR_NOT_REF)
+				goto done;
+			err = got_ref_alloc(&ref, refname, &new_id);
+			if (err)
+				goto done;
+			err = got_ref_write(ref, repo); /* will lock/unlock */
+			if (err)
+				goto done;
+		} else {
+			err = got_error_fmt(GOT_ERR_REF_BUSY,
+			    "%s has been created by someone else "
+			    "while transaction was in progress",
+			    got_ref_get_name(ref));
+			goto done;
+		}
+	} else {
+		err = got_ref_open(&ref, repo, refname, 1 /* lock */);
+		if (err)
+			goto done;
+		locked = 1;
+
+		err = got_ref_resolve(&id, repo, ref);
+		if (err)
+			goto done;
+
+		if (got_object_id_cmp(id, &old_id) != 0) {
+			err = got_error_fmt(GOT_ERR_REF_BUSY,
+			    "%s has been modified by someone else "
+			    "while transaction was in progress",
+			    got_ref_get_name(ref));
+			goto done;
+		}
+
+		err = got_ref_change_ref(ref, &new_id);
+		if (err)
+			goto done;
+
+		err = got_ref_write(ref, repo);
+		if (err)
+			goto done;
+
+		free(id);
+		id = NULL;
+	}
+done:
+	if (err) {
+		if (err->code == GOT_ERR_LOCKFILE_TIMEOUT) {
+			err = got_error_fmt(GOT_ERR_LOCKFILE_TIMEOUT,
+			    "could not acquire exclusive file lock for %s",
+			    refname);
+		}
+		send_ref_update_ng(client, &iref, refname, err->msg);
+	} else
+		send_ref_update_ok(client, &iref, refname);
+
+	if (client->nref_updates > 0) {
+		client->nref_updates--;
+		if (client->nref_updates == 0)
+			send_refs_updated(client);
+
+	}
+	if (locked) {
+		const struct got_error *unlock_err;
+		unlock_err = got_ref_unlock(ref);
+		if (unlock_err && err == NULL)
+			err = unlock_err;
+	}
+	if (ref)
+		got_ref_close(ref);
+	if (obj)
+		got_object_close(obj);
+	if (repo)
+		got_repo_close(repo);
+	free(refname);
+	free(id);
+	return err;
+}
+
+static void
+session_dispatch_repo_child(int fd, short event, void *arg)
+{
+	struct gotd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct gotd_session_client *client = &gotd_session_client;
+	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;
+		}
+	}
+
+	for (;;) {
+		const struct got_error *err = 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__);
+		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_PACKFILE_DONE:
+			do_disconnect = 1;
+			err = recv_packfile_done(&client_id, &imsg);
+			break;
+		case GOTD_IMSG_PACKFILE_INSTALL:
+			err = recv_packfile_install(&client_id, &imsg);
+			if (err == NULL)
+				do_packfile_install = 1;
+			break;
+		case GOTD_IMSG_REF_UPDATES_START:
+			err = recv_ref_updates_start(&client_id, &imsg);
+			if (err == NULL)
+				do_ref_updates = 1;
+			break;
+		case GOTD_IMSG_REF_UPDATE:
+			err = recv_ref_update(&client_id, &imsg);
+			if (err == NULL)
+				do_ref_update = 1;
+			break;
+		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+
+		if (do_disconnect) {
+			if (err)
+				disconnect_on_error(client, err);
+			else
+				disconnect(client);
+		} else {
+			if (do_packfile_install)
+				err = install_pack(client,
+				    gotd_session.repo->path, &imsg);
+			else if (do_ref_updates)
+				err = begin_ref_updates(client, &imsg);
+			else if (do_ref_update)
+				err = update_ref(client,
+				    gotd_session.repo->path, &imsg);
+			if (err)
+				log_warnx("uid %d: %s", client->euid, err->msg);
+		}
+		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 const struct got_error *
+recv_capabilities(struct gotd_session_client *client, struct imsg *imsg)
+{
+	struct gotd_imsg_capabilities icapas;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(icapas))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&icapas, imsg->data, sizeof(icapas));
+
+	client->ncapa_alloc = icapas.ncapabilities;
+	client->capabilities = calloc(client->ncapa_alloc,
+	    sizeof(*client->capabilities));
+	if (client->capabilities == NULL) {
+		client->ncapa_alloc = 0;
+		return got_error_from_errno("calloc");
+	}
+
+	log_debug("expecting %zu capabilities from uid %d",
+	    client->ncapa_alloc, client->euid);
+	return NULL;
+}
+
+static const struct got_error *
+recv_capability(struct gotd_session_client *client, struct imsg *imsg)
+{
+	struct gotd_imsg_capability icapa;
+	struct gotd_client_capability *capa;
+	size_t datalen;
+	char *key, *value = NULL;
+
+	if (client->capabilities == NULL ||
+	    client->ncapabilities >= client->ncapa_alloc) {
+		return got_error_msg(GOT_ERR_BAD_REQUEST,
+		    "unexpected capability received");
+	}
+
+	memset(&icapa, 0, sizeof(icapa));
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(icapa))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&icapa, imsg->data, sizeof(icapa));
+
+	if (datalen != sizeof(icapa) + icapa.key_len + icapa.value_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	key = malloc(icapa.key_len + 1);
+	if (key == NULL)
+		return got_error_from_errno("malloc");
+	if (icapa.value_len > 0) {
+		value = malloc(icapa.value_len + 1);
+		if (value == NULL) {
+			free(key);
+			return got_error_from_errno("malloc");
+		}
+	}
+
+	memcpy(key, imsg->data + sizeof(icapa), icapa.key_len);
+	key[icapa.key_len] = '\0';
+	if (value) {
+		memcpy(value, imsg->data + sizeof(icapa) + icapa.key_len,
+		    icapa.value_len);
+		value[icapa.value_len] = '\0';
+	}
+
+	capa = &client->capabilities[client->ncapabilities++];
+	capa->key = key;
+	capa->value = value;
+
+	if (value)
+		log_debug("uid %d: capability %s=%s", client->euid, key, value);
+	else
+		log_debug("uid %d: capability %s", client->euid, key);
+
+	return NULL;
+}
+
+static const struct got_error *
+ensure_client_is_reading(struct gotd_session_client *client)
+{
+	if (client->is_writing) {
+		return got_error_fmt(GOT_ERR_BAD_PACKET,
+		    "uid %d made a read-request but is not reading from "
+		    "a repository", client->euid);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+ensure_client_is_writing(struct gotd_session_client *client)
+{
+	if (!client->is_writing) {
+		return got_error_fmt(GOT_ERR_BAD_PACKET,
+		    "uid %d made a write-request but is not writing to "
+		    "a repository", client->euid);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+forward_want(struct gotd_session_client *client, struct imsg *imsg)
+{
+	struct gotd_imsg_want ireq;
+	struct gotd_imsg_want iwant;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(ireq))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	memcpy(&ireq, imsg->data, datalen);
+
+	memset(&iwant, 0, sizeof(iwant));
+	memcpy(iwant.object_id, ireq.object_id, SHA1_DIGEST_LENGTH);
+	iwant.client_id = client->id;
+
+	if (gotd_imsg_compose_event(&client->repo_child_iev, GOTD_IMSG_WANT,
+	    PROC_SESSION, -1, &iwant, sizeof(iwant)) == -1)
+		return got_error_from_errno("imsg compose WANT");
+
+	return NULL;
+}
+
+static const struct got_error *
+forward_ref_update(struct gotd_session_client *client, struct imsg *imsg)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_ref_update ireq;
+	struct gotd_imsg_ref_update *iref = NULL;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(ireq))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&ireq, imsg->data, sizeof(ireq));
+	if (datalen != sizeof(ireq) + ireq.name_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	iref = malloc(datalen);
+	if (iref == NULL)
+		return got_error_from_errno("malloc");
+	memcpy(iref, imsg->data, datalen);
+
+	iref->client_id = client->id;
+	if (gotd_imsg_compose_event(&client->repo_child_iev,
+	    GOTD_IMSG_REF_UPDATE, PROC_SESSION, -1, iref, datalen) == -1)
+		err = got_error_from_errno("imsg compose REF_UPDATE");
+	free(iref);
+	return err;
+}
+
+static const struct got_error *
+forward_have(struct gotd_session_client *client, struct imsg *imsg)
+{
+	struct gotd_imsg_have ireq;
+	struct gotd_imsg_have ihave;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(ireq))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	memcpy(&ireq, imsg->data, datalen);
+
+	memset(&ihave, 0, sizeof(ihave));
+	memcpy(ihave.object_id, ireq.object_id, SHA1_DIGEST_LENGTH);
+	ihave.client_id = client->id;
+
+	if (gotd_imsg_compose_event(&client->repo_child_iev, GOTD_IMSG_HAVE,
+	    PROC_SESSION, -1, &ihave, sizeof(ihave)) == -1)
+		return got_error_from_errno("imsg compose HAVE");
+
+	return NULL;
+}
+
+static int
+client_has_capability(struct gotd_session_client *client, const char *capastr)
+{
+	struct gotd_client_capability *capa;
+	size_t i;
+
+	if (client->ncapabilities == 0)
+		return 0;
+
+	for (i = 0; i < client->ncapabilities; i++) {
+		capa = &client->capabilities[i];
+		if (strcmp(capa->key, capastr) == 0)
+			return 1;
+	}
+
+	return 0;
+}
+
+static const struct got_error *
+recv_packfile(struct gotd_session_client *client)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_recv_packfile ipack;
+	struct gotd_imsg_packfile_pipe ipipe;
+	struct gotd_imsg_packidx_file ifile;
+	char *basepath = NULL, *pack_path = NULL, *idx_path = NULL;
+	int packfd = -1, idxfd = -1;
+	int pipe[2] = { -1, -1 };
+
+	if (client->packfile_path) {
+		return got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+		    "uid %d already has a pack file", client->euid);
+	}
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1)
+		return got_error_from_errno("socketpair");
+
+	memset(&ipipe, 0, sizeof(ipipe));
+	ipipe.client_id = client->id;
+
+	/* Send pack pipe end 0 to repo child process. */
+	if (gotd_imsg_compose_event(&client->repo_child_iev,
+	    GOTD_IMSG_PACKFILE_PIPE, PROC_SESSION, pipe[0],
+	        &ipipe, sizeof(ipipe)) == -1) {
+		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
+		pipe[0] = -1;
+		goto done;
+	}
+	pipe[0] = -1;
+
+	/* Send pack pipe end 1 to gotsh(1) (expects just an fd, no data). */
+	if (gotd_imsg_compose_event(&client->iev,
+	    GOTD_IMSG_PACKFILE_PIPE, PROC_SESSION, pipe[1], NULL, 0) == -1)
+		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
+	pipe[1] = -1;
+
+	if (asprintf(&basepath, "%s/%s/receiving-from-uid-%d.pack",
+	    got_repo_get_path(gotd_session.repo), GOT_OBJECTS_PACK_DIR,
+	    client->euid) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	err = got_opentemp_named_fd(&pack_path, &packfd, basepath, "");
+	if (err)
+		goto done;
+
+	free(basepath);
+	if (asprintf(&basepath, "%s/%s/receiving-from-uid-%d.idx",
+	    got_repo_get_path(gotd_session.repo), GOT_OBJECTS_PACK_DIR,
+	    client->euid) == -1) {
+		err = got_error_from_errno("asprintf");
+		basepath = NULL;
+		goto done;
+	}
+	err = got_opentemp_named_fd(&idx_path, &idxfd, basepath, "");
+	if (err)
+		goto done;
+
+	memset(&ifile, 0, sizeof(ifile));
+	ifile.client_id = client->id;
+	if (gotd_imsg_compose_event(&client->repo_child_iev,
+	    GOTD_IMSG_PACKIDX_FILE, PROC_SESSION,
+	    idxfd, &ifile, sizeof(ifile)) == -1) {
+		err = got_error_from_errno("imsg compose PACKIDX_FILE");
+		idxfd = -1;
+		goto done;
+	}
+	idxfd = -1;
+
+	memset(&ipack, 0, sizeof(ipack));
+	ipack.client_id = client->id;
+	if (client_has_capability(client, GOT_CAPA_REPORT_STATUS))
+		ipack.report_status = 1;
+
+	if (gotd_imsg_compose_event(&client->repo_child_iev,
+	    GOTD_IMSG_RECV_PACKFILE, PROC_SESSION, packfd,
+	    &ipack, sizeof(ipack)) == -1) {
+		err = got_error_from_errno("imsg compose RECV_PACKFILE");
+		packfd = -1;
+		goto done;
+	}
+	packfd = -1;
+
+done:
+	free(basepath);
+	if (pipe[0] != -1 && close(pipe[0]) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (pipe[1] != -1 && close(pipe[1]) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (packfd != -1 && close(packfd) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (idxfd != -1 && close(idxfd) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (err) {
+		free(pack_path);
+		free(idx_path);
+	} else {
+		client->packfile_path = pack_path;
+		client->packidx_path = idx_path;
+	}
+	return err;
+}
+
+static const struct got_error *
+send_packfile(struct gotd_session_client *client)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_send_packfile ipack;
+	struct gotd_imsg_packfile_pipe ipipe;
+	int pipe[2];
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1)
+		return got_error_from_errno("socketpair");
+
+	memset(&ipack, 0, sizeof(ipack));
+	memset(&ipipe, 0, sizeof(ipipe));
+
+	ipack.client_id = client->id;
+	if (client_has_capability(client, GOT_CAPA_SIDE_BAND_64K))
+		ipack.report_progress = 1;
+
+	client->delta_cache_fd = got_opentempfd();
+	if (client->delta_cache_fd == -1)
+		return got_error_from_errno("got_opentempfd");
+
+	if (gotd_imsg_compose_event(&client->repo_child_iev,
+	    GOTD_IMSG_SEND_PACKFILE, PROC_GOTD, client->delta_cache_fd,
+	    &ipack, sizeof(ipack)) == -1) {
+		err = got_error_from_errno("imsg compose SEND_PACKFILE");
+		close(pipe[0]);
+		close(pipe[1]);
+		return err;
+	}
+
+	ipipe.client_id = client->id;
+
+	/* Send pack pipe end 0 to repo child process. */
+	if (gotd_imsg_compose_event(&client->repo_child_iev,
+	    GOTD_IMSG_PACKFILE_PIPE, PROC_GOTD,
+	    pipe[0], &ipipe, sizeof(ipipe)) == -1) {
+		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
+		close(pipe[1]);
+		return err;
+	}
+
+	/* Send pack pipe end 1 to gotsh(1) (expects just an fd, no data). */
+	if (gotd_imsg_compose_event(&client->iev,
+	    GOTD_IMSG_PACKFILE_PIPE, PROC_GOTD, pipe[1], NULL, 0) == -1)
+		err = got_error_from_errno("imsg compose PACKFILE_PIPE");
+
+	return err;
+}
+
+static void
+session_dispatch_listener(int fd, short events, void *arg)
+{
+	struct gotd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct gotd_session_client *client = &gotd_session_client;
+	const struct got_error *err = NULL;
+	struct imsg imsg;
+	ssize_t n;
+
+	if (events & EV_WRITE) {
+		while (ibuf->w.queued) {
+			n = msgbuf_write(&ibuf->w);
+			if (n == -1 && errno == EPIPE) {
+				/*
+				 * The client has closed its socket.
+				 * This can happen when Git clients are
+				 * done sending pack file data.
+				 */
+				msgbuf_clear(&ibuf->w);
+				continue;
+			} else if (n == -1 && errno != EAGAIN) {
+				err = got_error_from_errno("imsg_flush");
+				disconnect_on_error(client, err);
+				return;
+			}
+			if (n == 0) {
+				/* Connection closed. */
+				err = got_error(GOT_ERR_EOF);
+				disconnect_on_error(client, err);
+				return;
+			}
+		}
+	}
+
+	if ((events & EV_READ) == 0)
+		return;
+
+	memset(&imsg, 0, sizeof(imsg));
+
+	while (err == NULL) {
+		err = gotd_imsg_recv(&imsg, ibuf, 0);
+		if (err) {
+			if (err->code == GOT_ERR_PRIVSEP_READ)
+				err = NULL;
+			break;
+		}
+
+		evtimer_del(&client->tmo);
+
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_CAPABILITIES:
+			if (client->state != GOTD_STATE_EXPECT_CAPABILITIES) {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected capabilities received");
+				break;
+			}
+			log_debug("receiving capabilities from uid %d",
+			    client->euid);
+			err = recv_capabilities(client, &imsg);
+			break;
+		case GOTD_IMSG_CAPABILITY:
+			if (client->state != GOTD_STATE_EXPECT_CAPABILITIES) {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected capability received");
+				break;
+			}
+			err = recv_capability(client, &imsg);
+			if (err || client->ncapabilities < client->ncapa_alloc)
+				break;
+			if (!client->is_writing) {
+				client->state = GOTD_STATE_EXPECT_WANT;
+				log_debug("uid %d: expecting want-lines",
+				    client->euid);
+			} else if (client->is_writing) {
+				client->state = GOTD_STATE_EXPECT_REF_UPDATE;
+				log_debug("uid %d: expecting ref-update-lines",
+				    client->euid);
+			} else
+				fatalx("client %d is both reading and writing",
+				    client->euid);
+			break;
+		case GOTD_IMSG_WANT:
+			if (client->state != GOTD_STATE_EXPECT_WANT) {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected want-line received");
+				break;
+			}
+			log_debug("received want-line from uid %d",
+			    client->euid);
+			err = ensure_client_is_reading(client);
+			if (err)
+				break;
+			err = forward_want(client, &imsg);
+			break;
+		case GOTD_IMSG_REF_UPDATE:
+			if (client->state != GOTD_STATE_EXPECT_REF_UPDATE) {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected ref-update-line received");
+				break;
+			}
+			log_debug("received ref-update-line from uid %d",
+			    client->euid);
+			err = ensure_client_is_writing(client);
+			if (err)
+				break;
+			err = forward_ref_update(client, &imsg);
+			if (err)
+				break;
+			client->state = GOTD_STATE_EXPECT_MORE_REF_UPDATES;
+			break;
+		case GOTD_IMSG_HAVE:
+			if (client->state != GOTD_STATE_EXPECT_HAVE) {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected have-line received");
+				break;
+			}
+			log_debug("received have-line from uid %d",
+			    client->euid);
+			err = ensure_client_is_reading(client);
+			if (err)
+				break;
+			err = forward_have(client, &imsg);
+			if (err)
+				break;
+			break;
+		case GOTD_IMSG_FLUSH:
+			if (client->state == GOTD_STATE_EXPECT_WANT ||
+			    client->state == GOTD_STATE_EXPECT_HAVE) {
+				err = ensure_client_is_reading(client);
+				if (err)
+					break;
+			} else if (client->state ==
+			    GOTD_STATE_EXPECT_MORE_REF_UPDATES) {
+				err = ensure_client_is_writing(client);
+				if (err)
+					break;
+			} else {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected flush-pkt received");
+				break;
+			}
+			log_debug("received flush-pkt from uid %d",
+			    client->euid);
+			if (client->state == GOTD_STATE_EXPECT_WANT) {
+				client->state = GOTD_STATE_EXPECT_HAVE;
+				log_debug("uid %d: expecting have-lines",
+				    client->euid);
+			} else if (client->state == GOTD_STATE_EXPECT_HAVE) {
+				client->state = GOTD_STATE_EXPECT_DONE;
+				log_debug("uid %d: expecting 'done'",
+				    client->euid);
+			} else if (client->state ==
+			    GOTD_STATE_EXPECT_MORE_REF_UPDATES) {
+				client->state = GOTD_STATE_EXPECT_PACKFILE;
+				log_debug("uid %d: expecting packfile",
+				    client->euid);
+				err = recv_packfile(client);
+			} else {
+				/* should not happen, see above */
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected client state");
+				break;
+			}
+			break;
+		case GOTD_IMSG_DONE:
+			if (client->state != GOTD_STATE_EXPECT_HAVE &&
+			    client->state != GOTD_STATE_EXPECT_DONE) {
+				err = got_error_msg(GOT_ERR_BAD_REQUEST,
+				    "unexpected flush-pkt received");
+				break;
+			}
+			log_debug("received 'done' from uid %d", client->euid);
+			err = ensure_client_is_reading(client);
+			if (err)
+				break;
+			client->state = GOTD_STATE_DONE;
+			err = send_packfile(client);
+			break;
+		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (err) {
+		if (err->code != GOT_ERR_EOF ||
+		    client->state != GOTD_STATE_EXPECT_PACKFILE)
+			disconnect_on_error(client, err);
+	} else {
+		gotd_imsg_event_add(iev);
+		evtimer_add(&client->tmo, &gotd_session.request_timeout);
+	}
+}
+
+static const struct got_error *
+list_refs_request(void)
+{
+	static const struct got_error *err;
+	struct gotd_session_client *client = &gotd_session_client;
+	struct gotd_imsgev *iev = &client->repo_child_iev;
+	struct gotd_imsg_list_refs_internal ilref;
+	int fd;
+
+	if (client->state != GOTD_STATE_EXPECT_LIST_REFS)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	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_SESSION, fd, &ilref, sizeof(ilref)) == -1) {
+		err = got_error_from_errno("imsg compose LIST_REFS_INTERNAL");
+		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_connect(struct imsg *imsg)
+{
+	struct gotd_session_client *client = &gotd_session_client;
+	struct gotd_imsg_connect iconnect;
+	size_t datalen;
+
+	if (client->state != GOTD_STATE_EXPECT_LIST_REFS)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(iconnect))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&iconnect, imsg->data, sizeof(iconnect));
+
+	if (imsg->fd == -1)
+		return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+	client->fd = imsg->fd;
+	client->euid = iconnect.euid;
+	client->egid = iconnect.egid;
+
+	imsg_init(&client->iev.ibuf, client->fd);
+	client->iev.handler = session_dispatch_listener;
+	client->iev.events = EV_READ;
+	client->iev.handler_arg = NULL;
+	event_set(&client->iev.ev, client->iev.ibuf.fd, EV_READ,
+	    session_dispatch_listener, &client->iev);
+	gotd_imsg_event_add(&client->iev);
+	evtimer_set(&client->tmo, gotd_request_timeout, client);
+
+	return NULL;
+}
+
+static const struct got_error *
+recv_repo_child(struct imsg *imsg)
+{
+	struct gotd_imsg_connect_repo_child ichild;
+	struct gotd_session_client *client = &gotd_session_client;
+	size_t datalen;
+
+	if (client->state != GOTD_STATE_EXPECT_LIST_REFS)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	/* We should already have received a pipe to the listener. */
+	if (client->fd == -1)
+		return got_error(GOT_ERR_PRIVSEP_MSG);
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(ichild))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	memcpy(&ichild, imsg->data, sizeof(ichild));
+
+	client->id = ichild.client_id;
+	if (ichild.proc_id == PROC_REPO_WRITE)
+		client->is_writing = 1;
+	else if (ichild.proc_id == PROC_REPO_READ)
+		client->is_writing = 0;
+	else
+		return got_error_msg(GOT_ERR_PRIVSEP_MSG,
+		    "bad child process type");
+		
+	if (imsg->fd == -1)
+		return got_error(GOT_ERR_PRIVSEP_NO_FD);
+
+	imsg_init(&client->repo_child_iev.ibuf, imsg->fd);
+	client->repo_child_iev.handler = session_dispatch_repo_child;
+	client->repo_child_iev.events = EV_READ;
+	client->repo_child_iev.handler_arg = NULL;
+	event_set(&client->repo_child_iev.ev, client->repo_child_iev.ibuf.fd,
+	    EV_READ, session_dispatch_repo_child, &client->repo_child_iev);
+	gotd_imsg_event_add(&client->repo_child_iev);
+
+	/* The "recvfd" pledge promise is no longer needed. */
+	if (pledge("stdio rpath wpath cpath sendfd fattr flock", NULL) == -1)
+		fatal("pledge");
+
+	return NULL;
+}
+
+static void
+session_dispatch(int fd, short event, void *arg)
+{
+	struct gotd_imsgev *iev = arg;
+	struct imsgbuf *ibuf = &iev->ibuf;
+	struct gotd_session_client *client = &gotd_session_client;
+	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;
+		}
+	}
+
+	for (;;) {
+		const struct got_error *err = NULL;
+		uint32_t client_id = 0;
+		int do_disconnect = 0, do_list_refs = 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_CONNECT:
+			err = recv_connect(&imsg);
+			break;
+		case GOTD_IMSG_DISCONNECT:
+			do_disconnect = 1;
+			break;
+		case GOTD_IMSG_CONNECT_REPO_CHILD:
+			err = recv_repo_child(&imsg);
+			if (err)
+				break;
+			do_list_refs = 1;
+			break;
+		default:
+			log_debug("unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+
+		if (do_disconnect) {
+			if (err)
+				disconnect_on_error(client, err);
+			else
+				disconnect(client);
+		} else if (do_list_refs)
+			err = list_refs_request();
+
+		if (err)
+			log_warnx("uid %d: %s", client->euid, err->msg);
+	}
+done:
+	if (!shut) {
+		gotd_imsg_event_add(iev);
+	} else {
+		/* This pipe is dead. Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}	
+}
+
+void
+session_main(const char *title, const char *repo_path,
+    int *pack_fds, int *temp_fds, struct timeval *request_timeout)
+{
+	const struct got_error *err = NULL;
+	struct event evsigint, evsigterm, evsighup, evsigusr1;
+
+	gotd_session.title = title;
+	gotd_session.pid = getpid();
+	gotd_session.pack_fds = pack_fds;
+	gotd_session.temp_fds = temp_fds;
+	memcpy(&gotd_session.request_timeout, request_timeout,
+	    sizeof(gotd_session.request_timeout));
+
+	err = got_repo_open(&gotd_session.repo, repo_path, NULL, pack_fds);
+	if (err)
+		goto done;
+	if (!got_repo_is_bare(gotd_session.repo)) {
+		err = got_error_msg(GOT_ERR_NOT_GIT_REPO,
+		    "bare git repository required");
+		goto done;
+	}
+
+	got_repo_temp_fds_set(gotd_session.repo, temp_fds);
+
+	signal_set(&evsigint, SIGINT, gotd_session_sighdlr, NULL);
+	signal_set(&evsigterm, SIGTERM, gotd_session_sighdlr, NULL);
+	signal_set(&evsighup, SIGHUP, gotd_session_sighdlr, NULL);
+	signal_set(&evsigusr1, SIGUSR1, gotd_session_sighdlr, NULL);
+	signal(SIGPIPE, SIG_IGN);
+
+	signal_add(&evsigint, NULL);
+	signal_add(&evsigterm, NULL);
+	signal_add(&evsighup, NULL);
+	signal_add(&evsigusr1, NULL);
+
+	gotd_session_client.state = GOTD_STATE_EXPECT_LIST_REFS;
+	gotd_session_client.fd = -1;
+	gotd_session_client.nref_updates = -1;
+
+	imsg_init(&gotd_session.parent_iev.ibuf, GOTD_FILENO_MSG_PIPE);
+	gotd_session.parent_iev.handler = session_dispatch;
+	gotd_session.parent_iev.events = EV_READ;
+	gotd_session.parent_iev.handler_arg = NULL;
+	event_set(&gotd_session.parent_iev.ev, gotd_session.parent_iev.ibuf.fd,
+	    EV_READ, session_dispatch, &gotd_session.parent_iev);
+	if (gotd_imsg_compose_event(&gotd_session.parent_iev,
+	    GOTD_IMSG_CLIENT_SESSION_READY, PROC_SESSION, -1, NULL, 0) == -1) {
+		err = got_error_from_errno("imsg compose CLIENT_SESSION_READY");
+		goto done;
+	}
+
+	event_dispatch();
+done:
+	if (err)
+		log_warnx("%s: %s", title, err->msg);
+	gotd_session_shutdown();
+}
+
+void
+gotd_session_shutdown(void)
+{
+	log_debug("%s: shutting down", gotd_session.title);
+	if (gotd_session.repo)
+		got_repo_close(gotd_session.repo);
+	got_repo_pack_fds_close(gotd_session.pack_fds);
+	got_repo_temp_fds_close(gotd_session.temp_fds);
+	exit(0);
+}
blob - /dev/null
blob + 671359022739ca8e501ab0fbd98de3e76447490e (mode 644)
--- /dev/null
+++ gotd/session.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2022, 2023 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 session_main(const char *, const char *, int *, int *, struct timeval *);