blob: ed66e717ae5489be2f46ea00a88d71449655de9e [file]
// Copyright 2022 The Centipede Authors.
//
// 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.
#include "./centipede/command.h"
#ifndef _WIN32
#include <errno.h>
#include <fcntl.h>
#include <spawn.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif // _WIN32
#ifdef __APPLE__
#include <inttypes.h>
#include <libproc.h>
#endif // __APPLE__
#include <algorithm>
#include <atomic>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <filesystem> // NOLINT
#include <fstream>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <system_error> // NOLINT
#include <utility>
#include <vector>
#include "absl/base/const_init.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/str_split.h"
#include "absl/synchronization/mutex.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "./centipede/stop.h"
#include "./centipede/util.h"
#include "./common/logging.h"
#include "./fuzztest/internal/escaping.h"
#ifdef _WIN32
#include "absl/cleanup/cleanup.h"
#include "absl/container/btree_map.h"
#include "absl/strings/ascii.h" // NOLINT
#include "./common/windows_includes.h"
#endif
#ifndef _WIN32
// 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 {
namespace {
// See the definition of --fork_server flag.
constexpr std::string_view kCommandLineSeparator(" \\\n");
constexpr std::string_view kNoForkServerRequestPrefix("%f");
#ifdef _WIN32
// Do not define `GetProcessCreationStamp`, which is for fork servers.
#else
absl::StatusOr<std::string> GetProcessCreationStamp(pid_t pid) {
#if defined(__APPLE__)
struct proc_bsdinfo info = {};
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, PROC_PIDTBSDINFO_SIZE) !=
PROC_PIDTBSDINFO_SIZE) {
return absl::InternalError(
absl::StrCat("failed to get proc bsdinfo for ", pid));
}
return absl::StrFormat("%" PRIu64 ".%06" PRIu64, info.pbi_start_tvsec,
info.pbi_start_tvusec);
#else
constexpr int kFieldIndexOfStartTimeAfterComm = 19; // From `man procfs`
const std::string proc_stat_path = absl::StrFormat("/proc/%d/stat", pid);
std::string proc_stat_line;
// Cannot use `ReadFromLocalFile` on procfs since seek does not work.
// This seems to work assuming the filename of the command does not contain
// newline, which should be in our control when the process is ours.
if (std::getline(std::ifstream(proc_stat_path), proc_stat_line).bad()) {
return absl::InternalError(absl::StrCat("failed to read ", proc_stat_path));
}
// According to the current format of `/proc/[pid]/stat`, only the comm field
// can contain ')'.
const size_t comm_end_pos = proc_stat_line.find_last_of(')');
if (comm_end_pos == proc_stat_line.npos) {
return absl::NotFoundError(
absl::StrCat("cannot find the end of command in the first line of ",
proc_stat_path, ": ", proc_stat_line));
}
std::string_view proc_stat_after_comm =
std::string_view(proc_stat_line).substr(comm_end_pos + 1);
const std::vector<std::string_view> fields =
absl::StrSplit(proc_stat_after_comm, ' ', absl::SkipEmpty());
if (fields.size() <= kFieldIndexOfStartTimeAfterComm) {
return absl::NotFoundError(
absl::StrCat("not enough fields in the first line of ", proc_stat_path,
": ", proc_stat_line));
}
return std::string(fields[kFieldIndexOfStartTimeAfterComm]);
#endif // __APPLE__
}
#endif // _WIN32
std::string GetUniqueSuffix() {
static std::atomic<uint64_t> suffix_counter = {0};
return absl::StrCat(suffix_counter++);
}
} // namespace
// TODO(ussuri): Encapsulate as much of the fork server functionality from
// this source as possible in this struct, and make it a class.
#ifdef _WIN32
struct Command::PlatformContext {
HANDLE win_process_handle = INVALID_HANDLE_VALUE;
~PlatformContext() {
if (win_process_handle != INVALID_HANDLE_VALUE) {
CloseHandle(win_process_handle);
}
}
};
#else
struct ForkServerProps {
// The file paths of the comms pipes.
std::string fifo_path_[2];
// The file descriptors of the comms pipes.
int pipe_[2] = {-1, -1};
// The file path to write the PID of the fork server process to.
std::string pid_file_path_;
// The PID of the fork server process. Used to verify that the fork server is
// running and the pipes are ready for comms.
pid_t pid_ = -1;
// The creation stamp of the fork server process. Used to detect that the
// running process with `pid_` is still the original fork server, not a PID
// recycled by the OS.
std::string creation_stamp;
template <typename T>
bool ReadPipe(absl::Time deadline, T& out) {
struct pollfd poll_fd = {};
int poll_ret = -1;
do {
poll_fd = {
/*fd=*/pipe_[1],
/*events=*/POLLIN,
};
poll_ret = poll(&poll_fd, 1, PollTimeoutMs(deadline - absl::Now()));
// The `poll()` syscall can get interrupted: it sets errno==EINTR in that
// case. We should tolerate that.
} while (poll_ret < 0 && errno == EINTR);
if (poll_ret == 1 && (poll_fd.revents & POLLIN)) {
FUZZTEST_CHECK_EQ(sizeof(T), read(pipe_[1], &out, sizeof(T)));
return true;
} else if (poll_ret == 0) {
FUZZTEST_LOG(ERROR) << "poll timed out on fork server pipe";
} else {
FUZZTEST_PLOG(ERROR) << "poll on fork server pipe returned " << poll_ret;
}
return false;
}
~ForkServerProps() {
for (int i = 0; i < 2; ++i) {
if (pipe_[i] >= 0 && close(pipe_[i]) != 0) {
FUZZTEST_LOG(ERROR)
<< "Failed to close fork server pipe for " << fifo_path_[i];
}
std::error_code ec;
if (!fifo_path_[i].empty() &&
!std::filesystem::remove(fifo_path_[i], ec)) {
FUZZTEST_LOG(ERROR) << "Failed to remove fork server pipe file "
<< fifo_path_[i] << ": " << ec;
}
}
}
};
struct Command::PlatformContext {
pid_t pid = -1;
std::unique_ptr<ForkServerProps> fork_server;
};
#endif // _WIN32
// NOTE: Because std::unique_ptr<T> requires T to be a complete type wherever
// the deleter is instantiated, the special member functions must be defined
// out-of-line here, now that PlatformContext is complete (that's by-the-book
// PIMPL).
Command::~Command() {
if (is_executing()) {
FUZZTEST_LOG(WARNING) << "Destructing Command object for " << path()
<< " with "
#ifdef _WIN32
<< GetProcessId(platform_context_->win_process_handle)
#else
<< (platform_context_->fork_server
? absl::StrCat(
"fork server PID ",
platform_context_->fork_server->pid_)
: absl::StrCat("PID ",
platform_context_->pid))
#endif
<< " still running. Requesting it to force-stop "
"without waiting for it...";
RequestStop(/*force=*/true);
}
ResetRedirectionFiles(/*new_suffix=*/"");
}
Command::Command(std::string_view path, Options options)
: path_(path),
options_(std::move(options)),
platform_context_(std::make_unique<PlatformContext>()) {}
Command::Command(std::string_view path) : Command{path, {}} {}
std::string Command::ToString() const {
#ifdef _WIN32
std::string path = path_;
if (absl::StartsWith(path, kNoForkServerRequestPrefix)) {
path = path.substr(kNoForkServerRequestPrefix.size());
}
constexpr std::string_view kTempFileWildCard = "@@";
if (absl::StrContains(path, kTempFileWildCard)) {
FUZZTEST_CHECK(!options_.temp_file_path.empty());
std::string temp_file = options_.temp_file_path;
path = absl::StrReplaceAll(path, {{kTempFileWildCard, temp_file}});
}
std::string binary_cmd = path;
auto Escape = [](std::string_view s) {
std::string r = "\"";
size_t num_bs = 0;
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '"') {
r.append(num_bs + 1, '\\');
num_bs = 0;
} else if (s[i] == '\\') {
++num_bs;
} else {
num_bs = 0;
}
r += s[i];
}
r.append(num_bs, '\\');
r += '"';
return r;
};
for (const auto& arg : options_.args) {
absl::StrAppend(&binary_cmd, " ", Escape(arg));
}
return binary_cmd;
#else // _WIN32
std::vector<std::string> ss;
ss.reserve(/*env*/ 1 + options_.env_diff.size() + /*path*/ 1 +
/*args*/ options_.args.size() + /*in/out/err*/ 3);
// env.
ss.push_back("exec env");
std::vector<std::string> env_to_set;
env_to_set.reserve(options_.env_diff.size());
// Arguments that unset environment variables must appear first.
for (std::string_view var : options_.env_diff) {
if (absl::StartsWith(var, "-")) {
ss.push_back(absl::StrCat("-u ", var.substr(1)));
} else {
env_to_set.emplace_back(var);
}
}
for (auto& var : env_to_set) {
ss.push_back(ShellEscape(var));
}
// path.
std::string path = path_;
// Strip the % prefixes, if any.
if (absl::StartsWith(path, kNoForkServerRequestPrefix)) {
path = path.substr(kNoForkServerRequestPrefix.size());
}
// Replace @@ with temp_file_path_.
constexpr std::string_view kTempFileWildCard = "@@";
if (absl::StrContains(path, kTempFileWildCard)) {
FUZZTEST_CHECK(!options_.temp_file_path.empty());
path = absl::StrReplaceAll(path,
{{kTempFileWildCard, options_.temp_file_path}});
}
ss.push_back(std::move(path));
// args.
for (const auto& arg : options_.args) {
ss.push_back(ShellEscape(arg));
}
// in/out/err.
if (!options_.stdin_file_path.empty()) {
ss.push_back(absl::StrCat("< ", ShellEscape(options_.stdin_file_path)));
}
if (!stdout_file_.empty()) {
ss.push_back(absl::StrCat("> ", ShellEscape(stdout_file_)));
}
if (!stderr_file_.empty()) {
if (stdout_file_ != stderr_file_) {
ss.push_back(absl::StrCat("2> ", ShellEscape(stderr_file_)));
} else {
ss.push_back("2>&1");
}
}
// Trim trailing space and return.
return absl::StrJoin(ss, kCommandLineSeparator);
#endif // _WIN32
}
bool Command::StartForkServer(std::string_view temp_dir_path,
std::string_view prefix) {
#ifdef _WIN32
return false;
#else
if (absl::StartsWith(path_, kNoForkServerRequestPrefix)) {
FUZZTEST_VLOG(2) << "Fork server disabled for " << path();
return false;
}
FUZZTEST_CHECK(!is_executing_ && !platform_context_->fork_server);
FUZZTEST_VLOG(2) << "Starting fork server for " << path();
ResetRedirectionFiles(GetUniqueSuffix());
command_line_ = ToString();
platform_context_->fork_server = std::make_unique<ForkServerProps>();
platform_context_->fork_server->fifo_path_[0] =
std::filesystem::path(temp_dir_path)
.append(absl::StrCat(prefix, "_FIFO0"));
platform_context_->fork_server->fifo_path_[1] =
std::filesystem::path(temp_dir_path)
.append(absl::StrCat(prefix, "_FIFO1"));
const std::string pid_file_path =
std::filesystem::path(temp_dir_path).append("pid");
(void)std::filesystem::create_directory(temp_dir_path); // it may not exist.
for (int i = 0; i < 2; ++i) {
FUZZTEST_PCHECK(
mkfifo(platform_context_->fork_server->fifo_path_[i].c_str(), 0600) ==
0)
<< VV(i) << VV(platform_context_->fork_server->fifo_path_[i]);
}
// NOTE: A background process does not return its exit status to the subshell,
// so failures will never propagate to the caller of `system()`. Instead, we
// save out the background process's PID to a file and use it later to assert
// that the process has started and is still running.
static constexpr std::string_view kForkServerCommandStub = R"sh(
{
CENTIPEDE_FORK_SERVER_FIFO0="%s" \
CENTIPEDE_FORK_SERVER_FIFO1="%s" \
%s
} &
printf "%%s" $! > "%s"
)sh";
const std::string fork_server_command = absl::StrFormat(
kForkServerCommandStub, platform_context_->fork_server->fifo_path_[0],
platform_context_->fork_server->fifo_path_[1], command_line_,
pid_file_path);
FUZZTEST_VLOG(1) << "Fork server command:" << fork_server_command;
const int exit_code = system(fork_server_command.c_str());
// Check if `system()` was able to parse and run the command at all.
if (exit_code != EXIT_SUCCESS) {
LogProblemInfo(
"Failed to parse or run command to launch fork server; will proceed "
"without it");
return false;
}
// The fork server is probably running now. However, one failure scenario is
// that it starts and exits early. Try opening the read/write comms pipes with
// it: if that fails, something is wrong.
// We use non-blocking I/O to open the pipes. That is good and safe, because:
// 1) This prevents the `open()` calls from hanging when the fork server fails
// to open the pipes on its side (note the use of O_RDWR, not O_WRONLY, to
// avoid ENXIO).
// 2) In `Command::Execute`, we wait for the return channel pipe with a
// `poll()`, so it should always have data when we attempt to `read()` from
// it.
// See more at
// https://www.gnu.org/software/libc/manual/html_node/Operating-Modes.html.
if ((platform_context_->fork_server->pipe_[0] =
open(platform_context_->fork_server->fifo_path_[0].c_str(),
O_RDWR | O_NONBLOCK)) < 0 ||
(platform_context_->fork_server->pipe_[1] =
open(platform_context_->fork_server->fifo_path_[1].c_str(),
O_RDONLY | O_NONBLOCK)) < 0) {
LogProblemInfo(
"Failed to establish communication with fork server; will proceed "
"without it");
return false;
}
std::string pid_str;
ReadFromLocalFile(pid_file_path, pid_str);
FUZZTEST_CHECK(
absl::SimpleAtoi(pid_str, &platform_context_->fork_server->pid_))
<< VV(pid_str);
auto creation_stamp =
GetProcessCreationStamp(platform_context_->fork_server->pid_);
if (!creation_stamp.ok()) {
LogProblemInfo(
absl::StrCat("Failed to get the fork server's creation stamp; will "
"proceed without it "
"(failure status: ",
creation_stamp.status(), ")"));
return false;
}
platform_context_->fork_server->creation_stamp = *std::move(creation_stamp);
return true;
#endif // _WIN32
}
void Command::ResetRedirectionFiles(std::string_view new_suffix) {
if (!stdout_file_.empty()) {
std::error_code ec;
(void)std::filesystem::remove(stdout_file_, ec);
}
if (!stderr_file_.empty() && stderr_file_ != stdout_file_) {
std::error_code ec;
(void)std::filesystem::remove(stderr_file_, ec);
}
if (!options_.stdout_file_prefix.empty() && !new_suffix.empty()) {
stdout_file_ = absl::StrCat(options_.stdout_file_prefix, new_suffix);
} else {
stdout_file_.clear();
}
if (!options_.stderr_file_prefix.empty() && !new_suffix.empty()) {
stderr_file_ = absl::StrCat(options_.stderr_file_prefix, new_suffix);
} else {
stderr_file_.clear();
}
}
absl::Status Command::VerifyForkServerIsHealthy() {
#ifdef _WIN32
return absl::UnimplementedError("Fork server not supported on Windows");
#else
// Preconditions: the callers (`Execute()`) should call us only when the fork
// server is presumed to be running (`fork_server_pid_` >= 0). If it is, the
// comms pipes are guaranteed to be opened by `StartForkServer()`.
FUZZTEST_CHECK(platform_context_->fork_server != nullptr)
<< "Fork server wasn't started";
FUZZTEST_CHECK(platform_context_->fork_server->pid_ >= 0)
<< "Fork server process failed to start";
FUZZTEST_CHECK(platform_context_->fork_server->pipe_[0] >= 0 &&
platform_context_->fork_server->pipe_[1] >= 0)
<< "Failed to connect to fork server";
// A process with the fork server PID exists (_some_ process, possibly with a
// recycled PID)...
if (kill(platform_context_->fork_server->pid_, 0) != EXIT_SUCCESS) {
return absl::UnknownError(
absl::StrCat("Can't communicate with fork server, PID=",
platform_context_->fork_server->pid_));
}
// ...and it is a process has the same creation stamp, so it's practically
// guaranteed to be our original fork server process.
const auto creation_stamp =
GetProcessCreationStamp(platform_context_->fork_server->pid_);
if (!creation_stamp.ok()) return creation_stamp.status();
if (*creation_stamp != platform_context_->fork_server->creation_stamp) {
return absl::UnknownError(absl::StrCat(
"Fork server's creation stamp changed (new process?) - expected ",
platform_context_->fork_server->creation_stamp, ", but got ",
*creation_stamp));
}
return absl::OkStatus();
#endif // _WIN32
}
bool Command::ExecuteAsync() {
FUZZTEST_CHECK(!is_executing());
#ifdef _WIN32
FUZZTEST_CHECK_EQ(platform_context_->win_process_handle,
INVALID_HANDLE_VALUE);
ResetRedirectionFiles(GetUniqueSuffix());
command_line_ = ToString();
struct CaseInsensitiveCompare {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const {
return std::lexicographical_compare(
a.begin(), a.end(), b.begin(), b.end(),
[](unsigned char ca, unsigned char cb) {
return static_cast<unsigned char>(absl::ascii_tolower(ca)) <
static_cast<unsigned char>(absl::ascii_tolower(cb));
});
}
};
absl::btree_map<std::string, std::string, CaseInsensitiveCompare> env_map;
LPCH env_strings = GetEnvironmentStringsA();
if (env_strings != nullptr) {
const char* ptr = env_strings;
while (*ptr != '\0') {
std::string_view entry(ptr);
ptr += entry.size() + 1;
size_t eq_pos = entry.find('=', 1);
if (eq_pos != entry.npos) {
env_map[entry.substr(0, eq_pos)] =
std::string(entry.substr(eq_pos + 1));
} else {
env_map[entry] = "";
}
}
FreeEnvironmentStringsA(env_strings);
}
for (std::string_view env_var : options_.env_diff) {
if (absl::StartsWith(env_var, "-")) {
std::string_view key = env_var.substr(1);
if (absl::EndsWith(key, "=")) {
key = key.substr(0, key.size() - 1);
}
env_map.erase(key);
} else {
auto pos = env_var.find('=');
if (pos != env_var.npos) {
env_map[env_var.substr(0, pos)] = std::string(env_var.substr(pos + 1));
}
}
}
std::string env_block = absl::StrJoin(env_map, absl::string_view("\0", 1),
absl::PairFormatter("="));
env_block.append(2, '\0');
STARTUPINFOEXA si = {};
si.StartupInfo.cb = sizeof(si);
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
PROCESS_INFORMATION pi = {};
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
HANDLE handle_in = INVALID_HANDLE_VALUE;
HANDLE handle_out = INVALID_HANDLE_VALUE;
HANDLE handle_err = INVALID_HANDLE_VALUE;
absl::Cleanup close_handles = [&] {
if (handle_in != INVALID_HANDLE_VALUE) CloseHandle(handle_in);
if (handle_out != INVALID_HANDLE_VALUE) CloseHandle(handle_out);
if (handle_err != INVALID_HANDLE_VALUE) CloseHandle(handle_err);
};
if (!options_.stdin_file_path.empty()) {
handle_in = CreateFileA(options_.stdin_file_path.c_str(), FILE_READ_DATA,
/*dwShareMode=*/0, &sa, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (handle_in == INVALID_HANDLE_VALUE) {
FUZZTEST_LOG(ERROR) << "Failed to open stdin file: "
<< options_.stdin_file_path;
return false;
}
si.StartupInfo.hStdInput = handle_in;
} else {
si.StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
}
if (!stdout_file_.empty()) {
handle_out =
CreateFileA(stdout_file_.c_str(), GENERIC_WRITE, FILE_SHARE_READ, &sa,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle_out == INVALID_HANDLE_VALUE) {
FUZZTEST_LOG(ERROR) << "Failed to open stdout file: " << stdout_file_;
return false;
}
si.StartupInfo.hStdOutput = handle_out;
} else {
si.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
}
if (!stderr_file_.empty()) {
if (stderr_file_ == stdout_file_) {
si.StartupInfo.hStdError = si.StartupInfo.hStdOutput;
} else {
handle_err =
CreateFileA(stderr_file_.c_str(), GENERIC_WRITE, FILE_SHARE_READ, &sa,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle_err == INVALID_HANDLE_VALUE) {
FUZZTEST_LOG(ERROR) << "Failed to open stderr file: " << stderr_file_;
return false;
}
si.StartupInfo.hStdError = handle_err;
}
} else {
si.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
}
std::vector<HANDLE> handles_to_inherit;
for (HANDLE h : {si.StartupInfo.hStdInput, si.StartupInfo.hStdOutput,
si.StartupInfo.hStdError}) {
if (h != nullptr && h != INVALID_HANDLE_VALUE &&
std::find(handles_to_inherit.begin(), handles_to_inherit.end(), h) ==
handles_to_inherit.end()) {
SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
handles_to_inherit.push_back(h);
}
}
SIZE_T attr_list_size = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &attr_list_size);
auto attr_list_buffer = std::make_unique<uint8_t[]>(attr_list_size);
si.lpAttributeList =
reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attr_list_buffer.get());
if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0,
&attr_list_size)) {
DWORD err = GetLastError();
FUZZTEST_LOG(ERROR) << "InitializeProcThreadAttributeList failed: " << err;
return false;
}
absl::Cleanup delete_attr_list = [&] {
DeleteProcThreadAttributeList(si.lpAttributeList);
};
if (!handles_to_inherit.empty()) {
if (!UpdateProcThreadAttribute(
si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handles_to_inherit.data(),
handles_to_inherit.size() * sizeof(HANDLE), nullptr, nullptr)) {
DWORD err = GetLastError();
FUZZTEST_LOG(ERROR) << "UpdateProcThreadAttribute failed: " << err;
return false;
}
}
std::string cmd = command_line_;
const BOOL cp_res = CreateProcessA(
NULL, cmd.data(), NULL, NULL,
/*bInheritHandles=*/!handles_to_inherit.empty(),
EXTENDED_STARTUPINFO_PRESENT, env_block.empty() ? NULL : env_block.data(),
NULL, &si.StartupInfo, &pi);
if (!cp_res) {
DWORD err = GetLastError();
FUZZTEST_LOG(ERROR) << "CreateProcessA failed for '" << cmd
<< "': error=" << err;
return false;
}
platform_context_->win_process_handle = pi.hProcess;
CloseHandle(pi.hThread);
is_executing_ = true;
return true;
#else // _WIN32
if (platform_context_->fork_server != nullptr) {
FUZZTEST_VLOG(1) << "Sending execution request to fork server";
if (const auto status = VerifyForkServerIsHealthy(); !status.ok()) {
LogProblemInfo(absl::StrCat("Fork server should be running, but isn't: ",
status.message()));
return false;
}
// Wake up the fork server.
char x = ' ';
if (write(platform_context_->fork_server->pipe_[0], &x, 1) != 1) {
LogProblemInfo(
absl::StrCat("Failed to write to fork server pipe. Errno: ", errno));
return false;
}
// Read the one-byte ack.
// Use 60s as an arbitrary duration to wait for the process to load and
// enter the fork server.
if (!platform_context_->fork_server->ReadPipe(
absl::Now() + absl::Seconds(60), x)) {
LogProblemInfo("Failed to read from fork server pipe.");
return false;
}
} else {
FUZZTEST_CHECK_EQ(platform_context_->pid, -1);
ResetRedirectionFiles(GetUniqueSuffix());
command_line_ = ToString();
FUZZTEST_VLOG(1) << "Executing command '" << command_line_ << "'...";
std::vector<std::string> argv_strs = {"/bin/sh", "-c", command_line_};
std::vector<char*> argv;
argv.reserve(argv_strs.size() + 1);
for (auto& argv_str : argv_strs) {
argv.push_back(argv_str.data());
}
argv.push_back(nullptr);
FUZZTEST_PCHECK(posix_spawn(&platform_context_->pid, argv[0],
/*file_actions=*/nullptr,
/*attrp=*/nullptr, argv.data(), environ) == 0);
}
is_executing_ = true;
return true;
#endif // _WIN32
}
std::optional<int> Command::Wait(absl::Time deadline,
StopCondition* stop_condition) {
FUZZTEST_CHECK(is_executing());
#ifdef _WIN32
FUZZTEST_CHECK_NE(platform_context_->win_process_handle,
INVALID_HANDLE_VALUE);
DWORD timeout_ms = INFINITE;
if (deadline != absl::InfiniteFuture()) {
auto dur = deadline - absl::Now();
if (dur <= absl::ZeroDuration()) {
timeout_ms = 0;
} else {
timeout_ms = static_cast<DWORD>(absl::ToInt64Milliseconds(dur));
}
}
DWORD res =
WaitForSingleObject(platform_context_->win_process_handle, timeout_ms);
if (res == WAIT_TIMEOUT) {
VlogProblemInfo(
absl::StrCat("Timeout while waiting for command process: deadline is ",
deadline),
/*vlog_level=*/1);
return std::nullopt;
}
DWORD exit_code = 0;
GetExitCodeProcess(platform_context_->win_process_handle, &exit_code);
CloseHandle(platform_context_->win_process_handle);
platform_context_->win_process_handle = INVALID_HANDLE_VALUE;
is_executing_ = false;
if (exit_code == STATUS_CONTROL_C_EXIT && stop_condition != nullptr) {
stop_condition->RequestStop(
EXIT_FAILURE, "Command killed: signal=SIGINT (likely Ctrl-C)");
}
return static_cast<int>(exit_code);
#else // _WIN32
int exit_code = EXIT_SUCCESS;
if (platform_context_->fork_server != nullptr) {
// The fork server forks, the child is running. Block until some readable
// data appears in the pipe (that is, after the fork server writes the
// execution result to it).
if (!platform_context_->fork_server->ReadPipe(deadline, exit_code)) {
VlogProblemInfo(
absl::StrCat("Waiting for fork server failed, deadline is ",
deadline),
/*vlog_level=*/1);
return std::nullopt;
}
} else {
FUZZTEST_CHECK_NE(platform_context_->pid, -1);
while (true) {
const pid_t r = waitpid(platform_context_->pid, &exit_code, WNOHANG);
FUZZTEST_CHECK_NE(r, -1);
if (r == platform_context_->pid &&
(WIFEXITED(exit_code) || WIFSIGNALED(exit_code)))
break;
FUZZTEST_CHECK_EQ(r, 0);
const auto timeout = deadline - absl::Now();
if (timeout > absl::ZeroDuration()) {
const auto duration = std::clamp<useconds_t>(
absl::ToInt64Microseconds(timeout), 0, 100000);
usleep(duration); // NOLINT: early return on SIGCHLD is desired.
continue;
} else {
VlogProblemInfo(
absl::StrCat(
"Timeout while waiting for the command process: deadline is ",
deadline),
/*vlog_level=*/1);
return std::nullopt;
}
}
platform_context_->pid = -1;
}
is_executing_ = false;
// When the command is actually a wrapper shell launching the binary(-es)
// (e.g. a Docker container), the shell will preserve a normal exit code
// returned by the binary (the legal range for such codes that can be
// passed to `exit()` is [0..125]); but the shell will specially encode
// the exit code returned by the binary when the binary is killed by a
// signal by adding 128 to the signal number and returning the result as
// a normal exit code. This encoding is used in `bash` and `dash` but may be
// different in other shells, e.g., `ksh`.
//
// For more details, see https://tldp.org/LDP/abs/html/exitcodes.html.
//
// Therefore, to handle this case, we need to first unpack these special
// pseudo-normal exit codes before analyzing them further. After
// reassigning `WEXITSTATUS()` to exit_code, the if-else below will take
// the else-branch and unpack the signal number from the updated value. This
// has experimentally been observed to work with existing implementations of
// the `wait` macros but there is no definitive documentation for it.
if (WIFEXITED(exit_code) && WEXITSTATUS(exit_code) > 128 &&
WEXITSTATUS(exit_code) < 255) {
exit_code = WEXITSTATUS(exit_code);
}
if (WIFEXITED(exit_code) && WEXITSTATUS(exit_code) != EXIT_SUCCESS) {
const auto exit_status = WEXITSTATUS(exit_code);
VlogProblemInfo(
absl::StrCat("Command errored out: exit status=", exit_status),
/*vlog_level=*/1);
exit_code = exit_status;
} else if (WIFSIGNALED(exit_code)) {
const auto signal = WTERMSIG(exit_code);
if (signal == SIGINT) {
if (stop_condition != nullptr) {
stop_condition->RequestStop(
EXIT_FAILURE, "Command killed: signal=SIGINT (likely Ctrl-C)");
}
// When the user kills Centipede via ^C, they are unlikely to be
// interested in any of the subprocesses' outputs. Also, ^C terminates all
// the subprocesses, including all the runners, so all their outputs would
// get printed simultaneously, flooding the log. Hence log at a high
// `vlog_level`.
VlogProblemInfo("Command killed: signal=SIGINT (likely Ctrl-C)",
/*vlog_level=*/10);
} else {
// The fork server subprocess was killed by something other than ^C: log
// at a lower `vlog_level` to help diagnose problems.
VlogProblemInfo(absl::StrCat("Command killed: signal=", signal),
/*vlog_level=*/1);
}
// TODO(ussuri): Consider changing this to exit_code = EXIT_FAILURE.
exit_code = signal;
}
return exit_code;
#endif // _WIN32
}
void Command::RequestStop(bool force) {
FUZZTEST_CHECK(is_executing());
#ifdef _WIN32
FUZZTEST_CHECK_NE(platform_context_->win_process_handle,
INVALID_HANDLE_VALUE);
TerminateProcess(platform_context_->win_process_handle, 1);
#else
if (platform_context_->fork_server) {
FUZZTEST_CHECK_NE(platform_context_->fork_server->pid_, -1);
// Cannot send SIGKILL to the fork server as it kills only the parent
// process, but not the child. The fork server would send SIGKILL to the
// child on SIGUSR1.
kill(platform_context_->fork_server->pid_, force ? SIGUSR1 : SIGTERM);
return;
}
FUZZTEST_CHECK_NE(platform_context_->pid, -1);
kill(platform_context_->pid, force ? SIGKILL : SIGTERM);
#endif // _WIN32
}
std::string Command::ReadRedirectedStdout() const {
std::string ret;
if (!stdout_file_.empty()) {
ReadFromLocalFile(stdout_file_, ret);
if (ret.empty()) ret = "<EMPTY>";
}
return ret;
}
std::string Command::ReadRedirectedStderr() const {
std::string ret;
if (!stderr_file_.empty()) {
if (stderr_file_ == stdout_file_) {
ret = "<DUPED TO STDOUT>";
} else {
ReadFromLocalFile(stderr_file_, ret);
if (ret.empty()) ret = "<EMPTY>";
}
}
return ret;
}
void Command::LogProblemInfo(std::string_view message) const {
absl::MutexLock lock(GetExecutionLoggingMutex());
FUZZTEST_LOG(ERROR) << message;
FUZZTEST_LOG(ERROR).NoPrefix() << "=== COMMAND ===";
FUZZTEST_LOG(ERROR).NoPrefix() << command_line_;
FUZZTEST_LOG(ERROR).NoPrefix() << "=== STDOUT ===";
for (const auto& line : absl::StrSplit(ReadRedirectedStdout(), '\n')) {
FUZZTEST_LOG(ERROR).NoPrefix() << line;
}
FUZZTEST_LOG(ERROR).NoPrefix() << "=== STDERR ===";
for (const auto& line : absl::StrSplit(ReadRedirectedStderr(), '\n')) {
FUZZTEST_LOG(ERROR).NoPrefix() << line;
}
}
void Command::VlogProblemInfo(std::string_view message, int vlog_level) const {
if (FUZZTEST_VLOG_IS_ON(vlog_level)) LogProblemInfo(message);
}
absl::Mutex& GetExecutionLoggingMutex() {
static absl::Mutex mu{absl::kConstInit};
return mu;
}
} // namespace fuzztest::internal