| // Copyright 2018 The Chromium OS 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 "crash-reporter/anomaly_detector.h" |
| |
| #include <optional> |
| #include <memory> |
| #include <unordered_set> |
| #include <utility> |
| |
| #include <anomaly_detector/proto_bindings/anomaly_detector.pb.h> |
| #include <base/logging.h> |
| #include <base/rand_util.h> |
| #include <base/strings/strcat.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <chromeos/dbus/service_constants.h> |
| #include <dbus/cryptohome/dbus-constants.h> |
| #include <dbus/bus.h> |
| #include <dbus/exported_object.h> |
| #include <dbus/message.h> |
| #include <metrics/metrics_library.h> |
| #include <re2/re2.h> |
| |
| #include "crash-reporter/util.h" |
| |
| namespace { |
| |
| // This hashing algorithm dates back to before this was migrated from C to C++. |
| // We're stuck with it now because we would like the hashes to remain the same |
| // over time for a given crash as the hashes are used in the crash signatures. |
| uint32_t StringHash(const std::string& input) { |
| uint32_t hash = 0; |
| for (auto& c : input) { |
| hash = (hash << 5) + hash + c; |
| } |
| return hash; |
| } |
| |
| std::string OnlyAsciiAlpha(std::string s) { |
| s.erase(std::remove_if(s.begin(), s.end(), |
| [](char c) -> bool { return !base::IsAsciiAlpha(c); }), |
| s.end()); |
| return s; |
| } |
| |
| } // namespace |
| |
| namespace anomaly { |
| |
| CrashReport::CrashReport(std::string t, std::vector<std::string> f) |
| : text(std::move(t)), flags(std::move(f)) {} |
| |
| bool operator==(const CrashReport& lhs, const CrashReport& rhs) { |
| return lhs.text == rhs.text && lhs.flags == rhs.flags; |
| } |
| |
| std::ostream& operator<<(std::ostream& out, const CrashReport& cr) { |
| out << "{.text='" << cr.text << "', .flags={" |
| << base::JoinString(cr.flags, " ") << "}}"; |
| return out; |
| } |
| |
| Parser::~Parser() {} |
| |
| // We expect only a handful of different anomalies per boot session, so the |
| // probability of a collision is very low, and statistically it won't matter |
| // (unless anomalies with the same hash also happens in tandem, which is even |
| // rarer). |
| bool Parser::WasAlreadySeen(uint32_t hash) { |
| size_t bit_index = hash % HASH_BITMAP_SIZE; |
| bool return_val = hash_bitmap_[bit_index]; |
| hash_bitmap_.set(bit_index, true); |
| return return_val; |
| } |
| |
| MaybeCrashReport Parser::PeriodicUpdate() { |
| return std::nullopt; |
| } |
| |
| constexpr LazyRE2 service_failure = { |
| R"((\S+) \S+ process \(\d+\) terminated with status (\d+))"}; |
| |
| ServiceParser::ServiceParser(bool testonly_send_all) |
| : testonly_send_all_(testonly_send_all) {} |
| |
| MaybeCrashReport ServiceParser::ParseLogEntry(const std::string& line) { |
| std::string service_name; |
| std::string exit_status; |
| if (!RE2::FullMatch(line, *service_failure, &service_name, &exit_status)) |
| return std::nullopt; |
| |
| if (service_name == "cros-camera") { |
| // cros-camera uses non-zero exit status to indicate transient failures and |
| // to request that the service be re-started. This is 'nominal' and should |
| // not be reported. (It's also flooding our servers.) |
| return std::nullopt; |
| } |
| |
| // We only want to report a limited number of service failures due to noise. |
| if (!testonly_send_all_ && |
| base::RandGenerator(util::GetServiceFailureWeight()) != 0) { |
| return std::nullopt; |
| } |
| |
| uint32_t hash = StringHash(service_name.c_str()); |
| |
| if (WasAlreadySeen(hash)) |
| return std::nullopt; |
| |
| std::string text = base::StringPrintf( |
| "%08x-exit%s-%s\n", hash, exit_status.c_str(), service_name.c_str()); |
| std::string flag; |
| if (base::StartsWith(service_name, "arc-", base::CompareCase::SENSITIVE)) |
| flag = "--arc_service_failure=" + service_name; |
| else |
| flag = "--service_failure=" + service_name; |
| return CrashReport(std::move(text), {std::move(flag)}); |
| } |
| |
| std::string GetField(const std::string& line, std::string pattern) { |
| std::string field_value; |
| RE2::PartialMatch(line, pattern, &field_value); |
| // This will return the empty string if there wasn't a match. |
| return field_value; |
| } |
| |
| // Make cursory checks on specific fields in the selinux audit report to see if |
| // the content is a CrOS selinux violation. |
| bool IsCrosSelinuxViolation(const std::vector<std::string>& contents) { |
| for (const std::string& s : contents) { |
| if (s.find("cros") != std::string::npos) { |
| return true; |
| } |
| if (s.find("minijail") != std::string::npos) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| constexpr LazyRE2 granted = {"avc:[ ]*granted"}; |
| |
| SELinuxParser::SELinuxParser(bool testonly_send_all) |
| : testonly_send_all_(testonly_send_all) {} |
| |
| MaybeCrashReport SELinuxParser::ParseLogEntry(const std::string& line) { |
| // Ignore permissive "errors". These are extremely common and don't have any |
| // real impact. The noise from them would crowd out other crashes that have |
| // a more significant impact. |
| if (line.find("permissive=1") != std::string::npos) { |
| return std::nullopt; |
| } |
| |
| // We only want to report 0.1% of selinux violations due to noise. |
| if (!testonly_send_all_ && |
| base::RandGenerator(util::GetSelinuxWeight()) != 0) { |
| return std::nullopt; |
| } |
| |
| std::string only_alpha = OnlyAsciiAlpha(line); |
| uint32_t hash = StringHash(only_alpha.c_str()); |
| if (WasAlreadySeen(hash)) |
| return std::nullopt; |
| |
| std::string signature; |
| |
| // This case is strange: the '-' is only added if 'granted' was present. |
| if (RE2::PartialMatch(line, *granted)) |
| signature += "granted-"; |
| |
| std::string scontext = GetField(line, R"(scontext=(\S*))"); |
| std::string tcontext = GetField(line, R"(tcontext=(\S*))"); |
| std::string permission = GetField(line, R"(\{ (\S*) \})"); |
| std::string comm = GetField(line, R"'(comm="([^"]*)")'"); |
| std::string name = GetField(line, R"'(name="([^"]*)")'"); |
| |
| // Ignore ARC++, and other non-CrOS, errors. They are extremely common and |
| // largely not used anyway, providing a lot of noise. |
| // (We do this by checking scontext, tcontext, and comm for certain known-CrOS |
| // strings.) |
| if (!IsCrosSelinuxViolation({scontext, tcontext, comm})) { |
| if (testonly_send_all_) { |
| // For tests, log something that we can match on to make sure |
| // anomaly_detector saw the line and ignored it. |
| LOG(INFO) << "Skipping non-CrOS selinux violation: " << line; |
| } |
| return std::nullopt; |
| } |
| |
| signature += base::JoinString({scontext, tcontext, permission, |
| OnlyAsciiAlpha(comm), OnlyAsciiAlpha(name)}, |
| "-"); |
| std::string text = |
| base::StringPrintf("%08x-selinux-%s\n", hash, signature.c_str()); |
| |
| if (!comm.empty()) |
| text += "comm\x01" + comm + "\x02"; |
| if (!name.empty()) |
| text += "name\x01" + name + "\x02"; |
| if (!scontext.empty()) |
| text += "scontext\x01" + scontext + "\x02"; |
| if (!tcontext.empty()) |
| text += "tcontext\x01" + tcontext + "\x02"; |
| text += "\n"; |
| text += line; |
| |
| return CrashReport(std::move(text), {"--selinux_violation"}); |
| } |
| |
| std::string DetermineFlag(const std::string& info) { |
| // Paths like: |
| // drivers/net/wireless/... |
| // net/wireless/... |
| // net/mac80211/... |
| if (info.find("net/wireless") != std::string::npos || |
| info.find("net/mac80211") != std::string::npos) |
| return "--kernel_wifi_warning"; |
| if (info.find("drivers/idle") != std::string::npos) |
| return "--kernel_suspend_warning"; |
| |
| return "--kernel_warning"; |
| } |
| |
| constexpr LazyRE2 start_ath10k_dump = {R"(ath10k_.*firmware crashed!)"}; |
| constexpr LazyRE2 end_ath10k_dump = {R"(ath10k_.*htt-ver)"}; |
| constexpr LazyRE2 tag_ath10k_dump = {R"(ath10k_)"}; |
| |
| // Older wifi chips have lmac dump only and newer wifi chips have lmac followed |
| // by umac dumps. The KernelParser should parse the dumps accordingly. |
| // The following regexp identify the beginning of the iwlwifi dump. |
| constexpr LazyRE2 start_iwlwifi_dump = {R"(iwlwifi.*Loaded firmware version:)"}; |
| |
| // The following regexp separates the umac and lmac. |
| constexpr LazyRE2 start_iwlwifi_dump_umac = {R"(Start IWL Error Log Dump(.+))"}; |
| // The following regexps identify the iwlwifi error dump end. |
| constexpr LazyRE2 end_iwlwifi_dump_umac = {R"((.+)isr status reg)"}; |
| constexpr LazyRE2 end_iwlwifi_dump_lmac = {R"((.+)flow_handler)"}; |
| |
| constexpr char cut_here[] = "------------[ cut here"; |
| constexpr char end_trace[] = "---[ end trace"; |
| constexpr char crash_report_rlimit[] = |
| "(crash_reporter) has RLIMIT_CORE set to"; |
| |
| // The CPU and PID information got added in the 3.11 kernel development cycle |
| // per commit dcb6b45254e2281b6f99ea7f2d51343954aa3ba8. That part is marked |
| // optional to make sure the old format still gets accepted. Once we no longer |
| // care about kernel version 3.10 and earlier, we can update the code to require |
| // CPU and PID to be present unconditionally. |
| constexpr LazyRE2 header = { |
| R"(^\[\s*\S+\] WARNING:(?: CPU: \d+ PID: \d+)? at (.+))"}; |
| |
| constexpr LazyRE2 smmu_fault = {R"(Unhandled context fault: fsr=0x)"}; |
| |
| KernelParser::KernelParser(bool testonly_send_all) |
| : testonly_send_all_(testonly_send_all) {} |
| |
| MaybeCrashReport KernelParser::ParseLogEntry(const std::string& line) { |
| if (last_line_ == LineType::None) { |
| if (line.find(cut_here) != std::string::npos) |
| last_line_ = LineType::Start; |
| } else if (last_line_ == LineType::Start || last_line_ == LineType::Header) { |
| std::string info; |
| if (RE2::FullMatch(line, *header, &info)) { |
| // The info string looks like: "file:line func+offset/offset() [mod]". |
| // The [mod] suffix is only present if the address is located within a |
| // kernel module. |
| uint32_t hash = StringHash(info.c_str()); |
| if (WasAlreadySeen(hash)) { |
| last_line_ = LineType::None; |
| return std::nullopt; |
| } |
| flag_ = DetermineFlag(info); |
| |
| size_t spacep = info.find(" "); |
| bool found = spacep != std::string::npos; |
| auto function = found ? info.substr(spacep + 1) : "unknown-function"; |
| |
| text_ += base::StringPrintf("%08x-%s\n", hash, function.c_str()); |
| text_ += base::StringPrintf("%s\n", info.c_str()); |
| last_line_ = LineType::Body; |
| } else if (last_line_ == LineType::Start) { |
| // Allow for a single header line between the "cut here" and the "WARNING" |
| last_line_ = LineType::Header; |
| text_ += line + "\n"; |
| } else { |
| last_line_ = LineType::None; |
| } |
| } else if (last_line_ == LineType::Body) { |
| if (line.find(end_trace) != std::string::npos) { |
| last_line_ = LineType::None; |
| std::string text_tmp; |
| text_tmp.swap(text_); |
| |
| // Sample kernel warnings since they are too noisy and overload the crash |
| // server. (See http://b/185156234.) |
| const int kWeight = util::GetKernelWarningWeight(flag_); |
| if (!testonly_send_all_ && base::RandGenerator(kWeight) != 0) { |
| return std::nullopt; |
| } |
| return CrashReport( |
| std::move(text_tmp), |
| {std::move(flag_), base::StringPrintf("--weight=%d", kWeight)}); |
| } |
| text_ += line + "\n"; |
| } |
| |
| if (ath10k_last_line_ == Ath10kLineType::None) { |
| if (RE2::PartialMatch(line, *start_ath10k_dump)) { |
| ath10k_last_line_ = Ath10kLineType::Start; |
| ath10k_text_ += line + "\n"; |
| } |
| } else if (ath10k_last_line_ == Ath10kLineType::Start) { |
| // Return if the end_ath10k_dump is reached or the log tag doesn't match |
| // tag_ath10k_dump. |
| if (RE2::PartialMatch(line, *end_ath10k_dump) || |
| !RE2::PartialMatch(line, *tag_ath10k_dump)) { |
| ath10k_last_line_ = Ath10kLineType::None; |
| if (RE2::PartialMatch(line, *end_ath10k_dump)) { |
| ath10k_text_ += line + "\n"; |
| } |
| std::string ath10k_text_tmp; |
| ath10k_text_tmp.swap(ath10k_text_); |
| |
| const std::string kFlag = "--kernel_ath10k_error"; |
| const int kWeight = util::GetKernelWarningWeight(kFlag); |
| if (!testonly_send_all_ && base::RandGenerator(kWeight) != 0) { |
| return std::nullopt; |
| } |
| |
| return CrashReport( |
| std::move(ath10k_text_tmp), |
| {std::move(kFlag), base::StringPrintf("--weight=%d", kWeight)}); |
| } |
| |
| ath10k_text_ += line + "\n"; |
| } |
| |
| if (iwlwifi_last_line_ == IwlwifiLineType::None) { |
| if (RE2::PartialMatch(line, *start_iwlwifi_dump)) { |
| iwlwifi_last_line_ = IwlwifiLineType::Start; |
| iwlwifi_text_ += line + "\n"; |
| } |
| } else if (iwlwifi_last_line_ == IwlwifiLineType::Start) { |
| if (RE2::PartialMatch(line, *end_iwlwifi_dump_lmac)) { |
| iwlwifi_last_line_ = IwlwifiLineType::Lmac; |
| } else if (RE2::PartialMatch(line, *end_iwlwifi_dump_umac)) { |
| // Return if the line is equal to the umac end. There is never anything |
| // after the umac end. |
| iwlwifi_last_line_ = IwlwifiLineType::None; |
| iwlwifi_text_ += line + "\n"; |
| std::string iwlwifi_text_tmp; |
| iwlwifi_text_tmp.swap(iwlwifi_text_); |
| |
| const std::string kFlag = "--kernel_iwlwifi_error"; |
| const int kWeight = util::GetKernelWarningWeight(kFlag); |
| if (!testonly_send_all_ && base::RandGenerator(kWeight) != 0) { |
| return std::nullopt; |
| } |
| |
| return CrashReport( |
| std::move(iwlwifi_text_tmp), |
| {std::move(kFlag), base::StringPrintf("--weight=%d", kWeight)}); |
| } |
| iwlwifi_text_ += line + "\n"; |
| } else if (iwlwifi_last_line_ == IwlwifiLineType::Lmac) { |
| // Check if there is an umac dump. |
| if (RE2::PartialMatch(line, *start_iwlwifi_dump_umac)) { |
| iwlwifi_last_line_ = IwlwifiLineType::Start; |
| iwlwifi_text_ += line + "\n"; |
| } else { |
| // Return if there is no umac. |
| iwlwifi_last_line_ = IwlwifiLineType::None; |
| std::string iwlwifi_text_tmp; |
| iwlwifi_text_tmp.swap(iwlwifi_text_); |
| |
| const std::string kFlag = "--kernel_iwlwifi_error"; |
| const int kWeight = util::GetKernelWarningWeight(kFlag); |
| if (!testonly_send_all_ && base::RandGenerator(kWeight) != 0) { |
| return std::nullopt; |
| } |
| |
| return CrashReport( |
| std::move(iwlwifi_text_tmp), |
| {std::move(kFlag), base::StringPrintf("--weight=%d", kWeight)}); |
| } |
| } |
| |
| if (RE2::PartialMatch(line, *smmu_fault)) { |
| std::string smmu_text_tmp = line + "\n"; |
| return CrashReport(std::move(smmu_text_tmp), |
| {std::move("--kernel_smmu_fault")}); |
| } |
| |
| if (line.find(crash_report_rlimit) != std::string::npos) { |
| LOG(INFO) << "crash_reporter crashed!"; |
| // Rate limit reporting crash_reporter failures to prevent crash loops. |
| if (crash_reporter_last_crashed_.is_null() || |
| (base::TimeTicks::Now() - crash_reporter_last_crashed_) > |
| base::Hours(1)) { |
| crash_reporter_last_crashed_ = base::TimeTicks::Now(); |
| return CrashReport("", {std::move("--crash_reporter_crashed")}); |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| constexpr char begin_suspend_error_stats[] = |
| "Error writing to /sys/power/state: "; |
| constexpr char end_suspend_error_stats[] = |
| "--- end /sys/kernel/debug/suspend_stats ---"; |
| constexpr LazyRE2 last_failed_dev = {R"(\s*last_failed_dev: (.+))"}; |
| constexpr LazyRE2 last_failed_errno = {R"(\s*last_failed_errno: (.+))"}; |
| constexpr LazyRE2 last_failed_step = {R"(\s*last_failed_step: (.+))"}; |
| |
| SuspendParser::SuspendParser(bool testonly_send_all) |
| : testonly_send_all_(testonly_send_all) {} |
| |
| MaybeCrashReport SuspendParser::ParseLogEntry(const std::string& line) { |
| // We only want to report a fraction of suspend failures due to noise. |
| if (!testonly_send_all_ && |
| base::RandGenerator(util::GetSuspendFailureWeight()) != 0) { |
| return std::nullopt; |
| } |
| |
| if (last_line_ == LineType::None && |
| line.find(begin_suspend_error_stats) == 0) { |
| last_line_ = LineType::Start; |
| dev_str_ = "none"; |
| errno_str_ = "unknown"; |
| step_str_ = "unknown"; |
| return std::nullopt; |
| } |
| |
| if (last_line_ != LineType::Start && last_line_ != LineType::Body) { |
| return std::nullopt; |
| } |
| |
| if (line.find(end_suspend_error_stats) != 0) { |
| std::string info; |
| if (RE2::FullMatch(line, *last_failed_dev, &info)) { |
| dev_str_ = info; |
| } else if (RE2::FullMatch(line, *last_failed_errno, &info)) { |
| errno_str_ = info; |
| } else if (RE2::FullMatch(line, *last_failed_step, &info)) { |
| step_str_ = info; |
| } |
| |
| last_line_ = LineType::Body; |
| return std::nullopt; |
| } |
| |
| uint32_t hash = StringHash((dev_str_ + errno_str_ + step_str_).c_str()); |
| std::string text = base::StringPrintf( |
| "%08x-suspend failure: device: %s step: %s errno: %s\n", hash, |
| dev_str_.c_str(), step_str_.c_str(), errno_str_.c_str()); |
| return CrashReport(std::move(text), {"--suspend_failure"}); |
| } |
| |
| TerminaParser::TerminaParser( |
| scoped_refptr<dbus::Bus> dbus, |
| std::unique_ptr<MetricsLibraryInterface> metrics_lib) |
| : dbus_(dbus), metrics_lib_(std::move(metrics_lib)) { |
| metrics_lib_->Init(); |
| } |
| |
| constexpr LazyRE2 btrfs_extent_corruption = { |
| R"(BTRFS warning \(device .*\): csum failed root [[:digit:]]+ )" |
| R"(ino [[:digit:]]+ off [[:digit:]]+ csum 0x[[:xdigit:]]+ expected )" |
| R"(csum 0x[[:xdigit:]]+ mirror [[:digit:]]+)"}; |
| constexpr LazyRE2 btrfs_tree_node_corruption = { |
| R"(BTRFS warning \(device .*\): .* checksum verify failed on )" |
| R"([[:digit:]]+ wanted (0x)?[[:xdigit:]]+ found (0x)?[[:xdigit:]]+ level )" |
| R"([[:digit:]]+)"}; |
| constexpr char kUMAOomEvent[] = "Crostini.OomEvent"; |
| |
| MaybeCrashReport TerminaParser::ParseLogEntryForBtrfs(int cid, |
| const std::string& line) { |
| if (!RE2::PartialMatch(line, *btrfs_extent_corruption) && |
| !RE2::PartialMatch(line, *btrfs_tree_node_corruption)) { |
| return std::nullopt; |
| } |
| |
| anomaly_detector::GuestFileCorruptionSignal message; |
| message.set_vsock_cid(cid); |
| dbus::Signal signal(anomaly_detector::kAnomalyEventServiceInterface, |
| anomaly_detector::kAnomalyGuestFileCorruptionSignalName); |
| |
| dbus::MessageWriter writer(&signal); |
| writer.AppendProtoAsArrayOfBytes(message); |
| |
| dbus::ExportedObject* exported_object = dbus_->GetExportedObject( |
| dbus::ObjectPath(anomaly_detector::kAnomalyEventServicePath)); |
| exported_object->SendSignal(&signal); |
| |
| // Don't send a crash report here, because the gap between when the |
| // corruption occurs and when we detect it can be arbitrarily large. |
| return std::nullopt; |
| } |
| |
| constexpr LazyRE2 oom_event = { |
| R"(Out of memory: Killed process ([[:digit:]]+) \(.*\) total-vm:)" |
| R"([[:digit:]]+kB, anon-rss:[[:digit:]]+kB, file-rss:[[:digit:]]+kB, )" |
| R"(shmem-rss:[[:digit:]]+kB, UID:[[:digit:]]+ pgtables:[[:digit:]]+kB )" |
| R"(oom_score_adj:[[:digit:]]+)"}; |
| |
| MaybeCrashReport TerminaParser::ParseLogEntryForOom(int cid, |
| const std::string& line) { |
| if (!RE2::PartialMatch(line, *oom_event)) { |
| return std::nullopt; |
| } |
| |
| anomaly_detector::GuestOomEventSignal message; |
| message.set_vsock_cid(cid); |
| dbus::Signal signal(anomaly_detector::kAnomalyEventServiceInterface, |
| anomaly_detector::kAnomalyGuestOomEventSignalName); |
| |
| dbus::MessageWriter writer(&signal); |
| writer.AppendProtoAsArrayOfBytes(message); |
| |
| dbus::ExportedObject* exported_object = dbus_->GetExportedObject( |
| dbus::ObjectPath(anomaly_detector::kAnomalyEventServicePath)); |
| exported_object->SendSignal(&signal); |
| |
| if (!metrics_lib_->SendCrosEventToUMA(kUMAOomEvent)) { |
| LOG(WARNING) << "Could not send OomEvent metric to UMA"; |
| } |
| |
| // TODO(crbug/1193485): we would like to submit a crash report here, impl |
| // is pending resolution of privacy concerns. |
| return std::nullopt; |
| } |
| |
| constexpr LazyRE2 cryptohome_mount_failure = { |
| R"(Failed to mount cryptohome, error = (\d+))"}; |
| |
| MaybeCrashReport CryptohomeParser::ParseLogEntry(const std::string& line) { |
| uint64_t error_code; |
| if (!RE2::PartialMatch(line, *cryptohome_mount_failure, &error_code)) { |
| return std::nullopt; |
| } |
| |
| // Avoid creating crash reports if the user doesn't exist or if cryptohome |
| // can't authenticate the user's password. |
| if (error_code == cryptohome::MOUNT_ERROR_USER_DOES_NOT_EXIST || |
| error_code == cryptohome::MOUNT_ERROR_KEY_FAILURE) |
| return std::nullopt; |
| |
| return CrashReport("", {std::move("--mount_failure"), |
| std::move("--mount_device=cryptohome")}); |
| } |
| |
| constexpr LazyRE2 auth_failure = { |
| R"(Found auth failure in the last life cycle. \(0x(.+)\))"}; |
| |
| const std::unordered_set<uint32_t> auth_failure_blocklist = { |
| 0x2010c9ae, // wrong password attempts |
| }; |
| |
| MaybeCrashReport TcsdParser::ParseLogEntry(const std::string& line) { |
| uint32_t hash; |
| if (!RE2::PartialMatch(line, *auth_failure, RE2::Hex(&hash))) { |
| return std::nullopt; |
| } |
| if (auth_failure_blocklist.count(hash)) { |
| LOG(INFO) << "Ignoring auth_failure 0x" << std::hex << hash; |
| return std::nullopt; |
| } |
| std::string text = base::StringPrintf("%08x-auth failure\n", hash); |
| |
| return CrashReport(std::move(text), {std::move("--auth_failure")}); |
| } |
| |
| } // namespace anomaly |