| // Copyright 2023 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/stats.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <sstream> |
| |
| #include <re2/re2.h> |
| |
| namespace fuzzing { |
| |
| using fuchsia::fuzzer::ProcessStats; |
| |
| bool ParseLibFuzzerStats(std::string_view line, UpdateReason* reason, Status* status) { |
| re2::StringPiece input(line); |
| uint32_t runs; |
| if (!re2::RE2::Consume(&input, "#(\\d+)", &runs)) { |
| return false; |
| } |
| |
| // Parse reason. |
| std::string reason_str; |
| if (!re2::RE2::Consume(&input, "\\t(\\S+)", &reason_str)) { |
| return false; |
| } |
| |
| // Is `runs` valid? |
| uint64_t scaled_runs; |
| if (runs == 0 || mul_overflow(runs, kOneSecond, &scaled_runs)) { |
| return false; |
| } |
| status->set_runs(runs); |
| |
| *reason = UpdateReason::PULSE; // By default, assume it's just a status update. |
| if (reason_str == "INITED") { |
| *reason = UpdateReason::INIT; |
| } else if (reason_str == "NEW") { |
| *reason = UpdateReason::NEW; |
| } else if (reason_str == "REDUCE") { |
| *reason = UpdateReason::REDUCE; |
| } else if (reason_str == "DONE") { |
| *reason = UpdateReason::DONE; |
| status->set_running(false); |
| } |
| |
| // Parse covered PCs. |
| size_t covered_pcs; |
| if (re2::RE2::FindAndConsume(&input, "cov: (\\d+)", &covered_pcs)) { |
| status->set_covered_pcs(covered_pcs); |
| } |
| |
| // Parse covered features. |
| size_t covered_features; |
| if (re2::RE2::FindAndConsume(&input, "ft: (\\d+)", &covered_features)) { |
| status->set_covered_features(covered_features); |
| } |
| |
| // Parse corpus stats. |
| size_t corpus_num_inputs; |
| if (re2::RE2::FindAndConsume(&input, "corp: (\\d+)", &corpus_num_inputs)) { |
| size_t corpus_total_size; |
| status->set_corpus_num_inputs(corpus_num_inputs); |
| if (re2::RE2::Consume(&input, "/(\\d+)b", &corpus_total_size)) { |
| status->set_corpus_total_size(corpus_total_size); |
| } else if (re2::RE2::Consume(&input, "/(\\d+)Kb", &corpus_total_size)) { |
| status->set_corpus_total_size(corpus_total_size * kOneKb); |
| } else if (re2::RE2::Consume(&input, "/(\\d+)Mb", &corpus_total_size)) { |
| status->set_corpus_total_size(corpus_total_size * kOneMb); |
| } |
| } |
| |
| // Best effort: libFuzzer does not track total elapsed time, but it does have number of runs and |
| // runs per second. |
| int64_t execs_per_second; |
| if (re2::RE2::FindAndConsume(&input, "exec/s: (\\d+)", &execs_per_second)) { |
| if (execs_per_second != 0 && scaled_runs < std::numeric_limits<int64_t>::max()) { |
| int64_t elapsed = static_cast<int64_t>(scaled_runs) / execs_per_second; |
| if (!status->has_elapsed() || status->elapsed() < elapsed) { |
| status->set_elapsed(elapsed); |
| } |
| } |
| } |
| |
| // Best effort: the libFuzzer output obviously does not have `ZX_INFO_TASK_*` details. Instead, |
| // this will add a minimal `ProcessStats` with a rough estimate of memory consumed if the status |
| // does not have more detailed process info. These `ProcessStats` are easily identifiable by |
| // having an invalid KOID. |
| if (!status->has_process_stats() || |
| (status->process_stats().size() == 1 && status->process_stats()[0].koid == ZX_KOID_INVALID)) { |
| size_t rss_mb; |
| if (re2::RE2::FindAndConsume(&input, "rss: (\\d+)Mb", &rss_mb)) { |
| std::vector<ProcessStats> process_stats(1); |
| process_stats[0].koid = ZX_KOID_INVALID; |
| process_stats[0].mem_private_bytes = rss_mb * kOneMb; |
| status->set_process_stats(std::move(process_stats)); |
| } |
| } |
| |
| return true; |
| } |
| |
| std::string FormatLibFuzzerStats(UpdateReason reason, const Status& status) { |
| FX_CHECK(status.has_runs()); |
| int64_t runs = status.runs(); |
| |
| // Since `status.runs` is a `u32`, this scaling should always succeed. |
| int64_t scaled_runs; |
| FX_CHECK(!mul_overflow(runs, kOneSecond, &scaled_runs)); |
| |
| std::ostringstream oss; |
| oss << "#" << runs << "\t"; |
| |
| switch (reason) { |
| case UpdateReason::INIT: |
| oss << "INITED"; |
| break; |
| case UpdateReason::NEW: |
| oss << "NEW "; |
| break; |
| case UpdateReason::PULSE: |
| oss << "pulse "; |
| break; |
| case UpdateReason::REDUCE: |
| oss << "REDUCE"; |
| break; |
| case UpdateReason::DONE: |
| oss << "DONE "; |
| break; |
| } |
| |
| if (status.has_covered_pcs()) { |
| if (auto covered_pcs = status.covered_pcs()) { |
| oss << " cov: " << covered_pcs; |
| } |
| } |
| |
| if (status.has_covered_features()) { |
| if (auto covered_features = status.covered_features()) { |
| oss << " ft: " << covered_features; |
| } |
| } |
| |
| if (status.has_corpus_num_inputs()) { |
| if (auto corpus_num_inputs = status.corpus_num_inputs()) { |
| oss << " corp: " << corpus_num_inputs; |
| if (status.has_corpus_total_size()) { |
| auto corpus_total_size = status.corpus_total_size(); |
| oss << "/"; |
| if (corpus_total_size >= kOneMb) { |
| oss << (corpus_total_size / kOneMb) << "Mb"; |
| } else if (corpus_total_size >= kOneKb) { |
| oss << (corpus_total_size / kOneKb) << "Kb"; |
| } else if (corpus_total_size) { |
| oss << corpus_total_size << "b"; |
| } |
| } |
| } |
| } |
| |
| oss << " exec/s: "; |
| if (status.has_elapsed()) { |
| auto elapsed = status.elapsed(); |
| oss << scaled_runs / elapsed; |
| } else { |
| oss << 0; |
| } |
| |
| oss << " rss: "; |
| if (status.has_process_stats()) { |
| size_t mem_bytes = 0; |
| for (const auto& process_stats : status.process_stats()) { |
| mem_bytes += process_stats.mem_private_bytes + process_stats.mem_scaled_shared_bytes; |
| } |
| oss << ((mem_bytes + kOneMb - 1) / kOneMb); |
| } else { |
| oss << 0; |
| } |
| oss << "Mb"; |
| |
| return oss.str(); |
| } |
| |
| } // namespace fuzzing |