blob: 26bed48d7680ad9d74c2904dd188b25a2a8cb8a1 [file] [log] [blame]
// Copyright 2016 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/user_collector_base.h"
#include "crash-reporter/vm_support.h"
#include <base/files/file_util.h>
#include <base/strings/string_split.h>
#include <base/strings/stringprintf.h>
#include <brillo/process/process.h>
#include <re2/re2.h>
#include "crash-reporter/constants.h"
#include "crash-reporter/util.h"
using base::FilePath;
using base::ReadFileToString;
using base::StringPrintf;
namespace {
const char kStatePrefix[] = "State:\t";
const char kUptimeField[] = "ptime";
const char kUserCrashSignal[] = "org.chromium.CrashReporter.UserCrash";
} // namespace
const char* UserCollectorBase::kUserId = "Uid:\t";
const char* UserCollectorBase::kGroupId = "Gid:\t";
UserCollectorBase::UserCollectorBase(
const std::string& collector_name,
CrashDirectorySelectionMethod crash_directory_selection_method)
: CrashCollector(collector_name,
crash_directory_selection_method,
kNormalCrashSendMode,
collector_name) {}
void UserCollectorBase::Initialize(bool directory_failure, bool early) {
CrashCollector::Initialize(early);
initialized_ = true;
directory_failure_ = directory_failure;
}
void UserCollectorBase::AccounceUserCrash() {
brillo::ProcessImpl dbus;
dbus.AddArg("/usr/bin/dbus-send");
dbus.AddArg("--type=signal");
dbus.AddArg("--system");
dbus.AddArg("/");
dbus.AddArg(kUserCrashSignal);
// Announce through D-Bus whenever a user crash happens. This is
// used by the metrics daemon to log active use time between
// crashes.
//
// This could be done more efficiently by explicit fork/exec or
// using a dbus library directly. However, this should run
// relatively rarely and longer term we may need to implement a
// better way to do this that doesn't rely on D-Bus.
LOG_IF(WARNING, !dbus.Start()) << "dbus-send running failed";
// We run in the background in case dbus daemon itself is crashed
// and not responding. This allows us to not block and potentially
// deadlock on a dbus-daemon crash. If dbus-daemon crashes without
// restarting, each crash will fork off a lot of dbus-send
// processes. Such a system is in a unusable state and will need
// to be restarted anyway.
dbus.Release();
}
bool UserCollectorBase::HandleCrash(
const UserCollectorBase::CrashAttributes& attrs, const char* force_exec) {
CHECK(initialized_);
base::TimeDelta crash_time;
GetUptime(&crash_time);
std::string exec;
if (force_exec) {
exec.assign(force_exec);
} else if (!GetExecutableBaseNameFromPid(attrs.pid, &exec)) {
// If we cannot find the exec name, use the kernel supplied name.
// We don't always use the kernel's since it truncates the name to
// 16 characters.
exec = StringPrintf("supplied_%s", attrs.exec_name.c_str());
}
std::string reason;
bool dump = ShouldDump(attrs.pid, attrs.uid, exec, &reason);
// anomaly_detector's CrashReporterParser looks for this message; don't change
// it without updating the regex.
const auto message = StringPrintf(
"Received crash notification for %s[%d] sig %d, user %u group %u",
exec.c_str(), attrs.pid, attrs.signal, attrs.uid, attrs.gid);
// TODO(crbug.com/1053847) The executable name is sensitive user data inside
// the VM, so don't log this message. Eventually we will move the VM logs
// inside the cryptohome and this will be unnecessary.
if (!VmSupport::Get()) {
LogCrash(message, reason);
}
if (dump) {
AccounceUserCrash();
AddExtraMetadata(exec, attrs.pid);
bool out_of_capacity = false;
ErrorType error_type = ConvertAndEnqueueCrash(
attrs.pid, exec, attrs.uid, attrs.gid, crash_time, &out_of_capacity);
if (error_type != kErrorNone) {
if (!out_of_capacity) {
EnqueueCollectionErrorLog(error_type, exec);
}
return false;
}
}
return true;
}
base::Optional<UserCollectorBase::CrashAttributes>
UserCollectorBase::ParseCrashAttributes(const std::string& crash_attributes) {
RE2 re("(\\d+):(\\d+):(\\d+):(\\d+):(.*)");
UserCollectorBase::CrashAttributes attrs;
if (!RE2::FullMatch(crash_attributes, re, &attrs.pid, &attrs.signal,
&attrs.uid, &attrs.gid, &attrs.exec_name)) {
return base::nullopt;
}
return attrs;
}
bool UserCollectorBase::ShouldDump(base::Optional<pid_t> pid,
std::string* reason) const {
VmSupport* vm_support = VmSupport::Get();
if (vm_support) {
if (!pid.has_value()) {
*reason = "ignoring - unknown PID inside VM";
return false;
}
if (!vm_support->ShouldDump(*pid, reason)) {
return false;
}
}
*reason = "handling";
return true;
}
bool UserCollectorBase::ShouldDump(std::string* reason) const {
return ShouldDump(base::nullopt, reason);
}
bool UserCollectorBase::GetFirstLineWithPrefix(
const std::vector<std::string>& lines,
const char* prefix,
std::string* line) {
for (const auto& current_line : lines) {
if (current_line.find(prefix) == 0) {
*line = current_line;
return true;
}
}
return false;
}
bool UserCollectorBase::GetIdFromStatus(
const char* prefix,
IdKind kind,
const std::vector<std::string>& status_lines,
int* id) {
// From fs/proc/array.c:task_state(), this file contains:
// \nUid:\t<uid>\t<euid>\t<suid>\t<fsuid>\n
std::string id_line;
if (!GetFirstLineWithPrefix(status_lines, prefix, &id_line)) {
return false;
}
std::string id_substring = id_line.substr(strlen(prefix), std::string::npos);
std::vector<std::string> ids = base::SplitString(
id_substring, "\t", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (ids.size() != kIdMax || kind < 0 || kind >= kIdMax) {
return false;
}
const char* number = ids[kind].c_str();
char* end_number = nullptr;
*id = strtol(number, &end_number, 10);
if (*end_number != '\0') {
return false;
}
return true;
}
bool UserCollectorBase::GetStateFromStatus(
const std::vector<std::string>& status_lines, std::string* state) {
std::string state_line;
if (!GetFirstLineWithPrefix(status_lines, kStatePrefix, &state_line)) {
return false;
}
*state = state_line.substr(strlen(kStatePrefix), std::string::npos);
return true;
}
bool UserCollectorBase::ClobberContainerDirectory(
const base::FilePath& container_dir) {
// Delete a pre-existing directory from crash reporter that may have
// been left around for diagnostics from a failed conversion attempt.
// If we don't, existing files can cause forking to fail.
if (!base::DeletePathRecursively(container_dir)) {
PLOG(ERROR) << "Could not delete " << container_dir.value();
return false;
}
if (!base::CreateDirectory(container_dir)) {
PLOG(ERROR) << "Could not create " << container_dir.value();
return false;
}
return true;
}
const FilePath UserCollectorBase::GetCrashProcessingDir() {
return FilePath("/tmp/crash_reporter");
}
UserCollectorBase::ErrorType UserCollectorBase::ConvertAndEnqueueCrash(
pid_t pid,
const std::string& exec,
uid_t supplied_ruid,
gid_t supplied_rgid,
const base::TimeDelta& crash_time,
bool* out_of_capacity) {
FilePath crash_path;
if (!GetCreatedCrashDirectory(pid, supplied_ruid, &crash_path,
out_of_capacity)) {
LOG(ERROR) << "Unable to find/create process-specific crash path";
return kErrorSystemIssue;
}
// Directory like /tmp/crash_reporter/1234 which contains the
// procfs entries and other temporary files used during conversion.
const FilePath container_dir =
GetCrashProcessingDir().Append(StringPrintf("%d", pid));
if (!ClobberContainerDirectory(container_dir))
return kErrorSystemIssue;
std::string dump_basename = FormatDumpBasename(exec, time(nullptr), pid);
FilePath core_path = GetCrashPath(crash_path, dump_basename, "core");
FilePath meta_path = GetCrashPath(crash_path, dump_basename, "meta");
FilePath minidump_path =
GetCrashPath(crash_path, dump_basename, constants::kMinidumpExtension);
FilePath log_path = GetCrashPath(crash_path, dump_basename, "log");
FilePath proc_log_path = GetCrashPath(crash_path, dump_basename, "proclog");
if (GetLogContents(FilePath(log_config_path_), exec, log_path)) {
AddCrashMetaUploadFile("log", log_path.BaseName().value());
}
if (GetProcessTree(pid, proc_log_path)) {
AddCrashMetaUploadFile("process_tree", proc_log_path.BaseName().value());
}
#if USE_DIRENCRYPTION
// Join the session keyring, if one exists.
util::JoinSessionKeyring();
#endif // USE_DIRENCRYPTION
ErrorType error_type =
ConvertCoreToMinidump(pid, container_dir, core_path, minidump_path);
if (error_type != kErrorNone) {
if (error_type != kErrorReadCoreData)
LOG(INFO) << "Leaving core file at " << core_path.value()
<< " due to conversion error";
return error_type;
} else {
base::FilePath target;
if (!NormalizeFilePath(minidump_path, &target))
target = minidump_path;
// TODO(crbug.com/1053847) The executable name is sensitive user data inside
// the VM, so don't log this message. Eventually we will move the VM logs
// inside the cryptohome and this will be unnecessary.
if (!VmSupport::Get()) {
LOG(INFO) << "Stored minidump to " << target.value();
}
}
base::TimeDelta start_time;
if (GetUptimeAtProcessStart(pid, &start_time) && crash_time > start_time) {
const base::TimeDelta uptime = crash_time - start_time;
AddCrashMetaUploadData(kUptimeField,
std::to_string(uptime.InMilliseconds()));
} else {
LOG(WARNING) << "Failed to get process uptime.";
}
// Here we commit to sending this file. We must not return false
// after this point or we will generate a log report as well as a
// crash report.
FinishCrash(meta_path, exec, minidump_path.BaseName().value());
if (!util::IsDeveloperImage()) {
base::DeleteFile(core_path);
} else {
LOG(INFO) << "Leaving core file at " << core_path.value()
<< " due to developer image";
}
base::DeletePathRecursively(container_dir);
return kErrorNone;
}
bool UserCollectorBase::GetCreatedCrashDirectory(pid_t pid,
uid_t supplied_ruid,
FilePath* crash_file_path,
bool* out_of_capacity) {
FilePath process_path = GetProcessPath(pid);
std::string status;
if (directory_failure_) {
LOG(ERROR) << "Purposefully failing to create spool directory";
return false;
}
uid_t uid;
if (base::ReadFileToString(process_path.Append("status"), &status)) {
std::vector<std::string> status_lines = base::SplitString(
status, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
std::string process_state;
if (!GetStateFromStatus(status_lines, &process_state)) {
LOG(ERROR) << "Could not find process state in status file";
return false;
}
LOG(INFO) << "State of crashed process [" << pid << "]: " << process_state;
// Get effective UID of crashing process.
int id;
if (!GetIdFromStatus(kUserId, kIdEffective, status_lines, &id)) {
LOG(ERROR) << "Could not find euid in status file";
return false;
}
uid = id;
} else {
LOG(INFO) << "Using supplied UID " << supplied_ruid
<< " for crashed process [" << pid
<< "] due to error reading status file";
uid = supplied_ruid;
}
if (!GetCreatedCrashDirectoryByEuid(uid, crash_file_path, out_of_capacity)) {
LOG(ERROR) << "Could not create crash directory";
return false;
}
return true;
}
std::vector<std::string> UserCollectorBase::GetCommandLine(pid_t pid) const {
const FilePath path = GetProcessPath(pid).Append("cmdline");
// The /proc/[pid]/cmdline file contains the command line separated and
// terminated by a null byte, e.g. "command\0arg\0arg\0". The file is
// empty if the process is a zombie.
std::string cmdline;
if (!ReadFileToString(path, &cmdline)) {
PLOG(ERROR) << "Could not read " << path.value();
return std::vector<std::string>();
}
if (cmdline.empty()) {
LOG(ERROR) << "Empty cmdline for " << path.value();
return std::vector<std::string>();
}
// Split the string by null bytes.
return base::SplitString(cmdline, std::string(1, '\0'), base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
}