| // 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 |