aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/tools/process-wrapper.c
diff options
context:
space:
mode:
authorGravatar Philipp Wollermann <philwo@google.com>2015-08-25 12:52:57 +0000
committerGravatar Lukacs Berki <lberki@google.com>2015-08-26 07:37:05 +0000
commit43c4a1a1452603bfe5e6883626c5ac91ea4e8eb6 (patch)
tree257c9f0f924b5b2cf96c208cd53ba4ff40259aca /src/main/tools/process-wrapper.c
parent988bb21407c3abf97100d90cff2b823dd594ef30 (diff)
Execute spawns inside sandboxes to improve hermeticity (spawns can no longer use non-declared inputs) and safety (spawns can no longer affect the host system, e.g. accidentally wipe your home directory). This implementation works on Linux only and uses Linux containers ("namespaces").
The strategy works with all actions that Bazel supports (C++ / Java compilation, genrules, test execution, Skylark-based rules, ...) and in tests, Bazel could successfully bootstrap itself and pass the whole test suite using sandboxed execution. This is not the default behavior yet, but can be activated explicitly by using: bazel build --genrule_strategy=sandboxed --spawn_strategy=sandboxed //my:stuff -- MOS_MIGRATED_REVID=101457297
Diffstat (limited to 'src/main/tools/process-wrapper.c')
-rw-r--r--src/main/tools/process-wrapper.c263
1 files changed, 89 insertions, 174 deletions
diff --git a/src/main/tools/process-wrapper.c b/src/main/tools/process-wrapper.c
index 27601bf212..d156463726 100644
--- a/src/main/tools/process-wrapper.c
+++ b/src/main/tools/process-wrapper.c
@@ -22,220 +22,117 @@
// die with raise(SIGTERM) even if the child process handles SIGTERM with
// exit(0).
-#ifndef _GNU_SOURCE
#define _GNU_SOURCE
-#endif
#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/time.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 int global_pid; // Returned from fork().
-static int global_signal = -1;
-static double global_kill_delay = 0.0;
-
-#define DIE(args...) { \
- fprintf(stderr, args); \
- fprintf(stderr, " --- "); \
- perror(NULL); \
- fprintf(stderr, "\n"); \
- exit(EXIT_FAILURE); \
+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);
}
-#define CHECK_CALL(x) if (x != 0) { perror(#x); exit(1); }
-
-// Make sure the process and all subprocesses are killed.
-static void KillEverything(int pgrp) {
- kill(-pgrp, SIGTERM);
-
- // Round up fractional seconds in this polling implementation.
- int kill_delay = (int)(global_kill_delay+0.999) ;
- // If the process is still alive, give it some time to die gracefully.
- while (kill(-pgrp, 0) == 0 && kill_delay-- > 0) {
- sleep(1);
+// 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);
}
- kill(-pgrp, SIGKILL);
+ 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.
-static void OnSignal(int sig) {
+// 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_pid);
+ 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".
- kill(-global_pid, SIGKILL);
- }
-}
-
-// Set up a signal handler which kills all subprocesses when the
-// given signal is triggered.
-static void InstallSignalHandler(int sig) {
- struct sigaction sa = {};
-
- sa.sa_handler = OnSignal;
- sigemptyset(&sa.sa_mask);
- CHECK_CALL(sigaction(sig, &sa, NULL));
-}
-
-// Revert signal handler to default.
-static void UnHandle(int sig) {
- struct sigaction sa = {};
- sa.sa_handler = SIG_DFL;
- sigemptyset(&sa.sa_mask);
- CHECK_CALL(sigaction(sig, &sa, NULL));
-}
-
-// Enable the given timeout, or no-op if the timeout is non-positive.
-static void EnableAlarm(double timeout) {
- if (timeout <= 0) return;
-
- struct itimerval timer = {};
- timer.it_interval.tv_sec = 0;
- timer.it_interval.tv_usec = 0;
-
- double int_val, fraction_val;
- fraction_val = modf(timeout, &int_val);
- timer.it_value.tv_sec = (long) int_val;
- timer.it_value.tv_usec = (long) (fraction_val * 1e6);
- CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL));
-}
-
-static void ClearSignalMask() {
- // Use an empty signal mask and default signal handlers in the
- // subprocess.
- sigset_t sset;
- sigemptyset(&sset);
- sigprocmask(SIG_SETMASK, &sset, NULL);
- for (int i = 1; i < NSIG; ++i) {
- if (i == SIGKILL || i == SIGSTOP) continue;
-
- struct sigaction sa = {};
- sa.sa_handler = SIG_DFL;
- sigemptyset(&sa.sa_mask);
- sigaction(i, &sa, NULL);
+ KillEverything(global_child_pid, false, global_kill_delay);
}
}
-static int WaitChild(pid_t pid, const char *name) {
- int err = 0;
- int status = 0;
- do {
- err = waitpid(pid, &status, 0);
- } while (err == -1 && errno == EINTR);
-
- if (err == -1) {
- DIE("wait on %s (pid %d) failed", name, pid);
- }
- return status;
-}
-
-// Usage: process-wrapper
-// <timeout_sec> <kill_delay_sec> <stdout file> <stderr file>
-// [cmdline]
-int main(int argc, char *argv[]) {
- if (argc <= 5) {
- DIE("Not enough cmd line arguments to process-wrapper");
- }
-
- int uid = getuid();
- int euid = geteuid();
- if (uid != euid) {
- // Switch completely to the target 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.
- if (setreuid(euid, euid) != 0) {
- DIE("changing uid failed: setreuid");
- }
- }
-
- int gid = getgid();
- int egid = getegid();
- if (gid != egid) {
- // Switch completely to the target gid.
- if (setregid(egid, egid) != 0) {
- DIE("changing gid failed: setregid");
- }
- }
-
- // Parse the cmdline args to get the timeout and redirect files.
- argv++;
- double timeout;
- if (sscanf(*argv++, "%lf", &timeout) != 1) {
- DIE("timeout_sec is not a real number.");
- }
- if (sscanf(*argv++, "%lf", &global_kill_delay) != 1) {
- DIE("kill_delay_sec is not a real number.");
- }
- char *stdout_path = *argv++;
- char *stderr_path = *argv++;
-
- if (strcmp(stdout_path, "-")) {
- // Redirect stdout and stderr.
- int fd_out = open(stdout_path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if (fd_out == -1) {
- DIE("Could not open %s for stdout", stdout_path);
- }
- if (dup2(fd_out, STDOUT_FILENO) == -1) {
- DIE("dup2 failed for stdout");
- }
- CHECK_CALL(close(fd_out));
- }
-
- if (strcmp(stderr_path, "-")) {
- int fd_err = open(stderr_path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if (fd_err == -1) {
- DIE("Could not open %s for stderr", stderr_path);
- }
- if (dup2(fd_err, STDERR_FILENO) == -1) {
- DIE("dup2 failed for stderr");
- }
- CHECK_CALL(close(fd_err));
- }
-
- global_pid = fork();
- if (global_pid < 0) {
- DIE("Fork failed");
- } else if (global_pid == 0) {
+// 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.
- if (setsid() == -1) {
- DIE("Could not setsid from child");
- }
+ CHECK_CALL(setsid());
ClearSignalMask();
+
// Force umask to include read and execute for everyone, to make
// output permissions predictable.
umask(022);
- execvp(argv[0], argv); // Does not return.
- DIE("execvpe %s failed", argv[0]);
+ // Does not return unless something went wrong.
+ CHECK_CALL(execvp(argv[0], argv));
} else {
// In parent.
- InstallSignalHandler(SIGALRM);
- InstallSignalHandler(SIGTERM);
- InstallSignalHandler(SIGINT);
- EnableAlarm(timeout);
- int status = WaitChild(global_pid, argv[0]);
+ // 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);
- // The child is done, but may have grandchildren.
- kill(-global_pid, SIGKILL);
if (global_signal > 0) {
// Don't trust the exit code if we got a timeout or signal.
UnHandle(global_signal);
@@ -249,3 +146,21 @@ int main(int argc, char *argv[]) {
}
}
}
+
+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;
+}