Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not close FDs 0, 1, or 2 #186

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
180 changes: 118 additions & 62 deletions agent/qrexec-agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@

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;

static const char *agent_trigger_path = QREXEC_AGENT_TRIGGER_PATH;
static const char *fork_server_path = QREXEC_FORK_SERVER_SOCKET;

Expand Down Expand Up @@ -120,6 +124,67 @@
NULL
};
#endif

_Noreturn static void really_exec(const struct passwd *pw, char **env,
const char *cmd)
{
/* 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);

Check warning on line 137 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L137

Added line #L137 was not covered by tests

/* call QUBESRPC if requested */
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);

Check warning on line 145 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L145

Added line #L145 was not covered by tests
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);

Check warning on line 150 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L150

Added line #L150 was not covered by tests
arg0[0] = '-';
strcpy (arg0 + 1, shell_basename);
execle(pw->pw_shell, arg0, "-c", cmd, (char*)NULL, 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... */
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();

Check warning on line 167 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L167

Added line #L167 was not covered by tests
}
}

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;

Check warning on line 180 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L180

Added line #L180 was not covered by tests
else
status = WEXITSTATUS (status);
} else
status = QREXEC_EXIT_PROBLEM;

Check warning on line 184 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L184

Added line #L184 was not covered by tests
return status;
}

/* Start program requested by dom0 in already prepared process
* (stdin/stdout/stderr already set, etc)
* Called in two cases:
Expand All @@ -144,11 +209,9 @@
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;
char *shell_basename;
#endif
sigset_t sigmask;

Expand All @@ -158,33 +221,40 @@
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(1);
exit(QREXEC_EXIT_PROBLEM);

Check warning on line 230 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L230

Added line #L230 was not covered by tests
}
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);

Check warning on line 235 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L235

Added line #L235 was not covered by tests
}
/* call QUBESRPC if requested */
exec_qubes_rpc_if_requested(cmd, environ);

/* otherwise exec shell */
execl("/bin/sh", "sh", "-c", cmd, NULL);
PERROR("execl");
exit(1);
/* FORK HERE */
child = fork();

switch (child) {
case -1:
goto error;

Check warning on line 243 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L242-L243

Added lines #L242 - L243 were not covered by tests
case 0:
really_exec(pw, environ, cmd);
default:
/* parent */
close_std();
exit(really_wait(child));
}
}

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);

Check warning on line 257 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L257

Added line #L257 was not covered by tests
}

/* Make a copy of the password information and point pw at the local
Expand All @@ -193,23 +263,20 @@
*/
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);

Check warning on line 271 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L266-L271

Added lines #L266 - L271 were not covered by tests
}
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;
arg0[0] = '-';
strcpy (arg0 + 1, shell_basename);

retval = pam_start("qrexec", user, &conv, &pamh);
if (retval != PAM_SUCCESS)
if (retval != PAM_SUCCESS) {
pamh = NULL;

Check warning on line 277 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L276-L277

Added lines #L276 - L277 were not covered by tests
goto error;
}

retval = pam_authenticate(pamh, 0);
if (retval != PAM_SUCCESS)
Expand All @@ -230,28 +297,38 @@
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;

Check warning on line 301 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L300-L301

Added lines #L300 - L301 were not covered by tests
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;

Check warning on line 308 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L307-L308

Added lines #L307 - L308 were not covered by tests
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;

Check warning on line 315 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L314-L315

Added lines #L314 - L315 were not covered by tests
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;

Check warning on line 322 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L321-L322

Added lines #L321 - L322 were not covered by tests
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;

Check warning on line 329 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L328-L329

Added lines #L328 - L329 were not covered by tests
goto error;
}
retval = pam_putenv(pamh, env_buf);
if (retval != PAM_SUCCESS)
goto error;
Expand All @@ -269,60 +346,37 @@
_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);

Check warning on line 352 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L352

Added line #L352 was not covered by tests
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();

Check warning on line 355 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L355

Added line #L355 was not covered by tests
}

/* 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);

Check warning on line 359 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L359

Added line #L359 was not covered by tests
retval = pam_close_session (pamh, 0);

retval = pam_setcred (pamh, PAM_DELETE_CRED | PAM_SILENT);

if (pam_end(pamh, retval) != PAM_SUCCESS) { /* close Linux-PAM */
pamh = NULL;
exit(1);
exit(QREXEC_EXIT_PROBLEM);

Check warning on line 366 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L366

Added line #L366 was not covered by tests
}
exit(status);
error:
pam_end(pamh, PAM_ABORT);
exit(1);
exit(QREXEC_EXIT_PROBLEM);

Check warning on line 371 in agent/qrexec-agent.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-agent.c#L371

Added line #L371 was not covered by tests
#else
/* call QUBESRPC if requested */
exec_qubes_rpc_if_requested(cmd, environ);

/* otherwise exec shell */
execl("/bin/su", "su", "-", user, "-c", cmd, NULL);
PERROR("execl");
exit(1);
exit(QREXEC_EXIT_PROBLEM);
#endif

}
Expand Down Expand Up @@ -379,6 +433,7 @@
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);
Expand Down Expand Up @@ -873,6 +928,7 @@
int main(int argc, char **argv)
{
sigset_t selectmask;
null_fd = qrexec_open_dev_null();

setup_logging("qrexec-agent");

Expand Down
38 changes: 35 additions & 3 deletions libqrexec/exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#include "qrexec.h"
#include "libqrexec-utils.h"
#include "private.h"
Expand All @@ -47,6 +48,26 @@
exec_func = func;
}

static int null_fd = -1;
int qrexec_open_dev_null(void) {
if (null_fd != -1)
abort();

Check warning on line 54 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L54

Added line #L54 was not covered by tests
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;

Check warning on line 59 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L57-L59

Added lines #L57 - L59 were not covered by tests
}
/* stderr is open */
if (null_fd == -1) {
errno = problem;
err(1, "open /dev/null");

Check warning on line 64 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L62-L64

Added lines #L62 - L64 were not covered by tests
}
errx(1, "File descriptor %d is closed, cannot continue", null_fd);

Check warning on line 66 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L66

Added line #L66 was not covered by tests
}
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) {
Expand Down Expand Up @@ -99,7 +120,16 @@
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);

Check warning on line 130 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L130

Added line #L130 was not covered by tests
}
}
if (close_range_res == 0)
return;
assert(close_range_res == -1);
Expand All @@ -108,8 +138,10 @@
abort();
}
#endif
for (int i = 3; i < 256; ++i)
close(i);
for (int i = 3; i < 256; ++i) {
if (i != null_fd)
close(i);

Check warning on line 143 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L141-L143

Added lines #L141 - L143 were not covered by tests
}
}

static int do_fork_exec(const char *user,
Expand Down
5 changes: 5 additions & 0 deletions libqrexec/libqrexec-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it simpler (and safer) to simply open /dev/null just before dup-ing it over 0,1,2 (in the child process already)?

* 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);
Expand Down