blob: efe505d2cc00139d232fcec2d312761d18190b4f [file] [log] [blame]
// 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/util.h"
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
#include <algorithm>
#include <map>
#include <memory>
#include <base/check_op.h>
#include <base/logging.h>
#if USE_DIRENCRYPTION
#include <keyutils.h>
#endif // USE_DIRENCRYPTION
#include <base/command_line.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <brillo/cryptohome.h>
#include <brillo/key_value_store.h>
#include <brillo/userdb_utils.h>
#include <re2/re2.h>
#include <zlib.h>
#include "crash-reporter/crossystem.h"
#include "crash-reporter/paths.h"
#include "crash-reporter/vm_support.h"
namespace util {
namespace {
constexpr size_t kBufferSize = 4096;
// Path to hardware class description.
constexpr char kHwClassPath[] = "/sys/devices/platform/chromeos_acpi/HWID";
constexpr char kDevSwBoot[] = "devsw_boot";
constexpr char kDevMode[] = "dev";
// If the OS version is older than this we do not upload crash reports.
constexpr base::TimeDelta kAgeForNoUploads = base::TimeDelta::FromDays(180);
#if USE_DIRENCRYPTION
// Name of the session keyring.
const char kDircrypt[] = "dircrypt";
#endif // USE_DIRENCRYPTION
} // namespace
const int kDefaultMaxUploadBytes = 1024 * 1024;
bool IsCrashTestInProgress() {
return base::PathExists(paths::GetAt(paths::kSystemRunStateDirectory,
paths::kCrashTestInProgress));
}
bool IsDeviceCoredumpUploadAllowed() {
return base::PathExists(paths::GetAt(paths::kCrashReporterStateDirectory,
paths::kDeviceCoredumpUploadAllowed));
}
bool IsDeveloperImage() {
// If we're testing crash reporter itself, we don't want to special-case
// for developer images.
if (IsCrashTestInProgress())
return false;
return base::PathExists(paths::Get(paths::kLeaveCoreFile));
}
// Determines if this is a test image, IGNORING IsCrashTestInProgress.
// Use sparingly, and only if you're really sure you want to have different
// behavior during crash tests than on real devices.
bool IsReallyTestImage() {
std::string channel;
if (!GetCachedKeyValueDefault(base::FilePath(paths::kLsbRelease),
"CHROMEOS_RELEASE_TRACK", &channel)) {
return false;
}
return base::StartsWith(channel, "test", base::CompareCase::SENSITIVE);
}
bool IsTestImage() {
// If we're testing crash reporter itself, we don't want to special-case
// for test images.
if (IsCrashTestInProgress())
return false;
return IsReallyTestImage();
}
bool IsOfficialImage() {
std::string description;
if (!GetCachedKeyValueDefault(base::FilePath(paths::kLsbRelease),
"CHROMEOS_RELEASE_DESCRIPTION", &description)) {
return false;
}
return description.find("Official") != std::string::npos;
}
bool HasMockConsent() {
// Don't bypass user consent on real Chromebooks; this is for testing.
// We can't use IsTestImage because that's always false if a crash test is in
// progress.
if (!IsReallyTestImage()) {
return false;
}
return base::PathExists(
paths::GetAt(paths::kSystemRunStateDirectory, paths::kMockConsent));
}
bool IsFeedbackAllowed(MetricsLibraryInterface* metrics_lib) {
if (HasMockConsent()) {
LOG(INFO) << "mock-consent file present; assuming consent";
return true;
}
// For developer builds, we always want to keep the crash reports unless
// we're testing the crash facilities themselves. This overrides
// feedback. Crash sending still obeys consent.
if (IsDeveloperImage()) {
LOG(INFO) << "developer build - not testing - always dumping";
return true;
}
VmSupport* vm_support = VmSupport::Get();
bool ret = false;
if (vm_support) {
ret = vm_support->GetMetricsConsent();
} else {
ret = metrics_lib->AreMetricsEnabled();
}
if (!ret) {
LOG(WARNING)
<< "No consent. Not handling invocation: "
<< base::CommandLine::ForCurrentProcess()->GetCommandLineString();
}
return ret;
}
// Determine if the filter-in file tells us to skip
static bool SkipForFilterIn(std::string command_line) {
base::FilePath file =
paths::GetAt(paths::kSystemRunStateDirectory, paths::kFilterInFile);
if (!base::PathExists(file)) {
return false;
}
std::string contents;
if (!base::ReadFileToString(file, &contents)) {
LOG(WARNING) << "Failed to read " << file;
return false;
}
if (contents == "none" || command_line.find(contents) == std::string::npos) {
// Doesn't match, so skip this crash.
LOG(WARNING) << "Ignoring crash invocation '" << command_line << "' due to "
<< "filter_in=" << contents << ".";
return true;
}
return false;
}
// Determine if the filter-out file tells us to skip
static bool SkipForFilterOut(std::string command_line) {
base::FilePath file =
paths::GetAt(paths::kSystemRunStateDirectory, paths::kFilterOutFile);
if (!base::PathExists(file)) {
return false;
}
std::string contents;
if (!base::ReadFileToString(file, &contents)) {
LOG(WARNING) << "Failed to read " << file;
return false;
}
if (command_line.find(contents) != std::string::npos) {
// Matches, so skip this crash.
LOG(WARNING) << "Ignoring crash invocation '" << command_line << "' due to "
<< "filter_out=" << contents << ".";
return true;
}
return false;
}
bool SkipCrashCollection(int argc, const char* const argv[]) {
// Don't skip crashes on real Chromebooks; this is for testing.
// We can't use IsTestImage because that's always false if a crash test is in
// progress.
if (!IsReallyTestImage()) {
return false;
}
std::vector<std::string> args(argv + 1, argv + argc);
std::string command_line = base::JoinString(args, " ");
// If the command line consists solely of these flags, it's always allowed
// (regardless of filter-in state).
// These flags are always accepted because they do not create crash files.
// Tests may wish to verify or depend on their effects while also blocking all
// crashes (using a filter-in of "none").
const std::vector<std::string> allowlist = {
"--init",
"--clean_shutdown",
"--log_to_stderr",
};
bool all_args_allowed = true;
for (auto it = args.begin(); it != args.end() && all_args_allowed; ++it) {
if (std::find(allowlist.begin(), allowlist.end(), *it) == allowlist.end()) {
all_args_allowed = false;
}
}
if (all_args_allowed) {
return false;
}
return SkipForFilterIn(command_line) || SkipForFilterOut(command_line);
}
bool SetGroupAndPermissions(const base::FilePath& file,
const char* group,
bool execute) {
gid_t gid;
if (!brillo::userdb::GetGroupInfo(group, &gid)) {
LOG(ERROR) << "Couldn't look up group " << group;
return false;
}
if (lchown(file.value().c_str(), -1, gid) != 0) {
PLOG(ERROR) << "Couldn't chown " << file.value();
return false;
}
int mode;
if (!base::GetPosixFilePermissions(file, &mode)) {
PLOG(ERROR) << "Couldn't get file permissions for " << file.value();
return false;
}
mode_t group_mode = S_IRGRP | S_IWGRP;
if (execute) {
group_mode |= S_IXGRP;
}
if (!base::SetPosixFilePermissions(file, mode | group_mode)) {
PLOG(ERROR) << "Couldn't chmod " << file.value();
return false;
}
return true;
}
base::Time GetOsTimestamp() {
base::FilePath lsb_release_path =
paths::Get(paths::kEtcDirectory).Append(paths::kLsbRelease);
base::File::Info info;
if (!base::GetFileInfo(lsb_release_path, &info)) {
LOG(ERROR) << "Failed reading info for /etc/lsb-release";
return base::Time();
}
return info.last_modified;
}
bool IsBuildTimestampTooOldForUploads(int64_t build_time_millis,
base::Clock* clock) {
if (build_time_millis == 0) {
return true;
} else if (build_time_millis < 0) {
LOG(ERROR) << "build timestamp is negative: " << build_time_millis;
return false;
}
base::Time timestamp(base::Time::UnixEpoch() +
base::TimeDelta::FromMilliseconds(build_time_millis));
if (timestamp.is_null()) {
return false;
}
base::Time now = clock->Now();
// In case of invalid timestamps, always upload a crash -- something strange
// is happening.
if (timestamp > now) {
LOG(ERROR) << "OS timestamp is in the future: " << timestamp;
return false;
} else if (timestamp < base::Time::UnixEpoch()) {
LOG(ERROR) << "OS timestamp is negative: " << timestamp;
return false;
}
return (now - timestamp) > kAgeForNoUploads;
}
std::string GetHardwareClass() {
std::string hw_class;
if (base::ReadFileToString(paths::Get(kHwClassPath), &hw_class))
return hw_class;
auto vb_value = crossystem::GetInstance()->VbGetSystemPropertyString("hwid");
return vb_value ? vb_value.value() : "undefined";
}
std::string GetBootModeString() {
// If we're testing crash reporter itself, we don't want to special-case
// for developer mode.
if (IsCrashTestInProgress())
return "";
auto vb_value = crossystem::GetInstance()->VbGetSystemPropertyInt(kDevSwBoot);
if (!vb_value) {
LOG(ERROR) << "Error trying to determine boot mode";
return "missing-crossystem";
}
if (vb_value.value() == 1)
return kDevMode;
return "";
}
bool GetCachedKeyValue(const base::FilePath& base_name,
const std::string& key,
const std::vector<base::FilePath>& directories,
std::string* value) {
std::vector<std::string> error_reasons;
for (const auto& directory : directories) {
const base::FilePath file_name = directory.Append(base_name);
if (!base::PathExists(file_name)) {
error_reasons.push_back(file_name.value() + " not found");
continue;
}
brillo::KeyValueStore store;
if (!store.Load(file_name)) {
LOG(WARNING) << "Problem parsing " << file_name.value();
// Even though there was some failure, take as much as we could read.
}
if (!store.GetString(key, value)) {
error_reasons.push_back("Key not found in " + file_name.value());
continue;
}
return true;
}
LOG(WARNING) << "Unable to find " << key << ": "
<< base::JoinString(error_reasons, ", ");
return false;
}
bool GetCachedKeyValueDefault(const base::FilePath& base_name,
const std::string& key,
std::string* value) {
const std::vector<base::FilePath> kDirectories = {
paths::Get(paths::kCrashReporterStateDirectory),
paths::Get(paths::kEtcDirectory),
};
return GetCachedKeyValue(base_name, key, kDirectories, value);
}
bool GetUserHomeDirectories(
org::chromium::SessionManagerInterfaceProxyInterface* session_manager_proxy,
std::vector<base::FilePath>* directories) {
brillo::ErrorPtr error;
std::map<std::string, std::string> sessions;
session_manager_proxy->RetrieveActiveSessions(&sessions, &error);
if (error) {
LOG(ERROR) << "Error calling D-Bus proxy call to interface "
<< "'" << session_manager_proxy->GetObjectPath().value()
<< "': " << error->GetMessage();
return false;
}
for (const auto& iter : sessions) {
directories->push_back(paths::Get(
brillo::cryptohome::home::GetHashedUserPath(iter.second).value()));
}
return true;
}
bool GetUserCrashDirectories(
org::chromium::SessionManagerInterfaceProxyInterface* session_manager_proxy,
std::vector<base::FilePath>* directories) {
std::vector<base::FilePath> home_directories;
if (!GetUserHomeDirectories(session_manager_proxy, &home_directories)) {
return false;
}
for (const auto& iter : home_directories) {
directories->push_back(iter.Append("crash"));
}
return true;
}
bool GetDaemonStoreCrashDirectories(
org::chromium::SessionManagerInterfaceProxyInterface* session_manager_proxy,
std::vector<base::FilePath>* directories) {
brillo::ErrorPtr error;
std::map<std::string, std::string> sessions;
session_manager_proxy->RetrieveActiveSessions(&sessions, &error);
if (error) {
LOG(ERROR) << "Error calling D-Bus proxy call to interface "
<< "'" << session_manager_proxy->GetObjectPath().value()
<< "': " << error->GetMessage();
return false;
}
for (const auto& iter : sessions) {
directories->push_back(
paths::Get(base::FilePath(paths::kCryptohomeCrashDirectory)
.Append(iter.second)
.value()));
}
return true;
}
std::vector<unsigned char> GzipStream(brillo::StreamPtr data) {
// Adapted from https://zlib.net/zlib_how.html
z_stream deflate_stream;
memset(&deflate_stream, 0, sizeof(deflate_stream));
deflate_stream.zalloc = Z_NULL;
deflate_stream.zfree = Z_NULL;
// Using a window size of 31 sets us to gzip mode (16) + default window size
// (15).
const int kDefaultWindowSize = 15;
const int kWindowSizeGzipAdd = 16;
const int kDefaultMemLevel = 8;
int result = deflateInit2(&deflate_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
kDefaultWindowSize + kWindowSizeGzipAdd,
kDefaultMemLevel, Z_DEFAULT_STRATEGY);
if (result != Z_OK) {
LOG(ERROR) << "Error initializing zlib: error code " << result
<< ", error msg: "
<< (deflate_stream.msg == nullptr ? "None" : deflate_stream.msg);
return std::vector<unsigned char>();
}
std::vector<unsigned char> deflated;
int flush = Z_NO_FLUSH;
/* compress until end of stream */
do {
size_t read_size = 0;
unsigned char in[kBufferSize];
if (!data->ReadBlocking(in, kBufferSize, &read_size, nullptr)) {
// We are reading from a memory stream, so this really shouldn't happen.
LOG(ERROR) << "Error reading from input stream";
deflateEnd(&deflate_stream);
return std::vector<unsigned char>();
}
if (data->GetRemainingSize() <= 0) {
// We must request a flush on the last chunk of data, else deflateEnd
// may just discard some compressed data.
flush = Z_FINISH;
}
deflate_stream.next_in = in;
deflate_stream.avail_in = read_size;
do {
unsigned char out[kBufferSize];
deflate_stream.avail_out = kBufferSize;
deflate_stream.next_out = out;
result = deflate(&deflate_stream, flush);
// Note that most return values are acceptable; don't error out if result
// is not Z_OK. See discussion at https://zlib.net/zlib_how.html
DCHECK_NE(result, Z_STREAM_ERROR);
DCHECK_LE(deflate_stream.avail_out, kBufferSize);
int amount_in_output_buffer = kBufferSize - deflate_stream.avail_out;
deflated.insert(deflated.end(), out, out + amount_in_output_buffer);
} while (deflate_stream.avail_out == 0);
DCHECK_EQ(deflate_stream.avail_in, 0) << "deflate did not consume all data";
} while (flush != Z_FINISH);
DCHECK_EQ(result, Z_STREAM_END) << "Stream did not complete properly";
deflateEnd(&deflate_stream);
return deflated;
}
int RunAndCaptureOutput(brillo::ProcessImpl* process,
int fd,
std::string* output) {
process->RedirectUsingPipe(fd, false);
if (process->Start()) {
const int out = process->GetPipe(fd);
char buffer[kBufferSize];
output->clear();
while (true) {
const ssize_t count = HANDLE_EINTR(read(out, buffer, kBufferSize));
if (count < 0) {
process->Wait();
break;
}
if (count == 0)
return process->Wait();
output->append(buffer, count);
}
}
return -1;
}
void LogMultilineError(const std::string& error) {
std::vector<base::StringPiece> lines = base::SplitStringPiece(
error, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (auto line : lines)
LOG(ERROR) << line;
}
bool ReadMemfdToString(int mem_fd, std::string* contents) {
if (contents)
contents->clear();
base::ScopedFILE file(fdopen(mem_fd, "r"));
if (!file) {
PLOG(ERROR) << "Failed to fdopen(" << mem_fd << ")";
return false;
}
if (fseeko(file.get(), 0, SEEK_END) != 0) {
PLOG(ERROR) << "fseeko() error";
return false;
}
off_t file_size = ftello(file.get());
if (file_size < 0) {
PLOG(ERROR) << "ftello() error";
return false;
} else if (file_size == 0) {
LOG(ERROR) << "Minidump memfd has size of 0";
return false;
}
if (fseeko(file.get(), 0, SEEK_SET) != 0) {
PLOG(ERROR) << "fseeko() error";
return false;
}
std::unique_ptr<char[]> buf(new char[file_size]);
if (fread(buf.get(), 1, file_size, file.get()) != file_size) {
PLOG(ERROR) << "fread() error";
return false;
}
if (contents)
contents->assign(buf.get(), file_size);
return true;
}
int GetSelinuxWeight() {
return 1000;
}
int GetServiceFailureWeight() {
return 1000;
}
int GetSuspendFailureWeight() {
return 50;
}
int GetKernelWarningWeight(const std::string& flag) {
// Sample kernel wifi warnings at a higher weight, since they're heavily
// environmentally influenced.
// Sample iwlwifi errors and ath10k errors using a higher weight since they're
// not as useful and are only actionable for WiFi vendors.
if (flag == "--kernel_wifi_warning" || flag == "--kernel_ath10k_error" ||
flag == "--kernel_iwlwifi_error") {
return 50;
} else if (flag == "--kernel_warning" || flag == "--kernel_suspend_warning") {
return 10;
} else {
return 1; // e.g. kernel_smmu_fault
}
}
int GetUmountStatefulFailureWeight() {
return 10;
}
bool ReadFdToStream(unsigned int fd, std::stringstream* stream) {
base::File src(fd);
char buffer[kBufferSize];
while (true) {
const int count = src.ReadAtCurrentPosNoBestEffort(buffer, kBufferSize);
if (count < 0)
return false;
if (count == 0)
return stream->tellp() > 0; // Crash log should not be empty.
stream->write(buffer, count);
}
}
#if USE_DIRENCRYPTION
void JoinSessionKeyring() {
key_serial_t session_keyring = keyctl_join_session_keyring(kDircrypt);
if (session_keyring == -1) {
// The session keyring may not exist if ext4 encryption isn't enabled so
// just log an info message instead of an error.
PLOG(INFO) << "Unable to join session keying";
}
}
#endif // USE_DIRENCRYPTION
// Hash a string to a number. We define our own hash function to not
// be dependent on a C++ library that might change. This function
// uses basically the same approach as tr1/functional_hash.h but with
// a larger prime number (16127 vs 131).
unsigned HashString(base::StringPiece input) {
unsigned hash = 0;
for (auto c : input)
hash = hash * 16127 + c;
return hash;
}
base::FilePath GetPathToThisBinary(const char* const argv[]) {
// To support usage with lddtree, allow overriding argv[0] with the LD_ARGV0
// environment variable set by lddtree wrappers. Without this, core_pattern
// will include a .elf suffix, bypassing the lddtree wrapper, leaving
// crash_reporter unable to start in response to crashes.
//
// TODO(crbug.com/1003841): Remove LD_ARGV0 once ld.so supports forwarding
// the binary name.
const char* arg0 = getenv("LD_ARGV0");
if (arg0 == nullptr || arg0[0] == '\0') {
arg0 = argv[0];
}
return base::MakeAbsoluteFilePath(base::FilePath(arg0));
}
bool RedactDigests(std::string* to_filter) {
static constexpr const LazyRE2 digest_re = {
R"((^|[^0-9a-fA-F])(?:[0-9a-fA-F]{32,})([^0-9a-fA-F]|$))"};
return re2::RE2::Replace(to_filter, *digest_re, R"(\1<Redacted Digest>\2)");
}
} // namespace util