| // 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/realmfuzzer/engine/runner.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/clock.h> |
| #include <zircon/sanitizer.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls/exception.h> |
| |
| #include <deque> |
| #include <iostream> |
| |
| #include "src/lib/fxl/macros.h" |
| #include "src/sys/fuzzing/libfuzzer/stats.h" |
| #include "src/sys/fuzzing/realmfuzzer/target/process.h" |
| |
| namespace fuzzing { |
| |
| using ::fuchsia::fuzzer::Data; |
| using ::fuchsia::fuzzer::MAX_PROCESS_STATS; |
| using ::fuchsia::fuzzer::OutputFlags; |
| |
| RunnerPtr RealmFuzzerRunner::MakePtr(ExecutorPtr executor) { |
| return RunnerPtr(new RealmFuzzerRunner(std::move(executor))); |
| } |
| |
| RealmFuzzerRunner::RealmFuzzerRunner(ExecutorPtr executor) |
| : Runner(executor), |
| generated_receiver_(&generated_sender_), |
| leak_receiver_(&leak_sender_), |
| adapter_(executor), |
| provider_(executor), |
| processed_receiver_(&processed_sender_), |
| workflow_(this) { |
| generated_receiver_.Close(); |
| processed_receiver_.Close(); |
| leak_receiver_.Close(); |
| |
| seed_corpus_ = Corpus::MakePtr(); |
| live_corpus_ = Corpus::MakePtr(); |
| pool_ = std::make_shared<ModulePool>(); |
| } |
| |
| void RealmFuzzerRunner::SetAdapterHandler(TargetAdapterClient::RequestHandler handler) { |
| adapter_.set_handler(std::move(handler)); |
| } |
| |
| void RealmFuzzerRunner::SetProviderHandler(CoverageDataProviderClient::RequestHandler handler) { |
| provider_.Bind(std::move(handler)); |
| } |
| |
| ZxPromise<> RealmFuzzerRunner::Initialize(std::string pkg_dir, std::vector<std::string> args) { |
| return adapter_.GetParameters() |
| .or_else([]() -> ZxResult<std::vector<std::string>> { |
| FX_LOGS(WARNING) << "Failed to get parameters from adapter"; |
| return fpromise::error(ZX_ERR_INTERNAL); |
| }) |
| .and_then([this, pkg_dir = std::move(pkg_dir), |
| args = std::move(args)](const std::vector<std::string>& parameters) mutable { |
| std::copy(parameters.begin(), parameters.end(), std::back_inserter(args)); |
| return Runner::Initialize(std::move(pkg_dir), std::move(args)); |
| }) |
| .wrap_with(workflow_.scope()); |
| } |
| |
| ZxPromise<> RealmFuzzerRunner::Configure() { |
| return Runner::Configure() |
| .and_then([this]() -> ZxResult<> { |
| const OptionsPtr& options = this->options(); |
| if (bool(options->output_flags() & OutputFlags::CLOSE_STDOUT)) { |
| FX_LOGS(WARNING) |
| << "realmfuzzer ignored a request to close stdout: the engine does not " |
| "currently have the ability to suppress stdout from other components being fuzzed."; |
| } |
| if (bool(options->output_flags() & OutputFlags::CLOSE_STDERR)) { |
| FX_LOGS(WARNING) |
| << "realmfuzzer ignored a request to close stderr: the engine does not " |
| "currently have the ability to suppress stderr from other components being fuzzed."; |
| } |
| seed_corpus_->Configure(options); |
| live_corpus_->Configure(options); |
| mutagen_.Configure(options); |
| adapter_.Configure(options); |
| for (auto& [target_id, process_proxy] : process_proxies_) { |
| process_proxy->Configure(options); |
| } |
| return fpromise::ok(); |
| }) |
| .wrap_with(workflow_); |
| } |
| |
| zx_status_t RealmFuzzerRunner::AddToCorpus(CorpusType corpus_type, Input input) { |
| switch (corpus_type) { |
| case CorpusType::SEED: |
| return seed_corpus_->Add(std::move(input)); |
| case CorpusType::LIVE: |
| return live_corpus_->Add(std::move(input)); |
| default: |
| return ZX_ERR_INVALID_ARGS; |
| } |
| } |
| |
| std::vector<Input> RealmFuzzerRunner::GetCorpus(CorpusType corpus_type) { |
| CorpusPtr corpus; |
| switch (corpus_type) { |
| case CorpusType::SEED: |
| corpus = seed_corpus_; |
| break; |
| case CorpusType::LIVE: |
| corpus = live_corpus_; |
| break; |
| default: |
| FX_NOTREACHED(); |
| } |
| // The first input is always the implicit empty input; skip it. |
| auto num_inputs = corpus->num_inputs(); |
| std::vector<Input> inputs(num_inputs - 1); |
| for (size_t i = 0; i < inputs.size(); ++i) { |
| corpus->At(i + 1, &inputs[i]); |
| } |
| return inputs; |
| } |
| |
| zx_status_t RealmFuzzerRunner::ParseDictionary(const Input& input) { |
| Dictionary dict; |
| dict.Configure(options()); |
| if (!dict.Parse(input)) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| mutagen_.set_dictionary(std::move(dict)); |
| return ZX_OK; |
| } |
| |
| Input RealmFuzzerRunner::GetDictionaryAsInput() const { return mutagen_.dictionary().AsInput(); } |
| |
| /////////////////////////////////////////////////////////////// |
| // Asynchronous workflows. |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::Fuzz() { |
| return FuzzInputs(/* backlog= */ options()->mutation_depth()).wrap_with(workflow_); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::TryEach(std::vector<Input> inputs) { |
| return fpromise::make_promise([this, inputs = std::move(inputs)]() mutable -> ZxResult<> { |
| for (auto& input : inputs) { |
| if (auto status = generated_sender_.Send(std::move(input)); status != ZX_OK) { |
| FX_LOGS(ERROR) << "Input queue closed prematurely: " << zx_status_get_string(status); |
| return fpromise::error(status); |
| } |
| } |
| generated_receiver_.Close(); |
| return fpromise::ok(); |
| }) |
| .and_then(TestInputs(kNoPostProcessing)) |
| .then([](ZxResult<Artifact>& result) -> ZxResult<FuzzResult> { |
| if (result.is_ok()) { |
| auto artifact = result.take_value(); |
| return fpromise::ok(artifact.fuzz_result()); |
| } |
| auto status = result.take_error(); |
| if (status == ZX_ERR_STOP) { |
| return fpromise::ok(FuzzResult::NO_ERRORS); |
| } |
| return fpromise::error(status); |
| }) |
| .and_then([](FuzzResult& fuzz_result) -> ZxResult<Artifact> { |
| return fpromise::ok(Artifact(fuzz_result)); |
| }) |
| .wrap_with(workflow_); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::ValidateMinimize(Input input) { |
| // Check that the input can be minimized, and that minimization is bounded. |
| return TestOneAsync(std::move(input), kNoPostProcessing) |
| .or_else([this](const zx_status_t& status) { |
| return CheckPrevious(status).and_then([]() -> ZxResult<Artifact> { |
| FX_LOGS(WARNING) << "Test input did not trigger an error."; |
| return fpromise::error(ZX_ERR_INVALID_ARGS); |
| }); |
| }) |
| .wrap_with(workflow_, Workflow::Mode::kStart); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::Minimize(Artifact artifact) { |
| FX_DCHECK(artifact.has_input()); |
| auto orig_corpus = live_corpus_; |
| auto orig_options = CopyOptions(options()); |
| return fpromise::make_promise([this, fuzz_result = artifact.fuzz_result(), |
| input = artifact.take_input(), minimize = ZxFuture<Artifact>()]( |
| Context& context) mutable -> ZxResult<Input> { |
| while (true) { |
| if (!minimize) { |
| // Ratchet down the input one byte. |
| if (input.size() < 2) { |
| FX_LOGS(INFO) << "Input is " << input.size() |
| << " byte(s); will not minimize further."; |
| return fpromise::ok(std::move(input)); |
| } |
| if (!options()->has_runs() && !options()->has_max_total_time()) { |
| FX_LOGS(INFO) |
| << "'max_total_time' and 'runs' are both not set. Defaulting to 10 minutes."; |
| options()->set_max_total_time(zx::min(10).get()); |
| } |
| auto next_input = input.Duplicate(); |
| next_input.Truncate(input.size() - 1); |
| options()->set_max_input_size(next_input.size()); |
| // Start each fuzzing pass using the seed corpus and the minimized input. |
| live_corpus_ = Corpus::MakePtr(); |
| live_corpus_->Configure(options()); |
| auto status = live_corpus_->Add(std::move(next_input)); |
| if (status != ZX_OK) { |
| FX_LOGS(ERROR) << "Failed to reset corpus: " << zx_status_get_string(status); |
| return fpromise::error(status); |
| } |
| // Imitate libFuzzer and count from 0 so long as errors are found. |
| Reset(); |
| run_ = 0; |
| pool_->Clear(); |
| minimize = FuzzInputs(); |
| } |
| if (!minimize(context)) { |
| return fpromise::pending(); |
| } |
| if (minimize.is_error()) { |
| return fpromise::error(minimize.error()); |
| } |
| auto minimized = minimize.take_value(); |
| if (minimized.fuzz_result() == FuzzResult::NO_ERRORS) { |
| FX_LOGS(INFO) << "Did not reduce error input beyond " << input.size() |
| << " bytes; exiting."; |
| return fpromise::ok(std::move(input)); |
| } |
| // TODO(https://fxbug.dev/42166322): This needs a more rigorous way of deduplicating crashes. |
| if (minimized.fuzz_result() != fuzz_result) { |
| FX_LOGS(WARNING) << "Different error detected; will not minimize further."; |
| return fpromise::ok(std::move(input)); |
| } |
| input = minimized.take_input(); |
| } |
| }) |
| .then( |
| [this, orig_corpus, options = std::move(orig_options)](ZxResult<Input>& result) mutable { |
| pool_->Clear(); |
| live_corpus_ = orig_corpus; |
| set_options(std::move(options)); |
| return std::move(result); |
| }) |
| .and_then([](Input& input) -> ZxResult<Artifact> { |
| return fpromise::ok(Artifact(FuzzResult::MINIMIZED, std::move(input))); |
| }) |
| .wrap_with(workflow_, Workflow::Mode::kFinish); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::Cleanse(Input input) { |
| // The general approach of this loop is to take tested inputs and their fuzzing results and return |
| // them to |GenerateCleanInputs| as |Artifacts|. |
| AsyncSender<Artifact> sender; |
| auto receiver = AsyncReceiver<Artifact>::MakePtr(&sender); |
| return fpromise::make_promise([this, generate = Future<>(), sender = std::move(sender), |
| receiver = std::move(receiver), test_inputs = ZxFuture<Artifact>(), |
| receive = Future<Input>(), |
| result = Artifact(FuzzResult::NO_ERRORS, std::move(input)), |
| artifacts = std::array<Artifact, 2>(), |
| num_artifacts = 0U](Context& context) mutable -> ZxResult<Input> { |
| while (true) { |
| if (!generate) { |
| // To set up initial conditions, simulate having just completed an "extra" attempt. |
| constexpr size_t kMaxCleanseAttempts = 5; |
| auto attempts_left = kMaxCleanseAttempts + 1; |
| |
| // Prepare the pipeline with some artifacts that make the attempt succeed and won't |
| // be reverted. This only fails if the `sender` is closed, in which case the promise |
| // below returns an error. |
| for (size_t i = 0; i < 2; ++i) { |
| if (auto status = |
| sender.Send(Artifact(FuzzResult::CRASH, result.input().Duplicate())); |
| status != ZX_OK) { |
| FX_LOGS(ERROR) << "Failed to prepare fuzzing input pipeline: " |
| << zx_status_get_string(status); |
| } |
| } |
| generate = GenerateCleanInputs(std::move(receiver), attempts_left); |
| } |
| if (!test_inputs) { |
| test_inputs = TestInputs(kNoPostProcessing); |
| } |
| if (!receive) { |
| receive = processed_receiver_.Receive(); |
| } |
| if (generate(context) && generate.is_error()) { |
| // |GenerateCleanInputs| only returns an error if its queues close unexpectedly. |
| return fpromise::error(ZX_ERR_BAD_STATE); |
| } |
| if (test_inputs(context)) { |
| if (test_inputs.is_error()) { |
| auto status = test_inputs.error(); |
| if (status != ZX_ERR_STOP) { |
| return fpromise::error(status); |
| } |
| return fpromise::ok(result.take_input()); |
| } |
| // Cleansed input triggered an error. Use it as the basis for further attempts. |
| result = test_inputs.take_value(); |
| artifacts[0] = result.Duplicate(); |
| artifacts[1] = result.Duplicate(); |
| receive = nullptr; |
| Reset(); |
| num_artifacts = 2; |
| } else if (receive(context)) { |
| if (receive.is_error()) { |
| FX_LOGS(ERROR) << "Output queue closed unexpectedly."; |
| return fpromise::error(ZX_ERR_BAD_STATE); |
| } |
| // Cleansed input didn't trigger an error. Save it for recycling. |
| artifacts[num_artifacts++] = Artifact(FuzzResult::NO_ERRORS, receive.take_value()); |
| } else { |
| // Still testing an input. |
| return fpromise::pending(); |
| } |
| if (num_artifacts < artifacts.size()) { |
| continue; |
| } |
| // Recycle inputs in pairs, one for each "clean" byte. |
| if (sender.Send(std::move(artifacts[0])) != ZX_OK || |
| sender.Send(std::move(artifacts[1])) != ZX_OK) { |
| // No more inputs are needed; all done. |
| return fpromise::ok(result.take_input()); |
| } |
| num_artifacts = 0; |
| } |
| }) |
| .and_then([](Input& input) -> ZxResult<Artifact> { |
| return fpromise::ok(Artifact(FuzzResult::CLEANSED, std::move(input))); |
| }) |
| .wrap_with(workflow_); |
| } |
| |
| ZxPromise<> RealmFuzzerRunner::ValidateMerge() { |
| // Accumulate the coverage from testing all the elements of the seed corpus. |
| return TestOneAsync(Input(), kAccumulateCoverage) |
| .or_else([this](const zx_status_t& status) { |
| return CheckPrevious(status).and_then(TestCorpusAsync(seed_corpus_, kAccumulateCoverage)); |
| }) |
| .and_then([](Artifact& artifact) -> ZxResult<> { |
| FX_LOGS(WARNING) << "Seed corpus contains an input that triggers an error: '" |
| << artifact.input().ToHex() << "'"; |
| return fpromise::error(ZX_ERR_INVALID_ARGS); |
| }) |
| .or_else([this](zx_status_t& status) { return CheckPrevious(status); }) |
| .wrap_with(workflow_, Workflow::Mode::kStart); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::Merge() { |
| auto collect_errors = std::make_shared<std::vector<Input>>(); |
| return fpromise::make_promise([this, collect_errors] { |
| // Measure what coverage each element of the live corpus provides beyond that accumulated |
| // by the seed corpus. After this step the live corpus contains only valid, measured |
| // inputs. |
| auto unmeasured = live_corpus_; |
| live_corpus_ = Corpus::MakePtr(); |
| live_corpus_->Configure(options()); |
| return TestCorpusAsync(unmeasured, kMeasureCoverageAndKeepInputs, collect_errors); |
| }) |
| .or_else([this, collect_errors](const zx_status_t& status) { |
| return CheckPrevious(status).and_then([this, collect_errors] { |
| if (!collect_errors->empty()) { |
| FX_LOGS(WARNING) << "Corpus contains input(s) that trigger error(s):"; |
| for (auto& input : *collect_errors) { |
| FX_LOGS(WARNING) << " '" << input.ToHex() << "'"; |
| } |
| } |
| // Accumulate the coverage from each element of the live corpus. The live corpus will be |
| // stably sorted by size, number of features measured above, and lexicographical order. |
| // Only elements that add coverage not accumulated by previous elements will be kept. |
| auto measured = live_corpus_; |
| live_corpus_ = Corpus::MakePtr(); |
| live_corpus_->Configure(options()); |
| return TestCorpusAsync(measured, kAccumulateCoverageAndKeepInputs); |
| }); |
| }) |
| .and_then([](const Artifact& artifact) -> ZxResult<> { |
| FX_LOGS(ERROR) << "Previously successful input triggered an error: '" |
| << artifact.input().ToHex() << "'"; |
| return fpromise::error(ZX_ERR_BAD_STATE); |
| }) |
| .or_else([this, collect_errors](const zx_status_t& status) { |
| return CheckPrevious(status).and_then([this, collect_errors]() -> ZxResult<> { |
| // As a final step, keep any inputs that triggered errors. |
| for (auto& input : *collect_errors) { |
| if (auto status = live_corpus_->Add(std::move(input)); status != ZX_OK) { |
| return fpromise::error(status); |
| } |
| } |
| return fpromise::ok(); |
| }); |
| }) |
| .and_then([]() -> ZxResult<Artifact> { return fpromise::ok(Artifact(FuzzResult::MERGED)); }) |
| .wrap_with(workflow_, Workflow::Mode::kFinish); |
| } |
| |
| void RealmFuzzerRunner::UpdateMonitorsWithStatus(UpdateReason reason, Status status) { |
| if (bool(options()->output_flags() & OutputFlags::LIBFUZZER)) { |
| std::cerr << FormatLibFuzzerStats(reason, status) << std::endl; |
| } |
| Runner::UpdateMonitorsWithStatus(reason, std::move(status)); |
| } |
| |
| Status RealmFuzzerRunner::CollectStatus() { |
| Status status; |
| status.set_running(!stopped_); |
| status.set_runs(run_); |
| |
| auto elapsed = zx::clock::get_monotonic() - start_; |
| status.set_elapsed(elapsed.to_nsecs()); |
| |
| size_t covered_features; |
| auto covered_pcs = pool_->GetCoverage(&covered_features); |
| status.set_covered_pcs(covered_pcs); |
| status.set_covered_features(covered_features); |
| |
| status.set_corpus_num_inputs(seed_corpus_->num_inputs() + live_corpus_->num_inputs()); |
| status.set_corpus_total_size(seed_corpus_->total_size() + live_corpus_->total_size()); |
| |
| std::vector<ProcessStats> all_stats; |
| all_stats.reserve(std::min<size_t>(process_proxies_.size(), MAX_PROCESS_STATS)); |
| for (auto& [target_id, process_proxy] : process_proxies_) { |
| if (all_stats.size() == all_stats.capacity()) { |
| break; |
| } |
| ProcessStats stats; |
| auto status = process_proxy->GetStats(&stats); |
| if (status == ZX_OK) { |
| all_stats.push_back(stats); |
| } else { |
| FX_LOGS(WARNING) << "Failed to get stats for process: " << zx_status_get_string(status); |
| } |
| } |
| status.set_process_stats(std::move(all_stats)); |
| return status; |
| } |
| |
| ZxPromise<> RealmFuzzerRunner::Stop() { |
| stopped_ = true; |
| return workflow_.Stop(); |
| } |
| |
| /////////////////////////////////////////////////////////////// |
| // Workflow-related methods. |
| |
| void RealmFuzzerRunner::StartWorkflow(Scope& scope) { |
| Reset(); |
| run_ = 0; |
| pool_->Clear(); |
| start_ = zx::clock::get_monotonic(); |
| pulse_start_ = start_ + zx::sec(2); |
| stopped_ = false; |
| // Handle coverage data produced during the workflow. |
| auto task = |
| fpromise::make_promise([this, |
| fut = Future<CoverageData>()](Context& context) mutable -> Result<> { |
| while (true) { |
| if (!fut) { |
| fut = provider_.GetCoverageData(); |
| } |
| if (!fut(context)) { |
| return fpromise::pending(); |
| } |
| if (fut.is_error()) { |
| return fpromise::error(); |
| } |
| auto coverage = fut.take_value(); |
| switch (coverage.data.Which()) { |
| case Data::Tag::kInstrumented: |
| ConnectProcess(std::move(coverage)); |
| break; |
| case Data::Tag::kInline8bitCounters: |
| AddInline8bitCounters(std::move(coverage)); |
| break; |
| default: |
| FX_LOGS(WARNING) << "Unrecgonized coverage data type: " << coverage.data.Which(); |
| return fpromise::error(); |
| } |
| } |
| }).wrap_with(scope); |
| executor()->schedule_task(std::move(task)); |
| UpdateMonitors(UpdateReason::INIT); |
| } |
| |
| void RealmFuzzerRunner::FinishWorkflow() { |
| generated_receiver_.Clear(); |
| processed_receiver_.Clear(); |
| leak_receiver_.Clear(); |
| stopped_ = true; |
| UpdateMonitors(UpdateReason::DONE); |
| } |
| |
| /////////////////////////////////////////////////////////////// |
| // Methods to generate fuzzing inputs. |
| |
| ZxPromise<> RealmFuzzerRunner::GenerateInputs(size_t num_inputs, size_t backlog) { |
| // Set up parameters for determining what inputs to generate and for how long. |
| auto max_size = options()->max_input_size(); |
| auto max_time = zx::duration(options()->max_total_time()); |
| auto deadline = max_time.get() ? zx::deadline_after(max_time) : zx::time::infinite(); |
| auto mutation_depth = options()->mutation_depth(); |
| return fpromise::make_promise([this, backlog, max_size]() -> ZxResult<> { |
| // "Precycle" some inputs by making it look like they are ready for reuse. |
| for (size_t i = 0; i <= backlog; i++) { |
| auto status = processed_sender_.Send(Input(max_size)); |
| if (status != ZX_OK) { |
| FX_LOGS(ERROR) << "Input queue closed prematurely while preparing to fuzz: " |
| << zx_status_get_string(status); |
| return fpromise::error(status); |
| } |
| } |
| return fpromise::ok(); |
| }) |
| .and_then([this, num_sent = 0U, num_inputs, deadline, num_mutations = mutation_depth, |
| mutation_depth, |
| recycle = Future<Input>()](Context& context) mutable -> ZxResult<> { |
| while (true) { |
| if (num_inputs != 0 && num_sent >= num_inputs) { |
| // Run limit will be reached by inputs already queued; all done. |
| return fpromise::ok(); |
| } |
| if (zx::clock::get_monotonic() >= deadline) { |
| // Time limit reached; all done. |
| return fpromise::ok(); |
| } |
| if (stopped_) { |
| // Interrupted; all done. |
| return fpromise::ok(); |
| } |
| if (!recycle) { |
| // Use inputs recycled from earlier runs to reduce heap allocations. |
| recycle = processed_receiver_.Receive(); |
| } |
| if (!recycle(context)) { |
| return fpromise::pending(); |
| } |
| if (recycle.is_error()) { |
| // Queue was closed; all done. |
| return fpromise::ok(); |
| } |
| auto input = recycle.take_value(); |
| if (num_mutations >= mutation_depth) { |
| // Pick an input and mutate it |mutation_depth| times in a row. |
| mutagen_.reset_mutations(); |
| live_corpus_->Pick(mutagen_.base_input()); |
| live_corpus_->Pick(mutagen_.crossover()); |
| num_mutations = 0; |
| } |
| mutagen_.Mutate(&input); |
| ++num_mutations; |
| auto status = generated_sender_.Send(std::move(input)); |
| num_sent++; |
| if (status != ZX_OK) { |
| // Queue was closed; all done. |
| return fpromise::ok(); |
| } |
| } |
| }) |
| .and_then([this] { |
| generated_receiver_.Close(); |
| return fpromise::ok(); |
| }); |
| } |
| |
| Promise<> RealmFuzzerRunner::GenerateCleanInputs(AsyncReceiverPtr<Artifact> receiver, |
| size_t attempts_left) { |
| // Ensure that a new attempt will be started. |
| auto offset = std::numeric_limits<size_t>::max() - 1; |
| |
| // The general approach is to produce two inputs at a time, each with one byte replaced by a |
| // space or 0xff. Bytes that are already a space or 0xff are skipped. Each iteration over all |
| // input bytes is an attempt, and inputs are produced until an attempt doesn't produce any errors |
| // or five attempts have been performed. |
| return fpromise::make_promise([this, receiver = std::move(receiver), receive = Future<Artifact>(), |
| artifacts = std::array<Artifact, 2>(), num_artifacts = 0U, |
| attempts_left, offset, found_error = false, |
| original = uint8_t(0)](Context& context) mutable -> Result<> { |
| while (true) { |
| // Recycle two artifacts. |
| if (!receive) { |
| receive = receiver->Receive(); |
| } |
| if (!receive(context)) { |
| return fpromise::pending(); |
| } |
| if (receive.is_error()) { |
| FX_LOGS(ERROR) << "Recycled input queue closed unexpectedly."; |
| return fpromise::error(); |
| } |
| artifacts[num_artifacts++] = receive.take_value(); |
| if (num_artifacts < artifacts.size()) { |
| continue; |
| } |
| auto fuzz_result = artifacts[0].fuzz_result(); |
| auto input0 = artifacts[0].take_input(); |
| auto input1 = artifacts[1].take_input(); |
| auto* data0 = input0.data(); |
| auto* data1 = input1.data(); |
| if (fuzz_result == FuzzResult::NO_ERRORS) { |
| // Last inputs didn't trigger any errors; restore the modified byte. |
| data0[offset] = original; |
| data1[offset] = original; |
| } else { |
| found_error = true; |
| } |
| // Find a "cleanable" byte, i.e. one that isn't already 0x20 or 0xff. |
| do { |
| if (++offset < input0.size()) { |
| // Continue the current attempt. |
| continue; |
| } |
| // Reached the end of the input. Start a new attempt. |
| offset = 0; |
| if (--attempts_left == 0 || !found_error) { |
| // Out of attempts, or last attempt didn't trigger any error. All done. |
| receiver->Close(); |
| generated_receiver_.Close(); |
| return fpromise::ok(); |
| } |
| found_error = false; |
| } while (data0[offset] == 0x20 || data0[offset] == 0xff); |
| // Now actually clean the byte and send them to be tested. |
| original = data0[offset]; |
| data0[offset] = 0x20; |
| data1[offset] = 0xff; |
| if (generated_sender_.Send(std::move(input0)) != ZX_OK || |
| generated_sender_.Send(std::move(input1)) != ZX_OK) { |
| FX_LOGS(ERROR) << "Input queue unexpectedly closed."; |
| return fpromise::error(); |
| } |
| num_artifacts = 0; |
| } |
| }); |
| } |
| |
| /////////////////////////////////////////////////////////////// |
| // Methods to perform a sequence of fuzzing runs. |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::FuzzInputs(size_t backlog) { |
| size_t num_inputs = options()->runs(); |
| if (num_inputs != 0) { |
| // Always perform at least enough runs to test every input in the seed and live corpora. |
| // `num_fixed_inputs` cannot underflow, as the corpus always includes exactly one empty input, |
| // of which the one double-counted instance is removed. |
| // It also cannot overflow, as the combined `num_inputs` is less than the amount of memory being |
| // used (modulo the single empty input), which by definition must fit within a `size_t`. |
| size_t num_fixed_inputs = seed_corpus_->num_inputs() + live_corpus_->num_inputs() - 1; |
| if (num_inputs < num_fixed_inputs) { |
| num_inputs = num_fixed_inputs; |
| } else { |
| num_inputs -= num_fixed_inputs; |
| } |
| } |
| return TestOneAsync(Input(), kAccumulateCoverage) |
| .or_else([this](const zx_status_t& status) { |
| return CheckPrevious(status).and_then( |
| [this] { return TestCorpusAsync(seed_corpus_, kAccumulateCoverage); }); |
| }) |
| .or_else([this](const zx_status_t& status) { |
| return CheckPrevious(status).and_then( |
| [this] { return TestCorpusAsync(live_corpus_, kAccumulateCoverage); }); |
| }) |
| .or_else([this, backlog, num_inputs](const zx_status_t& status) { |
| live_corpus_->Add(seed_corpus_); |
| return CheckPrevious(status).and_then( |
| [generate = ZxFuture<>(GenerateInputs(num_inputs, backlog)), |
| test = ZxFuture<Artifact>(TestInputs(kAccumulateCoverageAndKeepInputs))]( |
| Context& context) mutable -> ZxResult<Artifact> { |
| if (generate(context) && generate.is_error()) { |
| return fpromise::error(generate.error()); |
| } |
| if (!test(context)) { |
| return fpromise::pending(); |
| } |
| return test.take_result(); |
| }); |
| }) |
| .or_else([this](const zx_status_t& status) { |
| return CheckPrevious(status).and_then([]() -> ZxResult<Artifact> { |
| // Finished without finding an input that causes an error; return an empty artifact. |
| return fpromise::ok(Artifact()); |
| }); |
| }); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::TestOneAsync(Input input, PostProcessing mode) { |
| return fpromise::make_promise([this, input = std::move(input)]() mutable -> ZxResult<> { |
| return AsZxResult(generated_sender_.Send(std::move(input))); |
| }) |
| .and_then([this]() { |
| generated_receiver_.Close(); |
| return fpromise::ok(); |
| }) |
| .and_then([this, mode] { return TestInputs(mode); }); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::TestCorpusAsync(CorpusPtr corpus, PostProcessing mode, |
| InputsPtr collect_errors) { |
| return fpromise::make_promise([this]() -> ZxResult<> { |
| // Prime the output queue. |
| Reset(); |
| return AsZxResult(processed_sender_.Send(Input())); |
| }) |
| .and_then([this, corpus, mode, collect_errors, test_inputs = ZxFuture<Artifact>(), |
| receive = Future<Input>(), |
| offset = 1U](Context& context) mutable -> ZxResult<Artifact> { |
| while (true) { |
| if (!test_inputs) { |
| test_inputs = TestInputs(mode, collect_errors); |
| } |
| if (!receive) { |
| receive = processed_receiver_.Receive(); |
| } |
| if (test_inputs(context)) { |
| // Done testing inputs. |
| Reset(); |
| return test_inputs.take_result(); |
| } |
| if (!receive(context)) { |
| // Still testing. |
| return fpromise::pending(); |
| } |
| // Ready for the next input from the corpus. |
| if (receive.is_error()) { |
| FX_LOGS(ERROR) << "Output queue closed prematurely."; |
| return fpromise::error(ZX_ERR_BAD_STATE); |
| } |
| auto input = receive.take_value(); |
| if (!corpus->At(offset++, &input)) { |
| generated_receiver_.Close(); |
| continue; |
| } |
| if (auto status = generated_sender_.Send(std::move(input)); status != ZX_OK) { |
| FX_LOGS(ERROR) << "Input queue closed prematurely: " << zx_status_get_string(status); |
| return fpromise::error(status); |
| } |
| } |
| }); |
| } |
| |
| ZxPromise<Artifact> RealmFuzzerRunner::TestInputs(PostProcessing mode, InputsPtr collect_errors) { |
| constexpr size_t kMaxLeakDetectionAttempts = 1000; |
| auto leak_detections = options()->detect_leaks() ? kMaxLeakDetectionAttempts : 0; |
| return fpromise::make_promise( |
| [this, mode, collect_errors, input = Input(), leak_detections, detect_leaks = false, |
| prepare = ZxFuture<Input>(), |
| run = Future<bool, FuzzResult>()](Context& context) mutable -> ZxResult<Artifact> { |
| while (true) { |
| // Reset process coverage and get a new input. |
| if (!prepare) { |
| prepare = Prepare(detect_leaks); |
| } |
| if (!prepare(context)) { |
| return fpromise::pending(); |
| } |
| // Make sure no errors have been received before testing an input. |
| if (prepare.is_error()) { |
| return fpromise::error(prepare.error()); |
| } |
| if (!run) { |
| input = std::move(prepare.value()); |
| run = RunOne(input); |
| } |
| // Now check if the run has finished and if any process reported an error. |
| if (!run(context)) { |
| return fpromise::pending(); |
| } |
| auto leak_suspected = false; |
| if (run.is_ok()) { |
| leak_suspected = run.take_value(); |
| } else if (collect_errors) { |
| // If collecting errors, clear errors and continue. Simulate already having attempted to |
| // |detect_leaks| to true to skip analysis and leak detection. |
| collect_errors->emplace_back(std::move(input)); |
| detect_leaks = true; |
| run = nullptr; |
| } else { |
| return fpromise::ok(Artifact(run.error(), std::move(input))); |
| } |
| // Skip post-processing when repeating inputs for leak detection. |
| if (!detect_leaks) { |
| Analyze(input, mode); |
| } |
| // Iteration complete! Clear the futures so that the loop starts from the top again. |
| detect_leaks = Recycle(std::move(input), leak_detections, leak_suspected, detect_leaks); |
| prepare = nullptr; |
| } |
| }); |
| } |
| |
| /////////////////////////////////////////////////////////////// |
| // Methods to perform individual steps of a single fuzzing run. |
| |
| ZxPromise<> RealmFuzzerRunner::CheckPrevious(zx_status_t status) { |
| return fpromise::make_promise([status]() -> ZxResult<> { |
| if (status != ZX_ERR_STOP) { |
| return fpromise::error(status); |
| } |
| return fpromise::ok(); |
| }); |
| } |
| |
| ZxPromise<Input> RealmFuzzerRunner::Prepare(bool detect_leaks) { |
| return fpromise::make_promise([this, detect_leaks]() { |
| // Send start signals. |
| std::vector<ZxPromise<>> starts; |
| for (auto& [target_id, process_proxy] : process_proxies_) { |
| starts.emplace_back(process_proxy->Start(detect_leaks)); |
| }; |
| // Wait for processes to acknowledge. |
| return fpromise::join_promise_vector(std::move(starts)); |
| }) |
| .then([](Result<std::vector<ZxResult<>>>& results) -> ZxResult<> { |
| for (auto& result : results.value()) { |
| if (result.is_error()) { |
| // Ideally, processes should only return errors as a result of testing inputs. |
| FX_LOGS(WARNING) |
| << "Detected error between fuzzing runs. This error cannot be associated " |
| "with a specific input. The fuzzer may be non-deterministic and/or " |
| "non-hermetic, and may need to be modified to make results more " |
| "reproducible."; |
| return fpromise::error(ZX_ERR_BAD_STATE); |
| } |
| } |
| return fpromise::ok(); |
| }) |
| .and_then([this, leak = Future<Input>(), |
| generate = Future<Input>()](Context& context) mutable -> ZxResult<Input> { |
| if (!leak) { |
| leak = leak_receiver_.Receive(); |
| } |
| if (!generate) { |
| generate = generated_receiver_.Receive(); |
| } |
| if (leak(context)) { |
| if (leak.is_error()) { |
| return fpromise::error(ZX_ERR_STOP); |
| } |
| return fpromise::ok(leak.take_value()); |
| } |
| if (generate(context)) { |
| if (generate.is_error()) { |
| // No more inputs means the workflow is done. |
| return fpromise::error(ZX_ERR_STOP); |
| } |
| return fpromise::ok(generate.take_value()); |
| } |
| return fpromise::pending(); |
| }); |
| } |
| |
| Promise<bool, FuzzResult> RealmFuzzerRunner::RunOne(const Input& input) { |
| return fpromise::make_promise([this, &input, run_limit = options()->run_limit(), |
| timeout = Future<>(), |
| first = true](Context& context) mutable -> Result<bool, uint64_t> { |
| // Create a future for the per-run timeout. If this completes, it's an error. |
| if (run_limit && !timeout) { |
| timeout = executor()->MakeDelayedPromise(zx::duration(run_limit)); |
| } |
| if (run_limit && timeout(context)) { |
| return fpromise::error(kTimeoutTargetId); |
| } |
| if (first) { |
| ++run_; |
| for (auto& [target_id, process_proxy] : process_proxies_) { |
| futures_.emplace_back(process_proxy->AwaitFinish()); |
| } |
| futures_.emplace_back(adapter_.TestOneInput(input) |
| .or_else([] { return fpromise::error(kInvalidTargetId); }) |
| .and_then([this]() -> Result<bool, uint64_t> { |
| for (auto& [target_id, process_proxy] : process_proxies_) { |
| if (auto status = process_proxy->Finish(); |
| status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to signal process: " |
| << zx_status_get_string(status); |
| } |
| } |
| return fpromise::ok(false); |
| })); |
| first = false; |
| } |
| auto all_done = true; |
| auto leak_suspected = false; |
| for (auto& future : futures_) { |
| if (!future(context)) { |
| all_done = false; |
| continue; |
| } |
| if (future.is_error()) { |
| return fpromise::error(future.error()); |
| } |
| leak_suspected |= future.value(); |
| } |
| if (!all_done) { |
| suspended_ = context.suspend_task(); |
| return fpromise::pending(); |
| } |
| return fpromise::ok(leak_suspected); |
| }) |
| .inspect([this](const Result<bool, uint64_t>& ignored) { futures_.clear(); }) |
| .or_else([this](const uint64_t& target_id) { return GetFuzzResult(target_id); }); |
| } |
| |
| void RealmFuzzerRunner::ConnectProcess(CoverageData coverage) { |
| auto process_proxy = std::make_unique<ProcessProxy>(executor(), pool_); |
| process_proxy->Configure(options()); |
| if (auto status = process_proxy->Connect(coverage.target_id, coverage.data.instrumented()); |
| status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to add process: " << zx_status_get_string(status); |
| return; |
| } |
| auto target_id = process_proxy->target_id(); |
| futures_.emplace_back(process_proxy->AwaitFinish()); |
| process_proxies_[target_id] = std::move(process_proxy); |
| // Kick |RunOne| to check the |AwaitFinish| future. |
| suspended_.resume_task(); |
| } |
| |
| void RealmFuzzerRunner::AddInline8bitCounters(CoverageData coverage) { |
| auto iter = process_proxies_.find(coverage.target_id); |
| if (iter == process_proxies_.end()) { |
| FX_LOGS(WARNING) << "Failed to add module: no such target_id: " << coverage.target_id; |
| return; |
| } |
| auto& process_proxy = iter->second; |
| if (auto status = process_proxy->AddInline8bitCounters(coverage.data.inline_8bit_counters()); |
| status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to add module: " << zx_status_get_string(status); |
| return; |
| } |
| } |
| |
| Promise<bool, FuzzResult> RealmFuzzerRunner::GetFuzzResult(uint64_t target_id) { |
| return fpromise::make_promise( |
| [this, target_id, process_proxy = std::unique_ptr<ProcessProxy>(), |
| result = ZxFuture<FuzzResult>()](Context& context) mutable -> ZxResult<FuzzResult> { |
| if (target_id == kTimeoutTargetId) { |
| // For timeouts, dump all threads to the sanitizer log. |
| constexpr size_t kBufSize = 1ULL << 20; |
| auto buf = std::make_unique<char[]>(kBufSize); |
| for (auto& [target_id, proxy] : process_proxies_) { |
| auto len = proxy->Dump(buf.get(), kBufSize); |
| __sanitizer_log_write(buf.get(), len); |
| } |
| return fpromise::ok(FuzzResult::TIMEOUT); |
| } |
| if (!result) { |
| // For all other errors, wait on the result from the process exitcode. |
| auto iter = process_proxies_.find(target_id); |
| if (iter == process_proxies_.end()) { |
| FX_LOGS(ERROR) << "Cannot get error from unknown target_id: 0x" << std::hex |
| << target_id; |
| return fpromise::error(ZX_ERR_NOT_FOUND); |
| } |
| process_proxy = std::move(iter->second); |
| process_proxies_.erase(iter); |
| result = process_proxy->GetResult(); |
| } |
| if (!result(context)) { |
| return fpromise::pending(); |
| } |
| return result.take_result(); |
| }) |
| .or_else([](const zx_status_t& status) -> Result<FuzzResult, FuzzResult> { |
| FX_LOGS(WARNING) << "Failed to get result: " << zx_status_get_string(status); |
| FX_LOGS(WARNING) << "Defaulting to error type of 'crash'."; |
| return fpromise::ok(FuzzResult::CRASH); |
| }) |
| .and_then([this](const FuzzResult& fuzz_result) -> Result<bool, FuzzResult> { |
| if (fuzz_result == FuzzResult::NO_ERRORS) { |
| return fpromise::ok(false); |
| } |
| // If it's an ignored exit(),just remove that one process_proxy and treat it like a success. |
| if (fuzz_result == FuzzResult::EXIT && !options()->detect_exits()) { |
| return fpromise::ok(false); |
| } |
| // Otherwise, it's really an error. Remove the target adapter and all proxies. |
| adapter_.Disconnect(); |
| process_proxies_.clear(); |
| return fpromise::error(fuzz_result); |
| }); |
| } |
| |
| void RealmFuzzerRunner::Analyze(Input& input, PostProcessing mode) { |
| bool updated = false; |
| switch (mode) { |
| case kNoPostProcessing: { |
| break; |
| } |
| case kAccumulateCoverage: { |
| pool_->Accumulate(); |
| break; |
| } |
| case kMeasureCoverageAndKeepInputs: { |
| auto num_features = pool_->Measure(); |
| if (num_features) { |
| input.set_num_features(num_features); |
| if (auto status = live_corpus_->Add(std::move(input)); status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to save input: " << zx_status_get_string(status); |
| } |
| } |
| break; |
| } |
| case kAccumulateCoverageAndKeepInputs: { |
| if (pool_->Accumulate()) { |
| if (auto status = live_corpus_->Add(std::move(input)); status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to save input: " << zx_status_get_string(status); |
| } |
| UpdateMonitors(UpdateReason::NEW); |
| updated = true; |
| } |
| break; |
| } |
| } |
| // After a few seconds, reassure the user that the fuzzer is running by reporting each run that |
| // equals a power of 2, unless it was already reported above. |
| if (!updated && (run_ & (run_ - 1)) == 0 && zx::clock::get_monotonic() > pulse_start_) { |
| UpdateMonitors(UpdateReason::PULSE); |
| } |
| } |
| |
| bool RealmFuzzerRunner::Recycle(Input&& input, size_t& attempts_left, bool suspected, |
| bool detecting) { |
| // Determine if leak detection is needed and thereby where to send the input. Leak detection is |
| // expensive, so the strategy is as follows: |
| // 1. Try inputs once without leak detection. |
| // 2. If leak detection is requested, check if leaks are suspected (unbalanced malloc/frees). |
| // 3. If a leak if suspected, do the normal feedback analysis and then try the input again, this |
| // time with leak detection. Skip the feedback analysis on the second try. |
| // 4. Keep track of how many suspected leaks don't result in an error. After |
| // |kMaxLeakDetections|, disable further leak detection. |
| if (attempts_left) { |
| if (detecting) { |
| // Already tried detecting a leak. Decrement the number of attempts. |
| --attempts_left; |
| if (attempts_left == 0) { |
| FX_LOGS(INFO) << "Disabling leak detection: No memory leaks have been found in any inputs " |
| << "suspected of leaking. Memory may be accumulating in some global state " |
| << "without leaking. End-of-process leak checks will still be performed."; |
| } |
| } else if (suspected) { |
| // Leak detection is still possible, and the last run exhibited a suspected leak. Push the |
| // input to the front of the queue to retry with leak detection. |
| |
| if (auto status = leak_sender_.Send(std::move(input)); status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to resend input: " << zx_status_get_string(status); |
| } else { |
| return true; |
| } |
| } |
| } |
| // Send input to be recycled. |
| if (auto status = processed_sender_.Send(std::move(input)); status != ZX_OK) { |
| FX_LOGS(WARNING) << "Failed to recycle input: " << zx_status_get_string(status); |
| } |
| return false; |
| } |
| |
| /////////////////////////////////////////////////////////////// |
| // Clean-up methods. |
| |
| void RealmFuzzerRunner::Disconnect() { |
| adapter_.Disconnect(); |
| process_proxies_.clear(); |
| } |
| |
| void RealmFuzzerRunner::Reset() { |
| generated_receiver_.Reset(); |
| processed_receiver_.Reset(); |
| leak_receiver_.Reset(); |
| } |
| |
| } // namespace fuzzing |