blob: ef93c75c0f0567aa3902313286a79c17e5daa9e8 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/sys/fuzzing/libfuzzer/runner.h"
#include <lib/fdio/spawn.h>
#include <lib/fit/defer.h>
#include <lib/fpromise/single_threaded_executor.h>
#include <lib/syslog/cpp/macros.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <zircon/status.h>
#include <zircon/syscalls/object.h>
#include <zircon/time.h>
#include <filesystem>
#include <iostream>
#include <sstream>
#include <openssl/sha.h>
#include <re2/re2.h>
#include "src/lib/files/directory.h"
#include "src/lib/files/eintr_wrapper.h"
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/sys/fuzzing/common/dictionary.h"
#include "src/sys/fuzzing/common/status.h"
#include "src/sys/fuzzing/libfuzzer/stats.h"
namespace fuzzing {
namespace {
using fuchsia::fuzzer::ProcessStats;
const char* kTestInputPath = "/tmp/test_input";
const char* kLiveCorpusPath = "/tmp/live_corpus";
const char* kSeedCorpusPath = "/tmp/seed_corpus";
const char* kTempCorpusPath = "/tmp/temp_corpus";
const char* kDictionaryPath = "/tmp/dictionary";
const char* kResultInputPath = "/tmp/result_input";
// See libFuzzer's |fuzzer::FuzzingOptions|.
constexpr int64_t kLibFuzzerNoErrorExitCode = 0;
constexpr int64_t kLibFuzzerTimeoutExitCode = 70;
constexpr int64_t kLibFuzzerOOMExitCode = 71;
// Returns |one| if |original| is non-zero and less than |one|, otherwise returns |original|.
template <typename T>
T Clamp(T original, const T& one, const char* type, const char* unit, const char* flag) {
if (!original) {
return 0;
}
if (original < one) {
FX_LOGS(WARNING) << "libFuzzer does not support " << type << "s of less than 1 " << unit
<< " for '" << flag << "'.";
return one;
}
return original;
}
// Converts a flag into a libFuzzer command line argument.
template <typename T>
std::string MakeArg(const std::string& flag, T value) {
std::ostringstream oss;
oss << "-" << flag << "=" << value;
return oss.str();
}
void CreateDirectory(const char* pathname) {
if (!files::CreateDirectory(pathname)) {
FX_LOGS(FATAL) << "Failed to create '" << pathname << "': " << strerror(errno);
}
}
// Reads a byte sequence from a file.
Input ReadInputFromFile(const std::string& pathname) {
std::vector<uint8_t> data;
if (!files::ReadFileToVector(pathname, &data)) {
FX_LOGS(FATAL) << "Failed to read input from '" << pathname << "': " << strerror(errno);
}
return Input(data);
}
// Writes a byte sequence to a file.
void WriteInputToFile(const Input& input, const std::string& pathname) {
FX_DCHECK(input.size() < std::numeric_limits<ssize_t>::max());
const auto* data = reinterpret_cast<const char*>(input.data());
auto size = static_cast<ssize_t>(input.size());
if (!files::WriteFile(pathname, data, size)) {
FX_LOGS(FATAL) << "Failed to write input to '" << pathname << "': " << strerror(errno);
}
}
std::string MakeFilename(const Input& input) {
uint8_t digest[SHA_DIGEST_LENGTH];
SHA1(input.data(), input.size(), digest);
std::ostringstream filename;
filename << std::hex;
for (size_t i = 0; i < SHA_DIGEST_LENGTH; ++i) {
filename << std::setw(2) << std::setfill('0') << size_t(digest[i]);
}
return filename.str();
}
} // namespace
RunnerPtr LibFuzzerRunner::MakePtr(ExecutorPtr executor) {
return RunnerPtr(new LibFuzzerRunner(std::move(executor)));
}
LibFuzzerRunner::LibFuzzerRunner(ExecutorPtr executor)
: Runner(executor), process_(executor), workflow_(this) {
options_ = Runner::options();
options_->set_detect_exits(true);
CreateDirectory(kSeedCorpusPath);
CreateDirectory(kLiveCorpusPath);
}
ZxPromise<> LibFuzzerRunner::Initialize(std::string pkg_dir, std::vector<std::string> args) {
// First argument is the subprocess name.
if (args.empty()) {
return fpromise::make_promise(
[]() -> ZxResult<> { return fpromise::error(ZX_ERR_INVALID_ARGS); });
}
auto args_begin = args.begin();
cmdline_.push_back(*args_begin++);
// Rearrange args before '--' so that positional args come before flags. Keep the positional args
// intact, and copy and erase the rest of the command line.
auto args_end = std::find(args_begin, args.end(), "--");
auto positional_end =
std::partition(args_begin, args_end, [](const std::string& arg) { return arg[0] != '-'; });
std::copy(positional_end, args.end(), std::back_inserter(cmdline_));
args = std::vector<std::string>(args_begin, positional_end);
return Runner::Initialize(std::move(pkg_dir), std::move(args));
}
///////////////////////////////////////////////////////////////
// Corpus-related methods.
zx_status_t LibFuzzerRunner::AddToCorpus(CorpusType corpus_type, Input input) {
auto filename = MakeFilename(input);
switch (corpus_type) {
case CorpusType::SEED:
if (auto [iter, inserted] = seed_corpus_.insert(filename); inserted) {
WriteInputToFile(input, files::JoinPath(kSeedCorpusPath, filename));
}
break;
case CorpusType::LIVE:
if (auto [iter, inserted] = live_corpus_.insert(filename); inserted) {
WriteInputToFile(input, files::JoinPath(kLiveCorpusPath, filename));
}
break;
default:
return ZX_ERR_INVALID_ARGS;
}
return ZX_OK;
}
std::vector<Input> LibFuzzerRunner::GetCorpus(CorpusType corpus_type) {
std::vector<Input> inputs;
switch (corpus_type) {
case CorpusType::SEED:
inputs.reserve(seed_corpus_.size());
for (const auto& filename : seed_corpus_) {
inputs.emplace_back(ReadInputFromFile(files::JoinPath(kSeedCorpusPath, filename)));
}
break;
case CorpusType::LIVE:
inputs.reserve(live_corpus_.size());
for (const auto& filename : live_corpus_) {
inputs.emplace_back(ReadInputFromFile(files::JoinPath(kLiveCorpusPath, filename)));
}
break;
default:
FX_NOTIMPLEMENTED();
}
return inputs;
}
void LibFuzzerRunner::ReloadLiveCorpus() {
live_corpus_.clear();
std::unordered_set<std::string> dups;
for (const auto& dir_entry : std::filesystem::directory_iterator(kLiveCorpusPath)) {
auto filename = dir_entry.path().filename().string();
if (files::IsFile(files::JoinPath(kSeedCorpusPath, filename))) {
dups.insert(files::JoinPath(kLiveCorpusPath, filename));
} else {
live_corpus_.insert(std::move(filename));
}
}
for (const auto& dup_path : dups) {
std::filesystem::remove(dup_path);
}
}
///////////////////////////////////////////////////////////////
// Dictionary-related methods.
zx_status_t LibFuzzerRunner::ParseDictionary(const Input& input) {
Dictionary dict;
dict.Configure(options());
if (!dict.Parse(input)) {
return ZX_ERR_INVALID_ARGS;
}
WriteInputToFile(input, kDictionaryPath);
has_dictionary_ = true;
return ZX_OK;
}
Input LibFuzzerRunner::GetDictionaryAsInput() const {
return has_dictionary_ ? ReadInputFromFile(kDictionaryPath) : Input();
}
///////////////////////////////////////////////////////////////
// Fuzzing workflows.
ZxPromise<Artifact> LibFuzzerRunner::Fuzz() {
return fpromise::make_promise([this]() -> ZxResult<> {
if (auto status = AddArgs(); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kLiveCorpusPath); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kSeedCorpusPath); status != ZX_OK) {
return fpromise::error(status);
}
return fpromise::ok();
})
.and_then(RunAsync())
.and_then([this](Artifact& artifact) {
ReloadLiveCorpus();
return fpromise::ok(std::move(artifact));
})
.wrap_with(workflow_);
}
ZxPromise<Artifact> LibFuzzerRunner::TryEach(std::vector<Input> inputs) {
// Write the inputs out to storage for libFuzzer.
std::filesystem::remove_all(kTempCorpusPath);
CreateDirectory(kTempCorpusPath);
std::vector<std::string> input_files;
input_files.reserve(inputs.size());
for (auto& input : inputs) {
auto input_file = files::JoinPath(kTempCorpusPath, MakeFilename(input));
WriteInputToFile(input, input_file);
input_files.emplace_back(std::move(input_file));
}
inputs.clear();
return TryFiles(std::move(input_files)).wrap_with(workflow_);
}
ZxPromise<Artifact> LibFuzzerRunner::TryFiles(std::vector<std::string> input_files) {
print_all_ = true;
return fpromise::make_promise(
[this, fut = ZxFuture<Artifact>(),
input_files = std::move(input_files)](Context& context) mutable -> ZxResult<Artifact> {
// Large corpora can result in a command line that is too big for `fdio_spawn`. Try
// inputs in batches.
while (true) {
if (!fut) {
if (input_files.empty()) {
return fpromise::ok(Artifact(FuzzResult::NO_ERRORS));
}
if (auto status = AddArgs(); status != ZX_OK) {
FX_LOGS(WARNING) << "Failed to add libFuzzer command line flags.";
return fpromise::error(status);
}
std::vector<std::string> deferred;
zx_status_t status = ZX_OK;
for (auto& input_file : input_files) {
if (status == ZX_OK) {
status = process_.AddArg(input_file);
}
if (status != ZX_OK) {
deferred.emplace_back(std::move(input_file));
}
}
if (!deferred.empty()) {
FX_LOGS(INFO) << "Will try " << deferred.size()
<< " remaining input(s) in a subsequent attempt.";
}
input_files = std::move(deferred);
fut = RunAsync();
}
if (!fut(context)) {
return fpromise::pending();
}
if (fut.is_error()) {
return fpromise::error(fut.take_error());
}
auto artifact = fut.take_value();
if (artifact.fuzz_result() != FuzzResult::NO_ERRORS) {
return fpromise::ok(std::move(artifact));
}
}
});
}
ZxPromise<Artifact> LibFuzzerRunner::ValidateMinimize(Input input) {
WriteInputToFile(input, kTestInputPath);
return TryFiles(std::vector<std::string>({kTestInputPath}))
.and_then([](Artifact& artifact) -> ZxResult<Artifact> {
if (artifact.fuzz_result() == FuzzResult::NO_ERRORS) {
FX_LOGS(WARNING) << "Test input did not trigger an error.";
return fpromise::error(ZX_ERR_INVALID_ARGS);
}
return fpromise::ok(std::move(artifact));
})
.wrap_with(workflow_, Workflow::Mode::kStart);
}
ZxPromise<Artifact> LibFuzzerRunner::Minimize(Artifact artifact) {
FX_DCHECK(artifact.has_input());
WriteInputToFile(artifact.input(), kTestInputPath);
return fpromise::make_promise([this]() -> ZxResult<> {
if (auto status = AddArgs(); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg("-minimize_crash=1"); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kTestInputPath); status != ZX_OK) {
return fpromise::error(status);
}
return fpromise::ok();
})
.and_then(RunAsync())
.and_then([](Artifact& artifact) -> ZxResult<Artifact> {
return fpromise::ok(Artifact(FuzzResult::MINIMIZED, artifact.take_input()));
})
.wrap_with(workflow_, Workflow::Mode::kFinish);
}
ZxPromise<Artifact> LibFuzzerRunner::Cleanse(Input input) {
WriteInputToFile(input, kTestInputPath);
return fpromise::make_promise([this]() -> ZxResult<> {
if (auto status = AddArgs(); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg("-cleanse_crash=1"); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kTestInputPath); status != ZX_OK) {
return fpromise::error(status);
}
return fpromise::ok();
})
.and_then(RunAsync())
.and_then([input = std::move(input)](Artifact& artifact) mutable {
// A quirk of libFuzzer's cleanse workflow is that it returns no error and an empty input if
// the input doesn't crash or is already "clean", and doesn't distinguish between the two.
if (artifact.has_input()) {
input = artifact.take_input();
}
return fpromise::ok(Artifact(FuzzResult::CLEANSED, std::move(input)));
})
.wrap_with(workflow_);
}
ZxPromise<> LibFuzzerRunner::ValidateMerge() {
std::vector<std::string> input_files;
input_files.reserve(seed_corpus_.size());
for (const auto& filename : seed_corpus_) {
input_files.emplace_back(files::JoinPath(kSeedCorpusPath, filename));
}
return TryFiles(std::move(input_files))
.and_then([](Artifact& artifact) -> ZxResult<> {
if (artifact.fuzz_result() != FuzzResult::NO_ERRORS) {
FX_LOGS(WARNING) << "Seed corpus contains an input that triggers an error: '"
<< artifact.input().ToHex() << "'";
return fpromise::error(ZX_ERR_INVALID_ARGS);
}
return fpromise::ok();
})
.wrap_with(workflow_, Workflow::Mode::kStart);
}
ZxPromise<Artifact> LibFuzzerRunner::Merge() {
std::filesystem::remove_all(kTempCorpusPath);
CreateDirectory(kTempCorpusPath);
return fpromise::make_promise([this]() -> ZxResult<> {
if (auto status = AddArgs(); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg("-merge=1"); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kTempCorpusPath); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kSeedCorpusPath); status != ZX_OK) {
return fpromise::error(status);
}
if (auto status = process_.AddArg(kLiveCorpusPath); status != ZX_OK) {
return fpromise::error(status);
}
return fpromise::ok();
})
.and_then(RunAsync())
.and_then([this](const Artifact& artifact) -> ZxResult<Artifact> {
std::filesystem::remove_all(kLiveCorpusPath);
std::filesystem::rename(kTempCorpusPath, kLiveCorpusPath);
ReloadLiveCorpus();
return fpromise::ok(Artifact(FuzzResult::MERGED));
})
.or_else([](const zx_status_t& status) -> ZxResult<Artifact> {
std::filesystem::remove_all(kTempCorpusPath);
return fpromise::error(status);
})
.wrap_with(workflow_, Workflow::Mode::kFinish);
}
Status LibFuzzerRunner::CollectStatus() {
// For libFuzzer, we return the most recently parsed status rather than point-in-time status.
auto status = CopyStatus(status_);
// Add other stats.
auto elapsed = zx::clock::get_monotonic() - start_;
status.set_elapsed(elapsed.to_nsecs());
if (process_.IsAlive()) {
auto stats = process_.GetStats();
if (stats.is_ok()) {
std::vector<ProcessStats> process_stats({stats.take_value()});
status.set_process_stats(std::move(process_stats));
}
}
return status;
}
ZxPromise<> LibFuzzerRunner::Stop() {
// TODO(https://fxbug.dev/42168245): If libFuzzer-for-Fuchsia watches for something sent to stdin in order to
// call its |Fuzzer::StaticInterruptCallback|, we could ask libFuzzer to shut itself down. This
// would guarantee we get all of its output.
return process_.Kill().and_then(workflow_.Stop());
}
///////////////////////////////////////////////////////////////
// Process-related methods.
zx_status_t LibFuzzerRunner::AddArgs() {
auto cmdline_iter = cmdline_.begin();
while (cmdline_iter != cmdline_.end() && *cmdline_iter != "--") {
if (auto status = process_.AddArg(*cmdline_iter++); status != ZX_OK) {
return status;
}
}
if (auto runs = options_->runs(); runs != kDefaultRuns) {
if (auto status = process_.AddArg(MakeArg("runs", options_->runs())); status != ZX_OK) {
return status;
}
}
if (auto max_total_time = options_->max_total_time(); max_total_time != kDefaultMaxTotalTime) {
max_total_time = Clamp(max_total_time, kOneSecond, "duration", "second", "max_total_time");
options_->set_max_total_time(max_total_time);
if (auto status = process_.AddArg(MakeArg("max_total_time", max_total_time / kOneSecond));
status != ZX_OK) {
return status;
}
}
if (auto seed = options_->seed(); seed != kDefaultSeed) {
if (auto status = process_.AddArg(MakeArg("seed", seed)); status != ZX_OK) {
return status;
}
}
if (auto max_input_size = options_->max_input_size(); max_input_size != kDefaultMaxInputSize) {
if (auto status = process_.AddArg(MakeArg("max_len", max_input_size)); status != ZX_OK) {
return status;
}
}
if (auto mutation_depth = options_->mutation_depth(); mutation_depth != kDefaultMutationDepth) {
if (auto status = process_.AddArg(MakeArg("mutate_depth", mutation_depth)); status != ZX_OK) {
return status;
}
}
if (options_->dictionary_level() != kDefaultDictionaryLevel) {
FX_LOGS(WARNING) << "libFuzzer does not support setting the dictionary level.";
}
if (!options_->detect_exits()) {
FX_LOGS(WARNING) << "libFuzzer does not support ignoring process exits.";
}
if (options_->detect_leaks()) {
if (auto status = process_.AddArg(MakeArg("detect_leaks", "1")); status != ZX_OK) {
return status;
}
}
if (auto run_limit = options_->run_limit(); run_limit != kDefaultRunLimit) {
run_limit = Clamp(run_limit, kOneSecond, "duration", "second", "run_limit");
options_->set_run_limit(run_limit);
if (auto status = process_.AddArg(MakeArg("timeout", run_limit / kOneSecond));
status != ZX_OK) {
return status;
}
}
if (auto malloc_limit = options_->malloc_limit(); malloc_limit != kDefaultMallocLimit) {
malloc_limit = Clamp(malloc_limit, kOneMb, "memory amount", "MB", "malloc_limit");
options_->set_malloc_limit(malloc_limit);
if (auto status = process_.AddArg(MakeArg("malloc_limit_mb", malloc_limit / kOneMb));
status != ZX_OK) {
return status;
}
}
if (auto oom_limit = options_->oom_limit(); oom_limit != kDefaultOomLimit) {
oom_limit = Clamp(oom_limit, kOneMb, "memory amount", "MB", "oom_limit");
options_->set_oom_limit(oom_limit);
if (auto status = process_.AddArg(MakeArg("rss_limit_mb", oom_limit / kOneMb));
status != ZX_OK) {
return status;
}
}
if (auto purge_interval = options_->purge_interval(); purge_interval != kDefaultPurgeInterval) {
purge_interval = Clamp(purge_interval, kOneSecond, "duration", "second", "purge_interval");
options_->set_purge_interval(purge_interval);
if (auto status =
process_.AddArg(MakeArg("purge_allocator_interval", purge_interval / kOneSecond));
status != ZX_OK) {
return status;
}
}
if (options_->malloc_exitcode() != kDefaultMallocExitcode) {
FX_LOGS(WARNING) << "libFuzzer does not support setting the 'malloc_exitcode'.";
}
if (options_->death_exitcode() != kDefaultDeathExitcode) {
FX_LOGS(WARNING) << "libFuzzer does not support setting the 'death_exitcode'.";
}
if (options_->leak_exitcode() != kDefaultLeakExitcode) {
FX_LOGS(WARNING) << "libFuzzer does not support setting the 'leak_exitcode'.";
}
if (options_->oom_exitcode() != kDefaultOomExitcode) {
FX_LOGS(WARNING) << "libFuzzer does not support setting the 'oom_exitcode'.";
}
if (options_->pulse_interval() != kDefaultPulseInterval) {
FX_LOGS(WARNING) << "libFuzzer does not support setting the 'pulse_interval'.";
}
if (options_->debug()) {
if (auto status = process_.AddArg(MakeArg("handle_segv", 0)); status != ZX_OK) {
return status;
}
if (auto status = process_.AddArg(MakeArg("handle_bus", 0)); status != ZX_OK) {
return status;
}
if (auto status = process_.AddArg(MakeArg("handle_ill", 0)); status != ZX_OK) {
return status;
}
if (auto status = process_.AddArg(MakeArg("handle_fpe", 0)); status != ZX_OK) {
return status;
}
if (auto status = process_.AddArg(MakeArg("handle_abrt", 0)); status != ZX_OK) {
return status;
}
}
if (options_->print_final_stats()) {
if (auto status = process_.AddArg(MakeArg("print_final_stats", 1)); status != ZX_OK) {
return status;
}
}
if (options_->use_value_profile()) {
if (auto status = process_.AddArg(MakeArg("use_value_profile", 1)); status != ZX_OK) {
return status;
}
}
auto sanitizer_options = options_->sanitizer_options();
const auto& name = sanitizer_options.name;
const auto& value = sanitizer_options.value;
if (!name.empty() && !value.empty()) {
if (auto status = process_.SetEnvVar(name, value); status != ZX_OK) {
return status;
}
}
auto output_flags = options_->output_flags();
size_t close_fd_mask = 0;
if (bool(output_flags & OutputFlags::CLOSE_STDOUT)) {
close_fd_mask |= 1;
}
if (bool(output_flags & OutputFlags::CLOSE_STDERR)) {
close_fd_mask |= 2;
}
if (close_fd_mask) {
if (auto status = process_.AddArg(MakeArg("close_fd_mask", close_fd_mask)); status != ZX_OK) {
return status;
}
}
if (bool(output_flags & OutputFlags::VERBOSE)) {
if (auto status = process_.AddArg(MakeArg("verbosity", 2)); status != ZX_OK) {
return status;
}
}
if (has_dictionary_) {
if (auto status = process_.AddArg(MakeArg("dict", kDictionaryPath)); status != ZX_OK) {
return status;
}
}
std::filesystem::remove(kResultInputPath);
result_input_pathname_.clear();
if (auto status = process_.AddArg(MakeArg("exact_artifact_path", kResultInputPath));
status != ZX_OK) {
return status;
}
while (cmdline_iter != cmdline_.end()) {
if (auto status = process_.AddArg(*cmdline_iter++); status != ZX_OK) {
return status;
}
}
return ZX_OK;
}
ZxPromise<Artifact> LibFuzzerRunner::RunAsync() {
return fpromise::make_promise([this](Context& context) -> ZxResult<> {
// Nothing is currently written to libFuzzer's stdin or read from its stdout, but they
// must be available. For certain workflows, libFuzzer re-executes itself using
// `fdio_spawn` with `FDIO_SPAWN_CLONE_ALL`, which will fail if it cannot clone an fd.
if (auto status = process_.AddStdinPipe(); status != ZX_OK) {
FX_LOGS(WARNING) << "Failed to pipe stdin to libFuzzer";
return fpromise::error(status);
}
if (auto status = process_.AddStdoutPipe(); status != ZX_OK) {
FX_LOGS(WARNING) << "Failed to pipe stdout from libFuzzer";
return fpromise::error(status);
}
if (auto status = process_.AddStderrPipe(); status != ZX_OK) {
FX_LOGS(WARNING) << "Failed to pipe stderr from libFuzzer";
return fpromise::error(status);
}
if (auto status = process_.Spawn(); status != ZX_OK) {
FX_LOGS(WARNING) << "Failed to spawn libFuzzer";
return fpromise::error(status);
}
status_.set_running(true);
start_ = zx::clock::get_monotonic();
return fpromise::ok();
})
.and_then(ParseOutput())
.and_then(
[this, wait = ZxFuture<int64_t>()](Context& context) mutable -> ZxResult<FuzzResult> {
if (!wait) {
wait = process_.Wait();
}
if (!wait(context)) {
return fpromise::pending();
}
if (wait.is_error()) {
return fpromise::error(wait.take_error());
}
if (fuzz_result_ != FuzzResult::NO_ERRORS) {
return fpromise::ok(fuzz_result_);
}
auto exitcode = wait.take_value();
switch (exitcode) {
case kLibFuzzerNoErrorExitCode:
case ZX_TASK_RETCODE_SYSCALL_KILL:
return fpromise::ok(FuzzResult::NO_ERRORS);
case kLibFuzzerOOMExitCode:
case ZX_TASK_RETCODE_OOM_KILL:
return fpromise::ok(FuzzResult::OOM);
case kLibFuzzerTimeoutExitCode:
return fpromise::ok(FuzzResult::TIMEOUT);
default:
return fpromise::ok(FuzzResult::CRASH);
}
})
.or_else([this, kill = ZxFuture<>()](
Context& context, const zx_status_t& status) mutable -> ZxResult<FuzzResult> {
if (!kill) {
kill = process_.Kill();
}
if (!kill(context)) {
return fpromise::pending();
}
return fpromise::error(status);
})
.then([this](ZxResult<FuzzResult>& result) -> ZxResult<FuzzResult> {
status_.set_running(false);
process_.Reset();
print_all_ = false;
pid_ = int64_t(-1);
fuzz_result_ = FuzzResult::NO_ERRORS;
return std::move(result);
})
.and_then([this](const FuzzResult& fuzz_result) -> ZxResult<Artifact> {
if (files::IsFile(kResultInputPath)) {
return fpromise::ok(Artifact(fuzz_result, ReadInputFromFile(kResultInputPath)));
}
if (!result_input_pathname_.empty()) {
return fpromise::ok(Artifact(fuzz_result, ReadInputFromFile(result_input_pathname_)));
}
return fpromise::ok(Artifact(fuzz_result));
});
}
///////////////////////////////////////////////////////////////
// Output parsing methods.
ZxPromise<> LibFuzzerRunner::ParseOutput() {
std::vector<ZxPromise<>> outputs;
outputs.push_back(ParseStdout());
outputs.push_back(ParseStderr());
return fpromise::join_promise_vector(std::move(outputs))
.or_else([]() -> ZxResult<std::vector<ZxResult<>>> {
// Not possible, but needed to transform the joined promise's error type.
return fpromise::error(ZX_ERR_INTERNAL);
})
.and_then([](std::vector<ZxResult<>>& results) -> ZxResult<> {
for (const auto& result : results) {
if (result.is_error()) {
return fpromise::error(result.error());
}
}
return fpromise::ok();
});
}
ZxPromise<> LibFuzzerRunner::ParseStdout() {
return fpromise::make_promise(
[this, read_line = ZxFuture<std::string>()](Context& context) mutable -> ZxResult<> {
while (true) {
if (!read_line) {
read_line = process_.ReadFromStdout();
}
if (!read_line(context)) {
return fpromise::pending();
}
if (read_line.is_error()) {
if (auto status = read_line.error(); status != ZX_ERR_PEER_CLOSED) {
FX_LOGS(ERROR) << "failed to read libFuzzer stdout: " << zx_status_get_string(status);
return fpromise::error(status);
}
return fpromise::ok();
}
auto line = read_line.take_value();
if (verbose_ && print_all_) {
std::cout << line << std::endl;
}
}
});
}
ZxPromise<> LibFuzzerRunner::ParseStderr() {
return fpromise::make_promise(
[this, read_line = ZxFuture<std::string>()](Context& context) mutable -> ZxResult<> {
while (true) {
if (!read_line) {
read_line = process_.ReadFromStderr();
}
if (!read_line(context)) {
return fpromise::pending();
}
if (read_line.is_error()) {
if (auto status = read_line.error(); status != ZX_ERR_PEER_CLOSED) {
FX_LOGS(ERROR) << "failed to read libFuzzer stderr: " << zx_status_get_string(status);
return fpromise::error(status);
}
// TODO(https://fxbug.dev/42060461): Rarely, the process output will be truncated. This causes
// problems for tooling like undercoat. This is the only location in the LibFuzzerRunner
// that returns `ZX_ERR_IO_INVALID`.
if (pid_ < 0 && !process_.is_killed()) {
FX_LOGS(ERROR) << "libFuzzer output terminated prematurely.";
return fpromise::error(ZX_ERR_IO_INVALID);
}
return fpromise::ok();
}
auto line = read_line.take_value();
if (verbose_) {
std::cerr << line << std::endl;
}
// See libFuzzer's |Fuzzer::TryDetectingAMemoryLeak|.
// This match is ugly, but it's the only message in current libFuzzer that this code can
// rely on to detect a leak.
if (line == "INFO: to ignore leaks on libFuzzer side use -detect_leaks=0.") {
fuzz_result_ = FuzzResult::LEAK;
continue;
}
// When running explicit test inputs, libFuzzer doesn't save artifacts. Record the input
// name to determine which caused the error.
re2::StringPiece input(line);
if (re2::RE2::Consume(&input, "Running: (\\S+)", &result_input_pathname_)) {
continue;
}
if (re2::RE2::Consume(&input, "==(\\d+)==", &pid_)) {
continue;
}
UpdateReason reason;
if (ParseLibFuzzerStats(line, &reason, &status_)) {
UpdateMonitors(reason);
}
}
});
}
} // namespace fuzzing