| // Copyright 2016 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. | |
| /** | |
| * linux-sandbox runs commands in a restricted environment where they are | |
| * subject to a few rules: | |
| * | |
| * - The entire filesystem is made read-only. | |
| * - The working directory (-W) will be made read-write, though. | |
| * - Individual files or directories can be made writable (but not deletable) | |
| * (-w). | |
| * - If the process takes longer than the timeout (-T), it will be killed with | |
| * SIGTERM. If it does not exit within the grace period (-t), it all of its | |
| * children will be killed with SIGKILL. | |
| * - tmpfs can be mounted on top of existing directories (-e). | |
| * - If option -R is passed, the process will run as user 'root'. | |
| * - If option -U is passed, the process will run as user 'nobody'. | |
| * - Otherwise, the process runs using the current uid / gid. | |
| * - If linux-sandbox itself gets killed, the process and all of its children | |
| * will be killed. | |
| * - If linux-sandbox's parent dies, it will kill itself, the process and all | |
| * the children. | |
| * - Network access is allowed, but can be disabled via -N. | |
| * - The hostname and domainname will be set to "sandbox". | |
| * - The process runs in its own PID namespace, so other processes on the | |
| * system are invisible. | |
| */ | |
| #include "src/main/tools/linux-sandbox.h" | |
| #include <ctype.h> | |
| #include <dirent.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <math.h> | |
| #include <sched.h> | |
| #include <signal.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/prctl.h> | |
| #include <sys/resource.h> | |
| #include <sys/stat.h> | |
| #include <sys/time.h> | |
| #include <sys/types.h> | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #include <atomic> | |
| #include <string> | |
| #include <vector> | |
| #include "src/main/tools/linux-sandbox-options.h" | |
| #include "src/main/tools/linux-sandbox-pid1.h" | |
| #include "src/main/tools/logging.h" | |
| #include "src/main/tools/process-tools.h" | |
| uid_t global_outer_uid; | |
| gid_t global_outer_gid; | |
| // The PID of our child process, for use in signal handlers. | |
| static std::atomic<pid_t> global_child_pid{0}; | |
| // Our parent's pid at the outset, to check if the original parent has exited. | |
| pid_t initial_ppid; | |
| // Must we politely ask the child to exit before we send it a SIGKILL (once we | |
| // want it to exit)? Holds only zero or one. | |
| static std::atomic<int> global_need_polite_sigterm{false}; | |
| #if __cplusplus >= 201703L | |
| static_assert(global_child_pid.is_always_lock_free); | |
| static_assert(global_need_polite_sigterm.is_always_lock_free); | |
| #endif | |
| // Make sure the child process does not inherit any accidentally left open file | |
| // handles from our parent. | |
| static void CloseFds() { | |
| DIR *fds = opendir("/proc/self/fd"); | |
| if (fds == nullptr) { | |
| DIE("opendir"); | |
| } | |
| while (1) { | |
| errno = 0; | |
| struct dirent *dent = readdir(fds); | |
| if (dent == nullptr) { | |
| if (errno != 0) { | |
| DIE("readdir"); | |
| } | |
| break; | |
| } | |
| if (isdigit(dent->d_name[0])) { | |
| errno = 0; | |
| int fd = strtol(dent->d_name, nullptr, 10); | |
| // (1) Skip unparseable entries. | |
| // (2) Close everything except stdin, stdout, stderr and debug output. | |
| // (3) Do not accidentally close our directory handle. | |
| if (errno == 0 && fd > STDERR_FILENO && | |
| (global_debug == NULL || fd != fileno(global_debug)) && | |
| fd != dirfd(fds)) { | |
| if (close(fd) < 0) { | |
| DIE("close"); | |
| } | |
| } | |
| } | |
| } | |
| if (closedir(fds) < 0) { | |
| DIE("closedir"); | |
| } | |
| } | |
| static void MaybeAddChildProcessToCgroup(const pid_t pid) { | |
| for (const std::string &cgroups_dir : opt.cgroups_dirs) { | |
| PRINT_DEBUG("Adding process %d to cgroups dir %s", pid, | |
| cgroups_dir.c_str()); | |
| WriteFile(cgroups_dir + "/cgroup.procs", "%d", pid); | |
| } | |
| } | |
| static void OnTimeoutOrTerm(int) { | |
| // Find the PID of the child, which main set up before installing us as a | |
| // signal handler. | |
| const pid_t child_pid = global_child_pid.load(std::memory_order_relaxed); | |
| // Figure out whether we should send a SIGTERM here. If so, we won't want to | |
| // next time we're called. | |
| const bool need_polite_sigterm = | |
| global_need_polite_sigterm.fetch_and(0, std::memory_order_relaxed); | |
| // If we're not supposed to ask politely, simply forcibly kill the child. | |
| if (!need_polite_sigterm) { | |
| kill(child_pid, SIGKILL); | |
| return; | |
| } | |
| // Otherwise make a polite request, then arrange to be called again after a | |
| // delay, at which point we'll send SIGKILL. | |
| // | |
| // Note that main sets us up as the signal handler for SIGALRM, and arranges | |
| // for this code path to be taken only if kill_delay_secs > 0. | |
| kill(child_pid, SIGTERM); | |
| alarm(opt.kill_delay_secs); | |
| } | |
| static pid_t SpawnPid1() { | |
| const int kStackSize = 1024 * 1024; | |
| std::vector<char> child_stack(kStackSize); | |
| PRINT_DEBUG("calling pipe(2)..."); | |
| int pipe_from_child[2], pipe_to_child[2]; | |
| if (pipe(pipe_from_child) < 0) { | |
| DIE("pipe"); | |
| } | |
| if (pipe(pipe_to_child) < 0) { | |
| DIE("pipe"); | |
| } | |
| int clone_flags = | |
| CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWPID | SIGCHLD; | |
| PRINT_DEBUG("Netns is %d", opt.create_netns); | |
| if (opt.create_netns != NO_NETNS) { | |
| clone_flags |= CLONE_NEWNET; | |
| } | |
| if (opt.fake_hostname) { | |
| clone_flags |= CLONE_NEWUTS; | |
| } | |
| // 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). | |
| PRINT_DEBUG("calling clone(2)..."); | |
| Pid1Args pid1Args; | |
| pid1Args.pipe_to_parent = pipe_from_child; | |
| pid1Args.pipe_from_parent = pipe_to_child; | |
| const pid_t child_pid = clone(Pid1Main, child_stack.data() + kStackSize, | |
| clone_flags, &pid1Args); | |
| if (child_pid < 0) { | |
| DIE("clone"); | |
| } | |
| MaybeAddChildProcessToCgroup(child_pid); | |
| // Signal the child that it can now proceed to spawn pid2. | |
| SignalPipe(pipe_to_child); | |
| PRINT_DEBUG("linux-sandbox-pid1 has PID %d", child_pid); | |
| // Wait for a signal from the child linux-sandbox-pid1 process; this proves to | |
| // the child process that we still existed after it ran | |
| // prctl(PR_SET_PDEATHSIG, SIGKILL), thus preventing a race condition where | |
| // the parent is killed before that call was made. | |
| WaitPipe(pipe_from_child); | |
| PRINT_DEBUG("done manipulating pipes"); | |
| return child_pid; | |
| } | |
| static int WaitForPid1(const pid_t child_pid) { | |
| // Wait for the child to exit, obtaining usage information. Restart in the | |
| // case of a signal interrupting us. | |
| int child_status; | |
| struct rusage child_rusage; | |
| while (true) { | |
| const int ret = wait4(child_pid, &child_status, 0, &child_rusage); | |
| if (ret > 0) { | |
| break; | |
| } | |
| // We've been handed off to a reaper process and should die. | |
| if (getppid() != initial_ppid) { | |
| break; | |
| } | |
| if (errno == EINTR) { | |
| continue; | |
| } | |
| DIE("wait4"); | |
| } | |
| // If we're supposed to write stats to a file, do so now. | |
| if (!opt.stats_path.empty()) { | |
| WriteStatsToFile(&child_rusage, opt.stats_path); | |
| } | |
| // We want to exit in the same manner as the child. | |
| if (WIFSIGNALED(child_status)) { | |
| const int signal = WTERMSIG(child_status); | |
| PRINT_DEBUG("child exited due to receiving signal: %s", strsignal(signal)); | |
| return 128 + signal; | |
| } | |
| const int exit_code = WEXITSTATUS(child_status); | |
| PRINT_DEBUG("child exited normally with code %d", exit_code); | |
| return exit_code; | |
| } | |
| 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"); | |
| } | |
| // Parse our command-line options. | |
| ParseOptions(argc, argv); | |
| // Open the file PRINT_DEBUG writes to. | |
| // Must happen early enough so we don't lose any debugging output. | |
| if (!opt.debug_path.empty()) { | |
| global_debug = fopen(opt.debug_path.c_str(), "w"); | |
| if (!global_debug) { | |
| DIE("fopen(%s)", opt.debug_path.c_str()); | |
| } | |
| } | |
| // Start with default signal actions and a clear signal mask. | |
| ClearSignalMask(); | |
| // Ignore SIGTTIN and SIGTTOU, as we hand off the terminal to the child in | |
| // SpawnChild. | |
| IgnoreSignal(SIGTTIN); | |
| IgnoreSignal(SIGTTOU); | |
| // Remember the parent pid so we can exit if the parent has exited. | |
| // Doing this before prctl(PR_SET_PDEATHDIG, 0) ensures no race condition. | |
| initial_ppid = getppid(); | |
| if (opt.persistent_process) { | |
| if (prctl(PR_SET_PDEATHSIG, 0) < 0) { | |
| DIE("prctl"); | |
| } | |
| } | |
| // Redirect output as requested. | |
| Redirect(opt.stdout_path, STDOUT_FILENO); | |
| Redirect(opt.stderr_path, STDERR_FILENO); | |
| // Set up two globals used by the child process. | |
| global_outer_uid = getuid(); | |
| global_outer_gid = getgid(); | |
| // Ensure we don't pass on any FDs from our parent to our child other than | |
| // stdin, stdout, stderr and global_debug. | |
| CloseFds(); | |
| // Spawn the child that will fork the sandboxed program with fresh | |
| // namespaces etc. | |
| const pid_t child_pid = SpawnPid1(); | |
| // Let the signal handlers installed below know the PID of the child. | |
| global_child_pid.store(child_pid, std::memory_order_relaxed); | |
| // If a kill delay has been configured, let the signal handlers installed | |
| // below know that it needs to be respected. | |
| if (opt.kill_delay_secs > 0) { | |
| global_need_polite_sigterm.store(1, std::memory_order_relaxed); | |
| } | |
| // OnTimeoutOrTerm, which is used for other signals below, assumes that it | |
| // handles SIGALRM. We also explicitly invoke it after the timeout using | |
| // alarm(2). | |
| InstallSignalHandler(SIGALRM, OnTimeoutOrTerm); | |
| // If requested, arrange for the child to be killed (optionally after being | |
| // asked politely to terminate) once the timeout expires. | |
| // | |
| // Note that it's important to set this up before support for SIGTERM and | |
| // SIGINT. Otherwise one of those signals could arrive before we get here, | |
| // and then we would reset its opt.kill_delay_secs interval timer. | |
| if (opt.timeout_secs > 0) { | |
| alarm(opt.timeout_secs); | |
| } | |
| // Also ask/tell the child to quit on SIGTERM, and optionally for SIGINT | |
| // too. | |
| InstallSignalHandler(SIGTERM, OnTimeoutOrTerm); | |
| if (opt.sigint_sends_sigterm) { | |
| InstallSignalHandler(SIGINT, OnTimeoutOrTerm); | |
| } | |
| // Wait for the child to exit, returning an appropriate status. | |
| return WaitForPid1(child_pid); | |
| } |