aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/tools/namespace-sandbox.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/tools/namespace-sandbox.c')
-rw-r--r--src/main/tools/namespace-sandbox.c738
1 files changed, 417 insertions, 321 deletions
diff --git a/src/main/tools/namespace-sandbox.c b/src/main/tools/namespace-sandbox.c
index 5cf6b433be..060356d13f 100644
--- a/src/main/tools/namespace-sandbox.c
+++ b/src/main/tools/namespace-sandbox.c
@@ -1,5 +1,3 @@
-#define _GNU_SOURCE
-
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,404 +12,502 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+#define _GNU_SOURCE
+
#include <errno.h>
#include <fcntl.h>
-#include <getopt.h>
-#include <limits.h>
-#include <linux/capability.h>
+#include <libgen.h>
+#include <pwd.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
-#include <sys/time.h>
#include <sys/types.h>
-#include <sys/wait.h>
#include <unistd.h>
-static int global_debug = 0;
+#include "process-tools.h"
-#define PRINT_DEBUG(...) do { if (global_debug) {fprintf(stderr, "sandbox.c: " __VA_ARGS__);}} while(0)
+#define PRINT_DEBUG(...) \
+ do { \
+ if (global_debug) { \
+ fprintf(stderr, __FILE__ ":" S__LINE__ ": " __VA_ARGS__); \
+ } \
+ } while (0)
-#define CHECK_CALL(x) if ((x) == -1) { perror(#x); exit(1); }
-#define CHECK_NOT_NULL(x) if (x == NULL) { perror(#x); exit(1); }
-#define DIE() do { fprintf(stderr, "Error in %d\n", __LINE__); exit(-1); } while(0);
+static bool global_debug = false;
+static double global_kill_delay;
+static int global_child_pid;
+static volatile sig_atomic_t global_signal;
-const int kChildrenCleanupDelay = 1;
+// The uid and gid of the user and group 'nobody'.
+static const int kNobodyUid = 65534;
+static const int kNobodyGid = 65534;
-static volatile sig_atomic_t global_signal_received = 0;
-
-//
-// Options parsing result
-//
+// Options parsing result.
struct Options {
- char **args; // Command to run (-C / --)
- char *include_prefix; // Include prefix (-N)
- char *sandbox_root; // Sandbox root (-S)
- char *tools; // tools directory (-t)
- char **mounts; // List of directories to mount (-m)
- char **includes; // List of include directories (-n)
- int num_mounts; // size of mounts
- int num_includes; // size of includes
- int timeout; // Timeout (-T)
+ double timeout_secs; // How long to wait before killing the child (-T)
+ double kill_delay_secs; // How long to wait before sending SIGKILL in case of
+ // timeout (-t)
+ const char *stdout_path; // Where to redirect stdout (-l)
+ const char *stderr_path; // Where to redirect stderr (-L)
+ char *const *args; // Command to run (--)
+ const char *sandbox_root; // Sandbox root (-S)
+ const char *working_dir; // Working directory (-W)
+ char **mount_sources; // Map of directories to mount, from
+ char **mount_targets; // sources -> targets (-m)
+ int num_mounts; // How many mounts were specified
};
-// 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.
-void Usage(int argc, char **argv, char *fmt, ...);
-// Parse the command line flags and return the result in an
-// Options structure passed as argument.
-void ParseCommandLine(int argc, char **argv, struct Options *opt);
-
-// Signal hanlding
-void PropagateSignals();
-void EnableAlarm();
-// Sandbox setup
-void SetupDirectories(struct Options* opt);
-void SetupSlashDev();
-void SetupUserNamespace(int uid, int gid);
-void ChangeRoot();
-// Write the file "filename" using a format string specified by "fmt".
-// Returns -1 on failure.
-int WriteFile(const char *filename, const char *fmt, ...);
-// Run the command specified by the argv array and kill it after
-// timeout seconds.
-void SpawnCommand(char **argv, int timeout);
-
-
-
-int main(int argc, char *argv[]) {
- struct Options opt = {
- .args = NULL,
- .include_prefix = NULL,
- .sandbox_root = NULL,
- .tools = NULL,
- .mounts = calloc(argc, sizeof(char*)),
- .includes = calloc(argc, sizeof(char*)),
- .num_mounts = 0,
- .num_includes = 0,
- .timeout = 0
- };
- ParseCommandLine(argc, argv, &opt);
- int uid = getuid();
- int gid = getgid();
-
- // parsed all arguments, now prepare sandbox
- PRINT_DEBUG("%s\n", opt.sandbox_root);
- // create new namespaces in which this process and its children will live
- CHECK_CALL(unshare(CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER));
- CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL));
- // Create the sandbox directory layout
- SetupDirectories(&opt);
- // Set the user namespace (user_namespaces(7))
- SetupUserNamespace(uid, gid);
- // make sandbox actually hermetic:
- ChangeRoot();
+// 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(int argc, char *const *argv, const char *fmt, ...) {
+ int i;
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
- // Finally call the command
- free(opt.mounts);
- free(opt.includes);
- SpawnCommand(opt.args, opt.timeout);
- return 0;
+ fprintf(stderr,
+ "\nUsage: %s [-S sandbox-root] [-W working-dir] [-M source -m "
+ "target] -- command arg1\n",
+ argv[0]);
+ fprintf(stderr, " provided:");
+ for (i = 0; i < argc; i++) {
+ fprintf(stderr, " %s", argv[i]);
+ }
+ fprintf(stderr,
+ "\nMandatory arguments:\n"
+ " -S directory which will become the root of the sandbox\n"
+ " -- command to run inside sandbox, followed by arguments\n"
+ "\n"
+ "Optional arguments:\n"
+ " -W working directory\n"
+ " -t time to give the child to shutdown cleanly before sending it a "
+ "SIGKILL\n"
+ " -T timeout after which sandbox will be terminated\n"
+ " -t in case timeout occurs, how long to wait before killing the "
+ "child with SIGKILL\n"
+ " -M/-m system directory to mount inside the sandbox\n"
+ " Multiple directories can be specified and each of them will\n"
+ " be mounted readonly. The -M option specifies which directory\n"
+ " to mount, the -m option specifies where to mount it in the\n"
+ " sandbox.\n"
+ " -D if set, debug info will be printed\n"
+ " -l redirect stdout to a file\n"
+ " -L redirect stderr to a file\n");
+ exit(EXIT_FAILURE);
}
-void SpawnCommand(char **argv, int timeout) {
- for (int i = 0; argv[i] != NULL; i++) {
- PRINT_DEBUG("arg: %s\n", argv[i]);
- }
+// 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) {
+ extern char *optarg;
+ extern int optind, optopt;
+ int c;
- // spawn child and wait until it finishes
- pid_t cpid = fork();
- if (cpid == 0) {
- CHECK_CALL(setpgid(0, 0));
- // if the execvp below fails with "No such file or directory" it means that:
- // a) the binary is not in the sandbox (which means it wasn't included in
- // the inputs)
- // b) the binary uses shared library which is not inside sandbox - you can
- // check for that by running "ldd ./a.out" (by default directories
- // starting with /lib* and /usr/lib* should be there)
- // c) the binary uses elf interpreter which is not inside sandbox - you can
- // check for that by running "readelf -a a.out | grep interpreter" (the
- // sandbox code assumes that it is either in /lib*/ or /usr/lib*/)
- CHECK_CALL(execvp(argv[0], argv));
- PRINT_DEBUG("Exec failed near %s:%d\n", __FILE__, __LINE__);
- exit(1);
- } else {
- // PARENT
- // make sure that all signals propagate to children (mostly useful to kill
- // entire sandbox)
- PropagateSignals();
- // after given timeout, kill children
- EnableAlarm(timeout);
- int status = 0;
- while (1) {
- PRINT_DEBUG("Waiting for the child...\n");
- pid_t pid = wait(&status);
- if (global_signal_received) {
- PRINT_DEBUG("Received signal: %s\n", strsignal(global_signal_received));
- CHECK_CALL(killpg(cpid, global_signal_received));
- // give children some time for cleanup before they terminate
- sleep(kChildrenCleanupDelay);
- CHECK_CALL(killpg(cpid, SIGKILL));
- exit(128 | global_signal_received);
- }
- if (errno == EINTR) {
- continue;
- }
- if (pid < 0) {
- perror("Wait failed:");
- exit(1);
- }
- if (WIFEXITED(status)) {
- PRINT_DEBUG("Child exited with status: %d\n", WEXITSTATUS(status));
- exit(WEXITSTATUS(status));
- }
- if (WIFSIGNALED(status)) {
- PRINT_DEBUG("Child terminated by a signal: %d\n", WTERMSIG(status));
- exit(WEXITSTATUS(status));
- }
- if (WIFSTOPPED(status)) {
- PRINT_DEBUG("Child stopped by a signal: %d\n", WSTOPSIG(status));
- }
+ while ((c = getopt(argc, argv, ":DS:W:t:T:M:m:l:L:")) != -1) {
+ switch (c) {
+ case 'S':
+ if (opt->sandbox_root == NULL) {
+ opt->sandbox_root = optarg;
+ } else {
+ Usage(argc, argv,
+ "Multiple sandbox roots (-S) specified, expected one.");
+ }
+ break;
+ case 'W':
+ if (opt->working_dir == NULL) {
+ opt->working_dir = optarg;
+ } else {
+ Usage(argc, argv,
+ "Multiple working directories (-W) specified, expected at most "
+ "one.");
+ }
+ break;
+ case 't':
+ if (sscanf(optarg, "%lf", &opt->kill_delay_secs) != 1 ||
+ opt->kill_delay_secs < 0) {
+ Usage(argc, argv, "Invalid kill delay (-t) value: %lf",
+ opt->kill_delay_secs);
+ }
+ break;
+ case 'T':
+ if (sscanf(optarg, "%lf", &opt->timeout_secs) != 1 ||
+ opt->timeout_secs < 0) {
+ Usage(argc, argv, "Invalid timeout (-T) value: %lf",
+ opt->timeout_secs);
+ }
+ break;
+ case 'M':
+ if (opt->mount_sources[opt->num_mounts] != NULL) {
+ Usage(argc, argv, "The -M option must be followed by an -m option.");
+ }
+ opt->mount_sources[opt->num_mounts] = optarg;
+ break;
+ case 'm':
+ if (opt->mount_sources[opt->num_mounts] == NULL) {
+ Usage(argc, argv, "The -m option must be preceded by an -M option.");
+ }
+ if (opt->sandbox_root == NULL) {
+ Usage(argc, argv,
+ "The sandbox root must be set via the -S option before "
+ "specifying an"
+ " -m option.");
+ }
+ if (strstr(optarg, opt->sandbox_root) != optarg) {
+ Usage(argc, argv,
+ "A path passed to the -m option must start with the sandbox "
+ "root.");
+ }
+ opt->mount_targets[opt->num_mounts++] = optarg;
+ break;
+ case 'D':
+ global_debug = true;
+ break;
+ case 'l':
+ if (opt->stdout_path == NULL) {
+ opt->stdout_path = optarg;
+ } else {
+ Usage(argc, argv,
+ "Cannot redirect stdout to more than one destination.");
+ }
+ break;
+ case 'L':
+ if (opt->stderr_path == NULL) {
+ opt->stderr_path = optarg;
+ } else {
+ Usage(argc, argv,
+ "Cannot redirect stderr to more than one destination.");
+ }
+ break;
+ case '?':
+ Usage(argc, argv, "Unrecognized argument: -%c (%d)", optopt, optind);
+ break;
+ case ':':
+ Usage(argc, argv, "Flag -%c requires an argument", optopt);
+ break;
}
}
-}
-int WriteFile(const char *filename, const char *fmt, ...) {
- int r;
- va_list ap;
- FILE *stream = fopen(filename, "w");
- if (stream == NULL) {
- return -1;
+ if (opt->sandbox_root == NULL) {
+ Usage(argc, argv, "Sandbox root (-S) must be specified");
}
- va_start(ap, fmt);
- r = vfprintf(stream, fmt, ap);
- va_end(ap);
- if (r >= 0) {
- r = fclose(stream);
+
+ if (opt->mount_sources[opt->num_mounts] != NULL &&
+ opt->mount_sources[opt->num_mounts] == NULL) {
+ Usage(argc, argv, "An -m option is missing.");
}
- return r;
-}
-//
-// Signal handling
-//
-void SignalHandler(int signum, siginfo_t *info, void *uctxt) {
- global_signal_received = signum;
+ opt->args = argv + optind;
+ if (argc <= optind) {
+ Usage(argc, argv, "No command specified.");
+ }
}
-void PropagateSignals() {
- // propagate some signals received by the parent to processes in sandbox, so
- // that it's easier to terminate entire sandbox
- struct sigaction action = {};
- action.sa_flags = SA_SIGINFO;
- action.sa_sigaction = SignalHandler;
-
- // handle all signals that could terminate the process
- int signals[] = {SIGHUP, SIGINT, SIGKILL, SIGPIPE, SIGALRM, SIGTERM, SIGPOLL,
- SIGPROF, SIGVTALRM,
- // signals below produce core dump by default, however at the moment we'll
- // just terminate
- SIGQUIT, SIGILL, SIGABRT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS, SIGTRAP, SIGXCPU,
- SIGXFSZ, -1};
- for (int *p = signals; *p != -1; p++) {
- sigaction(*p, &action, NULL);
+static void CreateNamespaces() {
+ // This weird workaround is necessary due to unshare sometimes failing with EINVAL due to a race
+ // condition in the Linux kernel (see https://lkml.org/lkml/2015/7/28/833).
+ // An alternative would be to use clone/waitpid instead.
+ int delay = 1;
+ int tries = 0;
+ const int max_tries = 5000000;
+ while (tries++ < max_tries) {
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC) ==
+ 0) {
+ PRINT_DEBUG("unshare succeeded after %d tries\n", tries);
+ return;
+ } else {
+ if (errno != EINVAL) {
+ perror("unshare");
+ exit(EXIT_FAILURE);
+ }
+ }
+ usleep(delay);
+ delay = (delay * 3) / 2;
}
+ fprintf(stderr,
+ "unshare failed with EINVAL even after %d tries, giving up.\n",
+ tries);
+ exit(EXIT_FAILURE);
}
-void EnableAlarm(int timeout) {
- if (timeout <= 0) return;
-
- struct itimerval timer = {};
- timer.it_value.tv_sec = (long) timeout;
- CHECK_CALL(setitimer(ITIMER_REAL, &timer, NULL));
+static void CreateFile(const char *path) {
+ int handle;
+ CHECK_CALL(handle = open(path, O_CREAT | O_WRONLY | O_EXCL, 0666));
+ CHECK_CALL(close(handle));
}
-//
-// Sandbox setup
-//
-void SetupSlashDev() {
+static void SetupDevices() {
CHECK_CALL(mkdir("dev", 0755));
- const char *devs[] = {
- "/dev/null",
- "/dev/random",
- "/dev/urandom",
- "/dev/zero",
- NULL
- };
+ const char *devs[] = {"/dev/null", "/dev/random", "/dev/urandom", "/dev/zero",
+ NULL};
for (int i = 0; devs[i] != NULL; i++) {
- // open+close to create the file, which will become mount point for actual
- // device
- int handle = open(devs[i] + 1, O_CREAT | O_RDONLY, 0644);
- CHECK_CALL(handle);
- CHECK_CALL(close(handle));
+ CreateFile(devs[i] + 1);
CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL));
}
+
+ CHECK_CALL(symlink("/proc/self/fd", "dev/fd"));
}
-void SetupDirectories(struct Options *opt) {
- // Mount the sandbox and go there.
- CHECK_CALL(mount(opt->sandbox_root, opt->sandbox_root, NULL, MS_BIND | MS_NOSUID, NULL));
- CHECK_CALL(chdir(opt->sandbox_root));
- SetupSlashDev();
- // Mount blaze specific directories - tools/ and build-runfiles/.
- if (opt->tools != NULL) {
- PRINT_DEBUG("tools: %s\n", opt->tools);
- CHECK_CALL(mkdir("tools", 0755));
- CHECK_CALL(mount(opt->tools, "tools", NULL, MS_BIND | MS_RDONLY, NULL));
+// Recursively creates the file or directory specified in "path" and its parent
+// directories.
+static int CreateTarget(const char *path, bool is_directory) {
+ if (path == NULL) {
+ errno = EINVAL;
+ return -1;
}
- // Mount directories passed in argv; those are mostly dirs for shared libs.
- for (int i = 0; i < opt->num_mounts; i++) {
- CHECK_CALL(mount(opt->mounts[i], opt->mounts[i] + 1, NULL, MS_BIND | MS_RDONLY, NULL));
+ struct stat sb;
+ // If the path already exists...
+ if (stat(path, &sb) == 0) {
+ if (is_directory && S_ISDIR(sb.st_mode)) {
+ // and it's a directory and supposed to be a directory, we're done here.
+ return 0;
+ } else if (!is_directory && S_ISREG(sb.st_mode)) {
+ // and it's a regular file and supposed to be one, we're done here.
+ return 0;
+ } else {
+ // otherwise something is really wrong.
+ errno = is_directory ? ENOTDIR : EEXIST;
+ return -1;
+ }
+ } else {
+ // If stat failed because of any error other than "the path does not exist",
+ // this is an error.
+ if (errno != ENOENT) {
+ return -1;
+ }
}
- // C++ compilation
- // C++ headers go in a separate directory.
- if (opt->include_prefix != NULL) {
- CHECK_CALL(chdir(opt->include_prefix));
- for (int i = 0; i < opt->num_includes; i++) {
- // TODO(bazel-team): sometimes list of -iquote given by bazel contains
- // invalid (non-existing) entries, ideally we would like not to have them
- PRINT_DEBUG("include: %s\n", opt->includes[i]);
- if (mount(opt->includes[i], opt->includes[i] + 1 , NULL, MS_BIND, NULL) > -1) {
- continue;
- }
- if (errno == ENOENT) {
- continue;
- }
- CHECK_CALL(-1);
- }
- CHECK_CALL(chdir(".."));
+ // Create the parent directory.
+ CHECK_CALL(CreateTarget(dirname(strdupa(path)), true));
+
+ if (is_directory) {
+ CHECK_CALL(mkdir(path, 0755));
+ } else {
+ CreateFile(path);
}
+ return 0;
+}
+
+static void SetupDirectories(struct Options *opt) {
+ // Mount the sandbox and go there.
+ CHECK_CALL(mount(opt->sandbox_root, opt->sandbox_root, NULL,
+ MS_BIND | MS_NOSUID, NULL));
+ CHECK_CALL(chdir(opt->sandbox_root));
+
+ // Setup /dev.
+ SetupDevices();
+
CHECK_CALL(mkdir("proc", 0755));
CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL));
+
+ CHECK_CALL(mkdir("tmp", 0755));
+ CHECK_CALL(mount("tmpfs", "tmp", "tmpfs", MS_NOSUID | MS_NODEV,
+ "size=25%,mode=1777"));
+
+ // Make sure the home directory exists and is writable.
+ const char *homedir;
+ if ((homedir = getenv("HOME")) == NULL) {
+ homedir = getpwuid(getuid())->pw_dir;
+ }
+
+ if (homedir[0] != '/') {
+ DIE("Home directory of user nobody must be an absolute path, but is %s", homedir);
+ }
+
+ char *homedir_absolute = malloc(strlen(opt->sandbox_root) + strlen(homedir) + 1);
+ strcpy(homedir_absolute, opt->sandbox_root);
+ strcat(homedir_absolute, homedir);
+
+ CreateTarget(homedir_absolute, true);
+ CHECK_CALL(mount("tmpfs", homedir_absolute, "tmpfs", MS_NOSUID | MS_NODEV,
+ "size=25%,mode=1777"));
+
+ // Mount directories passed in argv
+ for (int i = 0; i < opt->num_mounts; i++) {
+ struct stat sb;
+ stat(opt->mount_sources[i], &sb);
+
+ CHECK_CALL(CreateTarget(opt->mount_targets[i], S_ISDIR(sb.st_mode)));
+
+ PRINT_DEBUG("mount -o rbind,ro %s %s\n", opt->mount_sources[i],
+ opt->mount_targets[i]);
+ CHECK_CALL(mount(opt->mount_sources[i], opt->mount_targets[i], NULL,
+ MS_REC | MS_BIND | MS_RDONLY, NULL));
+ }
}
-void SetupUserNamespace(int uid, int gid) {
+// Write the file "filename" using a format string specified by "fmt". Returns
+// -1 on failure.
+static int WriteFile(const char *filename, const char *fmt, ...) {
+ int r;
+ va_list ap;
+ FILE *stream = fopen(filename, "w");
+ if (stream == NULL) {
+ return -1;
+ }
+ va_start(ap, fmt);
+ r = vfprintf(stream, fmt, ap);
+ va_end(ap);
+ if (r >= 0) {
+ r = fclose(stream);
+ }
+ return r;
+}
+
+static void SetupUserNamespace(int uid, int gid) {
// Disable needs for CAP_SETGID
int r = WriteFile("/proc/self/setgroups", "deny");
if (r < 0 && errno != ENOENT) {
// Writing to /proc/self/setgroups might fail on earlier
// version of linux because setgroups does not exist, ignore.
perror("WriteFile(\"/proc/self/setgroups\", \"deny\")");
- exit(-1);
+ exit(EXIT_FAILURE);
}
- // set group and user mapping from outer namespace to inner:
- // no changes in the parent, be root in the child
- CHECK_CALL(WriteFile("/proc/self/uid_map", "0 %d 1\n", uid));
- CHECK_CALL(WriteFile("/proc/self/gid_map", "0 %d 1\n", gid));
- CHECK_CALL(setresuid(0, 0, 0));
- CHECK_CALL(setresgid(0, 0, 0));
+ // Set group and user mapping from outer namespace to inner:
+ // No changes in the parent, be nobody in the child.
+ //
+ // We can't be root in the child, because some code may assume that running as root grants it
+ // certain capabilities that it doesn't in fact have. It's safer to let the child think that it
+ // is just a normal user.
+ CHECK_CALL(WriteFile("/proc/self/uid_map", "%d %d 1\n", kNobodyUid, uid));
+ CHECK_CALL(WriteFile("/proc/self/gid_map", "%d %d 1\n", kNobodyGid, gid));
+
+ CHECK_CALL(setresuid(kNobodyUid, kNobodyUid, kNobodyUid));
+ CHECK_CALL(setresgid(kNobodyGid, kNobodyGid, kNobodyGid));
}
-void ChangeRoot() {
+static void ChangeRoot(struct Options *opt) {
// move the real root to old_root, then detach it
char old_root[16] = "old-root-XXXXXX";
- CHECK_NOT_NULL(mkdtemp(old_root));
+ if (mkdtemp(old_root) == NULL) {
+ perror("mkdtemp");
+ DIE("mkdtemp returned NULL\n");
+ }
+
// pivot_root has no wrapper in libc, so we need syscall()
CHECK_CALL(syscall(SYS_pivot_root, ".", old_root));
CHECK_CALL(chroot("."));
CHECK_CALL(umount2(old_root, MNT_DETACH));
CHECK_CALL(rmdir(old_root));
+
+ if (opt->working_dir != NULL) {
+ CHECK_CALL(chdir(opt->working_dir));
+ }
}
-//
-// Command line parsing
-//
-void Usage(int argc, char **argv, char *fmt, ...) {
- int i;
- va_list ap;
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
+// Called when timeout or signal occurs.
+void OnSignal(int sig) {
+ global_signal = sig;
- fprintf(stderr,
- "\nUsage: %s [-S sandbox-root] [-m mount] [-C|--] command arg1\n",
- argv[0]);
- fprintf(stderr, " provided:");
- for (i = 0; i < argc; i++) {
- fprintf(stderr, " %s", argv[i]);
+ // 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);
}
- fprintf(stderr,
- "\nMandatory arguments:\n"
- " [-C|--] command to run inside sandbox, followed by arguments\n"
- " -S directory which will become the root of the sandbox\n"
- "\n"
- "Optional arguments:\n"
- " -t absolute path to bazel tools directory\n"
- " -T timeout after which sandbox will be terminated\n"
- " -m system directory to mount inside the sandbox\n"
- " Multiple directories can be specified and each of them will\n"
- " be mount as readonly\n"
- " -D if set, debug info will be printed\n");
- exit(1);
}
-void ParseCommandLine(int argc, char **argv, struct Options *opt) {
- extern char *optarg;
- extern int optind, optopt;
- int c;
+// Run the command specified by the argv array and kill it after timeout
+// seconds.
+static void SpawnCommand(char *const *argv, double timeout_secs) {
+ for (int i = 0; argv[i] != NULL; i++) {
+ PRINT_DEBUG("arg: %s\n", argv[i]);
+ }
- opt->include_prefix = NULL;
- opt->sandbox_root = NULL;
- opt->tools = NULL;
- opt->mounts = malloc(argc * sizeof(char*));
- opt->includes = malloc(argc * sizeof(char*));
- opt->num_mounts = 0;
- opt->num_includes = 0;
- opt->timeout = 0;
-
- while ((c = getopt(argc, argv, "+:S:t:T:m:N:n:DC")) != -1) {
- switch(c) {
- case 'S':
- if (opt->sandbox_root == NULL) {
- opt->sandbox_root = optarg;
- } else {
- Usage(argc, argv,
- "Multiple sandbox roots (-S) specified (expected one).");
- }
- break;
- case 'm':
- opt->mounts[opt->num_mounts++] = optarg;
- break;
- case 'D':
- global_debug = 1;
- break;
- case 'T':
- sscanf(optarg, "%d", &opt->timeout);
- if (opt->timeout < 0) {
- Usage(argc, argv, "Invalid timeout (-T) value: %d", opt->timeout);
- }
- break;
- case 'N':
- opt->include_prefix = optarg;
- break;
- case 'n':
- opt->includes[opt->num_includes++] = optarg;
- break;
- case 'C':
- break; // deprecated, ignore.
- case 't':
- opt->tools = optarg;
- break;
- case '?':
- Usage(argc, argv, "Unrecognized argument: -%c (%d)", optopt, optind);
- break;
- case ':':
- Usage(argc, argv, "Flag -%c requires an argument", optopt);
- break;
+ 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.
+ CHECK_CALL(execvp(argv[0], argv));
+ } 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);
}
}
+}
- opt->args = argv + optind;
- if (argc <= optind) {
- Usage(argc, argv, "No command specified");
- }
+int main(int argc, char *const argv[]) {
+ struct Options opt;
+ memset(&opt, 0, sizeof(opt));
+ opt.mount_sources = calloc(argc, sizeof(char *));
+ opt.mount_targets = calloc(argc, sizeof(char *));
+
+ ParseCommandLine(argc, argv, &opt);
+ global_kill_delay = opt.kill_delay_secs;
+
+ int uid = SwitchToEuid();
+ int gid = SwitchToEgid();
+
+ RedirectStdout(opt.stdout_path);
+ RedirectStderr(opt.stderr_path);
+
+ PRINT_DEBUG("sandbox root is %s\n", opt.sandbox_root);
+ PRINT_DEBUG("working dir is %s\n",
+ (opt.working_dir != NULL) ? opt.working_dir : "/ (default)");
+
+ CreateNamespaces();
+
+ // Make our mount namespace private, so that further mounts do not affect the
+ // outside environment.
+ CHECK_CALL(mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ SetupDirectories(&opt);
+ SetupUserNamespace(uid, gid);
+ ChangeRoot(&opt);
+
+ SpawnCommand(opt.args, opt.timeout_secs);
+
+ free(opt.mount_sources);
+ free(opt.mount_targets);
+
+ return 0;
}