| // Copyright (c) 2012 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/crash_collector.h" |
| |
| #include <dirent.h> |
| #include <fcntl.h> // For file creation modes. |
| #include <inttypes.h> |
| #include <linux/limits.h> // PATH_MAX |
| #include <sys/mman.h> // for memfd_create |
| #include <sys/types.h> // for mode_t and gid_t. |
| #include <sys/utsname.h> // For uname. |
| #include <sys/wait.h> // For waitpid. |
| #include <unistd.h> // For execv and fork. |
| |
| #include <ctime> |
| #include <map> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/bind.h> |
| #include <base/check.h> |
| #include <base/check_op.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.h> |
| #include <base/notreached.h> |
| #include <base/posix/eintr_wrapper.h> |
| #include <base/rand_util.h> |
| #include <base/run_loop.h> |
| #include <base/scoped_clear_last_error.h> |
| #include <base/strings/strcat.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_split.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/threading/thread_task_runner_handle.h> |
| #include <brillo/key_value_store.h> |
| #include <brillo/process/process.h> |
| #include <brillo/syslog_logging.h> |
| #include <brillo/userdb_utils.h> |
| #include <debugd/dbus-constants.h> |
| #include <policy/device_policy_impl.h> |
| #include <re2/re2.h> |
| #include <zlib.h> |
| |
| #include "crash-reporter/constants.h" |
| #include "crash-reporter/paths.h" |
| #include "crash-reporter/util.h" |
| |
| namespace { |
| |
| const char kCollectChromeFile[] = |
| "/mnt/stateful_partition/etc/collect_chrome_crashes"; |
| const char kDefaultLogConfig[] = "/etc/crash_reporter_logs.conf"; |
| const char kDefaultUserName[] = "chronos"; |
| const char kShellPath[] = "/bin/sh"; |
| const char kCollectorNameKey[] = "collector"; |
| const char kCrashLoopModeKey[] = "crash_loop_mode"; |
| const char kEarlyCrashKey[] = "is_early_boot"; |
| const char kChannelKey[] = "channel"; |
| // These should be kept in sync with variations::kNumExperimentsKey and |
| // variations::kExperimentListKey in the chromium repo. |
| const char kVariationsKey[] = "variations"; |
| const char kNumExperimentsKey[] = "num-experiments"; |
| // Arbitrarily say we won't accept more than 1MiB for the variations file |
| const int64_t kArbitraryMaxVariationsSize = 1 << 20; |
| |
| // Key of the lsb-release entry containing the OS version. |
| const char kLsbOsVersionKey[] = "CHROMEOS_RELEASE_VERSION"; |
| |
| // Key of the lsb-release entry containing the OS milestone. |
| const char kLsbOsMilestoneKey[] = "CHROMEOS_RELEASE_CHROME_MILESTONE"; |
| |
| // Key of the lsb-release entry containing the OS description. |
| const char kLsbOsDescriptionKey[] = "CHROMEOS_RELEASE_DESCRIPTION"; |
| |
| // Key of the lsb-release entry containing the channel. |
| const char kLsbChannelKey[] = "CHROMEOS_RELEASE_TRACK"; |
| |
| #if !USE_KVM_GUEST |
| // Directory mode of the user crash spool directory. |
| // This is SGID so that files created in it are also accessible to the group. |
| const mode_t kUserCrashPathMode = 02770; |
| |
| // Directory mode of the non-chronos cryptohome spool directory. This has the |
| // sticky bit set to prevent different crash collectors from messing with each |
| // others files. |
| const mode_t kDaemonStoreCrashPathMode = 03770; |
| #endif |
| |
| // Directory mode of the system crash spool directory. |
| // This is SGID so that files created in it are also accessible to the group. |
| const mode_t kSystemCrashDirectoryMode = 02770; |
| |
| // Directory mode of the run time state directory. |
| // Since we place flag files in here for checking by tests, we make it readable. |
| constexpr mode_t kSystemRunStateDirectoryMode = 0755; |
| |
| // Directory mode of /var/lib/crash_reporter. |
| constexpr mode_t kCrashReporterStateDirectoryMode = 0700; |
| |
| constexpr gid_t kRootGroup = 0; |
| |
| // Directory mode of /run/metrics/external/crash-reporter. Anyone in "metrics" |
| // group can read/write, and not readable by any other user. |
| constexpr mode_t kSystemRunMetricsFlagMode = 0770; |
| |
| // Buffer size for reading a log into memory. |
| constexpr size_t kMaxLogSize = 1024 * 1024; |
| |
| // Limit how many processes we walk back up. This avoids any possible races |
| // and loops, and we probably don't need that many in the first place. |
| constexpr size_t kMaxParentProcessLogs = 8; |
| |
| const char kCollectionErrorSignature[] = "crash_reporter-user-collection"; |
| |
| } // namespace |
| |
| const char* const CrashCollector::kUnknownValue = "unknown"; |
| |
| // Maximum crash reports per crash spool directory. Note that this is |
| // a separate maximum from the maximum rate at which we upload these |
| // diagnostics. The higher this rate is, the more space we allow for |
| // core files, minidumps, and kcrash logs, and equivalently the more |
| // processor and I/O bandwidth we dedicate to handling these crashes when |
| // many occur at once. Also note that if core files are configured to |
| // be left on the file system, we stop adding crashes when either the |
| // number of core files or minidumps reaches this number. |
| const int CrashCollector::kMaxCrashDirectorySize = 32; |
| |
| const uid_t CrashCollector::kRootUid = 0; |
| |
| // metrics user for creating /run/metrics/external/crash-reporter. |
| constexpr char kMetricsUserName[] = "metrics"; |
| |
| // metrics group for creating /run/metrics/external/crash-reporter. |
| constexpr char kMetricsGroupName[] = "metrics"; |
| |
| // CrosEventEnum for crash reports. |
| constexpr char kReportCountEnum[] = "Crash.Collector.CollectionCount"; |
| |
| using base::FileEnumerator; |
| using base::FilePath; |
| using base::StringPrintf; |
| |
| // Walk the directory tree to make sure we avoid symlinks. |
| // All parent parts must already exist else we abort. |
| bool ValidatePathAndOpen(const FilePath& dir, int* outfd) { |
| std::vector<FilePath::StringType> components; |
| dir.GetComponents(&components); |
| int parentfd = AT_FDCWD; |
| |
| for (const auto& component : components) { |
| int dirfd = openat(parentfd, component.c_str(), |
| O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH); |
| if (dirfd < 0) { |
| PLOG(ERROR) << "Unable to access crash path: " << dir.value() << " (" |
| << component << ")"; |
| if (parentfd != AT_FDCWD) |
| close(parentfd); |
| return false; |
| } |
| if (parentfd != AT_FDCWD) |
| close(parentfd); |
| parentfd = dirfd; |
| } |
| *outfd = parentfd; |
| return true; |
| } |
| |
| // Create a directory using the specified mode/user/group, and make sure it |
| // is actually a directory with the specified permissions. |
| // static |
| bool CrashCollector::CreateDirectoryWithSettings(const FilePath& dir, |
| mode_t mode, |
| uid_t owner, |
| gid_t group, |
| int* dirfd_out, |
| mode_t files_mode) { |
| const FilePath parent_dir = dir.DirName(); |
| const FilePath final_dir = dir.BaseName(); |
| |
| int parentfd; |
| if (!ValidatePathAndOpen(parent_dir, &parentfd)) { |
| return false; |
| } |
| |
| // Now handle the final part of the crash dir. This one we can initialize. |
| // Note: We omit O_CLOEXEC on purpose as children will use it. |
| const char* final_dir_str = final_dir.value().c_str(); |
| int dirfd = |
| openat(parentfd, final_dir_str, O_DIRECTORY | O_NOFOLLOW | O_RDONLY); |
| if (dirfd < 0) { |
| if (errno != ENOENT) { |
| // Delete whatever is there. |
| if (unlinkat(parentfd, final_dir_str, 0) < 0) { |
| PLOG(ERROR) << "Unable to clean up crash path: " << dir.value(); |
| close(parentfd); |
| return false; |
| } |
| } |
| |
| // It doesn't exist, so create it! We'll recheck the mode below. |
| if (mkdirat(parentfd, final_dir_str, mode) < 0) { |
| if (errno != EEXIST) { |
| PLOG(ERROR) << "Unable to create crash directory: " << dir.value(); |
| close(parentfd); |
| return false; |
| } |
| } |
| |
| // Try once more before we give up. |
| // Note: We omit O_CLOEXEC on purpose as children will use it. |
| dirfd = |
| openat(parentfd, final_dir_str, O_DIRECTORY | O_NOFOLLOW | O_RDONLY); |
| if (dirfd < 0) { |
| PLOG(ERROR) << "Unable to open crash directory: " << dir.value(); |
| close(parentfd); |
| return false; |
| } |
| } |
| close(parentfd); |
| |
| // Make sure the ownership/permissions are correct in case they got reset. |
| // We stat it to avoid pointless metadata updates in the common case. |
| struct stat st; |
| if (fstat(dirfd, &st) < 0) { |
| PLOG(ERROR) << "Unable to stat crash path: " << dir.value(); |
| close(dirfd); |
| return false; |
| } |
| |
| // Change the ownership before we change the mode. |
| if (st.st_uid != owner || st.st_gid != group) { |
| if (fchown(dirfd, owner, group)) { |
| PLOG(ERROR) << "Unable to chown crash directory: " << dir.value(); |
| close(dirfd); |
| return false; |
| } |
| } |
| |
| // Update the mode bits. |
| if ((st.st_mode & 07777) != mode) { |
| if (fchmod(dirfd, mode)) { |
| PLOG(ERROR) << "Unable to chmod crash directory: " << dir.value(); |
| close(dirfd); |
| return false; |
| } |
| } |
| |
| if (files_mode) { |
| FileEnumerator files(dir, /*recursive=*/true, |
| FileEnumerator::FILES | FileEnumerator::DIRECTORIES | |
| FileEnumerator::SHOW_SYM_LINKS); |
| for (FilePath name = files.Next(); !name.empty(); name = files.Next()) { |
| const base::stat_wrapper_t st = files.GetInfo().stat(); |
| const FilePath subdir_path = name.DirName(); |
| const FilePath file = name.BaseName(); |
| |
| mode_t desired_mode = files.GetInfo().IsDirectory() ? mode : files_mode; |
| |
| if (st.st_uid != owner || st.st_gid != group || |
| (st.st_mode & 07777) != desired_mode) { |
| // Something needs to change, so open the file. |
| int subdir_fd; |
| if (subdir_path == dir) { |
| subdir_fd = dirfd; |
| } else { |
| if (!ValidatePathAndOpen(subdir_path, &subdir_fd)) { |
| close(dirfd); |
| return false; |
| } |
| } |
| |
| int file_fd = |
| openat(subdir_fd, file.value().c_str(), O_NOFOLLOW | O_RDONLY); |
| if (file_fd < 0) { |
| PLOG(ERROR) << "Unable to open subfile: " << name.value(); |
| if (subdir_fd != dirfd) { |
| close(subdir_fd); |
| } |
| close(dirfd); |
| return false; |
| } |
| |
| if (subdir_fd != dirfd) { |
| close(subdir_fd); |
| } |
| |
| if (st.st_uid != owner || st.st_gid != group) { |
| if (fchown(file_fd, owner, group)) { |
| PLOG(ERROR) << "Unable to chown crash file: " << name.value(); |
| close(file_fd); |
| close(dirfd); |
| return false; |
| } |
| } |
| if ((st.st_mode & 07777) != desired_mode) { |
| if (fchmod(file_fd, desired_mode)) { |
| PLOG(ERROR) << "Unable to chmod crash file: " << name.value(); |
| close(file_fd); |
| close(dirfd); |
| return false; |
| } |
| } |
| |
| close(file_fd); |
| } |
| } |
| } |
| |
| if (dirfd_out) |
| *dirfd_out = dirfd; |
| else |
| close(dirfd); |
| return true; |
| } |
| |
| bool CarefullyReadFileToStringWithMaxSize(const base::FilePath& path, |
| int64_t max_size, |
| std::string* contents) { |
| const FilePath parent_dir = path.DirName(); |
| const FilePath file = path.BaseName(); |
| |
| int parentfd; |
| if (!ValidatePathAndOpen(parent_dir, &parentfd)) { |
| LOG(ERROR) << "Failed to open parent dir "; |
| return false; |
| } |
| base::ScopedFD scoped_parentfd(parentfd); |
| int fd = openat(parentfd, file.value().c_str(), O_CLOEXEC | O_NOFOLLOW); |
| if (fd < 0) { |
| PLOG(ERROR) << "Failed to open " << file.value(); |
| return false; |
| } |
| |
| base::File f(fd); |
| base::File::Info info; |
| if (!f.GetInfo(&info)) { |
| PLOG(ERROR) << "Failed to get file info"; |
| return false; |
| } |
| int64_t size = info.size; |
| if (size > max_size) { |
| LOG(ERROR) << path.value() << " is too large (" << size |
| << " bytes, wanted at most " << max_size << ")"; |
| return false; |
| } |
| |
| std::vector<uint8_t> data(size); |
| // Then, read the file in to memory. |
| if (!f.ReadAtCurrentPosAndCheck(data)) { |
| PLOG(ERROR) << "Failed to read variant file"; |
| return false; |
| } |
| contents->append(data.begin(), data.end()); |
| |
| return true; |
| } |
| |
| CrashCollector::CrashCollector(const std::string& collector_name, |
| const std::string& tag) |
| : CrashCollector(collector_name, |
| kUseNormalCrashDirectorySelectionMethod, |
| kNormalCrashSendMode, |
| tag) {} |
| |
| CrashCollector::CrashCollector( |
| const std::string& collector_name, |
| CrashDirectorySelectionMethod crash_directory_selection_method, |
| CrashSendingMode crash_sending_mode, |
| const std::string& tag) |
| |
| : collector_name_(collector_name), |
| lsb_release_(FilePath(paths::kEtcDirectory).Append(paths::kLsbRelease)), |
| system_crash_path_(paths::kSystemCrashDirectory), |
| crash_reporter_state_path_(paths::kCrashReporterStateDirectory), |
| log_config_path_(kDefaultLogConfig), |
| max_log_size_(kMaxLogSize), |
| device_policy_loaded_(false), |
| device_policy_(std::make_unique<policy::DevicePolicyImpl>()), |
| crash_sending_mode_(crash_sending_mode), |
| crash_directory_selection_method_(crash_directory_selection_method), |
| is_finished_(false), |
| bytes_written_(0), |
| tag_(tag) { |
| AddCrashMetaUploadData(kCollectorNameKey, collector_name); |
| if (crash_sending_mode_ == kCrashLoopSendingMode) { |
| AddCrashMetaUploadData(kCrashLoopModeKey, "true"); |
| } |
| metrics_lib_ = std::make_unique<MetricsLibrary>(); |
| } |
| |
| CrashCollector::~CrashCollector() { |
| if (bus_) |
| bus_->ShutdownAndBlock(); |
| } |
| |
| void CrashCollector::Initialize(bool early) { |
| // For early boot crash collectors, /var and /home will not be accessible. |
| // Instead, collect the crashes into /run. |
| if (early) { |
| AddCrashMetaUploadData(kEarlyCrashKey, "true"); |
| system_crash_path_ = base::FilePath(paths::kSystemRunCrashDirectory); |
| } |
| } |
| |
| bool CrashCollector::TrySetUpDBus() { |
| if (bus_) |
| return true; |
| |
| dbus::Bus::Options options; |
| options.bus_type = dbus::Bus::SYSTEM; |
| |
| bus_ = new dbus::Bus(options); |
| if (!bus_->Connect()) { |
| return false; |
| } |
| |
| session_manager_proxy_.reset( |
| new org::chromium::SessionManagerInterfaceProxy(bus_)); |
| |
| debugd_proxy_.reset(new org::chromium::debugdProxy(bus_)); |
| return true; |
| } |
| |
| void CrashCollector::SetUpDBus() { |
| CHECK(TrySetUpDBus()); |
| } |
| |
| bool CrashCollector::InMemoryFileExists(const base::FilePath& filename) const { |
| base::FilePath base_name = filename.BaseName(); |
| for (const auto& in_memory_file : in_memory_files_) { |
| if (std::get<0>(in_memory_file) == base_name.value()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| base::ScopedFD CrashCollector::GetNewFileHandle( |
| const base::FilePath& filename) { |
| DCHECK(!is_finished_); |
| int fd = -1; |
| // Note: Getting the c_str() before calling open or memfd_create ensures that |
| // PLOG works correctly -- there won't be intervening standard library calls |
| // between the open / memfd_create and PLOG which could overwrite errno. |
| std::string filename_string; |
| const char* filename_cstr; |
| switch (crash_sending_mode_) { |
| case kNormalCrashSendMode: |
| filename_string = filename.value(); |
| filename_cstr = filename_string.c_str(); |
| // The O_NOFOLLOW is redundant with O_CREAT|O_EXCL, but doesn't hurt. |
| fd = HANDLE_EINTR( |
| open(filename_cstr, |
| O_CREAT | O_WRONLY | O_TRUNC | O_EXCL | O_NOFOLLOW | O_CLOEXEC, |
| kSystemCrashFilesMode)); |
| if (fd < 0) { |
| PLOG(ERROR) << "Could not open " << filename_cstr; |
| } |
| break; |
| case kCrashLoopSendingMode: |
| filename_string = filename.BaseName().value(); |
| filename_cstr = filename_string.c_str(); |
| fd = memfd_create(filename_cstr, MFD_CLOEXEC); |
| if (fd < 0) { |
| PLOG(ERROR) << "Could not memfd_create " << filename_cstr; |
| } |
| break; |
| default: |
| NOTREACHED(); |
| } |
| return base::ScopedFD(fd); |
| } |
| |
| int CrashCollector::WriteNewFile(const FilePath& filename, |
| base::StringPiece data) { |
| base::ScopedFD fd = GetNewFileHandle(filename); |
| if (!fd.is_valid()) { |
| return -1; |
| } |
| |
| if (!base::WriteFileDescriptor(fd.get(), data)) { |
| base::ScopedClearLastError restore_error; |
| fd.reset(); |
| return -1; |
| } |
| |
| if (crash_sending_mode_ == kCrashLoopSendingMode) { |
| if (InMemoryFileExists(filename)) { |
| LOG(ERROR) |
| << "Duplicate file names not allowed in crash loop sending mode: " |
| << filename.value(); |
| errno = EEXIST; |
| return -1; |
| } |
| in_memory_files_.emplace_back(filename.BaseName().value(), std::move(fd)); |
| } |
| |
| int size = data.size(); |
| bytes_written_ += size; |
| return size; |
| } |
| |
| bool CrashCollector::WriteNewCompressedFile(const FilePath& filename, |
| const char* data, |
| size_t size) { |
| DCHECK_EQ(filename.FinalExtension(), ".gz") |
| << filename.value() << " must end in .gz"; |
| base::ScopedFD fd = GetNewFileHandle(filename); |
| if (!fd.is_valid()) { |
| LOG(ERROR) << "Failed to open " << filename.value(); |
| return false; |
| } |
| // No way to stop gzclose_w from closing the file descriptor, but we need a |
| // copy to send to debugd if crash_sending_mode_ == kCrashLoopSendingMode, so |
| // duplicate first. We don't *need* a duplicate for kNormalCrashSendMode, but |
| // it makes the bytes_written_-updating code easier below, so we duplicate in |
| // both crash sending modes. |
| base::ScopedFD fd_dup(dup(fd.get())); |
| if (!fd_dup.is_valid()) { |
| PLOG(ERROR) << "Failed to dup file descriptor"; |
| return false; |
| } |
| |
| gzFile compressed_output = gzdopen(fd.get(), "wb"); |
| if (compressed_output == nullptr) { |
| LOG(ERROR) << "Failed to gzip " << filename.value(); |
| return false; |
| } |
| |
| // zlib now owns the file descriptor; we must not close it past this point. |
| // Note that if gzdopen fails, we are still responsible for closing the file, |
| // so we can't just put the release() call inside gzdopen(). |
| (void)fd.release(); |
| int result = gzwrite(compressed_output, data, size); |
| if (result != size) { |
| if (result <= 0) { |
| int saved_errno = errno; |
| int errnum = 0; |
| const char* error_msg = gzerror(compressed_output, &errnum); |
| if (errnum == Z_ERRNO) { |
| LOG(ERROR) << "gzwrite failed with file system error: " |
| << strerror(saved_errno); |
| } else { |
| LOG(ERROR) << "gzwrite failed: error code " << errnum << ", error msg: " |
| << (error_msg == nullptr ? "None" : error_msg); |
| } |
| } else { |
| LOG(ERROR) << "gzwrite wrote partial output"; |
| } |
| gzclose_w(compressed_output); |
| return false; |
| } |
| |
| result = gzclose_w(compressed_output); |
| if (result != Z_OK) { |
| LOG(ERROR) << "gzclose_w failed with error code " << result; |
| return false; |
| } |
| |
| struct stat compressed_output_stats; |
| if (fstat(fd_dup.get(), &compressed_output_stats) < 0) { |
| PLOG(WARNING) << "Failed to fstat compressed file"; |
| // Make sure st_size is set so we don't add junk to bytes_written. |
| compressed_output_stats.st_size = 0; |
| } |
| |
| if (crash_sending_mode_ == kCrashLoopSendingMode) { |
| if (InMemoryFileExists(filename)) { |
| LOG(ERROR) |
| << "Duplicate file names not allowed in crash loop sending mode: " |
| << filename.value(); |
| return false; |
| } |
| in_memory_files_.emplace_back(filename.BaseName().value(), |
| std::move(fd_dup)); |
| } |
| bytes_written_ += compressed_output_stats.st_size; |
| return true; |
| } |
| |
| bool CrashCollector::RemoveNewFile(const base::FilePath& file_name) { |
| switch (crash_sending_mode_) { |
| case kNormalCrashSendMode: { |
| if (!base::PathExists(file_name)) { |
| return false; |
| } |
| int64_t file_size = 0; |
| if (base::GetFileSize(file_name, &file_size)) { |
| bytes_written_ -= file_size; |
| } |
| return base::DeleteFile(file_name); |
| } |
| case kCrashLoopSendingMode: { |
| base::FilePath base_name = file_name.BaseName(); |
| for (auto it = in_memory_files_.begin(); it != in_memory_files_.end(); |
| ++it) { |
| if (std::get<0>(*it) == base_name.value()) { |
| struct stat file_stat; |
| const brillo::dbus_utils::FileDescriptor& fd = std::get<1>(*it); |
| if (fstat(fd.get(), &file_stat) == 0) { |
| bytes_written_ -= file_stat.st_size; |
| } |
| // Resources for memfd_create files are automatically released once |
| // the last file descriptor is closed, and this will close what should |
| // be the last file descriptor, so we are effectively deleting the |
| // file by erasing the vector entry. |
| in_memory_files_.erase(it); |
| return true; |
| } |
| } |
| return false; |
| } |
| default: |
| NOTREACHED(); |
| return false; |
| } |
| } |
| |
| std::string CrashCollector::Sanitize(const std::string& name) { |
| // Make sure the sanitized name does not include any periods. |
| // The logic in crash_sender relies on this. |
| std::string result = name; |
| for (size_t i = 0; i < name.size(); ++i) { |
| if (!isalnum(result[i]) && result[i] != '_') |
| result[i] = '_'; |
| } |
| return result; |
| } |
| |
| void CrashCollector::StripSensitiveData(std::string* contents) { |
| // At the moment, the only sensitive data we strip is MAC addresses, emails |
| // and serial numbers. |
| StripMacAddresses(contents); |
| StripEmailAddresses(contents); |
| StripSerialNumbers(contents); |
| } |
| |
| void CrashCollector::StripMacAddresses(std::string* contents) { |
| std::ostringstream result; |
| re2::StringPiece input(*contents); |
| std::string pre_re_str; |
| std::string re_str; |
| |
| // Get rid of things that look like MAC addresses, since they could possibly |
| // give information about where someone has been. This is strings that look |
| // like this: 11:22:33:44:55:66 |
| // Complications: |
| // - Within a given log, we want to be able to tell when the same MAC |
| // was used more than once. Thus, we'll consistently replace the first |
| // MAC found with 00:00:00:00:00:01, the second with ...:02, etc. |
| // - ACPI commands look like MAC addresses. We'll specifically avoid getting |
| // rid of those. |
| std::map<std::string, std::string> mac_map; |
| |
| // This RE will find the next MAC address and can return us the data preceding |
| // the MAC and the MAC itself. |
| RE2::Options opt; |
| opt.set_dot_nl(true); |
| |
| RE2 mac_re( |
| "(.*?)(" |
| "[0-9a-fA-F][0-9a-fA-F]:" |
| "[0-9a-fA-F][0-9a-fA-F]:" |
| "[0-9a-fA-F][0-9a-fA-F]:" |
| "[0-9a-fA-F][0-9a-fA-F]:" |
| "[0-9a-fA-F][0-9a-fA-F]:" |
| "[0-9a-fA-F][0-9a-fA-F])", |
| opt); |
| |
| // This RE will identify when the 'pre_mac_str' shows that the MAC address |
| // was really an ACPI cmd. The full string looks like this: |
| // ata1.00: ACPI cmd ef/10:03:00:00:00:a0 (SET FEATURES) filtered out |
| RE2 acpi_re("(?m)ACPI cmd ef/$", opt); |
| |
| // Keep consuming, building up a result string as we go. |
| while (RE2::Consume(&input, mac_re, &pre_re_str, &re_str)) { |
| if (RE2::PartialMatch(pre_re_str, acpi_re)) { |
| // We really saw an ACPI command; add to result w/ no stripping. |
| result << pre_re_str << re_str; |
| } else { |
| // Found a MAC address; look up in our hash for the mapping. |
| std::string replacement_mac = mac_map[re_str]; |
| if (replacement_mac == "") { |
| // It wasn't present, so build up a replacement string. |
| int mac_id = mac_map.size(); |
| |
| // Handle up to 2^32 unique MAC address; overkill, but doesn't hurt. |
| replacement_mac = StringPrintf( |
| "00:00:%02x:%02x:%02x:%02x", (mac_id & 0xff000000) >> 24, |
| (mac_id & 0x00ff0000) >> 16, (mac_id & 0x0000ff00) >> 8, |
| (mac_id & 0x000000ff)); |
| mac_map[re_str] = replacement_mac; |
| } |
| |
| // Dump the string before the MAC and the fake MAC address into result. |
| result << pre_re_str << replacement_mac; |
| } |
| } |
| |
| // One last bit of data might still be in the input. |
| result << input; |
| |
| // We'll just assign right back to |contents|. |
| *contents = result.str(); |
| } |
| |
| void CrashCollector::StripEmailAddresses(std::string* contents) { |
| // Simplified email-matching regex based on |
| // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#Validation |
| RE2 email_re(R"(\b)" |
| R"([a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]{1,256})" |
| "@" |
| R"([a-zA-Z0-9-\.]{1,256}[^\.])" |
| R"(\b)"); |
| CHECK_EQ("", email_re.error()); |
| |
| RE2::GlobalReplace(contents, email_re, "<redacted email address>"); |
| } |
| |
| void CrashCollector::StripSerialNumbers(std::string* contents) { |
| std::ostringstream result; |
| re2::StringPiece input(*contents); |
| std::string pre_re_str; |
| std::string re_str; |
| // Adapted from chromium:components/feedback/anonymizer_tool.cc |
| RE2::Options opt; |
| opt.set_dot_nl(true); |
| opt.set_case_sensitive(false); |
| RE2 serialnumber_re(R"((.*?)(\bserial\s*_?(?:number)?['"]?\s*[:=]\s*['"]?))" |
| R"(([0-9a-zA-Z\-.:\/\\\x00-\x09\x0B-\x1F]+)(\b))", |
| opt); |
| |
| CHECK_EQ("", serialnumber_re.error()); |
| |
| while (RE2::Consume(&input, serialnumber_re, &pre_re_str, &re_str)) { |
| result << pre_re_str << "<redacted serial number>"; |
| } |
| result << input; |
| *contents = result.str(); |
| } |
| |
| std::string CrashCollector::FormatDumpBasename(const std::string& exec_name, |
| time_t timestamp, |
| pid_t pid) { |
| struct tm tm; |
| localtime_r(×tamp, &tm); |
| std::string sanitized_exec_name = Sanitize(exec_name); |
| // Add a random 5-digit number to reduce the chance of filename collisions. |
| int rand = base::RandGenerator(100'000); |
| return StringPrintf("%s.%04d%02d%02d.%02d%02d%02d.%05d.%d", |
| sanitized_exec_name.c_str(), tm.tm_year + 1900, |
| tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, |
| tm.tm_sec, rand, pid); |
| } |
| |
| FilePath CrashCollector::GetCrashPath(const FilePath& crash_directory, |
| const std::string& basename, |
| const std::string& extension) { |
| return crash_directory.Append( |
| StringPrintf("%s.%s", basename.c_str(), extension.c_str())); |
| } |
| |
| bool CrashCollector::GetUserCrashDirectories(std::vector<FilePath>* directories, |
| bool use_non_chronos_cryptohome) { |
| SetUpDBus(); |
| if (use_non_chronos_cryptohome) { |
| return util::GetDaemonStoreCrashDirectories(session_manager_proxy_.get(), |
| directories); |
| } else { |
| return util::GetUserCrashDirectories(session_manager_proxy_.get(), |
| directories); |
| } |
| } |
| |
| FilePath CrashCollector::GetUserCrashDirectory( |
| bool use_non_chronos_cryptohome) { |
| FilePath user_directory = FilePath(paths::kFallbackUserCrashDirectory); |
| // When testing, store crashes in the fallback crash directory; otherwise, the |
| // test framework can't get to them after logging the user out. We don't so |
| // this when using the daemon-store crash directory because crash_reporter |
| // won't be able to write to the fallback directory. |
| if ((util::IsTestImage() || ShouldHandleChromeCrashes()) && |
| !use_non_chronos_cryptohome) { |
| return user_directory; |
| } |
| // In this multiprofile world, there is no one-specific user dir anymore. |
| // Ask the session manager for the active ones, then just run with the |
| // first result we get back. |
| std::vector<FilePath> directories; |
| if (!GetUserCrashDirectories(&directories, use_non_chronos_cryptohome) || |
| directories.empty()) { |
| LOG(ERROR) << "Could not get user crash directories, using default."; |
| return user_directory; |
| } |
| |
| user_directory = directories[0]; |
| return user_directory; |
| } |
| |
| base::Optional<FilePath> CrashCollector::GetCrashDirectoryInfo( |
| uid_t process_euid, |
| uid_t default_user_id, |
| bool use_non_chronos_cryptohome, |
| mode_t* mode, |
| uid_t* directory_owner, |
| gid_t* directory_group) { |
| // User crashes should go into the cryptohome, since they may contain PII. |
| // For system crashes, and crashes in the VM, there may not be a cryptohome |
| // mounted, so we use the system crash path. |
| #if !USE_KVM_GUEST |
| if (process_euid == default_user_id || |
| crash_directory_selection_method_ == kAlwaysUseUserCrashDirectory) { |
| if (use_non_chronos_cryptohome) { |
| *mode = kDaemonStoreCrashPathMode; |
| if (!brillo::userdb::GetGroupInfo(constants::kCrashName, |
| directory_owner)) { |
| PLOG(ERROR) << "Couldn't look up user " << constants::kCrashName; |
| return base::nullopt; |
| } |
| } else { |
| *mode = kUserCrashPathMode; |
| *directory_owner = default_user_id; |
| } |
| if (!brillo::userdb::GetGroupInfo(constants::kCrashUserGroupName, |
| directory_group)) { |
| PLOG(ERROR) << "Couldn't look up group " |
| << constants::kCrashUserGroupName; |
| return base::nullopt; |
| } |
| return GetUserCrashDirectory(use_non_chronos_cryptohome); |
| } |
| #endif // !USE_KVM_GUEST |
| *mode = kSystemCrashDirectoryMode; |
| *directory_owner = kRootUid; |
| if (!brillo::userdb::GetGroupInfo(constants::kCrashGroupName, |
| directory_group)) { |
| PLOG(ERROR) << "Couldn't look up group " << constants::kCrashGroupName; |
| return base::nullopt; |
| } |
| return system_crash_path_; |
| } |
| |
| bool CrashCollector::GetCreatedCrashDirectoryByEuid( |
| uid_t euid, |
| FilePath* crash_directory, |
| bool* out_of_capacity, |
| bool use_non_chronos_cryptohome) { |
| if (out_of_capacity) |
| *out_of_capacity = false; |
| |
| // In crash loop mode, we don't actually need a crash directory, so don't |
| // bother creating one. |
| if (crash_sending_mode_ == kCrashLoopSendingMode) { |
| crash_directory->clear(); |
| return true; |
| } |
| |
| // For testing. |
| if (!forced_crash_directory_.empty()) { |
| *crash_directory = forced_crash_directory_; |
| return true; |
| } |
| |
| uid_t default_user_id; |
| if (!brillo::userdb::GetUserInfo(kDefaultUserName, &default_user_id, |
| nullptr)) { |
| LOG(ERROR) << "Could not find default user info"; |
| return false; |
| } |
| |
| mode_t directory_mode; |
| uid_t directory_owner; |
| gid_t directory_group; |
| base::Optional<base::FilePath> maybe_path = GetCrashDirectoryInfo( |
| euid, default_user_id, use_non_chronos_cryptohome, &directory_mode, |
| &directory_owner, &directory_group); |
| if (!maybe_path) { |
| return false; |
| } |
| base::FilePath full_path = *maybe_path; |
| |
| // Note: We "leak" dirfd to children so the /proc symlink below stays valid |
| // in their own context. We can't pass other /proc paths as they might not |
| // be accessible in the children (when dropping privs), and we don't want to |
| // pass the direct path in the filesystem as it'd be subject to TOCTOU. |
| int dirfd; |
| if (!CreateDirectoryWithSettings(full_path, directory_mode, directory_owner, |
| directory_group, &dirfd)) { |
| LOG(ERROR) << "CreateDirectory failed"; |
| return false; |
| } |
| |
| // Have all the rest of the tools access the directory by file handle. This |
| // avoids any TOCTOU races in case the underlying dir is changed on us. |
| const FilePath crash_dir_procfd = |
| FilePath("/proc/self/fd").Append(std::to_string(dirfd)); |
| LOG(INFO) << "Accessing crash dir '" << full_path.value() |
| << "' via symlinked handle '" << crash_dir_procfd.value() << "'"; |
| |
| if (!CheckHasCapacity(crash_dir_procfd, full_path.value())) { |
| if (out_of_capacity) |
| *out_of_capacity = true; |
| return false; |
| } |
| |
| *crash_directory = crash_dir_procfd; |
| return true; |
| } |
| |
| // static |
| FilePath CrashCollector::GetProcessPath(pid_t pid) { |
| return FilePath(StringPrintf("/proc/%d", pid)); |
| } |
| |
| // static |
| bool CrashCollector::GetUptime(base::TimeDelta* uptime) { |
| timespec boot_time; |
| if (clock_gettime(CLOCK_BOOTTIME, &boot_time) != 0) { |
| PLOG(ERROR) << "Failed to get boot time."; |
| return false; |
| } |
| |
| *uptime = base::TimeDelta::FromSeconds(boot_time.tv_sec) + |
| base::TimeDelta::FromMicroseconds( |
| boot_time.tv_nsec / base::Time::kNanosecondsPerMicrosecond); |
| return true; |
| } |
| |
| // static |
| bool CrashCollector::GetUptimeAtProcessStart(pid_t pid, |
| base::TimeDelta* uptime) { |
| std::string stat; |
| if (!base::ReadFileToString(GetProcessPath(pid).Append("stat"), &stat)) { |
| PLOG(ERROR) << "Failed to read process status."; |
| return false; |
| } |
| |
| uint64_t ticks; |
| if (!ParseProcessTicksFromStat(stat, &ticks)) { |
| LOG(ERROR) << "Failed to parse process status: " << stat; |
| return false; |
| } |
| |
| *uptime = base::TimeDelta::FromSecondsD(static_cast<double>(ticks) / |
| sysconf(_SC_CLK_TCK)); |
| |
| return true; |
| } |
| |
| bool CrashCollector::GetExecutableBaseNameFromPid(pid_t pid, |
| std::string* base_name) { |
| FilePath target; |
| FilePath process_path = GetProcessPath(pid); |
| FilePath exe_path = process_path.Append("exe"); |
| if (!base::ReadSymbolicLink(exe_path, &target)) { |
| LOG(INFO) << "ReadSymbolicLink failed - Path " << process_path.value() |
| << " DirectoryExists: " << base::DirectoryExists(process_path); |
| // Try to further diagnose exe readlink failure cause. |
| struct stat buf; |
| int stat_result = stat(exe_path.value().c_str(), &buf); |
| int saved_errno = errno; |
| if (stat_result < 0) { |
| LOG(INFO) << "stat " << exe_path.value() << " failed: " << stat_result |
| << " " << saved_errno; |
| } else { |
| LOG(INFO) << "stat " << exe_path.value() |
| << " succeeded: st_mode=" << buf.st_mode; |
| } |
| return false; |
| } |
| *base_name = target.BaseName().value(); |
| return true; |
| } |
| |
| // Return true if the given crash directory has not already reached |
| // maximum capacity. |
| bool CrashCollector::CheckHasCapacity(const FilePath& crash_directory, |
| const std::string& display_path) { |
| DIR* dir = opendir(crash_directory.value().c_str()); |
| if (!dir) { |
| PLOG(ERROR) << "Unable to open directory to check capacity: " |
| << crash_directory.value(); |
| return false; |
| } |
| struct dirent* ent; |
| bool full = false; |
| std::set<std::string> basenames; |
| // readdir_r is deprecated from glibc and we need to use readdir instead. |
| // readdir is safe for glibc because it guarantees readdir is thread safe, |
| // and atm we aren't supporting other C libraries |
| while ((ent = readdir(dir))) { |
| // Only count crash reports. Ignore all other supplemental files. |
| // We define "crash reports" as .meta, .dmp, .js_stack, or .core files. |
| // This does mean that we ignore random files that might accumulate but |
| // didn't come from us, but not a lot we can do about that. Our crash |
| // sender process should clean up unknown files independently. |
| const base::FilePath filename(ent->d_name); |
| const std::string ext = filename.FinalExtension(); |
| if (ext != ".core" && ext != constants::kMinidumpExtensionWithDot && |
| ext != ".meta" && ext != constants::kJavaScriptStackExtensionWithDot) |
| continue; |
| |
| // Track the basenames as our unique identifiers. When the core/dmp files |
| // are part of a single report, this will count them as one report. |
| const std::string basename = filename.RemoveFinalExtension().value(); |
| basenames.insert(basename); |
| |
| if (basenames.size() >= static_cast<size_t>(kMaxCrashDirectorySize)) { |
| LOG(WARNING) << "Crash directory " << display_path |
| << " already full with " << kMaxCrashDirectorySize |
| << " pending reports"; |
| full = true; |
| break; |
| } |
| } |
| closedir(dir); |
| return !full; |
| } |
| |
| bool CrashCollector::CheckHasCapacity(const FilePath& crash_directory) { |
| return CheckHasCapacity(crash_directory, crash_directory.value()); |
| } |
| |
| bool CrashCollector::GetLogContents(const FilePath& config_path, |
| const std::string& exec_name, |
| const FilePath& output_file) { |
| return GetMultipleLogContents(config_path, {exec_name}, output_file); |
| } |
| |
| bool CrashCollector::GetMultipleLogContents( |
| const FilePath& config_path, |
| const std::vector<std::string>& exec_names, |
| const FilePath& output_file) { |
| brillo::KeyValueStore store; |
| if (!store.Load(config_path)) { |
| LOG(WARNING) << "Unable to read log configuration file " |
| << config_path.value(); |
| return false; |
| } |
| |
| std::string collated_log_contents; |
| for (auto exec_name : exec_names) { |
| std::string command; |
| if (!store.GetString(exec_name, &command)) { |
| LOG(WARNING) << "exec name '" << exec_name << "' not found in log file"; |
| continue; |
| } |
| |
| FilePath raw_output_file; |
| if (!base::CreateTemporaryFile(&raw_output_file)) { |
| PLOG(WARNING) << "Failed to create temporary file for raw log output."; |
| continue; |
| } |
| |
| brillo::ProcessImpl diag_process; |
| diag_process.AddArg(kShellPath); |
| diag_process.AddStringOption("-c", command); |
| diag_process.RedirectOutput(raw_output_file.value()); |
| |
| const int result = diag_process.Run(); |
| |
| std::string log_contents; |
| const bool fully_read = base::ReadFileToStringWithMaxSize( |
| raw_output_file, &log_contents, max_log_size_); |
| base::DeleteFile(raw_output_file); |
| |
| if (!fully_read) { |
| if (log_contents.empty()) { |
| LOG(WARNING) << "Failed to read raw log contents."; |
| continue; |
| } |
| // If ReadFileToStringWithMaxSize returned false and log_contents is |
| // non-empty, this means the log is larger than max_log_size_. |
| LOG(WARNING) << "Log is larger than " << max_log_size_ |
| << " bytes. Truncating."; |
| log_contents.append("\n<TRUNCATED>\n"); |
| } |
| |
| // If the registered command failed, we include any (partial) output it |
| // might have produced to improve crash reports. But make a note of the |
| // failure. |
| if (result != 0) { |
| const std::string warning = StringPrintf( |
| "\nLog command \"%s\" exited with %i\n", command.c_str(), result); |
| log_contents.append(warning); |
| LOG(WARNING) << warning; |
| } |
| |
| collated_log_contents.append(log_contents); |
| } |
| |
| if (collated_log_contents.empty()) |
| return false; |
| |
| // Always do this after collated_log_contents is "finished" so we don't |
| // accidentally leak data. |
| StripSensitiveData(&collated_log_contents); |
| |
| if (output_file.FinalExtension() == ".gz") { |
| if (!WriteNewCompressedFile(output_file, collated_log_contents.data(), |
| collated_log_contents.size())) { |
| LOG(WARNING) << "Error writing sanitized log to " << output_file.value(); |
| return false; |
| } |
| } else { |
| if (WriteNewFile(output_file, collated_log_contents) != |
| static_cast<int>(collated_log_contents.length())) { |
| PLOG(WARNING) << "Error writing sanitized log to " << output_file.value(); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool CrashCollector::GetProcessTree(pid_t pid, |
| const base::FilePath& output_file) { |
| std::ostringstream stream; |
| |
| // Grab a limited number of parent process details. |
| for (size_t depth = 0; depth < kMaxParentProcessLogs; ++depth) { |
| std::string contents; |
| |
| stream << "### Process " << pid << std::endl; |
| |
| const FilePath proc_path = GetProcessPath(pid); |
| const FilePath status_path = proc_path.Append("status"); |
| |
| // Read the command line and append it to the log. |
| if (!base::ReadFileToString(proc_path.Append("cmdline"), &contents)) |
| break; |
| base::ReplaceChars(contents, std::string(1, '\0'), " ", &contents); |
| stream << "cmdline: " << contents << std::endl; |
| |
| // Read the status file and append it to the log. |
| if (!base::ReadFileToString(proc_path.Append("status"), &contents)) |
| break; |
| stream << contents << std::endl; |
| |
| // Pull out the parent pid from the status file. The line will look like: |
| // PPid:\t1234 |
| base::StringPairs pairs; |
| if (!base::SplitStringIntoKeyValuePairs(contents, ':', '\n', &pairs)) |
| break; |
| pid = 0; |
| for (const auto& key_value : pairs) { |
| if (key_value.first == "PPid") { |
| std::string value; |
| int ppid; |
| |
| // Parse the parent pid. Set it only if it's valid. |
| base::TrimWhitespaceASCII(key_value.second, base::TRIM_ALL, &value); |
| if (base::StringToInt(value, &ppid)) |
| pid = ppid; |
| break; |
| } |
| } |
| // If we couldn't find the parent pid, break out. |
| if (pid == 0) |
| break; |
| } |
| |
| // Always do this after log collection is "finished" so we don't accidentally |
| // leak data. |
| std::string log = stream.str(); |
| StripSensitiveData(&log); |
| |
| if (WriteNewFile(output_file, log) != static_cast<int>(log.size())) { |
| PLOG(WARNING) << "Error writing sanitized log to " << output_file.value(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void CrashCollector::AddCrashMetaData(const std::string& key, |
| const std::string& value) { |
| if (key.empty()) { |
| LOG(ERROR) << "Cannot use empty key"; |
| return; |
| } |
| |
| std::string sanitized_key; |
| for (char c : key) { |
| if (!(base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || c == '_' || |
| c == '-' || c == '.')) { |
| // Replace invalid characters with '_' |
| c = '_'; |
| } |
| sanitized_key.push_back(c); |
| } |
| |
| std::string sanitized_value; |
| for (char c : value) { |
| if (c == '\n') { |
| // append a literal '\n' to indicate to users that there was a newline |
| // here, but do not use an actual newline, since brillo's KeyValueStore |
| // parser cannot handle unescaped newlines, and downstream systems might |
| // also have trouble with them. |
| sanitized_value.append("\\n"); |
| } else { |
| sanitized_value.push_back(c); |
| } |
| } |
| extra_metadata_.append( |
| StringPrintf("%s=%s\n", sanitized_key.c_str(), sanitized_value.c_str())); |
| } |
| |
| void CrashCollector::AddCrashMetaUploadFile(const std::string& key, |
| const std::string& path) { |
| if (!path.empty()) { |
| if (path.find('/') != std::string::npos) { |
| LOG(ERROR) << "Upload files must be basenames only: " << path; |
| return; |
| } |
| AddCrashMetaData(constants::kUploadFilePrefix + key, path); |
| } |
| } |
| |
| void CrashCollector::AddCrashMetaUploadData(const std::string& key, |
| const std::string& value) { |
| if (!value.empty()) |
| AddCrashMetaData(constants::kUploadVarPrefix + key, value); |
| } |
| |
| void CrashCollector::AddCrashMetaUploadText(const std::string& key, |
| const std::string& path) { |
| if (!path.empty()) { |
| if (path.find('/') != std::string::npos) { |
| LOG(ERROR) << "Upload files must be basenames only: " << path; |
| return; |
| } |
| AddCrashMetaData(constants::kUploadTextPrefix + key, path); |
| } |
| } |
| |
| std::string CrashCollector::GetLsbReleaseValue(const std::string& key) const { |
| std::vector<base::FilePath> directories = {crash_reporter_state_path_, |
| lsb_release_.DirName()}; |
| |
| std::string value; |
| if (util::GetCachedKeyValue(lsb_release_.BaseName(), key, directories, |
| &value)) { |
| return value; |
| } |
| return kUnknownValue; |
| } |
| |
| std::string CrashCollector::GetOsVersion() const { |
| return GetLsbReleaseValue(kLsbOsVersionKey); |
| } |
| |
| std::string CrashCollector::GetOsMilestone() const { |
| return GetLsbReleaseValue(kLsbOsMilestoneKey); |
| } |
| |
| std::string CrashCollector::GetOsDescription() const { |
| return GetLsbReleaseValue(kLsbOsDescriptionKey); |
| } |
| |
| std::string CrashCollector::GetChannel() const { |
| // gives a string with "-channel" suffix, e.g. "testimage-channel", |
| // "stable-channel", "beta-channel", "dev-channel", "canary-channel". |
| std::string channel = GetLsbReleaseValue(kLsbChannelKey); |
| |
| // strip the "-channel" suffix. |
| channel = channel.substr(0, channel.find("-")); |
| |
| if (channel == "testimage") { |
| return "test"; |
| } |
| |
| return channel; |
| } |
| |
| std::string CrashCollector::GetProductVersion() const { |
| return GetOsVersion(); |
| } |
| |
| std::string CrashCollector::GetKernelName() const { |
| struct utsname buf; |
| if (!test_kernel_name_.empty()) |
| return test_kernel_name_; |
| |
| if (uname(&buf)) |
| return kUnknownValue; |
| |
| return buf.sysname; |
| } |
| |
| std::string CrashCollector::GetKernelVersion() const { |
| struct utsname buf; |
| if (!test_kernel_version_.empty()) |
| return test_kernel_version_; |
| |
| if (uname(&buf)) |
| return kUnknownValue; |
| |
| // 3.8.11 #1 SMP Wed Aug 22 02:18:30 PDT 2018 |
| return StringPrintf("%s %s", buf.release, buf.version); |
| } |
| |
| base::Optional<bool> CrashCollector::IsEnterpriseEnrolled() { |
| DCHECK(device_policy_); |
| if (!device_policy_loaded_) { |
| if (!device_policy_->LoadPolicy()) { |
| LOG(ERROR) << "Failed to load device policy"; |
| return base::nullopt; |
| } |
| device_policy_loaded_ = true; |
| } |
| |
| return device_policy_->IsEnterpriseEnrolled(); |
| } |
| |
| // Callback for CallMethodWithErrorCallback(). Discards the response pointer |
| // and just calls |callback|. |
| static void IgnoreResponsePointer(base::OnceCallback<void()> callback, |
| dbus::Response*) { |
| std::move(callback).Run(); |
| } |
| |
| // Error callback for CallMethodWithErrorCallback(). Discards the error pointer |
| // and just calls |callback|. |
| static void IgnoreErrorResponsePointer(base::OnceCallback<void()> callback, |
| dbus::ErrorResponse*) { |
| // We set the timeout to 0, so of course we time out before we get a response. |
| // 99% of the time, the ErrorResponse is just "NoReply". Don't spam the error |
| // log with that information, just discard the error response. |
| std::move(callback).Run(); |
| } |
| |
| void CrashCollector::FinishCrash(const FilePath& meta_path, |
| const std::string& exec_name, |
| const std::string& payload_name) { |
| DCHECK(!is_finished_); |
| |
| // All files are relative to the metadata, so reject anything else. |
| if (payload_name.find('/') != std::string::npos) { |
| LOG(ERROR) << "Upload files must be basenames only: " << payload_name; |
| return; |
| } |
| |
| const FilePath payload_path = meta_path.DirName().Append(payload_name); |
| |
| LOG(INFO) << "Finishing crash. Meta file: " << meta_path.value(); |
| |
| if (!AddVariations()) { |
| LOG(ERROR) << "Failed to add variations to report"; |
| } |
| |
| const std::string product_version = GetProductVersion(); |
| std::string product_version_info = |
| StringPrintf("ver=%s\n", product_version.c_str()); |
| |
| const std::string milestone = GetOsMilestone(); |
| const std::string description = GetOsDescription(); |
| base::Time os_timestamp = util::GetOsTimestamp(); |
| std::string os_timestamp_str; |
| if (!os_timestamp.is_null()) { |
| os_timestamp_str = |
| StringPrintf("os_millis=%" PRId64 "\n", |
| (os_timestamp - base::Time::UnixEpoch()).InMilliseconds()); |
| } |
| |
| // Populate the channel (if not already populated--chrome will first attempt |
| // to populate this). |
| if (extra_metadata_.find("upload_var_channel") == std::string::npos) { |
| AddCrashMetaUploadData(kChannelKey, GetChannel()); |
| } |
| |
| std::string lsb_release_info = StringPrintf( |
| "upload_var_lsb-release=%s\n" |
| "upload_var_cros_milestone=%s\n" |
| "%s", |
| description.c_str(), milestone.c_str(), os_timestamp_str.c_str()); |
| |
| const std::string kernel_name = GetKernelName(); |
| const std::string kernel_version = GetKernelVersion(); |
| std::string kernel_info = StringPrintf( |
| "upload_var_osName=%s\n" |
| "upload_var_osVersion=%s\n", |
| kernel_name.c_str(), kernel_version.c_str()); |
| |
| std::string version_info = |
| product_version_info + lsb_release_info + kernel_info; |
| |
| base::Optional<bool> is_enterprise_enrolled = IsEnterpriseEnrolled(); |
| if (is_enterprise_enrolled.has_value()) { |
| AddCrashMetaUploadData("is-enterprise-enrolled", |
| *is_enterprise_enrolled ? "true" : "false"); |
| } |
| |
| std::string in_progress_test; |
| if (base::ReadFileToString(paths::GetAt(paths::kSystemRunStateDirectory, |
| paths::kInProgressTestName), |
| &in_progress_test)) { |
| AddCrashMetaUploadData("in_progress_integration_test", in_progress_test); |
| } |
| |
| std::string exec_name_line; |
| if (!exec_name.empty()) { |
| exec_name_line = base::StrCat({"exec_name=", exec_name, "\n"}); |
| } |
| |
| base::Time now = test_clock_ ? test_clock_->Now() : base::Time::Now(); |
| int64_t now_millis = (now - base::Time::UnixEpoch()).InMilliseconds(); |
| std::string meta_data = |
| StringPrintf("%supload_var_reportTimeMillis=%" PRId64 |
| "\n" |
| "%s" |
| "%s" |
| "payload=%s\n" |
| "done=1\n", |
| extra_metadata_.c_str(), now_millis, exec_name_line.c_str(), |
| version_info.c_str(), payload_name.c_str()); |
| // 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. |
| if (WriteNewFile(meta_path, meta_data) < 0) { |
| PLOG(ERROR) << "Unable to write " << meta_path.value(); |
| } |
| |
| // Record report created metric in UMA. |
| metrics_lib_->SendCrosEventToUMA(kReportCountEnum); |
| |
| if (crash_sending_mode_ == kCrashLoopSendingMode) { |
| SetUpDBus(); |
| |
| // We'd like to call debugd_proxy_->UploadSingleCrash here; that seems like |
| // the simplest method. However, calling debugd_proxy_->UploadSingleCrash |
| // with a timeout of zero will spam the error log with messages about timing |
| // out and not receiving a response. Going through |
| // CallMethodWithErrorCallback avoids the error messages, but it does mean |
| // we need a RunLoop. |
| base::RunLoop run_loop; |
| auto quit_closure = run_loop.QuitClosure(); |
| dbus::MethodCall method_call(debugd::kDebugdInterface, |
| debugd::kUploadSingleCrash); |
| dbus::MessageWriter writer(&method_call); |
| brillo::dbus_utils::DBusParamWriter::Append(&writer, |
| std::move(in_memory_files_)); |
| debugd_proxy_->GetObjectProxy()->CallMethodWithErrorCallback( |
| &method_call, 0 /*timeout_ms*/, |
| base::BindOnce(IgnoreResponsePointer, quit_closure), |
| base::BindOnce(IgnoreErrorResponsePointer, quit_closure)); |
| run_loop.Run(); |
| } |
| |
| is_finished_ = true; |
| } |
| |
| bool CrashCollector::ShouldHandleChromeCrashes() { |
| // If we're testing crash reporter itself, we don't want to allow an |
| // override for chrome crashes. And, let's be conservative and only |
| // allow an override for developer images. |
| if (!util::IsCrashTestInProgress() && util::IsDeveloperImage()) { |
| // Check if there's an override to indicate we should indeed collect |
| // chrome crashes. This allows the crashes to still be tracked when |
| // they occur in integration tests. See "crosbug.com/17987". |
| if (base::PathExists(FilePath(kCollectChromeFile))) |
| return true; |
| } |
| // We default to ignoring chrome crashes. |
| return false; |
| } |
| |
| bool CrashCollector::InitializeSystemCrashDirectories(bool early) { |
| if (!CreateDirectoryWithSettings(FilePath(paths::kSystemRunStateDirectory), |
| kSystemRunStateDirectoryMode, kRootUid, |
| kRootGroup, nullptr)) |
| return false; |
| |
| if (early) { |
| if (!CreateDirectoryWithSettings(FilePath(paths::kSystemRunCrashDirectory), |
| kSystemRunStateDirectoryMode, kRootUid, |
| kRootGroup, nullptr)) |
| return false; |
| } else { |
| gid_t directory_group; |
| if (!brillo::userdb::GetGroupInfo(constants::kCrashGroupName, |
| &directory_group)) { |
| PLOG(ERROR) << "Group " << constants::kCrashGroupName << " doesn't exist"; |
| return false; |
| } |
| if (!CreateDirectoryWithSettings(FilePath(paths::kSystemCrashDirectory), |
| kSystemCrashDirectoryMode, kRootUid, |
| directory_group, nullptr, |
| /*files_mode=*/kSystemCrashFilesMode)) |
| return false; |
| |
| if (!CreateDirectoryWithSettings( |
| FilePath(paths::kCrashReporterStateDirectory), |
| kCrashReporterStateDirectoryMode, kRootUid, kRootGroup, nullptr)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool CrashCollector::InitializeSystemMetricsDirectories() { |
| uid_t metrics_user_id; |
| gid_t metrics_group_id; |
| |
| if (!brillo::userdb::GetUserInfo(kMetricsUserName, &metrics_user_id, |
| &metrics_group_id)) { |
| PLOG(ERROR) << "Could not find user " << kMetricsUserName; |
| return false; |
| } |
| |
| if (!brillo::userdb::GetGroupInfo(kMetricsGroupName, &metrics_group_id)) { |
| PLOG(ERROR) << "Could not find group " << kMetricsGroupName; |
| return false; |
| } |
| |
| FilePath metrics_flag_directory(paths::kSystemRunMetricsFlagDirectory); |
| FilePath metrics_external_dir = metrics_flag_directory.DirName(); |
| FilePath metrics_dir = metrics_external_dir.DirName(); |
| |
| // Ensure /run/metrics directory exists. |
| if (!CreateDirectoryWithSettings(metrics_dir, kSystemRunMetricsFlagMode, |
| metrics_user_id, metrics_group_id, nullptr)) |
| return false; |
| |
| // Ensure metrics/external exists. |
| if (!CreateDirectoryWithSettings(metrics_external_dir, |
| kSystemRunMetricsFlagMode, metrics_user_id, |
| metrics_group_id, nullptr)) |
| return false; |
| |
| // Create final crash-reporter flag directory. |
| if (!CreateDirectoryWithSettings(metrics_flag_directory, |
| kSystemRunMetricsFlagMode, metrics_user_id, |
| metrics_group_id, nullptr)) |
| return false; |
| |
| return true; |
| } |
| |
| // static |
| bool CrashCollector::ParseProcessTicksFromStat(base::StringPiece stat, |
| uint64_t* ticks) { |
| // Skip "pid" and "comm" fields. See format in proc(5). |
| const auto pos = stat.find_last_of(')'); |
| if (pos == base::StringPiece::npos) |
| return false; |
| |
| stat.remove_prefix(pos + 1); |
| const auto fields = base::SplitStringPiece(stat, " ", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| |
| constexpr size_t kStartTimePos = 19; |
| return fields.size() > kStartTimePos && |
| base::StringToUint64(fields[kStartTimePos], ticks); |
| } |
| |
| bool CrashCollector::AddVariations() { |
| std::vector<FilePath> directories; |
| if (extra_metadata_.find(kVariationsKey) != std::string::npos) { |
| // Don't add variations a second time if something (e.g. chrome) already |
| // did. |
| return true; |
| } |
| |
| FilePath home_directory; |
| // In this multiprofile world, there is no one-specific user dir anymore. |
| // Ask the session manager for the active ones, then just run with the |
| // first result we get back. |
| if (!TrySetUpDBus() || !session_manager_proxy_ || |
| !util::GetUserHomeDirectories(session_manager_proxy_.get(), |
| &directories) || |
| directories.empty()) { |
| LOG(ERROR) << "Could not get user home directories, using default."; |
| home_directory = paths::Get(paths::kFallbackToHomeDir); |
| } else { |
| home_directory = directories[0]; |
| } |
| |
| std::string contents; |
| // TODO(mutexlox): When anomaly-detector invokes crash_reporter it cannot read |
| // this file as it's in the user's home dir. Get the info to anomaly-detector |
| // some other way. |
| base::FilePath to_read = home_directory.Append(paths::kVariationsListFile); |
| if (!CarefullyReadFileToStringWithMaxSize( |
| to_read, kArbitraryMaxVariationsSize, &contents)) { |
| LOG(ERROR) << "Couldn't read " << to_read.value(); |
| return false; |
| } |
| // Validate the variations file in case a user overwrote it. |
| brillo::KeyValueStore variant_store; |
| if (!variant_store.LoadFromString(contents)) { |
| LOG(ERROR) << "Failed to load contents " << contents; |
| return false; |
| } |
| std::string num_exp; |
| if (!variant_store.GetString(kNumExperimentsKey, &num_exp)) { |
| LOG(ERROR) << "Failed to get value for " << kNumExperimentsKey |
| << " from contents " << contents; |
| return false; |
| } |
| std::string variations; |
| if (!variant_store.GetString(kVariationsKey, &variations)) { |
| LOG(ERROR) << "Failed to get value for " << kVariationsKey |
| << " from contents " << contents; |
| return false; |
| } |
| AddCrashMetaUploadData(kVariationsKey, variations); |
| AddCrashMetaUploadData(kNumExperimentsKey, num_exp); |
| return true; |
| } |
| |
| void CrashCollector::EnqueueCollectionErrorLog(ErrorType error_type, |
| const std::string& orig_exec) { |
| LOG(INFO) << "Writing conversion problems as separate crash report."; |
| |
| const std::string exec = "crash_reporter_failure"; |
| // We use a distinct basename to avoid having to deal with any possible files |
| // that the collector may have started to write before failing. |
| const std::string basename = |
| FormatDumpBasename(exec, time(nullptr), getpid()); |
| |
| // Get rid of the existing metadata, since we're now writing info about |
| // errors *pertaining to collection* rather than the original program. |
| extra_metadata_.clear(); |
| AddCrashMetaUploadData(kCollectorNameKey, exec); |
| // Record the original collector name for analytics purposes. (e.g. to see |
| // if one collector fails more often than others.) |
| AddCrashMetaUploadData("orig_collector", collector_name_); |
| AddCrashMetaUploadData("orig_exec", orig_exec); |
| |
| FilePath crash_path; |
| if (!GetCreatedCrashDirectoryByEuid(0, &crash_path, nullptr)) { |
| LOG(ERROR) << "Could not even get log directory; out of space?"; |
| return; |
| } |
| |
| std::string type = GetErrorTypeSignature(error_type); |
| AddCrashMetaData("sig", base::StrCat({kCollectionErrorSignature, "_", type})); |
| AddCrashMetaData("error_type", type); |
| FilePath log_path = GetCrashPath(crash_path, basename, "log"); |
| |
| std::string error_log = brillo::GetLog(); |
| // 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. |
| if (WriteNewFile(log_path, error_log) < 0) { |
| LOG(ERROR) << "Error writing new file " << log_path.value(); |
| return; |
| } |
| |
| // If we fail to get this log, still try to proceed (the other log could be |
| // useful on its own). |
| FilePath ps_log_path = GetCrashPath(crash_path, basename, "pslog"); |
| if (GetLogContents(FilePath(log_config_path_), kCollectionErrorSignature, |
| ps_log_path)) { |
| AddCrashMetaUploadFile("pslog", ps_log_path.BaseName().value()); |
| } else { |
| LOG(ERROR) << "Failed getting collection error log contents for " |
| << kCollectionErrorSignature; |
| } |
| |
| FilePath meta_path = GetCrashPath(crash_path, basename, "meta"); |
| FinishCrash(meta_path, exec, log_path.BaseName().value()); |
| } |
| |
| void CrashCollector::LogCrash(const std::string& message, |
| const std::string& reason) const { |
| LOG(WARNING) << '[' << tag_ << "] " << message << " (" << reason << ')'; |
| } |
| |
| std::string CrashCollector::GetErrorTypeSignature(ErrorType error_type) const { |
| switch (error_type) { |
| case kErrorSystemIssue: |
| return "system-issue"; |
| case kErrorReadCoreData: |
| return "read-core-data"; |
| case kErrorUnusableProcFiles: |
| return "unusable-proc-files"; |
| case kErrorInvalidCoreFile: |
| return "invalid-core-file"; |
| case kErrorUnsupported32BitCoreFile: |
| return "unsupported-32bit-core-file"; |
| case kErrorCore2MinidumpConversion: |
| return "core2md-conversion"; |
| default: |
| return ""; |
| } |
| } |