From 81ef5573116bfe7e2b69a27448e420b53d507c87 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 11:44:54 -0500 Subject: [PATCH 01/10] Use QREXEC_EXIT_PROBLEM for errors spawning child process This is the convention used by the rest of qrexec. This commit should be backported to stable branches. --- agent/qrexec-agent.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index b4f41212..304f4a16 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -164,12 +164,12 @@ _Noreturn void do_exec(const char *cmd, const char *user) pw = getpwuid(geteuid()); if (!pw) { PERROR("getpwuid"); - exit(1); + exit(QREXEC_EXIT_PROBLEM); } if (strcmp(pw->pw_name, user)) { LOG(ERROR, "requested user %s, but qrexec-agent is running as user %s", user, pw->pw_name); - exit(1); + exit(QREXEC_EXIT_PROBLEM); } /* call QUBESRPC if requested */ exec_qubes_rpc_if_requested(cmd, environ); @@ -177,14 +177,14 @@ _Noreturn void do_exec(const char *cmd, const char *user) /* otherwise exec shell */ execl("/bin/sh", "sh", "-c", cmd, NULL); PERROR("execl"); - exit(1); + exit(QREXEC_EXIT_PROBLEM); } pw = getpwnam(user); if (! (pw && pw->pw_name && pw->pw_name[0] && pw->pw_dir && pw->pw_dir[0] && pw->pw_passwd)) { LOG(ERROR, "user %s does not exist", user); - exit(1); + exit(QREXEC_EXIT_PROBLEM); } /* Make a copy of the password information and point pw at the local @@ -309,12 +309,12 @@ _Noreturn void do_exec(const char *cmd, const char *user) if (pam_end(pamh, retval) != PAM_SUCCESS) { /* close Linux-PAM */ pamh = NULL; - exit(1); + exit(QREXEC_EXIT_PROBLEM); } exit(status); error: pam_end(pamh, PAM_ABORT); - exit(1); + exit(QREXEC_EXIT_PROBLEM); #else /* call QUBESRPC if requested */ exec_qubes_rpc_if_requested(cmd, environ); @@ -322,7 +322,7 @@ _Noreturn void do_exec(const char *cmd, const char *user) /* otherwise exec shell */ execl("/bin/su", "su", "-", user, "-c", cmd, NULL); PERROR("execl"); - exit(1); + exit(QREXEC_EXIT_PROBLEM); #endif } From f76920ad5a237ac3fc80976dadd2a98974e80dac Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 16:32:00 -0500 Subject: [PATCH 02/10] Fix checking for memory allocation errors These should never happen, but call exit() if they do. --- agent/qrexec-agent.c | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 304f4a16..41d6b844 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -193,23 +193,30 @@ _Noreturn void do_exec(const char *cmd, const char *user) */ pw_copy = *pw; pw = &pw_copy; - pw->pw_name = strdup(pw->pw_name); - pw->pw_passwd = strdup(pw->pw_passwd); - pw->pw_dir = strdup(pw->pw_dir); - pw->pw_shell = strdup(pw->pw_shell); + if (!((pw->pw_name = strdup(pw->pw_name)) && + (pw->pw_passwd = strdup(pw->pw_passwd)) && + (pw->pw_dir = strdup(pw->pw_dir)) && + (pw->pw_shell = strdup(pw->pw_shell)))) { + PERROR("strdup"); + exit(QREXEC_EXIT_PROBLEM); + } endpwent(); shell_basename = basename (pw->pw_shell); /* this process is going to die shortly, so don't care about freeing */ arg0 = malloc (strlen (shell_basename) + 2); - if (!arg0) - goto error; + if (!arg0) { + PERROR("malloc"); + exit(QREXEC_EXIT_PROBLEM); + } arg0[0] = '-'; strcpy (arg0 + 1, shell_basename); retval = pam_start("qrexec", user, &conv, &pamh); - if (retval != PAM_SUCCESS) + if (retval != PAM_SUCCESS) { + pamh = NULL; goto error; + } retval = pam_authenticate(pamh, 0); if (retval != PAM_SUCCESS) From a18818e06e916a241eead53aa08ae9733f916be2 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 16:36:32 -0500 Subject: [PATCH 03/10] Set PAM error if snprintf() fails --- agent/qrexec-agent.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 41d6b844..3c8e6743 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -237,28 +237,38 @@ _Noreturn void do_exec(const char *cmd, const char *user) goto error; /* provide this variable to child process */ - if ((unsigned)snprintf(env_buf, sizeof(env_buf), "QREXEC_AGENT_PID=%d", getppid()) >= sizeof(env_buf)) + if ((unsigned)snprintf(env_buf, sizeof(env_buf), "QREXEC_AGENT_PID=%d", getppid()) >= sizeof(env_buf)) { + retval = PAM_ABORT; goto error; + } retval = pam_putenv(pamh, env_buf); if (retval != PAM_SUCCESS) goto error; - if ((unsigned)snprintf(env_buf, sizeof(env_buf), "HOME=%s", pw->pw_dir) >= sizeof(env_buf)) + if ((unsigned)snprintf(env_buf, sizeof(env_buf), "HOME=%s", pw->pw_dir) >= sizeof(env_buf)) { + retval = PAM_ABORT; goto error; + } retval = pam_putenv(pamh, env_buf); if (retval != PAM_SUCCESS) goto error; - if ((unsigned)snprintf(env_buf, sizeof(env_buf), "SHELL=%s", pw->pw_shell) >= sizeof(env_buf)) + if ((unsigned)snprintf(env_buf, sizeof(env_buf), "SHELL=%s", pw->pw_shell) >= sizeof(env_buf)) { + retval = PAM_ABORT; goto error; + } retval = pam_putenv(pamh, env_buf); if (retval != PAM_SUCCESS) goto error; - if ((unsigned)snprintf(env_buf, sizeof(env_buf), "USER=%s", pw->pw_name) >= sizeof(env_buf)) + if ((unsigned)snprintf(env_buf, sizeof(env_buf), "USER=%s", pw->pw_name) >= sizeof(env_buf)) { + retval = PAM_ABORT; goto error; + } retval = pam_putenv(pamh, env_buf); if (retval != PAM_SUCCESS) goto error; - if ((unsigned)snprintf(env_buf, sizeof(env_buf), "LOGNAME=%s", pw->pw_name) >= sizeof(env_buf)) + if ((unsigned)snprintf(env_buf, sizeof(env_buf), "LOGNAME=%s", pw->pw_name) >= sizeof(env_buf)) { + retval = PAM_ABORT; goto error; + } retval = pam_putenv(pamh, env_buf); if (retval != PAM_SUCCESS) goto error; From 8b8b49b44511412669dc73e29f1361ba77db1e0a Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 11:47:20 -0500 Subject: [PATCH 04/10] Get effective UID at startup Saves an (admittedly cheap) system call. --- agent/qrexec-agent.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 3c8e6743..7f0aeb1f 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -77,6 +77,8 @@ static int terminate_requested; static int meminfo_write_started = 0; +static uid_t myuid; + static const char *agent_trigger_path = QREXEC_AGENT_TRIGGER_PATH; static const char *fork_server_path = QREXEC_FORK_SERVER_SOCKET; @@ -158,10 +160,10 @@ _Noreturn void do_exec(const char *cmd, const char *user) signal(SIGPIPE, SIG_DFL); #ifdef HAVE_PAM - if (geteuid() != 0) { + if (myuid != 0) { /* We're not root, assume this is a testing environment. */ - pw = getpwuid(geteuid()); + pw = getpwuid(myuid); if (!pw) { PERROR("getpwuid"); exit(QREXEC_EXIT_PROBLEM); @@ -396,6 +398,7 @@ static void init(void) if (handle_handshake(ctrl_vchan) < 0) exit(1); old_umask = umask(0); + myuid = geteuid(); trigger_fd = get_server_socket(agent_trigger_path); umask(old_umask); register_exec_func(do_exec); From 6e6739a4cc9a74a554a5efe40c9833bf9b630832 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 11:57:37 -0500 Subject: [PATCH 05/10] Move waiting on the child to a helper function No functional change intended. --- agent/qrexec-agent.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 7f0aeb1f..f2a0053f 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -122,6 +122,24 @@ static struct pam_conv conv = { NULL }; #endif + +static int really_wait(pid_t child) +{ + int status; + pid_t pid; + do { + pid = waitpid (child, &status, 0); + } while (pid == -1 && errno == EINTR); + if (pid != (pid_t)-1) { + if (WIFSIGNALED (status)) + status = WTERMSIG (status) + 128; + else + status = WEXITSTATUS (status); + } else + status = QREXEC_EXIT_PROBLEM; + return status; +} + /* Start program requested by dom0 in already prepared process * (stdin/stdout/stderr already set, etc) * Called in two cases: @@ -146,7 +164,7 @@ _Noreturn void do_exec(const char *cmd, const char *user) pam_handle_t *pamh=NULL; struct passwd *pw; struct passwd pw_copy; - pid_t child, pid; + pid_t child; char **env; char env_buf[64]; char *arg0; @@ -313,15 +331,7 @@ _Noreturn void do_exec(const char *cmd, const char *user) } /* reachable only in parent */ - pid = waitpid (child, &status, 0); - if (pid != (pid_t)-1) { - if (WIFSIGNALED (status)) - status = WTERMSIG (status) + 128; - else - status = WEXITSTATUS (status); - } else - status = 1; - + status = really_wait(child); retval = pam_close_session (pamh, 0); retval = pam_setcred (pamh, PAM_DELETE_CRED | PAM_SILENT); From 0cf051d8ff27267c9cac6110c640c831d2912c9f Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 12:00:40 -0500 Subject: [PATCH 06/10] agent: Move exec to helper function This will be used by tests later. No functional change intended. --- agent/qrexec-agent.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index f2a0053f..2aa095bd 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -123,6 +123,25 @@ static struct pam_conv conv = { }; #endif +_Noreturn static void really_exec(const struct passwd *pw, char **env, + const char *cmd, const char *arg0) +{ + /* child */ + setsid(); + + /* try to enter home dir, but don't abort if it fails */ + int retval = chdir(pw->pw_dir); + if (retval == -1) + warn("chdir(%s)", pw->pw_dir); + + /* call QUBESRPC if requested */ + exec_qubes_rpc_if_requested(cmd, env); + + /* otherwise exec shell */ + execle(pw->pw_shell, arg0, "-c", cmd, (char*)NULL, env); + _exit(QREXEC_EXIT_PROBLEM); +} + static int really_wait(pid_t child) { int status; @@ -306,21 +325,10 @@ _Noreturn void do_exec(const char *cmd, const char *user) _exit(QREXEC_EXIT_PROBLEM); if (setuid (pw->pw_uid)) _exit(QREXEC_EXIT_PROBLEM); - setsid(); /* This is a copy but don't care to free as we exec later anyway. */ env = pam_getenvlist (pamh); - /* try to enter home dir, but don't abort if it fails */ - retval = chdir(pw->pw_dir); - if (retval == -1) - warn("chdir(%s)", pw->pw_dir); - - /* call QUBESRPC if requested */ - exec_qubes_rpc_if_requested(cmd, env); - - /* otherwise exec shell */ - execle(pw->pw_shell, arg0, "-c", cmd, (char*)NULL, env); - _exit(QREXEC_EXIT_PROBLEM); + really_exec(pw, env, cmd, arg0); default: /* parent */ /* close std*, so when child process closes them, qrexec-agent will receive EOF */ From 79bc074d459ff9ff3f2bde601d5e94fb762583a5 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 12:01:22 -0500 Subject: [PATCH 07/10] Move closing fds to helper function This will be used by tests later. No functional change intended. --- agent/qrexec-agent.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 2aa095bd..809c7851 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -142,6 +142,15 @@ _Noreturn static void really_exec(const struct passwd *pw, char **env, _exit(QREXEC_EXIT_PROBLEM); } +static void close_std(void) +{ + /* close std*, so when child process closes them, qrexec-agent will receive EOF */ + /* this is the main purpose of this reimplementation of /bin/su... */ + close(0); + close(1); + close(2); +} + static int really_wait(pid_t child) { int status; @@ -331,11 +340,7 @@ _Noreturn void do_exec(const char *cmd, const char *user) really_exec(pw, env, cmd, arg0); default: /* parent */ - /* close std*, so when child process closes them, qrexec-agent will receive EOF */ - /* this is the main purpose of this reimplementation of /bin/su... */ - close(0); - close(1); - close(2); + close_std(); } /* reachable only in parent */ From b83c05cbed8f597995a375e7090fe701891a5707 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 16:36:22 -0500 Subject: [PATCH 08/10] Move basename handling to common function This also fixes a bug: basename can mutate its argument, so a copy must be passed to it. --- agent/qrexec-agent.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 809c7851..1a2e138e 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -124,7 +124,7 @@ static struct pam_conv conv = { #endif _Noreturn static void really_exec(const struct passwd *pw, char **env, - const char *cmd, const char *arg0) + const char *cmd) { /* child */ setsid(); @@ -138,6 +138,16 @@ _Noreturn static void really_exec(const struct passwd *pw, char **env, exec_qubes_rpc_if_requested(cmd, env); /* otherwise exec shell */ + char *shell_dup = strdup(pw->pw_shell); + if (shell_dup == NULL) + _exit(QREXEC_EXIT_PROBLEM); + char *shell_basename = basename (shell_dup); + /* this process is going to die shortly, so don't care about freeing */ + char *arg0 = malloc (strlen (shell_basename) + 2); + if (!arg0) + _exit(QREXEC_EXIT_PROBLEM); + arg0[0] = '-'; + strcpy (arg0 + 1, shell_basename); execle(pw->pw_shell, arg0, "-c", cmd, (char*)NULL, env); _exit(QREXEC_EXIT_PROBLEM); } @@ -195,8 +205,6 @@ _Noreturn void do_exec(const char *cmd, const char *user) pid_t child; char **env; char env_buf[64]; - char *arg0; - char *shell_basename; #endif sigset_t sigmask; @@ -250,16 +258,6 @@ _Noreturn void do_exec(const char *cmd, const char *user) } endpwent(); - shell_basename = basename (pw->pw_shell); - /* this process is going to die shortly, so don't care about freeing */ - arg0 = malloc (strlen (shell_basename) + 2); - if (!arg0) { - PERROR("malloc"); - exit(QREXEC_EXIT_PROBLEM); - } - arg0[0] = '-'; - strcpy (arg0 + 1, shell_basename); - retval = pam_start("qrexec", user, &conv, &pamh); if (retval != PAM_SUCCESS) { pamh = NULL; @@ -337,7 +335,7 @@ _Noreturn void do_exec(const char *cmd, const char *user) /* This is a copy but don't care to free as we exec later anyway. */ env = pam_getenvlist (pamh); - really_exec(pw, env, cmd, arg0); + really_exec(pw, env, cmd); default: /* parent */ close_std(); From fc921e11f6fcdb2d298498088334db4c9ca2983d Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sat, 11 Jan 2025 12:02:17 -0500 Subject: [PATCH 09/10] Use fork()/exec() in unit test code This makes the unit test code more like the actual code used by end-users, and therefore makes the tests more accurate. --- agent/qrexec-agent.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 1a2e138e..3b0bb0d6 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -227,13 +227,20 @@ _Noreturn void do_exec(const char *cmd, const char *user) user, pw->pw_name); exit(QREXEC_EXIT_PROBLEM); } - /* call QUBESRPC if requested */ - exec_qubes_rpc_if_requested(cmd, environ); - /* otherwise exec shell */ - execl("/bin/sh", "sh", "-c", cmd, NULL); - PERROR("execl"); - exit(QREXEC_EXIT_PROBLEM); + /* FORK HERE */ + child = fork(); + + switch (child) { + case -1: + goto error; + case 0: + really_exec(pw, environ, cmd); + default: + /* parent */ + close_std(); + exit(really_wait(child)); + } } pw = getpwnam(user); From c7a7826c18fa22e89800cf58f02924f4dbfea5be Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Thu, 9 Jan 2025 16:47:15 -0500 Subject: [PATCH 10/10] Do not close FDs 0, 1, or 2 If they are closed, another file descriptor could be created with these numbers, and so standard library functions that use them might write to an unwanted place. dup2() a file descriptor to /dev/null over them instead. Also statically initialize trigger_fd to -1, which is the conventional value for an invalid file descriptor. This requires care to avoid closing the file descriptor to /dev/null in fix_fds(), which took over an hour to debug. --- agent/qrexec-agent.c | 16 ++++++++++++---- libqrexec/exec.c | 38 ++++++++++++++++++++++++++++++++++--- libqrexec/libqrexec-utils.h | 5 +++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index 3b0bb0d6..70ac7262 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -71,10 +71,12 @@ static libvchan_t *ctrl_vchan; static pid_t wait_for_session_pid = -1; -static int trigger_fd; +static int trigger_fd = -1; static int terminate_requested; +static int null_fd = -1; + static int meminfo_write_started = 0; static uid_t myuid; @@ -156,9 +158,14 @@ static void close_std(void) { /* close std*, so when child process closes them, qrexec-agent will receive EOF */ /* this is the main purpose of this reimplementation of /bin/su... */ - close(0); - close(1); - close(2); + for (int i = 0; i < 3; ++i) { + int j; + do { + j = dup2(null_fd, i); + } while (j == -1 && (errno == EINTR || errno == EBUSY)); + if (j != i) + abort(); + } } static int really_wait(pid_t child) @@ -921,6 +928,7 @@ static _Noreturn void usage(const char *argv0) int main(int argc, char **argv) { sigset_t selectmask; + null_fd = qrexec_open_dev_null(); setup_logging("qrexec-agent"); diff --git a/libqrexec/exec.c b/libqrexec/exec.c index 1c6196c2..0c8a222d 100644 --- a/libqrexec/exec.c +++ b/libqrexec/exec.c @@ -36,6 +36,7 @@ #include #include #include +#include #include "qrexec.h" #include "libqrexec-utils.h" #include "private.h" @@ -47,6 +48,26 @@ void register_exec_func(do_exec_t *func) { exec_func = func; } +static int null_fd = -1; +int qrexec_open_dev_null(void) { + if (null_fd != -1) + abort(); + null_fd = open("/dev/null", O_RDWR|O_CLOEXEC|O_NOCTTY); + if (null_fd < 3) { + int problem = errno; + if (null_fd == 2 || (fcntl(2, F_GETFD) == -1 && errno == EBADF)) { + return 1; + } + /* stderr is open */ + if (null_fd == -1) { + errno = problem; + err(1, "open /dev/null"); + } + errx(1, "File descriptor %d is closed, cannot continue", null_fd); + } + return null_fd; +} + void exec_qubes_rpc_if_requested(const char *prog, char *const envp[]) { /* avoid calling qubes-rpc-multiplexer through shell */ if (strncmp(prog, RPC_REQUEST_COMMAND, RPC_REQUEST_COMMAND_LEN) == 0) { @@ -99,7 +120,16 @@ void fix_fds(int fdin, int fdout, int fderr) abort(); } #ifdef SYS_close_range - int close_range_res = syscall(SYS_close_range, 3, ~0U, 0); + int close_range_res; + if (null_fd == -1) { + close_range_res = syscall(SYS_close_range, 3, ~0U, 0); + } else { + assert(null_fd >= 3); + close_range_res = syscall(SYS_close_range, null_fd + 1, ~0U, 0); + if (null_fd > 3 && close_range_res == 0) { + close_range_res = syscall(SYS_close_range, 3, (unsigned)(null_fd - 1), 0); + } + } if (close_range_res == 0) return; assert(close_range_res == -1); @@ -108,8 +138,10 @@ void fix_fds(int fdin, int fdout, int fderr) abort(); } #endif - for (int i = 3; i < 256; ++i) - close(i); + for (int i = 3; i < 256; ++i) { + if (i != null_fd) + close(i); + } } static int do_fork_exec(const char *user, diff --git a/libqrexec/libqrexec-utils.h b/libqrexec/libqrexec-utils.h index 18d9b141..24aa12fa 100644 --- a/libqrexec/libqrexec-utils.h +++ b/libqrexec/libqrexec-utils.h @@ -162,6 +162,11 @@ void buffer_append(struct buffer *b, const char *data, int len); void buffer_remove(struct buffer *b, int len); int buffer_len(struct buffer *b); void *buffer_data(struct buffer *b); +/* Open /dev/null and keep it from being closed before the exec func is called. + * Returns the newly opened FD. Crashes if called twice. The FD is marked CLOEXEC, + * so it doesn't need to be closed before execve(). */ +__attribute__((visibility("default"))) +int qrexec_open_dev_null(void); int flush_client_data(int fd, struct buffer *buffer); int write_stdin(int fd, const char *data, int len, struct buffer *buffer);