blob: 8174e7077bddb8f1f89c4aefda0add190eac9337 [file] [log] [blame]
// 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 <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/process.h>
#include <brillo/syslog_logging.h>
#include "crash-reporter/util.h"
using base::FilePath;
namespace {
const char kDefaultMinidumpName[] = "upload_file_minidump";
// 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";
// Filename for the pid of the browser process if it was aborted due to a
// browser hang. Written by session_manager.
constexpr char kAbortedBrowserPidPath[] = "/run/chrome/aborted_browser_pid";
// 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;
}
} // namespace
ChromeCollector::ChromeCollector(CrashSendingMode crash_sending_mode)
: CrashCollector("chrome",
kUseNormalCrashDirectorySelectionMethod,
crash_sending_mode),
output_file_ptr_(stdout),
max_upload_bytes_(util::kDefaultMaxUploadBytes) {}
ChromeCollector::~ChromeCollector() {}
bool ChromeCollector::HandleCrashWithDumpData(const std::string& data,
pid_t pid,
uid_t uid,
const std::string& exe_name,
const std::string& dump_dir) {
// Perform basic input validation.
CHECK(pid >= (pid_t)0) << "--pid= must be set";
CHECK(uid >= (uid_t)0) << "--uid= must be set";
CHECK(!exe_name.empty()) << "--exe= must be set";
CHECK(dump_dir.empty() || util::IsTestImage())
<< "--chrome_dump_dir is only for tests";
// anomaly_detector's CrashReporterParser looks for this message; don't change
// it without updating the regex.
LOG(WARNING) << "Received crash notification for " << exe_name << "[" << pid
<< "] user " << uid << " (called directly)";
if (!is_feedback_allowed_function_()) {
LOG(WARNING) << "consent not given - ignoring";
return true;
}
if (exe_name.find('/') != std::string::npos) {
LOG(ERROR) << "exe_name contains illegal characters: " << exe_name;
return false;
}
FilePath dir;
if (!dump_dir.empty()) {
dir = FilePath(dump_dir);
} else 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");
if (!ParseCrashLog(data, dir, minidump_path, dump_basename)) {
LOG(ERROR) << "Failed to parse Chrome's crash log";
return false;
}
// Keyed by crash metadata key name.
const std::map<std::string, base::FilePath> additional_logs =
GetAdditionalLogs(dir, dump_basename, exe_name);
for (const auto& it : additional_logs) {
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.BaseName().value());
}
base::FilePath aborted_path(kAbortedBrowserPidPath);
std::string pid_data;
if (base::ReadFileToString(aborted_path, &pid_data)) {
base::TrimWhitespaceASCII(pid_data, base::TRIM_TRAILING, &pid_data);
if (pid_data == base::NumberToString(pid)) {
AddCrashMetaUploadData("browser_hang", "true");
base::DeleteFile(aborted_path, false);
}
}
// We're done.
FinishCrash(meta_path, exe_name, minidump_path.BaseName().value());
// In production |output_file_ptr_| must be stdout because chrome expects to
// read the magic string there.
fprintf(output_file_ptr_, "%s", kSuccessMagic);
fflush(output_file_ptr_);
return true;
}
bool ChromeCollector::HandleCrash(const FilePath& file_path,
pid_t pid,
uid_t uid,
const std::string& exe_name) {
std::string data;
if (!base::ReadFileToString(base::FilePath(file_path), &data)) {
PLOG(ERROR) << "Can't read crash log: " << file_path.value();
return false;
}
return HandleCrashWithDumpData(data, pid, uid, exe_name, "" /* dump_dir */);
}
bool ChromeCollector::HandleCrashThroughMemfd(int memfd,
pid_t pid,
uid_t uid,
const std::string& exe_name,
const std::string& dump_dir) {
std::string data;
if (!util::ReadMemfdToString(memfd, &data)) {
PLOG(ERROR) << "Can't read crash log from memfd: " << memfd;
return false;
}
return HandleCrashWithDumpData(data, pid, uid, exe_name, dump_dir);
}
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.BaseName().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();
}
void ChromeCollector::AddLogIfNotTooBig(
const char* log_map_key,
const base::FilePath& complete_file_name,
std::map<std::string, base::FilePath>* logs) {
if (get_bytes_written() <= max_upload_bytes_) {
(*logs)[log_map_key] = complete_file_name.BaseName();
} else {
// Logs were really big, don't upload them.
LOG(WARNING) << "Skipping upload of " << complete_file_name.value()
<< " because report size would exceed limit ("
<< max_upload_bytes_ << "B)";
// And free up resources to avoid leaving orphaned file around.
if (!RemoveNewFile(complete_file_name)) {
LOG(WARNING) << "Could not remove " << complete_file_name.value();
}
}
}
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;
if (get_bytes_written() > max_upload_bytes_) {
// Minidump is already too big, no point in processing logs or querying
// debugd.
LOG(WARNING) << "Skipping upload of supplemental logs because report size "
<< "already exceeds limit (" << max_upload_bytes_ << "B)";
return logs;
}
// Run the command specified by the config file to gather logs.
const FilePath chrome_log_path =
GetCrashPath(dir, basename, kChromeLogFilename).AddExtension("gz");
if (GetLogContents(log_config_path_, exe_name, chrome_log_path)) {
AddLogIfNotTooBig(kChromeLogFilename, chrome_log_path, &logs);
}
// For unit testing, debugd_proxy_ isn't initialized, so skip attempting to
// get the GPU error state from debugd.
SetUpDBus();
if (debugd_proxy_) {
const FilePath dri_error_state_path =
GetCrashPath(dir, basename, kGpuStateFilename);
if (GetDriErrorState(dri_error_state_path))
AddLogIfNotTooBig(kGpuStateFilename, dri_error_state_path, &logs);
}
return logs;
}
bool ChromeCollector::GetDriErrorState(const FilePath& error_state_path) {
brillo::ErrorPtr error;
std::string error_state_str;
// Chrome has a 12 second timeout for crash_reporter to execute when it
// invokes it, so use a 5 second timeout here on our D-Bus call.
constexpr int kDebugdGetLogTimeoutMsec = 5000;
debugd_proxy_->GetLog("i915_error_state", &error_state_str, &error,
kDebugdGetLogTimeoutMsec);
if (error) {
LOG(ERROR) << "Error calling D-Bus proxy call to interface "
<< "'" << debugd_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;
}
// We must use WriteNewFile instead of base::WriteFile as we
// do not want to write with root access to a symlink that an attacker
// might have created.
int written = WriteNewFile(error_state_path, decoded_error_state.c_str(),
decoded_error_state.length());
if (written < 0 ||
static_cast<size_t>(written) != decoded_error_state.length()) {
PLOG(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;
}
// See chrome's src/components/crash/content/app/breakpad_linux.cc.
// static
const char ChromeCollector::kSuccessMagic[] = "_sys_cr_finished";