| // Copyright (c) 2013 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/chrome_collector.h" |
| |
| #include <pcrecpp.h> |
| #include <stdint.h> |
| |
| #include <map> |
| #include <string> |
| #include <vector> |
| |
| #include <base/files/file_util.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_util.h> |
| #include <brillo/data_encoding.h> |
| #include <brillo/process.h> |
| #include <brillo/syslog_logging.h> |
| |
| using base::FilePath; |
| using base::StringPrintf; |
| |
| namespace { |
| |
| const char kDefaultMinidumpName[] = "upload_file_minidump"; |
| |
| // Path to the gzip binary. |
| const char kGzipPath[] = "/bin/gzip"; |
| |
| // Filenames for logs attached to crash reports. Also used as metadata keys. |
| const char kChromeLogFilename[] = "chrome.txt"; |
| const char kGpuStateFilename[] = "i915_error_state.log.xz"; |
| |
| // From //net/crash/collector/collector.h |
| const int kDefaultMaxUploadBytes = 1024 * 1024; |
| |
| // Extract a string delimited by the given character, from the given offset |
| // into a source string. Returns false if the string is zero-sized or no |
| // delimiter was found. |
| bool GetDelimitedString(const std::string &str, char ch, size_t offset, |
| std::string *substr) { |
| size_t at = str.find_first_of(ch, offset); |
| if (at == std::string::npos || at == offset) |
| return false; |
| *substr = str.substr(offset, at - offset); |
| return true; |
| } |
| |
| // Gets the GPU's error state from debugd and writes it to |error_state_path|. |
| // Returns true on success. |
| bool GetDriErrorState(const FilePath &error_state_path, |
| org::chromium::debugdProxy *proxy) { |
| brillo::ErrorPtr error; |
| std::string error_state_str; |
| |
| proxy->GetLog("i915_error_state", &error_state_str, &error); |
| |
| if (error) { |
| LOG(ERROR) << "Error calling D-Bus proxy call to interface " |
| << "'" << proxy->GetObjectPath().value() << "':" |
| << error->GetMessage(); |
| return false; |
| } |
| |
| if (error_state_str == "<empty>") |
| return false; |
| |
| const char kBase64Header[] = "<base64>: "; |
| const size_t kBase64HeaderLength = sizeof(kBase64Header) - 1; |
| if (error_state_str.compare(0, kBase64HeaderLength, kBase64Header)) { |
| LOG(ERROR) << "i915_error_state is missing base64 header"; |
| return false; |
| } |
| |
| std::string decoded_error_state; |
| |
| if (!brillo::data_encoding::Base64Decode( |
| error_state_str.c_str() + kBase64HeaderLength, |
| &decoded_error_state)) { |
| LOG(ERROR) << "Could not decode i915_error_state"; |
| return false; |
| } |
| |
| int written = base::WriteFile(error_state_path, |
| decoded_error_state.c_str(), |
| decoded_error_state.length()); |
| if (written < 0 || |
| static_cast<size_t>(written) != decoded_error_state.length()) { |
| LOG(ERROR) << "Could not write file " << error_state_path.value() |
| << " Written: " << written << " Len: " |
| << decoded_error_state.length(); |
| base::DeleteFile(error_state_path, false); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Gzip-compresses |path|, removes the original file, and returns the path of |
| // the new file. On failure, the original file is left alone and an empty path |
| // is returned. |
| FilePath GzipFile(const FilePath& path) { |
| brillo::ProcessImpl proc; |
| proc.AddArg(kGzipPath); |
| proc.AddArg(path.value()); |
| const int res = proc.Run(); |
| if (res != 0) { |
| LOG(ERROR) << "Failed to gzip " << path.value(); |
| return FilePath(); |
| } |
| return path.AddExtension(".gz"); |
| } |
| |
| } // namespace |
| |
| |
| ChromeCollector::ChromeCollector() : output_file_ptr_(stdout) {} |
| |
| ChromeCollector::~ChromeCollector() {} |
| |
| bool ChromeCollector::HandleCrash(const FilePath &file_path, |
| const std::string &pid_string, |
| const std::string &uid_string, |
| const std::string &exe_name) { |
| if (!is_feedback_allowed_function_()) |
| return true; |
| |
| LOG(WARNING) << "Received crash notification for " << exe_name << "[" |
| << pid_string << "] user " << uid_string << " (called directly)"; |
| |
| if (exe_name.find('/') != std::string::npos) { |
| LOG(ERROR) << "exe_name contains illegal characters: " << exe_name; |
| return false; |
| } |
| |
| FilePath dir; |
| uid_t uid = atoi(uid_string.c_str()); |
| pid_t pid = atoi(pid_string.c_str()); |
| if (!GetCreatedCrashDirectoryByEuid(uid, &dir, nullptr)) { |
| LOG(ERROR) << "Can't create crash directory for uid " << uid; |
| return false; |
| } |
| |
| std::string dump_basename = FormatDumpBasename(exe_name, time(nullptr), pid); |
| FilePath meta_path = GetCrashPath(dir, dump_basename, "meta"); |
| FilePath minidump_path = GetCrashPath(dir, dump_basename, "dmp"); |
| |
| std::string data; |
| if (!base::ReadFileToString(file_path, &data)) { |
| LOG(ERROR) << "Can't read crash log: " << file_path.value(); |
| return false; |
| } |
| |
| if (!ParseCrashLog(data, dir, minidump_path, dump_basename)) { |
| LOG(ERROR) << "Failed to parse Chrome's crash log"; |
| return false; |
| } |
| |
| |
| int64_t report_size = 0; |
| base::GetFileSize(minidump_path, &report_size); |
| |
| // Keyed by crash metadata key name. |
| const std::map<std::string, base::FilePath> additional_logs = |
| GetAdditionalLogs(dir, dump_basename, exe_name); |
| for (auto it : additional_logs) { |
| int64_t file_size = 0; |
| if (!base::GetFileSize(it.second, &file_size)) { |
| PLOG(WARNING) << "Unable to get size of " << it.second.value(); |
| continue; |
| } |
| if (report_size + file_size > kDefaultMaxUploadBytes) { |
| LOG(INFO) << "Skipping upload of " << it.second.value() << "(" |
| << file_size << "B) because report size would exceed limit (" |
| << kDefaultMaxUploadBytes << "B)"; |
| continue; |
| } |
| VLOG(1) << "Adding metadata: " << it.first << " -> " << it.second.value(); |
| // Call AddCrashMetaUploadFile() rather than AddCrashMetaData() here. The |
| // former adds a prefix to the key name; without the prefix, only the key |
| // "logs" appears to be displayed on the crash server. |
| AddCrashMetaUploadFile(it.first, it.second.value()); |
| report_size += file_size; |
| } |
| |
| // We're done. |
| WriteCrashMetaData(meta_path, exe_name, minidump_path.value()); |
| |
| fprintf(output_file_ptr_, "%s", kSuccessMagic); |
| fflush(output_file_ptr_); |
| |
| return true; |
| } |
| |
| void ChromeCollector::SetUpDBus() { |
| CrashCollector::SetUpDBus(); |
| |
| debugd_proxy_.reset(new org::chromium::debugdProxy(bus_)); |
| } |
| |
| bool ChromeCollector::ParseCrashLog(const std::string &data, |
| const FilePath &dir, |
| const FilePath &minidump, |
| const std::string &basename) { |
| size_t at = 0; |
| while (at < data.size()) { |
| // Look for a : followed by a decimal number, followed by another : |
| // followed by N bytes of data. |
| std::string name, size_string; |
| if (!GetDelimitedString(data, ':', at, &name)) { |
| LOG(ERROR) << "Can't find : after name @ offset " << at; |
| break; |
| } |
| at += name.size() + 1; // Skip the name & : delimiter. |
| |
| if (!GetDelimitedString(data, ':', at, &size_string)) { |
| LOG(ERROR) << "Can't find : after size @ offset " << at; |
| break; |
| } |
| at += size_string.size() + 1; // Skip the size & : delimiter. |
| |
| size_t size; |
| if (!base::StringToSizeT(size_string, &size)) { |
| LOG(ERROR) << "String not convertible to integer: " << size_string; |
| break; |
| } |
| |
| // Data would run past the end, did we get a truncated file? |
| if (at + size > data.size()) { |
| LOG(ERROR) << "Overrun, expected " << size << " bytes of data, got " |
| << (data.size() - at); |
| break; |
| } |
| |
| if (name.find("filename") != std::string::npos) { |
| // File. |
| // Name will be in a semi-MIME format of |
| // <descriptive name>"; filename="<name>" |
| // Descriptive name will be upload_file_minidump for the dump. |
| std::string desc, filename; |
| pcrecpp::RE re("(.*)\" *; *filename=\"(.*)\""); |
| if (!re.FullMatch(name.c_str(), &desc, &filename)) { |
| LOG(ERROR) << "Filename was not in expected format: " << name; |
| break; |
| } |
| |
| if (desc.compare(kDefaultMinidumpName) == 0) { |
| // The minidump. |
| WriteNewFile(minidump, data.c_str() + at, size); |
| } else { |
| // Some other file. |
| FilePath path = GetCrashPath(dir, basename + "-" + filename, "other"); |
| if (WriteNewFile(path, data.c_str() + at, size) >= 0) { |
| AddCrashMetaUploadFile(desc, path.value()); |
| } |
| } |
| } else { |
| // Other attribute. |
| std::string value_str; |
| value_str.reserve(size); |
| |
| // Since metadata is one line/value the values must be escaped properly. |
| for (size_t i = at; i < at + size; i++) { |
| switch (data[i]) { |
| case '"': |
| case '\\': |
| value_str.push_back('\\'); |
| value_str.push_back(data[i]); |
| break; |
| |
| case '\r': |
| value_str += "\\r"; |
| break; |
| |
| case '\n': |
| value_str += "\\n"; |
| break; |
| |
| case '\t': |
| value_str += "\\t"; |
| break; |
| |
| case '\0': |
| value_str += "\\0"; |
| break; |
| |
| default: |
| value_str.push_back(data[i]); |
| break; |
| } |
| } |
| AddCrashMetaUploadData(name, value_str); |
| } |
| |
| at += size; |
| } |
| |
| return at == data.size(); |
| } |
| |
| std::map<std::string, base::FilePath> ChromeCollector::GetAdditionalLogs( |
| const FilePath &dir, |
| const std::string &basename, |
| const std::string &exe_name) { |
| std::map<std::string, base::FilePath> logs; |
| |
| // Run the command specified by the config file to gather logs. |
| const FilePath chrome_log_path = |
| GetCrashPath(dir, basename, kChromeLogFilename); |
| if (GetLogContents(log_config_path_, exe_name, chrome_log_path)) { |
| const FilePath compressed_path = GzipFile(chrome_log_path); |
| if (!compressed_path.empty()) |
| logs[kChromeLogFilename] = compressed_path; |
| else |
| base::DeleteFile(chrome_log_path, false /* recursive */); |
| } |
| |
| // For unit testing, debugd_proxy_ isn't initialized, so skip attempting to |
| // get the GPU error state from debugd. |
| if (debugd_proxy_) { |
| const FilePath dri_error_state_path = |
| GetCrashPath(dir, basename, kGpuStateFilename); |
| if (GetDriErrorState(dri_error_state_path, debugd_proxy_.get())) |
| logs[kGpuStateFilename] = dri_error_state_path; |
| } |
| |
| return logs; |
| } |
| |
| // static |
| const char ChromeCollector::kSuccessMagic[] = "_sys_cr_finished"; |