| #define _GNU_SOURCE | |
| // Copyright 2014 Google Inc. 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 <errno.h> | |
| #include <fcntl.h> | |
| #include <getopt.h> | |
| #include <limits.h> | |
| #include <linux/capability.h> | |
| #include <sched.h> | |
| #include <signal.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; | |
| static int global_cpid; // Returned by fork() | |
| #define PRINT_DEBUG(...) do { if (global_debug) {fprintf(stderr, "sandbox.c: " __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); | |
| int kChildrenCleanupDelay = 1; | |
| void Usage() { | |
| fprintf(stderr, | |
| "Usage: ./sandbox [-R sandbox-root] [-m mount] -C command arg1\n" | |
| "Mandatory 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 PropagateSignals(); | |
| void EnableAlarm(); | |
| void SetupSlashDev(); | |
| static volatile sig_atomic_t global_signal_received = 0; | |
| int main(int argc, char *argv[]) { | |
| char *include_prefix = NULL; | |
| char *sandbox_root = NULL; | |
| char *tools = NULL; | |
| char **mounts = malloc(argc * sizeof(char*)); | |
| char **includes = malloc(argc * sizeof(char*)); | |
| int num_mounts = 0; | |
| int num_includes = 0; | |
| int iArg = 0; | |
| int uid = getuid(); | |
| int gid = getgid(); | |
| int timeout = 0; | |
| for (iArg = 1; iArg < argc - 1; iArg++) { | |
| if (strlen(argv[iArg]) != 2) { | |
| Usage(); | |
| } | |
| if (argv[iArg][0] != '-') { | |
| Usage(); | |
| } | |
| switch (argv[iArg][1]) { | |
| case 'S': | |
| if (sandbox_root == NULL) { | |
| sandbox_root = argv[++iArg]; | |
| } else { | |
| fprintf(stderr, | |
| "Multiple sandbox roots (-S) specified (expected one).\n"); | |
| Usage(); | |
| } | |
| break; | |
| case 'm': | |
| mounts[num_mounts++] = argv[++iArg]; | |
| break; | |
| case 'D': | |
| global_debug = 1; | |
| break; | |
| case 'T': | |
| sscanf(argv[iArg], "%d", &timeout); | |
| break; | |
| case 'N': | |
| include_prefix = argv[++iArg]; | |
| break; | |
| case 'n': | |
| includes[num_includes++] = argv[++iArg]; | |
| break; | |
| case 'C': | |
| iArg++; | |
| goto parsing_finished; | |
| case 't': | |
| tools = argv[++iArg]; | |
| break; | |
| default: | |
| fprintf(stderr, "Unrecognized argument: %s\n", argv[iArg]); | |
| Usage(); | |
| } | |
| } | |
| parsing_finished: | |
| if (iArg == argc) { | |
| fprintf(stderr, "No command specified.\n"); | |
| Usage(); | |
| } | |
| if (timeout < 0) { | |
| fprintf(stderr, "Invalid timeout (-T) value: %d", timeout); | |
| Usage(); | |
| } | |
| // parsed all arguments, now prepare sandbox | |
| PRINT_DEBUG("%s\n", 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)); | |
| // mount sandbox and go there | |
| CHECK_CALL(mount(sandbox_root, sandbox_root, NULL, MS_BIND | MS_NOSUID, NULL)); | |
| CHECK_CALL(chdir(sandbox_root)); | |
| SetupSlashDev(); | |
| // mount blaze specific directories - tools/ and build-runfiles/ | |
| if (tools != NULL) { | |
| PRINT_DEBUG("tools: %s\n", tools); | |
| CHECK_CALL(mkdir("tools", 0755)); | |
| CHECK_CALL(mount(tools, "tools", NULL, MS_BIND | MS_RDONLY, NULL)); | |
| } | |
| // mounts passed in argv; those are mostly dirs for shared libs | |
| for (int i = 0; i < num_mounts; i++) { | |
| CHECK_CALL(mount(mounts[i], mounts[i] + 1, NULL, MS_BIND | MS_RDONLY, NULL)); | |
| } | |
| // c++ compilation | |
| // headers go in separate directory | |
| if (include_prefix != NULL) { | |
| CHECK_CALL(chdir(include_prefix)); | |
| for (int i = 0; i < 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", includes[i]); | |
| if (mount(includes[i], includes[i] + 1 , NULL, MS_BIND, NULL) > -1) { | |
| continue; | |
| } | |
| if (errno == ENOENT) { | |
| continue; | |
| } | |
| CHECK_CALL(-1); | |
| } | |
| CHECK_CALL(chdir("..")); | |
| } | |
| // set group and user mapping from outer namespace to inner: | |
| // no changes in the parent, be root in the child | |
| int uid_fd, gid_fd; | |
| char uid_mapping[64], gid_mapping[64]; | |
| sprintf(uid_mapping, "0 %d 1\n", uid); | |
| sprintf(gid_mapping, "0 %d 1\n", gid); | |
| uid_fd = open("/proc/self/uid_map", O_WRONLY); | |
| CHECK_CALL(uid_fd); | |
| CHECK_CALL(write(uid_fd, uid_mapping, strlen(uid_mapping))); | |
| CHECK_CALL(close(uid_fd)); | |
| gid_fd = open("/proc/self/gid_map", O_WRONLY); | |
| CHECK_CALL(gid_fd); | |
| CHECK_CALL(write(gid_fd, gid_mapping, strlen(gid_mapping))); | |
| CHECK_CALL(close(gid_fd)); | |
| CHECK_CALL(setresuid(0, 0, 0)); | |
| CHECK_CALL(setresgid(0, 0, 0)); | |
| CHECK_CALL(mkdir("proc", 0755)); | |
| CHECK_CALL(mount("/proc", "proc", NULL, MS_REC | MS_BIND, NULL)); | |
| // make sandbox actually hermetic: | |
| // move the real root to old_root, then detach it | |
| char old_root[16] = "old-root-XXXXXX"; | |
| CHECK_NOT_NULL(mkdtemp(old_root)); | |
| // 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)); | |
| free(mounts); | |
| free(includes); | |
| for (int i = iArg; i < argc; i += 1) { | |
| PRINT_DEBUG("arg: %s\n", argv[i]); | |
| } | |
| // spawn child and wait until it finishes | |
| global_cpid = fork(); | |
| if (global_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[iArg], argv + iArg)); | |
| 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(global_cpid, global_signal_received)); | |
| // give children some time for cleanup before they terminate | |
| sleep(kChildrenCleanupDelay); | |
| CHECK_CALL(killpg(global_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)); | |
| } | |
| } | |
| } | |
| return 0; | |
| } | |
| void SignalHandler(int signum, siginfo_t *info, void *uctxt) { | |
| global_signal_received = signum; | |
| } | |
| 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); | |
| } | |
| } | |
| void SetupSlashDev() { | |
| CHECK_CALL(mkdir("dev", 0755)); | |
| 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)); | |
| CHECK_CALL(mount(devs[i], devs[i] + 1, NULL, MS_BIND, NULL)); | |
| } | |
| } | |
| 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)); | |
| } |