diff options
author | Nick Mathewson <nickm@torproject.org> | 2018-06-28 10:53:34 -0400 |
---|---|---|
committer | Nick Mathewson <nickm@torproject.org> | 2018-06-28 11:18:13 -0400 |
commit | 315e6b59ddb91c19c5102625c6cf0f22b2d6f894 (patch) | |
tree | 6b8b4a72d3ebd1292307de21b9acf8c4c3810d35 /src/lib | |
parent | ec4eee63561c3f6a8bb0c466f17b92e99f832c5d (diff) | |
download | tor-315e6b59ddb91c19c5102625c6cf0f22b2d6f894.tar.gz tor-315e6b59ddb91c19c5102625c6cf0f22b2d6f894.zip |
Extract process-management functionality into a new lib/process
Note that procmon does *not* go here, since procmon needs to
integrate with the event loop.
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/process/.may_include | 15 | ||||
-rw-r--r-- | src/lib/process/daemon.c | 159 | ||||
-rw-r--r-- | src/lib/process/daemon.h | 12 | ||||
-rw-r--r-- | src/lib/process/env.c | 215 | ||||
-rw-r--r-- | src/lib/process/env.h | 36 | ||||
-rw-r--r-- | src/lib/process/include.am | 29 | ||||
-rw-r--r-- | src/lib/process/pidfile.c | 47 | ||||
-rw-r--r-- | src/lib/process/pidfile.h | 11 | ||||
-rw-r--r-- | src/lib/process/restrict.c | 144 | ||||
-rw-r--r-- | src/lib/process/restrict.h | 17 | ||||
-rw-r--r-- | src/lib/process/setuid.c | 375 | ||||
-rw-r--r-- | src/lib/process/setuid.h | 17 | ||||
-rw-r--r-- | src/lib/process/subprocess.c | 1231 | ||||
-rw-r--r-- | src/lib/process/subprocess.h | 129 | ||||
-rw-r--r-- | src/lib/process/waitpid.c | 156 | ||||
-rw-r--r-- | src/lib/process/waitpid.h | 29 |
16 files changed, 2622 insertions, 0 deletions
diff --git a/src/lib/process/.may_include b/src/lib/process/.may_include new file mode 100644 index 0000000000..b195fb0056 --- /dev/null +++ b/src/lib/process/.may_include @@ -0,0 +1,15 @@ +orconfig.h + +lib/cc/*.h +lib/container/*.h +lib/ctime/*.h +lib/err/*.h +lib/fs/*.h +lib/log/*.h +lib/malloc/*.h +lib/process/*.h +lib/string/*.h +lib/testsupport/*.h +lib/thread/*.h + +ht.h
\ No newline at end of file diff --git a/src/lib/process/daemon.c b/src/lib/process/daemon.c new file mode 100644 index 0000000000..edffb04683 --- /dev/null +++ b/src/lib/process/daemon.c @@ -0,0 +1,159 @@ +/* Copyright (c) 2003, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "orconfig.h" +#include "lib/process/daemon.h" + +#ifndef _WIN32 + +#include "lib/fs/files.h" +#include "lib/log/torlog.h" +#include "lib/thread/threads.h" + +#ifdef HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef HAVE_FCNTL_H +#include <fcntl.h> +#endif +#include <errno.h> +#include <stdlib.h> +#include <string.h> + +/* Based on code contributed by christian grothoff */ +/** True iff we've called start_daemon(). */ +static int start_daemon_called = 0; +/** True iff we've called finish_daemon(). */ +static int finish_daemon_called = 0; +/** Socketpair used to communicate between parent and child process while + * daemonizing. */ +static int daemon_filedes[2]; +/** Start putting the process into daemon mode: fork and drop all resources + * except standard fds. The parent process never returns, but stays around + * until finish_daemon is called. (Note: it's safe to call this more + * than once: calls after the first are ignored.) + */ +void +start_daemon(void) +{ + pid_t pid; + + if (start_daemon_called) + return; + start_daemon_called = 1; + + if (pipe(daemon_filedes)) { + /* LCOV_EXCL_START */ + log_err(LD_GENERAL,"pipe failed; exiting. Error was %s", strerror(errno)); + exit(1); // exit ok: during daemonize, pipe failed. + /* LCOV_EXCL_STOP */ + } + pid = fork(); + if (pid < 0) { + /* LCOV_EXCL_START */ + log_err(LD_GENERAL,"fork failed. Exiting."); + exit(1); // exit ok: during daemonize, fork failed + /* LCOV_EXCL_STOP */ + } + if (pid) { /* Parent */ + int ok; + char c; + + close(daemon_filedes[1]); /* we only read */ + ok = -1; + while (0 < read(daemon_filedes[0], &c, sizeof(char))) { + if (c == '.') + ok = 1; + } + fflush(stdout); + if (ok == 1) + exit(0); // exit ok: during daemonize, daemonizing. + else + exit(1); /* child reported error. exit ok: daemonize failed. */ + } else { /* Child */ + close(daemon_filedes[0]); /* we only write */ + + (void) setsid(); /* Detach from controlling terminal */ + /* + * Fork one more time, so the parent (the session group leader) can exit. + * This means that we, as a non-session group leader, can never regain a + * controlling terminal. This part is recommended by Stevens's + * _Advanced Programming in the Unix Environment_. + */ + if (fork() != 0) { + exit(0); // exit ok: during daemonize, fork failed (2) + } + set_main_thread(); /* We are now the main thread. */ + + return; + } +} + +/** Finish putting the process into daemon mode: drop standard fds, and tell + * the parent process to exit. (Note: it's safe to call this more than once: + * calls after the first are ignored. Calls start_daemon first if it hasn't + * been called already.) + */ +void +finish_daemon(const char *desired_cwd) +{ + int nullfd; + char c = '.'; + if (finish_daemon_called) + return; + if (!start_daemon_called) + start_daemon(); + finish_daemon_called = 1; + + if (!desired_cwd) + desired_cwd = "/"; + /* Don't hold the wrong FS mounted */ + if (chdir(desired_cwd) < 0) { + log_err(LD_GENERAL,"chdir to \"%s\" failed. Exiting.",desired_cwd); + exit(1); // exit ok: during daemonize, chdir failed. + } + + nullfd = tor_open_cloexec("/dev/null", O_RDWR, 0); + if (nullfd < 0) { + /* LCOV_EXCL_START */ + log_err(LD_GENERAL,"/dev/null can't be opened. Exiting."); + exit(1); // exit ok: during daemonize, couldn't open /dev/null + /* LCOV_EXCL_STOP */ + } + /* close fds linking to invoking terminal, but + * close usual incoming fds, but redirect them somewhere + * useful so the fds don't get reallocated elsewhere. + */ + if (dup2(nullfd,0) < 0 || + dup2(nullfd,1) < 0 || + dup2(nullfd,2) < 0) { + /* LCOV_EXCL_START */ + log_err(LD_GENERAL,"dup2 failed. Exiting."); + exit(1); // exit ok: during daemonize, dup2 failed. + /* LCOV_EXCL_STOP */ + } + if (nullfd > 2) + close(nullfd); + /* signal success */ + if (write(daemon_filedes[1], &c, sizeof(char)) != sizeof(char)) { + log_err(LD_GENERAL,"write failed. Exiting."); + } + close(daemon_filedes[1]); +} +#else /* !(!defined(_WIN32)) */ +/* defined(_WIN32) */ +void +start_daemon(void) +{ +} +void +finish_daemon(const char *cp) +{ + (void)cp; +} +#endif /* !defined(_WIN32) */ diff --git a/src/lib/process/daemon.h b/src/lib/process/daemon.h new file mode 100644 index 0000000000..48a65b22e6 --- /dev/null +++ b/src/lib/process/daemon.h @@ -0,0 +1,12 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#ifndef TOR_DAEMON_H +#define TOR_DAEMON_H + +void start_daemon(void); +void finish_daemon(const char *desired_cwd); + +#endif diff --git a/src/lib/process/env.c b/src/lib/process/env.c new file mode 100644 index 0000000000..95e1e515d4 --- /dev/null +++ b/src/lib/process/env.c @@ -0,0 +1,215 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "orconfig.h" +#include "lib/process/env.h" + +#include "lib/malloc/util_malloc.h" +#include "lib/ctime/di_ops.h" +#include "lib/container/smartlist.h" +#include "lib/log/util_bug.h" +#include "lib/log/torlog.h" +#include "lib/malloc/util_malloc.h" + +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <stdlib.h> +#include <string.h> + +#ifndef HAVE__NSGETENVIRON +#ifndef HAVE_EXTERN_ENVIRON_DECLARED +/* Some platforms declare environ under some circumstances, others don't. */ +#ifndef RUNNING_DOXYGEN +extern char **environ; +#endif +#endif /* !defined(HAVE_EXTERN_ENVIRON_DECLARED) */ +#endif /* !defined(HAVE__NSGETENVIRON) */ + +/** Return the current environment. This is a portable replacement for + * 'environ'. */ +char ** +get_environment(void) +{ +#ifdef HAVE__NSGETENVIRON + /* This is for compatibility between OSX versions. Otherwise (for example) + * when we do a mostly-static build on OSX 10.7, the resulting binary won't + * work on OSX 10.6. */ + return *_NSGetEnviron(); +#else /* !(defined(HAVE__NSGETENVIRON)) */ + return environ; +#endif /* defined(HAVE__NSGETENVIRON) */ +} + +/** Helper: return the number of characters in <b>s</b> preceding the first + * occurrence of <b>ch</b>. If <b>ch</b> does not occur in <b>s</b>, return + * the length of <b>s</b>. Should be equivalent to strspn(s, "ch"). */ +static inline size_t +str_num_before(const char *s, char ch) +{ + const char *cp = strchr(s, ch); + if (cp) + return cp - s; + else + return strlen(s); +} + +/** Return non-zero iff getenv would consider <b>s1</b> and <b>s2</b> + * to have the same name as strings in a process's environment. */ +int +environment_variable_names_equal(const char *s1, const char *s2) +{ + size_t s1_name_len = str_num_before(s1, '='); + size_t s2_name_len = str_num_before(s2, '='); + + return (s1_name_len == s2_name_len && + tor_memeq(s1, s2, s1_name_len)); +} + +/** Free <b>env</b> (assuming it was produced by + * process_environment_make). */ +void +process_environment_free_(process_environment_t *env) +{ + if (env == NULL) return; + + /* As both an optimization hack to reduce consing on Unixoid systems + * and a nice way to ensure that some otherwise-Windows-specific + * code will always get tested before changes to it get merged, the + * strings which env->unixoid_environment_block points to are packed + * into env->windows_environment_block. */ + tor_free(env->unixoid_environment_block); + tor_free(env->windows_environment_block); + + tor_free(env); +} + +/** Make a process_environment_t containing the environment variables + * specified in <b>env_vars</b> (as C strings of the form + * "NAME=VALUE"). */ +process_environment_t * +process_environment_make(struct smartlist_t *env_vars) +{ + process_environment_t *env = tor_malloc_zero(sizeof(process_environment_t)); + int n_env_vars = smartlist_len(env_vars); + int i; + size_t total_env_length; + smartlist_t *env_vars_sorted; + + tor_assert(n_env_vars + 1 != 0); + env->unixoid_environment_block = tor_calloc(n_env_vars + 1, sizeof(char *)); + /* env->unixoid_environment_block is already NULL-terminated, + * because we assume that NULL == 0 (and check that during compilation). */ + + total_env_length = 1; /* terminating NUL of terminating empty string */ + for (i = 0; i < n_env_vars; ++i) { + const char *s = smartlist_get(env_vars, (int)i); + size_t slen = strlen(s); + + tor_assert(slen + 1 != 0); + tor_assert(slen + 1 < SIZE_MAX - total_env_length); + total_env_length += slen + 1; + } + + env->windows_environment_block = tor_malloc_zero(total_env_length); + /* env->windows_environment_block is already + * (NUL-terminated-empty-string)-terminated. */ + + /* Some versions of Windows supposedly require that environment + * blocks be sorted. Or maybe some Windows programs (or their + * runtime libraries) fail to look up strings in non-sorted + * environment blocks. + * + * Also, sorting strings makes it easy to find duplicate environment + * variables and environment-variable strings without an '=' on all + * OSes, and they can cause badness. Let's complain about those. */ + env_vars_sorted = smartlist_new(); + smartlist_add_all(env_vars_sorted, env_vars); + smartlist_sort_strings(env_vars_sorted); + + /* Now copy the strings into the environment blocks. */ + { + char *cp = env->windows_environment_block; + const char *prev_env_var = NULL; + + for (i = 0; i < n_env_vars; ++i) { + const char *s = smartlist_get(env_vars_sorted, (int)i); + size_t slen = strlen(s); + size_t s_name_len = str_num_before(s, '='); + + if (s_name_len == slen) { + log_warn(LD_GENERAL, + "Preparing an environment containing a variable " + "without a value: %s", + s); + } + if (prev_env_var != NULL && + environment_variable_names_equal(s, prev_env_var)) { + log_warn(LD_GENERAL, + "Preparing an environment containing two variables " + "with the same name: %s and %s", + prev_env_var, s); + } + + prev_env_var = s; + + /* Actually copy the string into the environment. */ + memcpy(cp, s, slen+1); + env->unixoid_environment_block[i] = cp; + cp += slen+1; + } + + tor_assert(cp == env->windows_environment_block + total_env_length - 1); + } + + smartlist_free(env_vars_sorted); + + return env; +} + +/** Return a newly allocated smartlist containing every variable in + * this process's environment, as a NUL-terminated string of the form + * "NAME=VALUE". Note that on some/many/most/all OSes, the parent + * process can put strings not of that form in our environment; + * callers should try to not get crashed by that. + * + * The returned strings are heap-allocated, and must be freed by the + * caller. */ +struct smartlist_t * +get_current_process_environment_variables(void) +{ + smartlist_t *sl = smartlist_new(); + + char **environ_tmp; /* Not const char ** ? Really? */ + for (environ_tmp = get_environment(); *environ_tmp; ++environ_tmp) { + smartlist_add_strdup(sl, *environ_tmp); + } + + return sl; +} + +/** For each string s in <b>env_vars</b> such that + * environment_variable_names_equal(s, <b>new_var</b>), remove it; if + * <b>free_p</b> is non-zero, call <b>free_old</b>(s). If + * <b>new_var</b> contains '=', insert it into <b>env_vars</b>. */ +void +set_environment_variable_in_smartlist(struct smartlist_t *env_vars, + const char *new_var, + void (*free_old)(void*), + int free_p) +{ + SMARTLIST_FOREACH_BEGIN(env_vars, const char *, s) { + if (environment_variable_names_equal(s, new_var)) { + SMARTLIST_DEL_CURRENT(env_vars, s); + if (free_p) { + free_old((void *)s); + } + } + } SMARTLIST_FOREACH_END(s); + + if (strchr(new_var, '=') != NULL) { + smartlist_add(env_vars, (void *)new_var); + } +} diff --git a/src/lib/process/env.h b/src/lib/process/env.h new file mode 100644 index 0000000000..f22599355d --- /dev/null +++ b/src/lib/process/env.h @@ -0,0 +1,36 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#ifndef TOR_ENV_H +#define TOR_ENV_H + +char **get_environment(void); + +struct smartlist_t; + +int environment_variable_names_equal(const char *s1, const char *s2); + +/* DOCDOC process_environment_t */ +typedef struct process_environment_t { + /** A pointer to a sorted empty-string-terminated sequence of + * NUL-terminated strings of the form "NAME=VALUE". */ + char *windows_environment_block; + /** A pointer to a NULL-terminated array of pointers to + * NUL-terminated strings of the form "NAME=VALUE". */ + char **unixoid_environment_block; +} process_environment_t; + +process_environment_t *process_environment_make(struct smartlist_t *env_vars); +void process_environment_free_(process_environment_t *env); +#define process_environment_free(env) \ + FREE_AND_NULL(process_environment_t, process_environment_free_, (env)) + +struct smartlist_t *get_current_process_environment_variables(void); + +void set_environment_variable_in_smartlist(struct smartlist_t *env_vars, + const char *new_var, + void (*free_old)(void*), + int free_p); +#endif diff --git a/src/lib/process/include.am b/src/lib/process/include.am new file mode 100644 index 0000000000..c6cc3a6699 --- /dev/null +++ b/src/lib/process/include.am @@ -0,0 +1,29 @@ + +noinst_LIBRARIES += src/lib/libtor-process.a + +if UNITTESTS_ENABLED +noinst_LIBRARIES += src/lib/libtor-process-testing.a +endif + +src_lib_libtor_process_a_SOURCES = \ + src/lib/process/daemon.c \ + src/lib/process/env.c \ + src/lib/process/pidfile.c \ + src/lib/process/restrict.c \ + src/lib/process/setuid.c \ + src/lib/process/subprocess.c \ + src/lib/process/waitpid.c + +src_lib_libtor_process_testing_a_SOURCES = \ + $(src_lib_libtor_process_a_SOURCES) +src_lib_libtor_process_testing_a_CPPFLAGS = $(AM_CPPFLAGS) $(TEST_CPPFLAGS) +src_lib_libtor_process_testing_a_CFLAGS = $(AM_CFLAGS) $(TEST_CFLAGS) + +noinst_HEADERS += \ + src/lib/process/daemon.h \ + src/lib/process/env.h \ + src/lib/process/pidfile.h \ + src/lib/process/restrict.h \ + src/lib/process/setuid.h \ + src/lib/process/subprocess.h \ + src/lib/process/waitpid.h diff --git a/src/lib/process/pidfile.c b/src/lib/process/pidfile.c new file mode 100644 index 0000000000..f016f21697 --- /dev/null +++ b/src/lib/process/pidfile.c @@ -0,0 +1,47 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "orconfig.h" +#include "lib/process/pidfile.h" + +#include "lib/log/torlog.h" + +#ifdef HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif + +#include <errno.h> +#include <stdio.h> +#include <string.h> + +/** Write the current process ID, followed by NL, into <b>filename</b>. + * Return 0 on success, -1 on failure. + */ +int +write_pidfile(const char *filename) +{ + FILE *pidfile; + + if ((pidfile = fopen(filename, "w")) == NULL) { + log_warn(LD_FS, "Unable to open \"%s\" for writing: %s", filename, + strerror(errno)); + return -1; + } else { +#ifdef _WIN32 + int pid = (int)_getpid(); +#else + int pid = (int)getpid(); +#endif + int rv = 0; + if (fprintf(pidfile, "%d\n", pid) < 0) + rv = -1; + if (fclose(pidfile) < 0) + rv = -1; + return rv; + } +} diff --git a/src/lib/process/pidfile.h b/src/lib/process/pidfile.h new file mode 100644 index 0000000000..c85cd1905e --- /dev/null +++ b/src/lib/process/pidfile.h @@ -0,0 +1,11 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#ifndef TOR_PIDFILE_H +#define TOR_PIDFILE_H + +int write_pidfile(const char *filename); + +#endif diff --git a/src/lib/process/restrict.c b/src/lib/process/restrict.c new file mode 100644 index 0000000000..85c04efbbf --- /dev/null +++ b/src/lib/process/restrict.c @@ -0,0 +1,144 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "orconfig.h" +#include "lib/process/restrict.h" +#include "lib/log/torlog.h" + +/* We only use the linux prctl for now. There is no Win32 support; this may + * also work on various BSD systems and Mac OS X - send testing feedback! + * + * On recent Gnu/Linux kernels it is possible to create a system-wide policy + * that will prevent non-root processes from attaching to other processes + * unless they are the parent process; thus gdb can attach to programs that + * they execute but they cannot attach to other processes running as the same + * user. The system wide policy may be set with the sysctl + * kernel.yama.ptrace_scope or by inspecting + * /proc/sys/kernel/yama/ptrace_scope and it is 1 by default on Ubuntu 11.04. + * + * This ptrace scope will be ignored on Gnu/Linux for users with + * CAP_SYS_PTRACE and so it is very likely that root will still be able to + * attach to the Tor process. + */ +/** Attempt to disable debugger attachment: return 1 on success, -1 on + * failure, and 0 if we don't know how to try on this platform. */ +int +tor_disable_debugger_attach(void) +{ + int r = -1; + log_debug(LD_CONFIG, + "Attemping to disable debugger attachment to Tor for " + "unprivileged users."); +#if defined(__linux__) && defined(HAVE_SYS_PRCTL_H) \ + && defined(HAVE_PRCTL) && defined(PR_SET_DUMPABLE) +#define TRIED_TO_DISABLE + r = prctl(PR_SET_DUMPABLE, 0); +#elif defined(__APPLE__) && defined(PT_DENY_ATTACH) +#define TRIED_TO_ATTACH + r = ptrace(PT_DENY_ATTACH, 0, 0, 0); +#endif /* defined(__linux__) && defined(HAVE_SYS_PRCTL_H) ... || ... */ + + // XXX: TODO - Mac OS X has dtrace and this may be disabled. + // XXX: TODO - Windows probably has something similar +#ifdef TRIED_TO_DISABLE + if (r == 0) { + log_debug(LD_CONFIG,"Debugger attachment disabled for " + "unprivileged users."); + return 1; + } else { + log_warn(LD_CONFIG, "Unable to disable debugger attaching: %s", + strerror(errno)); + } +#endif /* defined(TRIED_TO_DISABLE) */ +#undef TRIED_TO_DISABLE + return r; +} + +#if defined(HAVE_MLOCKALL) && HAVE_DECL_MLOCKALL && defined(RLIMIT_MEMLOCK) +#define HAVE_UNIX_MLOCKALL +#endif + +#ifdef HAVE_UNIX_MLOCKALL +/** Attempt to raise the current and max rlimit to infinity for our process. + * This only needs to be done once and can probably only be done when we have + * not already dropped privileges. + */ +static int +tor_set_max_memlock(void) +{ + /* Future consideration for Windows is probably SetProcessWorkingSetSize + * This is similar to setting the memory rlimit of RLIMIT_MEMLOCK + * http://msdn.microsoft.com/en-us/library/ms686234(VS.85).aspx + */ + + struct rlimit limit; + + /* RLIM_INFINITY is -1 on some platforms. */ + limit.rlim_cur = RLIM_INFINITY; + limit.rlim_max = RLIM_INFINITY; + + if (setrlimit(RLIMIT_MEMLOCK, &limit) == -1) { + if (errno == EPERM) { + log_warn(LD_GENERAL, "You appear to lack permissions to change memory " + "limits. Are you root?"); + } + log_warn(LD_GENERAL, "Unable to raise RLIMIT_MEMLOCK: %s", + strerror(errno)); + return -1; + } + + return 0; +} +#endif /* defined(HAVE_UNIX_MLOCKALL) */ + +/** Attempt to lock all current and all future memory pages. + * This should only be called once and while we're privileged. + * Like mlockall() we return 0 when we're successful and -1 when we're not. + * Unlike mlockall() we return 1 if we've already attempted to lock memory. + */ +int +tor_mlockall(void) +{ + static int memory_lock_attempted = 0; + + if (memory_lock_attempted) { + return 1; + } + + memory_lock_attempted = 1; + + /* + * Future consideration for Windows may be VirtualLock + * VirtualLock appears to implement mlock() but not mlockall() + * + * http://msdn.microsoft.com/en-us/library/aa366895(VS.85).aspx + */ + +#ifdef HAVE_UNIX_MLOCKALL + if (tor_set_max_memlock() == 0) { + log_debug(LD_GENERAL, "RLIMIT_MEMLOCK is now set to RLIM_INFINITY."); + } + + if (mlockall(MCL_CURRENT|MCL_FUTURE) == 0) { + log_info(LD_GENERAL, "Insecure OS paging is effectively disabled."); + return 0; + } else { + if (errno == ENOSYS) { + /* Apple - it's 2009! I'm looking at you. Grrr. */ + log_notice(LD_GENERAL, "It appears that mlockall() is not available on " + "your platform."); + } else if (errno == EPERM) { + log_notice(LD_GENERAL, "It appears that you lack the permissions to " + "lock memory. Are you root?"); + } + log_notice(LD_GENERAL, "Unable to lock all current and future memory " + "pages: %s", strerror(errno)); + return -1; + } +#else /* !(defined(HAVE_UNIX_MLOCKALL)) */ + log_warn(LD_GENERAL, "Unable to lock memory pages. mlockall() unsupported?"); + return -1; +#endif /* defined(HAVE_UNIX_MLOCKALL) */ +} diff --git a/src/lib/process/restrict.h b/src/lib/process/restrict.h new file mode 100644 index 0000000000..d608c9c9bd --- /dev/null +++ b/src/lib/process/restrict.h @@ -0,0 +1,17 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file waitpid.h + * \brief Headers for waitpid.c + **/ + +#ifndef TOR_RESTRICT_H +#define TOR_RESTRICT_H + +int tor_disable_debugger_attach(void); +int tor_mlockall(void); + +#endif /* !defined(TOR_RESTRICT_H) */ diff --git a/src/lib/process/setuid.c b/src/lib/process/setuid.c new file mode 100644 index 0000000000..6c6cb48f52 --- /dev/null +++ b/src/lib/process/setuid.c @@ -0,0 +1,375 @@ +/* Copyright (c) 2003, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "orconfig.h" +#include "lib/process/setuid.h" + +#if defined(HAVE_SYS_CAPABILITY_H) && defined(HAVE_CAP_SET_PROC) +#define HAVE_LINUX_CAPABILITIES +#endif + +#include "lib/container/smartlist.h" +#include "lib/fs/userdb.h" +#include "lib/log/torlog.h" +#include "lib/log/util_bug.h" +#include "lib/malloc/util_malloc.h" + +#ifdef HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef HAVE_GRP_H +#include <grp.h> +#endif +#ifdef HAVE_PWD_H +#include <pwd.h> +#endif + +#include <errno.h> +#include <string.h> + +#ifndef _WIN32 +/** Log details of current user and group credentials. Return 0 on + * success. Logs and return -1 on failure. + */ +static int +log_credential_status(void) +{ +/** Log level to use when describing non-error UID/GID status. */ +#define CREDENTIAL_LOG_LEVEL LOG_INFO + /* Real, effective and saved UIDs */ + uid_t ruid, euid, suid; + /* Read, effective and saved GIDs */ + gid_t rgid, egid, sgid; + /* Supplementary groups */ + gid_t *sup_gids = NULL; + int sup_gids_size; + /* Number of supplementary groups */ + int ngids; + + /* log UIDs */ +#ifdef HAVE_GETRESUID + if (getresuid(&ruid, &euid, &suid) != 0 ) { + log_warn(LD_GENERAL, "Error getting changed UIDs: %s", strerror(errno)); + return -1; + } else { + log_fn(CREDENTIAL_LOG_LEVEL, LD_GENERAL, + "UID is %u (real), %u (effective), %u (saved)", + (unsigned)ruid, (unsigned)euid, (unsigned)suid); + } +#else /* !(defined(HAVE_GETRESUID)) */ + /* getresuid is not present on MacOS X, so we can't get the saved (E)UID */ + ruid = getuid(); + euid = geteuid(); + (void)suid; + + log_fn(CREDENTIAL_LOG_LEVEL, LD_GENERAL, + "UID is %u (real), %u (effective), unknown (saved)", + (unsigned)ruid, (unsigned)euid); +#endif /* defined(HAVE_GETRESUID) */ + + /* log GIDs */ +#ifdef HAVE_GETRESGID + if (getresgid(&rgid, &egid, &sgid) != 0 ) { + log_warn(LD_GENERAL, "Error getting changed GIDs: %s", strerror(errno)); + return -1; + } else { + log_fn(CREDENTIAL_LOG_LEVEL, LD_GENERAL, + "GID is %u (real), %u (effective), %u (saved)", + (unsigned)rgid, (unsigned)egid, (unsigned)sgid); + } +#else /* !(defined(HAVE_GETRESGID)) */ + /* getresgid is not present on MacOS X, so we can't get the saved (E)GID */ + rgid = getgid(); + egid = getegid(); + (void)sgid; + log_fn(CREDENTIAL_LOG_LEVEL, LD_GENERAL, + "GID is %u (real), %u (effective), unknown (saved)", + (unsigned)rgid, (unsigned)egid); +#endif /* defined(HAVE_GETRESGID) */ + + /* log supplementary groups */ + sup_gids_size = 64; + sup_gids = tor_calloc(64, sizeof(gid_t)); + while ((ngids = getgroups(sup_gids_size, sup_gids)) < 0 && + errno == EINVAL && + sup_gids_size < NGROUPS_MAX) { + sup_gids_size *= 2; + sup_gids = tor_reallocarray(sup_gids, sizeof(gid_t), sup_gids_size); + } + + if (ngids < 0) { + log_warn(LD_GENERAL, "Error getting supplementary GIDs: %s", + strerror(errno)); + tor_free(sup_gids); + return -1; + } else { + int i, retval = 0; + char *s = NULL; + smartlist_t *elts = smartlist_new(); + + for (i = 0; i<ngids; i++) { + smartlist_add_asprintf(elts, "%u", (unsigned)sup_gids[i]); + } + + s = smartlist_join_strings(elts, " ", 0, NULL); + + log_fn(CREDENTIAL_LOG_LEVEL, LD_GENERAL, "Supplementary groups are: %s",s); + + tor_free(s); + SMARTLIST_FOREACH(elts, char *, cp, tor_free(cp)); + smartlist_free(elts); + tor_free(sup_gids); + + return retval; + } + + return 0; +} +#endif /* !defined(_WIN32) */ + +/** Return true iff we were compiled with capability support, and capabilities + * seem to work. **/ +int +have_capability_support(void) +{ +#ifdef HAVE_LINUX_CAPABILITIES + cap_t caps = cap_get_proc(); + if (caps == NULL) + return 0; + cap_free(caps); + return 1; +#else /* !(defined(HAVE_LINUX_CAPABILITIES)) */ + return 0; +#endif /* defined(HAVE_LINUX_CAPABILITIES) */ +} + +#ifdef HAVE_LINUX_CAPABILITIES +/** Helper. Drop all capabilities but a small set, and set PR_KEEPCAPS as + * appropriate. + * + * If pre_setuid, retain only CAP_NET_BIND_SERVICE, CAP_SETUID, and + * CAP_SETGID, and use PR_KEEPCAPS to ensure that capabilities persist across + * setuid(). + * + * If not pre_setuid, retain only CAP_NET_BIND_SERVICE, and disable + * PR_KEEPCAPS. + * + * Return 0 on success, and -1 on failure. + */ +static int +drop_capabilities(int pre_setuid) +{ + /* We keep these three capabilities, and these only, as we setuid. + * After we setuid, we drop all but the first. */ + const cap_value_t caplist[] = { + CAP_NET_BIND_SERVICE, CAP_SETUID, CAP_SETGID + }; + const char *where = pre_setuid ? "pre-setuid" : "post-setuid"; + const int n_effective = pre_setuid ? 3 : 1; + const int n_permitted = pre_setuid ? 3 : 1; + const int n_inheritable = 1; + const int keepcaps = pre_setuid ? 1 : 0; + + /* Sets whether we keep capabilities across a setuid. */ + if (prctl(PR_SET_KEEPCAPS, keepcaps) < 0) { + log_warn(LD_CONFIG, "Unable to call prctl() %s: %s", + where, strerror(errno)); + return -1; + } + + cap_t caps = cap_get_proc(); + if (!caps) { + log_warn(LD_CONFIG, "Unable to call cap_get_proc() %s: %s", + where, strerror(errno)); + return -1; + } + cap_clear(caps); + + cap_set_flag(caps, CAP_EFFECTIVE, n_effective, caplist, CAP_SET); + cap_set_flag(caps, CAP_PERMITTED, n_permitted, caplist, CAP_SET); + cap_set_flag(caps, CAP_INHERITABLE, n_inheritable, caplist, CAP_SET); + + int r = cap_set_proc(caps); + cap_free(caps); + if (r < 0) { + log_warn(LD_CONFIG, "No permission to set capabilities %s: %s", + where, strerror(errno)); + return -1; + } + + return 0; +} +#endif /* defined(HAVE_LINUX_CAPABILITIES) */ + +/** Call setuid and setgid to run as <b>user</b> and switch to their + * primary group. Return 0 on success. On failure, log and return -1. + * + * If SWITCH_ID_KEEP_BINDLOW is set in 'flags', try to use the capability + * system to retain the abilitity to bind low ports. + * + * If SWITCH_ID_WARN_IF_NO_CAPS is set in flags, also warn if we have + * don't have capability support. + */ +int +switch_id(const char *user, const unsigned flags) +{ +#ifndef _WIN32 + const struct passwd *pw = NULL; + uid_t old_uid; + gid_t old_gid; + static int have_already_switched_id = 0; + const int keep_bindlow = !!(flags & SWITCH_ID_KEEP_BINDLOW); + const int warn_if_no_caps = !!(flags & SWITCH_ID_WARN_IF_NO_CAPS); + + tor_assert(user); + + if (have_already_switched_id) + return 0; + + /* Log the initial credential state */ + if (log_credential_status()) + return -1; + + log_fn(CREDENTIAL_LOG_LEVEL, LD_GENERAL, "Changing user and groups"); + + /* Get old UID/GID to check if we changed correctly */ + old_uid = getuid(); + old_gid = getgid(); + + /* Lookup the user and group information, if we have a problem, bail out. */ + pw = tor_getpwnam(user); + if (pw == NULL) { + log_warn(LD_CONFIG, "Error setting configured user: %s not found", user); + return -1; + } + +#ifdef HAVE_LINUX_CAPABILITIES + (void) warn_if_no_caps; + if (keep_bindlow) { + if (drop_capabilities(1)) + return -1; + } +#else /* !(defined(HAVE_LINUX_CAPABILITIES)) */ + (void) keep_bindlow; + if (warn_if_no_caps) { + log_warn(LD_CONFIG, "KeepBindCapabilities set, but no capability support " + "on this system."); + } +#endif /* defined(HAVE_LINUX_CAPABILITIES) */ + + /* Properly switch egid,gid,euid,uid here or bail out */ + if (setgroups(1, &pw->pw_gid)) { + log_warn(LD_GENERAL, "Error setting groups to gid %d: \"%s\".", + (int)pw->pw_gid, strerror(errno)); + if (old_uid == pw->pw_uid) { + log_warn(LD_GENERAL, "Tor is already running as %s. You do not need " + "the \"User\" option if you are already running as the user " + "you want to be. (If you did not set the User option in your " + "torrc, check whether it was specified on the command line " + "by a startup script.)", user); + } else { + log_warn(LD_GENERAL, "If you set the \"User\" option, you must start Tor" + " as root."); + } + return -1; + } + + if (setegid(pw->pw_gid)) { + log_warn(LD_GENERAL, "Error setting egid to %d: %s", + (int)pw->pw_gid, strerror(errno)); + return -1; + } + + if (setgid(pw->pw_gid)) { + log_warn(LD_GENERAL, "Error setting gid to %d: %s", + (int)pw->pw_gid, strerror(errno)); + return -1; + } + + if (setuid(pw->pw_uid)) { + log_warn(LD_GENERAL, "Error setting configured uid to %s (%d): %s", + user, (int)pw->pw_uid, strerror(errno)); + return -1; + } + + if (seteuid(pw->pw_uid)) { + log_warn(LD_GENERAL, "Error setting configured euid to %s (%d): %s", + user, (int)pw->pw_uid, strerror(errno)); + return -1; + } + + /* This is how OpenBSD rolls: + if (setgroups(1, &pw->pw_gid) || setegid(pw->pw_gid) || + setgid(pw->pw_gid) || setuid(pw->pw_uid) || seteuid(pw->pw_uid)) { + setgid(pw->pw_gid) || seteuid(pw->pw_uid) || setuid(pw->pw_uid)) { + log_warn(LD_GENERAL, "Error setting configured UID/GID: %s", + strerror(errno)); + return -1; + } + */ + + /* We've properly switched egid, gid, euid, uid, and supplementary groups if + * we're here. */ +#ifdef HAVE_LINUX_CAPABILITIES + if (keep_bindlow) { + if (drop_capabilities(0)) + return -1; + } +#endif /* defined(HAVE_LINUX_CAPABILITIES) */ + +#if !defined(CYGWIN) && !defined(__CYGWIN__) + /* If we tried to drop privilege to a group/user other than root, attempt to + * restore root (E)(U|G)ID, and abort if the operation succeeds */ + + /* Only check for privilege dropping if we were asked to be non-root */ + if (pw->pw_uid) { + /* Try changing GID/EGID */ + if (pw->pw_gid != old_gid && + (setgid(old_gid) != -1 || setegid(old_gid) != -1)) { + log_warn(LD_GENERAL, "Was able to restore group credentials even after " + "switching GID: this means that the setgid code didn't work."); + return -1; + } + + /* Try changing UID/EUID */ + if (pw->pw_uid != old_uid && + (setuid(old_uid) != -1 || seteuid(old_uid) != -1)) { + log_warn(LD_GENERAL, "Was able to restore user credentials even after " + "switching UID: this means that the setuid code didn't work."); + return -1; + } + } +#endif /* !defined(CYGWIN) && !defined(__CYGWIN__) */ + + /* Check what really happened */ + if (log_credential_status()) { + return -1; + } + + have_already_switched_id = 1; /* mark success so we never try again */ + +#if defined(__linux__) && defined(HAVE_SYS_PRCTL_H) && \ + defined(HAVE_PRCTL) && defined(PR_SET_DUMPABLE) + if (pw->pw_uid) { + /* Re-enable core dumps if we're not running as root. */ + log_info(LD_CONFIG, "Re-enabling coredumps"); + if (prctl(PR_SET_DUMPABLE, 1)) { + log_warn(LD_CONFIG, "Unable to re-enable coredumps: %s",strerror(errno)); + } + } +#endif /* defined(__linux__) && defined(HAVE_SYS_PRCTL_H) && ... */ + return 0; + +#else /* !(!defined(_WIN32)) */ + (void)user; + (void)flags; + + log_warn(LD_CONFIG, "Switching users is unsupported on your OS."); + return -1; +#endif /* !defined(_WIN32) */ +} diff --git a/src/lib/process/setuid.h b/src/lib/process/setuid.h new file mode 100644 index 0000000000..61aeefe1b7 --- /dev/null +++ b/src/lib/process/setuid.h @@ -0,0 +1,17 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#ifndef TOR_SETUID_H +#define TOR_SETUID_H + +int have_capability_support(void); + +/** Flag for switch_id; see switch_id() for documentation */ +#define SWITCH_ID_KEEP_BINDLOW (1<<0) +/** Flag for switch_id; see switch_id() for documentation */ +#define SWITCH_ID_WARN_IF_NO_CAPS (1<<1) +int switch_id(const char *user, unsigned flags); + +#endif diff --git a/src/lib/process/subprocess.c b/src/lib/process/subprocess.c new file mode 100644 index 0000000000..516494d105 --- /dev/null +++ b/src/lib/process/subprocess.c @@ -0,0 +1,1231 @@ +/* Copyright (c) 2003, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#define SUBPROCESS_PRIVATE +#include "lib/process/subprocess.h" + +#include "lib/container/smartlist.h" +#include "lib/err/torerr.h" +#include "lib/log/torlog.h" +#include "lib/log/util_bug.h" +#include "lib/log/win32err.h" +#include "lib/malloc/util_malloc.h" +#include "lib/process/env.h" +#include "lib/process/waitpid.h" +#include "lib/string/compat_ctype.h" + +#ifdef HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif +#ifdef HAVE_SYS_PRCTL_H +#include <sys/prctl.h> +#endif +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#ifdef HAVE_SIGNAL_H +#include <signal.h> +#endif +#ifdef HAVE_FCNTL_H +#include <fcntl.h> +#endif +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif +#include <errno.h> +#include <string.h> + +/** Format a single argument for being put on a Windows command line. + * Returns a newly allocated string */ +static char * +format_win_cmdline_argument(const char *arg) +{ + char *formatted_arg; + char need_quotes; + const char *c; + int i; + int bs_counter = 0; + /* Backslash we can point to when one is inserted into the string */ + const char backslash = '\\'; + + /* Smartlist of *char */ + smartlist_t *arg_chars; + arg_chars = smartlist_new(); + + /* Quote string if it contains whitespace or is empty */ + need_quotes = (strchr(arg, ' ') || strchr(arg, '\t') || '\0' == arg[0]); + + /* Build up smartlist of *chars */ + for (c=arg; *c != '\0'; c++) { + if ('"' == *c) { + /* Double up backslashes preceding a quote */ + for (i=0; i<(bs_counter*2); i++) + smartlist_add(arg_chars, (void*)&backslash); + bs_counter = 0; + /* Escape the quote */ + smartlist_add(arg_chars, (void*)&backslash); + smartlist_add(arg_chars, (void*)c); + } else if ('\\' == *c) { + /* Count backslashes until we know whether to double up */ + bs_counter++; + } else { + /* Don't double up slashes preceding a non-quote */ + for (i=0; i<bs_counter; i++) + smartlist_add(arg_chars, (void*)&backslash); + bs_counter = 0; + smartlist_add(arg_chars, (void*)c); + } + } + /* Don't double up trailing backslashes */ + for (i=0; i<bs_counter; i++) + smartlist_add(arg_chars, (void*)&backslash); + + /* Allocate space for argument, quotes (if needed), and terminator */ + const size_t formatted_arg_len = smartlist_len(arg_chars) + + (need_quotes ? 2 : 0) + 1; + formatted_arg = tor_malloc_zero(formatted_arg_len); + + /* Add leading quote */ + i=0; + if (need_quotes) + formatted_arg[i++] = '"'; + + /* Add characters */ + SMARTLIST_FOREACH(arg_chars, char*, ch, + { + formatted_arg[i++] = *ch; + }); + + /* Add trailing quote */ + if (need_quotes) + formatted_arg[i++] = '"'; + formatted_arg[i] = '\0'; + + smartlist_free(arg_chars); + return formatted_arg; +} + +/** Format a command line for use on Windows, which takes the command as a + * string rather than string array. Follows the rules from "Parsing C++ + * Command-Line Arguments" in MSDN. Algorithm based on list2cmdline in the + * Python subprocess module. Returns a newly allocated string */ +char * +tor_join_win_cmdline(const char *argv[]) +{ + smartlist_t *argv_list; + char *joined_argv; + int i; + + /* Format each argument and put the result in a smartlist */ + argv_list = smartlist_new(); + for (i=0; argv[i] != NULL; i++) { + smartlist_add(argv_list, (void *)format_win_cmdline_argument(argv[i])); + } + + /* Join the arguments with whitespace */ + joined_argv = smartlist_join_strings(argv_list, " ", 0, NULL); + + /* Free the newly allocated arguments, and the smartlist */ + SMARTLIST_FOREACH(argv_list, char *, arg, + { + tor_free(arg); + }); + smartlist_free(argv_list); + + return joined_argv; +} + +#ifndef _WIN32 +/** Format <b>child_state</b> and <b>saved_errno</b> as a hex string placed in + * <b>hex_errno</b>. Called between fork and _exit, so must be signal-handler + * safe. + * + * <b>hex_errno</b> must have at least HEX_ERRNO_SIZE+1 bytes available. + * + * The format of <b>hex_errno</b> is: "CHILD_STATE/ERRNO\n", left-padded + * with spaces. CHILD_STATE indicates where + * in the process of starting the child process did the failure occur (see + * CHILD_STATE_* macros for definition), and SAVED_ERRNO is the value of + * errno when the failure occurred. + * + * On success return the number of characters added to hex_errno, not counting + * the terminating NUL; return -1 on error. + */ +STATIC int +format_helper_exit_status(unsigned char child_state, int saved_errno, + char *hex_errno) +{ + unsigned int unsigned_errno; + int written, left; + char *cur; + size_t i; + int res = -1; + + /* Fill hex_errno with spaces, and a trailing newline (memset may + not be signal handler safe, so we can't use it) */ + for (i = 0; i < (HEX_ERRNO_SIZE - 1); i++) + hex_errno[i] = ' '; + hex_errno[HEX_ERRNO_SIZE - 1] = '\n'; + + /* Convert errno to be unsigned for hex conversion */ + if (saved_errno < 0) { + // Avoid overflow on the cast to unsigned int when result is INT_MIN + // by adding 1 to the signed int negative value, + // then, after it has been negated and cast to unsigned, + // adding the original 1 back (the double-addition is intentional). + // Otherwise, the cast to signed could cause a temporary int + // to equal INT_MAX + 1, which is undefined. + unsigned_errno = ((unsigned int) -(saved_errno + 1)) + 1; + } else { + unsigned_errno = (unsigned int) saved_errno; + } + + /* + * Count how many chars of space we have left, and keep a pointer into the + * current point in the buffer. + */ + left = HEX_ERRNO_SIZE+1; + cur = hex_errno; + + /* Emit child_state */ + written = format_hex_number_sigsafe(child_state, cur, left); + + if (written <= 0) + goto err; + + /* Adjust left and cur */ + left -= written; + cur += written; + if (left <= 0) + goto err; + + /* Now the '/' */ + *cur = '/'; + + /* Adjust left and cur */ + ++cur; + --left; + if (left <= 0) + goto err; + + /* Need minus? */ + if (saved_errno < 0) { + *cur = '-'; + ++cur; + --left; + if (left <= 0) + goto err; + } + + /* Emit unsigned_errno */ + written = format_hex_number_sigsafe(unsigned_errno, cur, left); + + if (written <= 0) + goto err; + + /* Adjust left and cur */ + left -= written; + cur += written; + + /* Check that we have enough space left for a newline and a NUL */ + if (left <= 1) + goto err; + + /* Emit the newline and NUL */ + *cur++ = '\n'; + *cur++ = '\0'; + + res = (int)(cur - hex_errno - 1); + + goto done; + + err: + /* + * In error exit, just write a '\0' in the first char so whatever called + * this at least won't fall off the end. + */ + *hex_errno = '\0'; + + done: + return res; +} +#endif /* !defined(_WIN32) */ + +/* Maximum number of file descriptors, if we cannot get it via sysconf() */ +#define DEFAULT_MAX_FD 256 + +/** Terminate the process of <b>process_handle</b>, if that process has not + * already exited. + * + * Return 0 if we succeeded in terminating the process (or if the process + * already exited), and -1 if we tried to kill the process but failed. + * + * Based on code originally borrowed from Python's os.kill. */ +int +tor_terminate_process(process_handle_t *process_handle) +{ +#ifdef _WIN32 + if (tor_get_exit_code(process_handle, 0, NULL) == PROCESS_EXIT_RUNNING) { + HANDLE handle = process_handle->pid.hProcess; + + if (!TerminateProcess(handle, 0)) + return -1; + else + return 0; + } +#else /* !(defined(_WIN32)) */ + if (process_handle->waitpid_cb) { + /* We haven't got a waitpid yet, so we can just kill off the process. */ + return kill(process_handle->pid, SIGTERM); + } +#endif /* defined(_WIN32) */ + + return 0; /* We didn't need to kill the process, so report success */ +} + +/** Return the Process ID of <b>process_handle</b>. */ +int +tor_process_get_pid(process_handle_t *process_handle) +{ +#ifdef _WIN32 + return (int) process_handle->pid.dwProcessId; +#else + return (int) process_handle->pid; +#endif +} + +#ifdef _WIN32 +HANDLE +tor_process_get_stdout_pipe(process_handle_t *process_handle) +{ + return process_handle->stdout_pipe; +} +#else /* !(defined(_WIN32)) */ +/* DOCDOC tor_process_get_stdout_pipe */ +int +tor_process_get_stdout_pipe(process_handle_t *process_handle) +{ + return process_handle->stdout_pipe; +} +#endif /* defined(_WIN32) */ + +/* DOCDOC process_handle_new */ +static process_handle_t * +process_handle_new(void) +{ + process_handle_t *out = tor_malloc_zero(sizeof(process_handle_t)); + +#ifdef _WIN32 + out->stdin_pipe = INVALID_HANDLE_VALUE; + out->stdout_pipe = INVALID_HANDLE_VALUE; + out->stderr_pipe = INVALID_HANDLE_VALUE; +#else + out->stdin_pipe = -1; + out->stdout_pipe = -1; + out->stderr_pipe = -1; +#endif /* defined(_WIN32) */ + + return out; +} + +#ifndef _WIN32 +/** Invoked when a process that we've launched via tor_spawn_background() has + * been found to have terminated. + */ +static void +process_handle_waitpid_cb(int status, void *arg) +{ + process_handle_t *process_handle = arg; + + process_handle->waitpid_exit_status = status; + clear_waitpid_callback(process_handle->waitpid_cb); + if (process_handle->status == PROCESS_STATUS_RUNNING) + process_handle->status = PROCESS_STATUS_NOTRUNNING; + process_handle->waitpid_cb = 0; +} +#endif /* !defined(_WIN32) */ + +/** + * @name child-process states + * + * Each of these values represents a possible state that a child process can + * be in. They're used to determine what to say when telling the parent how + * far along we were before failure. + * + * @{ + */ +#define CHILD_STATE_INIT 0 +#define CHILD_STATE_PIPE 1 +#define CHILD_STATE_MAXFD 2 +#define CHILD_STATE_FORK 3 +#define CHILD_STATE_DUPOUT 4 +#define CHILD_STATE_DUPERR 5 +#define CHILD_STATE_DUPIN 6 +#define CHILD_STATE_CLOSEFD 7 +#define CHILD_STATE_EXEC 8 +#define CHILD_STATE_FAILEXEC 9 +/** @} */ +/** + * Boolean. If true, then Tor may call execve or CreateProcess via + * tor_spawn_background. + **/ +static int may_spawn_background_process = 1; +/** + * Turn off may_spawn_background_process, so that all future calls to + * tor_spawn_background are guaranteed to fail. + **/ +void +tor_disable_spawning_background_processes(void) +{ + may_spawn_background_process = 0; +} +/** Start a program in the background. If <b>filename</b> contains a '/', then + * it will be treated as an absolute or relative path. Otherwise, on + * non-Windows systems, the system path will be searched for <b>filename</b>. + * On Windows, only the current directory will be searched. Here, to search the + * system path (as well as the application directory, current working + * directory, and system directories), set filename to NULL. + * + * The strings in <b>argv</b> will be passed as the command line arguments of + * the child program (following convention, argv[0] should normally be the + * filename of the executable, and this must be the case if <b>filename</b> is + * NULL). The last element of argv must be NULL. A handle to the child process + * will be returned in process_handle (which must be non-NULL). Read + * process_handle.status to find out if the process was successfully launched. + * For convenience, process_handle.status is returned by this function. + * + * Some parts of this code are based on the POSIX subprocess module from + * Python, and example code from + * http://msdn.microsoft.com/en-us/library/ms682499%28v=vs.85%29.aspx. + */ +int +tor_spawn_background(const char *const filename, const char **argv, + process_environment_t *env, + process_handle_t **process_handle_out) +{ + if (BUG(may_spawn_background_process == 0)) { + /* We should never reach this point if we're forbidden to spawn + * processes. Instead we should have caught the attempt earlier. */ + return PROCESS_STATUS_ERROR; + } + +#ifdef _WIN32 + HANDLE stdout_pipe_read = NULL; + HANDLE stdout_pipe_write = NULL; + HANDLE stderr_pipe_read = NULL; + HANDLE stderr_pipe_write = NULL; + HANDLE stdin_pipe_read = NULL; + HANDLE stdin_pipe_write = NULL; + process_handle_t *process_handle; + int status; + + STARTUPINFOA siStartInfo; + BOOL retval = FALSE; + + SECURITY_ATTRIBUTES saAttr; + char *joined_argv; + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + /* TODO: should we set explicit security attributes? (#2046, comment 5) */ + saAttr.lpSecurityDescriptor = NULL; + + /* Assume failure to start process */ + status = PROCESS_STATUS_ERROR; + + /* Set up pipe for stdout */ + if (!CreatePipe(&stdout_pipe_read, &stdout_pipe_write, &saAttr, 0)) { + log_warn(LD_GENERAL, + "Failed to create pipe for stdout communication with child process: %s", + format_win32_error(GetLastError())); + return status; + } + if (!SetHandleInformation(stdout_pipe_read, HANDLE_FLAG_INHERIT, 0)) { + log_warn(LD_GENERAL, + "Failed to configure pipe for stdout communication with child " + "process: %s", format_win32_error(GetLastError())); + return status; + } + + /* Set up pipe for stderr */ + if (!CreatePipe(&stderr_pipe_read, &stderr_pipe_write, &saAttr, 0)) { + log_warn(LD_GENERAL, + "Failed to create pipe for stderr communication with child process: %s", + format_win32_error(GetLastError())); + return status; + } + if (!SetHandleInformation(stderr_pipe_read, HANDLE_FLAG_INHERIT, 0)) { + log_warn(LD_GENERAL, + "Failed to configure pipe for stderr communication with child " + "process: %s", format_win32_error(GetLastError())); + return status; + } + + /* Set up pipe for stdin */ + if (!CreatePipe(&stdin_pipe_read, &stdin_pipe_write, &saAttr, 0)) { + log_warn(LD_GENERAL, + "Failed to create pipe for stdin communication with child process: %s", + format_win32_error(GetLastError())); + return status; + } + if (!SetHandleInformation(stdin_pipe_write, HANDLE_FLAG_INHERIT, 0)) { + log_warn(LD_GENERAL, + "Failed to configure pipe for stdin communication with child " + "process: %s", format_win32_error(GetLastError())); + return status; + } + + /* Create the child process */ + + /* Windows expects argv to be a whitespace delimited string, so join argv up + */ + joined_argv = tor_join_win_cmdline(argv); + + process_handle = process_handle_new(); + process_handle->status = status; + + ZeroMemory(&(process_handle->pid), sizeof(PROCESS_INFORMATION)); + ZeroMemory(&siStartInfo, sizeof(STARTUPINFO)); + siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.hStdError = stderr_pipe_write; + siStartInfo.hStdOutput = stdout_pipe_write; + siStartInfo.hStdInput = stdin_pipe_read; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + + /* Create the child process */ + + retval = CreateProcessA(filename, // module name + joined_argv, // command line + /* TODO: should we set explicit security attributes? (#2046, comment 5) */ + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + /*(TODO: set CREATE_NEW CONSOLE/PROCESS_GROUP to make GetExitCodeProcess() + * work?) */ + CREATE_NO_WINDOW, // creation flags + (env==NULL) ? NULL : env->windows_environment_block, + NULL, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &(process_handle->pid)); // receives PROCESS_INFORMATION + + tor_free(joined_argv); + + if (!retval) { + log_warn(LD_GENERAL, + "Failed to create child process %s: %s", filename?filename:argv[0], + format_win32_error(GetLastError())); + tor_free(process_handle); + } else { + /* TODO: Close hProcess and hThread in process_handle->pid? */ + process_handle->stdout_pipe = stdout_pipe_read; + process_handle->stderr_pipe = stderr_pipe_read; + process_handle->stdin_pipe = stdin_pipe_write; + status = process_handle->status = PROCESS_STATUS_RUNNING; + } + + /* TODO: Close pipes on exit */ + *process_handle_out = process_handle; + return status; +#else /* !(defined(_WIN32)) */ + pid_t pid; + int stdout_pipe[2]; + int stderr_pipe[2]; + int stdin_pipe[2]; + int fd, retval; + process_handle_t *process_handle; + int status; + + const char *error_message = SPAWN_ERROR_MESSAGE; + size_t error_message_length; + + /* Represents where in the process of spawning the program is; + this is used for printing out the error message */ + unsigned char child_state = CHILD_STATE_INIT; + + char hex_errno[HEX_ERRNO_SIZE + 2]; /* + 1 should be sufficient actually */ + + static int max_fd = -1; + + status = PROCESS_STATUS_ERROR; + + /* We do the strlen here because strlen() is not signal handler safe, + and we are not allowed to use unsafe functions between fork and exec */ + error_message_length = strlen(error_message); + + // child_state = CHILD_STATE_PIPE; + + /* Set up pipe for redirecting stdout, stderr, and stdin of child */ + retval = pipe(stdout_pipe); + if (-1 == retval) { + log_warn(LD_GENERAL, + "Failed to set up pipe for stdout communication with child process: %s", + strerror(errno)); + return status; + } + + retval = pipe(stderr_pipe); + if (-1 == retval) { + log_warn(LD_GENERAL, + "Failed to set up pipe for stderr communication with child process: %s", + strerror(errno)); + + close(stdout_pipe[0]); + close(stdout_pipe[1]); + + return status; + } + + retval = pipe(stdin_pipe); + if (-1 == retval) { + log_warn(LD_GENERAL, + "Failed to set up pipe for stdin communication with child process: %s", + strerror(errno)); + + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stderr_pipe[0]); + close(stderr_pipe[1]); + + return status; + } + + // child_state = CHILD_STATE_MAXFD; + +#ifdef _SC_OPEN_MAX + if (-1 == max_fd) { + max_fd = (int) sysconf(_SC_OPEN_MAX); + if (max_fd == -1) { + max_fd = DEFAULT_MAX_FD; + log_warn(LD_GENERAL, + "Cannot find maximum file descriptor, assuming %d", max_fd); + } + } +#else /* !(defined(_SC_OPEN_MAX)) */ + max_fd = DEFAULT_MAX_FD; +#endif /* defined(_SC_OPEN_MAX) */ + + // child_state = CHILD_STATE_FORK; + + pid = fork(); + if (0 == pid) { + /* In child */ + +#if defined(HAVE_SYS_PRCTL_H) && defined(__linux__) + /* Attempt to have the kernel issue a SIGTERM if the parent + * goes away. Certain attributes of the binary being execve()ed + * will clear this during the execve() call, but it's better + * than nothing. + */ + prctl(PR_SET_PDEATHSIG, SIGTERM); +#endif /* defined(HAVE_SYS_PRCTL_H) && defined(__linux__) */ + + child_state = CHILD_STATE_DUPOUT; + + /* Link child stdout to the write end of the pipe */ + retval = dup2(stdout_pipe[1], STDOUT_FILENO); + if (-1 == retval) + goto error; + + child_state = CHILD_STATE_DUPERR; + + /* Link child stderr to the write end of the pipe */ + retval = dup2(stderr_pipe[1], STDERR_FILENO); + if (-1 == retval) + goto error; + + child_state = CHILD_STATE_DUPIN; + + /* Link child stdin to the read end of the pipe */ + retval = dup2(stdin_pipe[0], STDIN_FILENO); + if (-1 == retval) + goto error; + + // child_state = CHILD_STATE_CLOSEFD; + + close(stderr_pipe[0]); + close(stderr_pipe[1]); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stdin_pipe[0]); + close(stdin_pipe[1]); + + /* Close all other fds, including the read end of the pipe */ + /* XXX: We should now be doing enough FD_CLOEXEC setting to make + * this needless. */ + for (fd = STDERR_FILENO + 1; fd < max_fd; fd++) { + close(fd); + } + + // child_state = CHILD_STATE_EXEC; + + /* Call the requested program. We need the cast because + execvp doesn't define argv as const, even though it + does not modify the arguments */ + if (env) + execve(filename, (char *const *) argv, env->unixoid_environment_block); + else { + static char *new_env[] = { NULL }; + execve(filename, (char *const *) argv, new_env); + } + + /* If we got here, the exec or open(/dev/null) failed */ + + child_state = CHILD_STATE_FAILEXEC; + + error: + { + /* XXX: are we leaking fds from the pipe? */ + int n, err=0; + ssize_t nbytes; + + n = format_helper_exit_status(child_state, errno, hex_errno); + + if (n >= 0) { + /* Write the error message. GCC requires that we check the return + value, but there is nothing we can do if it fails */ + /* TODO: Don't use STDOUT, use a pipe set up just for this purpose */ + nbytes = write(STDOUT_FILENO, error_message, error_message_length); + err = (nbytes < 0); + nbytes = write(STDOUT_FILENO, hex_errno, n); + err += (nbytes < 0); + } + + _exit(err?254:255); // exit ok: in child. + } + + /* Never reached, but avoids compiler warning */ + return status; // LCOV_EXCL_LINE + } + + /* In parent */ + + if (-1 == pid) { + log_warn(LD_GENERAL, "Failed to fork child process: %s", strerror(errno)); + close(stdin_pipe[0]); + close(stdin_pipe[1]); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + close(stderr_pipe[0]); + close(stderr_pipe[1]); + return status; + } + + process_handle = process_handle_new(); + process_handle->status = status; + process_handle->pid = pid; + + /* TODO: If the child process forked but failed to exec, waitpid it */ + + /* Return read end of the pipes to caller, and close write end */ + process_handle->stdout_pipe = stdout_pipe[0]; + retval = close(stdout_pipe[1]); + + if (-1 == retval) { + log_warn(LD_GENERAL, + "Failed to close write end of stdout pipe in parent process: %s", + strerror(errno)); + } + + process_handle->waitpid_cb = set_waitpid_callback(pid, + process_handle_waitpid_cb, + process_handle); + + process_handle->stderr_pipe = stderr_pipe[0]; + retval = close(stderr_pipe[1]); + + if (-1 == retval) { + log_warn(LD_GENERAL, + "Failed to close write end of stderr pipe in parent process: %s", + strerror(errno)); + } + + /* Return write end of the stdin pipe to caller, and close the read end */ + process_handle->stdin_pipe = stdin_pipe[1]; + retval = close(stdin_pipe[0]); + + if (-1 == retval) { + log_warn(LD_GENERAL, + "Failed to close read end of stdin pipe in parent process: %s", + strerror(errno)); + } + + status = process_handle->status = PROCESS_STATUS_RUNNING; + /* Set stdin/stdout/stderr pipes to be non-blocking */ + if (fcntl(process_handle->stdout_pipe, F_SETFL, O_NONBLOCK) < 0 || + fcntl(process_handle->stderr_pipe, F_SETFL, O_NONBLOCK) < 0 || + fcntl(process_handle->stdin_pipe, F_SETFL, O_NONBLOCK) < 0) { + log_warn(LD_GENERAL, "Failed to set stderror/stdout/stdin pipes " + "nonblocking in parent process: %s", strerror(errno)); + } + + *process_handle_out = process_handle; + return status; +#endif /* defined(_WIN32) */ +} + +/** Destroy all resources allocated by the process handle in + * <b>process_handle</b>. + * If <b>also_terminate_process</b> is true, also terminate the + * process of the process handle. */ +MOCK_IMPL(void, +tor_process_handle_destroy,(process_handle_t *process_handle, + int also_terminate_process)) +{ + if (!process_handle) + return; + + if (also_terminate_process) { + if (tor_terminate_process(process_handle) < 0) { + const char *errstr = +#ifdef _WIN32 + format_win32_error(GetLastError()); +#else + strerror(errno); +#endif + log_notice(LD_GENERAL, "Failed to terminate process with " + "PID '%d' ('%s').", tor_process_get_pid(process_handle), + errstr); + } else { + log_info(LD_GENERAL, "Terminated process with PID '%d'.", + tor_process_get_pid(process_handle)); + } + } + + process_handle->status = PROCESS_STATUS_NOTRUNNING; + +#ifdef _WIN32 + if (process_handle->stdout_pipe) + CloseHandle(process_handle->stdout_pipe); + + if (process_handle->stderr_pipe) + CloseHandle(process_handle->stderr_pipe); + + if (process_handle->stdin_pipe) + CloseHandle(process_handle->stdin_pipe); +#else /* !(defined(_WIN32)) */ + close(process_handle->stdout_pipe); + close(process_handle->stderr_pipe); + close(process_handle->stdin_pipe); + + clear_waitpid_callback(process_handle->waitpid_cb); +#endif /* defined(_WIN32) */ + + memset(process_handle, 0x0f, sizeof(process_handle_t)); + tor_free(process_handle); +} + +/** Get the exit code of a process specified by <b>process_handle</b> and store + * it in <b>exit_code</b>, if set to a non-NULL value. If <b>block</b> is set + * to true, the call will block until the process has exited. Otherwise if + * the process is still running, the function will return + * PROCESS_EXIT_RUNNING, and exit_code will be left unchanged. Returns + * PROCESS_EXIT_EXITED if the process did exit. If there is a failure, + * PROCESS_EXIT_ERROR will be returned and the contents of exit_code (if + * non-NULL) will be undefined. N.B. Under *nix operating systems, this will + * probably not work in Tor, because waitpid() is called in main.c to reap any + * terminated child processes.*/ +int +tor_get_exit_code(process_handle_t *process_handle, + int block, int *exit_code) +{ +#ifdef _WIN32 + DWORD retval; + BOOL success; + + if (block) { + /* Wait for the process to exit */ + retval = WaitForSingleObject(process_handle->pid.hProcess, INFINITE); + if (retval != WAIT_OBJECT_0) { + log_warn(LD_GENERAL, "WaitForSingleObject() failed (%d): %s", + (int)retval, format_win32_error(GetLastError())); + return PROCESS_EXIT_ERROR; + } + } else { + retval = WaitForSingleObject(process_handle->pid.hProcess, 0); + if (WAIT_TIMEOUT == retval) { + /* Process has not exited */ + return PROCESS_EXIT_RUNNING; + } else if (retval != WAIT_OBJECT_0) { + log_warn(LD_GENERAL, "WaitForSingleObject() failed (%d): %s", + (int)retval, format_win32_error(GetLastError())); + return PROCESS_EXIT_ERROR; + } + } + + if (exit_code != NULL) { + success = GetExitCodeProcess(process_handle->pid.hProcess, + (PDWORD)exit_code); + if (!success) { + log_warn(LD_GENERAL, "GetExitCodeProcess() failed: %s", + format_win32_error(GetLastError())); + return PROCESS_EXIT_ERROR; + } + } +#else /* !(defined(_WIN32)) */ + int stat_loc; + int retval; + + if (process_handle->waitpid_cb) { + /* We haven't processed a SIGCHLD yet. */ + retval = waitpid(process_handle->pid, &stat_loc, block?0:WNOHANG); + if (retval == process_handle->pid) { + clear_waitpid_callback(process_handle->waitpid_cb); + process_handle->waitpid_cb = NULL; + process_handle->waitpid_exit_status = stat_loc; + } + } else { + /* We already got a SIGCHLD for this process, and handled it. */ + retval = process_handle->pid; + stat_loc = process_handle->waitpid_exit_status; + } + + if (!block && 0 == retval) { + /* Process has not exited */ + return PROCESS_EXIT_RUNNING; + } else if (retval != process_handle->pid) { + log_warn(LD_GENERAL, "waitpid() failed for PID %d: %s", + (int)process_handle->pid, strerror(errno)); + return PROCESS_EXIT_ERROR; + } + + if (!WIFEXITED(stat_loc)) { + log_warn(LD_GENERAL, "Process %d did not exit normally", + (int)process_handle->pid); + return PROCESS_EXIT_ERROR; + } + + if (exit_code != NULL) + *exit_code = WEXITSTATUS(stat_loc); +#endif /* defined(_WIN32) */ + + return PROCESS_EXIT_EXITED; +} + +#ifdef _WIN32 +/** Read from a handle <b>h</b> into <b>buf</b>, up to <b>count</b> bytes. If + * <b>hProcess</b> is NULL, the function will return immediately if there is + * nothing more to read. Otherwise <b>hProcess</b> should be set to the handle + * to the process owning the <b>h</b>. In this case, the function will exit + * only once the process has exited, or <b>count</b> bytes are read. Returns + * the number of bytes read, or -1 on error. */ +ssize_t +tor_read_all_handle(HANDLE h, char *buf, size_t count, + const process_handle_t *process) +{ + size_t numread = 0; + BOOL retval; + DWORD byte_count; + BOOL process_exited = FALSE; + + if (count > SIZE_T_CEILING || count > SSIZE_MAX) + return -1; + + while (numread < count) { + /* Check if there is anything to read */ + retval = PeekNamedPipe(h, NULL, 0, NULL, &byte_count, NULL); + if (!retval) { + log_warn(LD_GENERAL, + "Failed to peek from handle: %s", + format_win32_error(GetLastError())); + return -1; + } else if (0 == byte_count) { + /* Nothing available: process exited or it is busy */ + + /* Exit if we don't know whether the process is running */ + if (NULL == process) + break; + + /* The process exited and there's nothing left to read from it */ + if (process_exited) + break; + + /* If process is not running, check for output one more time in case + it wrote something after the peek was performed. Otherwise keep on + waiting for output */ + tor_assert(process != NULL); + byte_count = WaitForSingleObject(process->pid.hProcess, 0); + if (WAIT_TIMEOUT != byte_count) + process_exited = TRUE; + + continue; + } + + /* There is data to read; read it */ + retval = ReadFile(h, buf+numread, count-numread, &byte_count, NULL); + tor_assert(byte_count + numread <= count); + if (!retval) { + log_warn(LD_GENERAL, "Failed to read from handle: %s", + format_win32_error(GetLastError())); + return -1; + } else if (0 == byte_count) { + /* End of file */ + break; + } + numread += byte_count; + } + return (ssize_t)numread; +} +#else /* !(defined(_WIN32)) */ +/** Read from a handle <b>fd</b> into <b>buf</b>, up to <b>count</b> bytes. If + * <b>process</b> is NULL, the function will return immediately if there is + * nothing more to read. Otherwise data will be read until end of file, or + * <b>count</b> bytes are read. Returns the number of bytes read, or -1 on + * error. Sets <b>eof</b> to true if <b>eof</b> is not NULL and the end of the + * file has been reached. */ +ssize_t +tor_read_all_handle(int fd, char *buf, size_t count, + const process_handle_t *process, + int *eof) +{ + size_t numread = 0; + ssize_t result; + + if (eof) + *eof = 0; + + if (count > SIZE_T_CEILING || count > SSIZE_MAX) + return -1; + + while (numread < count) { + result = read(fd, buf+numread, count-numread); + + if (result == 0) { + log_debug(LD_GENERAL, "read() reached end of file"); + if (eof) + *eof = 1; + break; + } else if (result < 0 && errno == EAGAIN) { + if (process) + continue; + else + break; + } else if (result < 0) { + log_warn(LD_GENERAL, "read() failed: %s", strerror(errno)); + return -1; + } + + numread += result; + } + + log_debug(LD_GENERAL, "read() read %d bytes from handle", (int)numread); + return (ssize_t)numread; +} +#endif /* defined(_WIN32) */ + +/** Read from stdout of a process until the process exits. */ +ssize_t +tor_read_all_from_process_stdout(const process_handle_t *process_handle, + char *buf, size_t count) +{ +#ifdef _WIN32 + return tor_read_all_handle(process_handle->stdout_pipe, buf, count, + process_handle); +#else + return tor_read_all_handle(process_handle->stdout_pipe, buf, count, + process_handle, NULL); +#endif /* defined(_WIN32) */ +} + +/** Read from stdout of a process until the process exits. */ +ssize_t +tor_read_all_from_process_stderr(const process_handle_t *process_handle, + char *buf, size_t count) +{ +#ifdef _WIN32 + return tor_read_all_handle(process_handle->stderr_pipe, buf, count, + process_handle); +#else + return tor_read_all_handle(process_handle->stderr_pipe, buf, count, + process_handle, NULL); +#endif /* defined(_WIN32) */ +} + +/** Return a string corresponding to <b>stream_status</b>. */ +const char * +stream_status_to_string(enum stream_status stream_status) +{ + switch (stream_status) { + case IO_STREAM_OKAY: + return "okay"; + case IO_STREAM_EAGAIN: + return "temporarily unavailable"; + case IO_STREAM_TERM: + return "terminated"; + case IO_STREAM_CLOSED: + return "closed"; + default: + tor_fragile_assert(); + return "unknown"; + } +} + +/** Split buf into lines, and add to smartlist. The buffer <b>buf</b> will be + * modified. The resulting smartlist will consist of pointers to buf, so there + * is no need to free the contents of sl. <b>buf</b> must be a NUL-terminated + * string. <b>len</b> should be set to the length of the buffer excluding the + * NUL. Non-printable characters (including NUL) will be replaced with "." */ +int +tor_split_lines(smartlist_t *sl, char *buf, int len) +{ + /* Index in buf of the start of the current line */ + int start = 0; + /* Index in buf of the current character being processed */ + int cur = 0; + /* Are we currently in a line */ + char in_line = 0; + + /* Loop over string */ + while (cur < len) { + /* Loop until end of line or end of string */ + for (; cur < len; cur++) { + if (in_line) { + if ('\r' == buf[cur] || '\n' == buf[cur]) { + /* End of line */ + buf[cur] = '\0'; + /* Point cur to the next line */ + cur++; + /* Line starts at start and ends with a nul */ + break; + } else { + if (!TOR_ISPRINT(buf[cur])) + buf[cur] = '.'; + } + } else { + if ('\r' == buf[cur] || '\n' == buf[cur]) { + /* Skip leading vertical space */ + ; + } else { + in_line = 1; + start = cur; + if (!TOR_ISPRINT(buf[cur])) + buf[cur] = '.'; + } + } + } + /* We are at the end of the line or end of string. If in_line is true there + * is a line which starts at buf+start and ends at a NUL. cur points to + * the character after the NUL. */ + if (in_line) + smartlist_add(sl, (void *)(buf+start)); + in_line = 0; + } + return smartlist_len(sl); +} + +#ifdef _WIN32 + +/** Return a smartlist containing lines outputted from + * <b>handle</b>. Return NULL on error, and set + * <b>stream_status_out</b> appropriately. */ +MOCK_IMPL(smartlist_t *, +tor_get_lines_from_handle, (HANDLE *handle, + enum stream_status *stream_status_out)) +{ + int pos; + char stdout_buf[600] = {0}; + smartlist_t *lines = NULL; + + tor_assert(stream_status_out); + + *stream_status_out = IO_STREAM_TERM; + + pos = tor_read_all_handle(handle, stdout_buf, sizeof(stdout_buf) - 1, NULL); + if (pos < 0) { + *stream_status_out = IO_STREAM_TERM; + return NULL; + } + if (pos == 0) { + *stream_status_out = IO_STREAM_EAGAIN; + return NULL; + } + + /* End with a null even if there isn't a \r\n at the end */ + /* TODO: What if this is a partial line? */ + stdout_buf[pos] = '\0'; + + /* Split up the buffer */ + lines = smartlist_new(); + tor_split_lines(lines, stdout_buf, pos); + + /* Currently 'lines' is populated with strings residing on the + stack. Replace them with their exact copies on the heap: */ + SMARTLIST_FOREACH(lines, char *, line, + SMARTLIST_REPLACE_CURRENT(lines, line, tor_strdup(line))); + + *stream_status_out = IO_STREAM_OKAY; + + return lines; +} + +#else /* !(defined(_WIN32)) */ + +/** Return a smartlist containing lines outputted from + * <b>fd</b>. Return NULL on error, and set + * <b>stream_status_out</b> appropriately. */ +MOCK_IMPL(smartlist_t *, +tor_get_lines_from_handle, (int fd, enum stream_status *stream_status_out)) +{ + enum stream_status stream_status; + char stdout_buf[400]; + smartlist_t *lines = NULL; + + while (1) { + memset(stdout_buf, 0, sizeof(stdout_buf)); + + stream_status = get_string_from_pipe(fd, + stdout_buf, sizeof(stdout_buf) - 1); + if (stream_status != IO_STREAM_OKAY) + goto done; + + if (!lines) lines = smartlist_new(); + smartlist_split_string(lines, stdout_buf, "\n", 0, 0); + } + + done: + *stream_status_out = stream_status; + return lines; +} + +#endif /* defined(_WIN32) */ + +/** Reads from <b>fd</b> and stores input in <b>buf_out</b> making + * sure it's below <b>count</b> bytes. + * If the string has a trailing newline, we strip it off. + * + * This function is specifically created to handle input from managed + * proxies, according to the pluggable transports spec. Make sure it + * fits your needs before using it. + * + * Returns: + * IO_STREAM_CLOSED: If the stream is closed. + * IO_STREAM_EAGAIN: If there is nothing to read and we should check back + * later. + * IO_STREAM_TERM: If something is wrong with the stream. + * IO_STREAM_OKAY: If everything went okay and we got a string + * in <b>buf_out</b>. */ +enum stream_status +get_string_from_pipe(int fd, char *buf_out, size_t count) +{ + ssize_t ret; + + tor_assert(count <= INT_MAX); + + ret = read(fd, buf_out, count); + + if (ret == 0) + return IO_STREAM_CLOSED; + else if (ret < 0 && errno == EAGAIN) + return IO_STREAM_EAGAIN; + else if (ret < 0) + return IO_STREAM_TERM; + + if (buf_out[ret - 1] == '\n') { + /* Remove the trailing newline */ + buf_out[ret - 1] = '\0'; + } else + buf_out[ret] = '\0'; + + return IO_STREAM_OKAY; +} diff --git a/src/lib/process/subprocess.h b/src/lib/process/subprocess.h new file mode 100644 index 0000000000..a319b3505c --- /dev/null +++ b/src/lib/process/subprocess.h @@ -0,0 +1,129 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#ifndef TOR_SUBPROCESS_H +#define TOR_SUBPROCESS_H + +#include "lib/cc/torint.h" +#include "lib/testsupport/testsupport.h" +#include <stddef.h> +#ifdef _WIN32 +#include <windows.h> +#endif + +struct smartlist_t; + +void tor_disable_spawning_background_processes(void); + +typedef struct process_handle_t process_handle_t; +struct process_environment_t; +int tor_spawn_background(const char *const filename, const char **argv, + struct process_environment_t *env, + process_handle_t **process_handle_out); + +#define SPAWN_ERROR_MESSAGE "ERR: Failed to spawn background process - code " + +/** Status of an I/O stream. */ +enum stream_status { + IO_STREAM_OKAY, + IO_STREAM_EAGAIN, + IO_STREAM_TERM, + IO_STREAM_CLOSED +}; + +const char *stream_status_to_string(enum stream_status stream_status); + +enum stream_status get_string_from_pipe(int fd, char *buf, size_t count); + +/* Values of process_handle_t.status. */ +#define PROCESS_STATUS_NOTRUNNING 0 +#define PROCESS_STATUS_RUNNING 1 +#define PROCESS_STATUS_ERROR -1 + +#ifdef SUBPROCESS_PRIVATE +struct waitpid_callback_t; + +/** Structure to represent the state of a process with which Tor is + * communicating. The contents of this structure are private to util.c */ +struct process_handle_t { + /** One of the PROCESS_STATUS_* values */ + int status; +#ifdef _WIN32 + HANDLE stdin_pipe; + HANDLE stdout_pipe; + HANDLE stderr_pipe; + PROCESS_INFORMATION pid; +#else /* !(defined(_WIN32)) */ + int stdin_pipe; + int stdout_pipe; + int stderr_pipe; + pid_t pid; + /** If the process has not given us a SIGCHLD yet, this has the + * waitpid_callback_t that gets invoked once it has. Otherwise this + * contains NULL. */ + struct waitpid_callback_t *waitpid_cb; + /** The exit status reported by waitpid. */ + int waitpid_exit_status; +#endif /* defined(_WIN32) */ +}; +#endif /* defined(SUBPROCESS_PRIVATE) */ + +/* Return values of tor_get_exit_code() */ +#define PROCESS_EXIT_RUNNING 1 +#define PROCESS_EXIT_EXITED 0 +#define PROCESS_EXIT_ERROR -1 +int tor_get_exit_code(process_handle_t *process_handle, + int block, int *exit_code); +int tor_split_lines(struct smartlist_t *sl, char *buf, int len); +#ifdef _WIN32 +ssize_t tor_read_all_handle(HANDLE h, char *buf, size_t count, + const process_handle_t *process); +#else +ssize_t tor_read_all_handle(int fd, char *buf, size_t count, + const process_handle_t *process, + int *eof); +#endif /* defined(_WIN32) */ +ssize_t tor_read_all_from_process_stdout( + const process_handle_t *process_handle, char *buf, size_t count); +ssize_t tor_read_all_from_process_stderr( + const process_handle_t *process_handle, char *buf, size_t count); +char *tor_join_win_cmdline(const char *argv[]); + +int tor_process_get_pid(process_handle_t *process_handle); +#ifdef _WIN32 +HANDLE tor_process_get_stdout_pipe(process_handle_t *process_handle); +#else +int tor_process_get_stdout_pipe(process_handle_t *process_handle); +#endif + +#ifdef _WIN32 +MOCK_DECL(struct smartlist_t *, tor_get_lines_from_handle,(HANDLE *handle, + enum stream_status *stream_status)); +#else +MOCK_DECL(struct smartlist_t *, tor_get_lines_from_handle,(int fd, + enum stream_status *stream_status)); +#endif /* defined(_WIN32) */ + +int tor_terminate_process(process_handle_t *process_handle); + +MOCK_DECL(void, tor_process_handle_destroy,(process_handle_t *process_handle, + int also_terminate_process)); + +#ifdef SUBPROCESS_PRIVATE +/* Prototypes for private functions only used by util.c (and unit tests) */ + +#ifndef _WIN32 +STATIC int format_helper_exit_status(unsigned char child_state, + int saved_errno, char *hex_errno); + +/* Space for hex values of child state, a slash, saved_errno (with + leading minus) and newline (no null) */ +#define HEX_ERRNO_SIZE (sizeof(char) * 2 + 1 + \ + 1 + sizeof(int) * 2 + 1) +#endif /* !defined(_WIN32) */ + +#endif /* defined(SUBPROCESS_PRIVATE) */ + +#endif diff --git a/src/lib/process/waitpid.c b/src/lib/process/waitpid.c new file mode 100644 index 0000000000..66c77b05f3 --- /dev/null +++ b/src/lib/process/waitpid.c @@ -0,0 +1,156 @@ +/* Copyright (c) 2003-2004, Roger Dingledine + * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson. + * Copyright (c) 2007-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file util_process.c + * \brief utility functions for launching processes and checking their + * status. These functions are kept separately from procmon so that they + * won't require linking against libevent. + **/ + +#include "orconfig.h" + +#ifndef _WIN32 + +#include "lib/process/waitpid.h" +#include "lib/log/torlog.h" +#include "lib/log/util_bug.h" +#include "lib/malloc/util_malloc.h" +#include "ht.h" + +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif + +#include <string.h> + +/* ================================================== */ +/* Convenience structures for handlers for waitpid(). + * + * The tor_process_monitor*() code above doesn't use them, since it is for + * monitoring a non-child process. + */ + +/** Mapping from a PID to a userfn/userdata pair. */ +struct waitpid_callback_t { + HT_ENTRY(waitpid_callback_t) node; + pid_t pid; + + void (*userfn)(int, void *userdata); + void *userdata; + + unsigned running; +}; + +static inline unsigned int +process_map_entry_hash_(const waitpid_callback_t *ent) +{ + return (unsigned) ent->pid; +} + +static inline unsigned int +process_map_entries_eq_(const waitpid_callback_t *a, + const waitpid_callback_t *b) +{ + return a->pid == b->pid; +} + +static HT_HEAD(process_map, waitpid_callback_t) process_map = HT_INITIALIZER(); + +HT_PROTOTYPE(process_map, waitpid_callback_t, node, process_map_entry_hash_, + process_map_entries_eq_) +HT_GENERATE2(process_map, waitpid_callback_t, node, process_map_entry_hash_, + process_map_entries_eq_, 0.6, tor_reallocarray_, tor_free_) + +/** + * Begin monitoring the child pid <b>pid</b> to see if we get a SIGCHLD for + * it. If we eventually do, call <b>fn</b>, passing it the exit status (as + * yielded by waitpid) and the pointer <b>arg</b>. + * + * To cancel this, or clean up after it has triggered, call + * clear_waitpid_callback(). + */ +waitpid_callback_t * +set_waitpid_callback(pid_t pid, void (*fn)(int, void *), void *arg) +{ + waitpid_callback_t *old_ent; + waitpid_callback_t *ent = tor_malloc_zero(sizeof(waitpid_callback_t)); + ent->pid = pid; + ent->userfn = fn; + ent->userdata = arg; + ent->running = 1; + + old_ent = HT_REPLACE(process_map, &process_map, ent); + if (old_ent) { + log_warn(LD_BUG, "Replaced a waitpid monitor on pid %u. That should be " + "impossible.", (unsigned) pid); + old_ent->running = 0; + } + + return ent; +} + +/** + * Cancel a waitpid_callback_t, or clean up after one has triggered. Releases + * all storage held by <b>ent</b>. + */ +void +clear_waitpid_callback(waitpid_callback_t *ent) +{ + waitpid_callback_t *old_ent; + if (ent == NULL) + return; + + if (ent->running) { + old_ent = HT_REMOVE(process_map, &process_map, ent); + if (old_ent != ent) { + log_warn(LD_BUG, "Couldn't remove waitpid monitor for pid %u.", + (unsigned) ent->pid); + return; + } + } + + tor_free(ent); +} + +/** Helper: find the callack for <b>pid</b>; if there is one, run it, + * reporting the exit status as <b>status</b>. */ +static void +notify_waitpid_callback_by_pid(pid_t pid, int status) +{ + waitpid_callback_t search, *ent; + + search.pid = pid; + ent = HT_REMOVE(process_map, &process_map, &search); + if (!ent || !ent->running) { + log_info(LD_GENERAL, "Child process %u has exited; no callback was " + "registered", (unsigned)pid); + return; + } + + log_info(LD_GENERAL, "Child process %u has exited; running callback.", + (unsigned)pid); + + ent->running = 0; + ent->userfn(status, ent->userdata); +} + +/** Use waitpid() to wait for all children that have exited, and invoke any + * callbacks registered for them. */ +void +notify_pending_waitpid_callbacks(void) +{ + /* I was going to call this function reap_zombie_children(), but + * that makes it sound way more exciting than it really is. */ + pid_t child; + int status = 0; + + while ((child = waitpid(-1, &status, WNOHANG)) > 0) { + notify_waitpid_callback_by_pid(child, status); + status = 0; /* should be needless */ + } +} + +#endif /* !defined(_WIN32) */ diff --git a/src/lib/process/waitpid.h b/src/lib/process/waitpid.h new file mode 100644 index 0000000000..85905da6bf --- /dev/null +++ b/src/lib/process/waitpid.h @@ -0,0 +1,29 @@ +/* Copyright (c) 2011-2018, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file waitpid.h + * \brief Headers for waitpid.c + **/ + +#ifndef TOR_WAITPID_H +#define TOR_WAITPID_H + +#ifndef _WIN32 +#ifdef HAVE_SYS_TYPES_H +#include <sys/types.h> +#endif + +/** A callback structure waiting for us to get a SIGCHLD informing us that a + * PID has been closed. Created by set_waitpid_callback. Cancelled or cleaned- + * up from clear_waitpid_callback(). Do not access outside of the main thread; + * do not access from inside a signal handler. */ +typedef struct waitpid_callback_t waitpid_callback_t; + +waitpid_callback_t *set_waitpid_callback(pid_t pid, + void (*fn)(int, void *), void *arg); +void clear_waitpid_callback(waitpid_callback_t *ent); +void notify_pending_waitpid_callbacks(void); +#endif /* !defined(_WIN32) */ + +#endif /* !defined(TOR_WAITPID_H) */ |