aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar philwo <philwo@google.com>2017-04-21 14:19:29 +0200
committerGravatar Vladimir Moskva <vladmos@google.com>2017-04-24 16:50:37 +0200
commit3e5edafa2a04a71cd3596e929e83222da725f3f9 (patch)
treea35cac6e5e05d3b6c3d471af99533936792318c4
parent15e1b9b140a3cd389bfcf68fc8c21100d888da06 (diff)
process-wrapper: Wait for all (grand)children before exiting.
This uses Linux's PR_SET_CHILD_SUBREAPER and FreeBSD's PROC_REAP_ACQUIRE features to become an init-like process for all (grand)children spawned by process-wrapper, which allows us to a) kill them reliably and then b) wait for them reliably. Before this change, we only killed the main child, waited for it, then fired off a kill -9 on the process group, without waiting for it. This led to a race condition where Bazel would try to use or delete files that were still helt open by children of the main child and thus to bugs like #2371. This means we now have reliable process management on Linux, FreeBSD and Windows. Unfortunately I couldn't find any feature like this on macOS, so this is the only OS that will still have this race condition. PiperOrigin-RevId: 153817210
-rw-r--r--src/main/tools/BUILD31
-rw-r--r--src/main/tools/linux-sandbox-options.cc16
-rw-r--r--src/main/tools/linux-sandbox-pid1.cc211
-rw-r--r--src/main/tools/linux-sandbox.cc163
-rw-r--r--src/main/tools/process-tools.c151
-rw-r--r--src/main/tools/process-tools.cc280
-rw-r--r--src/main/tools/process-tools.h91
-rw-r--r--src/main/tools/process-wrapper.c169
-rw-r--r--src/main/tools/process-wrapper.cc186
-rwxr-xr-xsrc/test/shell/bazel/process-wrapper_test.sh2
10 files changed, 659 insertions, 641 deletions
diff --git a/src/main/tools/BUILD b/src/main/tools/BUILD
index 3ddad54f9c..1a38423d7c 100644
--- a/src/main/tools/BUILD
+++ b/src/main/tools/BUILD
@@ -1,20 +1,28 @@
package(default_visibility = ["//src:__subpackages__"])
+cc_library(
+ name = "process-tools",
+ srcs = [
+ "process-tools.cc",
+ "process-tools.h",
+ ],
+)
+
cc_binary(
name = "process-wrapper",
srcs = select({
"//src:windows_msvc": ["process-wrapper-windows.cc"],
"//conditions:default": [
- "process-tools.c",
- "process-tools.h",
- "process-wrapper.c",
+ "process-wrapper.cc",
],
}),
- copts = select({
+ linkopts = ["-lm"],
+ deps = select({
"//src:windows_msvc": [],
- "//conditions:default": ["-std=c99"],
+ "//conditions:default": [
+ ":process-tools",
+ ],
}),
- linkopts = ["-lm"],
)
cc_binary(
@@ -45,6 +53,17 @@ cc_binary(
],
}),
linkopts = ["-lm"],
+ deps = select({
+ "//src:darwin": [],
+ "//src:darwin_x86_64": [],
+ "//src:freebsd": [],
+ "//src:windows": [],
+ "//src:windows_msys": [],
+ "//src:windows_msvc": [],
+ "//conditions:default": [
+ ":process-tools",
+ ],
+ }),
)
filegroup(
diff --git a/src/main/tools/linux-sandbox-options.cc b/src/main/tools/linux-sandbox-options.cc
index cbb8ea8d19..0d56f7ce62 100644
--- a/src/main/tools/linux-sandbox-options.cc
+++ b/src/main/tools/linux-sandbox-options.cc
@@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-#include "linux-sandbox-options.h"
-#include "linux-sandbox-utils.h"
+#include "src/main/tools/linux-sandbox-options.h"
#define DIE(args...) \
{ \
fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \
fprintf(stderr, "\": "); \
- perror(NULL); \
+ perror(nullptr); \
exit(EXIT_FAILURE); \
}
@@ -39,6 +38,8 @@
#include <string>
#include <vector>
+#include "src/main/tools/linux-sandbox-utils.h"
+
using std::ifstream;
using std::unique_ptr;
using std::vector;
@@ -199,6 +200,7 @@ static void ParseCommandLine(unique_ptr<vector<char *>> args) {
if (optind < static_cast<int>(args->size())) {
if (opt.args.empty()) {
opt.args.assign(args->begin() + optind, args->end());
+ opt.args.push_back(nullptr);
} else {
Usage(args->front(), "Merging commands not supported.");
}
@@ -207,8 +209,8 @@ static void ParseCommandLine(unique_ptr<vector<char *>> args) {
// Expands a single argument, expanding options @filename to read in the content
// of the file and add it to the list of processed arguments.
-unique_ptr<vector<char *>> ExpandArgument(unique_ptr<vector<char *>> expanded,
- char *arg) {
+static unique_ptr<vector<char *>> ExpandArgument(
+ unique_ptr<vector<char *>> expanded, char *arg) {
if (arg[0] == '@') {
const char *filename = arg + 1; // strip off the '@'.
ifstream f(filename);
@@ -236,7 +238,7 @@ unique_ptr<vector<char *>> ExpandArgument(unique_ptr<vector<char *>> expanded,
// Pre-processes an argument list, expanding options @filename to read in the
// content of the file and add it to the list of arguments. Stops expanding
// arguments once it encounters "--".
-unique_ptr<vector<char *>> ExpandArguments(const vector<char *> &args) {
+static unique_ptr<vector<char *>> ExpandArguments(const vector<char *> &args) {
unique_ptr<vector<char *>> expanded(new vector<char *>());
expanded->reserve(args.size());
for (auto arg = args.begin(); arg != args.end(); ++arg) {
@@ -260,6 +262,6 @@ void ParseOptions(int argc, char *argv[]) {
}
if (opt.working_dir.empty()) {
- opt.working_dir = getcwd(NULL, 0);
+ opt.working_dir = getcwd(nullptr, 0);
}
}
diff --git a/src/main/tools/linux-sandbox-pid1.cc b/src/main/tools/linux-sandbox-pid1.cc
index 0095d7262c..4a366f2a35 100644
--- a/src/main/tools/linux-sandbox-pid1.cc
+++ b/src/main/tools/linux-sandbox-pid1.cc
@@ -17,21 +17,6 @@
* mount, UTS, IPC and PID namespace.
*/
-#include "linux-sandbox-options.h"
-#include "linux-sandbox-utils.h"
-#include "linux-sandbox.h"
-
-// Note that we define DIE() here and not in a shared header, because we want to
-// use _exit() in the
-// pid1 child, but exit() in the parent.
-#define DIE(args...) \
- { \
- fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \
- fprintf(stderr, "\": "); \
- perror(NULL); \
- _exit(EXIT_FAILURE); \
- }
-
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
@@ -56,6 +41,11 @@
#include <string>
+#include "src/main/tools/linux-sandbox-options.h"
+#include "src/main/tools/linux-sandbox-utils.h"
+#include "src/main/tools/linux-sandbox.h"
+#include "src/main/tools/process-tools.h"
+
static int global_child_pid;
static void SetupSelfDestruction(int *sync_pipe) {
@@ -84,31 +74,11 @@ static void SetupSelfDestruction(int *sync_pipe) {
static void SetupMountNamespace() {
// Fully isolate our mount namespace private from outside events, so that
// mounts in the outside environment do not affect our sandbox.
- if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) {
+ if (mount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE, nullptr) < 0) {
DIE("mount");
}
}
-static void WriteFile(const std::string &filename, const char *fmt, ...) {
- FILE *stream = fopen(filename.c_str(), "w");
- if (stream == NULL) {
- DIE("fopen(%s)", filename.c_str());
- }
-
- va_list ap;
- va_start(ap, fmt);
- int r = vfprintf(stream, fmt, ap);
- va_end(ap);
-
- if (r < 0) {
- DIE("vfprintf");
- }
-
- if (fclose(stream) != 0) {
- DIE("fclose(%s)", filename.c_str());
- }
-}
-
static void SetupUserNamespace() {
// Disable needs for CAP_SETGID.
struct stat sb;
@@ -130,7 +100,7 @@ static void SetupUserNamespace() {
} else if (opt.fake_username) {
// Change our username to 'nobody'.
struct passwd *pwd = getpwnam("nobody");
- if (pwd == NULL) {
+ if (pwd == nullptr) {
DIE("unable to find passwd entry for user nobody")
}
@@ -160,8 +130,8 @@ static void MountFilesystems() {
for (const std::string &tmpfs_dir : opt.tmpfs_dirs) {
PRINT_DEBUG("tmpfs: %s", tmpfs_dir.c_str());
if (mount("tmpfs", tmpfs_dir.c_str(), "tmpfs",
- MS_NOSUID | MS_NODEV | MS_NOATIME, NULL) < 0) {
- DIE("mount(tmpfs, %s, tmpfs, MS_NOSUID | MS_NODEV | MS_NOATIME, NULL)",
+ MS_NOSUID | MS_NODEV | MS_NOATIME, nullptr) < 0) {
+ DIE("mount(tmpfs, %s, tmpfs, MS_NOSUID | MS_NODEV | MS_NOATIME, nullptr)",
tmpfs_dir.c_str());
}
}
@@ -170,9 +140,9 @@ static void MountFilesystems() {
// do this is by bind-mounting it upon itself.
PRINT_DEBUG("working dir: %s", opt.working_dir.c_str());
- if (mount(opt.working_dir.c_str(), opt.working_dir.c_str(), NULL, MS_BIND,
- NULL) < 0) {
- DIE("mount(%s, %s, NULL, MS_BIND, NULL)", opt.working_dir.c_str(),
+ if (mount(opt.working_dir.c_str(), opt.working_dir.c_str(), nullptr, MS_BIND,
+ nullptr) < 0) {
+ DIE("mount(%s, %s, nullptr, MS_BIND, nullptr)", opt.working_dir.c_str(),
opt.working_dir.c_str());
}
@@ -180,16 +150,17 @@ static void MountFilesystems() {
std::string source = opt.bind_mount_sources.at(i);
std::string target = opt.bind_mount_targets.at(i);
PRINT_DEBUG("bind mount: %s -> %s", source.c_str(), target.c_str());
- if (mount(source.c_str(), target.c_str(), NULL, MS_BIND, NULL) < 0) {
- DIE("mount(%s, %s, NULL, MS_BIND, NULL)", source.c_str(), target.c_str());
+ if (mount(source.c_str(), target.c_str(), nullptr, MS_BIND, nullptr) < 0) {
+ DIE("mount(%s, %s, nullptr, MS_BIND, nullptr)", source.c_str(),
+ target.c_str());
}
}
for (const std::string &writable_file : opt.writable_files) {
PRINT_DEBUG("writable: %s", writable_file.c_str());
- if (mount(writable_file.c_str(), writable_file.c_str(), NULL, MS_BIND,
- NULL) < 0) {
- DIE("mount(%s, %s, NULL, MS_BIND, NULL)", writable_file.c_str(),
+ if (mount(writable_file.c_str(), writable_file.c_str(), nullptr, MS_BIND,
+ nullptr) < 0) {
+ DIE("mount(%s, %s, nullptr, MS_BIND, nullptr)", writable_file.c_str(),
writable_file.c_str());
}
}
@@ -221,34 +192,34 @@ static bool ShouldBeWritable(const std::string &mnt_dir) {
// ShouldBeWritable returns true.
static void MakeFilesystemMostlyReadOnly() {
FILE *mounts = setmntent("/proc/self/mounts", "r");
- if (mounts == NULL) {
+ if (mounts == nullptr) {
DIE("setmntent");
}
struct mntent *ent;
- while ((ent = getmntent(mounts)) != NULL) {
+ while ((ent = getmntent(mounts)) != nullptr) {
int mountFlags = MS_BIND | MS_REMOUNT;
// MS_REMOUNT does not allow us to change certain flags. This means, we have
// to first read them out and then pass them in back again. There seems to
// be no better way than this (an API for just getting the mount flags of a
// mount entry as a bitmask would be great).
- if (hasmntopt(ent, "nodev") != NULL) {
+ if (hasmntopt(ent, "nodev") != nullptr) {
mountFlags |= MS_NODEV;
}
- if (hasmntopt(ent, "noexec") != NULL) {
+ if (hasmntopt(ent, "noexec") != nullptr) {
mountFlags |= MS_NOEXEC;
}
- if (hasmntopt(ent, "nosuid") != NULL) {
+ if (hasmntopt(ent, "nosuid") != nullptr) {
mountFlags |= MS_NOSUID;
}
- if (hasmntopt(ent, "noatime") != NULL) {
+ if (hasmntopt(ent, "noatime") != nullptr) {
mountFlags |= MS_NOATIME;
}
- if (hasmntopt(ent, "nodiratime") != NULL) {
+ if (hasmntopt(ent, "nodiratime") != nullptr) {
mountFlags |= MS_NODIRATIME;
}
- if (hasmntopt(ent, "relatime") != NULL) {
+ if (hasmntopt(ent, "relatime") != nullptr) {
mountFlags |= MS_RELATIME;
}
@@ -258,7 +229,7 @@ static void MakeFilesystemMostlyReadOnly() {
PRINT_DEBUG("remount %s: %s", (mountFlags & MS_RDONLY) ? "ro" : "rw",
ent->mnt_dir);
- if (mount(NULL, ent->mnt_dir, NULL, mountFlags, NULL) < 0) {
+ if (mount(nullptr, ent->mnt_dir, nullptr, mountFlags, nullptr) < 0) {
// If we get EACCES or EPERM, this might be a mount-point for which we
// don't have read access. Not much we can do about this, but it also
// won't do any harm, so let's go on. The same goes for EINVAL or ENOENT,
@@ -272,7 +243,8 @@ static void MakeFilesystemMostlyReadOnly() {
// should just ignore it.
if (errno != EACCES && errno != EPERM && errno != EINVAL &&
errno != ENOENT && errno != ESTALE) {
- DIE("remount(NULL, %s, NULL, %d, NULL)", ent->mnt_dir, mountFlags);
+ DIE("remount(nullptr, %s, nullptr, %d, nullptr)", ent->mnt_dir,
+ mountFlags);
}
}
}
@@ -283,8 +255,8 @@ static void MakeFilesystemMostlyReadOnly() {
static void MountProc() {
// Mount a new proc on top of the old one, because the old one still refers to
// our parent PID namespace.
- if (mount("/proc", "/proc", "proc", MS_NODEV | MS_NOEXEC | MS_NOSUID, NULL) <
- 0) {
+ if (mount("/proc", "/proc", "proc", MS_NODEV | MS_NOEXEC | MS_NOSUID,
+ nullptr) < 0) {
DIE("mount");
}
}
@@ -326,57 +298,6 @@ static void EnterSandbox() {
}
}
-static void InstallSignalHandler(int signum, void (*handler)(int)) {
- struct sigaction sa;
- memset(&sa, 0, sizeof(sa));
- sa.sa_handler = handler;
- if (handler == SIG_IGN || handler == SIG_DFL) {
- // No point in blocking signals when using the default handler or ignoring
- // the signal.
- if (sigemptyset(&sa.sa_mask) < 0) {
- DIE("sigemptyset");
- }
- } else {
- // When using a custom handler, block all signals from firing while the
- // handler is running.
- if (sigfillset(&sa.sa_mask) < 0) {
- DIE("sigfillset");
- }
- }
- // sigaction may fail for certain reserved signals. Ignore failure in this
- // case, but report it in debug mode, just in case.
- if (sigaction(signum, &sa, NULL) < 0) {
- PRINT_DEBUG("sigaction(%d, &sa, NULL) failed", signum);
- }
-}
-
-static void IgnoreSignal(int signum) { InstallSignalHandler(signum, SIG_IGN); }
-
-// Reset the signal mask and restore the default handler for all signals.
-static void RestoreSignalHandlersAndMask() {
- // Use an empty signal mask for the process (= unblock all signals).
- sigset_t empty_set;
- if (sigemptyset(&empty_set) < 0) {
- DIE("sigemptyset");
- }
- if (sigprocmask(SIG_SETMASK, &empty_set, nullptr) < 0) {
- DIE("sigprocmask(SIG_SETMASK, <empty set>, nullptr)");
- }
-
- // Set the default signal handler for all signals.
- struct sigaction sa;
- memset(&sa, 0, sizeof(sa));
- if (sigemptyset(&sa.sa_mask) < 0) {
- DIE("sigemptyset");
- }
- sa.sa_handler = SIG_DFL;
- for (int i = 1; i < NSIG; ++i) {
- // Ignore possible errors, because we might not be allowed to set the
- // handler for certain signals, but we still want to try.
- sigaction(i, &sa, nullptr);
- }
-}
-
static void ForwardSignal(int signum) {
PRINT_DEBUG("ForwardSignal(%d)", signum);
kill(-global_child_pid, signum);
@@ -419,71 +340,6 @@ static void SetupSignalHandlers() {
}
}
-static void SpawnChild() {
- global_child_pid = fork();
-
- if (global_child_pid < 0) {
- DIE("fork()");
- } else if (global_child_pid == 0) {
- // Put the child into its own process group.
- if (setpgid(0, 0) < 0) {
- DIE("setpgid");
- }
-
- // Try to assign our terminal to the child process.
- if (tcsetpgrp(STDIN_FILENO, getpgrp()) < 0 && errno != ENOTTY) {
- DIE("tcsetpgrp")
- }
-
- // Unblock all signals, restore default handlers.
- RestoreSignalHandlersAndMask();
-
- // Force umask to include read and execute for everyone, to make output
- // permissions predictable.
- umask(022);
-
- // argv[] passed to execve() must be a null-terminated array.
- opt.args.push_back(nullptr);
-
- if (execvp(opt.args[0], opt.args.data()) < 0) {
- DIE("execvp(%s, %p)", opt.args[0], opt.args.data());
- }
- }
-}
-
-static void WaitForChild() {
- while (1) {
- // Check for zombies to be reaped and exit, if our own child exited.
- int status;
- pid_t killed_pid = waitpid(-1, &status, 0);
- PRINT_DEBUG("waitpid returned %d", killed_pid);
-
- if (killed_pid < 0) {
- // Our PID1 process got a signal that interrupted the waitpid() call and
- // that was either ignored or forwared to the child. This is expected &
- // fine, just continue waiting.
- if (errno == EINTR) {
- continue;
- }
- DIE("waitpid")
- } else {
- if (killed_pid == global_child_pid) {
- // If the child process we spawned earlier terminated, we'll also
- // terminate. We can simply _exit() here, because the Linux kernel will
- // kindly SIGKILL all remaining processes in our PID namespace once we
- // exit.
- if (WIFSIGNALED(status)) {
- PRINT_DEBUG("child died due to signal %d", WTERMSIG(status));
- _exit(128 + WTERMSIG(status));
- } else {
- PRINT_DEBUG("child exited with code %d", WEXITSTATUS(status));
- _exit(WEXITSTATUS(status));
- }
- }
- }
- }
-}
-
int Pid1Main(void *sync_pipe_param) {
if (getpid() != 1) {
DIE("Using PID namespaces, but we are not PID 1");
@@ -501,7 +357,6 @@ int Pid1Main(void *sync_pipe_param) {
SetupNetworking();
EnterSandbox();
SetupSignalHandlers();
- SpawnChild();
- WaitForChild();
- _exit(EXIT_FAILURE);
+ global_child_pid = SpawnCommand(opt.args);
+ return WaitForChild(global_child_pid);
}
diff --git a/src/main/tools/linux-sandbox.cc b/src/main/tools/linux-sandbox.cc
index 1f078026d7..129454a666 100644
--- a/src/main/tools/linux-sandbox.cc
+++ b/src/main/tools/linux-sandbox.cc
@@ -37,18 +37,6 @@
* system are invisible.
*/
-#include "linux-sandbox-options.h"
-#include "linux-sandbox-pid1.h"
-#include "linux-sandbox-utils.h"
-
-#define DIE(args...) \
- { \
- fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" args); \
- fprintf(stderr, "\": "); \
- perror(NULL); \
- exit(EXIT_FAILURE); \
- }
-
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
@@ -70,20 +58,26 @@
#include <string>
#include <vector>
+#include "src/main/tools/linux-sandbox-options.h"
+#include "src/main/tools/linux-sandbox-pid1.h"
+#include "src/main/tools/linux-sandbox-utils.h"
+#include "src/main/tools/process-tools.h"
+
int global_outer_uid;
int global_outer_gid;
-static int global_child_pid;
+// The PID of our child.
+static volatile sig_atomic_t global_child_pid;
// The signal that will be sent to the child when a timeout occurs.
static volatile sig_atomic_t global_next_timeout_signal = SIGTERM;
-// The signal that caused us to kill the child (e.g. on timeout).
-static volatile sig_atomic_t global_signal;
+// Whether the child was killed due to a timeout.
+static volatile sig_atomic_t global_timeout_occurred;
static void CloseFds() {
DIR *fds = opendir("/proc/self/fd");
- if (fds == NULL) {
+ if (fds == nullptr) {
DIE("opendir");
}
@@ -91,7 +85,7 @@ static void CloseFds() {
errno = 0;
struct dirent *dent = readdir(fds);
- if (dent == NULL) {
+ if (dent == nullptr) {
if (errno != 0) {
DIE("readdir");
}
@@ -118,28 +112,67 @@ static void CloseFds() {
}
}
-static void HandleSignal(int signum, void (*handler)(int)) {
- struct sigaction sa;
- memset(&sa, 0, sizeof(sa));
- sa.sa_handler = handler;
- if (sigemptyset(&sa.sa_mask) < 0) {
- DIE("sigemptyset");
- }
- if (sigaction(signum, &sa, NULL) < 0) {
- DIE("sigaction");
- }
-}
-
static void OnTimeout(int sig) {
- global_signal = sig;
+ global_timeout_occurred = true;
kill(global_child_pid, global_next_timeout_signal);
if (global_next_timeout_signal == SIGTERM && opt.kill_delay_secs > 0) {
global_next_timeout_signal = SIGKILL;
- alarm(opt.kill_delay_secs);
+ SetTimeout(opt.kill_delay_secs);
+ }
+}
+
+static void ForwardSignal(int signum) {
+ if (global_child_pid > 0) {
+ kill(global_child_pid, signum);
+ }
+}
+
+static void SetupSignalHandlers() {
+ RestoreSignalHandlersAndMask();
+
+ for (int signum = 1; signum < NSIG; signum++) {
+ switch (signum) {
+ // Some signals should indeed kill us and not be forwarded to the child,
+ // thus we can use the default handler.
+ case SIGABRT:
+ case SIGBUS:
+ case SIGFPE:
+ case SIGILL:
+ case SIGSEGV:
+ case SIGSYS:
+ case SIGTRAP:
+ break;
+ // It's fine to use the default handler for SIGCHLD, because we use
+ // waitpid() in the main loop to wait for our child to die anyway.
+ case SIGCHLD:
+ break;
+ // One does not simply install a signal handler for these two signals
+ case SIGKILL:
+ case SIGSTOP:
+ break;
+ // Ignore SIGTTIN and SIGTTOU, as we hand off the terminal to the child in
+ // SpawnChild() later.
+ case SIGTTIN:
+ case SIGTTOU:
+ IgnoreSignal(signum);
+ break;
+ // We need a special signal handler for this if we use a timeout.
+ case SIGALRM:
+ if (opt.timeout_secs > 0) {
+ InstallSignalHandler(signum, OnTimeout);
+ } else {
+ InstallSignalHandler(signum, ForwardSignal);
+ }
+ break;
+ // All other signals should be forwarded to the child.
+ default:
+ InstallSignalHandler(signum, ForwardSignal);
+ break;
+ }
}
}
-static void SpawnPid1() {
+static int SpawnPid1() {
const int kStackSize = 1024 * 1024;
std::vector<char> child_stack(kStackSize);
@@ -160,13 +193,13 @@ static void SpawnPid1() {
// We use clone instead of unshare, because unshare sometimes fails with
// EINVAL due to a race condition in the Linux kernel (see
// https://lkml.org/lkml/2015/7/28/833).
- global_child_pid =
+ int child_pid =
clone(Pid1Main, child_stack.data() + kStackSize, clone_flags, sync_pipe);
- if (global_child_pid < 0) {
+ if (child_pid < 0) {
DIE("clone");
}
- PRINT_DEBUG("linux-sandbox-pid1 has PID %d", global_child_pid);
+ PRINT_DEBUG("linux-sandbox-pid1 has PID %d", child_pid);
// We close the write end of the sync pipe, read a byte and then close the
// pipe. This proves to the linux-sandbox-pid1 process that we still existed
@@ -182,25 +215,26 @@ static void SpawnPid1() {
if (close(sync_pipe[0]) < 0) {
DIE("close");
}
+
+ return child_pid;
}
-static int WaitForPid1() {
+static int WaitForPid1(int child_pid) {
int err, status;
do {
- err = waitpid(global_child_pid, &status, 0);
+ err = waitpid(child_pid, &status, 0);
} while (err < 0 && errno == EINTR);
if (err < 0) {
DIE("waitpid");
}
- if (global_signal > 0) {
+ if (global_timeout_occurred) {
// The child exited because we killed it due to receiving a signal
// ourselves. Do not trust the exitcode in this case, just calculate it from
// the signal.
- PRINT_DEBUG("child exited due to us catching signal: %s",
- strsignal(global_signal));
- return 128 + global_signal;
+ PRINT_DEBUG("child exited due to timeout");
+ return 128 + SIGALRM;
} else if (WIFSIGNALED(status)) {
PRINT_DEBUG("child exited due to receiving signal: %s",
strsignal(WTERMSIG(status)));
@@ -211,48 +245,14 @@ static int WaitForPid1() {
}
}
-static void Redirect(const std::string &target_path, int fd) {
- if (!target_path.empty() && target_path != "-") {
- const int flags = O_WRONLY | O_CREAT | O_TRUNC | O_APPEND;
- int fd_out = open(target_path.c_str(), flags, 0666);
- if (fd_out < 0) {
- DIE("open(%s)", target_path.c_str());
- }
- // If we were launched with less than 3 fds (stdin, stdout, stderr) open,
- // but redirection is still requested via a command-line flag, something is
- // wacky and the following code would not do what we intend to do, so let's
- // bail.
- if (fd_out < 3) {
- DIE("open(%s) returned a handle that is reserved for stdin / stdout / "
- "stderr",
- target_path.c_str());
- }
- if (dup2(fd_out, fd) < 0) {
- DIE("dup2()");
- }
- if (close(fd_out) < 0) {
- DIE("close()");
- }
- }
-}
-
int main(int argc, char *argv[]) {
- // Ask the kernel to kill us with SIGKILL if our parent dies.
- if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) {
- DIE("prctl");
- }
-
+ KillMeWhenMyParentDies(SIGKILL);
+ DropPrivileges();
ParseOptions(argc, argv);
Redirect(opt.stdout_path, STDOUT_FILENO);
Redirect(opt.stderr_path, STDERR_FILENO);
- // This should never be called as a setuid binary, drop privileges just in
- // case. We don't need to be root, because we use user namespaces anyway.
- if (setuid(getuid()) < 0) {
- DIE("setuid");
- }
-
global_outer_uid = getuid();
global_outer_gid = getgid();
@@ -260,11 +260,12 @@ int main(int argc, char *argv[]) {
// file handles from our parent.
CloseFds();
- HandleSignal(SIGALRM, OnTimeout);
+ SetupSignalHandlers();
+ global_child_pid = SpawnPid1();
+
if (opt.timeout_secs > 0) {
- alarm(opt.timeout_secs);
+ SetTimeout(opt.timeout_secs);
}
- SpawnPid1();
- return WaitForPid1();
+ return WaitForPid1(global_child_pid);
}
diff --git a/src/main/tools/process-tools.c b/src/main/tools/process-tools.c
deleted file mode 100644
index 9dddfee5f5..0000000000
--- a/src/main/tools/process-tools.c
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright 2015 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#define _GNU_SOURCE
-
-#include <unistd.h>
-#include <sys/stat.h>
-#include <sys/time.h>
-#include <sys/types.h>
-#include <sys/wait.h>
-#include <errno.h>
-#include <signal.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <fcntl.h>
-
-#include "process-tools.h"
-
-int SwitchToEuid() {
- int uid = getuid();
- int euid = geteuid();
- if (uid != euid) {
- CHECK_CALL(setreuid(euid, euid));
- }
- return euid;
-}
-
-int SwitchToEgid() {
- int gid = getgid();
- int egid = getegid();
- if (gid != egid) {
- CHECK_CALL(setregid(egid, egid));
- }
- return egid;
-}
-
-void Redirect(const char *target_path, int fd, const char *name) {
- if (target_path != NULL && strcmp(target_path, "-") != 0) {
- int fd_out;
- const int flags = O_WRONLY | O_CREAT | O_TRUNC | O_APPEND;
- CHECK_CALL(fd_out = open(target_path, flags, 0666));
- CHECK_CALL(dup2(fd_out, fd));
- CHECK_CALL(close(fd_out));
- }
-}
-
-void RedirectStdout(const char *stdout_path) {
- Redirect(stdout_path, STDOUT_FILENO, "stdout");
-}
-
-void RedirectStderr(const char *stderr_path) {
- Redirect(stderr_path, STDERR_FILENO, "stderr");
-}
-
-void KillEverything(int pgrp, bool gracefully, double graceful_kill_delay) {
- if (gracefully) {
- kill(-pgrp, SIGTERM);
-
- // Round up fractional seconds in this polling implementation.
- int kill_delay = (int)(ceil(graceful_kill_delay));
-
- // If the process is still alive, give it some time to die gracefully.
- while (kill_delay-- > 0 && kill(-pgrp, 0) == 0) {
- sleep(1);
- }
- }
-
- kill(-pgrp, SIGKILL);
-}
-
-void HandleSignal(int sig, void (*handler)(int)) {
- struct sigaction sa = {.sa_handler = handler};
- CHECK_CALL(sigemptyset(&sa.sa_mask));
- CHECK_CALL(sigaction(sig, &sa, NULL));
-}
-
-void UnHandle(int sig) {
- switch (sig) {
- case SIGSTOP:
- case SIGKILL:
- // These signals can't be handled, so they'll always have a valid default
- // handler. In fact, even trying to install SIG_DFL again will result in
- // EINVAL, so we'll just not do anything for these.
- return;
- default:
- HandleSignal(sig, SIG_DFL);
- }
-}
-
-void ClearSignalMask() {
- // Use an empty signal mask for the process.
- sigset_t empty_sset;
- CHECK_CALL(sigemptyset(&empty_sset));
- CHECK_CALL(sigprocmask(SIG_SETMASK, &empty_sset, NULL));
-
- // Set the default signal handler for all signals.
- for (int i = 1; i < NSIG; ++i) {
- if (i == SIGKILL || i == SIGSTOP) {
- continue;
- }
- struct sigaction sa = {.sa_handler = SIG_DFL};
- CHECK_CALL(sigemptyset(&sa.sa_mask));
- // Ignore possible errors, because we might not be allowed to set the
- // handler for certain signals, but we still want to try.
- sigaction(i, &sa, NULL);
- }
-}
-
-void SetTimeout(double timeout_secs) {
- if (timeout_secs <= 0) {
- return;
- }
-
- double int_val, fraction_val;
- fraction_val = modf(timeout_secs, &int_val);
-
- struct itimerval timer;
- timer.it_interval.tv_sec = 0;
- timer.it_interval.tv_usec = 0;
- timer.it_value.tv_sec = (long)int_val,
- timer.it_value.tv_usec = (long)(fraction_val * 1e6);
-
- CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL));
-}
-
-int WaitChild(pid_t pid, const char *name) {
- int err, status;
-
- do {
- err = waitpid(pid, &status, 0);
- } while (err == -1 && errno == EINTR);
-
- if (err == -1) {
- DIE("wait on %s (pid %d) failed\n", name, pid);
- }
-
- return status;
-}
diff --git a/src/main/tools/process-tools.cc b/src/main/tools/process-tools.cc
new file mode 100644
index 0000000000..7dfb6a7e42
--- /dev/null
+++ b/src/main/tools/process-tools.cc
@@ -0,0 +1,280 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/main/tools/process-tools.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#if defined(__linux__)
+#include <sys/prctl.h>
+#endif
+#if defined(__FreeBSD__)
+#include <sys/procctl.h>
+#endif
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+using std::vector;
+
+// Drops privileges irrevocably to the real uid / gid by setting the effective
+// and saved uid / gid to the real uid / gid. Useful if we happen to have been
+// called as a setuid-/setgid-root binary.
+void DropPrivileges() {
+ if (setgid(getgid()) < 0) {
+ DIE("setgid");
+ }
+ if (setuid(getuid()) < 0) {
+ DIE("setuid");
+ }
+}
+
+void Redirect(const std::string &target_path, int fd) {
+ if (!target_path.empty() && target_path != "-") {
+ const int flags = O_WRONLY | O_CREAT | O_TRUNC | O_APPEND;
+ int fd_out = open(target_path.c_str(), flags, 0666);
+ if (fd_out < 0) {
+ DIE("open(%s)", target_path.c_str());
+ }
+ // If we were launched with less than 3 fds (stdin, stdout, stderr) open,
+ // but redirection is still requested via a command-line flag, something is
+ // wacky and the following code would not do what we intend to do, so let's
+ // bail.
+ if (fd_out < 3) {
+ DIE("open(%s) returned a handle that is reserved for stdin / stdout / "
+ "stderr",
+ target_path.c_str());
+ }
+ if (dup2(fd_out, fd) < 0) {
+ DIE("dup2()");
+ }
+ if (close(fd_out) < 0) {
+ DIE("close()");
+ }
+ }
+}
+
+void WriteFile(const std::string &filename, const char *fmt, ...) {
+ FILE *stream = fopen(filename.c_str(), "w");
+ if (stream == nullptr) {
+ DIE("fopen(%s)", filename.c_str());
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ // Use a local variable to make sure we call va_end before DIE() in case this
+ // returns an error.
+ int r = vfprintf(stream, fmt, ap);
+ va_end(ap);
+
+ if (r < 0) {
+ DIE("vfprintf");
+ }
+
+ if (fclose(stream) != 0) {
+ DIE("fclose(%s)", filename.c_str());
+ }
+}
+
+void SetTimeout(double timeout_secs) {
+ if (timeout_secs <= 0) {
+ DIE("timeout_secs must be positive");
+ }
+
+ double int_val, fraction_val;
+ fraction_val = modf(timeout_secs, &int_val);
+
+ struct itimerval timer;
+ timer.it_interval.tv_sec = 0;
+ timer.it_interval.tv_usec = 0;
+ timer.it_value.tv_sec = (time_t)int_val,
+ timer.it_value.tv_usec = (suseconds_t)(fraction_val * 1e6);
+
+ if (setitimer(ITIMER_REAL, &timer, nullptr) < 0) {
+ DIE("setitimer");
+ }
+}
+
+void InstallSignalHandler(int signum, void (*handler)(int)) {
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = handler;
+ if (handler == SIG_IGN || handler == SIG_DFL) {
+ // No point in blocking signals when using the default handler or ignoring
+ // the signal.
+ if (sigemptyset(&sa.sa_mask) < 0) {
+ DIE("sigemptyset");
+ }
+ } else {
+ // When using a custom handler, block all signals from firing while the
+ // handler is running.
+ if (sigfillset(&sa.sa_mask) < 0) {
+ DIE("sigfillset");
+ }
+ }
+ // sigaction may fail for certain reserved signals. Ignore failure in this
+ // case.
+ sigaction(signum, &sa, nullptr);
+}
+
+void IgnoreSignal(int signum) { InstallSignalHandler(signum, SIG_IGN); }
+
+void RestoreSignalHandlersAndMask() {
+ // Use an empty signal mask for the process (= unblock all signals).
+ sigset_t empty_set;
+ if (sigemptyset(&empty_set) < 0) {
+ DIE("sigemptyset");
+ }
+ if (sigprocmask(SIG_SETMASK, &empty_set, nullptr) < 0) {
+ DIE("sigprocmask(SIG_SETMASK, <empty set>, nullptr)");
+ }
+
+ // Set the default signal handler for all signals.
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ if (sigemptyset(&sa.sa_mask) < 0) {
+ DIE("sigemptyset");
+ }
+ sa.sa_handler = SIG_DFL;
+ for (int i = 1; i < NSIG; ++i) {
+ // Ignore possible errors, because we might not be allowed to set the
+ // handler for certain signals, but we still want to try.
+ sigaction(i, &sa, nullptr);
+ }
+}
+
+void KillMeWhenMyParentDies(int signum) {
+#if defined(__linux__)
+ if (prctl(PR_SET_PDEATHSIG, signum) < 0) {
+ DIE("prctl");
+ }
+#endif
+}
+
+void BecomeSubreaper() {
+#if defined(__FreeBSD__)
+ if (procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, 0) < 0) {
+ DIE("procctl");
+ }
+#endif
+#if defined(__linux__)
+ if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
+ DIE("prctl");
+ }
+#endif
+}
+
+int SpawnCommand(const vector<char *> &args) {
+ int child_pid = fork();
+ if (child_pid < 0) {
+ DIE("fork");
+ } else if (child_pid == 0) {
+ // Put the child into its own process group.
+ if (setpgid(0, 0) < 0) {
+ DIE("setpgid");
+ }
+
+ // Try to assign our terminal to the child process.
+ if (tcsetpgrp(STDIN_FILENO, getpgrp()) < 0 && errno != ENOTTY) {
+ DIE("tcsetpgrp")
+ }
+
+ // Unblock all signals, restore default handlers.
+ RestoreSignalHandlersAndMask();
+
+ // Force umask to include read and execute for everyone, to make output
+ // permissions predictable.
+ umask(022);
+
+ if (execvp(args[0], args.data()) < 0) {
+ DIE("execvp(%s, %p)", args[0], args.data());
+ }
+ }
+ return child_pid;
+}
+
+static void KillAllRemainingChildren(int main_child_pid) {
+ // If the child process we spawned earlier terminated, we want to make
+ // sure all remaining (grand)children are killed, too.
+ if (getpid() == 1) {
+ // If we're PID 1, this is easy.
+ if (kill(-1, SIGKILL) < 0 && errno != ESRCH) {
+ DIE("kill");
+ }
+ } else {
+#if defined(__FreeBSD__)
+ // FreeBSD is cool, because it has an API to kill all our descendants in one
+ // go.
+ struct procctl_reaper_kill data;
+ data.rk_sig = SIGKILL;
+ if (procctl(P_PID, getpid(), PROC_REAP_KILL, &data) < 0 && errno != ESRCH) {
+ DIE("procctl")
+ }
+#else
+ // On other operating systems, we have to resort to sending SIGKILL to the
+ // process group of our child and hope that this kills them all.
+ // TODO(philwo) - what if a child switched to a different process group
+ // and we can't kill it like this? Maybe parse /proc/*/stat and filter
+ // by "their PPID = my PID"?
+ if (kill(-main_child_pid, SIGKILL) < 0 && errno != ESRCH) {
+ DIE("kill");
+ }
+#endif
+ }
+}
+
+int WaitForChild(int main_child_pid) {
+ // This will be overwritten by the real exitcode from the child in the loop
+ // below. In case something goes horribly wrong and that doesn't happen, at
+ // least exit with a failure.
+ int exitcode = EXIT_FAILURE;
+ while (1) {
+ // Check for zombies to be reaped and exit, if our own child exited.
+ int status;
+ pid_t killed_pid = wait(&status);
+
+ if (killed_pid < 0) {
+ // Our PID1 process got a signal that interrupted the wait() call and that
+ // was either ignored or forwarded to the child. This is expected and
+ // fine, just continue waiting.
+ if (errno == EINTR) {
+ continue;
+ } else if (errno == ECHILD) {
+ // No children left to wait for, we're done here.
+ break;
+ }
+ DIE("waitpid")
+ } else {
+ if (killed_pid == main_child_pid) {
+ KillAllRemainingChildren(main_child_pid);
+
+ if (WIFSIGNALED(status)) {
+ exitcode = 128 + WTERMSIG(status);
+ } else {
+ exitcode = WEXITSTATUS(status);
+ }
+ }
+ }
+ }
+ return exitcode;
+}
diff --git a/src/main/tools/process-tools.h b/src/main/tools/process-tools.h
index a0ba38816d..0244ebb816 100644
--- a/src/main/tools/process-tools.h
+++ b/src/main/tools/process-tools.h
@@ -15,71 +15,66 @@
#ifndef PROCESS_TOOLS_H__
#define PROCESS_TOOLS_H__
-#include <sys/types.h>
-#include <stdbool.h>
+#include <string>
+#include <vector>
-// see
-// http://stackoverflow.com/questions/5641427/how-to-make-preprocessor-generate-a-string-for-line-keyword
#define S(x) #x
#define S_(x) S(x)
#define S__LINE__ S_(__LINE__)
-#define DIE(args...) \
- { \
- fprintf(stderr, __FILE__ ":" S__LINE__ ": " args); \
- exit(EXIT_FAILURE); \
+#define DIE(...) \
+ { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": \"" __VA_ARGS__); \
+ fprintf(stderr, "\": "); \
+ perror(nullptr); \
+ exit(EXIT_FAILURE); \
}
-#define CHECK_CALL(x) \
- if ((x) == -1) { \
- fprintf(stderr, __FILE__ ":" S__LINE__ ": "); \
- perror(#x); \
- exit(EXIT_FAILURE); \
- }
+#define PRINT_DEBUG(...) \
+ do { \
+ if (opt.debug) { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \
+ fprintf(stderr, "\n"); \
+ } \
+ } while (0)
-#define CHECK_NOT_NULL(x) \
- if (x == NULL) { \
- perror(#x); \
- exit(EXIT_FAILURE); \
- }
+// Set the effective and saved uid / gid to the real uid / gid.
+void DropPrivileges();
-// Switch completely to the effective uid.
-// Some programs (notably, bash) ignore the euid and just use the uid. This
-// limits the ability for us to use process-wrapper as a setuid binary for
-// security/user-isolation.
-int SwitchToEuid();
+// Redirect the open file descriptor fd to the file target_path. Do nothing if
+// target_path is '-'.
+void Redirect(const std::string &target_path, int fd);
-// Switch completely to the effective gid.
-int SwitchToEgid();
+// Write formatted contents into the file filename.
+void WriteFile(const std::string &filename, const char *fmt, ...);
-// Redirect stdout to the file stdout_path (but not if stdout_path is "-").
-void RedirectStdout(const char *stdout_path);
+// Receive SIGALRM after the given timeout. timeout_secs must be positive.
+void SetTimeout(double timeout_secs);
-// Redirect stderr to the file stdout_path (but not if stderr_path is "-").
-void RedirectStderr(const char *stderr_path);
+// Installs a signal handler for signum and sets all signals to block during
+// that signal.
+void InstallSignalHandler(int signum, void (*handler)(int));
-// Make sure the process group "pgrp" and all its subprocesses are killed.
-// If "gracefully" is true, sends SIGTERM first and after a timeout of
-// "graceful_kill_delay" seconds, sends SIGKILL.
-// If not, send SIGKILL immediately.
-void KillEverything(int pgrp, bool gracefully, double graceful_kill_delay);
+// Sets the signal handler of signum to SIG_IGN.
+void IgnoreSignal(int signum);
-// Set up a signal handler for a signal.
-void HandleSignal(int sig, void (*handler)(int));
+// Reset the signal mask and restore the default handler for all signals.
+void RestoreSignalHandlersAndMask();
-// Revert signal handler for a signal to the default.
-void UnHandle(int sig);
+// Ask the kernel to kill us with signum if our parent dies.
+void KillMeWhenMyParentDies(int signum);
-// Use an empty signal mask for the process and set all signal handlers to their
-// default.
-void ClearSignalMask();
+// This is the magic that makes waiting for all children (even grandchildren)
+// work. By becoming a subreaper, all grandchildren that are not waited for by
+// our direct child will be reparented to us, which allows us to wait for them.
+void BecomeSubreaper();
-// Receive SIGALRM after the given timeout. No-op if the timeout is
-// non-positive.
-void SetTimeout(double timeout_secs);
+// Forks and execvp's the process specified in args in its own process group.
+// Returns the pid of the spawned process.
+int SpawnCommand(const std::vector<char *> &args);
-// Wait for "pid" to exit and return its exit code.
-// "name" is used for the error message only.
-int WaitChild(pid_t pid, const char *name);
+// Waits for child_pid to exit, then kills all remaining (grand)children, waits
+// for them to exit, then returns the exitcode of child_pid.
+int WaitForChild(int child_pid);
#endif // PROCESS_TOOLS_H__
diff --git a/src/main/tools/process-wrapper.c b/src/main/tools/process-wrapper.c
deleted file mode 100644
index 07ae840a52..0000000000
--- a/src/main/tools/process-wrapper.c
+++ /dev/null
@@ -1,169 +0,0 @@
-// Copyright 2014 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// process-wrapper runs a subprocess with a given timeout (optional),
-// redirecting stdout and stderr to given files. Upon exit, whether
-// from normal termination or timeout, the subprocess (and any of its children)
-// is killed.
-//
-// The exit status of this program is whatever the child process returned,
-// unless process-wrapper receives a signal. ie, on SIGTERM this program will
-// die with raise(SIGTERM) even if the child process handles SIGTERM with
-// exit(0).
-
-#define _GNU_SOURCE
-
-#include <err.h>
-#include <errno.h>
-#include <signal.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include "process-tools.h"
-
-// Not in headers on OSX.
-extern char **environ;
-
-static double global_kill_delay;
-static int global_child_pid;
-static volatile sig_atomic_t global_signal;
-
-// Options parsing result.
-struct Options {
- double timeout_secs;
- double kill_delay_secs;
- const char *stdout_path;
- const char *stderr_path;
- char *const *args;
-};
-
-// Print out a usage error. argc and argv are the argument counter and vector,
-// fmt is a format,
-// string for the error message to print.
-static void Usage(char *const *argv) {
- fprintf(stderr,
- "Usage: %s <timeout-secs> <kill-delay-secs> <stdout-redirect> "
- "<stderr-redirect> <command> [args] ...\n",
- argv[0]);
- exit(EXIT_FAILURE);
-}
-
-// Parse the command line flags and return the result in an Options structure
-// passed as argument.
-static void ParseCommandLine(int argc, char *const *argv, struct Options *opt) {
- if (argc <= 5) {
- Usage(argv);
- }
-
- argv++;
- if (sscanf(*argv++, "%lf", &opt->timeout_secs) != 1) {
- DIE("timeout_secs is not a real number.\n");
- }
- if (sscanf(*argv++, "%lf", &opt->kill_delay_secs) != 1) {
- DIE("kill_delay_secs is not a real number.\n");
- }
- opt->stdout_path = *argv++;
- opt->stderr_path = *argv++;
- opt->args = argv;
-}
-
-// Called when timeout or signal occurs.
-void OnSignal(int sig) {
- global_signal = sig;
-
- // Nothing to do if we received a signal before spawning the child.
- if (global_child_pid == -1) {
- return;
- }
-
- if (sig == SIGALRM) {
- // SIGALRM represents a timeout, so we should give the process a bit of
- // time to die gracefully if it needs it.
- KillEverything(global_child_pid, true, global_kill_delay);
- } else {
- // Signals should kill the process quickly, as it's typically blocking
- // the return of the prompt after a user hits "Ctrl-C".
- KillEverything(global_child_pid, false, global_kill_delay);
- }
-}
-
-// Run the command specified by the argv array and kill it after timeout
-// seconds.
-static void SpawnCommand(char *const *argv, double timeout_secs) {
- CHECK_CALL(global_child_pid = fork());
- if (global_child_pid == 0) {
- // In child.
- CHECK_CALL(setsid());
- ClearSignalMask();
-
- // Force umask to include read and execute for everyone, to make
- // output permissions predictable.
- umask(022);
-
- // Does not return unless something went wrong.
- execvp(argv[0], argv);
- err(EXIT_FAILURE, "execvp(\"%s\", ...)", argv[0]);
- } else {
- // In parent.
-
- // Set up a signal handler which kills all subprocesses when the given
- // signal is triggered.
- HandleSignal(SIGALRM, OnSignal);
- HandleSignal(SIGTERM, OnSignal);
- HandleSignal(SIGINT, OnSignal);
- SetTimeout(timeout_secs);
-
- int status = WaitChild(global_child_pid, argv[0]);
-
- // The child is done for, but may have grandchildren that we still have to
- // kill.
- kill(-global_child_pid, SIGKILL);
-
- if (global_signal > 0) {
- // Don't trust the exit code if we got a timeout or signal.
- UnHandle(global_signal);
- raise(global_signal);
- } else if (WIFEXITED(status)) {
- exit(WEXITSTATUS(status));
- } else {
- int sig = WTERMSIG(status);
- UnHandle(sig);
- raise(sig);
- }
- }
-}
-
-int main(int argc, char *argv[]) {
- struct Options opt;
- memset(&opt, 0, sizeof(opt));
-
- ParseCommandLine(argc, argv, &opt);
- global_kill_delay = opt.kill_delay_secs;
-
- SwitchToEuid();
- SwitchToEgid();
-
- RedirectStdout(opt.stdout_path);
- RedirectStderr(opt.stderr_path);
-
- SpawnCommand(opt.args, opt.timeout_secs);
-
- return 0;
-}
diff --git a/src/main/tools/process-wrapper.cc b/src/main/tools/process-wrapper.cc
new file mode 100644
index 0000000000..9d02eae79f
--- /dev/null
+++ b/src/main/tools/process-wrapper.cc
@@ -0,0 +1,186 @@
+// Copyright 2014 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// process-wrapper runs a subprocess with a given timeout (optional),
+// redirecting stdout and stderr to given files. Upon exit, whether
+// from normal termination or timeout, the subprocess (and any of its children)
+// is killed.
+//
+// The exit status of this program is whatever the child process returned,
+// unless process-wrapper receives a signal. ie, on SIGTERM this program will
+// die with raise(SIGTERM) even if the child process handles SIGTERM with
+// exit(0).
+
+#include "src/main/tools/process-tools.h"
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+using std::vector;
+
+// Not in headers on OSX.
+extern char **environ;
+
+// The pid of the spawned child process.
+static volatile sig_atomic_t global_child_pid;
+
+// The signal that will be sent to the child when a timeout occurs.
+static volatile sig_atomic_t global_next_timeout_signal = SIGTERM;
+
+// Whether the child was killed due to a timeout.
+static volatile sig_atomic_t global_timeout_occurred;
+
+// Options parsing result.
+struct Options {
+ double timeout_secs;
+ double kill_delay_secs;
+ std::string stdout_path;
+ std::string stderr_path;
+ bool debug;
+ vector<char *> args;
+};
+
+static struct Options opt;
+
+// Print out a usage error and exit with EXIT_FAILURE.
+static void Usage(char *program_name) {
+ fprintf(stderr,
+ "Usage: %s <timeout-secs> <kill-delay-secs> <stdout-redirect> "
+ "<stderr-redirect> <command> [args] ...\n",
+ program_name);
+ exit(EXIT_FAILURE);
+}
+
+// Parse the command line flags and put the results in the global opt variable.
+static void ParseCommandLine(vector<char *> args) {
+ if (args.size() <= 5) {
+ Usage(args.front());
+ }
+
+ int optind = 1;
+
+ if (sscanf(args[optind++], "%lf", &opt.timeout_secs) != 1) {
+ DIE("timeout_secs is not a real number.\n");
+ }
+ if (sscanf(args[optind++], "%lf", &opt.kill_delay_secs) != 1) {
+ DIE("kill_delay_secs is not a real number.\n");
+ }
+ opt.stdout_path.assign(args[optind++]);
+ opt.stderr_path.assign(args[optind++]);
+ opt.args.assign(args.begin() + optind, args.end());
+
+ // argv[] passed to execve() must be a null-terminated array.
+ opt.args.push_back(nullptr);
+}
+
+static void OnTimeout(int signum) {
+ global_timeout_occurred = true;
+ kill(-global_child_pid, global_next_timeout_signal);
+ if (global_next_timeout_signal == SIGTERM && opt.kill_delay_secs > 0) {
+ global_next_timeout_signal = SIGKILL;
+ SetTimeout(opt.kill_delay_secs);
+ }
+}
+
+static void ForwardSignal(int signum) {
+ if (global_child_pid > 0) {
+ kill(-global_child_pid, signum);
+ }
+}
+
+static void SetupSignalHandlers() {
+ RestoreSignalHandlersAndMask();
+
+ for (int signum = 1; signum < NSIG; signum++) {
+ switch (signum) {
+ // Some signals should indeed kill us and not be forwarded to the child,
+ // thus we can use the default handler.
+ case SIGABRT:
+ case SIGBUS:
+ case SIGFPE:
+ case SIGILL:
+ case SIGSEGV:
+ case SIGSYS:
+ case SIGTRAP:
+ break;
+ // It's fine to use the default handler for SIGCHLD, because we use wait()
+ // in the main loop to wait for children to die anyway.
+ case SIGCHLD:
+ break;
+ // One does not simply install a signal handler for these two signals
+ case SIGKILL:
+ case SIGSTOP:
+ break;
+ // Ignore SIGTTIN and SIGTTOU, as we hand off the terminal to the child in
+ // SpawnChild().
+ case SIGTTIN:
+ case SIGTTOU:
+ IgnoreSignal(signum);
+ break;
+ // We need a special signal handler for this if we use a timeout.
+ case SIGALRM:
+ if (opt.timeout_secs > 0) {
+ InstallSignalHandler(signum, OnTimeout);
+ } else {
+ InstallSignalHandler(signum, ForwardSignal);
+ }
+ break;
+ // All other signals should be forwarded to the child.
+ default:
+ InstallSignalHandler(signum, ForwardSignal);
+ break;
+ }
+ }
+}
+
+int main(int argc, char *argv[]) {
+ KillMeWhenMyParentDies(SIGTERM);
+ DropPrivileges();
+
+ vector<char *> args(argv, argv + argc);
+ ParseCommandLine(args);
+
+ Redirect(opt.stdout_path, STDOUT_FILENO);
+ Redirect(opt.stderr_path, STDERR_FILENO);
+
+ SetupSignalHandlers();
+ BecomeSubreaper();
+ global_child_pid = SpawnCommand(opt.args);
+
+ if (opt.timeout_secs > 0) {
+ SetTimeout(opt.timeout_secs);
+ }
+
+ int exitcode = WaitForChild(global_child_pid);
+ if (global_timeout_occurred) {
+ return 128 + SIGALRM;
+ }
+
+ return exitcode;
+}
diff --git a/src/test/shell/bazel/process-wrapper_test.sh b/src/test/shell/bazel/process-wrapper_test.sh
index b720ab8dbd..18903d3bfc 100755
--- a/src/test/shell/bazel/process-wrapper_test.sh
+++ b/src/test/shell/bazel/process-wrapper_test.sh
@@ -105,7 +105,7 @@ function test_execvp_error_message() {
local code=0
$process_wrapper -1 0 $OUT $ERR /bin/notexisting &> $TEST_log || code=$?
assert_equals 1 "$code"
- assert_contains "execvp(\"/bin/notexisting\", ...): No such file or directory" "$ERR"
+ assert_contains "\"execvp(/bin/notexisting, [[:alnum:]]\+)\": No such file or directory" "$ERR"
}
run_suite "process-wrapper"