blob: f8580385e129050ef7836c603f51f9be0b05fa75 [file] [log] [blame]
// Copyright 2022 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
//
// 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 "./fuzztest/internal/subprocess.h"
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <variant>
#if !defined(_MSC_VER)
#include <fcntl.h>
#include <poll.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#endif // !defined(_MSC_VER)
#include <future>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include "absl/container/flat_hash_map.h"
#include "absl/functional/function_ref.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "./common/logging.h"
#if !defined(_MSC_VER)
// Needed to pass the current environment to posix_spawn, which needs an
// explicit envp without an option to inherit implicitly.
extern char** environ;
#endif
namespace fuzztest::internal {
#if !defined(_MSC_VER) && \
!(defined(__ANDROID_MIN_SDK_VERSION__) && \
__ANDROID_MIN_SDK_VERSION__ < 28) && \
!(defined(TARGET_OS_TV) && TARGET_OS_TV)
TerminationStatus::TerminationStatus(int status) : status_(status) {}
bool TerminationStatus::Exited() const { return WIFEXITED(status_); }
bool TerminationStatus::Signaled() const { return WIFSIGNALED(status_); }
std::variant<ExitCodeT, SignalT> TerminationStatus::Status() const {
if (Exited()) return static_cast<ExitCodeT>(WEXITSTATUS(status_));
FUZZTEST_CHECK(Signaled()) << "!Exited && !Signaled";
return static_cast<SignalT>(WTERMSIG(status_));
}
// Helper class for running commands in a subprocess.
class SubProcess {
public:
TerminationStatus Run(
absl::Span<const std::string> command_line,
absl::FunctionRef<void(absl::string_view)> on_stdout_output,
absl::FunctionRef<void(absl::string_view)> on_stderr_output,
absl::FunctionRef<bool()> should_stop,
const std::optional<absl::flat_hash_map<std::string, std::string>>&
environment);
private:
void CreatePipes();
void CloseChildPipes();
void CloseParentPipes();
posix_spawn_file_actions_t CreateChildFileActions();
void StartWatchdog(absl::Duration timeout);
pid_t StartChild(
absl::Span<const std::string> command_line,
const std::optional<absl::flat_hash_map<std::string, std::string>>&
environment);
void ReadChildOutput(
absl::FunctionRef<void(absl::string_view)> on_stdout_output,
absl::FunctionRef<void(absl::string_view)> on_stderr_output);
// Pipe file descriptors pairs. Index 0 is for stdout, index 1 is for stderr.
static constexpr int kStdOutIdx = 0;
static constexpr int kStdErrIdx = 1;
int parent_pipe_[2];
int child_pipe_[2];
};
// Creates parent/child pipes for piping stdout/stderr from child to parent.
void SubProcess::CreatePipes() {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
int pipe_fds[2];
FUZZTEST_PCHECK(pipe(pipe_fds) == 0) << "Cannot create pipe";
parent_pipe_[channel] = pipe_fds[0];
child_pipe_[channel] = pipe_fds[1];
FUZZTEST_PCHECK(fcntl(parent_pipe_[channel], F_SETFL, O_NONBLOCK) != -1)
<< "Cannot make pipe non-blocking";
}
}
void SubProcess::CloseChildPipes() {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
FUZZTEST_PCHECK(close(child_pipe_[channel]) != -1) << "Cannot close pipe";
}
}
void SubProcess::CloseParentPipes() {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
FUZZTEST_PCHECK(close(parent_pipe_[channel]) != -1) << "Cannot close pipe";
}
}
// Create file actions, which specify file-related actions to be performed in
// the child between the fork() and exec() steps.
posix_spawn_file_actions_t SubProcess::CreateChildFileActions() {
posix_spawn_file_actions_t actions;
int err;
err = posix_spawn_file_actions_init(&actions);
FUZZTEST_CHECK(err == 0) << "Cannot initialize file actions: "
<< strerror(err);
// Close stdin.
err = posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);
FUZZTEST_CHECK(err == 0) << "Cannot add close() action: " << strerror(err);
for (int channel : {kStdOutIdx, kStdErrIdx}) {
// Close parent-side pipes.
err = posix_spawn_file_actions_addclose(&actions, parent_pipe_[channel]);
FUZZTEST_CHECK(err == 0) << "Cannot add close() action: " << strerror(err);
// Replace stdout/stderr file descriptors with the pipes.
int fd = channel == kStdOutIdx ? STDOUT_FILENO : STDERR_FILENO;
err = posix_spawn_file_actions_adddup2(&actions, child_pipe_[channel], fd);
FUZZTEST_CHECK(err == 0) << "Cannot add dup2() action: " << strerror(err);
err = posix_spawn_file_actions_addclose(&actions, child_pipe_[channel]);
FUZZTEST_CHECK(err == 0) << "Cannot add close() action: " << strerror(err);
}
return actions;
}
// Do fork() and exec() in one step, using posix_spawnp().
pid_t SubProcess::StartChild(
absl::Span<const std::string> command_line,
const std::optional<absl::flat_hash_map<std::string, std::string>>&
environment) {
posix_spawn_file_actions_t actions = CreateChildFileActions();
// Create `argv` and `envp` parameters for exec().
size_t argc = command_line.size();
std::vector<char*> argv(argc + 1);
for (int i = 0; i < argc; i++) {
argv[i] = strndup(command_line[i].data(), command_line[i].size());
}
argv[argc] = nullptr;
std::vector<char*> envp;
if (environment.has_value()) {
size_t envc = environment->size();
envp.resize(envc + 1);
int i = 0;
for (const auto& [key, value] : *environment) {
envp[i++] = strdup(absl::StrCat(key, "=", value).c_str());
}
envp[envc] = nullptr;
}
pid_t child_pid;
int err;
err = posix_spawnp(&child_pid, argv[0], &actions, nullptr, argv.data(),
environment.has_value() ? envp.data() : environ);
FUZZTEST_PCHECK(err == 0) << "Cannot spawn child process.";
// Free up the used parameters.
for (char* p : argv) free(p);
for (char* p : envp) free(p);
err = posix_spawn_file_actions_destroy(&actions);
FUZZTEST_PCHECK(err == 0) << "Cannot destroy file actions.";
return child_pid;
}
static bool ShouldRetry(int e) {
return ((e == EINTR) || (e == EAGAIN) || (e == EWOULDBLOCK));
}
void SubProcess::ReadChildOutput(
absl::FunctionRef<void(absl::string_view)> on_stdout_output,
absl::FunctionRef<void(absl::string_view)> on_stderr_output) {
// Set up poll()-ing the pipes.
constexpr int fd_count = 2;
struct pollfd pfd[fd_count];
for (int channel : {kStdOutIdx, kStdErrIdx}) {
pfd[channel].fd = parent_pipe_[channel];
pfd[channel].events = POLLIN;
pfd[channel].revents = 0;
}
// Loop reading stdout and stderr from the child process.
int fd_remain = fd_count;
char buf[4096];
while (fd_remain > 0) {
int ret = poll(pfd, fd_count, -1);
if ((ret == -1) && !ShouldRetry(errno)) {
FUZZTEST_PLOG(FATAL) << "Cannot poll()";
} else if (ret == 0) {
FUZZTEST_PLOG(FATAL) << "Impossible timeout";
} else if (ret > 0) {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
// According to the poll() spec, use -1 for ignored entries.
if (pfd[channel].fd == -1) {
continue;
}
if ((pfd[channel].revents & (POLLIN | POLLHUP)) != 0) {
ssize_t n = read(pfd[channel].fd, buf, sizeof(buf));
if (n > 0) {
auto on_output =
channel == kStdOutIdx ? on_stdout_output : on_stderr_output;
on_output({buf, static_cast<size_t>(n)});
} else if ((n == 0) || !ShouldRetry(errno)) {
pfd[channel].fd = -1;
fd_remain--;
}
} else if ((pfd[channel].revents & (POLLERR | POLLNVAL)) != 0) {
pfd[channel].fd = -1;
fd_remain--;
}
}
}
}
}
namespace {
int Wait(pid_t pid) {
int status;
while (true) {
pid_t ret = waitpid(pid, &status, 0);
if (ret == -1 && ShouldRetry(errno)) {
continue;
} else if (ret == pid && (WIFEXITED(status) || WIFSIGNALED(status))) {
return status;
} else {
FUZZTEST_PLOG(FATAL) << "wait() error";
}
}
}
// TODO(lszekeres): Consider optimizing this further by eliminating polling.
// Could potentially be done using pselect() to wait for SIGCHLD with a timeout.
// I.e., by setting all args to null, except timeout with a sigmask for SIGCHLD.
int WaitWithStopChecker(pid_t pid, absl::FunctionRef<bool()> should_stop) {
int status;
constexpr absl::Duration sleep_duration = absl::Milliseconds(100);
while (true) {
pid_t ret = waitpid(pid, &status, WNOHANG);
if (ret == -1 && ShouldRetry(errno)) {
continue;
} else if (ret == 0) { // Still running.
if (should_stop()) {
FUZZTEST_PCHECK(kill(pid, SIGTERM) == 0) << "Cannot kill()";
return Wait(pid);
} else {
absl::SleepFor(sleep_duration);
continue;
}
} else if (ret == pid && (WIFEXITED(status) || WIFSIGNALED(status))) {
return status;
} else {
FUZZTEST_PLOG(FATAL) << "wait() error.";
}
}
}
} // anonymous namespace
TerminationStatus SubProcess::Run(
absl::Span<const std::string> command_line,
absl::FunctionRef<void(absl::string_view)> on_stdout_output,
absl::FunctionRef<void(absl::string_view)> on_stderr_output,
absl::FunctionRef<bool()> should_stop,
const std::optional<absl::flat_hash_map<std::string, std::string>>&
environment) {
CreatePipes();
pid_t child_pid = StartChild(command_line, environment);
CloseChildPipes();
std::future<int> status = std::async(std::launch::async, &WaitWithStopChecker,
child_pid, should_stop);
ReadChildOutput(on_stdout_output, on_stderr_output);
CloseParentPipes();
return TerminationStatus(status.get());
}
#endif // !defined(_MSC_VER) && !(defined(__ANDROID_MIN_SDK_VERSION__) &&
// __ANDROID_MIN_SDK_VERSION__ < 28)
TerminationStatus RunCommandWithCallbacks(
absl::Span<const std::string> command_line,
absl::FunctionRef<void(absl::string_view)> on_stdout_output,
absl::FunctionRef<void(absl::string_view)> on_stderr_output,
absl::FunctionRef<bool()> should_stop,
const std::optional<absl::flat_hash_map<std::string, std::string>>&
environment) {
#if defined(_MSC_VER)
FUZZTEST_LOG(FATAL) << "Subprocess library not implemented on Windows yet.";
#elif defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 28
FUZZTEST_LOG(FATAL)
<< "Subprocess library not implemented on older Android NDK versions yet";
#elif defined(TARGET_OS_TV) && TARGET_OS_TV
FUZZTEST_LOG(FATAL) << "Subprocess library not implemented on Apple tvOS yet";
#else
SubProcess proc;
return proc.Run(command_line, on_stdout_output, on_stderr_output, should_stop,
environment);
#endif
}
RunResults RunCommand(
absl::Span<const std::string> command_line,
const std::optional<absl::flat_hash_map<std::string, std::string>>&
environment,
absl::Duration timeout) {
const absl::Time wait_until = absl::Now() + timeout;
std::string stdout_str;
std::string stderr_str;
auto status = RunCommandWithCallbacks(
command_line,
[&stdout_str](absl::string_view output) { stdout_str.append(output); },
[&stderr_str](absl::string_view output) { stderr_str.append(output); },
[wait_until]() { return absl::Now() > wait_until; }, environment);
return {std::move(status), std::move(stdout_str), std::move(stderr_str)};
}
} // namespace fuzztest::internal