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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
-portable gotd chroot and privdrop
To:
gameoftrees@openbsd.org
Date:
Mon, 9 Nov 2026 15:40:25 +0100

Download raw body.

Thread
  • Stefan Sperling:

    -portable gotd chroot and privdrop

Currently, gotd does not run in -portable because it ends up
trying to call chroot() as a non-root user. It looks like the
privdrop parts are either unfinished or have been removed at
some point, since only the repo-read process even tries to drop
privileges.

Below is the best I could come up with in order to get gotd running
on a Debian system. I assume that the user account which gotd runs as
has a home directory to chroot into. This might not be the case by
default on every OS but is probably a better choice than hard-coding
something like "/var/empty" which will not exist everywhere either.

The only process which keeps running as root is the parent. All the
others are run as the gotd user account (_gotd on Debian as created by
the package). Not every process can use chroot, but several can.

I would merge some of this into got.git, the -U option in particular.

One change I made in got.git in order to help -portable gotd turned
out to be counter-productive. So I'll probably revert that change once
this patch is approved and merged into -portable.

ok?


 make gotd run properly on -portable with privdrop and chroot
 
M  gotd/chroot-notobsd.c  |   1+   3-
M  gotd/gotd.c            |  39+  28-
M  gotd/gotd.h            |   1+   1-

3 files changed, 41 insertions(+), 32 deletions(-)

commit - e18a5f85e18c62a89a11d0066a46a5d1acb56ef15cfb08c7f64df771d2fb5c89
commit + 27480126341922d439ec3620855d1cad8323d88d109d11f6d76feeab9c6033d0
blob - 2d912f39049f4188fa607e318740f84a2f6baccf88cfa92a692996caef2a80b3
blob + f33feda01b5e1c724e3137fd7c201487706b17e188dcb1ed1276902030ef73b6
--- gotd/chroot-notobsd.c
+++ gotd/chroot-notobsd.c
@@ -22,7 +22,7 @@
 
 #include "log.h"
 
-int
+void
 enter_chroot(const char *path)
 {
 	log_info("chroot into %s", path);
@@ -30,6 +30,4 @@ enter_chroot(const char *path)
 		fatal("chroot");
 	if (chdir("/") == -1)
 		fatal("chdir(\"/\")");
-
-	return 1;
 }
