// Copyright 2019 Google LLC // // 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 // // https://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. // Implementation of the sandbox2::ForkServer class. #include "sandboxed_api/sandbox2/forkserver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/flags/declare.h" #include "absl/flags/flag.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "libcap/include/sys/capability.h" #include "sandboxed_api/sandbox2/client.h" #include "sandboxed_api/sandbox2/comms.h" #include "sandboxed_api/sandbox2/fork_client.h" #include "sandboxed_api/sandbox2/forkserver.pb.h" #include "sandboxed_api/sandbox2/namespace.h" #include "sandboxed_api/sandbox2/policy.h" #include "sandboxed_api/sandbox2/sanitizer.h" #include "sandboxed_api/sandbox2/syscall.h" #include "sandboxed_api/sandbox2/unwind/unwind.h" #include "sandboxed_api/sandbox2/util.h" #include "sandboxed_api/sandbox2/util/bpf_helper.h" #include "sandboxed_api/util/fileops.h" #include "sandboxed_api/util/raw_logging.h" #include "sandboxed_api/util/strerror.h" ABSL_DECLARE_FLAG(bool, sandbox2_forkserver_use_waitpid); namespace sandbox2 { namespace file_util = ::sapi::file_util; namespace { using ::sapi::StrError; void SigChldHandler(int signal) { if (signal != SIGCHLD) { return; } pid_t pid; int wstatus; do { pid = waitpid(-1, &wstatus, WNOHANG); } while (pid > 0); } // "Moves" FDs in move_fds from current to target FD number while keeping FDs // in keep_fds open - potentially moving them to another FD number as well in // case of colisions. // Ignores invalid (-1) fds. void MoveFDs(std::initializer_list> move_fds, std::initializer_list keep_fds) { absl::flat_hash_map fd_map; for (int* fd : keep_fds) { if (*fd != -1) { fd_map.emplace(*fd, fd); } } for (auto [old_fd, new_fd] : move_fds) { if (*old_fd != -1) { fd_map.emplace(*old_fd, old_fd); } } for (auto [old_fd, new_fd] : move_fds) { if (*old_fd == -1 || *old_fd == new_fd) { continue; } // Make sure we won't override another fd auto it = fd_map.find(new_fd); if (it != fd_map.end()) { int fd = dup(new_fd); SAPI_RAW_CHECK(fd != -1, "Duplicating an FD failed."); *it->second = fd; fd_map.emplace(fd, it->second); fd_map.erase(it); } if (dup2(*old_fd, new_fd) == -1) { SAPI_RAW_PLOG(FATAL, "Moving temporary to proper FD failed."); } close(*old_fd); fd_map.erase(*old_fd); *old_fd = new_fd; } } void RunInitProcess(pid_t main_pid, int pipe_fd, const absl::flat_hash_set& open_fds) { if (prctl(PR_SET_NAME, "S2-INIT-PROC", 0, 0, 0) != 0) { SAPI_RAW_PLOG(WARNING, "prctl(PR_SET_NAME, 'S2-INIT-PROC')"); } // Close all open fds (equals to CloseAllFDsExcept but does not require /proc // to be available). for (const auto& fd : open_fds) { close(fd); } // Clear SA_NOCLDWAIT. struct sigaction sa; sa.sa_handler = SIG_DFL; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); SAPI_RAW_CHECK(sigaction(SIGCHLD, &sa, nullptr) == 0, "clearing SA_NOCLDWAIT"); // Apply seccomp. std::vector code = { LOAD_ARCH, JNE32(sandbox2::Syscall::GetHostAuditArch(), DENY), LOAD_SYSCALL_NR, SYSCALL(__NR_waitid, ALLOW), SYSCALL(__NR_exit, ALLOW), }; if (pipe_fd >= 0) { code.insert(code.end(), {SYSCALL(__NR_write, ALLOW)}); } code.push_back(DENY); struct sock_fprog prog { .len = static_cast(code.size()), .filter = code.data(), }; SAPI_RAW_CHECK(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0, "Denying new privs"); SAPI_RAW_CHECK(prctl(PR_SET_KEEPCAPS, 0) == 0, "Dropping caps"); SAPI_RAW_CHECK( syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, reinterpret_cast(&prog)) == 0, "Enabling seccomp filter"); siginfo_t info; // Reap children. for (;;) { int rv = TEMP_FAILURE_RETRY(waitid(P_ALL, -1, &info, WEXITED | __WALL)); if (rv != 0) { _exit(1); } if (info.si_pid == main_pid) { if (pipe_fd >= 0) { write(pipe_fd, &info.si_code, sizeof(info.si_code)); write(pipe_fd, &info.si_status, sizeof(info.si_status)); } _exit(0); } } } absl::Status SendPid(int signaling_fd) { // Send our PID (the actual sandboxee process) via SCM_CREDENTIALS. // The ancillary message will be attached to the message as SO_PASSCRED is set // on the socket. char dummy = ' '; if (TEMP_FAILURE_RETRY(send(signaling_fd, &dummy, 1, 0)) != 1) { return absl::ErrnoToStatus(errno, "Sending PID: send()"); } return absl::OkStatus(); } absl::StatusOr ReceivePid(int signaling_fd) { union { struct cmsghdr cmh; char ctrl[CMSG_SPACE(sizeof(struct ucred))]; } ucred_msg{}; struct msghdr msgh {}; struct iovec iov {}; msgh.msg_iov = &iov; msgh.msg_iovlen = 1; msgh.msg_control = ucred_msg.ctrl; msgh.msg_controllen = sizeof(ucred_msg); char dummy; iov.iov_base = &dummy; iov.iov_len = sizeof(char); if (TEMP_FAILURE_RETRY(recvmsg(signaling_fd, &msgh, MSG_WAITALL)) != 1) { return absl::ErrnoToStatus(errno, "Receiving pid failed: recvmsg"); } struct cmsghdr* cmsgp = CMSG_FIRSTHDR(&msgh); if (cmsgp->cmsg_len != CMSG_LEN(sizeof(struct ucred)) || cmsgp->cmsg_level != SOL_SOCKET || cmsgp->cmsg_type != SCM_CREDENTIALS) { return absl::InternalError("Receiving pid failed"); } auto* ucredp = reinterpret_cast(CMSG_DATA(cmsgp)); return ucredp->pid; } absl::StatusOr GetRootMountId(const std::string& proc_id) { std::ifstream mounts(absl::StrCat("/proc/", proc_id, "/mountinfo")); if (!mounts.good()) { return absl::InternalError("Failed to open mountinfo"); } std::string line; while (std::getline(mounts, line)) { std::vector parts = absl::StrSplit(line, absl::MaxSplits(' ', 4)); if (parts.size() >= 4 && parts[3] == "/") { return std::string(parts[0]); } } return absl::NotFoundError("Root entry not found in mountinfo"); } bool IsLikelyChrooted() { absl::StatusOr self_root_id = GetRootMountId("self"); if (!self_root_id.ok()) { return absl::IsNotFound(self_root_id.status()); } absl::StatusOr init_root_id = GetRootMountId("1"); if (!init_root_id.ok()) { return false; } return *self_root_id != *init_root_id; } } // namespace void ForkServer::PrepareExecveArgs(const ForkRequest& request, std::vector* args, std::vector* envp) { // Prepare arguments for execve. for (const auto& arg : request.args()) { args->push_back(arg); } // Prepare environment variables for execve. for (const auto& env : request.envs()) { envp->push_back(env); } // The child process should not start any fork-servers. envp->push_back(absl::StrCat(kForkServerDisableEnv, "=1")); constexpr char kSapiVlogLevel[] = "SAPI_VLOG_LEVEL"; char* sapi_vlog = getenv(kSapiVlogLevel); if (sapi_vlog && strlen(sapi_vlog) > 0) { envp->push_back(absl::StrCat(kSapiVlogLevel, "=", sapi_vlog)); } SAPI_RAW_VLOG(1, "Will execute args:['%s'], environment:['%s']", absl::StrJoin(*args, "', '").c_str(), absl::StrJoin(*envp, "', '").c_str()); } void ForkServer::LaunchChild(const ForkRequest& request, int execve_fd, int client_fd, uid_t uid, gid_t gid, int signaling_fd, int status_fd, bool avoid_pivot_root) const { SAPI_RAW_CHECK(request.mode() != FORKSERVER_FORK_UNSPECIFIED, "Forkserver mode is unspecified"); bool will_execve = (request.mode() == FORKSERVER_FORK_EXECVE || request.mode() == FORKSERVER_FORK_EXECVE_SANDBOX); // Prepare the arguments before sandboxing (if needed), as doing it after // sandoxing can cause syscall violations (e.g. related to memory management). std::vector args; std::vector envs; if (will_execve) { PrepareExecveArgs(request, &args, &envs); } MoveFDs({{&execve_fd, Comms::kSandbox2TargetExecFD}, {&client_fd, Comms::kSandbox2ClientCommsFD}}, {&signaling_fd}); SanitizeEnvironment(); absl::StatusOr> open_fds = sanitizer::GetListOfFDs(); if (!open_fds.ok()) { SAPI_RAW_LOG(WARNING, "Could not get list of current open FDs: %s", std::string(open_fds.status().message()).c_str()); open_fds = absl::flat_hash_set(); } InitializeNamespaces(request, uid, gid, avoid_pivot_root); auto caps = cap_init(); SAPI_RAW_CHECK(cap_set_proc(caps) == 0, "while dropping capabilities"); cap_free(caps); // A custom init process is only needed if a new PID NS is created. if (request.clone_flags() & CLONE_NEWPID) { // Spawn a child process pid_t child = fork(); if (child < 0) { SAPI_RAW_PLOG(FATAL, "Could not spawn init process"); } if (child != 0) { if (status_fd >= 0) { open_fds->erase(status_fd); } RunInitProcess(child, status_fd, *open_fds); } if (status_fd >= 0) { close(status_fd); } // Send sandboxee pid auto status = SendPid(signaling_fd); SAPI_RAW_CHECK(status.ok(), absl::StrCat("sending pid: ", status.message()).c_str()); } if (request.mode() == FORKSERVER_FORK_EXECVE_SANDBOX || request.mode() == FORKSERVER_FORK_JOIN_SANDBOX_UNWIND) { // Sandboxing can be enabled either here - just before execve, or somewhere // inside the executed binary (e.g. after basic structures have been // initialized, and resources acquired). In the latter case, it's up to the // sandboxed binary to establish proper Comms channel (using // Comms::kSandbox2ClientCommsFD) and call sandbox2::Client::SandboxMeHere() // Create a Comms object here and not above, as we know we will execve and // therefore not call the Comms destructor, which would otherwise close the // comms file descriptor, which we do not want for the general case. Comms client_comms(Comms::kDefaultConnection); Client c(&client_comms); // The following client calls are basically SandboxMeHere. We split it so // that we can set up the envp after we received the file descriptors but // before we enable the syscall filter. std::vector preserved_fds; if (request.mode() == FORKSERVER_FORK_EXECVE_SANDBOX) { preserved_fds.push_back(execve_fd); } c.PrepareEnvironment(&preserved_fds); if (request.mode() == FORKSERVER_FORK_EXECVE_SANDBOX) { execve_fd = preserved_fds[0]; } if (client_comms.GetConnectionFD() != Comms::kSandbox2ClientCommsFD) { envs.push_back(absl::StrCat(Comms::kSandbox2CommsFDEnvVar, "=", client_comms.GetConnectionFD())); } envs.push_back(c.GetFdMapEnvVar()); // Convert args and envs before enabling sandbox (it'll allocate which might // be blocked). util::CharPtrArray argv = util::CharPtrArray::FromStringVector(args); util::CharPtrArray envp = util::CharPtrArray::FromStringVector(envs); c.EnableSandbox(); if (request.mode() == FORKSERVER_FORK_JOIN_SANDBOX_UNWIND) { exit(RunLibUnwindAndSymbolizer(&client_comms) ? EXIT_SUCCESS : EXIT_FAILURE); } else { ExecuteProcess(execve_fd, argv.data(), envp.data()); } } if (will_execve) { ExecuteProcess(execve_fd, util::CharPtrArray::FromStringVector(args).data(), util::CharPtrArray::FromStringVector(envs).data()); } } pid_t ForkServer::ServeRequest() { ForkRequest fork_request; if (!comms_->RecvProtoBuf(&fork_request)) { if (comms_->IsTerminated()) { return -1; } SAPI_RAW_LOG(FATAL, "Failed to receive ForkServer request"); } int comms_fd; SAPI_RAW_CHECK(comms_->RecvFD(&comms_fd), "Failed to receive Comms FD"); SAPI_RAW_CHECK(fork_request.mode() != FORKSERVER_FORK_UNSPECIFIED, "Forkserver mode is unspecified"); int exec_fd = -1; if (fork_request.mode() == FORKSERVER_FORK_EXECVE || fork_request.mode() == FORKSERVER_FORK_EXECVE_SANDBOX) { SAPI_RAW_CHECK(comms_->RecvFD(&exec_fd), "Failed to receive Exec FD"); } // Make the kernel notify us with SIGCHLD when the process terminates. // We use sigaction(SIGCHLD, flags=SA_NOCLDWAIT) in combination with // this to make sure the zombie process is reaped immediately. int clone_flags = fork_request.clone_flags() | SIGCHLD; // Store uid and gid since they will change if CLONE_NEWUSER is set. uid_t uid = getuid(); uid_t gid = getgid(); int pfds[2] = {-1, -1}; if (fork_request.monitor_type() == FORKSERVER_MONITOR_UNOTIFY) { SAPI_RAW_PCHECK(pipe(pfds) == 0, "creating status pipe"); } int socketpair_fds[2]; SAPI_RAW_PCHECK( socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, socketpair_fds) == 0, "creating signaling socketpair"); for (int i = 0; i < 2; i++) { int val = 1; SAPI_RAW_PCHECK(setsockopt(socketpair_fds[i], SOL_SOCKET, SO_PASSCRED, &val, sizeof(val)) == 0, "setsockopt failed"); } file_util::fileops::FDCloser fd_closer0{socketpair_fds[0]}; file_util::fileops::FDCloser fd_closer1{socketpair_fds[1]}; // Note: init_pid will be overwritten with the actual init pid if the init // process was started or stays at 0 if that is not needed - no pidns. pid_t init_pid = 0; pid_t sandboxee_pid = -1; bool avoid_pivot_root = clone_flags & (CLONE_NEWUSER | CLONE_NEWNS); if (avoid_pivot_root) { // Create initial namespaces only when they're first needed. // This allows sandbox2 to be still used without any namespaces support if (initial_mntns_fd_ == -1) { CreateInitialNamespaces(); } // We first just fork a child, which will join the initial namespaces // Note: Not a regular fork() as one really needs to be single-threaded to // setns and this is not the case with TSAN. pid_t pid = util::ForkWithFlags(SIGCHLD); SAPI_RAW_PCHECK(pid != -1, "fork failed"); if (pid == 0) { SAPI_RAW_PCHECK(setns(initial_userns_fd_, CLONE_NEWUSER) != -1, "joining initial user namespace"); SAPI_RAW_PCHECK(setns(initial_mntns_fd_, CLONE_NEWNS) != -1, "joining initial mnt namespace"); close(initial_userns_fd_); close(initial_mntns_fd_); // Do not create new userns it will be unshared later sandboxee_pid = util::ForkWithFlags((clone_flags & ~CLONE_NEWUSER) | CLONE_PARENT); if (sandboxee_pid == -1) { SAPI_RAW_LOG(ERROR, "util::ForkWithFlags(%x)", clone_flags); } if (sandboxee_pid != 0) { _exit(0); } // Send sandboxee pid absl::Status status = SendPid(fd_closer1.get()); SAPI_RAW_CHECK(status.ok(), absl::StrCat("sending pid: ", status.message()).c_str()); } else if (auto pid_or = ReceivePid(fd_closer0.get()); !pid_or.ok()) { SAPI_RAW_LOG(ERROR, "receiving pid: %s", std::string(pid_or.status().message()).c_str()); } else { sandboxee_pid = pid_or.value(); } } else { sandboxee_pid = util::ForkWithFlags(clone_flags); if (sandboxee_pid == -1) { SAPI_RAW_LOG(ERROR, "util::ForkWithFlags(%x)", clone_flags); } if (sandboxee_pid == 0) { close(initial_userns_fd_); close(initial_mntns_fd_); } } // Child. if (sandboxee_pid == 0) { LaunchChild(fork_request, exec_fd, comms_fd, uid, gid, fd_closer1.get(), pfds[1], avoid_pivot_root); return sandboxee_pid; } fd_closer1.Close(); if (fork_request.clone_flags() & CLONE_NEWPID) { // The pid of the init process is equal to the child process that we've // previously forked. init_pid = sandboxee_pid; sandboxee_pid = -1; // And the actual sandboxee is forked from the init process, so we need to // receive the actual PID. if (auto pid_or = ReceivePid(fd_closer0.get()); !pid_or.ok()) { SAPI_RAW_LOG(ERROR, "%s", std::string(pid_or.status().message()).c_str()); kill(init_pid, SIGKILL); init_pid = -1; } else { sandboxee_pid = pid_or.value(); } } // Parent. if (pfds[1] >= 0) { close(pfds[1]); } close(comms_fd); if (exec_fd >= 0) { close(exec_fd); } SAPI_RAW_CHECK(comms_->SendInt32(init_pid), absl::StrCat("Failed to send init PID: ", init_pid).c_str()); SAPI_RAW_CHECK( comms_->SendInt32(sandboxee_pid), absl::StrCat("Failed to send sandboxee PID: ", sandboxee_pid).c_str()); if (pfds[0] >= 0) { SAPI_RAW_CHECK(comms_->SendFD(pfds[0]), "Failed to send status pipe"); close(pfds[0]); } return sandboxee_pid; } bool ForkServer::IsTerminated() const { return comms_->IsTerminated(); } bool ForkServer::Initialize() { // All processes spawned by the fork'd/execute'd process will see this process // as /sbin/init. Therefore it will receive (and ignore) their final status // (see the next comment as well). PR_SET_CHILD_SUBREAPER is available since // kernel version 3.4, so don't panic if it fails. if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == -1) { SAPI_RAW_VLOG(3, "prctl(PR_SET_CHILD_SUBREAPER, 1): %s [%d]", StrError(errno).c_str(), errno); } // Don't convert terminated child processes into zombies. It's up to the // sandbox (Monitor) to track them and receive/report their final status. struct sigaction sa; if (absl::GetFlag(FLAGS_sandbox2_forkserver_use_waitpid)) { sa.sa_handler = SigChldHandler; sa.sa_flags = 0; } else { sa.sa_handler = SIG_DFL; sa.sa_flags = SA_NOCLDWAIT; } sigemptyset(&sa.sa_mask); if (sigaction(SIGCHLD, &sa, nullptr) == -1) { if (absl::GetFlag(FLAGS_sandbox2_forkserver_use_waitpid)) { SAPI_RAW_PLOG(ERROR, "sigaction(SIGCHLD, sa_handler=SigChldHandler)"); } else { SAPI_RAW_PLOG(ERROR, "sigaction(SIGCHLD, flags=SA_NOCLDWAIT)"); } return false; } return true; } void ForkServer::CreateInitialNamespaces() { // Spawn a new process to create initial user and mount namespaces to be used // as a base for each namespaced sandboxee. // Store uid and gid to create mappings after CLONE_NEWUSER uid_t uid = getuid(); gid_t gid = getgid(); // Socket to synchronize so that we open ns fds before process dies int fds[2]; SAPI_RAW_PCHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != -1, "creating socket"); pid_t pid = util::ForkWithFlags(CLONE_NEWUSER | CLONE_NEWNS | SIGCHLD); if (pid == -1 && errno == EPERM && IsLikelyChrooted()) { SAPI_RAW_LOG(FATAL, "failed to fork initial namespaces process: parent process is " "likely chrooted"); } SAPI_RAW_PCHECK(pid != -1, "failed to fork initial namespaces process"); char unused = '\0'; if (pid == 0) { close(fds[1]); Namespace::InitializeInitialNamespaces(uid, gid); SAPI_RAW_PCHECK(TEMP_FAILURE_RETRY(write(fds[0], &unused, 1)) == 1, "synchronizing initial namespaces creation"); SAPI_RAW_PCHECK(TEMP_FAILURE_RETRY(read(fds[0], &unused, 1)) == 1, "synchronizing initial namespaces creation"); _exit(0); } close(fds[0]); initial_userns_fd_ = open(absl::StrCat("/proc/", pid, "/ns/user").c_str(), O_RDONLY | O_CLOEXEC); SAPI_RAW_PCHECK(initial_userns_fd_ != -1, "getting initial userns fd"); initial_mntns_fd_ = open(absl::StrCat("/proc/", pid, "/ns/mnt").c_str(), O_RDONLY | O_CLOEXEC); SAPI_RAW_PCHECK(initial_mntns_fd_ != -1, "getting initial mntns fd"); SAPI_RAW_PCHECK(TEMP_FAILURE_RETRY(read(fds[1], &unused, 1)) == 1, "synchronizing initial namespaces creation"); SAPI_RAW_PCHECK(TEMP_FAILURE_RETRY(write(fds[1], &unused, 1)) == 1, "synchronizing initial namespaces creation"); close(fds[1]); } void ForkServer::SanitizeEnvironment() { // Mark all file descriptors, except the standard ones (needed // for proper sandboxed process operations), as close-on-exec. absl::Status status = sanitizer::SanitizeCurrentProcess( {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, Comms::kSandbox2ClientCommsFD}, /* close_fds = */ false); SAPI_RAW_CHECK( status.ok(), absl::StrCat("while sanitizing process: ", status.message()).c_str()); } void ForkServer::ExecuteProcess(int execve_fd, const char* const* argv, const char* const* envp) { // Do not add any code before execve(), as it's subject to seccomp policies. // Indicate that it's a special execve(), by setting 4th, 5th and 6th syscall // argument to magic values. util::Execveat(execve_fd, "", argv, envp, AT_EMPTY_PATH, internal::kExecveMagic); int saved_errno = errno; SAPI_RAW_PLOG(ERROR, "sandbox2::ForkServer: execveat failed"); if (saved_errno == ENOSYS) { SAPI_RAW_LOG(ERROR, "sandbox2::ForkServer: This is likely caused by running" " sandbox2 on too old a kernel." ); } else if (saved_errno == ENOENT && execve_fd >= 0) { // Since we know the file exists, it must be that the file is dynamically // linked and the ELF interpreter is what's actually missing. SAPI_RAW_LOG(ERROR, "sandbox2::ForkServer: This is likely caused by running" " dynamically-linked sandboxee without calling" " .AddLibrariesForBinary() on the policy builder."); } util::Syscall(__NR_exit_group, EXIT_FAILURE); abort(); } void ForkServer::InitializeNamespaces(const ForkRequest& request, uid_t uid, gid_t gid, bool avoid_pivot_root) { if (!request.has_mount_tree()) { return; } Namespace::InitializeNamespaces( uid, gid, request.clone_flags(), Mounts(request.mount_tree()), request.hostname(), avoid_pivot_root, request.allow_mount_propagation()); } } // namespace sandbox2