blob: 200dd995fc40930e46d36c7f528d525ba04b4743 [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 <cstddef>
#include <cstdlib>
#include <cstring>
#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 <string>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/strings/str_cat.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "./fuzztest/internal/logging.h"
namespace fuzztest::internal {
TerminationStatus::TerminationStatus(int status) : status_(status) {}
bool TerminationStatus::Exited() const { return WIFEXITED(status_); }
bool TerminationStatus::Signaled() const { return WIFSIGNALED(status_); }
int TerminationStatus::ExitCode() const {
FUZZTEST_INTERNAL_CHECK(Exited(), "Can only get ExitCode() if Exited().");
return WEXITSTATUS(status_);
}
int TerminationStatus::Signal() const {
FUZZTEST_INTERNAL_CHECK(Signaled(), "Can only get Signal() if Signaled().");
return WTERMSIG(status_);
}
#if !defined(_MSC_VER)
// Helper class for running commands in a subprocess.
class SubProcess {
public:
RunResults Run(
const std::vector<std::string>& command_line,
const absl::flat_hash_map<std::string, std::string>& environment,
absl::Duration timeout);
private:
void CreatePipes();
void CloseChildPipes();
void CloseParentPipes();
posix_spawn_file_actions_t CreateChildFileActions();
void StartWatchdog(absl::Duration timeout);
pid_t StartChild(
const std::vector<std::string>& command_line,
const absl::flat_hash_map<std::string, std::string>& environment);
void ReadChildOutput(std::string* stdout_output, std::string* 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_INTERNAL_CHECK(pipe(pipe_fds) == 0,
"Cannot create pipe: ", strerror(errno));
parent_pipe_[channel] = pipe_fds[0];
child_pipe_[channel] = pipe_fds[1];
FUZZTEST_INTERNAL_CHECK(
fcntl(parent_pipe_[channel], F_SETFL, O_NONBLOCK) != -1,
"Cannot make pipe non-blocking: ", strerror(errno));
}
}
void SubProcess::CloseChildPipes() {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
FUZZTEST_INTERNAL_CHECK(close(child_pipe_[channel]) != -1,
"Cannot close pipe: ", strerror(errno));
}
}
void SubProcess::CloseParentPipes() {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
FUZZTEST_INTERNAL_CHECK(close(parent_pipe_[channel]) != -1,
"Cannot close pipe: ", strerror(errno));
}
}
// 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_INTERNAL_CHECK(err == 0,
"Cannot initialize file actions: ", strerror(err));
// Close stdin.
err = posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);
FUZZTEST_INTERNAL_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_INTERNAL_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_INTERNAL_CHECK(err == 0,
"Cannot add dup2() action: ", strerror(err));
err = posix_spawn_file_actions_addclose(&actions, child_pipe_[channel]);
FUZZTEST_INTERNAL_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(
const std::vector<std::string>& command_line,
const 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] = strdup(command_line[i].c_str());
}
argv[argc] = nullptr;
size_t envc = environment.size();
std::vector<char*> envp(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(),
envp.data());
FUZZTEST_INTERNAL_CHECK(err == 0,
"Cannot spawn child process: ", strerror(err));
// 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_INTERNAL_CHECK(err == 0,
"Cannot destroy file actions: ", strerror(err));
return child_pid;
}
static bool ShouldRetry(int e) {
return ((e == EINTR) || (e == EAGAIN) || (e == EWOULDBLOCK));
}
void SubProcess::ReadChildOutput(std::string* stdout_output,
std::string* stderr_output) {
// Set up poll()-ing the pipes.
constexpr int fd_count = 2;
struct pollfd pfd[fd_count];
std::string* out_str[fd_count];
for (int channel : {kStdOutIdx, kStdErrIdx}) {
pfd[channel].fd = parent_pipe_[channel];
pfd[channel].events = POLLIN;
pfd[channel].revents = 0;
out_str[channel] = channel == kStdOutIdx ? stdout_output : stderr_output;
}
// 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_INTERNAL_CHECK(false, "Cannot poll(): ", strerror(errno));
} else if (ret == 0) {
FUZZTEST_INTERNAL_CHECK(false, "Impossible timeout: ", strerror(errno));
} else if (ret > 0) {
for (int channel : {kStdOutIdx, kStdErrIdx}) {
if ((pfd[channel].revents & (POLLIN | POLLHUP)) != 0) {
ssize_t n = read(pfd[channel].fd, buf, sizeof(buf));
if (n > 0) {
out_str[channel]->append(buf, n);
} else if ((n == 0) || !ShouldRetry(errno)) {
fd_remain--;
}
} else if ((pfd[channel].revents & (POLLERR | POLLNVAL)) != 0) {
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_INTERNAL_CHECK(false, "wait() error: ", strerror(errno));
}
}
}
// 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 WaitWithTimeout(pid_t pid, absl::Duration timeout) {
int status;
const absl::Time wait_until = absl::Now() + timeout;
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 (absl::Now() > wait_until) {
FUZZTEST_INTERNAL_CHECK(kill(pid, SIGTERM) == 0,
"Cannot kill(): ", strerror(errno));
return Wait(pid);
} else {
absl::SleepFor(sleep_duration);
continue;
}
} else if (ret == pid && (WIFEXITED(status) || WIFSIGNALED(status))) {
return status;
} else {
FUZZTEST_INTERNAL_CHECK(false, "wait() error: ", strerror(errno));
}
}
}
} // anonymous namespace
RunResults SubProcess::Run(
const std::vector<std::string>& command_line,
const absl::flat_hash_map<std::string, std::string>& environment,
absl::Duration timeout) {
CreatePipes();
pid_t child_pid = StartChild(command_line, environment);
CloseChildPipes();
std::future<int> status =
std::async(std::launch::async, &WaitWithTimeout, child_pid, timeout);
std::string stdout_output, stderr_output;
ReadChildOutput(&stdout_output, &stderr_output);
CloseParentPipes();
return {TerminationStatus(status.get()), stdout_output, stderr_output};
}
#endif // !defined(_MSC_VER)
RunResults RunCommand(
const std::vector<std::string>& command_line,
const absl::flat_hash_map<std::string, std::string>& environment,
absl::Duration timeout) {
#if defined(_MSC_VER)
FUZZTEST_INTERNAL_CHECK(false,
"Subprocess library not implemented on Windows yet.");
#else
SubProcess proc;
return proc.Run(command_line, environment, timeout);
#endif
}
} // namespace fuzztest::internal