blob - c5a8eeb0ec0ca9641f64f796f57c671a7f3bb7a0e093b13580c1189acfafdbe0
blob + 301afe0a5e95da3d58f9ac3d29167bcf1285472c22cc0dd5e0273934374c0d48
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -3044,7 +3044,7 @@ start_child(enum gotd_procid proc_id, const char *repo
     char *argv0, const char *confpath, const char *secretspath,
     int fd, int daemonize, int verbosity)
 {
-	const char	*argv[13];
+	const char	*argv[15];
 	int		 argc = 0;
 	pid_t		 pid;
 
@@ -3097,6 +3097,9 @@ start_child(enum gotd_procid proc_id, const char *repo
 	argv[argc++] = "-f";
 	argv[argc++] = confpath;
 
+	argv[argc++] = "-U";
+	argv[argc++] = gotd.user_name;
+
 	if (secretspath) {
 		argv[argc++] = "-s";
 		argv[argc++] = secretspath;
@@ -3489,7 +3492,7 @@ main(int argc, char **argv)
 
 	log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
 
-	while ((ch = getopt_long(argc, argv, "df:nP:s:T:vV", longopts, NULL)) != -1) {
+	while ((ch = getopt_long(argc, argv, "df:nP:s:T:U:vV", longopts, NULL)) != -1) {
 		switch (ch) {
 		case 'd':
 			daemonize = 0;
@@ -3540,6 +3543,11 @@ main(int argc, char **argv)
 				errx(1, "unknown proc type %s", optarg);
 			}
 			break;
+		case 'U':
+			if (strlcpy(gotd.user_name, optarg,
+			    sizeof(gotd.user_name)) >= sizeof(gotd.user_name))
+				fatal("username too long: %s", optarg);
+			break;
 		case 'v':
 			if (verbosity < 3)
 				verbosity++;
@@ -3564,8 +3572,6 @@ main(int argc, char **argv)
 	}
 	if (geteuid() && proc_id == GOTD_PROC_GOTD)
 		fatalx("need root privileges");
-	if (geteuid() == 0 && proc_id != GOTD_PROC_GOTD)
-		fatalx("must not run as root");
 
 	if (proc_id == GOTD_PROC_GOTD) {
 		const char *p = secretspath ? secretspath : GOTD_SECRETS_PATH;
@@ -3750,6 +3756,15 @@ main(int argc, char **argv)
 		close(GOTD_FILENO_MSG_PIPE);
 
 		reload_reset();
+	} else {
+		pw = getpwnam(gotd.user_name);
+		if (pw == NULL) {
+			uid = strtonum(gotd.user_name, 0, UINT_MAX - 1, &errstr);
+			if (errstr == NULL)
+				pw = getpwuid(uid);
+		}
+		if (pw == NULL)
+			fatalx("user %s not found", gotd.user_name);
 	}
 
 	gotd.argv0 = argv0;
@@ -3804,18 +3819,10 @@ main(int argc, char **argv)
 
 	event_init();
 
-	if (proc_id == GOTD_PROC_RELOAD)
+	switch (proc_id) {
+	case GOTD_PROC_RELOAD:
 		proc_id = GOTD_PROC_GOTD;
-
-	if (proc_id == GOTD_PROC_GOTD) {
-		/* Drop root privileges. */
-		if (setgid(pw->pw_gid) == -1)
-			fatal("setgid %d failed", pw->pw_gid);
-		if (setuid(pw->pw_uid) == -1)
-			fatal("setuid %d failed", pw->pw_uid);
-	}
-
-	switch (proc_id) {
+		/* FALLTHOUGH */
 	case GOTD_PROC_GOTD:
 		if (verbosity) {
 			log_info("socket: %s", gotd.unix_socket_path);
@@ -3843,7 +3850,8 @@ main(int argc, char **argv)
 		 * sockets by revoking all filesystem access via unveil(2).
 		 */
 		apply_unveil_none();
-
+		enter_chroot(pw->pw_dir);
+		drop_privs(pw);
 		listen_main(title);
 		/* NOTREACHED */
 		break;
@@ -3859,7 +3867,7 @@ main(int argc, char **argv)
 		 * files will still work since "getpw" bypasses unveil(2).
 		 */
 		apply_unveil_none();
-
+		drop_privs(pw);
 		auth_main(title);
 		/* NOTREACHED */
 		break;
@@ -3874,6 +3882,7 @@ main(int argc, char **argv)
 			err(1, "pledge");
 #endif
 		apply_unveil_repo_readonly(repo_path, 1);
+		drop_privs(pw);
 		session_read_main(title, repo_path, pack_fds, temp_fds);
 		/* NOTREACHED */
 		break;
@@ -3891,6 +3900,7 @@ main(int argc, char **argv)
 			err(1, "pledge");
 #endif
 		apply_unveil_repo_readwrite(repo_path);
+		drop_privs(pw);
 		session_write_main(title, repo_path, pack_fds, temp_fds,
 		    tmp_fd);
 		/* NOTREACHED */
@@ -3902,17 +3912,12 @@ main(int argc, char **argv)
 			err(1, "pledge");
 #endif
 		apply_unveil_repo_readonly(repo_path, 0);
-
-		if (enter_chroot(repo_path)) {
-			log_info("change repo path %s", repo_path);
-			free(repo_path);
-			repo_path = strdup("/");
-			if (repo_path == NULL)
-				fatal("strdup");
-			log_info("repo path is now %s", repo_path);
-		}
+		enter_chroot(repo_path);
+		free(repo_path);
+		repo_path = strdup("/");
+		if (repo_path == NULL)
+			fatal("strdup");
 		drop_privs(pw);
-
 		repo_read_main(title, repo_path, pack_fds, temp_fds);
 		/* NOTREACHED */
 		exit(0);
@@ -3942,6 +3947,12 @@ main(int argc, char **argv)
 			err(1, "pledge");
 #endif
 		apply_unveil_repo_readonly(repo_path, 0);
+		enter_chroot(repo_path);
+		free(repo_path);
+		repo_path = strdup("/");
+		if (repo_path == NULL)
+			fatal("strdup");
+		drop_privs(pw);
 		repo_write_main(title, repo_path, pack_fds, temp_fds,
 		    tmp_f1, tmp_f2, diff_f1, diff_f2, diff_fd1, diff_fd2);
 		/* NOTREACHED */
@@ -3955,7 +3966,7 @@ main(int argc, char **argv)
 		 * Limit "exec" promise to notification helpers via unveil(2).
 		 */
 		unveil_notification_helpers();
-
+		drop_privs(pw);
 		notify_main(title);
 		/* NOTREACHED */
 		exit(0);
blob - 20a4a6e93efc2cb08d25c7004bd1f74166941d0c830aaa2242fbbb5ff2404b8d
blob + e825fa2b1827b5471fcf08b6629301af0f5f1ba96766e4d90ca827971433735a
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -672,7 +672,7 @@ struct gotd_imsg_notify {
 	/* Followed by username_len data bytes. */
 };
 
-int enter_chroot(const char *);
+void enter_chroot(const char *);
 int gotd_parse_config(const char *, int, enum gotd_procid,
     struct gotd_secrets *, struct gotd *);
 struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd_repolist *);