diff options
author | Xin Li <delphij@google.com> | 2016-06-24 11:25:21 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2016-06-24 11:29:58 -0700 |
commit | 4f92832ef26e4fbb6eedd42ad0ee0040877534bb (patch) | |
tree | 865dca4a11fa42b8e57b1e4392a0d3bcf5bc5932 | |
parent | b7c2af415708e949599ed3bd635136f564af6ee3 (diff) | |
parent | 85e7f90c3ce6e54fd4072a029ce3444618716cfa (diff) | |
download | crash_reporter-master.tar.gz |
Initial import of crash_reporter from platform/system/core
BUG: 29548040
42 files changed, 7119 insertions, 0 deletions
diff --git a/.project_alias b/.project_alias new file mode 100644 index 0000000..0bc3798 --- /dev/null +++ b/.project_alias @@ -0,0 +1 @@ +crash diff --git a/99-crash-reporter.rules b/99-crash-reporter.rules new file mode 100644 index 0000000..aea5b1c --- /dev/null +++ b/99-crash-reporter.rules @@ -0,0 +1,6 @@ +ACTION=="change", SUBSYSTEM=="drm", KERNEL=="card0", ENV{ERROR}=="1", RUN+="/sbin/crash_reporter --udev=KERNEL=card0:SUBSYSTEM=drm:ACTION=change" +# For detecting cypress trackpad issue. Passing into crash_reporter SUBSYSTEM=i2c-cyapa since crash_reporter does not handle DRIVER string. +ACTION=="change", SUBSYSTEM=="i2c", DRIVER=="cyapa", ENV{ERROR}=="1", RUN+="/sbin/crash_reporter --udev=SUBSYSTEM=i2c-cyapa:ACTION=change" +# For detecting Atmel trackpad/touchscreen issue. Passing into crash_reporter SUBSYSTEM=i2c-atmel_mxt_ts since crash_reporter does not handle DRIVER string. +ACTION=="change", SUBSYSTEM=="i2c", DRIVER=="atmel_mxt_ts", ENV{ERROR}=="1", RUN+="/sbin/crash_reporter --udev=SUBSYSTEM=i2c-atmel_mxt_ts:ACTION=change" +ACTION=="add", SUBSYSTEM=="devcoredump", RUN+="/sbin/crash_reporter --udev=SUBSYSTEM=devcoredump:ACTION=add:KERNEL_NUMBER=%n" diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..ce9dc73 --- /dev/null +++ b/Android.mk @@ -0,0 +1,147 @@ +# Copyright (C) 2015 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +crash_reporter_cpp_extension := .cc + +crash_reporter_src := crash_collector.cc \ + kernel_collector.cc \ + kernel_warning_collector.cc \ + unclean_shutdown_collector.cc \ + user_collector.cc + +crash_reporter_includes := external/gtest/include + +crash_reporter_test_src := crash_collector_test.cc \ + crash_reporter_logs_test.cc \ + kernel_collector_test.cc \ + testrunner.cc \ + unclean_shutdown_collector_test.cc \ + user_collector_test.cc + +warn_collector_src := warn_collector.l + +# Crash reporter static library. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libcrash +LOCAL_CPP_EXTENSION := $(crash_reporter_cpp_extension) +LOCAL_C_INCLUDES := $(crash_reporter_includes) +LOCAL_SHARED_LIBRARIES := libchrome \ + libbinder \ + libbrillo \ + libcutils \ + libmetrics \ + libpcrecpp +LOCAL_STATIC_LIBRARIES := libmetricscollectorservice +LOCAL_SRC_FILES := $(crash_reporter_src) +include $(BUILD_STATIC_LIBRARY) + +# Crash reporter client. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := crash_reporter +LOCAL_CPP_EXTENSION := $(crash_reporter_cpp_extension) +LOCAL_C_INCLUDES := $(crash_reporter_includes) +LOCAL_REQUIRED_MODULES := core2md \ + crash_reporter_logs.conf \ + crash_sender \ + crash_server +LOCAL_INIT_RC := crash_reporter.rc +LOCAL_SHARED_LIBRARIES := libchrome \ + libbinder \ + libbrillo \ + libcutils \ + libmetrics \ + libpcrecpp \ + libutils +LOCAL_SRC_FILES := crash_reporter.cc +LOCAL_STATIC_LIBRARIES := libcrash \ + libmetricscollectorservice +include $(BUILD_EXECUTABLE) + +# Crash sender script. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := crash_sender +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_PATH := $(TARGET_OUT_EXECUTABLES) +LOCAL_REQUIRED_MODULES := curl grep periodic_scheduler +LOCAL_SRC_FILES := crash_sender +include $(BUILD_PREBUILT) + +# Warn collector client. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := warn_collector +LOCAL_CPP_EXTENSION := $(crash_reporter_cpp_extension) +LOCAL_SHARED_LIBRARIES := libmetrics +LOCAL_SRC_FILES := $(warn_collector_src) +include $(BUILD_EXECUTABLE) + +# /etc/os-release.d/crash_server configuration file. +# ======================================================== +ifdef OSRELEASED_DIRECTORY +include $(CLEAR_VARS) +LOCAL_MODULE := crash_server +LOCAL_MODULE_CLASS := ETC +LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/$(OSRELEASED_DIRECTORY) +include $(BUILD_SYSTEM)/base_rules.mk + +# Optionally populate the BRILLO_CRASH_SERVER variable from a product +# configuration file: brillo/crash_server. +LOADED_BRILLO_CRASH_SERVER := $(call cfgtree-get-if-exists,brillo/crash_server) + +# If the crash server isn't set, use a blank value. crash_sender +# will log it as a configuration error. +$(LOCAL_BUILT_MODULE): BRILLO_CRASH_SERVER ?= "$(LOADED_BRILLO_CRASH_SERVER)" +$(LOCAL_BUILT_MODULE): + $(hide)mkdir -p $(dir $@) + echo $(BRILLO_CRASH_SERVER) > $@ +endif + +# Crash reporter logs conf file. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := crash_reporter_logs.conf +LOCAL_MODULE_CLASS := ETC +LOCAL_MODULE_PATH := $(PRODUCT_OUT)/system/etc +LOCAL_SRC_FILES := crash_reporter_logs.conf +include $(BUILD_PREBUILT) + +# Periodic Scheduler. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := periodic_scheduler +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_PATH := $(TARGET_OUT_EXECUTABLES) +LOCAL_SRC_FILES := periodic_scheduler +include $(BUILD_PREBUILT) + +# Crash reporter tests. +# ======================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := crash_reporter_tests +LOCAL_CPP_EXTENSION := $(crash_reporter_cpp_extension) +ifdef BRILLO +LOCAL_MODULE_TAGS := eng +endif +LOCAL_SHARED_LIBRARIES := libchrome \ + libbrillo \ + libcutils \ + libpcrecpp +LOCAL_SRC_FILES := $(crash_reporter_test_src) +LOCAL_STATIC_LIBRARIES := libcrash libgmock +include $(BUILD_NATIVE_TEST) @@ -0,0 +1,2 @@ +set noparent +vapier@chromium.org diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ac0a86 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# crash_reporter + +`crash_reporter` is a deamon running on the device that saves the call stack of +crashing programs. It makes use of the +[Breakpad](https://chromium.googlesource.com/breakpad/breakpad/) library. + +During a build, Breakpad symbol files are generated for all binaries. They are +packaged into a zip file when running `m dist`, so that a developer can upload +them to the crash server. + +On a device, if the user has opted in to metrics and crash reporting, a +Breakpad minidump is generated when an executable crashes, which is then +uploaded to the crash server. + +On the crash server, it compares the minidump's signature to the symbol files +that the developer has uploaded, and extracts and symbolizes the stack trace +from the minidump. + +## SELinux policies + +In order to correctly generate a minidump, `crash_reporter` needs to be given +the proper SELinux permissions for accessing the domain of the crashing +executable. By default, `crash_reporter` has only been given access to a select +number of system domains, such as `metricsd`, `weave`, and `update_engine`. If +a developer wants their executable's crashes to be caught by `crash_reporter`, +they will have to set their SELinux policies in their .te file to allow +`crash_reporter` access to their domain. This can be done through a simple +[macro](https://android.googlesource.com/device/generic/brillo/+/master/sepolicy/te_macros): + + allow_crash_reporter(domain_name) + +Replace *domain_name* with whatever domain is assigned to the executable in +the `file_contexts` file. + +## Configuration + +`crash_reporter` has a few different configuration options that have to be set. + +- Crashes are only handled and uploaded if analytics reporting is enabled, + either via the weave call to set `_metrics.enableAnalyticsReporting` or by + manually creating the file `/data/misc/metrics/enabled` (for testing only). +- The `BRILLO_CRASH_SERVER` make variable should be set in the `product.mk` + file to the URL of the crash server. For Brillo builds, it is set + automatically through the product configuration. Setting this variable will + populate the `/etc/os-release.d/crash_server` file on the device, which is + read by `crash_sender`. +- The `BRILLO_PRODUCT_ID` make variable should be set in the `product.mk` file + to the product's ID. For Brillo builds, it is set automatically through the + product configuration. Setting this variable will populate the + `/etc/os-release.d/product_id`, which is read by `crash_sender`. + +## Uploading crash reports in *eng* builds + +By default, crash reports are only uploaded to the server for production +*user* and *userdebug* images. In *eng* builds, with crash reporting enabled +the device will generate minidumps for any crashing executables but will not +send them to the crash server. If a developer does want to force an upload, +they can do so by issuing the command `SECONDS_SEND_SPREAD=5 FORCE_OFFICIAL=1 +crash_sender` from an ADB shell. This will send the report to the server, with +the *image_type* field set to *force-official* so that these reports can be +differentiated from normal reports. diff --git a/TEST_WARNING b/TEST_WARNING new file mode 100644 index 0000000..64ad2e9 --- /dev/null +++ b/TEST_WARNING @@ -0,0 +1,31 @@ +Apr 31 25:25:25 localhost kernel: [117959.226729] [<ffffffff810e16bf>] do_vfs_ioctl+0x469/0x4b3 +Apr 31 25:25:25 localhost kernel: [117959.226738] [<ffffffff810d3117>] ? fsnotify_access+0x58/0x60 +Apr 31 25:25:25 localhost kernel: [117959.226747] [<ffffffff810d3791>] ? vfs_read+0xad/0xd7 +Apr 31 25:25:25 localhost kernel: [117959.226756] [<ffffffff810e175f>] sys_ioctl+0x56/0x7b +Apr 31 25:25:25 localhost kernel: [117959.226765] [<ffffffff810d37fe>] ? sys_read+0x43/0x73 +Apr 31 25:25:25 localhost kernel: [117959.226774] [<ffffffff8146b7d2>] system_call_fastpath+0x16/0x1b +Apr 31 25:25:25 localhost kernel: [117959.226782] ---[ end trace f16822cad7406cec ]--- +Apr 31 25:25:25 localhost kernel: [117959.231085] ------------[ cut here ]------------ +Apr 31 25:25:25 localhost kernel: [117959.231100] WARNING: at /mnt/host/source/src/third_party/kernel/files/drivers/gpu/drm/i915/intel_dp.c:351 intel_dp_check_edp+0x6b/0xb9() +Apr 31 25:25:25 localhost kernel: [117959.231113] Hardware name: Link +Apr 31 25:25:25 localhost kernel: [117959.231117] eDP powered off while attempting aux channel communication. +Apr 31 25:25:25 localhost kernel: [117959.231240] Pid: 10508, comm: X Tainted: G WC 3.4.0 #1 +Apr 31 25:25:25 localhost kernel: [117959.231247] Call Trace: +Apr 31 25:25:25 localhost kernel: [117959.231393] [<ffffffff810d3117>] ? fsnotify_access+0x58/0x60 +Apr 31 25:25:25 localhost kernel: [117959.231402] [<ffffffff810d3791>] ? vfs_read+0xad/0xd7 +Apr 31 25:25:25 localhost kernel: [117959.231411] [<ffffffff810e175f>] sys_ioctl+0x56/0x7b +Apr 31 25:25:25 localhost kernel: [117959.231420] [<ffffffff810d37fe>] ? sys_read+0x43/0x73 +Apr 31 25:25:25 localhost kernel: [117959.231431] [<ffffffff8146b7d2>] system_call_fastpath+0x16/0x1b +Apr 31 25:25:25 localhost kernel: [117959.231439] ---[ end trace f16822cad7406ced ]--- +Apr 31 25:25:25 localhost kernel: [117959.231450] ------------[ cut here ]------------ +Apr 31 25:25:25 localhost kernel: [117959.231458] BARNING: at /mnt/host/source/src/third_party/kernel/files/drivers/gpu/drm/i915/intel_dp.c:351 intel_dp_check_edp+0x6b/0xb9() +Apr 31 25:25:25 localhost kernel: [117959.231458] ("BARNING" above is intentional) +Apr 31 25:25:25 localhost kernel: [117959.231471] Hardware name: Link +Apr 31 25:25:25 localhost kernel: [117959.231475] eDP powered off while attempting aux channel communication. +Apr 31 25:25:25 localhost kernel: [117959.231482] Modules linked in: nls_iso8859_1 nls_cp437 vfat fat rfcomm i2c_dev ath9k_btcoex snd_hda_codec_hdmi snd_hda_codec_ca0132 mac80211 snd_hda_intel ath9k_common_btcoex snd_hda_codec ath9k_hw_btcoex aesni_intel cryptd snd_hwdep ath snd_pcm aes_x86_64 isl29018(C) memconsole snd_timer snd_page_alloc industrialio(C) cfg80211 rtc_cmos nm10_gpio zram(C) zsmalloc(C) lzo_decompress lzo_compress fuse nf_conntrack_ipv6 nf_defrag_ipv6 ip6table_filter ip6_tables xt_mark option usb_wwan cdc_ether usbnet ath3k btusb bluetooth uvcvideo videobuf2_core videodev videobuf2_vmalloc videobuf2_memops joydev +Apr 31 25:25:25 localhost kernel: [117959.231588] Pid: 10508, comm: X Tainted: G WC 3.4.0 #1 +Apr 31 25:25:25 localhost kernel: [117959.231595] Call Trace: +Apr 31 25:25:25 localhost kernel: [117959.231601] [<ffffffff8102a931>] warn_slowpath_common+0x83/0x9c +Apr 31 25:25:25 localhost kernel: [117959.231610] [<ffffffff8102a9ed>] warn_slowpath_fmt+0x46/0x48 +Apr 31 25:25:25 localhost kernel: [117959.231620] [<ffffffff812af495>] intel_dp_check_edp+0x6b/0xb9 +Apr 31 25:25:25 localhost kernel: [117959.231629] [<ffffffff8102a9ed>] ? warn_slowpath_fmt+ diff --git a/crash_collector.cc b/crash_collector.cc new file mode 100644 index 0000000..d993576 --- /dev/null +++ b/crash_collector.cc @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "crash_collector.h" + +#include <dirent.h> +#include <fcntl.h> // For file creation modes. +#include <inttypes.h> +#include <linux/limits.h> // PATH_MAX +#include <pwd.h> // For struct passwd. +#include <sys/types.h> // for mode_t. +#include <sys/wait.h> // For waitpid. +#include <unistd.h> // For execv and fork. + +#include <set> +#include <utility> +#include <vector> + +#include <base/files/file_util.h> +#include <base/logging.h> +#include <base/posix/eintr_wrapper.h> +#include <base/strings/string_split.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <brillo/key_value_store.h> +#include <brillo/osrelease_reader.h> +#include <brillo/process.h> + +namespace { + +const char kCollectChromeFile[] = + "/mnt/stateful_partition/etc/collect_chrome_crashes"; +const char kCrashTestInProgressPath[] = + "/data/misc/crash_reporter/tmp/crash-test-in-progress"; +const char kDefaultLogConfig[] = "/etc/crash_reporter_logs.conf"; +const char kDefaultUserName[] = "chronos"; +const char kLeaveCoreFile[] = "/data/misc/crash_reporter/.leave_core"; +const char kShellPath[] = "/system/bin/sh"; +const char kSystemCrashPath[] = "/data/misc/crash_reporter/crash"; +const char kUploadVarPrefix[] = "upload_var_"; +const char kUploadFilePrefix[] = "upload_file_"; + +// Product information keys in the /etc/os-release.d folder. +static const char kBdkVersionKey[] = "bdk_version"; +static const char kProductIDKey[] = "product_id"; +static const char kProductVersionKey[] = "product_version"; + +// Normally this path is not used. Unfortunately, there are a few edge cases +// where we need this. Any process that runs as kDefaultUserName that crashes +// is consider a "user crash". That includes the initial Chrome browser that +// runs the login screen. If that blows up, there is no logged in user yet, +// so there is no per-user dir for us to stash things in. Instead we fallback +// to this path as it is at least encrypted on a per-system basis. +// +// This also comes up when running autotests. The GUI is sitting at the login +// screen while tests are sshing in, changing users, and triggering crashes as +// the user (purposefully). +const char kFallbackUserCrashPath[] = "/home/chronos/crash"; + +// Directory mode of the user crash spool directory. +const mode_t kUserCrashPathMode = 0755; + +// Directory mode of the system crash spool directory. +const mode_t kSystemCrashPathMode = 01755; + +const uid_t kRootOwner = 0; +const uid_t kRootGroup = 0; + +} // namespace + +// 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; + +using base::FilePath; +using base::StringPrintf; + +CrashCollector::CrashCollector() + : log_config_path_(kDefaultLogConfig) { +} + +CrashCollector::~CrashCollector() { +} + +void CrashCollector::Initialize( + CrashCollector::CountCrashFunction count_crash_function, + CrashCollector::IsFeedbackAllowedFunction is_feedback_allowed_function) { + CHECK(count_crash_function); + CHECK(is_feedback_allowed_function); + + count_crash_function_ = count_crash_function; + is_feedback_allowed_function_ = is_feedback_allowed_function; +} + +int CrashCollector::WriteNewFile(const FilePath &filename, + const char *data, + int size) { + int fd = HANDLE_EINTR(open(filename.value().c_str(), + O_CREAT | O_WRONLY | O_TRUNC | O_EXCL, 0666)); + if (fd < 0) { + return -1; + } + + int rv = base::WriteFileDescriptor(fd, data, size) ? size : -1; + IGNORE_EINTR(close(fd)); + return rv; +} + +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; +} + +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); + return StringPrintf("%s.%04d%02d%02d.%02d%02d%02d.%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, + 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())); +} + +FilePath CrashCollector::GetCrashDirectoryInfo( + mode_t *mode, + uid_t *directory_owner, + gid_t *directory_group) { + *mode = kSystemCrashPathMode; + *directory_owner = kRootOwner; + *directory_group = kRootGroup; + return FilePath(kSystemCrashPath); +} + +bool CrashCollector::GetUserInfoFromName(const std::string &name, + uid_t *uid, + gid_t *gid) { + char storage[256]; + struct passwd passwd_storage; + struct passwd *passwd_result = nullptr; + + if (getpwnam_r(name.c_str(), &passwd_storage, storage, sizeof(storage), + &passwd_result) != 0 || passwd_result == nullptr) { + LOG(ERROR) << "Cannot find user named " << name; + return false; + } + + *uid = passwd_result->pw_uid; + *gid = passwd_result->pw_gid; + return true; +} + +bool CrashCollector::GetCreatedCrashDirectoryByEuid(uid_t euid __unused, + FilePath *crash_directory, + bool *out_of_capacity) { + if (out_of_capacity) *out_of_capacity = false; + + // For testing. + if (!forced_crash_directory_.empty()) { + *crash_directory = forced_crash_directory_; + return true; + } + + mode_t directory_mode; + uid_t directory_owner; + gid_t directory_group; + *crash_directory = + GetCrashDirectoryInfo(&directory_mode, + &directory_owner, + &directory_group); + + if (!base::PathExists(*crash_directory)) { + // Create the spool directory with the appropriate mode (regardless of + // umask) and ownership. + mode_t old_mask = umask(0); + if (mkdir(crash_directory->value().c_str(), directory_mode) < 0 || + chown(crash_directory->value().c_str(), + directory_owner, + directory_group) < 0) { + LOG(ERROR) << "Unable to create appropriate crash directory"; + return false; + } + umask(old_mask); + } + + if (!base::PathExists(*crash_directory)) { + LOG(ERROR) << "Unable to create crash directory " + << crash_directory->value().c_str(); + return false; + } + + if (!CheckHasCapacity(*crash_directory)) { + if (out_of_capacity) *out_of_capacity = true; + LOG(ERROR) << "Directory " << crash_directory->value() + << " is out of capacity."; + return false; + } + + return true; +} + +FilePath CrashCollector::GetProcessPath(pid_t pid) { + return FilePath(StringPrintf("/proc/%d", pid)); +} + +bool CrashCollector::GetSymlinkTarget(const FilePath &symlink, + FilePath *target) { + ssize_t max_size = 64; + std::vector<char> buffer; + + while (true) { + buffer.resize(max_size + 1); + ssize_t size = readlink(symlink.value().c_str(), buffer.data(), max_size); + if (size < 0) { + int saved_errno = errno; + LOG(ERROR) << "Readlink failed on " << symlink.value() << " with " + << saved_errno; + return false; + } + + buffer[size] = 0; + if (size == max_size) { + max_size *= 2; + if (max_size > PATH_MAX) { + return false; + } + continue; + } + break; + } + + *target = FilePath(buffer.data()); + 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 (!GetSymlinkTarget(exe_path, &target)) { + LOG(INFO) << "GetSymlinkTarget 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) { + DIR* dir = opendir(crash_directory.value().c_str()); + if (!dir) { + LOG(WARNING) << "Unable to open crash directory " + << crash_directory.value(); + return false; + } + struct dirent ent_buf; + struct dirent* ent; + bool full = false; + std::set<std::string> basenames; + while (readdir_r(dir, &ent_buf, &ent) == 0 && ent) { + if ((strcmp(ent->d_name, ".") == 0) || + (strcmp(ent->d_name, "..") == 0)) + continue; + + std::string filename(ent->d_name); + size_t last_dot = filename.rfind("."); + std::string basename; + // If there is a valid looking extension, use the base part of the + // name. If the only dot is the first byte (aka a dot file), treat + // it as unique to avoid allowing a directory full of dot files + // from accumulating. + if (last_dot != std::string::npos && last_dot != 0) + basename = filename.substr(0, last_dot); + else + basename = filename; + basenames.insert(basename); + + if (basenames.size() >= static_cast<size_t>(kMaxCrashDirectorySize)) { + LOG(WARNING) << "Crash directory " << crash_directory.value() + << " already full with " << kMaxCrashDirectorySize + << " pending reports"; + full = true; + break; + } + } + closedir(dir); + return !full; +} + +bool CrashCollector::GetLogContents(const FilePath &config_path, + const std::string &exec_name, + const FilePath &output_file) { + brillo::KeyValueStore store; + if (!store.Load(config_path)) { + LOG(INFO) << "Unable to read log configuration file " + << config_path.value(); + return false; + } + + std::string command; + if (!store.GetString(exec_name, &command)) + return false; + + brillo::ProcessImpl diag_process; + diag_process.AddArg(kShellPath); + diag_process.AddStringOption("-c", command); + diag_process.RedirectOutput(output_file.value()); + + const int result = diag_process.Run(); + if (result != 0) { + LOG(INFO) << "Log command \"" << command << "\" exited with " << result; + return false; + } + return true; +} + +void CrashCollector::AddCrashMetaData(const std::string &key, + const std::string &value) { + extra_metadata_.append(StringPrintf("%s=%s\n", key.c_str(), value.c_str())); +} + +void CrashCollector::AddCrashMetaUploadFile(const std::string &key, + const std::string &path) { + if (!path.empty()) + AddCrashMetaData(kUploadFilePrefix + key, path); +} + +void CrashCollector::AddCrashMetaUploadData(const std::string &key, + const std::string &value) { + if (!value.empty()) + AddCrashMetaData(kUploadVarPrefix + key, value); +} + +void CrashCollector::WriteCrashMetaData(const FilePath &meta_path, + const std::string &exec_name, + const std::string &payload_path) { + int64_t payload_size = -1; + base::GetFileSize(FilePath(payload_path), &payload_size); + + brillo::OsReleaseReader reader; + if (!forced_osreleased_directory_.empty()) { + reader.LoadTestingOnly(forced_osreleased_directory_); + } else { + reader.Load(); + } + std::string bdk_version = "undefined"; + std::string product_id = "undefined"; + std::string product_version = "undefined"; + + if (!reader.GetString(kBdkVersionKey, &bdk_version)) { + LOG(ERROR) << "Could not read " << kBdkVersionKey + << " from /etc/os-release.d/"; + } + + if (!reader.GetString(kProductIDKey, &product_id)) { + LOG(ERROR) << "Could not read " << kProductIDKey + << " from /etc/os-release.d/"; + } + + if (!reader.GetString(kProductVersionKey, &product_version)) { + LOG(ERROR) << "Could not read " << kProductVersionKey + << " from /etc/os-release.d/"; + } + + std::string meta_data = StringPrintf("%sexec_name=%s\n" + "payload=%s\n" + "payload_size=%" PRId64 "\n" + "%s=%s\n" + "%s=%s\n" + "%s=%s\n" + "done=1\n", + extra_metadata_.c_str(), + exec_name.c_str(), + payload_path.c_str(), + payload_size, + kBdkVersionKey, + bdk_version.c_str(), + kProductIDKey, + product_id.c_str(), + kProductVersionKey, + product_version.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.c_str(), meta_data.size()) < 0) { + LOG(ERROR) << "Unable to write " << meta_path.value(); + } +} + +bool CrashCollector::IsCrashTestInProgress() { + return base::PathExists(FilePath(kCrashTestInProgressPath)); +} + +bool CrashCollector::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(FilePath(kLeaveCoreFile)); +} diff --git a/crash_collector.h b/crash_collector.h new file mode 100644 index 0000000..21b9198 --- /dev/null +++ b/crash_collector.h @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_CRASH_COLLECTOR_H_ +#define CRASH_REPORTER_CRASH_COLLECTOR_H_ + +#include <sys/stat.h> + +#include <map> +#include <string> + +#include <base/files/file_path.h> +#include <base/macros.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +// User crash collector. +class CrashCollector { + public: + typedef void (*CountCrashFunction)(); + typedef bool (*IsFeedbackAllowedFunction)(); + + CrashCollector(); + + virtual ~CrashCollector(); + + // Initialize the crash collector for detection of crashes, given a + // crash counting function, and metrics collection enabled oracle. + void Initialize(CountCrashFunction count_crash, + IsFeedbackAllowedFunction is_metrics_allowed); + + protected: + friend class CrashCollectorTest; + FRIEND_TEST(ChromeCollectorTest, HandleCrash); + FRIEND_TEST(CrashCollectorTest, CheckHasCapacityCorrectBasename); + FRIEND_TEST(CrashCollectorTest, CheckHasCapacityStrangeNames); + FRIEND_TEST(CrashCollectorTest, CheckHasCapacityUsual); + FRIEND_TEST(CrashCollectorTest, GetCrashDirectoryInfo); + FRIEND_TEST(CrashCollectorTest, GetCrashPath); + FRIEND_TEST(CrashCollectorTest, GetLogContents); + FRIEND_TEST(CrashCollectorTest, ForkExecAndPipe); + FRIEND_TEST(CrashCollectorTest, FormatDumpBasename); + FRIEND_TEST(CrashCollectorTest, Initialize); + FRIEND_TEST(CrashCollectorTest, MetaData); + FRIEND_TEST(CrashCollectorTest, Sanitize); + FRIEND_TEST(CrashCollectorTest, WriteNewFile); + FRIEND_TEST(ForkExecAndPipeTest, Basic); + FRIEND_TEST(ForkExecAndPipeTest, NonZeroReturnValue); + FRIEND_TEST(ForkExecAndPipeTest, BadOutputFile); + FRIEND_TEST(ForkExecAndPipeTest, ExistingOutputFile); + FRIEND_TEST(ForkExecAndPipeTest, BadExecutable); + FRIEND_TEST(ForkExecAndPipeTest, StderrCaptured); + FRIEND_TEST(ForkExecAndPipeTest, NULLParam); + FRIEND_TEST(ForkExecAndPipeTest, NoParams); + FRIEND_TEST(ForkExecAndPipeTest, SegFaultHandling); + + // Set maximum enqueued crashes in a crash directory. + static const int kMaxCrashDirectorySize; + + // Writes |data| of |size| to |filename|, which must be a new file. + // If the file already exists or writing fails, return a negative value. + // Otherwise returns the number of bytes written. + int WriteNewFile(const base::FilePath &filename, const char *data, int size); + + // Return a filename that has only [a-z0-1_] characters by mapping + // all others into '_'. + std::string Sanitize(const std::string &name); + + // For testing, set the directory always returned by + // GetCreatedCrashDirectoryByEuid. + void ForceCrashDirectory(const base::FilePath &forced_directory) { + forced_crash_directory_ = forced_directory; + } + + // For testing, set the root directory to read etc/os-release.d properties + // from. + void ForceOsReleaseDDirectory(const base::FilePath &forced_directory) { + forced_osreleased_directory_ = forced_directory; + } + + base::FilePath GetCrashDirectoryInfo(mode_t *mode, + uid_t *directory_owner, + gid_t *directory_group); + bool GetUserInfoFromName(const std::string &name, + uid_t *uid, + gid_t *gid); + + // Determines the crash directory for given euid, and creates the + // directory if necessary with appropriate permissions. If + // |out_of_capacity| is not nullptr, it is set to indicate if the call + // failed due to not having capacity in the crash directory. Returns + // true whether or not directory needed to be created, false on any + // failure. If the crash directory is at capacity, returns false. + bool GetCreatedCrashDirectoryByEuid(uid_t euid, + base::FilePath *crash_file_path, + bool *out_of_capacity); + + // Format crash name based on components. + std::string FormatDumpBasename(const std::string &exec_name, + time_t timestamp, + pid_t pid); + + // Create a file path to a file in |crash_directory| with the given + // |basename| and |extension|. + base::FilePath GetCrashPath(const base::FilePath &crash_directory, + const std::string &basename, + const std::string &extension); + + base::FilePath GetProcessPath(pid_t pid); + bool GetSymlinkTarget(const base::FilePath &symlink, + base::FilePath *target); + bool GetExecutableBaseNameFromPid(pid_t pid, + std::string *base_name); + + // Check given crash directory still has remaining capacity for another + // crash. + bool CheckHasCapacity(const base::FilePath &crash_directory); + + // Write a log applicable to |exec_name| to |output_file| based on the + // log configuration file at |config_path|. + bool GetLogContents(const base::FilePath &config_path, + const std::string &exec_name, + const base::FilePath &output_file); + + // Add non-standard meta data to the crash metadata file. Call + // before calling WriteCrashMetaData. Key must not contain "=" or + // "\n" characters. Value must not contain "\n" characters. + void AddCrashMetaData(const std::string &key, const std::string &value); + + // Add a file to be uploaded to the crash reporter server. The file must + // persist until the crash report is sent; ideally it should live in the same + // place as the .meta file, so it can be cleaned up automatically. + void AddCrashMetaUploadFile(const std::string &key, const std::string &path); + + // Add non-standard meta data to the crash metadata file. + // Data added though this call will be uploaded to the crash reporter server, + // appearing as a form field. + void AddCrashMetaUploadData(const std::string &key, const std::string &value); + + // Write a file of metadata about crash. + void WriteCrashMetaData(const base::FilePath &meta_path, + const std::string &exec_name, + const std::string &payload_path); + + // Returns true if the a crash test is currently running. + bool IsCrashTestInProgress(); + // Returns true if we should consider ourselves to be running on a + // developer image. + bool IsDeveloperImage(); + + CountCrashFunction count_crash_function_; + IsFeedbackAllowedFunction is_feedback_allowed_function_; + std::string extra_metadata_; + base::FilePath forced_crash_directory_; + base::FilePath forced_osreleased_directory_; + base::FilePath log_config_path_; + + private: + DISALLOW_COPY_AND_ASSIGN(CrashCollector); +}; + +#endif // CRASH_REPORTER_CRASH_COLLECTOR_H_ diff --git a/crash_collector_test.cc b/crash_collector_test.cc new file mode 100644 index 0000000..a386cd1 --- /dev/null +++ b/crash_collector_test.cc @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "crash_collector_test.h" + +#include <unistd.h> +#include <utility> + +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <brillo/syslog_logging.h> +#include <gtest/gtest.h> + +#include "crash_collector.h" + +using base::FilePath; +using base::StringPrintf; +using brillo::FindLog; +using ::testing::Invoke; +using ::testing::Return; + +namespace { + +void CountCrash() { + ADD_FAILURE(); +} + +bool IsMetrics() { + ADD_FAILURE(); + return false; +} + +} // namespace + +class CrashCollectorTest : public ::testing::Test { + public: + void SetUp() { + EXPECT_CALL(collector_, SetUpDBus()).WillRepeatedly(Return()); + + collector_.Initialize(CountCrash, IsMetrics); + EXPECT_TRUE(test_dir_.CreateUniqueTempDir()); + brillo::ClearLog(); + } + + bool CheckHasCapacity(); + + protected: + CrashCollectorMock collector_; + + // Temporary directory used for tests. + base::ScopedTempDir test_dir_; +}; + +TEST_F(CrashCollectorTest, Initialize) { + ASSERT_TRUE(CountCrash == collector_.count_crash_function_); + ASSERT_TRUE(IsMetrics == collector_.is_feedback_allowed_function_); +} + +TEST_F(CrashCollectorTest, WriteNewFile) { + FilePath test_file = test_dir_.path().Append("test_new"); + const char kBuffer[] = "buffer"; + unsigned int numBytesWritten = collector_.WriteNewFile( + test_file, + kBuffer, + strlen(kBuffer)); + EXPECT_EQ(strlen(kBuffer), numBytesWritten); + EXPECT_LT(collector_.WriteNewFile(test_file, + kBuffer, + strlen(kBuffer)), 0); +} + +TEST_F(CrashCollectorTest, Sanitize) { + EXPECT_EQ("chrome", collector_.Sanitize("chrome")); + EXPECT_EQ("CHROME", collector_.Sanitize("CHROME")); + EXPECT_EQ("1chrome2", collector_.Sanitize("1chrome2")); + EXPECT_EQ("chrome__deleted_", collector_.Sanitize("chrome (deleted)")); + EXPECT_EQ("foo_bar", collector_.Sanitize("foo.bar")); + EXPECT_EQ("", collector_.Sanitize("")); + EXPECT_EQ("_", collector_.Sanitize(" ")); +} + +TEST_F(CrashCollectorTest, FormatDumpBasename) { + struct tm tm = {}; + tm.tm_sec = 15; + tm.tm_min = 50; + tm.tm_hour = 13; + tm.tm_mday = 23; + tm.tm_mon = 4; + tm.tm_year = 110; + tm.tm_isdst = -1; + std::string basename = + collector_.FormatDumpBasename("foo", mktime(&tm), 100); + ASSERT_EQ("foo.20100523.135015.100", basename); +} + +TEST_F(CrashCollectorTest, GetCrashPath) { + EXPECT_EQ("/var/spool/crash/myprog.20100101.1200.1234.core", + collector_.GetCrashPath(FilePath("/var/spool/crash"), + "myprog.20100101.1200.1234", + "core").value()); + EXPECT_EQ("/home/chronos/user/crash/chrome.20100101.1200.1234.dmp", + collector_.GetCrashPath(FilePath("/home/chronos/user/crash"), + "chrome.20100101.1200.1234", + "dmp").value()); +} + + +bool CrashCollectorTest::CheckHasCapacity() { + const char* kFullMessage = + StringPrintf("Crash directory %s already full", + test_dir_.path().value().c_str()).c_str(); + bool has_capacity = collector_.CheckHasCapacity(test_dir_.path()); + bool has_message = FindLog(kFullMessage); + EXPECT_EQ(has_message, !has_capacity); + return has_capacity; +} + +TEST_F(CrashCollectorTest, CheckHasCapacityUsual) { + // Test kMaxCrashDirectorySize - 1 non-meta files can be added. + for (int i = 0; i < CrashCollector::kMaxCrashDirectorySize - 1; ++i) { + base::WriteFile(test_dir_.path().Append(StringPrintf("file%d.core", i)), + "", 0); + EXPECT_TRUE(CheckHasCapacity()); + } + + // Test an additional kMaxCrashDirectorySize - 1 meta files fit. + for (int i = 0; i < CrashCollector::kMaxCrashDirectorySize - 1; ++i) { + base::WriteFile(test_dir_.path().Append(StringPrintf("file%d.meta", i)), + "", 0); + EXPECT_TRUE(CheckHasCapacity()); + } + + // Test an additional kMaxCrashDirectorySize meta files don't fit. + for (int i = 0; i < CrashCollector::kMaxCrashDirectorySize; ++i) { + base::WriteFile(test_dir_.path().Append(StringPrintf("overage%d.meta", i)), + "", 0); + EXPECT_FALSE(CheckHasCapacity()); + } +} + +TEST_F(CrashCollectorTest, CheckHasCapacityCorrectBasename) { + // Test kMaxCrashDirectorySize - 1 files can be added. + for (int i = 0; i < CrashCollector::kMaxCrashDirectorySize - 1; ++i) { + base::WriteFile(test_dir_.path().Append(StringPrintf("file.%d.core", i)), + "", 0); + EXPECT_TRUE(CheckHasCapacity()); + } + base::WriteFile(test_dir_.path().Append("file.last.core"), "", 0); + EXPECT_FALSE(CheckHasCapacity()); +} + +TEST_F(CrashCollectorTest, CheckHasCapacityStrangeNames) { + // Test many files with different extensions and same base fit. + for (int i = 0; i < 5 * CrashCollector::kMaxCrashDirectorySize; ++i) { + base::WriteFile(test_dir_.path().Append(StringPrintf("a.%d", i)), "", 0); + EXPECT_TRUE(CheckHasCapacity()); + } + // Test dot files are treated as individual files. + for (int i = 0; i < CrashCollector::kMaxCrashDirectorySize - 2; ++i) { + base::WriteFile(test_dir_.path().Append(StringPrintf(".file%d", i)), "", 0); + EXPECT_TRUE(CheckHasCapacity()); + } + base::WriteFile(test_dir_.path().Append("normal.meta"), "", 0); + EXPECT_FALSE(CheckHasCapacity()); +} + +TEST_F(CrashCollectorTest, MetaData) { + const char kMetaFileBasename[] = "generated.meta"; + FilePath meta_file = test_dir_.path().Append(kMetaFileBasename); + FilePath payload_file = test_dir_.path().Append("payload-file"); + FilePath osreleased_directory = + test_dir_.path().Append("etc").Append("os-release.d"); + ASSERT_TRUE(base::CreateDirectory(osreleased_directory)); + collector_.ForceOsReleaseDDirectory(test_dir_.path()); + + std::string contents; + const char kPayload[] = "foo"; + ASSERT_TRUE(base::WriteFile(payload_file, kPayload, strlen(kPayload))); + const char kBdkVersion[] = "1"; + ASSERT_TRUE(base::WriteFile(osreleased_directory.Append("bdk_version"), + kBdkVersion, + strlen(kBdkVersion))); + const char kProductId[] = "baz"; + ASSERT_TRUE(base::WriteFile(osreleased_directory.Append("product_id"), + kProductId, + strlen(kProductId))); + const char kProductVersion[] = "1.2.3.4"; + ASSERT_TRUE(base::WriteFile(osreleased_directory.Append("product_version"), + kProductVersion, + strlen(kProductVersion))); + collector_.AddCrashMetaData("foo", "bar"); + collector_.WriteCrashMetaData(meta_file, "kernel", payload_file.value()); + EXPECT_TRUE(base::ReadFileToString(meta_file, &contents)); + const std::string kExpectedMeta = + StringPrintf("foo=bar\n" + "exec_name=kernel\n" + "payload=%s\n" + "payload_size=3\n" + "bdk_version=1\n" + "product_id=baz\n" + "product_version=1.2.3.4\n" + "done=1\n", + test_dir_.path().Append("payload-file").value().c_str()); + EXPECT_EQ(kExpectedMeta, contents); + + // Test target of symlink is not overwritten. + payload_file = test_dir_.path().Append("payload2-file"); + ASSERT_TRUE(base::WriteFile(payload_file, kPayload, strlen(kPayload))); + FilePath meta_symlink_path = test_dir_.path().Append("symlink.meta"); + ASSERT_EQ(0, + symlink(kMetaFileBasename, + meta_symlink_path.value().c_str())); + ASSERT_TRUE(base::PathExists(meta_symlink_path)); + brillo::ClearLog(); + collector_.WriteCrashMetaData(meta_symlink_path, + "kernel", + payload_file.value()); + // Target metadata contents should have stayed the same. + contents.clear(); + EXPECT_TRUE(base::ReadFileToString(meta_file, &contents)); + EXPECT_EQ(kExpectedMeta, contents); + EXPECT_TRUE(FindLog("Unable to write")); + + // Test target of dangling symlink is not created. + base::DeleteFile(meta_file, false); + ASSERT_FALSE(base::PathExists(meta_file)); + brillo::ClearLog(); + collector_.WriteCrashMetaData(meta_symlink_path, "kernel", + payload_file.value()); + EXPECT_FALSE(base::PathExists(meta_file)); + EXPECT_TRUE(FindLog("Unable to write")); +} + +TEST_F(CrashCollectorTest, GetLogContents) { + FilePath config_file = test_dir_.path().Append("crash_config"); + FilePath output_file = test_dir_.path().Append("crash_log"); + const char kConfigContents[] = + "foobar=echo hello there | \\\n sed -e \"s/there/world/\""; + ASSERT_TRUE( + base::WriteFile(config_file, kConfigContents, strlen(kConfigContents))); + base::DeleteFile(FilePath(output_file), false); + EXPECT_FALSE(collector_.GetLogContents(config_file, + "barfoo", + output_file)); + EXPECT_FALSE(base::PathExists(output_file)); + base::DeleteFile(FilePath(output_file), false); + EXPECT_TRUE(collector_.GetLogContents(config_file, + "foobar", + output_file)); + ASSERT_TRUE(base::PathExists(output_file)); + std::string contents; + EXPECT_TRUE(base::ReadFileToString(output_file, &contents)); + EXPECT_EQ("hello world\n", contents); +} diff --git a/crash_collector_test.h b/crash_collector_test.h new file mode 100644 index 0000000..cfbb97b --- /dev/null +++ b/crash_collector_test.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_CRASH_COLLECTOR_TEST_H_ +#define CRASH_REPORTER_CRASH_COLLECTOR_TEST_H_ + +#include "crash_collector.h" + +#include <map> +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +class CrashCollectorMock : public CrashCollector { + public: + MOCK_METHOD0(SetUpDBus, void()); + MOCK_METHOD1(GetActiveUserSessions, + bool(std::map<std::string, std::string> *sessions)); +}; + +#endif // CRASH_REPORTER_CRASH_COLLECTOR_TEST_H_ diff --git a/crash_reporter.cc b/crash_reporter.cc new file mode 100644 index 0000000..16e70d8 --- /dev/null +++ b/crash_reporter.cc @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <fcntl.h> // for open + +#include <string> +#include <vector> + +#include <base/files/file_util.h> +#include <base/guid.h> +#include <base/logging.h> +#include <base/strings/string_split.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <binder/IServiceManager.h> +#include <brillo/flag_helper.h> +#include <brillo/syslog_logging.h> +#include <metrics/metrics_collector_service_client.h> +#include <metrics/metrics_library.h> +#include <utils/String16.h> + + +#include "kernel_collector.h" +#include "kernel_warning_collector.h" +#include "unclean_shutdown_collector.h" +#include "user_collector.h" + +#if !defined(__ANDROID__) +#include "udev_collector.h" +#endif + +static const char kCrashCounterHistogram[] = "Logging.CrashCounter"; +static const char kKernelCrashDetected[] = + "/data/misc/crash_reporter/run/kernel-crash-detected"; +static const char kUncleanShutdownDetected[] = + "/var/run/unclean-shutdown-detected"; +static const char kGUIDFileName[] = "/data/misc/crash_reporter/guid"; + +// Enumeration of kinds of crashes to be used in the CrashCounter histogram. +enum CrashKinds { + kCrashKindUncleanShutdown = 1, + kCrashKindUser = 2, + kCrashKindKernel = 3, + kCrashKindUdev = 4, + kCrashKindKernelWarning = 5, + kCrashKindMax +}; + +static MetricsLibrary s_metrics_lib; + +using android::brillo::metrics::IMetricsCollectorService; +using base::FilePath; +using base::StringPrintf; + +static bool IsFeedbackAllowed() { + return s_metrics_lib.AreMetricsEnabled(); +} + +static bool TouchFile(const FilePath &file_path) { + return base::WriteFile(file_path, "", 0) == 0; +} + +static void SendCrashMetrics(CrashKinds type, const char* name) { + // TODO(kmixter): We can remove this histogram as part of + // crosbug.com/11163. + s_metrics_lib.SendEnumToUMA(kCrashCounterHistogram, type, kCrashKindMax); + s_metrics_lib.SendCrashToUMA(name); +} + +static void CountKernelCrash() { + SendCrashMetrics(kCrashKindKernel, "kernel"); +} + +static void CountUdevCrash() { + SendCrashMetrics(kCrashKindUdev, "udevcrash"); +} + +static void CountUncleanShutdown() { + SendCrashMetrics(kCrashKindUncleanShutdown, "uncleanshutdown"); +} + +static void CountUserCrash() { + SendCrashMetrics(kCrashKindUser, "user"); + // Tell the metrics collector about the user crash, in order to log active + // use time between crashes. + MetricsCollectorServiceClient metrics_collector_service; + + if (metrics_collector_service.Init()) + metrics_collector_service.notifyUserCrash(); + else + LOG(ERROR) << "Failed to send user crash notification to metrics_collector"; +} + + +static int Initialize(KernelCollector *kernel_collector, + UserCollector *user_collector, + UncleanShutdownCollector *unclean_shutdown_collector, + const bool unclean_check, + const bool clean_shutdown) { + CHECK(!clean_shutdown) << "Incompatible options"; + + // Try to read the GUID from kGUIDFileName. If the file doesn't exist, is + // blank, or the read fails, generate a new GUID and write it to the file. + std::string guid; + base::FilePath filepath(kGUIDFileName); + if (!base::ReadFileToString(filepath, &guid) || guid.empty()) { + guid = base::GenerateGUID(); + // If we can't read or write the file, log an error. However it is not + // a fatal error, as the crash server will assign a random GUID based + // on a hash of the IP address if one is not provided in the report. + if (base::WriteFile(filepath, guid.c_str(), guid.size()) <= 0) { + LOG(ERROR) << "Could not write guid " << guid << " to file " + << filepath.value(); + } + } + + bool was_kernel_crash = false; + bool was_unclean_shutdown = false; + kernel_collector->Enable(); + if (kernel_collector->is_enabled()) { + was_kernel_crash = kernel_collector->Collect(); + } + + if (unclean_check) { + was_unclean_shutdown = unclean_shutdown_collector->Collect(); + } + + // Touch a file to notify the metrics daemon that a kernel + // crash has been detected so that it can log the time since + // the last kernel crash. + if (IsFeedbackAllowed()) { + if (was_kernel_crash) { + TouchFile(FilePath(kKernelCrashDetected)); + } else if (was_unclean_shutdown) { + // We only count an unclean shutdown if it did not come with + // an associated kernel crash. + TouchFile(FilePath(kUncleanShutdownDetected)); + } + } + + // Must enable the unclean shutdown collector *after* collecting. + unclean_shutdown_collector->Enable(); + user_collector->Enable(); + + return 0; +} + +static int HandleUserCrash(UserCollector *user_collector, + const std::string& user, const bool crash_test) { + // Handle a specific user space crash. + CHECK(!user.empty()) << "--user= must be set"; + + // Make it possible to test what happens when we crash while + // handling a crash. + if (crash_test) { + *(volatile char *)0 = 0; + return 0; + } + + // Accumulate logs to help in diagnosing failures during user collection. + brillo::LogToString(true); + // Handle the crash, get the name of the process from procfs. + bool handled = user_collector->HandleCrash(user, nullptr); + brillo::LogToString(false); + if (!handled) + return 1; + return 0; +} + +#if !defined(__ANDROID__) +static int HandleUdevCrash(UdevCollector *udev_collector, + const std::string& udev_event) { + // Handle a crash indicated by a udev event. + CHECK(!udev_event.empty()) << "--udev= must be set"; + + // Accumulate logs to help in diagnosing failures during user collection. + brillo::LogToString(true); + bool handled = udev_collector->HandleCrash(udev_event); + brillo::LogToString(false); + if (!handled) + return 1; + return 0; +} +#endif + +static int HandleKernelWarning(KernelWarningCollector + *kernel_warning_collector) { + // Accumulate logs to help in diagnosing failures during collection. + brillo::LogToString(true); + bool handled = kernel_warning_collector->Collect(); + brillo::LogToString(false); + if (!handled) + return 1; + return 0; +} + +// Interactive/diagnostics mode for generating kernel crash signatures. +static int GenerateKernelSignature(KernelCollector *kernel_collector, + const std::string& kernel_signature_file) { + std::string kcrash_contents; + std::string signature; + if (!base::ReadFileToString(FilePath(kernel_signature_file), + &kcrash_contents)) { + fprintf(stderr, "Could not read file.\n"); + return 1; + } + if (!kernel_collector->ComputeKernelStackSignature( + kcrash_contents, + &signature, + true)) { + fprintf(stderr, "Signature could not be generated.\n"); + return 1; + } + printf("Kernel crash signature is \"%s\".\n", signature.c_str()); + return 0; +} + +// Ensure stdout, stdin, and stderr are open file descriptors. If +// they are not, any code which writes to stderr/stdout may write out +// to files opened during execution. In particular, when +// crash_reporter is run by the kernel coredump pipe handler (via +// kthread_create/kernel_execve), it will not have file table entries +// 1 and 2 (stdout and stderr) populated. We populate them here. +static void OpenStandardFileDescriptors() { + int new_fd = -1; + // We open /dev/null to fill in any of the standard [0, 2] file + // descriptors. We leave these open for the duration of the + // process. This works because open returns the lowest numbered + // invalid fd. + do { + new_fd = open("/dev/null", 0); + CHECK_GE(new_fd, 0) << "Unable to open /dev/null"; + } while (new_fd >= 0 && new_fd <= 2); + close(new_fd); +} + +int main(int argc, char *argv[]) { + DEFINE_bool(init, false, "Initialize crash logging"); + DEFINE_bool(clean_shutdown, false, "Signal clean shutdown"); + DEFINE_string(generate_kernel_signature, "", + "Generate signature from given kcrash file"); + DEFINE_bool(crash_test, false, "Crash test"); + DEFINE_string(user, "", "User crash info (pid:signal:exec_name)"); + DEFINE_bool(unclean_check, true, "Check for unclean shutdown"); + +#if !defined(__ANDROID__) + DEFINE_string(udev, "", "Udev event description (type:device:subsystem)"); +#endif + + DEFINE_bool(kernel_warning, false, "Report collected kernel warning"); + DEFINE_string(pid, "", "PID of crashing process"); + DEFINE_string(uid, "", "UID of crashing process"); + DEFINE_string(exe, "", "Executable name of crashing process"); + DEFINE_bool(core2md_failure, false, "Core2md failure test"); + DEFINE_bool(directory_failure, false, "Spool directory failure test"); + DEFINE_string(filter_in, "", + "Ignore all crashes but this for testing"); + + OpenStandardFileDescriptors(); + FilePath my_path = base::MakeAbsoluteFilePath(FilePath(argv[0])); + s_metrics_lib.Init(); + brillo::FlagHelper::Init(argc, argv, "Chromium OS Crash Reporter"); + brillo::OpenLog(my_path.BaseName().value().c_str(), true); + brillo::InitLog(brillo::kLogToSyslog); + + KernelCollector kernel_collector; + kernel_collector.Initialize(CountKernelCrash, IsFeedbackAllowed); + UserCollector user_collector; + user_collector.Initialize(CountUserCrash, + my_path.value(), + IsFeedbackAllowed, + true, // generate_diagnostics + FLAGS_core2md_failure, + FLAGS_directory_failure, + FLAGS_filter_in); + UncleanShutdownCollector unclean_shutdown_collector; + unclean_shutdown_collector.Initialize(CountUncleanShutdown, + IsFeedbackAllowed); + +#if !defined(__ANDROID__) + UdevCollector udev_collector; + udev_collector.Initialize(CountUdevCrash, IsFeedbackAllowed); +#endif + + KernelWarningCollector kernel_warning_collector; + kernel_warning_collector.Initialize(CountUdevCrash, IsFeedbackAllowed); + + if (FLAGS_init) { + return Initialize(&kernel_collector, + &user_collector, + &unclean_shutdown_collector, + FLAGS_unclean_check, + FLAGS_clean_shutdown); + } + + if (FLAGS_clean_shutdown) { + unclean_shutdown_collector.Disable(); + user_collector.Disable(); + return 0; + } + + if (!FLAGS_generate_kernel_signature.empty()) { + return GenerateKernelSignature(&kernel_collector, + FLAGS_generate_kernel_signature); + } + +#if !defined(__ANDROID__) + if (!FLAGS_udev.empty()) { + return HandleUdevCrash(&udev_collector, FLAGS_udev); + } +#endif + + if (FLAGS_kernel_warning) { + return HandleKernelWarning(&kernel_warning_collector); + } + + return HandleUserCrash(&user_collector, FLAGS_user, FLAGS_crash_test); +} diff --git a/crash_reporter.rc b/crash_reporter.rc new file mode 100644 index 0000000..e6d1ec5 --- /dev/null +++ b/crash_reporter.rc @@ -0,0 +1,37 @@ +on property:crash_reporter.coredump.enabled=1 + write /proc/sys/kernel/core_pattern \ + "|/system/bin/crash_reporter --user=%P:%s:%u:%g:%e" + +on property:crash_reporter.coredump.enabled=0 + write /proc/sys/kernel/core_pattern "core" + +on post-fs-data + # Allow catching multiple unrelated concurrent crashes, but use a finite + # number to prevent infinitely recursing on crash handling. + write /proc/sys/kernel/core_pipe_limit 4 + + # Remove any previous orphaned locks. + rmdir /data/misc/crash_reporter/lock/crash_sender + + # Remove any previous run files. + rm /data/misc/crash_reporter/run/kernel-crash-detected + rmdir /data/misc/crash_reporter/run + + # Create crash directories. + # These directories are group-writable by root so that crash_reporter can + # still access them when it switches users. + mkdir /data/misc/crash_reporter 0770 root root + mkdir /data/misc/crash_reporter/crash 0770 root root + mkdir /data/misc/crash_reporter/lock 0700 root root + mkdir /data/misc/crash_reporter/log 0700 root root + mkdir /data/misc/crash_reporter/run 0700 root root + mkdir /data/misc/crash_reporter/tmp 0770 root root + +service crash_reporter /system/bin/crash_reporter --init + class late_start + oneshot + +service crash_sender /system/bin/periodic_scheduler 3600 14400 crash_sender \ + /system/bin/crash_sender + class late_start + group system diff --git a/crash_reporter_logs.conf b/crash_reporter_logs.conf new file mode 100644 index 0000000..7db308c --- /dev/null +++ b/crash_reporter_logs.conf @@ -0,0 +1,122 @@ +# Copyright (C) 2012 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is parsed by chromeos::KeyValueStore. It has the format: +# +# <basename>=<shell command>\n +# +# Commands may be split across multiple lines using trailing backslashes. +# +# When an executable named <basename> crashes, the corresponding command is +# executed and its standard output and standard error are attached to the crash +# report. +# +# Use caution in modifying this file. Only run common Unix commands here, as +# these commands will be run when a crash has recently occurred and we should +# avoid running anything that might cause another crash. Similarly, these +# commands block notification of the crash to parent processes, so commands +# should execute quickly. + +update_engine=cat $(ls -1tr /var/log/update_engine | tail -5 | \ + sed s.^./var/log/update_engine/.) | tail -c 50000 + +# The cros_installer output is logged into the update engine log file, +# so it is handled in the same way as update_engine. +cros_installer=cat $(ls -1tr /var/log/update_engine | tail -5 | \ + sed s.^./var/log/update_engine/.) | tail -c 50000 + +# Dump the last 20 lines of the last two files in Chrome's system and user log +# directories, along with the last 20 messages from the session manager. +chrome=\ + for f in $(ls -1rt /var/log/chrome/chrome_[0-9]* | tail -2) \ + $(ls -1rt /home/chronos/u-*/log/chrome_[0-9]* 2>/dev/null | tail -2); do \ + echo "===$f (tail)==="; \ + tail -20 $f; \ + echo EOF; \ + echo; \ + done; \ + echo "===session_manager (tail)==="; \ + awk '$3 ~ "^session_manager\[" { print }' /var/log/messages | tail -20; \ + echo EOF + +# The following rule is used for generating additional diagnostics when +# collection of user crashes fails. This output should not be too large +# as it is stored in memory. The output format specified for 'ps' is the +# same as with the "u" ("user-oriented") option, except it doesn't show +# the commands' arguments (i.e. "comm" instead of "command"). +crash_reporter-user-collection=\ + echo "===ps output==="; \ + ps axw -o user,pid,%cpu,%mem,vsz,rss,tname,stat,start_time,bsdtime,comm | \ + tail -c 25000; \ + echo "===meminfo==="; \ + cat /proc/meminfo + +# This rule is similar to the crash_reporter-user-collection rule, except it is +# run for kernel errors reported through udev events. +crash_reporter-udev-collection-change-card0-drm=\ + for dri in /sys/kernel/debug/dri/*; do \ + echo "===$dri/i915_error_state==="; \ + cat $dri/i915_error_state; \ + done + +# When trackpad driver cyapa detects some abnormal behavior, we collect +# additional logs from kernel messages. +crash_reporter-udev-collection-change--i2c-cyapa=\ + /usr/sbin/kernel_log_collector.sh cyapa 30 +# When trackpad/touchscreen driver atmel_mxt_ts detects some abnormal behavior, +# we collect additional logs from kernel messages. +crash_reporter-udev-collection-change--i2c-atmel_mxt_ts=\ + /usr/sbin/kernel_log_collector.sh atmel 30 +# When touch device noise are detected, we collect relevant logs. +# (crosbug.com/p/16788) +crash_reporter-udev-collection---TouchNoise=cat /var/log/touch_noise.log +# Periodically collect touch event log for debugging (crosbug.com/p/17244) +crash_reporter-udev-collection---TouchEvent=cat /var/log/touch_event.log + +# Collect the last 50 lines of /var/log/messages and /var/log/net.log for +# intel wifi driver (iwlwifi) for debugging purpose. +crash_reporter-udev-collection-devcoredump-iwlwifi=\ + echo "===/var/log/messages==="; \ + tail -n 50 /var/log/messages; \ + echo "===/var/log/net.log==="; \ + tail -n 50 /var/log/net.log; \ + echo EOF + +# Dump the last 50 lines of the last two powerd log files -- if the job has +# already restarted, we want to see the end of the previous instance's logs. +powerd=\ + for f in $(ls -1tr /var/log/power_manager/powerd.[0-9]* | tail -2); do \ + echo "===$(basename $f) (tail)==="; \ + tail -50 $f; \ + echo EOF; \ + done +# If power_supply_info aborts (due to e.g. a bad battery), its failure message +# could end up in various places depending on which process was running it. +# Attach the end of powerd's log since it might've also logged the underlying +# problem. +power_supply_info=\ + echo "===powerd.LATEST (tail)==="; \ + tail -50 /var/log/power_manager/powerd.LATEST; \ + echo EOF +# powerd_setuid_helper gets run by powerd, so its stdout/stderr will be mixed in +# with powerd's stdout/stderr. +powerd_setuid_helper=\ + echo "===powerd.OUT (tail)==="; \ + tail -50 /var/log/powerd.out; \ + echo EOF + +# The following rules are only for testing purposes. +crash_log_test=echo hello world +crash_log_recursion_test=sleep 1 && \ + /usr/local/autotest/tests/crash_log_recursion_test diff --git a/crash_reporter_logs_test.cc b/crash_reporter_logs_test.cc new file mode 100644 index 0000000..77f5a7f --- /dev/null +++ b/crash_reporter_logs_test.cc @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> + +#include <base/files/file_path.h> +#include <brillo/key_value_store.h> +#include <gtest/gtest.h> + +namespace { + +// Name of the checked-in configuration file containing log-collection commands. +const char kConfigFile[] = "/system/etc/crash_reporter_logs.conf"; + +// Signature name for crash_reporter user collection. +// kConfigFile is expected to contain this entry. +const char kUserCollectorSignature[] = "crash_reporter-user-collection"; + +} // namespace + +// Tests that the config file is parsable and that Chrome is listed. +TEST(CrashReporterLogsTest, ReadConfig) { + brillo::KeyValueStore store; + ASSERT_TRUE(store.Load(base::FilePath(kConfigFile))); + std::string command; + EXPECT_TRUE(store.GetString(kUserCollectorSignature, &command)); + EXPECT_FALSE(command.empty()); +} diff --git a/crash_sender b/crash_sender new file mode 100755 index 0000000..a430ab5 --- /dev/null +++ b/crash_sender @@ -0,0 +1,719 @@ +#!/system/bin/sh + +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +# Default product ID in crash report (used if GOOGLE_CRASH_* is undefined). +BRILLO_PRODUCT=Brillo + +# Base directory that contains any crash reporter state files. +CRASH_STATE_DIR="/data/misc/crash_reporter" + +# File containing crash_reporter's anonymized guid. +GUID_FILE="${CRASH_STATE_DIR}/guid" + +# Crash sender lock in case the sender is already running. +CRASH_SENDER_LOCK="${CRASH_STATE_DIR}/lock/crash_sender" + +# Path to file that indicates a crash test is currently running. +CRASH_TEST_IN_PROGRESS_FILE="${CRASH_STATE_DIR}/tmp/crash-test-in-progress" + +# Set this to 1 in the environment to allow uploading crash reports +# for unofficial versions. +FORCE_OFFICIAL=${FORCE_OFFICIAL:-0} + +# Path to hardware class description. +HWCLASS_PATH="/sys/devices/platform/chromeos_acpi/HWID" + +# Path to file that indicates this is a developer image. +LEAVE_CORE_FILE="${CRASH_STATE_DIR}/.leave_core" + +# Path to list_proxies. +LIST_PROXIES="list_proxies" + +# Maximum crashes to send per day. +MAX_CRASH_RATE=${MAX_CRASH_RATE:-32} + +# File whose existence mocks crash sending. If empty we pretend the +# crash sending was successful, otherwise unsuccessful. +MOCK_CRASH_SENDING="${CRASH_STATE_DIR}/tmp/mock-crash-sending" + +# Set this to 1 in the environment to pretend to have booted in developer +# mode. This is used by autotests. +MOCK_DEVELOPER_MODE=${MOCK_DEVELOPER_MODE:-0} + +# Ignore PAUSE_CRASH_SENDING file if set. +OVERRIDE_PAUSE_SENDING=${OVERRIDE_PAUSE_SENDING:-0} + +# File whose existence causes crash sending to be delayed (for testing). +# Must be stateful to enable testing kernel crashes. +PAUSE_CRASH_SENDING="${CRASH_STATE_DIR}/lock/crash_sender_paused" + +# Path to a directory of restricted certificates which includes +# a certificate for the crash server. +RESTRICTED_CERTIFICATES_PATH="/system/etc/security/cacerts" +RESTRICTED_CERTIFICATES_PATH_GOOGLE="/system/etc/security/cacerts_google" + +# File whose existence implies we're running and not to start again. +RUN_FILE="${CRASH_STATE_DIR}/run/crash_sender.pid" + +# Maximum time to sleep between sends. +SECONDS_SEND_SPREAD=${SECONDS_SEND_SPREAD:-600} + +# Set this to 1 to allow uploading of device coredumps. +DEVCOREDUMP_UPLOAD_FLAG_FILE="${CRASH_STATE_DIR}/device_coredump_upload_allowed" + +# The weave configuration file. +WEAVE_CONF_FILE="/etc/weaved/weaved.conf" + +# The os-release.d folder. +OSRELEASED_FOLDER="/etc/os-release.d" + +# The syslog tag for all logging we emit. +TAG="$(basename $0)[$$]" + +# Directory to store timestamp files indicating the uploads in the past 24 +# hours. +TIMESTAMPS_DIR="${CRASH_STATE_DIR}/crash_sender" + +# Temp directory for this process. +TMP_DIR="" + +# Crash report log file. +CRASH_LOG="${CRASH_STATE_DIR}/log/uploads.log" + +lecho() { + log -t "${TAG}" "$@" +} + +lwarn() { + lecho -psyslog.warn "$@" +} + +# Returns true if mock is enabled. +is_mock() { + [ -f "${MOCK_CRASH_SENDING}" ] && return 0 + return 1 +} + +is_mock_successful() { + local mock_in=$(cat "${MOCK_CRASH_SENDING}") + [ "${mock_in}" = "" ] && return 0 # empty file means success + return 1 +} + +cleanup() { + if [ -n "${TMP_DIR}" ]; then + rm -rf "${TMP_DIR}" + fi + rm -f "${RUN_FILE}" + if [ -n "${CRASH_SENDER_LOCK}" ]; then + rm -rf "${CRASH_SENDER_LOCK}" + fi + crash_done +} + +crash_done() { + if is_mock; then + # For testing purposes, emit a message to log so that we + # know when the test has received all the messages from this run. + lecho "crash_sender done." + fi +} + +is_official_image() { + [ ${FORCE_OFFICIAL} -ne 0 ] && return 0 + if [ "$(getprop ro.secure)" = "1" ]; then + return 0 + else + return 1 + fi +} + +# Returns 0 if the a crash test is currently running. NOTE: Mirrors +# crash_collector.cc:CrashCollector::IsCrashTestInProgress(). +is_crash_test_in_progress() { + [ -f "${CRASH_TEST_IN_PROGRESS_FILE}" ] && return 0 + return 1 +} + +# Returns 0 if we should consider ourselves to be running on a developer +# image. NOTE: Mirrors crash_collector.cc:CrashCollector::IsDeveloperImage(). +is_developer_image() { + # If we're testing crash reporter itself, we don't want to special-case + # for developer images. + is_crash_test_in_progress && return 1 + [ -f "${LEAVE_CORE_FILE}" ] && return 0 + return 1 +} + +# Returns 0 if we should consider ourselves to be running on a test image. +is_test_image() { + # If we're testing crash reporter itself, we don't want to special-case + # for test images. + is_crash_test_in_progress && return 1 + case $(get_channel) in + test*) return 0;; + esac + return 1 +} + +# Returns 0 if the machine booted up in developer mode. +is_developer_mode() { + [ ${MOCK_DEVELOPER_MODE} -ne 0 ] && return 0 + # If we're testing crash reporter itself, we don't want to special-case + # for developer mode. + is_crash_test_in_progress && return 1 + if [ "$(getprop ro.debuggable)" = "1" ]; then + return 0 + else + return 1 + fi +} + +# Returns the path of the certificates directory to be used when sending +# reports to the crash server. +# If crash_reporter.full_certs=1, return the full certificates path. +# Otherwise return the Google-specific certificates path. +get_certificates_path() { + if [ "$(getprop crash_reporter.full_certs)" = "1" ]; then + echo "${RESTRICTED_CERTIFICATES_PATH}" + else + echo "${RESTRICTED_CERTIFICATES_PATH_GOOGLE}" + fi +} + +# Return 0 if the uploading of device coredumps is allowed. +is_device_coredump_upload_allowed() { + [ -f "${DEVCOREDUMP_UPLOAD_FLAG_FILE}" ] && return 0 + return 1 +} + +# Generate a uniform random number in 0..max-1. +# POSIX arithmetic expansion requires support of at least signed long integers. +# On 32-bit systems, that may mean 32-bit signed integers, in which case the +# 32-bit random number read from /dev/urandom may be interpreted as negative +# when used inside an arithmetic expansion (since the high bit might be set). +# mksh at least is known to behave this way. +# For this case, simply take the absolute value, which will still give a +# roughly uniform random distribution for the modulo (as we are merely ignoring +# the high/sign bit). +# See corresponding Arithmetic Expansion and Arithmetic Expression sections: +# POSIX: http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_04 +# mksh: http://linux.die.net/man/1/mksh +generate_uniform_random() { + local max=$1 + local random="$(od -An -N4 -tu /dev/urandom)" + echo $(((random < 0 ? -random : random) % max)) +} + +# Check if sending a crash now does not exceed the maximum 24hr rate and +# commit to doing so, if not. +check_rate() { + mkdir -p ${TIMESTAMPS_DIR} + # Only consider minidumps written in the past 24 hours by removing all older. + find "${TIMESTAMPS_DIR}" -mindepth 1 -mtime +1 \ + -exec rm -- '{}' ';' + local sends_in_24hrs=$(echo "${TIMESTAMPS_DIR}"/* | wc -w) + lecho "Current send rate: ${sends_in_24hrs}sends/24hrs" + if [ ${sends_in_24hrs} -ge ${MAX_CRASH_RATE} ]; then + lecho "Cannot send more crashes:" + lecho " current ${sends_in_24hrs}send/24hrs >= " \ + "max ${MAX_CRASH_RATE}send/24hrs" + return 1 + fi + mktemp "${TIMESTAMPS_DIR}"/XXXXXX > /dev/null + return 0 +} + +# Gets the base part of a crash report file, such as name.01234.5678.9012 from +# name.01234.5678.9012.meta or name.01234.5678.9012.log.tar.xz. We make sure +# "name" is sanitized in CrashCollector::Sanitize to not include any periods. +get_base() { + echo "$1" | cut -d. -f-4 +} + +get_extension() { + local extension="${1##*.}" + local filename="${1%.*}" + # For gzipped file, we ignore .gz and get the real extension + if [ "${extension}" = "gz" ]; then + echo "${filename##*.}" + else + echo "${extension}" + fi +} + +# Return which kind of report the given metadata file relates to +get_kind() { + local payload="$(get_key_value "$1" "payload")" + if [ ! -r "${payload}" ]; then + lecho "Missing payload: ${payload}" + echo "undefined" + return + fi + local kind="$(get_extension "${payload}")" + if [ "${kind}" = "dmp" ]; then + echo "minidump" + return + fi + echo "${kind}" +} + +get_key_value() { + local file="$1" key="$2" value + + if [ -f "${file}/${key}" ]; then + # Get the value from a folder where each key is its own file. The key + # file's entire contents is the value. + value=$(cat "${file}/${key}") + elif [ -f "${file}" ]; then + # Get the value from a file that has multiple key=value combinations. + # Return the first entry. There shouldn't be more than one anyways. + # Substr at length($1) + 2 skips past the key and following = sign (awk + # uses 1-based indexes), but preserves embedded = characters. + value=$(sed -n "/^${key}[[:space:]]*=/{s:^[^=]*=::p;q}" "${file}") + fi + + echo "${value:-undefined}" +} + +get_keys() { + local file="$1" regex="$2" + + cut -d '=' -f1 "${file}" | grep --color=never "${regex}" +} + +# Return the channel name (sans "-channel" suffix). +get_channel() { + getprop ro.product.channel | sed 's:-channel$::' +} + +# Return the hardware class or "undefined". +get_hardware_class() { + if [ -r "${HWCLASS_PATH}" ]; then + cat "${HWCLASS_PATH}" + else + echo "undefined" + fi +} + +# Return the log string filtered with only JSON-safe white-listed characters. +filter_log_string() { + echo "$1" | tr -cd '[:alnum:]_.\-:;' +} + +send_crash() { + local meta_path="$1" + local report_payload="$(get_key_value "${meta_path}" "payload")" + local kind="$(get_kind "${meta_path}")" + local exec_name="$(get_key_value "${meta_path}" "exec_name")" + local url="$(get_key_value "${OSRELEASED_FOLDER}" "crash_server")" + local bdk_version="$(get_key_value "${meta_path}" "bdk_version")" + local hwclass="$(get_hardware_class)" + local write_payload_size="$(get_key_value "${meta_path}" "payload_size")" + local log="$(get_key_value "${meta_path}" "log")" + local sig="$(get_key_value "${meta_path}" "sig")" + local send_payload_size="$(stat -c "%s" "${report_payload}" 2>/dev/null)" + local product="$(get_key_value "${meta_path}" "product_id")" + local version="$(get_key_value "${meta_path}" "product_version")" + local upload_prefix="$(get_key_value "${meta_path}" "upload_prefix")" + local guid + local model_manifest_id="$(get_key_value "${WEAVE_CONF_FILE}" "model_id")" + + # If crash_reporter.server is not set return with an error. + if [ -z "${url}" ]; then + lecho "Configuration error: crash_reporter.server not set." + return 1 + fi + + set -- \ + -F "write_payload_size=${write_payload_size}" \ + -F "send_payload_size=${send_payload_size}" + if [ "${sig}" != "undefined" ]; then + set -- "$@" \ + -F "sig=${sig}" \ + -F "sig2=${sig}" + fi + if [ -r "${report_payload}" ]; then + set -- "$@" \ + -F "upload_file_${kind}=@${report_payload}" + fi + if [ "${log}" != "undefined" -a -r "${log}" ]; then + set -- "$@" \ + -F "log=@${log}" + fi + + if [ "${upload_prefix}" = "undefined" ]; then + upload_prefix="" + fi + + # Grab any variable that begins with upload_. + local v + for k in $(get_keys "${meta_path}" "^upload_"); do + v="$(get_key_value "${meta_path}" "${k}")" + case ${k} in + # Product & version are handled separately. + upload_var_prod) ;; + upload_var_ver) ;; + upload_var_*) + set -- "$@" -F "${upload_prefix}${k#upload_var_}=${v}" + ;; + upload_file_*) + if [ -r "${v}" ]; then + set -- "$@" -F "${upload_prefix}${k#upload_file_}=@${v}" + fi + ;; + esac + done + + # If ID or VERSION_ID is undefined, we use the default product name + # and bdk_version from /etc/os-release.d. + if [ "${product}" = "undefined" ]; then + product="${BRILLO_PRODUCT}" + fi + if [ "${version}" = "undefined" ]; then + version="${bdk_version}" + fi + + local image_type + if is_test_image; then + image_type="test" + elif is_developer_image; then + image_type="dev" + elif [ ${FORCE_OFFICIAL} -ne 0 ]; then + image_type="force-official" + elif is_mock && ! is_mock_successful; then + image_type="mock-fail" + fi + + local boot_mode + if is_developer_mode; then + boot_mode="dev" + fi + + # Need to strip dashes ourselves as Chrome preserves it in the file + # nowadays. This is also what the Chrome breakpad client does. + guid=$(tr -d '-' < "${GUID_FILE}") + + local error_type="$(get_key_value "${meta_path}" "error_type")" + [ "${error_type}" = "undefined" ] && error_type= + + lecho "Sending crash:" + if [ "${product}" != "${BRILLO_PRODUCT}" ]; then + lecho " Sending crash report on behalf of ${product}" + fi + lecho " Metadata: ${meta_path} (${kind})" + lecho " Payload: ${report_payload}" + lecho " Version: ${version}" + lecho " Bdk Version: ${bdk_version}" + [ -n "${image_type}" ] && lecho " Image type: ${image_type}" + [ -n "${boot_mode}" ] && lecho " Boot mode: ${boot_mode}" + if is_mock; then + lecho " Product: ${product}" + lecho " URL: ${url}" + lecho " HWClass: ${hwclass}" + lecho " write_payload_size: ${write_payload_size}" + lecho " send_payload_size: ${send_payload_size}" + if [ "${log}" != "undefined" ]; then + lecho " log: @${log}" + fi + if [ "${sig}" != "undefined" ]; then + lecho " sig: ${sig}" + fi + fi + lecho " Exec name: ${exec_name}" + [ -n "${error_type}" ] && lecho " Error type: ${error_type}" + if is_mock; then + if ! is_mock_successful; then + lecho "Mocking unsuccessful send" + return 1 + fi + lecho "Mocking successful send" + return 0 + fi + + # Read in the first proxy, if any, for a given URL. NOTE: The + # double-quotes are necessary due to a bug in dash with the "local" + # builtin command and values that have spaces in them (see + # "https://bugs.launchpad.net/ubuntu/+source/dash/+bug/139097"). + if [ -f "${LIST_PROXIES}" ]; then + local proxy ret + proxy=$("${LIST_PROXIES}" --quiet "${url}") + ret=$? + if [ ${ret} -ne 0 ]; then + proxy='' + lwarn "Listing proxies failed with exit code ${ret}" + else + proxy=$(echo "${proxy}" | head -1) + fi + fi + # if a direct connection should be used, unset the proxy variable. + [ "${proxy}" = "direct://" ] && proxy= + local report_id="${TMP_DIR}/report_id" + local curl_stderr="${TMP_DIR}/curl_stderr" + + set +e + curl "${url}" -f -v ${proxy:+--proxy "$proxy"} \ + --capath "$(get_certificates_path)" --ciphers HIGH \ + -F "prod=${product}" \ + -F "ver=${version}" \ + -F "bdk_version=${bdk_version}" \ + -F "hwclass=${hwclass}" \ + -F "exec_name=${exec_name}" \ + -F "model_manifest_id=${model_manifest_id}" \ + ${image_type:+-F "image_type=${image_type}"} \ + ${boot_mode:+-F "boot_mode=${boot_mode}"} \ + ${error_type:+-F "error_type=${error_type}"} \ + -F "guid=${guid}" \ + -o "${report_id}" \ + "$@" \ + 2>"${curl_stderr}" + curl_result=$? + set -e + + if [ ${curl_result} -eq 0 ]; then + local id="$(cat "${report_id}")" + local timestamp="$(date +%s)" + local filter_prod="$(filter_log_string "${product}")" + local filter_exec="$(filter_log_string "${exec_name}")" + if [ "${filter_prod}" != "${product}" ]; then + lwarn "Product name filtered to: ${filter_prod}." + fi + if [ "${filter_exec}" != "${exec_name}" ]; then + lwarn "Exec name filtered to: ${filter_exec}." + fi + printf "{'time':%s,'id':'%s','product':'%s','exec_name':'%s'}\n" \ + "${timestamp}" "${id}" "${filter_prod}" "${filter_exec}" >> "${CRASH_LOG}" + lecho "Crash report receipt ID ${id}" + else + lecho "Crash sending failed with exit code ${curl_result}: " \ + "$(cat "${curl_stderr}")" + fi + + rm -f "${report_id}" + + return ${curl_result} +} + +# *.meta files always end with done=1 so we can tell if they are complete. +is_complete_metadata() { + grep -q "done=1" "$1" +} + +# Remove the given report path. +remove_report() { + local base="${1%.*}" + rm -f -- "${base}".* +} + +# Send all crashes from the given directory. This applies even when we're on a +# 3G connection (see crosbug.com/3304 for discussion). +send_crashes() { + local dir="$1" + lecho "Sending crashes for ${dir}" + + if [ ! -d "${dir}" ]; then + return + fi + + # Consider any old files which still have no corresponding meta file + # as orphaned, and remove them. + for old_file in $(find "${dir}" -mindepth 1 \ + -mtime +1 -type f); do + if [ ! -e "$(get_base "${old_file}").meta" ]; then + lecho "Removing old orphaned file: ${old_file}." + rm -f -- "${old_file}" + fi + done + + # Look through all metadata (*.meta) files, oldest first. That way, the rate + # limit does not stall old crashes if there's a high amount of new crashes + # coming in. + # For each crash report, first evaluate conditions that might lead to its + # removal to honor user choice and to free disk space as soon as possible, + # then decide whether it should be sent right now or kept for later sending. + for meta_path in $(ls -1tr "${dir}"/*.meta 2>/dev/null); do + lecho "Considering metadata ${meta_path}." + + local kind=$(get_kind "${meta_path}") + if [ "${kind}" != "minidump" ] && \ + [ "${kind}" != "kcrash" ] && \ + [ "${kind}" != "log" ] && + [ "${kind}" != "devcore" ]; then + lecho "Unknown report kind ${kind}. Removing report." + remove_report "${meta_path}" + continue + fi + + if ! is_complete_metadata "${meta_path}"; then + # This report is incomplete, so if it's old, just remove it. + local old_meta=$(find "${dir}" -mindepth 1 -name \ + $(basename "${meta_path}") -mtime +1 -type f) + if [ -n "${old_meta}" ]; then + lecho "Removing old incomplete metadata." + remove_report "${meta_path}" + else + lecho "Ignoring recent incomplete metadata." + fi + continue + fi + + # Ignore device coredump if device coredump uploading is not allowed. + if [ "${kind}" = "devcore" ] && ! is_device_coredump_upload_allowed; then + lecho "Ignoring device coredump. Device coredump upload not allowed." + continue + fi + + if ! is_mock && ! is_official_image; then + lecho "Not an official OS version. Removing crash." + remove_report "${meta_path}" + continue + fi + + # Remove existing crashes in case user consent has not (yet) been given or + # has been revoked. This must come after the guest mode check because + # metrics_client always returns "not consented" in guest mode. + if ! metrics_client -c; then + lecho "Crash reporting is disabled. Removing crash." + remove_report "${meta_path}" + continue + fi + + # Skip report if the upload rate is exceeded. (Don't exit right now because + # subsequent reports may be candidates for deletion.) + if ! check_rate; then + lecho "Sending ${meta_path} would exceed rate. Leaving for later." + continue + fi + + # The .meta file should be written *after* all to-be-uploaded files that it + # references. Nevertheless, as a safeguard, a hold-off time of thirty + # seconds after writing the .meta file is ensured. Also, sending of crash + # reports is spread out randomly by up to SECONDS_SEND_SPREAD. Thus, for + # the sleep call the greater of the two delays is used. + local now=$(date +%s) + local holdoff_time=$(($(stat -c "%Y" "${meta_path}") + 30 - ${now})) + local spread_time=$(generate_uniform_random "${SECONDS_SEND_SPREAD}") + local sleep_time + if [ ${spread_time} -gt ${holdoff_time} ]; then + sleep_time="${spread_time}" + else + sleep_time="${holdoff_time}" + fi + lecho "Scheduled to send in ${sleep_time}s." + if ! is_mock; then + if ! sleep "${sleep_time}"; then + lecho "Sleep failed" + return 1 + fi + fi + + # Try to upload. + if ! send_crash "${meta_path}"; then + lecho "Problem sending ${meta_path}, not removing." + continue + fi + + # Send was successful, now remove. + lecho "Successfully sent crash ${meta_path} and removing." + remove_report "${meta_path}" + done +} + +usage() { + cat <<EOF +Usage: crash_sender [options] + +Options: + -e <var>=<val> Set env |var| to |val| (only some vars) +EOF + exit ${1:-1} +} + +parseargs() { + # Parse the command line arguments. + while [ $# -gt 0 ]; do + case $1 in + -e) + shift + case $1 in + FORCE_OFFICIAL=*|\ + MAX_CRASH_RATE=*|\ + MOCK_DEVELOPER_MODE=*|\ + OVERRIDE_PAUSE_SENDING=*|\ + SECONDS_SEND_SPREAD=*) + export "$1" + ;; + *) + lecho "Unknown var passed to -e: $1" + exit 1 + ;; + esac + ;; + -h) + usage 0 + ;; + *) + lecho "Unknown options: $*" + exit 1 + ;; + esac + shift + done +} + +main() { + parseargs "$@" + + if [ -e "${PAUSE_CRASH_SENDING}" ] && \ + [ ${OVERRIDE_PAUSE_SENDING} -eq 0 ]; then + lecho "Exiting early due to ${PAUSE_CRASH_SENDING}." + exit 1 + fi + + if is_test_image; then + lecho "Exiting early due to test image." + exit 1 + fi + + # We don't perform checks on this because we have a master lock with the + # CRASH_SENDER_LOCK file. This pid file is for the system to keep track + # (like with autotests) that we're still running. + echo $$ > "${RUN_FILE}" + + for dependency in "$(get_certificates_path)"; do + if [ ! -x "${dependency}" ]; then + lecho "Fatal: Crash sending disabled: ${dependency} not found." + exit 1 + fi + done + + TMP_DIR="$(mktemp -d "${CRASH_STATE_DIR}/tmp/crash_sender.XXXXXX")" + + # Send system-wide crashes + send_crashes "${CRASH_STATE_DIR}/crash" +} + +trap cleanup EXIT INT TERM + +#TODO(http://b/23937249): Change the locking logic back to using flock. +if ! mkdir "${CRASH_SENDER_LOCK}" 2>/dev/null; then + lecho "Already running; quitting." + crash_done + exit 1 +fi +main "$@" diff --git a/dbus_bindings/org.chromium.LibCrosService.xml b/dbus_bindings/org.chromium.LibCrosService.xml new file mode 100644 index 0000000..64b8b84 --- /dev/null +++ b/dbus_bindings/org.chromium.LibCrosService.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<node name="/org/chromium/LibCrosService" + xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0"> + <interface name="org.chromium.LibCrosServiceInterface"> + <method name="ResolveNetworkProxy"> + <arg name="source_url" type="s" direction="in"/> + <arg name="signal_interface" type="s" direction="in"/> + <arg name="signal_name" type="s" direction="in"/> + <annotation name="org.chromium.DBus.Method.Kind" value="simple"/> + </method> + </interface> + <interface name="org.chromium.CrashReporterLibcrosProxyResolvedInterface"> + <signal name="ProxyResolved"> + <arg name="source_url" type="s" direction="out"/> + <arg name="proxy_info" type="s" direction="out"/> + <arg name="error_message" type="s" direction="out"/> + </signal> + </interface> +</node> diff --git a/init/crash-reporter.conf b/init/crash-reporter.conf new file mode 100644 index 0000000..19f2cdb --- /dev/null +++ b/init/crash-reporter.conf @@ -0,0 +1,27 @@ +# Copyright (c) 2011 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. + +description "Initialize crash reporting services" +author "chromium-os-dev@chromium.org" + +# This job merely initializes its service and then terminates; the +# actual checking and reporting of crash dumps is triggered by an +# hourly cron job. +start on starting system-services + +pre-start script + mkdir -p /var/spool + + # Only allow device coredumps on a "developer system". + if ! is_developer_end_user; then + # consumer end-user - disable device coredumps, if driver exists. + echo 1 > /sys/class/devcoredump/disabled || true + fi +end script + +# crash_reporter uses argv[0] as part of the command line for +# /proc/sys/kernel/core_pattern. That command line is invoked by +# the kernel, and can't rely on PATH, so argv[0] must be a full +# path; we invoke it as such here. +exec /sbin/crash_reporter --init diff --git a/init/crash-sender.conf b/init/crash-sender.conf new file mode 100644 index 0000000..892186f --- /dev/null +++ b/init/crash-sender.conf @@ -0,0 +1,11 @@ +# Copyright (c) 2014 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. + +description "Run the crash sender periodically" +author "chromium-os-dev@chromium.org" + +start on starting system-services +stop on stopping system-services + +exec periodic_scheduler 3600 14400 crash_sender /sbin/crash_sender diff --git a/init/warn-collector.conf b/init/warn-collector.conf new file mode 100644 index 0000000..3be80da --- /dev/null +++ b/init/warn-collector.conf @@ -0,0 +1,12 @@ +# 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. + +description "Runs a daemon which collects and reports kernel warnings" +author "chromium-os-dev@chromium.org" + +start on started system-services +stop on stopping system-services +respawn + +exec warn_collector diff --git a/kernel_collector.cc b/kernel_collector.cc new file mode 100644 index 0000000..68f2d9e --- /dev/null +++ b/kernel_collector.cc @@ -0,0 +1,603 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernel_collector.h" + +#include <map> +#include <sys/stat.h> + +#include <base/files/file_util.h> +#include <base/logging.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> + +using base::FilePath; +using base::StringPrintf; + +namespace { + +const char kDefaultKernelStackSignature[] = "kernel-UnspecifiedStackSignature"; +const char kDumpParentPath[] = "/sys/fs"; +const char kDumpPath[] = "/sys/fs/pstore"; +const char kDumpFormat[] = "dmesg-ramoops-%zu"; +const char kKernelExecName[] = "kernel"; +// Maximum number of records to examine in the kDumpPath. +const size_t kMaxDumpRecords = 100; +const pid_t kKernelPid = 0; +const char kKernelSignatureKey[] = "sig"; +// Byte length of maximum human readable portion of a kernel crash signature. +const int kMaxHumanStringLength = 40; +const uid_t kRootUid = 0; +// Time in seconds from the final kernel log message for a call stack +// to count towards the signature of the kcrash. +const int kSignatureTimestampWindow = 2; +// Kernel log timestamp regular expression. +const char kTimestampRegex[] = "^<.*>\\[\\s*(\\d+\\.\\d+)\\]"; + +// +// These regular expressions enable to us capture the PC in a backtrace. +// The backtrace is obtained through dmesg or the kernel's preserved/kcrashmem +// feature. +// +// For ARM we see: +// "<5>[ 39.458982] PC is at write_breakme+0xd0/0x1b4" +// For MIPS we see: +// "<5>[ 3378.552000] epc : 804010f0 lkdtm_do_action+0x68/0x3f8" +// For x86: +// "<0>[ 37.474699] EIP: [<790ed488>] write_breakme+0x80/0x108 +// SS:ESP 0068:e9dd3efc" +// +const char* const kPCRegex[] = { + 0, + " PC is at ([^\\+ ]+).*", + " epc\\s+:\\s+\\S+\\s+([^\\+ ]+).*", // MIPS has an exception program counter + " EIP: \\[<.*>\\] ([^\\+ ]+).*", // X86 uses EIP for the program counter + " RIP \\[<.*>\\] ([^\\+ ]+).*", // X86_64 uses RIP for the program counter +}; + +static_assert(arraysize(kPCRegex) == KernelCollector::kArchCount, + "Missing Arch PC regexp"); + +} // namespace + +KernelCollector::KernelCollector() + : is_enabled_(false), + ramoops_dump_path_(kDumpPath), + records_(0), + // We expect crash dumps in the format of architecture we are built for. + arch_(GetCompilerArch()) { +} + +KernelCollector::~KernelCollector() { +} + +void KernelCollector::OverridePreservedDumpPath(const FilePath &file_path) { + ramoops_dump_path_ = file_path; +} + +bool KernelCollector::ReadRecordToString(std::string *contents, + size_t current_record, + bool *record_found) { + // A record is a ramoops dump. It has an associated size of "record_size". + std::string record; + std::string captured; + + // Ramoops appends a header to a crash which contains ==== followed by a + // timestamp. Ignore the header. + pcrecpp::RE record_re( + "====\\d+\\.\\d+\n(.*)", + pcrecpp::RE_Options().set_multiline(true).set_dotall(true)); + + pcrecpp::RE sanity_check_re("\n<\\d+>\\[\\s*(\\d+\\.\\d+)\\]"); + + FilePath ramoops_record; + GetRamoopsRecordPath(&ramoops_record, current_record); + if (!base::ReadFileToString(ramoops_record, &record)) { + LOG(ERROR) << "Unable to open " << ramoops_record.value(); + return false; + } + + *record_found = false; + if (record_re.FullMatch(record, &captured)) { + // Found a ramoops header, so strip the header and append the rest. + contents->append(captured); + *record_found = true; + } else if (sanity_check_re.PartialMatch(record.substr(0, 1024))) { + // pstore compression has been added since kernel 3.12. In order to + // decompress dmesg correctly, ramoops driver has to strip the header + // before handing over the record to the pstore driver, so we don't + // need to do it here anymore. However, the sanity check is needed because + // sometimes a pstore record is just a chunk of uninitialized memory which + // is not the result of a kernel crash. See crbug.com/443764 + contents->append(record); + *record_found = true; + } else { + LOG(WARNING) << "Found invalid record at " << ramoops_record.value(); + } + + // Remove the record from pstore after it's found. + if (*record_found) + base::DeleteFile(ramoops_record, false); + + return true; +} + +void KernelCollector::GetRamoopsRecordPath(FilePath *path, + size_t record) { + // Disable error "format not a string literal, argument types not checked" + // because this is valid, but GNU apparently doesn't bother checking a const + // format string. + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wformat-nonliteral" + *path = ramoops_dump_path_.Append(StringPrintf(kDumpFormat, record)); + #pragma GCC diagnostic pop +} + +bool KernelCollector::LoadParameters() { + // Discover how many ramoops records are being exported by the driver. + size_t count; + + for (count = 0; count < kMaxDumpRecords; ++count) { + FilePath ramoops_record; + GetRamoopsRecordPath(&ramoops_record, count); + + if (!base::PathExists(ramoops_record)) + break; + } + + records_ = count; + return (records_ > 0); +} + +bool KernelCollector::LoadPreservedDump(std::string *contents) { + // Load dumps from the preserved memory and save them in contents. + // Since the system is set to restart on oops we won't actually ever have + // multiple records (only 0 or 1), but check in case we don't restart on + // oops in the future. + bool any_records_found = false; + bool record_found = false; + // clear contents since ReadFileToString actually appends to the string. + contents->clear(); + + for (size_t i = 0; i < records_; ++i) { + if (!ReadRecordToString(contents, i, &record_found)) { + break; + } + if (record_found) { + any_records_found = true; + } + } + + if (!any_records_found) { + LOG(ERROR) << "No valid records found in " << ramoops_dump_path_.value(); + return false; + } + + return true; +} + +void KernelCollector::StripSensitiveData(std::string *kernel_dump) { + // Strip any data that the user might not want sent up to the crash servers. + // We'll read in from kernel_dump and also place our output there. + // + // At the moment, the only sensitive data we strip is MAC addresses. + + // 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 kernel_dump, 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::ostringstream result; + std::string pre_mac_str; + std::string mac_str; + std::map<std::string, std::string> mac_map; + pcrecpp::StringPiece input(*kernel_dump); + + // This RE will find the next MAC address and can return us the data preceding + // the MAC and the MAC itself. + pcrecpp::RE 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])", + pcrecpp::RE_Options() + .set_multiline(true) + .set_dotall(true)); + + // 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 + pcrecpp::RE acpi_re("ACPI cmd ef/$", + pcrecpp::RE_Options() + .set_multiline(true) + .set_dotall(true)); + + // Keep consuming, building up a result string as we go. + while (mac_re.Consume(&input, &pre_mac_str, &mac_str)) { + if (acpi_re.PartialMatch(pre_mac_str)) { + // We really saw an ACPI command; add to result w/ no stripping. + result << pre_mac_str << mac_str; + } else { + // Found a MAC address; look up in our hash for the mapping. + std::string replacement_mac = mac_map[mac_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[mac_str] = replacement_mac; + } + + // Dump the string before the MAC and the fake MAC address into result. + result << pre_mac_str << replacement_mac; + } + } + + // One last bit of data might still be in the input. + result << input; + + // We'll just assign right back to kernel_dump. + *kernel_dump = result.str(); +} + +bool KernelCollector::DumpDirMounted() { + struct stat st_parent; + if (stat(kDumpParentPath, &st_parent)) { + PLOG(WARNING) << "Could not stat " << kDumpParentPath; + return false; + } + + struct stat st_dump; + if (stat(kDumpPath, &st_dump)) { + PLOG(WARNING) << "Could not stat " << kDumpPath; + return false; + } + + if (st_parent.st_dev == st_dump.st_dev) { + LOG(WARNING) << "Dump dir " << kDumpPath << " not mounted"; + return false; + } + + return true; +} + +bool KernelCollector::Enable() { + if (arch_ == kArchUnknown || arch_ >= kArchCount || + kPCRegex[arch_] == nullptr) { + LOG(WARNING) << "KernelCollector does not understand this architecture"; + return false; + } + + if (!DumpDirMounted()) { + LOG(WARNING) << "Kernel does not support crash dumping"; + return false; + } + + // To enable crashes, we will eventually need to set + // the chnv bit in BIOS, but it does not yet work. + LOG(INFO) << "Enabling kernel crash handling"; + is_enabled_ = true; + return true; +} + +// 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). +static unsigned HashString(const std::string &input) { + unsigned hash = 0; + for (size_t i = 0; i < input.length(); ++i) + hash = hash * 16127 + input[i]; + return hash; +} + +void KernelCollector::ProcessStackTrace( + pcrecpp::StringPiece kernel_dump, + bool print_diagnostics, + unsigned *hash, + float *last_stack_timestamp, + bool *is_watchdog_crash) { + pcrecpp::RE line_re("(.+)", pcrecpp::MULTILINE()); + pcrecpp::RE stack_trace_start_re(std::string(kTimestampRegex) + + " (Call Trace|Backtrace):$"); + + // Match lines such as the following and grab out "function_name". + // The ? may or may not be present. + // + // For ARM: + // <4>[ 3498.731164] [<c0057220>] ? (function_name+0x20/0x2c) from + // [<c018062c>] (foo_bar+0xdc/0x1bc) + // + // For MIPS: + // <5>[ 3378.656000] [<804010f0>] lkdtm_do_action+0x68/0x3f8 + // + // For X86: + // <4>[ 6066.849504] [<7937bcee>] ? function_name+0x66/0x6c + // + pcrecpp::RE stack_entry_re(std::string(kTimestampRegex) + + "\\s+\\[<[[:xdigit:]]+>\\]" // Matches " [<7937bcee>]" + "([\\s\\?(]+)" // Matches " ? (" (ARM) or " ? " (X86) + "([^\\+ )]+)"); // Matches until delimiter reached + std::string line; + std::string hashable; + std::string previous_hashable; + bool is_watchdog = false; + + *hash = 0; + *last_stack_timestamp = 0; + + // Find the last and second-to-last stack traces. The latter is used when + // the panic is from a watchdog timeout. + while (line_re.FindAndConsume(&kernel_dump, &line)) { + std::string certainty; + std::string function_name; + if (stack_trace_start_re.PartialMatch(line, last_stack_timestamp)) { + if (print_diagnostics) { + printf("Stack trace starting.%s\n", + hashable.empty() ? "" : " Saving prior trace."); + } + previous_hashable = hashable; + hashable.clear(); + is_watchdog = false; + } else if (stack_entry_re.PartialMatch(line, + last_stack_timestamp, + &certainty, + &function_name)) { + bool is_certain = certainty.find('?') == std::string::npos; + if (print_diagnostics) { + printf("@%f: stack entry for %s (%s)\n", + *last_stack_timestamp, + function_name.c_str(), + is_certain ? "certain" : "uncertain"); + } + // Do not include any uncertain (prefixed by '?') frames in our hash. + if (!is_certain) + continue; + if (!hashable.empty()) + hashable.append("|"); + if (function_name == "watchdog_timer_fn" || + function_name == "watchdog") { + is_watchdog = true; + } + hashable.append(function_name); + } + } + + // If the last stack trace contains a watchdog function we assume the panic + // is from the watchdog timer, and we hash the previous stack trace rather + // than the last one, assuming that the previous stack is that of the hung + // thread. + // + // In addition, if the hashable is empty (meaning all frames are uncertain, + // for whatever reason) also use the previous frame, as it cannot be any + // worse. + if (is_watchdog || hashable.empty()) { + hashable = previous_hashable; + } + + *hash = HashString(hashable); + *is_watchdog_crash = is_watchdog; + + if (print_diagnostics) { + printf("Hash based on stack trace: \"%s\" at %f.\n", + hashable.c_str(), *last_stack_timestamp); + } +} + +// static +KernelCollector::ArchKind KernelCollector::GetCompilerArch() { +#if defined(COMPILER_GCC) && defined(ARCH_CPU_ARM_FAMILY) + return kArchArm; +#elif defined(COMPILER_GCC) && defined(ARCH_CPU_MIPS_FAMILY) + return kArchMips; +#elif defined(COMPILER_GCC) && defined(ARCH_CPU_X86_64) + return kArchX86_64; +#elif defined(COMPILER_GCC) && defined(ARCH_CPU_X86_FAMILY) + return kArchX86; +#else + return kArchUnknown; +#endif +} + +bool KernelCollector::FindCrashingFunction( + pcrecpp::StringPiece kernel_dump, + bool print_diagnostics, + float stack_trace_timestamp, + std::string *crashing_function) { + float timestamp = 0; + + // Use the correct regex for this architecture. + pcrecpp::RE eip_re(std::string(kTimestampRegex) + kPCRegex[arch_], + pcrecpp::MULTILINE()); + + while (eip_re.FindAndConsume(&kernel_dump, ×tamp, crashing_function)) { + if (print_diagnostics) { + printf("@%f: found crashing function %s\n", + timestamp, + crashing_function->c_str()); + } + } + if (timestamp == 0) { + if (print_diagnostics) { + printf("Found no crashing function.\n"); + } + return false; + } + if (stack_trace_timestamp != 0 && + abs(static_cast<int>(stack_trace_timestamp - timestamp)) + > kSignatureTimestampWindow) { + if (print_diagnostics) { + printf("Found crashing function but not within window.\n"); + } + return false; + } + if (print_diagnostics) { + printf("Found crashing function %s\n", crashing_function->c_str()); + } + return true; +} + +bool KernelCollector::FindPanicMessage(pcrecpp::StringPiece kernel_dump, + bool print_diagnostics, + std::string *panic_message) { + // Match lines such as the following and grab out "Fatal exception" + // <0>[ 342.841135] Kernel panic - not syncing: Fatal exception + pcrecpp::RE kernel_panic_re(std::string(kTimestampRegex) + + " Kernel panic[^\\:]*\\:\\s*(.*)", + pcrecpp::MULTILINE()); + float timestamp = 0; + while (kernel_panic_re.FindAndConsume(&kernel_dump, + ×tamp, + panic_message)) { + if (print_diagnostics) { + printf("@%f: panic message %s\n", + timestamp, + panic_message->c_str()); + } + } + if (timestamp == 0) { + if (print_diagnostics) { + printf("Found no panic message.\n"); + } + return false; + } + return true; +} + +bool KernelCollector::ComputeKernelStackSignature( + const std::string &kernel_dump, + std::string *kernel_signature, + bool print_diagnostics) { + unsigned stack_hash = 0; + float last_stack_timestamp = 0; + std::string human_string; + bool is_watchdog_crash; + + ProcessStackTrace(kernel_dump, + print_diagnostics, + &stack_hash, + &last_stack_timestamp, + &is_watchdog_crash); + + if (!FindCrashingFunction(kernel_dump, + print_diagnostics, + last_stack_timestamp, + &human_string)) { + if (!FindPanicMessage(kernel_dump, print_diagnostics, &human_string)) { + if (print_diagnostics) { + printf("Found no human readable string, using empty string.\n"); + } + human_string.clear(); + } + } + + if (human_string.empty() && stack_hash == 0) { + if (print_diagnostics) { + printf("Found neither a stack nor a human readable string, failing.\n"); + } + return false; + } + + human_string = human_string.substr(0, kMaxHumanStringLength); + *kernel_signature = StringPrintf("%s-%s%s-%08X", + kKernelExecName, + (is_watchdog_crash ? "(HANG)-" : ""), + human_string.c_str(), + stack_hash); + return true; +} + +bool KernelCollector::Collect() { + std::string kernel_dump; + FilePath root_crash_directory; + + if (!LoadParameters()) { + return false; + } + if (!LoadPreservedDump(&kernel_dump)) { + return false; + } + StripSensitiveData(&kernel_dump); + if (kernel_dump.empty()) { + return false; + } + std::string signature; + if (!ComputeKernelStackSignature(kernel_dump, &signature, false)) { + signature = kDefaultKernelStackSignature; + } + + std::string reason = "handling"; + bool feedback = true; + if (IsDeveloperImage()) { + reason = "developer build - always dumping"; + feedback = true; + } else if (!is_feedback_allowed_function_()) { + reason = "ignoring - no consent"; + feedback = false; + } + + LOG(INFO) << "Received prior crash notification from " + << "kernel (signature " << signature << ") (" << reason << ")"; + + if (feedback) { + count_crash_function_(); + + if (!GetCreatedCrashDirectoryByEuid(kRootUid, + &root_crash_directory, + nullptr)) { + return true; + } + + std::string dump_basename = + FormatDumpBasename(kKernelExecName, time(nullptr), kKernelPid); + FilePath kernel_crash_path = root_crash_directory.Append( + StringPrintf("%s.kcrash", dump_basename.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(kernel_crash_path, + kernel_dump.data(), + kernel_dump.length()) != + static_cast<int>(kernel_dump.length())) { + LOG(INFO) << "Failed to write kernel dump to " + << kernel_crash_path.value().c_str(); + return true; + } + + AddCrashMetaData(kKernelSignatureKey, signature); + WriteCrashMetaData( + root_crash_directory.Append( + StringPrintf("%s.meta", dump_basename.c_str())), + kKernelExecName, + kernel_crash_path.value()); + + LOG(INFO) << "Stored kcrash to " << kernel_crash_path.value(); + } + + return true; +} diff --git a/kernel_collector.h b/kernel_collector.h new file mode 100644 index 0000000..206ee26 --- /dev/null +++ b/kernel_collector.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_KERNEL_COLLECTOR_H_ +#define CRASH_REPORTER_KERNEL_COLLECTOR_H_ + +#include <pcrecpp.h> + +#include <string> + +#include <base/files/file_path.h> +#include <base/macros.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "crash_collector.h" + +// Kernel crash collector. +class KernelCollector : public CrashCollector { + public: + // Enumeration to specify architecture type. + enum ArchKind { + kArchUnknown, + kArchArm, + kArchMips, + kArchX86, + kArchX86_64, + + kArchCount // Number of architectures. + }; + + KernelCollector(); + + ~KernelCollector() override; + + void OverridePreservedDumpPath(const base::FilePath &file_path); + + // Enable collection. + bool Enable(); + + // Returns true if the kernel collection currently enabled. + bool is_enabled() const { return is_enabled_; } + + // Collect any preserved kernel crash dump. Returns true if there was + // a dump (even if there were problems storing the dump), false otherwise. + bool Collect(); + + // Compute a stack signature string from a kernel dump. + bool ComputeKernelStackSignature(const std::string &kernel_dump, + std::string *kernel_signature, + bool print_diagnostics); + + // Set the architecture of the crash dumps we are looking at. + void set_arch(ArchKind arch) { arch_ = arch; } + ArchKind arch() const { return arch_; } + + private: + friend class KernelCollectorTest; + FRIEND_TEST(KernelCollectorTest, LoadPreservedDump); + FRIEND_TEST(KernelCollectorTest, StripSensitiveDataBasic); + FRIEND_TEST(KernelCollectorTest, StripSensitiveDataBulk); + FRIEND_TEST(KernelCollectorTest, StripSensitiveDataSample); + FRIEND_TEST(KernelCollectorTest, CollectOK); + + virtual bool DumpDirMounted(); + + bool LoadPreservedDump(std::string *contents); + void StripSensitiveData(std::string *kernel_dump); + + void GetRamoopsRecordPath(base::FilePath *path, size_t record); + bool LoadParameters(); + bool HasMoreRecords(); + + // Read a record to string, modified from file_utils since that didn't + // provide a way to restrict the read length. + // Return value indicates (only) error state: + // * false when we get an error (can't read from dump location). + // * true if no error occured. + // Not finding a valid record is not an error state and is signaled by the + // record_found output parameter. + bool ReadRecordToString(std::string *contents, + size_t current_record, + bool *record_found); + + void ProcessStackTrace(pcrecpp::StringPiece kernel_dump, + bool print_diagnostics, + unsigned *hash, + float *last_stack_timestamp, + bool *is_watchdog_crash); + bool FindCrashingFunction(pcrecpp::StringPiece kernel_dump, + bool print_diagnostics, + float stack_trace_timestamp, + std::string *crashing_function); + bool FindPanicMessage(pcrecpp::StringPiece kernel_dump, + bool print_diagnostics, + std::string *panic_message); + + // Returns the architecture kind for which we are built. + static ArchKind GetCompilerArch(); + + bool is_enabled_; + base::FilePath ramoops_dump_path_; + size_t records_; + + // The architecture of kernel dump strings we are working with. + ArchKind arch_; + + DISALLOW_COPY_AND_ASSIGN(KernelCollector); +}; + +#endif // CRASH_REPORTER_KERNEL_COLLECTOR_H_ diff --git a/kernel_collector_test.cc b/kernel_collector_test.cc new file mode 100644 index 0000000..60fd832 --- /dev/null +++ b/kernel_collector_test.cc @@ -0,0 +1,679 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernel_collector_test.h" + +#include <unistd.h> + +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <brillo/syslog_logging.h> +#include <gtest/gtest.h> + +using base::FilePath; +using base::StringPrintf; +using brillo::FindLog; +using brillo::GetLog; + +namespace { + +int s_crashes = 0; +bool s_metrics = false; + +void CountCrash() { + ++s_crashes; +} + +bool IsMetrics() { + return s_metrics; +} + +} // namespace + +class KernelCollectorTest : public ::testing::Test { + protected: + void WriteStringToFile(const FilePath &file_path, + const char *data) { + unsigned int numBytesWritten = + base::WriteFile(file_path, data, strlen(data)); + ASSERT_EQ(strlen(data), numBytesWritten); + } + + void SetUpSuccessfulCollect(); + void ComputeKernelStackSignatureCommon(); + + const FilePath &kcrash_file() const { return test_kcrash_; } + const FilePath &test_crash_directory() const { return test_crash_directory_; } + + KernelCollectorMock collector_; + + private: + void SetUp() override { + s_crashes = 0; + s_metrics = true; + + EXPECT_CALL(collector_, SetUpDBus()).WillRepeatedly(testing::Return()); + + collector_.Initialize(CountCrash, IsMetrics); + ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); + test_kcrash_ = scoped_temp_dir_.path().Append("kcrash"); + ASSERT_TRUE(base::CreateDirectory(test_kcrash_)); + collector_.OverridePreservedDumpPath(test_kcrash_); + + test_kcrash_ = test_kcrash_.Append("dmesg-ramoops-0"); + ASSERT_FALSE(base::PathExists(test_kcrash_)); + + test_crash_directory_ = scoped_temp_dir_.path().Append("crash_directory"); + ASSERT_TRUE(base::CreateDirectory(test_crash_directory_)); + brillo::ClearLog(); + } + + FilePath test_kcrash_; + FilePath test_crash_directory_; + base::ScopedTempDir scoped_temp_dir_; +}; + +TEST_F(KernelCollectorTest, ComputeKernelStackSignatureBase) { + // Make sure the normal build architecture is detected + EXPECT_NE(KernelCollector::kArchUnknown, collector_.arch()); +} + +TEST_F(KernelCollectorTest, LoadPreservedDump) { + ASSERT_FALSE(base::PathExists(kcrash_file())); + std::string dump; + dump.clear(); + + WriteStringToFile(kcrash_file(), + "CrashRecordWithoutRamoopsHeader\n<6>[ 0.078852]"); + ASSERT_TRUE(collector_.LoadParameters()); + ASSERT_TRUE(collector_.LoadPreservedDump(&dump)); + ASSERT_EQ("CrashRecordWithoutRamoopsHeader\n<6>[ 0.078852]", dump); + + WriteStringToFile(kcrash_file(), "====1.1\nsomething"); + ASSERT_TRUE(collector_.LoadParameters()); + ASSERT_TRUE(collector_.LoadPreservedDump(&dump)); + ASSERT_EQ("something", dump); + + WriteStringToFile(kcrash_file(), "\x01\x02\xfe\xff random blob"); + ASSERT_TRUE(collector_.LoadParameters()); + ASSERT_FALSE(collector_.LoadPreservedDump(&dump)); + ASSERT_EQ("", dump); +} + +TEST_F(KernelCollectorTest, EnableMissingKernel) { + ASSERT_FALSE(collector_.Enable()); + ASSERT_FALSE(collector_.is_enabled()); + ASSERT_TRUE(FindLog( + "Kernel does not support crash dumping")); + ASSERT_EQ(s_crashes, 0); +} + +TEST_F(KernelCollectorTest, EnableOK) { + WriteStringToFile(kcrash_file(), ""); + EXPECT_CALL(collector_, DumpDirMounted()).WillOnce(::testing::Return(true)); + ASSERT_TRUE(collector_.Enable()); + ASSERT_TRUE(collector_.is_enabled()); + ASSERT_TRUE(FindLog("Enabling kernel crash handling")); + ASSERT_EQ(s_crashes, 0); +} + +TEST_F(KernelCollectorTest, StripSensitiveDataBasic) { + // Basic tests of StripSensitiveData... + + // Make sure we work OK with a string w/ no MAC addresses. + const std::string kCrashWithNoMacsOrig = + "<7>[111566.131728] PM: Entering mem sleep\n"; + std::string crash_with_no_macs(kCrashWithNoMacsOrig); + collector_.StripSensitiveData(&crash_with_no_macs); + EXPECT_EQ(kCrashWithNoMacsOrig, crash_with_no_macs); + + // Make sure that we handle the case where there's nothing before/after the + // MAC address. + const std::string kJustAMacOrig = + "11:22:33:44:55:66"; + const std::string kJustAMacStripped = + "00:00:00:00:00:01"; + std::string just_a_mac(kJustAMacOrig); + collector_.StripSensitiveData(&just_a_mac); + EXPECT_EQ(kJustAMacStripped, just_a_mac); + + // Test MAC addresses crammed together to make sure it gets both of them. + // + // I'm not sure that the code does ideal on these two test cases (they don't + // look like two MAC addresses to me), but since we don't see them I think + // it's OK to behave as shown here. + const std::string kCrammedMacs1Orig = + "11:22:33:44:55:66:11:22:33:44:55:66"; + const std::string kCrammedMacs1Stripped = + "00:00:00:00:00:01:00:00:00:00:00:01"; + std::string crammed_macs_1(kCrammedMacs1Orig); + collector_.StripSensitiveData(&crammed_macs_1); + EXPECT_EQ(kCrammedMacs1Stripped, crammed_macs_1); + + const std::string kCrammedMacs2Orig = + "11:22:33:44:55:6611:22:33:44:55:66"; + const std::string kCrammedMacs2Stripped = + "00:00:00:00:00:0100:00:00:00:00:01"; + std::string crammed_macs_2(kCrammedMacs2Orig); + collector_.StripSensitiveData(&crammed_macs_2); + EXPECT_EQ(kCrammedMacs2Stripped, crammed_macs_2); + + // Test case-sensitiveness (we shouldn't be case-senstive). + const std::string kCapsMacOrig = + "AA:BB:CC:DD:EE:FF"; + const std::string kCapsMacStripped = + "00:00:00:00:00:01"; + std::string caps_mac(kCapsMacOrig); + collector_.StripSensitiveData(&caps_mac); + EXPECT_EQ(kCapsMacStripped, caps_mac); + + const std::string kLowerMacOrig = + "aa:bb:cc:dd:ee:ff"; + const std::string kLowerMacStripped = + "00:00:00:00:00:01"; + std::string lower_mac(kLowerMacOrig); + collector_.StripSensitiveData(&lower_mac); + EXPECT_EQ(kLowerMacStripped, lower_mac); +} + +TEST_F(KernelCollectorTest, StripSensitiveDataBulk) { + // Test calling StripSensitiveData w/ lots of MAC addresses in the "log". + + // Test that stripping code handles more than 256 unique MAC addresses, since + // that overflows past the last byte... + // We'll write up some code that generates 258 unique MAC addresses. Sorta + // cheating since the code is very similar to the current code in + // StripSensitiveData(), but would catch if someone changed that later. + std::string lotsa_macs_orig; + std::string lotsa_macs_stripped; + int i; + for (i = 0; i < 258; i++) { + lotsa_macs_orig += StringPrintf(" 11:11:11:11:%02X:%02x", + (i & 0xff00) >> 8, i & 0x00ff); + lotsa_macs_stripped += StringPrintf(" 00:00:00:00:%02X:%02x", + ((i+1) & 0xff00) >> 8, (i+1) & 0x00ff); + } + std::string lotsa_macs(lotsa_macs_orig); + collector_.StripSensitiveData(&lotsa_macs); + EXPECT_EQ(lotsa_macs_stripped, lotsa_macs); +} + +TEST_F(KernelCollectorTest, StripSensitiveDataSample) { + // Test calling StripSensitiveData w/ some actual lines from a real crash; + // included two MAC addresses (though replaced them with some bogusness). + const std::string kCrashWithMacsOrig = + "<6>[111567.195339] ata1.00: ACPI cmd ef/10:03:00:00:00:a0 (SET FEATURES)" + " filtered out\n" + "<7>[108539.540144] wlan0: authenticate with 11:22:33:44:55:66 (try 1)\n" + "<7>[108539.554973] wlan0: associate with 11:22:33:44:55:66 (try 1)\n" + "<6>[110136.587583] usb0: register 'QCUSBNet2k' at usb-0000:00:1d.7-2," + " QCUSBNet Ethernet Device, 99:88:77:66:55:44\n" + "<7>[110964.314648] wlan0: deauthenticated from 11:22:33:44:55:66" + " (Reason: 6)\n" + "<7>[110964.325057] phy0: Removed STA 11:22:33:44:55:66\n" + "<7>[110964.325115] phy0: Destroyed STA 11:22:33:44:55:66\n" + "<6>[110969.219172] usb0: register 'QCUSBNet2k' at usb-0000:00:1d.7-2," + " QCUSBNet Ethernet Device, 99:88:77:66:55:44\n" + "<7>[111566.131728] PM: Entering mem sleep\n"; + const std::string kCrashWithMacsStripped = + "<6>[111567.195339] ata1.00: ACPI cmd ef/10:03:00:00:00:a0 (SET FEATURES)" + " filtered out\n" + "<7>[108539.540144] wlan0: authenticate with 00:00:00:00:00:01 (try 1)\n" + "<7>[108539.554973] wlan0: associate with 00:00:00:00:00:01 (try 1)\n" + "<6>[110136.587583] usb0: register 'QCUSBNet2k' at usb-0000:00:1d.7-2," + " QCUSBNet Ethernet Device, 00:00:00:00:00:02\n" + "<7>[110964.314648] wlan0: deauthenticated from 00:00:00:00:00:01" + " (Reason: 6)\n" + "<7>[110964.325057] phy0: Removed STA 00:00:00:00:00:01\n" + "<7>[110964.325115] phy0: Destroyed STA 00:00:00:00:00:01\n" + "<6>[110969.219172] usb0: register 'QCUSBNet2k' at usb-0000:00:1d.7-2," + " QCUSBNet Ethernet Device, 00:00:00:00:00:02\n" + "<7>[111566.131728] PM: Entering mem sleep\n"; + std::string crash_with_macs(kCrashWithMacsOrig); + collector_.StripSensitiveData(&crash_with_macs); + EXPECT_EQ(kCrashWithMacsStripped, crash_with_macs); +} + +TEST_F(KernelCollectorTest, CollectPreservedFileMissing) { + ASSERT_FALSE(collector_.Collect()); + ASSERT_FALSE(FindLog("Stored kcrash to ")); + ASSERT_EQ(0, s_crashes); +} + +void KernelCollectorTest::SetUpSuccessfulCollect() { + collector_.ForceCrashDirectory(test_crash_directory()); + WriteStringToFile(kcrash_file(), "====1.1\nsomething"); + ASSERT_EQ(0, s_crashes); +} + +TEST_F(KernelCollectorTest, CollectOptedOut) { + SetUpSuccessfulCollect(); + s_metrics = false; + ASSERT_TRUE(collector_.Collect()); + ASSERT_TRUE(FindLog("(ignoring - no consent)")); + ASSERT_EQ(0, s_crashes); +} + +TEST_F(KernelCollectorTest, CollectOK) { + SetUpSuccessfulCollect(); + ASSERT_TRUE(collector_.Collect()); + ASSERT_EQ(1, s_crashes); + ASSERT_TRUE(FindLog("(handling)")); + static const char kNamePrefix[] = "Stored kcrash to "; + std::string log = brillo::GetLog(); + size_t pos = log.find(kNamePrefix); + ASSERT_NE(std::string::npos, pos) + << "Did not find string \"" << kNamePrefix << "\" in log: {\n" + << log << "}"; + pos += strlen(kNamePrefix); + std::string filename = log.substr(pos, std::string::npos); + // Take the name up until \n + size_t end_pos = filename.find_first_of("\n"); + ASSERT_NE(std::string::npos, end_pos); + filename = filename.substr(0, end_pos); + ASSERT_EQ(0U, filename.find(test_crash_directory().value())); + ASSERT_TRUE(base::PathExists(FilePath(filename))); + std::string contents; + ASSERT_TRUE(base::ReadFileToString(FilePath(filename), &contents)); + ASSERT_EQ("something", contents); +} + +// Perform tests which are common across architectures +void KernelCollectorTest::ComputeKernelStackSignatureCommon() { + std::string signature; + + const char kStackButNoPC[] = + "<4>[ 6066.829029] [<790340af>] __do_softirq+0xa6/0x143\n"; + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kStackButNoPC, &signature, false)); + EXPECT_EQ("kernel--83615F0A", signature); + + const char kMissingEverything[] = + "<4>[ 6066.829029] [<790340af>] ? __do_softirq+0xa6/0x143\n"; + EXPECT_FALSE( + collector_.ComputeKernelStackSignature(kMissingEverything, + &signature, + false)); + + // Long message. + const char kTruncatedMessage[] = + "<0>[ 87.485611] Kernel panic - not syncing: 01234567890123456789" + "01234567890123456789X\n"; + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kTruncatedMessage, + &signature, + false)); + EXPECT_EQ("kernel-0123456789012345678901234567890123456789-00000000", + signature); +} + +TEST_F(KernelCollectorTest, ComputeKernelStackSignatureARM) { + const char kBugToPanic[] = + "<5>[ 123.412524] Modules linked in:\n" + "<5>[ 123.412534] CPU: 0 Tainted: G W " + "(2.6.37-01030-g51cee64 #153)\n" + "<5>[ 123.412552] PC is at write_breakme+0xd0/0x1b4\n" + "<5>[ 123.412560] LR is at write_breakme+0xc8/0x1b4\n" + "<5>[ 123.412569] pc : [<c0058220>] lr : [<c005821c>] " + "psr: 60000013\n" + "<5>[ 123.412574] sp : f4e0ded8 ip : c04d104c fp : 000e45e0\n" + "<5>[ 123.412581] r10: 400ff000 r9 : f4e0c000 r8 : 00000004\n" + "<5>[ 123.412589] r7 : f4e0df80 r6 : f4820c80 r5 : 00000004 " + "r4 : f4e0dee8\n" + "<5>[ 123.412598] r3 : 00000000 r2 : f4e0decc r1 : c05f88a9 " + "r0 : 00000039\n" + "<5>[ 123.412608] Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA " + "ARM Segment user\n" + "<5>[ 123.412617] Control: 10c53c7d Table: 34dcc04a DAC: 00000015\n" + "<0>[ 123.412626] Process bash (pid: 1014, stack limit = 0xf4e0c2f8)\n" + "<0>[ 123.412634] Stack: (0xf4e0ded8 to 0xf4e0e000)\n" + "<0>[ 123.412641] dec0: " + " f4e0dee8 c0183678\n" + "<0>[ 123.412654] dee0: 00000000 00000000 00677562 0000081f c06a6a78 " + "400ff000 f4e0dfb0 00000000\n" + "<0>[ 123.412666] df00: bec7ab44 000b1719 bec7ab0c c004f498 bec7a314 " + "c024acc8 00000001 c018359c\n" + "<0>[ 123.412679] df20: f4e0df34 c04d10fc f5803c80 271beb39 000e45e0 " + "f5803c80 c018359c c017bfe0\n" + "<0>[ 123.412691] df40: 00000004 f4820c80 400ff000 f4e0df80 00000004 " + "f4e0c000 00000000 c01383e4\n" + "<0>[ 123.412703] df60: f4820c80 400ff000 f4820c80 400ff000 00000000 " + "00000000 00000004 c0138578\n" + "<0>[ 123.412715] df80: 00000000 00000000 00000004 00000000 00000004 " + "402f95d0 00000004 00000004\n" + "<0>[ 123.412727] dfa0: c0054984 c00547c0 00000004 402f95d0 00000001 " + "400ff000 00000004 00000000\n" + "<0>[ 123.412739] dfc0: 00000004 402f95d0 00000004 00000004 400ff000 " + "000c194c bec7ab58 000e45e0\n" + "<0>[ 123.412751] dfe0: 00000000 bec7aad8 40232520 40284e9c 60000010 " + "00000001 00000000 00000000\n" + "<5>[ 39.496577] Backtrace:\n" + "<5>[ 123.412782] [<c0058220>] (__bug+0x20/0x2c) from [<c0183678>] " + "(write_breakme+0xdc/0x1bc)\n" + "<5>[ 123.412798] [<c0183678>] (write_breakme+0xdc/0x1bc) from " + "[<c017bfe0>] (proc_reg_write+0x88/0x9c)\n"; + std::string signature; + + collector_.set_arch(KernelCollector::kArchArm); + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kBugToPanic, &signature, false)); + EXPECT_EQ("kernel-write_breakme-97D3E92F", signature); + + ComputeKernelStackSignatureCommon(); +} + +TEST_F(KernelCollectorTest, ComputeKernelStackSignatureMIPS) { + const char kBugToPanic[] = + "<5>[ 3378.472000] lkdtm: Performing direct entry BUG\n" + "<5>[ 3378.476000] Kernel bug detected[#1]:\n" + "<5>[ 3378.484000] CPU: 0 PID: 185 Comm: dash Not tainted 3.14.0 #1\n" + "<5>[ 3378.488000] task: 8fed5220 ti: 8ec4a000 task.ti: 8ec4a000\n" + "<5>[ 3378.496000] $ 0 : 00000000 804018b8 804010f0 7785b507\n" + "<5>[ 3378.500000] $ 4 : 8061ab64 81204478 81205b20 00000000\n" + "<5>[ 3378.508000] $ 8 : 80830000 20746365 72746e65 55422079\n" + "<5>[ 3378.512000] $12 : 8ec4be94 000000fc 00000000 00000048\n" + "<5>[ 3378.520000] $16 : 00000004 8ef54000 80710000 00000002\n" + "<5>[ 3378.528000] $20 : 7765b6d4 00000004 7fffffff 00000002\n" + "<5>[ 3378.532000] $24 : 00000001 803dc0dc \n" + "<5>[ 3378.540000] $28 : 8ec4a000 8ec4be20 7775438d 804018b8\n" + "<5>[ 3378.544000] Hi : 00000000\n" + "<5>[ 3378.548000] Lo : 49bf8080\n" + "<5>[ 3378.552000] epc : 804010f0 lkdtm_do_action+0x68/0x3f8\n" + "<5>[ 3378.560000] Not tainted\n" + "<5>[ 3378.564000] ra : 804018b8 direct_entry+0x110/0x154\n" + "<5>[ 3378.568000] Status: 3100dc03 KERNEL EXL IE \n" + "<5>[ 3378.572000] Cause : 10800024\n" + "<5>[ 3378.576000] PrId : 0001a120 (MIPS interAptiv (multi))\n" + "<5>[ 3378.580000] Modules linked in: uinput cfg80211 nf_conntrack_ipv6 " + "nf_defrag_ipv6 ip6table_filter ip6_tables pcnet32 mii fuse " + "ppp_async ppp_generic slhc tun\n" + "<5>[ 3378.600000] Process dash (pid: 185, threadinfo=8ec4a000, " + "task=8fed5220, tls=77632490)\n" + "<5>[ 3378.608000] Stack : 00000006 ffffff9c 00000000 00000000 00000000 " + "00000000 8083454a 00000022\n" + "<5> 7765baa1 00001fee 80710000 8ef54000 8ec4bf08 00000002 " + "7765b6d4 00000004\n" + "<5> 7fffffff 00000002 7775438d 805e5158 7fffffff 00000002 " + "00000000 7785b507\n" + "<5> 806a96bc 00000004 8ef54000 8ec4bf08 00000002 804018b8 " + "80710000 806a98bc\n" + "<5> 00000002 00000020 00000004 8d515600 77756450 00000004 " + "8ec4bf08 802377e4\n" + "<5> ...\n" + "<5>[ 3378.652000] Call Trace:\n" + "<5>[ 3378.656000] [<804010f0>] lkdtm_do_action+0x68/0x3f8\n" + "<5>[ 3378.660000] [<804018b8>] direct_entry+0x110/0x154\n" + "<5>[ 3378.664000] [<802377e4>] vfs_write+0xe0/0x1bc\n" + "<5>[ 3378.672000] [<80237f90>] SyS_write+0x78/0xf8\n" + "<5>[ 3378.676000] [<80111888>] handle_sys+0x128/0x14c\n" + "<5>[ 3378.680000] \n" + "<5>[ 3378.684000] \n" + "<5>Code: 3c04806b 0c1793aa 248494f0 <000c000d> 3c04806b 248494fc " + "0c04cc7f 2405017a 08100514 \n" + "<5>[ 3378.696000] ---[ end trace 75067432f24bbc93 ]---\n"; + std::string signature; + + collector_.set_arch(KernelCollector::kArchMips); + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kBugToPanic, &signature, false)); + EXPECT_EQ("kernel-lkdtm_do_action-5E600A6B", signature); + + ComputeKernelStackSignatureCommon(); +} + +TEST_F(KernelCollectorTest, ComputeKernelStackSignatureX86) { + const char kBugToPanic[] = + "<4>[ 6066.829029] [<79039d16>] ? run_timer_softirq+0x165/0x1e6\n" + "<4>[ 6066.829029] [<790340af>] ignore_old_stack+0xa6/0x143\n" + "<0>[ 6066.829029] EIP: [<b82d7c15>] ieee80211_stop_tx_ba_session+" + "0xa3/0xb5 [mac80211] SS:ESP 0068:7951febc\n" + "<0>[ 6066.829029] CR2: 00000000323038a7\n" + "<4>[ 6066.845422] ---[ end trace 12b058bb46c43500 ]---\n" + "<0>[ 6066.845747] Kernel panic - not syncing: Fatal exception " + "in interrupt\n" + "<0>[ 6066.846902] Call Trace:\n" + "<4>[ 6066.846902] [<7937a07b>] ? printk+0x14/0x19\n" + "<4>[ 6066.949779] [<79379fc1>] panic+0x3e/0xe4\n" + "<4>[ 6066.949971] [<7937c5c5>] oops_end+0x73/0x81\n" + "<4>[ 6066.950208] [<7901b260>] no_context+0x10d/0x117\n"; + std::string signature; + + collector_.set_arch(KernelCollector::kArchX86); + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kBugToPanic, &signature, false)); + EXPECT_EQ("kernel-ieee80211_stop_tx_ba_session-DE253569", signature); + + const char kPCButNoStack[] = + "<0>[ 6066.829029] EIP: [<b82d7c15>] ieee80211_stop_tx_ba_session+"; + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kPCButNoStack, &signature, false)); + EXPECT_EQ("kernel-ieee80211_stop_tx_ba_session-00000000", signature); + + const char kBreakmeBug[] = + "<4>[ 180.492137] [<790970c6>] ? handle_mm_fault+0x67f/0x96d\n" + "<4>[ 180.492137] [<790dcdfe>] ? proc_reg_write+0x5f/0x73\n" + "<4>[ 180.492137] [<790e2224>] ? write_breakme+0x0/0x108\n" + "<4>[ 180.492137] [<790dcd9f>] ? proc_reg_write+0x0/0x73\n" + "<4>[ 180.492137] [<790ac0aa>] vfs_write+0x85/0xe4\n" + "<0>[ 180.492137] Code: c6 44 05 b2 00 89 d8 e8 0c ef 09 00 85 c0 75 " + "0b c7 00 00 00 00 00 e9 8e 00 00 00 ba e6 75 4b 79 89 d8 e8 f1 ee 09 " + "00 85 c0 75 04 <0f> 0b eb fe ba 58 47 49 79 89 d8 e8 dd ee 09 00 85 " + "c0 75 0a 68\n" + "<0>[ 180.492137] EIP: [<790e22a4>] write_breakme+0x80/0x108 SS:ESP " + "0068:aa3e9efc\n" + "<4>[ 180.501800] ---[ end trace 2a6b72965e1b1523 ]---\n" + "<0>[ 180.502026] Kernel panic - not syncing: Fatal exception\n" + "<4>[ 180.502026] Call Trace:\n" + "<4>[ 180.502806] [<79379aba>] ? printk+0x14/0x1a\n" + "<4>[ 180.503033] [<79379a00>] panic+0x3e/0xe4\n" + "<4>[ 180.503287] [<7937c005>] oops_end+0x73/0x81\n" + "<4>[ 180.503520] [<790055dd>] die+0x58/0x5e\n" + "<4>[ 180.503538] [<7937b96c>] do_trap+0x8e/0xa7\n" + "<4>[ 180.503555] [<79003d70>] ? do_invalid_op+0x0/0x80\n"; + + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kBreakmeBug, &signature, false)); + EXPECT_EQ("kernel-write_breakme-122AB3CD", signature); + + const char kPCLineTooOld[] = + "<4>[ 174.492137] [<790970c6>] ignored_function+0x67f/0x96d\n" + "<4>[ 175.492137] [<790970c6>] ignored_function2+0x67f/0x96d\n" + "<0>[ 174.492137] EIP: [<790e22a4>] write_breakme+0x80/0x108 SS:ESP " + "0068:aa3e9efc\n" + "<4>[ 180.501800] ---[ end trace 2a6b72965e1b1523 ]---\n" + "<4>[ 180.502026] Call Trace:\n" + "<0>[ 180.502026] Kernel panic - not syncing: Fatal exception\n" + "<4>[ 180.502806] [<79379aba>] printk+0x14/0x1a\n"; + + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kPCLineTooOld, &signature, false)); + EXPECT_EQ("kernel-Fatal exception-ED4C84FE", signature); + + // Panic without EIP line. + const char kExamplePanicOnly[] = + "<0>[ 87.485611] Kernel panic - not syncing: Testing panic\n" + "<4>[ 87.485630] Pid: 2825, comm: bash Tainted: G " + "C 2.6.32.23+drm33.10 #1\n" + "<4>[ 87.485639] Call Trace:\n" + "<4>[ 87.485660] [<8133f71d>] ? printk+0x14/0x17\n" + "<4>[ 87.485674] [<8133f663>] panic+0x3e/0xe4\n" + "<4>[ 87.485689] [<810d062e>] write_breakme+0xaa/0x124\n"; + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kExamplePanicOnly, + &signature, + false)); + EXPECT_EQ("kernel-Testing panic-E0FC3552", signature); + + // Panic from hung task. + const char kHungTaskBreakMe[] = + "<3>[ 720.459157] INFO: task bash:2287 blocked blah blah\n" + "<5>[ 720.459282] Call Trace:\n" + "<5>[ 720.459307] [<810a457b>] ? __dentry_open+0x186/0x23e\n" + "<5>[ 720.459323] [<810b9c71>] ? mntput_no_expire+0x29/0xe2\n" + "<5>[ 720.459336] [<810b9d48>] ? mntput+0x1e/0x20\n" + "<5>[ 720.459350] [<810ad135>] ? path_put+0x1a/0x1d\n" + "<5>[ 720.459366] [<8137cacc>] schedule+0x4d/0x4f\n" + "<5>[ 720.459379] [<8137ccfb>] schedule_timeout+0x26/0xaf\n" + "<5>[ 720.459394] [<8102127e>] ? should_resched+0xd/0x27\n" + "<5>[ 720.459409] [<81174d1f>] ? _copy_from_user+0x3c/0x50\n" + "<5>[ 720.459423] [<8137cd9e>] " + "schedule_timeout_uninterruptible+0x1a/0x1c\n" + "<5>[ 720.459438] [<810dee63>] write_breakme+0xb3/0x178\n" + "<5>[ 720.459453] [<810dedb0>] ? meminfo_proc_show+0x2f2/0x2f2\n" + "<5>[ 720.459467] [<810d94ae>] proc_reg_write+0x6d/0x87\n" + "<5>[ 720.459481] [<810d9441>] ? proc_reg_poll+0x76/0x76\n" + "<5>[ 720.459493] [<810a5e9e>] vfs_write+0x79/0xa5\n" + "<5>[ 720.459505] [<810a6011>] sys_write+0x40/0x65\n" + "<5>[ 720.459519] [<8137e677>] sysenter_do_call+0x12/0x26\n" + "<0>[ 720.459530] Kernel panic - not syncing: hung_task: blocked tasks\n" + "<5>[ 720.459768] Pid: 31, comm: khungtaskd Tainted: " + "G C 3.0.8 #1\n" + "<5>[ 720.459998] Call Trace:\n" + "<5>[ 720.460140] [<81378a35>] panic+0x53/0x14a\n" + "<5>[ 720.460312] [<8105f875>] watchdog+0x15b/0x1a0\n" + "<5>[ 720.460495] [<8105f71a>] ? hung_task_panic+0x16/0x16\n" + "<5>[ 720.460693] [<81043af3>] kthread+0x67/0x6c\n" + "<5>[ 720.460862] [<81043a8c>] ? __init_kthread_worker+0x2d/0x2d\n" + "<5>[ 720.461106] [<8137eb9e>] kernel_thread_helper+0x6/0x10\n"; + + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kHungTaskBreakMe, + &signature, + false)); + + EXPECT_EQ("kernel-(HANG)-hung_task: blocked tasks-600B37EA", signature); + + // Panic with all question marks in the last stack trace. + const char kUncertainStackTrace[] = + "<0>[56279.689669] ------------[ cut here ]------------\n" + "<2>[56279.689677] kernel BUG at /build/x86-alex/tmp/portage/" + "sys-kernel/chromeos-kernel-0.0.1-r516/work/chromeos-kernel-0.0.1/" + "kernel/timer.c:844!\n" + "<0>[56279.689683] invalid opcode: 0000 [#1] SMP \n" + "<0>[56279.689688] last sysfs file: /sys/power/state\n" + "<5>[56279.689692] Modules linked in: nls_iso8859_1 nls_cp437 vfat fat " + "gobi usbnet tsl2583(C) industrialio(C) snd_hda_codec_realtek " + "snd_hda_intel i2c_dev snd_hda_codec snd_hwdep qcserial snd_pcm usb_wwan " + "i2c_i801 snd_timer nm10_gpio snd_page_alloc rtc_cmos fuse " + "nf_conntrack_ipv6 nf_defrag_ipv6 uvcvideo videodev ip6table_filter " + "ath9k ip6_tables ipv6 mac80211 ath9k_common ath9k_hw ath cfg80211 " + "xt_mark\n" + "<5>[56279.689731] \n" + "<5>[56279.689738] Pid: 24607, comm: powerd_suspend Tainted: G " + "WC 2.6.38.3+ #1 SAMSUNG ELECTRONICS CO., LTD. Alex/G100 \n" + "<5>[56279.689748] EIP: 0060:[<8103e3ea>] EFLAGS: 00210286 CPU: 3\n" + "<5>[56279.689758] EIP is at add_timer+0xd/0x1b\n" + "<5>[56279.689762] EAX: f5e00684 EBX: f5e003c0 ECX: 00000002 EDX: " + "00200246\n" + "<5>[56279.689767] ESI: f5e003c0 EDI: d28bc03c EBP: d2be5e40 ESP: " + "d2be5e40\n" + "<5>[56279.689772] DS: 007b ES: 007b FS: 00d8 GS: 00e0 SS: 0068\n" + "<0>[56279.689778] Process powerd_suspend (pid: 24607, ti=d2be4000 " + "task=f5dc9b60 task.ti=d2be4000)\n" + "<0>[56279.689782] Stack:\n" + "<5>[56279.689785] d2be5e4c f8dccced f4ac02c0 d2be5e70 f8ddc752 " + "f5e003c0 f4ac0458 f4ac092c\n" + "<5>[56279.689797] f4ac043c f4ac02c0 f4ac0000 f4ac007c d2be5e7c " + "f8dd4a33 f4ac0164 d2be5e94\n" + "<5>[56279.689809] f87e0304 f69ff0cc f4ac0164 f87e02a4 f4ac0164 " + "d2be5eb0 81248968 00000000\n" + "<0>[56279.689821] Call Trace:\n" + "<5>[56279.689840] [<f8dccced>] ieee80211_sta_restart+0x25/0x8c " + "[mac80211]\n" + "<5>[56279.689854] [<f8ddc752>] ieee80211_reconfig+0x2e9/0x339 " + "[mac80211]\n" + "<5>[56279.689869] [<f8dd4a33>] ieee80211_aes_cmac+0x182d/0x184e " + "[mac80211]\n" + "<5>[56279.689883] [<f87e0304>] cfg80211_get_dev_from_info+0x29b/0x2c0 " + "[cfg80211]\n" + "<5>[56279.689895] [<f87e02a4>] ? " + "cfg80211_get_dev_from_info+0x23b/0x2c0 [cfg80211]\n" + "<5>[56279.689904] [<81248968>] legacy_resume+0x25/0x5d\n" + "<5>[56279.689910] [<812490ae>] device_resume+0xdd/0x110\n" + "<5>[56279.689917] [<812491c2>] dpm_resume_end+0xe1/0x271\n" + "<5>[56279.689925] [<81060481>] suspend_devices_and_enter+0x18b/0x1de\n" + "<5>[56279.689932] [<810605ba>] enter_state+0xe6/0x132\n" + "<5>[56279.689939] [<8105fd4b>] state_store+0x91/0x9d\n" + "<5>[56279.689945] [<8105fcba>] ? state_store+0x0/0x9d\n" + "<5>[56279.689953] [<81178fb1>] kobj_attr_store+0x16/0x22\n" + "<5>[56279.689961] [<810eea5e>] sysfs_write_file+0xc1/0xec\n" + "<5>[56279.689969] [<810af443>] vfs_write+0x8f/0x101\n" + "<5>[56279.689975] [<810ee99d>] ? sysfs_write_file+0x0/0xec\n" + "<5>[56279.689982] [<810af556>] sys_write+0x40/0x65\n" + "<5>[56279.689989] [<81002d57>] sysenter_do_call+0x12/0x26\n" + "<0>[56279.689993] Code: c1 d3 e2 4a 89 55 f4 f7 d2 21 f2 6a 00 31 c9 89 " + "d8 e8 6e fd ff ff 5a 8d 65 f8 5b 5e 5d c3 55 89 e5 3e 8d 74 26 00 83 38 " + "00 74 04 <0f> 0b eb fe 8b 50 08 e8 6f ff ff ff 5d c3 55 89 e5 3e 8d 74 " + "26 \n" + "<0>[56279.690009] EIP: [<8103e3ea>] add_timer+0xd/0x1b SS:ESP " + "0068:d2be5e40\n" + "<4>[56279.690113] ---[ end trace b71141bb67c6032a ]---\n" + "<7>[56279.694069] wlan0: deauthenticated from 00:00:00:00:00:01 " + "(Reason: 6)\n" + "<0>[56279.703465] Kernel panic - not syncing: Fatal exception\n" + "<5>[56279.703471] Pid: 24607, comm: powerd_suspend Tainted: G D " + "WC 2.6.38.3+ #1\n" + "<5>[56279.703475] Call Trace:\n" + "<5>[56279.703483] [<8136648c>] ? panic+0x55/0x152\n" + "<5>[56279.703491] [<810057fa>] ? oops_end+0x73/0x81\n" + "<5>[56279.703497] [<81005a44>] ? die+0xed/0xf5\n" + "<5>[56279.703503] [<810033cb>] ? do_trap+0x7a/0x80\n" + "<5>[56279.703509] [<8100369b>] ? do_invalid_op+0x0/0x80\n" + "<5>[56279.703515] [<81003711>] ? do_invalid_op+0x76/0x80\n" + "<5>[56279.703522] [<8103e3ea>] ? add_timer+0xd/0x1b\n" + "<5>[56279.703529] [<81025e23>] ? check_preempt_curr+0x2e/0x69\n" + "<5>[56279.703536] [<8102ef28>] ? ttwu_post_activation+0x5a/0x11b\n" + "<5>[56279.703543] [<8102fa8d>] ? try_to_wake_up+0x213/0x21d\n" + "<5>[56279.703550] [<81368b7f>] ? error_code+0x67/0x6c\n" + "<5>[56279.703557] [<8103e3ea>] ? add_timer+0xd/0x1b\n" + "<5>[56279.703577] [<f8dccced>] ? ieee80211_sta_restart+0x25/0x8c " + "[mac80211]\n" + "<5>[56279.703591] [<f8ddc752>] ? ieee80211_reconfig+0x2e9/0x339 " + "[mac80211]\n" + "<5>[56279.703605] [<f8dd4a33>] ? ieee80211_aes_cmac+0x182d/0x184e " + "[mac80211]\n" + "<5>[56279.703618] [<f87e0304>] ? " + "cfg80211_get_dev_from_info+0x29b/0x2c0 [cfg80211]\n" + "<5>[56279.703630] [<f87e02a4>] ? " + "cfg80211_get_dev_from_info+0x23b/0x2c0 [cfg80211]\n" + "<5>[56279.703637] [<81248968>] ? legacy_resume+0x25/0x5d\n" + "<5>[56279.703643] [<812490ae>] ? device_resume+0xdd/0x110\n" + "<5>[56279.703649] [<812491c2>] ? dpm_resume_end+0xe1/0x271\n" + "<5>[56279.703657] [<81060481>] ? " + "suspend_devices_and_enter+0x18b/0x1de\n" + "<5>[56279.703663] [<810605ba>] ? enter_state+0xe6/0x132\n" + "<5>[56279.703670] [<8105fd4b>] ? state_store+0x91/0x9d\n" + "<5>[56279.703676] [<8105fcba>] ? state_store+0x0/0x9d\n" + "<5>[56279.703683] [<81178fb1>] ? kobj_attr_store+0x16/0x22\n" + "<5>[56279.703690] [<810eea5e>] ? sysfs_write_file+0xc1/0xec\n" + "<5>[56279.703697] [<810af443>] ? vfs_write+0x8f/0x101\n" + "<5>[56279.703703] [<810ee99d>] ? sysfs_write_file+0x0/0xec\n" + "<5>[56279.703709] [<810af556>] ? sys_write+0x40/0x65\n" + "<5>[56279.703716] [<81002d57>] ? sysenter_do_call+0x12/0x26\n"; + + EXPECT_TRUE( + collector_.ComputeKernelStackSignature(kUncertainStackTrace, + &signature, + false)); + // The first trace contains only uncertain entries and its hash is 00000000, + // so, if we used that, the signature would be kernel-add_timer-00000000. + // Instead we use the second-to-last trace for the hash. + EXPECT_EQ("kernel-add_timer-B5178878", signature); + + ComputeKernelStackSignatureCommon(); +} diff --git a/kernel_collector_test.h b/kernel_collector_test.h new file mode 100644 index 0000000..f689e7d --- /dev/null +++ b/kernel_collector_test.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_KERNEL_COLLECTOR_TEST_H_ +#define CRASH_REPORTER_KERNEL_COLLECTOR_TEST_H_ + +#include "kernel_collector.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +class KernelCollectorMock : public KernelCollector { + public: + MOCK_METHOD0(DumpDirMounted, bool()); + MOCK_METHOD0(SetUpDBus, void()); +}; + +#endif // CRASH_REPORTER_KERNEL_COLLECTOR_TEST_H_ diff --git a/kernel_log_collector.sh b/kernel_log_collector.sh new file mode 100644 index 0000000..82512c2 --- /dev/null +++ b/kernel_log_collector.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Usage example: "kernel_log_collector.sh XXX YYY" +# This script searches logs in the /var/log/messages which have the keyword XXX. +# And only those logs which are within the last YYY seconds of the latest log +# that has the keyword XXX are printed. + +# Kernel log has the possible formats: +# 2013-06-14T16:31:40.514513-07:00 localhost kernel: [ 2.682472] MSG MSG ... +# 2013-06-19T20:38:58.661826+00:00 localhost kernel: [ 1.668092] MSG MSG ... + +search_key=$1 +time_duration=$2 +msg_pattern="^[0-9-]*T[0-9:.+-]* localhost kernel" + +die() { + echo "kernel_log_collector: $*" >&2 + exit 1 +} + +get_timestamp() { + timestamp="$(echo $1 | cut -d " " -f 1)" + timestamp="$(date -d "${timestamp}" +%s)" || exit $? + echo "${timestamp}" +} + +last_line=$(grep "${msg_pattern}" /var/log/messages | grep -- "${search_key}" | tail -n 1) + +if [ -n "${last_line}" ]; then + if ! allowed_timestamp=$(get_timestamp "${last_line}"); then + die "coule not get timestamp from: ${last_line}" + fi + : $(( allowed_timestamp -= ${time_duration} )) + grep "${msg_pattern}" /var/log/messages | grep -- "${search_key}" | while read line; do + if ! timestamp=$(get_timestamp "${line}"); then + die "could not get timestamp from: ${line}" + fi + if [ ${timestamp} -gt ${allowed_timestamp} ]; then + echo "${line}" + fi + done +fi + +echo "END-OF-LOG" + diff --git a/kernel_warning_collector.cc b/kernel_warning_collector.cc new file mode 100644 index 0000000..e28e8fd --- /dev/null +++ b/kernel_warning_collector.cc @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kernel_warning_collector.h" + +#include <base/files/file_util.h> +#include <base/logging.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> + +namespace { +const char kExecName[] = "kernel-warning"; +const char kKernelWarningSignatureKey[] = "sig"; +const char kKernelWarningPath[] = "/var/run/kwarn/warning"; +const pid_t kKernelPid = 0; +const uid_t kRootUid = 0; +} // namespace + +using base::FilePath; +using base::StringPrintf; + +KernelWarningCollector::KernelWarningCollector() { +} + +KernelWarningCollector::~KernelWarningCollector() { +} + +bool KernelWarningCollector::LoadKernelWarning(std::string *content, + std::string *signature) { + FilePath kernel_warning_path(kKernelWarningPath); + if (!base::ReadFileToString(kernel_warning_path, content)) { + LOG(ERROR) << "Could not open " << kKernelWarningPath; + return false; + } + // The signature is in the first line. + std::string::size_type end_position = content->find('\n'); + if (end_position == std::string::npos) { + LOG(ERROR) << "unexpected kernel warning format"; + return false; + } + *signature = content->substr(0, end_position); + return true; +} + +bool KernelWarningCollector::Collect() { + std::string reason = "normal collection"; + bool feedback = true; + if (IsDeveloperImage()) { + reason = "always collect from developer builds"; + feedback = true; + } else if (!is_feedback_allowed_function_()) { + reason = "no user consent"; + feedback = false; + } + + LOG(INFO) << "Processing kernel warning: " << reason; + + if (!feedback) { + return true; + } + + std::string kernel_warning; + std::string warning_signature; + if (!LoadKernelWarning(&kernel_warning, &warning_signature)) { + return true; + } + + FilePath root_crash_directory; + if (!GetCreatedCrashDirectoryByEuid(kRootUid, &root_crash_directory, + nullptr)) { + return true; + } + + std::string dump_basename = + FormatDumpBasename(kExecName, time(nullptr), kKernelPid); + FilePath kernel_crash_path = root_crash_directory.Append( + StringPrintf("%s.kcrash", dump_basename.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(kernel_crash_path, + kernel_warning.data(), + kernel_warning.length()) != + static_cast<int>(kernel_warning.length())) { + LOG(INFO) << "Failed to write kernel warning to " + << kernel_crash_path.value().c_str(); + return true; + } + + AddCrashMetaData(kKernelWarningSignatureKey, warning_signature); + WriteCrashMetaData( + root_crash_directory.Append( + StringPrintf("%s.meta", dump_basename.c_str())), + kExecName, kernel_crash_path.value()); + + LOG(INFO) << "Stored kernel warning into " << kernel_crash_path.value(); + return true; +} diff --git a/kernel_warning_collector.h b/kernel_warning_collector.h new file mode 100644 index 0000000..5ccb780 --- /dev/null +++ b/kernel_warning_collector.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_KERNEL_WARNING_COLLECTOR_H_ +#define CRASH_REPORTER_KERNEL_WARNING_COLLECTOR_H_ + +#include <string> + +#include <base/macros.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "crash_collector.h" + +// Kernel warning collector. +class KernelWarningCollector : public CrashCollector { + public: + KernelWarningCollector(); + + ~KernelWarningCollector() override; + + // Collects warning. + bool Collect(); + + private: + friend class KernelWarningCollectorTest; + FRIEND_TEST(KernelWarningCollectorTest, CollectOK); + + // Reads the full content of the kernel warn dump and its signature. + bool LoadKernelWarning(std::string *content, std::string *signature); + + DISALLOW_COPY_AND_ASSIGN(KernelWarningCollector); +}; + +#endif // CRASH_REPORTER_KERNEL_WARNING_COLLECTOR_H_ diff --git a/list_proxies.cc b/list_proxies.cc new file mode 100644 index 0000000..3374b5f --- /dev/null +++ b/list_proxies.cc @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <sysexits.h> +#include <unistd.h> // for isatty() + +#include <string> +#include <vector> + +#include <base/cancelable_callback.h> +#include <base/command_line.h> +#include <base/files/file_util.h> +#include <base/memory/weak_ptr.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/string_tokenizer.h> +#include <base/strings/string_util.h> +#include <base/values.h> +#include <brillo/daemons/dbus_daemon.h> +#include <brillo/syslog_logging.h> + +#include "libcrosservice/dbus-proxies.h" + +using std::unique_ptr; + +namespace { + +const char kLibCrosProxyResolvedSignalInterface[] = + "org.chromium.CrashReporterLibcrosProxyResolvedInterface"; +const char kLibCrosProxyResolvedName[] = "ProxyResolved"; +const char kLibCrosServiceName[] = "org.chromium.LibCrosService"; +const char kNoProxy[] = "direct://"; + +const int kTimeoutDefaultSeconds = 5; + +const char kHelp[] = "help"; +const char kQuiet[] = "quiet"; +const char kTimeout[] = "timeout"; +const char kVerbose[] = "verbose"; +// Help message to show when the --help command line switch is specified. +const char kHelpMessage[] = + "Chromium OS Crash helper: proxy lister\n" + "\n" + "Available Switches:\n" + " --quiet Only print the proxies\n" + " --verbose Print additional messages even when not run from a TTY\n" + " --timeout=N Set timeout for browser resolving proxies (default is 5)\n" + " --help Show this help.\n"; + +// Copied from src/update_engine/chrome_browser_proxy_resolver.cc +// Parses the browser's answer for resolved proxies. It returns a +// list of strings, each of which is a resolved proxy. +std::vector<std::string> ParseProxyString(const std::string& input) { + std::vector<std::string> ret; + // Some of this code taken from + // http://src.chromium.org/svn/trunk/src/net/proxy/proxy_server.cc and + // http://src.chromium.org/svn/trunk/src/net/proxy/proxy_list.cc + base::StringTokenizer entry_tok(input, ";"); + while (entry_tok.GetNext()) { + std::string token = entry_tok.token(); + base::TrimWhitespaceASCII(token, base::TRIM_ALL, &token); + + // Start by finding the first space (if any). + std::string::iterator space; + for (space = token.begin(); space != token.end(); ++space) { + if (base::IsAsciiWhitespace(*space)) { + break; + } + } + + std::string scheme = base::ToLowerASCII(std::string(token.begin(), space)); + // Chrome uses "socks" to mean socks4 and "proxy" to mean http. + if (scheme == "socks") + scheme += "4"; + else if (scheme == "proxy") + scheme = "http"; + else if (scheme != "https" && + scheme != "socks4" && + scheme != "socks5" && + scheme != "direct") + continue; // Invalid proxy scheme + + std::string host_and_port = std::string(space, token.end()); + base::TrimWhitespaceASCII(host_and_port, base::TRIM_ALL, &host_and_port); + if (scheme != "direct" && host_and_port.empty()) + continue; // Must supply host/port when non-direct proxy used. + ret.push_back(scheme + "://" + host_and_port); + } + if (ret.empty() || *ret.rbegin() != kNoProxy) + ret.push_back(kNoProxy); + return ret; +} + +// A class for interfacing with Chrome to resolve proxies for a given source +// url. The class is initialized with the given source url to check, the +// signal interface and name that Chrome will reply to, and how long to wait +// for the resolve request to timeout. Once initialized, the Run() function +// must be called, which blocks on the D-Bus call to Chrome. The call returns +// after either the timeout or the proxy has been resolved. The resolved +// proxies can then be accessed through the proxies() function. +class ProxyResolver : public brillo::DBusDaemon { + public: + ProxyResolver(const std::string& source_url, + const std::string& signal_interface, + const std::string& signal_name, + base::TimeDelta timeout) + : source_url_(source_url), + signal_interface_(signal_interface), + signal_name_(signal_name), + timeout_(timeout), + weak_ptr_factory_(this), + timeout_callback_(base::Bind(&ProxyResolver::HandleBrowserTimeout, + weak_ptr_factory_.GetWeakPtr())) {} + + ~ProxyResolver() override {} + + const std::vector<std::string>& proxies() { + return proxies_; + } + + int Run() override { + // Add task for if the browser proxy call times out. + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + timeout_callback_.callback(), + timeout_); + + return brillo::DBusDaemon::Run(); + } + + protected: + // If the browser times out, quit the run loop. + void HandleBrowserTimeout() { + LOG(ERROR) << "Timeout while waiting for browser to resolve proxy"; + Quit(); + } + + // If the signal handler connects successfully, call the browser's + // ResolveNetworkProxy D-Bus method. Otherwise, don't do anything and let + // the timeout task quit the run loop. + void HandleDBusSignalConnected(const std::string& interface, + const std::string& signal, + bool success) { + if (!success) { + LOG(ERROR) << "Could not connect to signal " << interface << "." + << signal; + timeout_callback_.Cancel(); + Quit(); + return; + } + + brillo::ErrorPtr error; + call_proxy_->ResolveNetworkProxy(source_url_, + signal_interface_, + signal_name_, + &error); + + if (error) { + LOG(ERROR) << "Call to ResolveNetworkProxy failed: " + << error->GetMessage(); + timeout_callback_.Cancel(); + Quit(); + } + } + + // Handle incoming ProxyResolved signal. + void HandleProxyResolvedSignal(const std::string& source_url, + const std::string& proxy_info, + const std::string& error_message) { + timeout_callback_.Cancel(); + proxies_ = ParseProxyString(proxy_info); + LOG(INFO) << "Found proxies via browser signal: " + << base::JoinString(proxies_, "x"); + + Quit(); + } + + int OnInit() override { + int return_code = brillo::DBusDaemon::OnInit(); + if (return_code != EX_OK) + return return_code; + + // Initialize D-Bus proxies. + call_proxy_.reset( + new org::chromium::LibCrosServiceInterfaceProxy(bus_, + kLibCrosServiceName)); + signal_proxy_.reset( + new org::chromium::CrashReporterLibcrosProxyResolvedInterfaceProxy( + bus_, + kLibCrosServiceName)); + + // Set up the D-Bus signal handler. + // TODO(crbug.com/446115): Update ResolveNetworkProxy call to use an + // asynchronous return value rather than a return signal. + signal_proxy_->RegisterProxyResolvedSignalHandler( + base::Bind(&ProxyResolver::HandleProxyResolvedSignal, + weak_ptr_factory_.GetWeakPtr()), + base::Bind(&ProxyResolver::HandleDBusSignalConnected, + weak_ptr_factory_.GetWeakPtr())); + + return EX_OK; + } + + private: + unique_ptr<org::chromium::LibCrosServiceInterfaceProxy> call_proxy_; + unique_ptr<org::chromium::CrashReporterLibcrosProxyResolvedInterfaceProxy> + signal_proxy_; + + const std::string source_url_; + const std::string signal_interface_; + const std::string signal_name_; + base::TimeDelta timeout_; + + std::vector<std::string> proxies_; + base::WeakPtrFactory<ProxyResolver> weak_ptr_factory_; + + base::CancelableClosure timeout_callback_; + + DISALLOW_COPY_AND_ASSIGN(ProxyResolver); +}; + +static bool ShowBrowserProxies(std::string url, base::TimeDelta timeout) { + // Initialize and run the proxy resolver to watch for signals. + ProxyResolver resolver(url, + kLibCrosProxyResolvedSignalInterface, + kLibCrosProxyResolvedName, + timeout); + resolver.Run(); + + std::vector<std::string> proxies = resolver.proxies(); + + // If proxies is empty, then the timeout was reached waiting for the proxy + // resolved signal. If no proxies are defined, proxies will be populated + // with "direct://". + if (proxies.empty()) + return false; + + for (const auto& proxy : proxies) { + printf("%s\n", proxy.c_str()); + } + return true; +} + +} // namespace + +int main(int argc, char *argv[]) { + base::CommandLine::Init(argc, argv); + base::CommandLine* cl = base::CommandLine::ForCurrentProcess(); + + if (cl->HasSwitch(kHelp)) { + LOG(INFO) << kHelpMessage; + return 0; + } + + bool quiet = cl->HasSwitch(kQuiet); + bool verbose = cl->HasSwitch(kVerbose); + + int timeout = kTimeoutDefaultSeconds; + std::string str_timeout = cl->GetSwitchValueASCII(kTimeout); + if (!str_timeout.empty() && !base::StringToInt(str_timeout, &timeout)) { + LOG(ERROR) << "Invalid timeout value: " << str_timeout; + return 1; + } + + // Default to logging to syslog. + int init_flags = brillo::kLogToSyslog; + // Log to stderr if a TTY (and "-quiet" wasn't passed), or if "-verbose" + // was passed. + + if ((!quiet && isatty(STDERR_FILENO)) || verbose) + init_flags |= brillo::kLogToStderr; + brillo::InitLog(init_flags); + + std::string url; + base::CommandLine::StringVector urls = cl->GetArgs(); + if (!urls.empty()) { + url = urls[0]; + LOG(INFO) << "Resolving proxies for URL: " << url; + } else { + LOG(INFO) << "Resolving proxies without URL"; + } + + if (!ShowBrowserProxies(url, base::TimeDelta::FromSeconds(timeout))) { + LOG(ERROR) << "Error resolving proxies via the browser"; + LOG(INFO) << "Assuming direct proxy"; + printf("%s\n", kNoProxy); + } + + return 0; +} diff --git a/periodic_scheduler b/periodic_scheduler new file mode 100755 index 0000000..5408da7 --- /dev/null +++ b/periodic_scheduler @@ -0,0 +1,80 @@ +#!/system/bin/sh + +# Copyright (C) 2014 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Run tasks periodically. +# Usage: $0 <delay_seconds> <timeout_seconds> <task_name> <task_binary> +# +# Executes task <task_name> by running <task_binary> every <delay_seconds>. + +set -e -u + +SCRIPT_NAME="$(basename "$0")" +CHECK_DELAY=300 # Check every 5 minutes. +KILL_DELAY=10 # How long to let the job clean up after a timeout. +# Let the unittests override. +: ${SPOOL_DIR:=/data/misc/crash_reporter/spool/cron-lite} + +loginfo() { + log -p i -t "${SCRIPT_NAME}" "$@" +} + +trap "loginfo 'exiting'" EXIT + +check_and_fix_spool_paths() { + # Avoid weird spool paths if possible. + rm -f "$(dirname "${SPOOL_DIR}")" "${SPOOL_DIR}" 2>/dev/null || : + mkdir -p "${SPOOL_DIR}" + if [ ! -O "${SPOOL_DIR}" -o ! -d "${SPOOL_DIR}" ]; then + loginfo "Spool directory is damaged. Aborting!" + exit 1 + fi +} + +main() { + local delay="$1" + local timeout="$2" + local name="$3" + local spool_file="${SPOOL_DIR}/${name}" + shift 3 + + [ -z "${delay}" ] && exit 1 + [ -z "${timeout}" ] && exit 1 + [ -z "${name}" ] && exit 1 + [ $# -eq 0 ] && exit 1 + check_and_fix_spool_paths + + while true; do + # Allow the sleep to be killed manually without terminating the handler. + # Send stderr to /dev/null to suppress the shell's "Terminated" message. + sleep $(( CHECK_DELAY + KILL_DELAY )) 2>/dev/null || true + + [ ! -e "${spool_file}" ] && touch "${spool_file}" + + local last_rotation="$(stat -c "%Y" "${spool_file}" 2>/dev/null || echo 0)" + local now="$(date +%s)" + local time_diff=$((now - last_rotation)) + + if [ ${time_diff} -gt ${delay} ]; then + rm "${spool_file}" || true + touch "${spool_file}" + loginfo "${name}: running $*" + timeout -k ${KILL_DELAY} ${timeout} "$@" || true + loginfo "${name}: job completed" + fi + done +} + +main "$@" diff --git a/testrunner.cc b/testrunner.cc new file mode 100644 index 0000000..744cf10 --- /dev/null +++ b/testrunner.cc @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <brillo/test_helpers.h> +#include <gtest/gtest.h> + +int main(int argc, char** argv) { + SetUpTests(&argc, argv, true); + return RUN_ALL_TESTS(); +} diff --git a/udev_collector.cc b/udev_collector.cc new file mode 100644 index 0000000..1e018db --- /dev/null +++ b/udev_collector.cc @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "udev_collector.h" + +#include <map> +#include <utility> +#include <vector> + +#include <base/files/file_enumerator.h> +#include <base/files/file_util.h> +#include <base/logging.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 <brillo/process.h> + +using base::FilePath; + +namespace { + +const char kCollectUdevSignature[] = "crash_reporter-udev-collection"; +const char kGzipPath[] = "/bin/gzip"; +const char kUdevExecName[] = "udev"; +const char kUdevSignatureKey[] = "sig"; +const char kUdevSubsystemDevCoredump[] = "devcoredump"; +const char kDefaultDevCoredumpDirectory[] = "/sys/class/devcoredump"; +const char kDevCoredumpFilePrefixFormat[] = "devcoredump_%s"; + +} // namespace + +UdevCollector::UdevCollector() + : dev_coredump_directory_(kDefaultDevCoredumpDirectory) {} + +UdevCollector::~UdevCollector() {} + +bool UdevCollector::HandleCrash(const std::string &udev_event) { + if (IsDeveloperImage()) { + LOG(INFO) << "developer image - collect udev crash info."; + } else if (is_feedback_allowed_function_()) { + LOG(INFO) << "Consent given - collect udev crash info."; + } else { + LOG(INFO) << "Ignoring - Non-developer image and no consent given."; + return false; + } + + // Process the udev event string. + // First get all the key-value pairs. + std::vector<std::pair<std::string, std::string>> udev_event_keyval; + base::SplitStringIntoKeyValuePairs(udev_event, '=', ':', &udev_event_keyval); + std::vector<std::pair<std::string, std::string>>::const_iterator iter; + std::map<std::string, std::string> udev_event_map; + for (iter = udev_event_keyval.begin(); + iter != udev_event_keyval.end(); + ++iter) { + udev_event_map[iter->first] = iter->second; + } + + // Make sure the crash directory exists, or create it if it doesn't. + FilePath crash_directory; + if (!GetCreatedCrashDirectoryByEuid(0, &crash_directory, nullptr)) { + LOG(ERROR) << "Could not get crash directory."; + return false; + } + + if (udev_event_map["SUBSYSTEM"] == kUdevSubsystemDevCoredump) { + int instance_number; + if (!base::StringToInt(udev_event_map["KERNEL_NUMBER"], &instance_number)) { + LOG(ERROR) << "Invalid kernel number: " + << udev_event_map["KERNEL_NUMBER"]; + return false; + } + return ProcessDevCoredump(crash_directory, instance_number); + } + + return ProcessUdevCrashLogs(crash_directory, + udev_event_map["ACTION"], + udev_event_map["KERNEL"], + udev_event_map["SUBSYSTEM"]); +} + +bool UdevCollector::ProcessUdevCrashLogs(const FilePath& crash_directory, + const std::string& action, + const std::string& kernel, + const std::string& subsystem) { + // Construct the basename string for crash_reporter_logs.conf: + // "crash_reporter-udev-collection-[action]-[name]-[subsystem]" + // If a udev field is not provided, "" is used in its place, e.g.: + // "crash_reporter-udev-collection-[action]--[subsystem]" + // Hence, "" is used as a wildcard name string. + // TODO(sque, crosbug.com/32238): Implement wildcard checking. + std::string basename = action + "-" + kernel + "-" + subsystem; + std::string udev_log_name = std::string(kCollectUdevSignature) + '-' + + basename; + + // Create the destination path. + std::string log_file_name = + FormatDumpBasename(basename, time(nullptr), 0); + FilePath crash_path = GetCrashPath(crash_directory, log_file_name, "log"); + + // Handle the crash. + bool result = GetLogContents(log_config_path_, udev_log_name, crash_path); + if (!result) { + LOG(ERROR) << "Error reading udev log info " << udev_log_name; + return false; + } + + // Compress the output using gzip. + brillo::ProcessImpl gzip_process; + gzip_process.AddArg(kGzipPath); + gzip_process.AddArg(crash_path.value()); + int process_result = gzip_process.Run(); + FilePath crash_path_zipped = FilePath(crash_path.value() + ".gz"); + // If the zip file was not created, use the uncompressed file. + if (process_result != 0 || !base::PathExists(crash_path_zipped)) + LOG(ERROR) << "Could not create zip file " << crash_path_zipped.value(); + else + crash_path = crash_path_zipped; + + std::string exec_name = std::string(kUdevExecName) + "-" + subsystem; + AddCrashMetaData(kUdevSignatureKey, udev_log_name); + WriteCrashMetaData(GetCrashPath(crash_directory, log_file_name, "meta"), + exec_name, crash_path.value()); + return true; +} + +bool UdevCollector::ProcessDevCoredump(const FilePath& crash_directory, + int instance_number) { + FilePath coredump_path = + FilePath(base::StringPrintf("%s/devcd%d/data", + dev_coredump_directory_.c_str(), + instance_number)); + if (!base::PathExists(coredump_path)) { + LOG(ERROR) << "Device coredump file " << coredump_path.value() + << " does not exist"; + return false; + } + + // Add coredump file to the crash directory. + if (!AppendDevCoredump(crash_directory, coredump_path, instance_number)) { + ClearDevCoredump(coredump_path); + return false; + } + + // Clear the coredump data to allow generation of future device coredumps + // without having to wait for the 5-minutes timeout. + return ClearDevCoredump(coredump_path); +} + +bool UdevCollector::AppendDevCoredump(const FilePath& crash_directory, + const FilePath& coredump_path, + int instance_number) { + // Retrieve the driver name of the failing device. + std::string driver_name = GetFailingDeviceDriverName(instance_number); + if (driver_name.empty()) { + LOG(ERROR) << "Failed to obtain driver name for instance: " + << instance_number; + return false; + } + + std::string coredump_prefix = + base::StringPrintf(kDevCoredumpFilePrefixFormat, driver_name.c_str()); + + std::string dump_basename = FormatDumpBasename(coredump_prefix, + time(nullptr), + instance_number); + FilePath core_path = GetCrashPath(crash_directory, dump_basename, "devcore"); + FilePath log_path = GetCrashPath(crash_directory, dump_basename, "log"); + FilePath meta_path = GetCrashPath(crash_directory, dump_basename, "meta"); + + // Collect coredump data. + if (!base::CopyFile(coredump_path, core_path)) { + LOG(ERROR) << "Failed to copy device coredumpm file from " + << coredump_path.value() << " to " << core_path.value(); + return false; + } + + // Collect additional logs if one is specified in the config file. + std::string udev_log_name = std::string(kCollectUdevSignature) + '-' + + kUdevSubsystemDevCoredump + '-' + driver_name; + bool result = GetLogContents(log_config_path_, udev_log_name, log_path); + if (result) { + AddCrashMetaUploadFile("logs", log_path.value()); + } + + WriteCrashMetaData(meta_path, coredump_prefix, core_path.value()); + + return true; +} + +bool UdevCollector::ClearDevCoredump(const FilePath& coredump_path) { + if (!base::WriteFile(coredump_path, "0", 1)) { + LOG(ERROR) << "Failed to delete the coredump data file " + << coredump_path.value(); + return false; + } + return true; +} + +std::string UdevCollector::GetFailingDeviceDriverName(int instance_number) { + FilePath failing_uevent_path = + FilePath(base::StringPrintf("%s/devcd%d/failing_device/uevent", + dev_coredump_directory_.c_str(), + instance_number)); + if (!base::PathExists(failing_uevent_path)) { + LOG(ERROR) << "Failing uevent path " << failing_uevent_path.value() + << " does not exist"; + return ""; + } + + std::string uevent_content; + if (!base::ReadFileToString(failing_uevent_path, &uevent_content)) { + LOG(ERROR) << "Failed to read uevent file " << failing_uevent_path.value(); + return ""; + } + + // Parse uevent file contents as key-value pairs. + std::vector<std::pair<std::string, std::string>> uevent_keyval; + base::SplitStringIntoKeyValuePairs(uevent_content, '=', '\n', &uevent_keyval); + std::vector<std::pair<std::string, std::string>>::const_iterator iter; + for (iter = uevent_keyval.begin(); + iter != uevent_keyval.end(); + ++iter) { + if (iter->first == "DRIVER") { + return iter->second; + } + } + + return ""; +} diff --git a/udev_collector.h b/udev_collector.h new file mode 100644 index 0000000..e267b75 --- /dev/null +++ b/udev_collector.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_UDEV_COLLECTOR_H_ +#define CRASH_REPORTER_UDEV_COLLECTOR_H_ + +#include <string> + +#include <base/files/file_path.h> +#include <base/macros.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "crash_collector.h" + +// Udev crash collector. +class UdevCollector : public CrashCollector { + public: + UdevCollector(); + + ~UdevCollector() override; + + // The udev event string should be formatted as follows: + // "ACTION=[action]:KERNEL=[name]:SUBSYSTEM=[subsystem]" + // The values don't have to be in any particular order. One or more of them + // could be omitted, in which case it would be treated as a wildcard (*). + bool HandleCrash(const std::string& udev_event); + + protected: + std::string dev_coredump_directory_; + + private: + friend class UdevCollectorTest; + + // Process udev crash logs, collecting log files according to the config + // file (crash_reporter_logs.conf). + bool ProcessUdevCrashLogs(const base::FilePath& crash_directory, + const std::string& action, + const std::string& kernel, + const std::string& subsystem); + // Process device coredump, collecting device coredump file. + // |instance_number| is the kernel number of the virtual device for the device + // coredump instance. + bool ProcessDevCoredump(const base::FilePath& crash_directory, + int instance_number); + // Copy device coredump file to crash directory, and perform necessary + // coredump file management. + bool AppendDevCoredump(const base::FilePath& crash_directory, + const base::FilePath& coredump_path, + int instance_number); + // Clear the device coredump file by performing a dummy write to it. + bool ClearDevCoredump(const base::FilePath& coredump_path); + // Return the driver name of the device that generates the coredump. + std::string GetFailingDeviceDriverName(int instance_number); + + // Mutator for unit testing. + void set_log_config_path(const std::string& path) { + log_config_path_ = base::FilePath(path); + } + + DISALLOW_COPY_AND_ASSIGN(UdevCollector); +}; + +#endif // CRASH_REPORTER_UDEV_COLLECTOR_H_ diff --git a/udev_collector_test.cc b/udev_collector_test.cc new file mode 100644 index 0000000..5474f48 --- /dev/null +++ b/udev_collector_test.cc @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <base/files/file_enumerator.h> +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/strings/stringprintf.h> +#include <brillo/syslog_logging.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "udev_collector.h" + +using base::FilePath; + +namespace { + +// Dummy log config file name. +const char kLogConfigFileName[] = "log_config_file"; + +// Dummy directory for storing device coredumps. +const char kDevCoredumpDirectory[] = "devcoredump"; + +// A bunch of random rules to put into the dummy log config file. +const char kLogConfigFileContents[] = + "crash_reporter-udev-collection-change-card0-drm=echo change card0 drm\n" + "crash_reporter-udev-collection-add-state0-cpu=echo change state0 cpu\n" + "crash_reporter-udev-collection-devcoredump-iwlwifi=echo devcoredump\n" + "cros_installer=echo not for udev"; + +const char kCrashLogFilePattern[] = "*.log.gz"; +const char kDevCoredumpFilePattern[] = "*.devcore"; + +// Dummy content for device coredump data file. +const char kDevCoredumpDataContents[] = "coredump"; + +// Content for failing device's uevent file. +const char kFailingDeviceUeventContents[] = "DRIVER=iwlwifi\n"; + +void CountCrash() {} + +bool s_consent_given = true; + +bool IsMetrics() { + return s_consent_given; +} + +// Returns the number of files found in the given path that matches the +// specified file name pattern. +int GetNumFiles(const FilePath& path, const std::string& file_pattern) { + base::FileEnumerator enumerator(path, false, base::FileEnumerator::FILES, + file_pattern); + int num_files = 0; + for (FilePath file_path = enumerator.Next(); + !file_path.value().empty(); + file_path = enumerator.Next()) { + num_files++; + } + return num_files; +} + +} // namespace + +class UdevCollectorMock : public UdevCollector { + public: + MOCK_METHOD0(SetUpDBus, void()); +}; + +class UdevCollectorTest : public ::testing::Test { + protected: + base::ScopedTempDir temp_dir_generator_; + + void HandleCrash(const std::string &udev_event) { + collector_.HandleCrash(udev_event); + } + + void GenerateDevCoredump(const std::string& device_name) { + // Generate coredump data file. + ASSERT_TRUE(CreateDirectory( + FilePath(base::StringPrintf("%s/%s", + collector_.dev_coredump_directory_.c_str(), + device_name.c_str())))); + FilePath data_path = + FilePath(base::StringPrintf("%s/%s/data", + collector_.dev_coredump_directory_.c_str(), + device_name.c_str())); + ASSERT_EQ(strlen(kDevCoredumpDataContents), + base::WriteFile(data_path, + kDevCoredumpDataContents, + strlen(kDevCoredumpDataContents))); + // Generate uevent file for failing device. + ASSERT_TRUE(CreateDirectory( + FilePath(base::StringPrintf("%s/%s/failing_device", + collector_.dev_coredump_directory_.c_str(), + device_name.c_str())))); + FilePath uevent_path = + FilePath(base::StringPrintf("%s/%s/failing_device/uevent", + collector_.dev_coredump_directory_.c_str(), + device_name.c_str())); + ASSERT_EQ(strlen(kFailingDeviceUeventContents), + base::WriteFile(uevent_path, + kFailingDeviceUeventContents, + strlen(kFailingDeviceUeventContents))); + } + + private: + void SetUp() override { + s_consent_given = true; + + EXPECT_CALL(collector_, SetUpDBus()).WillRepeatedly(testing::Return()); + + collector_.Initialize(CountCrash, IsMetrics); + + ASSERT_TRUE(temp_dir_generator_.CreateUniqueTempDir()); + + FilePath log_config_path = + temp_dir_generator_.path().Append(kLogConfigFileName); + collector_.log_config_path_ = log_config_path; + collector_.ForceCrashDirectory(temp_dir_generator_.path()); + + FilePath dev_coredump_path = + temp_dir_generator_.path().Append(kDevCoredumpDirectory); + collector_.dev_coredump_directory_ = dev_coredump_path.value(); + + // Write to a dummy log config file. + ASSERT_EQ(strlen(kLogConfigFileContents), + base::WriteFile(log_config_path, + kLogConfigFileContents, + strlen(kLogConfigFileContents))); + + brillo::ClearLog(); + } + + UdevCollectorMock collector_; +}; + +TEST_F(UdevCollectorTest, TestNoConsent) { + s_consent_given = false; + HandleCrash("ACTION=change:KERNEL=card0:SUBSYSTEM=drm"); + EXPECT_EQ(0, GetNumFiles(temp_dir_generator_.path(), kCrashLogFilePattern)); +} + +TEST_F(UdevCollectorTest, TestNoMatch) { + // No rule should match this. + HandleCrash("ACTION=change:KERNEL=foo:SUBSYSTEM=bar"); + EXPECT_EQ(0, GetNumFiles(temp_dir_generator_.path(), kCrashLogFilePattern)); +} + +TEST_F(UdevCollectorTest, TestMatches) { + // Try multiple udev events in sequence. The number of log files generated + // should increase. + HandleCrash("ACTION=change:KERNEL=card0:SUBSYSTEM=drm"); + EXPECT_EQ(1, GetNumFiles(temp_dir_generator_.path(), kCrashLogFilePattern)); + HandleCrash("ACTION=add:KERNEL=state0:SUBSYSTEM=cpu"); + EXPECT_EQ(2, GetNumFiles(temp_dir_generator_.path(), kCrashLogFilePattern)); +} + +TEST_F(UdevCollectorTest, TestDevCoredump) { + GenerateDevCoredump("devcd0"); + HandleCrash("ACTION=add:KERNEL_NUMBER=0:SUBSYSTEM=devcoredump"); + EXPECT_EQ(1, GetNumFiles(temp_dir_generator_.path(), + kDevCoredumpFilePattern)); + GenerateDevCoredump("devcd1"); + HandleCrash("ACTION=add:KERNEL_NUMBER=1:SUBSYSTEM=devcoredump"); + EXPECT_EQ(2, GetNumFiles(temp_dir_generator_.path(), + kDevCoredumpFilePattern)); +} + +// TODO(sque, crosbug.com/32238) - test wildcard cases, multiple identical udev +// events. diff --git a/unclean_shutdown_collector.cc b/unclean_shutdown_collector.cc new file mode 100644 index 0000000..8a092ec --- /dev/null +++ b/unclean_shutdown_collector.cc @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "unclean_shutdown_collector.h" + +#include <base/files/file_util.h> +#include <base/logging.h> + +static const char kUncleanShutdownFile[] = + "/var/lib/crash_reporter/pending_clean_shutdown"; + +// Files created by power manager used for crash reporting. +static const char kPowerdTracePath[] = "/var/lib/power_manager"; +// Presence of this file indicates that the system was suspended +static const char kPowerdSuspended[] = "powerd_suspended"; + +using base::FilePath; + +UncleanShutdownCollector::UncleanShutdownCollector() + : unclean_shutdown_file_(kUncleanShutdownFile), + powerd_trace_path_(kPowerdTracePath), + powerd_suspended_file_(powerd_trace_path_.Append(kPowerdSuspended)) { +} + +UncleanShutdownCollector::~UncleanShutdownCollector() { +} + +bool UncleanShutdownCollector::Enable() { + FilePath file_path(unclean_shutdown_file_); + base::CreateDirectory(file_path.DirName()); + if (base::WriteFile(file_path, "", 0) != 0) { + LOG(ERROR) << "Unable to create shutdown check file"; + return false; + } + return true; +} + +bool UncleanShutdownCollector::DeleteUncleanShutdownFiles() { + if (!base::DeleteFile(FilePath(unclean_shutdown_file_), false)) { + LOG(ERROR) << "Failed to delete unclean shutdown file " + << unclean_shutdown_file_; + return false; + } + // Delete power manager state file if it exists. + base::DeleteFile(powerd_suspended_file_, false); + return true; +} + +bool UncleanShutdownCollector::Collect() { + FilePath unclean_file_path(unclean_shutdown_file_); + if (!base::PathExists(unclean_file_path)) { + return false; + } + LOG(WARNING) << "Last shutdown was not clean"; + if (DeadBatteryCausedUncleanShutdown()) { + DeleteUncleanShutdownFiles(); + return false; + } + DeleteUncleanShutdownFiles(); + + if (is_feedback_allowed_function_()) { + count_crash_function_(); + } + return true; +} + +bool UncleanShutdownCollector::Disable() { + LOG(INFO) << "Clean shutdown signalled"; + return DeleteUncleanShutdownFiles(); +} + +bool UncleanShutdownCollector::DeadBatteryCausedUncleanShutdown() { + // Check for case of battery running out while suspended. + if (base::PathExists(powerd_suspended_file_)) { + LOG(INFO) << "Unclean shutdown occurred while suspended. Not counting " + << "toward unclean shutdown statistic."; + return true; + } + return false; +} diff --git a/unclean_shutdown_collector.h b/unclean_shutdown_collector.h new file mode 100644 index 0000000..5bc9968 --- /dev/null +++ b/unclean_shutdown_collector.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_UNCLEAN_SHUTDOWN_COLLECTOR_H_ +#define CRASH_REPORTER_UNCLEAN_SHUTDOWN_COLLECTOR_H_ + +#include <string> + +#include <base/files/file_path.h> +#include <base/macros.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "crash_collector.h" + +// Unclean shutdown collector. +class UncleanShutdownCollector : public CrashCollector { + public: + UncleanShutdownCollector(); + ~UncleanShutdownCollector() override; + + // Enable collection - signal that a boot has started. + bool Enable(); + + // Collect if there is was an unclean shutdown. Returns true if + // there was, false otherwise. + bool Collect(); + + // Disable collection - signal that the system has been shutdown cleanly. + bool Disable(); + + private: + friend class UncleanShutdownCollectorTest; + FRIEND_TEST(UncleanShutdownCollectorTest, EnableCannotWrite); + FRIEND_TEST(UncleanShutdownCollectorTest, CollectDeadBatterySuspended); + + bool DeleteUncleanShutdownFiles(); + + // Check for unclean shutdown due to battery running out by analyzing powerd + // trace files. + bool DeadBatteryCausedUncleanShutdown(); + + const char *unclean_shutdown_file_; + base::FilePath powerd_trace_path_; + base::FilePath powerd_suspended_file_; + + DISALLOW_COPY_AND_ASSIGN(UncleanShutdownCollector); +}; + +#endif // CRASH_REPORTER_UNCLEAN_SHUTDOWN_COLLECTOR_H_ diff --git a/unclean_shutdown_collector_test.cc b/unclean_shutdown_collector_test.cc new file mode 100644 index 0000000..36372ae --- /dev/null +++ b/unclean_shutdown_collector_test.cc @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "unclean_shutdown_collector.h" + +#include <unistd.h> + +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/strings/string_util.h> +#include <brillo/syslog_logging.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +using base::FilePath; +using ::brillo::FindLog; + +namespace { + +int s_crashes = 0; +bool s_metrics = true; + +void CountCrash() { + ++s_crashes; +} + +bool IsMetrics() { + return s_metrics; +} + +} // namespace + +class UncleanShutdownCollectorMock : public UncleanShutdownCollector { + public: + MOCK_METHOD0(SetUpDBus, void()); +}; + +class UncleanShutdownCollectorTest : public ::testing::Test { + void SetUp() { + s_crashes = 0; + + EXPECT_CALL(collector_, SetUpDBus()).WillRepeatedly(testing::Return()); + + collector_.Initialize(CountCrash, + IsMetrics); + + EXPECT_TRUE(test_dir_.CreateUniqueTempDir()); + + test_directory_ = test_dir_.path().Append("test"); + test_unclean_ = test_dir_.path().Append("test/unclean"); + + collector_.unclean_shutdown_file_ = test_unclean_.value().c_str(); + base::DeleteFile(test_unclean_, true); + // Set up an alternate power manager state file as well + collector_.powerd_suspended_file_ = + test_dir_.path().Append("test/suspended"); + brillo::ClearLog(); + } + + protected: + void WriteStringToFile(const FilePath &file_path, + const char *data) { + unsigned int numBytesWritten = + base::WriteFile(file_path, data, strlen(data)); + ASSERT_EQ(strlen(data), numBytesWritten); + } + + UncleanShutdownCollectorMock collector_; + + // Temporary directory used for tests. + base::ScopedTempDir test_dir_; + FilePath test_directory_; + FilePath test_unclean_; +}; + +TEST_F(UncleanShutdownCollectorTest, EnableWithoutParent) { + ASSERT_TRUE(collector_.Enable()); + ASSERT_TRUE(base::PathExists(test_unclean_)); +} + +TEST_F(UncleanShutdownCollectorTest, EnableWithParent) { + mkdir(test_directory_.value().c_str(), 0777); + ASSERT_TRUE(collector_.Enable()); + ASSERT_TRUE(base::PathExists(test_unclean_)); +} + +TEST_F(UncleanShutdownCollectorTest, EnableCannotWrite) { + collector_.unclean_shutdown_file_ = "/bad/path"; + ASSERT_FALSE(collector_.Enable()); + ASSERT_TRUE(FindLog("Unable to create shutdown check file")); +} + +TEST_F(UncleanShutdownCollectorTest, CollectTrue) { + ASSERT_TRUE(collector_.Enable()); + ASSERT_TRUE(base::PathExists(test_unclean_)); + ASSERT_TRUE(collector_.Collect()); + ASSERT_FALSE(base::PathExists(test_unclean_)); + ASSERT_EQ(1, s_crashes); + ASSERT_TRUE(FindLog("Last shutdown was not clean")); +} + +TEST_F(UncleanShutdownCollectorTest, CollectFalse) { + ASSERT_FALSE(collector_.Collect()); + ASSERT_EQ(0, s_crashes); +} + +TEST_F(UncleanShutdownCollectorTest, CollectDeadBatterySuspended) { + ASSERT_TRUE(collector_.Enable()); + ASSERT_TRUE(base::PathExists(test_unclean_)); + base::WriteFile(collector_.powerd_suspended_file_, "", 0); + ASSERT_FALSE(collector_.Collect()); + ASSERT_FALSE(base::PathExists(test_unclean_)); + ASSERT_FALSE(base::PathExists(collector_.powerd_suspended_file_)); + ASSERT_EQ(0, s_crashes); + ASSERT_TRUE(FindLog("Unclean shutdown occurred while suspended.")); +} + +TEST_F(UncleanShutdownCollectorTest, Disable) { + ASSERT_TRUE(collector_.Enable()); + ASSERT_TRUE(base::PathExists(test_unclean_)); + ASSERT_TRUE(collector_.Disable()); + ASSERT_FALSE(base::PathExists(test_unclean_)); + ASSERT_FALSE(collector_.Collect()); +} + +TEST_F(UncleanShutdownCollectorTest, DisableWhenNotEnabled) { + ASSERT_TRUE(collector_.Disable()); +} + +TEST_F(UncleanShutdownCollectorTest, CantDisable) { + mkdir(test_directory_.value().c_str(), 0700); + if (mkdir(test_unclean_.value().c_str(), 0700)) { + ASSERT_EQ(EEXIST, errno) + << "Error while creating directory '" << test_unclean_.value() + << "': " << strerror(errno); + } + ASSERT_EQ(0, base::WriteFile(test_unclean_.Append("foo"), "", 0)) + << "Error while creating empty file '" + << test_unclean_.Append("foo").value() << "': " << strerror(errno); + ASSERT_FALSE(collector_.Disable()); + rmdir(test_unclean_.value().c_str()); +} diff --git a/user_collector.cc b/user_collector.cc new file mode 100644 index 0000000..48b64e9 --- /dev/null +++ b/user_collector.cc @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "user_collector.h" + +#include <elf.h> +#include <fcntl.h> +#include <grp.h> // For struct group. +#include <pcrecpp.h> +#include <pwd.h> // For struct passwd. +#include <stdint.h> +#include <sys/cdefs.h> // For __WORDSIZE +#include <sys/fsuid.h> +#include <sys/types.h> // For getpwuid_r, getgrnam_r, WEXITSTATUS. +#include <unistd.h> // For setgroups + +#include <iostream> // For std::oct +#include <string> +#include <vector> + +#include <base/files/file_util.h> +#include <base/logging.h> +#include <base/posix/eintr_wrapper.h> +#include <base/strings/string_split.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <brillo/process.h> +#include <brillo/syslog_logging.h> +#include <cutils/properties.h> +#include <private/android_filesystem_config.h> + +static const char kCollectionErrorSignature[] = + "crash_reporter-user-collection"; +static const char kCorePatternProperty[] = "crash_reporter.coredump.enabled"; +static const char kCoreToMinidumpConverterPath[] = "/system/bin/core2md"; + +static const char kStatePrefix[] = "State:\t"; + +static const char kCoreTempFolder[] = "/data/misc/crash_reporter/tmp"; + +// Define an otherwise invalid value that represents an unknown UID and GID. +static const uid_t kUnknownUid = -1; +static const gid_t kUnknownGid = -1; + +const char *UserCollector::kUserId = "Uid:\t"; +const char *UserCollector::kGroupId = "Gid:\t"; + + +using base::FilePath; +using base::StringPrintf; + +UserCollector::UserCollector() + : generate_diagnostics_(false), + initialized_(false) { +} + +void UserCollector::Initialize( + UserCollector::CountCrashFunction count_crash_function, + const std::string &our_path, + UserCollector::IsFeedbackAllowedFunction is_feedback_allowed_function, + bool generate_diagnostics, + bool core2md_failure, + bool directory_failure, + const std::string &filter_in) { + CrashCollector::Initialize(count_crash_function, + is_feedback_allowed_function); + our_path_ = our_path; + initialized_ = true; + generate_diagnostics_ = generate_diagnostics; + core2md_failure_ = core2md_failure; + directory_failure_ = directory_failure; + filter_in_ = filter_in; + + gid_t groups[] = { AID_ROOT, AID_SYSTEM, AID_DBUS, AID_READPROC }; + if (setgroups(arraysize(groups), groups) != 0) { + PLOG(FATAL) << "Unable to set groups to root, system, dbus, and readproc"; + } +} + +UserCollector::~UserCollector() { +} + +std::string UserCollector::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 ""; + } +} + +bool UserCollector::SetUpInternal(bool enabled) { + CHECK(initialized_); + LOG(INFO) << (enabled ? "Enabling" : "Disabling") << " user crash handling"; + + property_set(kCorePatternProperty, enabled ? "1" : "0"); + + return true; +} + +bool UserCollector::GetFirstLineWithPrefix( + const std::vector<std::string> &lines, + const char *prefix, std::string *line) { + std::vector<std::string>::const_iterator line_iterator; + for (line_iterator = lines.begin(); line_iterator != lines.end(); + ++line_iterator) { + if (line_iterator->find(prefix) == 0) { + *line = *line_iterator; + return true; + } + } + return false; +} + +bool UserCollector::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::TRIM_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 UserCollector::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; +} + +void UserCollector::EnqueueCollectionErrorLog(pid_t pid, + ErrorType error_type, + const std::string &exec) { + FilePath crash_path; + LOG(INFO) << "Writing conversion problems as separate crash report."; + if (!GetCreatedCrashDirectoryByEuid(0, &crash_path, nullptr)) { + LOG(ERROR) << "Could not even get log directory; out of space?"; + return; + } + AddCrashMetaData("sig", kCollectionErrorSignature); + AddCrashMetaData("error_type", GetErrorTypeSignature(error_type)); + std::string dump_basename = FormatDumpBasename(exec, time(nullptr), pid); + std::string error_log = brillo::GetLog(); + FilePath diag_log_path = GetCrashPath(crash_path, dump_basename, "diaglog"); + if (GetLogContents(FilePath(log_config_path_), kCollectionErrorSignature, + diag_log_path)) { + // We load the contents of diag_log into memory and append it to + // the error log. We cannot just append to files because we need + // to always create new files to prevent attack. + std::string diag_log_contents; + base::ReadFileToString(diag_log_path, &diag_log_contents); + error_log.append(diag_log_contents); + base::DeleteFile(diag_log_path, false); + } + FilePath log_path = GetCrashPath(crash_path, dump_basename, "log"); + FilePath meta_path = GetCrashPath(crash_path, dump_basename, "meta"); + // 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.data(), error_log.length()) < 0) { + LOG(ERROR) << "Error writing new file " << log_path.value(); + return; + } + WriteCrashMetaData(meta_path, exec, log_path.value()); +} + +bool UserCollector::CopyOffProcFiles(pid_t pid, + const FilePath &container_dir) { + if (!base::CreateDirectory(container_dir)) { + PLOG(ERROR) << "Could not create " << container_dir.value(); + return false; + } + int dir_mask = base::FILE_PERMISSION_READ_BY_USER + | base::FILE_PERMISSION_WRITE_BY_USER + | base::FILE_PERMISSION_EXECUTE_BY_USER + | base::FILE_PERMISSION_READ_BY_GROUP + | base::FILE_PERMISSION_WRITE_BY_GROUP; + if (!base::SetPosixFilePermissions(container_dir, + base::FILE_PERMISSION_MASK & dir_mask)) { + PLOG(ERROR) << "Could not set permissions for " << container_dir.value() + << " to " << std::oct + << (base::FILE_PERMISSION_MASK & dir_mask); + return false; + } + FilePath process_path = GetProcessPath(pid); + if (!base::PathExists(process_path)) { + LOG(ERROR) << "Path " << process_path.value() << " does not exist"; + return false; + } + static const char *proc_files[] = { + "auxv", + "cmdline", + "environ", + "maps", + "status" + }; + for (unsigned i = 0; i < arraysize(proc_files); ++i) { + if (!base::CopyFile(process_path.Append(proc_files[i]), + container_dir.Append(proc_files[i]))) { + LOG(ERROR) << "Could not copy " << proc_files[i] << " file"; + return false; + } + } + return true; +} + +bool UserCollector::ValidateProcFiles(const FilePath &container_dir) const { + // Check if the maps file is empty, which could be due to the crashed + // process being reaped by the kernel before finishing a core dump. + int64_t file_size = 0; + if (!base::GetFileSize(container_dir.Append("maps"), &file_size)) { + LOG(ERROR) << "Could not get the size of maps file"; + return false; + } + if (file_size == 0) { + LOG(ERROR) << "maps file is empty"; + return false; + } + return true; +} + +UserCollector::ErrorType UserCollector::ValidateCoreFile( + const FilePath &core_path) const { + int fd = HANDLE_EINTR(open(core_path.value().c_str(), O_RDONLY)); + if (fd < 0) { + PLOG(ERROR) << "Could not open core file " << core_path.value(); + return kErrorInvalidCoreFile; + } + + char e_ident[EI_NIDENT]; + bool read_ok = base::ReadFromFD(fd, e_ident, sizeof(e_ident)); + IGNORE_EINTR(close(fd)); + if (!read_ok) { + LOG(ERROR) << "Could not read header of core file"; + return kErrorInvalidCoreFile; + } + + if (e_ident[EI_MAG0] != ELFMAG0 || e_ident[EI_MAG1] != ELFMAG1 || + e_ident[EI_MAG2] != ELFMAG2 || e_ident[EI_MAG3] != ELFMAG3) { + LOG(ERROR) << "Invalid core file"; + return kErrorInvalidCoreFile; + } + +#if __WORDSIZE == 64 + // TODO(benchan, mkrebs): Remove this check once core2md can + // handles both 32-bit and 64-bit ELF on a 64-bit platform. + if (e_ident[EI_CLASS] == ELFCLASS32) { + LOG(ERROR) << "Conversion of 32-bit core file on 64-bit platform is " + << "currently not supported"; + return kErrorUnsupported32BitCoreFile; + } +#endif + + return kErrorNone; +} + +bool UserCollector::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::TRIM_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 if (supplied_ruid != kUnknownUid) { + LOG(INFO) << "Using supplied UID " << supplied_ruid + << " for crashed process [" << pid + << "] due to error reading status file"; + uid = supplied_ruid; + } else { + LOG(ERROR) << "Could not read status file and kernel did not supply UID"; + LOG(INFO) << "Path " << process_path.value() << " DirectoryExists: " + << base::DirectoryExists(process_path); + return false; + } + + if (!GetCreatedCrashDirectoryByEuid(uid, crash_file_path, out_of_capacity)) { + LOG(ERROR) << "Could not create crash directory"; + return false; + } + return true; +} + +bool UserCollector::CopyStdinToCoreFile(const FilePath &core_path) { + // Copy off all stdin to a core file. + FilePath stdin_path("/proc/self/fd/0"); + if (base::CopyFile(stdin_path, core_path)) { + return true; + } + + PLOG(ERROR) << "Could not write core file"; + // If the file system was full, make sure we remove any remnants. + base::DeleteFile(core_path, false); + return false; +} + +bool UserCollector::RunCoreToMinidump(const FilePath &core_path, + const FilePath &procfs_directory, + const FilePath &minidump_path, + const FilePath &temp_directory) { + FilePath output_path = temp_directory.Append("output"); + brillo::ProcessImpl core2md; + core2md.RedirectOutput(output_path.value()); + core2md.AddArg(kCoreToMinidumpConverterPath); + core2md.AddArg(core_path.value()); + core2md.AddArg(procfs_directory.value()); + + if (!core2md_failure_) { + core2md.AddArg(minidump_path.value()); + } else { + // To test how core2md errors are propagaged, cause an error + // by forgetting a required argument. + } + + int errorlevel = core2md.Run(); + + std::string output; + base::ReadFileToString(output_path, &output); + if (errorlevel != 0) { + LOG(ERROR) << "Problem during " << kCoreToMinidumpConverterPath + << " [result=" << errorlevel << "]: " << output; + return false; + } + + if (!base::PathExists(minidump_path)) { + LOG(ERROR) << "Minidump file " << minidump_path.value() + << " was not created"; + return false; + } + return true; +} + +UserCollector::ErrorType UserCollector::ConvertCoreToMinidump( + pid_t pid, + const FilePath &container_dir, + const FilePath &core_path, + const FilePath &minidump_path) { + // If proc files are unuable, we continue to read the core file from stdin, + // but only skip the core-to-minidump conversion, so that we may still use + // the core file for debugging. + bool proc_files_usable = + CopyOffProcFiles(pid, container_dir) && ValidateProcFiles(container_dir); + + // Switch back to the original UID/GID. + gid_t rgid, egid, sgid; + if (getresgid(&rgid, &egid, &sgid) != 0) { + PLOG(FATAL) << "Unable to read saved gid"; + } + if (setresgid(sgid, sgid, -1) != 0) { + PLOG(FATAL) << "Unable to set real group ID back to saved gid"; + } else { + if (getresgid(&rgid, &egid, &sgid) != 0) { + // If the groups cannot be read at this point, the rgid variable will + // contain the previously read group ID from before changing it. This + // will cause the chown call below to set the incorrect group for + // non-root crashes. But do not treat this as a fatal error, so that + // the rest of the collection will continue for potential manual + // collection by a developer. + PLOG(ERROR) << "Unable to read real group ID after setting it"; + } + } + + uid_t ruid, euid, suid; + if (getresuid(&ruid, &euid, &suid) != 0) { + PLOG(FATAL) << "Unable to read saved uid"; + } + if (setresuid(suid, suid, -1) != 0) { + PLOG(FATAL) << "Unable to set real user ID back to saved uid"; + } else { + if (getresuid(&ruid, &euid, &suid) != 0) { + // If the user ID cannot be read at this point, the ruid variable will + // contain the previously read user ID from before changing it. This + // will cause the chown call below to set the incorrect user for + // non-root crashes. But do not treat this as a fatal error, so that + // the rest of the collection will continue for potential manual + // collection by a developer. + PLOG(ERROR) << "Unable to read real user ID after setting it"; + } + } + + if (!CopyStdinToCoreFile(core_path)) { + return kErrorReadCoreData; + } + + if (!proc_files_usable) { + LOG(INFO) << "Skipped converting core file to minidump due to " + << "unusable proc files"; + return kErrorUnusableProcFiles; + } + + ErrorType error = ValidateCoreFile(core_path); + if (error != kErrorNone) { + return error; + } + + // Chown the temp container directory back to the original user/group that + // crash_reporter is run as, so that additional files can be written to + // the temp folder. + if (chown(container_dir.value().c_str(), ruid, rgid) < 0) { + PLOG(ERROR) << "Could not set owner for " << container_dir.value(); + } + + if (!RunCoreToMinidump(core_path, + container_dir, // procfs directory + minidump_path, + container_dir)) { // temporary directory + return kErrorCore2MinidumpConversion; + } + + LOG(INFO) << "Stored minidump to " << minidump_path.value(); + return kErrorNone; +} + +UserCollector::ErrorType UserCollector::ConvertAndEnqueueCrash( + pid_t pid, const std::string &exec, uid_t supplied_ruid, + 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. + FilePath container_dir(StringPrintf("%s/%d", kCoreTempFolder, pid)); + // 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. + base::DeleteFile(container_dir, true); + 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, "dmp"); + FilePath log_path = GetCrashPath(crash_path, dump_basename, "log"); + + if (GetLogContents(FilePath(log_config_path_), exec, log_path)) + AddCrashMetaData("log", log_path.value()); + + ErrorType error_type = + ConvertCoreToMinidump(pid, container_dir, core_path, minidump_path); + if (error_type != kErrorNone) { + LOG(INFO) << "Leaving core file at " << core_path.value() + << " due to conversion error"; + return error_type; + } + + // 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. + WriteCrashMetaData(meta_path, + exec, + minidump_path.value()); + + if (!IsDeveloperImage()) { + base::DeleteFile(core_path, false); + } else { + LOG(INFO) << "Leaving core file at " << core_path.value() + << " due to developer image"; + } + + base::DeleteFile(container_dir, true); + return kErrorNone; +} + +bool UserCollector::ParseCrashAttributes(const std::string &crash_attributes, + pid_t *pid, int *signal, uid_t *uid, + gid_t *gid, + std::string *kernel_supplied_name) { + pcrecpp::RE re("(\\d+):(\\d+):(\\d+):(\\d+):(.*)"); + if (re.FullMatch(crash_attributes, pid, signal, uid, gid, + kernel_supplied_name)) + return true; + + LOG(INFO) << "Falling back to parsing crash attributes '" + << crash_attributes << "' without UID and GID"; + pcrecpp::RE re_without_uid("(\\d+):(\\d+):(.*)"); + *uid = kUnknownUid; + *gid = kUnknownGid; + return re_without_uid.FullMatch(crash_attributes, pid, signal, + kernel_supplied_name); +} + +bool UserCollector::ShouldDump(bool has_owner_consent, + bool is_developer, + std::string *reason) { + reason->clear(); + + // 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 (is_developer) { + *reason = "developer build - not testing - always dumping"; + return true; + } + + if (!has_owner_consent) { + *reason = "ignoring - no consent"; + return false; + } + + *reason = "handling"; + return true; +} + +bool UserCollector::HandleCrash(const std::string &crash_attributes, + const char *force_exec) { + CHECK(initialized_); + pid_t pid = 0; + int signal = 0; + uid_t supplied_ruid = kUnknownUid; + gid_t supplied_rgid = kUnknownGid; + std::string kernel_supplied_name; + + if (!ParseCrashAttributes(crash_attributes, &pid, &signal, &supplied_ruid, + &supplied_rgid, &kernel_supplied_name)) { + LOG(ERROR) << "Invalid parameter: --user=" << crash_attributes; + return false; + } + + // Switch to the group and user that ran the crashing binary in order to + // access their /proc files. Do not set suid/sgid, so that we can switch + // back after copying the necessary files. + if (setresgid(supplied_rgid, supplied_rgid, -1) != 0) { + PLOG(FATAL) << "Unable to set real group ID to access process files"; + } + if (setresuid(supplied_ruid, supplied_ruid, -1) != 0) { + PLOG(FATAL) << "Unable to set real user ID to access process files"; + } + + std::string exec; + if (force_exec) { + exec.assign(force_exec); + } else if (!GetExecutableBaseNameFromPid(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", kernel_supplied_name.c_str()); + } + + // Allow us to test the crash reporting mechanism successfully even if + // other parts of the system crash. + if (!filter_in_.empty() && + (filter_in_ == "none" || + filter_in_ != exec)) { + // We use a different format message to make it more obvious in tests + // which crashes are test generated and which are real. + LOG(WARNING) << "Ignoring crash from " << exec << "[" << pid << "] while " + << "filter_in=" << filter_in_ << "."; + return true; + } + + std::string reason; + bool dump = ShouldDump(is_feedback_allowed_function_(), + IsDeveloperImage(), + &reason); + + LOG(WARNING) << "Received crash notification for " << exec << "[" << pid + << "] sig " << signal << ", user " << supplied_ruid + << " (" << reason << ")"; + + if (dump) { + count_crash_function_(); + + if (generate_diagnostics_) { + bool out_of_capacity = false; + ErrorType error_type = + ConvertAndEnqueueCrash(pid, exec, supplied_ruid, &out_of_capacity); + if (error_type != kErrorNone) { + if (!out_of_capacity) + EnqueueCollectionErrorLog(pid, error_type, exec); + return false; + } + } + } + + return true; +} diff --git a/user_collector.h b/user_collector.h new file mode 100644 index 0000000..7261ed4 --- /dev/null +++ b/user_collector.h @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CRASH_REPORTER_USER_COLLECTOR_H_ +#define CRASH_REPORTER_USER_COLLECTOR_H_ + +#include <string> +#include <vector> + +#include <base/files/file_path.h> +#include <base/macros.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "crash_collector.h" + +class SystemLogging; + +// User crash collector. +class UserCollector : public CrashCollector { + public: + UserCollector(); + + // Initialize the user crash collector for detection of crashes, + // given a crash counting function, the path to this executable, + // metrics collection enabled oracle, and system logger facility. + // Crash detection/reporting is not enabled until Enable is called. + // |generate_diagnostics| is used to indicate whether or not to try + // to generate a minidump from crashes. + void Initialize(CountCrashFunction count_crash, + const std::string &our_path, + IsFeedbackAllowedFunction is_metrics_allowed, + bool generate_diagnostics, + bool core2md_failure, + bool directory_failure, + const std::string &filter_in); + + ~UserCollector() override; + + // Enable collection. + bool Enable() { return SetUpInternal(true); } + + // Disable collection. + bool Disable() { return SetUpInternal(false); } + + // Handle a specific user crash. Returns true on success. + bool HandleCrash(const std::string &crash_attributes, + const char *force_exec); + + private: + friend class UserCollectorTest; + FRIEND_TEST(UserCollectorTest, CopyOffProcFilesBadPath); + FRIEND_TEST(UserCollectorTest, CopyOffProcFilesBadPid); + FRIEND_TEST(UserCollectorTest, CopyOffProcFilesOK); + FRIEND_TEST(UserCollectorTest, GetExecutableBaseNameFromPid); + FRIEND_TEST(UserCollectorTest, GetFirstLineWithPrefix); + FRIEND_TEST(UserCollectorTest, GetIdFromStatus); + FRIEND_TEST(UserCollectorTest, GetStateFromStatus); + FRIEND_TEST(UserCollectorTest, GetProcessPath); + FRIEND_TEST(UserCollectorTest, GetSymlinkTarget); + FRIEND_TEST(UserCollectorTest, GetUserInfoFromName); + FRIEND_TEST(UserCollectorTest, ParseCrashAttributes); + FRIEND_TEST(UserCollectorTest, ShouldDumpChromeOverridesDeveloperImage); + FRIEND_TEST(UserCollectorTest, ShouldDumpDeveloperImageOverridesConsent); + FRIEND_TEST(UserCollectorTest, ShouldDumpUseConsentProductionImage); + FRIEND_TEST(UserCollectorTest, ValidateProcFiles); + FRIEND_TEST(UserCollectorTest, ValidateCoreFile); + + // Enumeration to pass to GetIdFromStatus. Must match the order + // that the kernel lists IDs in the status file. + enum IdKind { + kIdReal = 0, // uid and gid + kIdEffective = 1, // euid and egid + kIdSet = 2, // suid and sgid + kIdFileSystem = 3, // fsuid and fsgid + kIdMax + }; + + enum ErrorType { + kErrorNone, + kErrorSystemIssue, + kErrorReadCoreData, + kErrorUnusableProcFiles, + kErrorInvalidCoreFile, + kErrorUnsupported32BitCoreFile, + kErrorCore2MinidumpConversion, + }; + + static const int kForkProblem = 255; + + // Returns an error type signature for a given |error_type| value, + // which is reported to the crash server along with the + // crash_reporter-user-collection signature. + std::string GetErrorTypeSignature(ErrorType error_type) const; + + bool SetUpInternal(bool enabled); + + // Returns, via |line|, the first line in |lines| that starts with |prefix|. + // Returns true if a line is found, or false otherwise. + bool GetFirstLineWithPrefix(const std::vector<std::string> &lines, + const char *prefix, std::string *line); + + // Returns the identifier of |kind|, via |id|, found in |status_lines| on + // the line starting with |prefix|. |status_lines| contains the lines in + // the status file. Returns true if the identifier can be determined. + bool GetIdFromStatus(const char *prefix, + IdKind kind, + const std::vector<std::string> &status_lines, + int *id); + + // Returns the process state, via |state|, found in |status_lines|, which + // contains the lines in the status file. Returns true if the process state + // can be determined. + bool GetStateFromStatus(const std::vector<std::string> &status_lines, + std::string *state); + + void LogCollectionError(const std::string &error_message); + void EnqueueCollectionErrorLog(pid_t pid, ErrorType error_type, + const std::string &exec_name); + + bool CopyOffProcFiles(pid_t pid, const base::FilePath &container_dir); + + // Validates the proc files at |container_dir| and returns true if they + // are usable for the core-to-minidump conversion later. For instance, if + // a process is reaped by the kernel before the copying of its proc files + // takes place, some proc files like /proc/<pid>/maps may contain nothing + // and thus become unusable. + bool ValidateProcFiles(const base::FilePath &container_dir) const; + + // Validates the core file at |core_path| and returns kErrorNone if + // the file contains the ELF magic bytes and an ELF class that matches the + // platform (i.e. 32-bit ELF on a 32-bit platform or 64-bit ELF on a 64-bit + // platform), which is due to the limitation in core2md. It returns an error + // type otherwise. + ErrorType ValidateCoreFile(const base::FilePath &core_path) const; + + // Determines the crash directory for given pid based on pid's owner, + // and creates the directory if necessary with appropriate permissions. + // Returns true whether or not directory needed to be created, false on + // any failure. + bool GetCreatedCrashDirectory(pid_t pid, uid_t supplied_ruid, + base::FilePath *crash_file_path, + bool *out_of_capacity); + bool CopyStdinToCoreFile(const base::FilePath &core_path); + bool RunCoreToMinidump(const base::FilePath &core_path, + const base::FilePath &procfs_directory, + const base::FilePath &minidump_path, + const base::FilePath &temp_directory); + ErrorType ConvertCoreToMinidump(pid_t pid, + const base::FilePath &container_dir, + const base::FilePath &core_path, + const base::FilePath &minidump_path); + ErrorType ConvertAndEnqueueCrash(pid_t pid, const std::string &exec_name, + uid_t supplied_ruid, bool *out_of_capacity); + bool ParseCrashAttributes(const std::string &crash_attributes, + pid_t *pid, int *signal, uid_t *uid, gid_t *gid, + std::string *kernel_supplied_name); + + bool ShouldDump(bool has_owner_consent, + bool is_developer, + std::string *reason); + + bool generate_diagnostics_; + std::string our_path_; + bool initialized_; + + bool core2md_failure_; + bool directory_failure_; + std::string filter_in_; + + static const char *kUserId; + static const char *kGroupId; + + DISALLOW_COPY_AND_ASSIGN(UserCollector); +}; + +#endif // CRASH_REPORTER_USER_COLLECTOR_H_ diff --git a/user_collector_test.cc b/user_collector_test.cc new file mode 100644 index 0000000..16a5cd5 --- /dev/null +++ b/user_collector_test.cc @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "user_collector.h" + +#include <elf.h> +#include <sys/cdefs.h> // For __WORDSIZE +#include <unistd.h> + +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/strings/string_split.h> +#include <brillo/syslog_logging.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +using base::FilePath; +using brillo::FindLog; + +namespace { + +int s_crashes = 0; +bool s_metrics = false; + +const char kFilePath[] = "/my/path"; + +void CountCrash() { + ++s_crashes; +} + +bool IsMetrics() { + return s_metrics; +} + +} // namespace + +class UserCollectorMock : public UserCollector { + public: + MOCK_METHOD0(SetUpDBus, void()); +}; + +class UserCollectorTest : public ::testing::Test { + void SetUp() { + s_crashes = 0; + + EXPECT_CALL(collector_, SetUpDBus()).WillRepeatedly(testing::Return()); + + collector_.Initialize(CountCrash, + kFilePath, + IsMetrics, + false, + false, + false, + ""); + + EXPECT_TRUE(test_dir_.CreateUniqueTempDir()); + + mkdir(test_dir_.path().Append("test").value().c_str(), 0777); + pid_ = getpid(); + brillo::ClearLog(); + } + + protected: + void ExpectFileEquals(const char *golden, + const FilePath &file_path) { + std::string contents; + EXPECT_TRUE(base::ReadFileToString(file_path, &contents)); + EXPECT_EQ(golden, contents); + } + + std::vector<std::string> SplitLines(const std::string &lines) const { + return base::SplitString(lines, "\n", base::TRIM_WHITESPACE, + base::SPLIT_WANT_ALL); + } + + UserCollectorMock collector_; + pid_t pid_; + base::ScopedTempDir test_dir_; +}; + +TEST_F(UserCollectorTest, ParseCrashAttributes) { + pid_t pid; + int signal; + uid_t uid; + gid_t gid; + std::string exec_name; + EXPECT_TRUE(collector_.ParseCrashAttributes("123456:11:1000:2000:foobar", + &pid, &signal, &uid, &gid, &exec_name)); + EXPECT_EQ(123456, pid); + EXPECT_EQ(11, signal); + EXPECT_EQ(1000U, uid); + EXPECT_EQ(2000U, gid); + EXPECT_EQ("foobar", exec_name); + EXPECT_TRUE(collector_.ParseCrashAttributes("4321:6:barfoo", + &pid, &signal, &uid, &gid, &exec_name)); + EXPECT_EQ(4321, pid); + EXPECT_EQ(6, signal); + EXPECT_EQ(-1U, uid); + EXPECT_EQ(-1U, gid); + EXPECT_EQ("barfoo", exec_name); + + EXPECT_FALSE(collector_.ParseCrashAttributes("123456:11", + &pid, &signal, &uid, &gid, &exec_name)); + + EXPECT_TRUE(collector_.ParseCrashAttributes("123456:11:exec:extra", + &pid, &signal, &uid, &gid, &exec_name)); + EXPECT_EQ("exec:extra", exec_name); + + EXPECT_FALSE(collector_.ParseCrashAttributes("12345p:11:foobar", + &pid, &signal, &uid, &gid, &exec_name)); + + EXPECT_FALSE(collector_.ParseCrashAttributes("123456:1 :foobar", + &pid, &signal, &uid, &gid, &exec_name)); + + EXPECT_FALSE(collector_.ParseCrashAttributes("123456::foobar", + &pid, &signal, &uid, &gid, &exec_name)); +} + +TEST_F(UserCollectorTest, ShouldDumpDeveloperImageOverridesConsent) { + std::string reason; + EXPECT_TRUE(collector_.ShouldDump(false, true, &reason)); + EXPECT_EQ("developer build - not testing - always dumping", reason); + + // When running a crash test, behave as normal. + EXPECT_FALSE(collector_.ShouldDump(false, false, &reason)); + EXPECT_EQ("ignoring - no consent", reason); +} + +TEST_F(UserCollectorTest, ShouldDumpUseConsentProductionImage) { + std::string result; + EXPECT_FALSE(collector_.ShouldDump(false, false, &result)); + EXPECT_EQ("ignoring - no consent", result); + + EXPECT_TRUE(collector_.ShouldDump(true, false, &result)); + EXPECT_EQ("handling", result); +} + +TEST_F(UserCollectorTest, HandleCrashWithoutConsent) { + s_metrics = false; + collector_.HandleCrash("20:10:ignored", "foobar"); + EXPECT_TRUE(FindLog( + "Received crash notification for foobar[20] sig 10")); + ASSERT_EQ(s_crashes, 0); +} + +TEST_F(UserCollectorTest, HandleNonChromeCrashWithConsent) { + s_metrics = true; + collector_.HandleCrash("5:2:ignored", "chromeos-wm"); + EXPECT_TRUE(FindLog( + "Received crash notification for chromeos-wm[5] sig 2")); + ASSERT_EQ(s_crashes, 1); +} + +TEST_F(UserCollectorTest, GetProcessPath) { + FilePath path = collector_.GetProcessPath(100); + ASSERT_EQ("/proc/100", path.value()); +} + +TEST_F(UserCollectorTest, GetSymlinkTarget) { + FilePath result; + ASSERT_FALSE(collector_.GetSymlinkTarget(FilePath("/does_not_exist"), + &result)); + ASSERT_TRUE(FindLog( + "Readlink failed on /does_not_exist with 2")); + std::string long_link = test_dir_.path().value(); + for (int i = 0; i < 50; ++i) + long_link += "0123456789"; + long_link += "/gold"; + + for (size_t len = 1; len <= long_link.size(); ++len) { + std::string this_link; + static const char* kLink = + test_dir_.path().Append("test/this_link").value().c_str(); + this_link.assign(long_link.c_str(), len); + ASSERT_EQ(len, this_link.size()); + ASSERT_EQ(0, symlink(this_link.c_str(), kLink)); + ASSERT_TRUE(collector_.GetSymlinkTarget(FilePath(kLink), &result)); + ASSERT_EQ(this_link, result.value()); + unlink(kLink); + } +} + +TEST_F(UserCollectorTest, GetExecutableBaseNameFromPid) { + std::string base_name; + EXPECT_FALSE(collector_.GetExecutableBaseNameFromPid(0, &base_name)); + EXPECT_TRUE(FindLog( + "Readlink failed on /proc/0/exe with 2")); + EXPECT_TRUE(FindLog( + "GetSymlinkTarget failed - Path /proc/0 DirectoryExists: 0")); + EXPECT_TRUE(FindLog("stat /proc/0/exe failed: -1 2")); + + brillo::ClearLog(); + pid_t my_pid = getpid(); + EXPECT_TRUE(collector_.GetExecutableBaseNameFromPid(my_pid, &base_name)); + EXPECT_FALSE(FindLog("Readlink failed")); + EXPECT_EQ("crash_reporter_tests", base_name); +} + +TEST_F(UserCollectorTest, GetFirstLineWithPrefix) { + std::vector<std::string> lines; + std::string line; + + EXPECT_FALSE(collector_.GetFirstLineWithPrefix(lines, "Name:", &line)); + EXPECT_EQ("", line); + + lines.push_back("Name:\tls"); + lines.push_back("State:\tR (running)"); + lines.push_back(" Foo:\t1000"); + + line.clear(); + EXPECT_TRUE(collector_.GetFirstLineWithPrefix(lines, "Name:", &line)); + EXPECT_EQ(lines[0], line); + + line.clear(); + EXPECT_TRUE(collector_.GetFirstLineWithPrefix(lines, "State:", &line)); + EXPECT_EQ(lines[1], line); + + line.clear(); + EXPECT_FALSE(collector_.GetFirstLineWithPrefix(lines, "Foo:", &line)); + EXPECT_EQ("", line); + + line.clear(); + EXPECT_TRUE(collector_.GetFirstLineWithPrefix(lines, " Foo:", &line)); + EXPECT_EQ(lines[2], line); + + line.clear(); + EXPECT_FALSE(collector_.GetFirstLineWithPrefix(lines, "Bar:", &line)); + EXPECT_EQ("", line); +} + +TEST_F(UserCollectorTest, GetIdFromStatus) { + int id = 1; + EXPECT_FALSE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdEffective, + SplitLines("nothing here"), + &id)); + EXPECT_EQ(id, 1); + + // Not enough parameters. + EXPECT_FALSE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdReal, + SplitLines("line 1\nUid:\t1\n"), + &id)); + + const std::vector<std::string> valid_contents = + SplitLines("\nUid:\t1\t2\t3\t4\nGid:\t5\t6\t7\t8\n"); + EXPECT_TRUE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdReal, + valid_contents, + &id)); + EXPECT_EQ(1, id); + + EXPECT_TRUE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdEffective, + valid_contents, + &id)); + EXPECT_EQ(2, id); + + EXPECT_TRUE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdFileSystem, + valid_contents, + &id)); + EXPECT_EQ(4, id); + + EXPECT_TRUE(collector_.GetIdFromStatus(UserCollector::kGroupId, + UserCollector::kIdEffective, + valid_contents, + &id)); + EXPECT_EQ(6, id); + + EXPECT_TRUE(collector_.GetIdFromStatus(UserCollector::kGroupId, + UserCollector::kIdSet, + valid_contents, + &id)); + EXPECT_EQ(7, id); + + EXPECT_FALSE(collector_.GetIdFromStatus(UserCollector::kGroupId, + UserCollector::IdKind(5), + valid_contents, + &id)); + EXPECT_FALSE(collector_.GetIdFromStatus(UserCollector::kGroupId, + UserCollector::IdKind(-1), + valid_contents, + &id)); + + // Fail if junk after number + EXPECT_FALSE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdReal, + SplitLines("Uid:\t1f\t2\t3\t4\n"), + &id)); + EXPECT_TRUE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdReal, + SplitLines("Uid:\t1\t2\t3\t4\n"), + &id)); + EXPECT_EQ(1, id); + + // Fail if more than 4 numbers. + EXPECT_FALSE(collector_.GetIdFromStatus(UserCollector::kUserId, + UserCollector::kIdReal, + SplitLines("Uid:\t1\t2\t3\t4\t5\n"), + &id)); +} + +TEST_F(UserCollectorTest, GetStateFromStatus) { + std::string state; + EXPECT_FALSE(collector_.GetStateFromStatus(SplitLines("nothing here"), + &state)); + EXPECT_EQ("", state); + + EXPECT_TRUE(collector_.GetStateFromStatus(SplitLines("State:\tR (running)"), + &state)); + EXPECT_EQ("R (running)", state); + + EXPECT_TRUE(collector_.GetStateFromStatus( + SplitLines("Name:\tls\nState:\tZ (zombie)\n"), &state)); + EXPECT_EQ("Z (zombie)", state); +} + +TEST_F(UserCollectorTest, GetUserInfoFromName) { + gid_t gid = 100; + uid_t uid = 100; + EXPECT_TRUE(collector_.GetUserInfoFromName("root", &uid, &gid)); + EXPECT_EQ(0U, uid); + EXPECT_EQ(0U, gid); +} + +TEST_F(UserCollectorTest, CopyOffProcFilesBadPath) { + // Try a path that is not writable. + ASSERT_FALSE(collector_.CopyOffProcFiles(pid_, FilePath("/bad/path"))); + EXPECT_TRUE(FindLog("Could not create /bad/path")); +} + +TEST_F(UserCollectorTest, CopyOffProcFilesBadPid) { + FilePath container_path(test_dir_.path().Append("test/container")); + ASSERT_FALSE(collector_.CopyOffProcFiles(0, container_path)); + EXPECT_TRUE(FindLog("Path /proc/0 does not exist")); +} + +TEST_F(UserCollectorTest, CopyOffProcFilesOK) { + FilePath container_path(test_dir_.path().Append("test/container")); + ASSERT_TRUE(collector_.CopyOffProcFiles(pid_, container_path)); + EXPECT_FALSE(FindLog("Could not copy")); + static struct { + const char *name; + bool exists; + } expectations[] = { + { "auxv", true }, + { "cmdline", true }, + { "environ", true }, + { "maps", true }, + { "mem", false }, + { "mounts", false }, + { "sched", false }, + { "status", true } + }; + for (unsigned i = 0; i < sizeof(expectations)/sizeof(expectations[0]); ++i) { + EXPECT_EQ(expectations[i].exists, + base::PathExists( + container_path.Append(expectations[i].name))); + } +} + +TEST_F(UserCollectorTest, ValidateProcFiles) { + FilePath container_dir = test_dir_.path(); + + // maps file not exists (i.e. GetFileSize fails) + EXPECT_FALSE(collector_.ValidateProcFiles(container_dir)); + + // maps file is empty + FilePath maps_file = container_dir.Append("maps"); + ASSERT_EQ(0, base::WriteFile(maps_file, nullptr, 0)); + ASSERT_TRUE(base::PathExists(maps_file)); + EXPECT_FALSE(collector_.ValidateProcFiles(container_dir)); + + // maps file is not empty + const char data[] = "test data"; + unsigned int numBytesWritten = + base::WriteFile(maps_file, data, sizeof(data)); + ASSERT_EQ(sizeof(data), numBytesWritten); + ASSERT_TRUE(base::PathExists(maps_file)); + EXPECT_TRUE(collector_.ValidateProcFiles(container_dir)); +} + +TEST_F(UserCollectorTest, ValidateCoreFile) { + FilePath container_dir = test_dir_.path(); + FilePath core_file = container_dir.Append("core"); + + // Core file does not exist + EXPECT_EQ(UserCollector::kErrorInvalidCoreFile, + collector_.ValidateCoreFile(core_file)); + char e_ident[EI_NIDENT]; + e_ident[EI_MAG0] = ELFMAG0; + e_ident[EI_MAG1] = ELFMAG1; + e_ident[EI_MAG2] = ELFMAG2; + e_ident[EI_MAG3] = ELFMAG3; +#if __WORDSIZE == 32 + e_ident[EI_CLASS] = ELFCLASS32; +#elif __WORDSIZE == 64 + e_ident[EI_CLASS] = ELFCLASS64; +#else +#error Unknown/unsupported value of __WORDSIZE. +#endif + + // Core file has the expected header + ASSERT_TRUE(base::WriteFile(core_file, e_ident, sizeof(e_ident))); + EXPECT_EQ(UserCollector::kErrorNone, + collector_.ValidateCoreFile(core_file)); + +#if __WORDSIZE == 64 + // 32-bit core file on 64-bit platform + e_ident[EI_CLASS] = ELFCLASS32; + ASSERT_TRUE(base::WriteFile(core_file, e_ident, sizeof(e_ident))); + EXPECT_EQ(UserCollector::kErrorUnsupported32BitCoreFile, + collector_.ValidateCoreFile(core_file)); + e_ident[EI_CLASS] = ELFCLASS64; +#endif + + // Invalid core files + ASSERT_TRUE(base::WriteFile(core_file, e_ident, sizeof(e_ident) - 1)); + EXPECT_EQ(UserCollector::kErrorInvalidCoreFile, + collector_.ValidateCoreFile(core_file)); + + e_ident[EI_MAG0] = 0; + ASSERT_TRUE(base::WriteFile(core_file, e_ident, sizeof(e_ident))); + EXPECT_EQ(UserCollector::kErrorInvalidCoreFile, + collector_.ValidateCoreFile(core_file)); +} diff --git a/warn_collector.l b/warn_collector.l new file mode 100644 index 0000000..70ab25c --- /dev/null +++ b/warn_collector.l @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This flex program reads /var/log/messages as it grows and saves kernel + * warnings to files. It keeps track of warnings it has seen (based on + * file/line only, ignoring differences in the stack trace), and reports only + * the first warning of each kind, but maintains a count of all warnings by + * using their hashes as buckets in a UMA sparse histogram. It also invokes + * the crash collector, which collects the warnings and prepares them for later + * shipment to the crash server. + */ + +%option noyywrap + +%{ +#include <fcntl.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdarg.h> +#include <sys/inotify.h> +#include <sys/select.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "metrics/c_metrics_library.h" + +int WarnStart(void); +void WarnEnd(void); +void WarnInput(char *buf, yy_size_t *result, size_t max_size); + +#define YY_INPUT(buf, result, max_size) WarnInput(buf, &result, max_size) + +%} + +/* Define a few useful regular expressions. */ + +D [0-9] +PREFIX .*" kernel: [ "*{D}+"."{D}+"]" +CUT_HERE {PREFIX}" ------------[ cut here".* +WARNING {PREFIX}" WARNING: at " +END_TRACE {PREFIX}" ---[ end trace".* + +/* Use exclusive start conditions. */ +%x PRE_WARN WARN + +%% + /* The scanner itself. */ + +^{CUT_HERE}\n{WARNING} BEGIN(PRE_WARN); +.|\n /* ignore all other input in state 0 */ +<PRE_WARN>[^ ]+.[^ ]+\n if (WarnStart()) { + /* yytext is + "file:line func+offset/offset()\n" */ + BEGIN(WARN); ECHO; + } else { + BEGIN(0); + } + + /* Assume the warning ends at the "end trace" line */ +<WARN>^{END_TRACE}\n ECHO; BEGIN(0); WarnEnd(); +<WARN>^.*\n ECHO; + +%% + +#define HASH_BITMAP_SIZE (1 << 15) /* size in bits */ +#define HASH_BITMAP_MASK (HASH_BITMAP_SIZE - 1) + +const char warn_hist_name[] = "Platform.KernelWarningHashes"; +uint32_t hash_bitmap[HASH_BITMAP_SIZE / 32]; +CMetricsLibrary metrics_library; + +const char *prog_name; /* the name of this program */ +int yyin_fd; /* instead of FILE *yyin to avoid buffering */ +int i_fd; /* for inotify, to detect file changes */ +int testing; /* 1 if running test */ +int filter; /* 1 when using as filter (for development) */ +int fifo; /* 1 when reading from fifo (for devel) */ +int draining; /* 1 when draining renamed log file */ + +const char *msg_path = "/var/log/messages"; +const char warn_dump_dir[] = "/var/run/kwarn"; +const char *warn_dump_path = "/var/run/kwarn/warning"; +const char *crash_reporter_command; + +__attribute__((__format__(__printf__, 1, 2))) +static void Die(const char *format, ...) { + va_list ap; + va_start(ap, format); + fprintf(stderr, "%s: ", prog_name); + vfprintf(stderr, format, ap); + exit(1); +} + +static void RunCrashReporter(void) { + int status = system(crash_reporter_command); + if (status != 0) + Die("%s exited with status %d\n", crash_reporter_command, status); +} + +static uint32_t StringHash(const char *string) { + uint32_t hash = 0; + while (*string != '\0') { + hash = (hash << 5) + hash + *string++; + } + return hash; +} + +/* We expect only a handful of different warnings per boot session, so the + * probability of a collision is very low, and statistically it won't matter + * (unless warnings with the same hash also happens in tandem, which is even + * rarer). + */ +static int HashSeen(uint32_t hash) { + int word_index = (hash & HASH_BITMAP_MASK) / 32; + int bit_index = (hash & HASH_BITMAP_MASK) % 32; + return hash_bitmap[word_index] & 1 << bit_index; +} + +static void SetHashSeen(uint32_t hash) { + int word_index = (hash & HASH_BITMAP_MASK) / 32; + int bit_index = (hash & HASH_BITMAP_MASK) % 32; + hash_bitmap[word_index] |= 1 << bit_index; +} + +#pragma GCC diagnostic ignored "-Wwrite-strings" +int WarnStart(void) { + uint32_t hash; + char *spacep; + + if (filter) + return 1; + + hash = StringHash(yytext); + if (!(testing || fifo || filter)) { + CMetricsLibrarySendSparseToUMA(metrics_library, warn_hist_name, (int) hash); + } + if (HashSeen(hash)) + return 0; + SetHashSeen(hash); + + yyout = fopen(warn_dump_path, "w"); + if (yyout == NULL) + Die("fopen %s failed: %s\n", warn_dump_path, strerror(errno)); + spacep = strchr(yytext, ' '); + if (spacep == NULL || spacep[1] == '\0') + spacep = "unknown-function"; + fprintf(yyout, "%08x-%s\n", hash, spacep + 1); + return 1; +} + +void WarnEnd(void) { + if (filter) + return; + fclose(yyout); + yyout = stdout; /* for debugging */ + RunCrashReporter(); +} + +static void WarnOpenInput(const char *path) { + yyin_fd = open(path, O_RDONLY); + if (yyin_fd < 0) + Die("could not open %s: %s\n", path, strerror(errno)); + if (!fifo) { + /* Go directly to the end of the file. We don't want to parse the same + * warnings multiple times on reboot/restart. We might miss some + * warnings, but so be it---it's too hard to keep track reliably of the + * last parsed position in the syslog. + */ + if (lseek(yyin_fd, 0, SEEK_END) < 0) + Die("could not lseek %s: %s\n", path, strerror(errno)); + /* Set up notification of file growth and rename. */ + i_fd = inotify_init(); + if (i_fd < 0) + Die("inotify_init: %s\n", strerror(errno)); + if (inotify_add_watch(i_fd, path, IN_MODIFY | IN_MOVE_SELF) < 0) + Die("inotify_add_watch: %s\n", strerror(errno)); + } +} + +/* We replace the default YY_INPUT() for the following reasons: + * + * 1. We want to read data as soon as it becomes available, but the default + * YY_INPUT() uses buffered I/O. + * + * 2. We want to block on end of input and wait for the file to grow. + * + * 3. We want to detect log rotation, and reopen the input file as needed. + */ +void WarnInput(char *buf, yy_size_t *result, size_t max_size) { + while (1) { + ssize_t ret = read(yyin_fd, buf, max_size); + if (ret < 0) + Die("read: %s", strerror(errno)); + *result = ret; + if (*result > 0 || fifo || filter) + return; + if (draining) { + /* Assume we're done with this log, and move to next + * log. Rsyslogd may keep writing to the old log file + * for a while, but we don't care since we don't have + * to be exact. + */ + close(yyin_fd); + if (YYSTATE == WARN) { + /* Be conservative in case we lose the warn + * terminator during the switch---or we may + * collect personally identifiable information. + */ + WarnEnd(); + } + BEGIN(0); /* see above comment */ + sleep(1); /* avoid race with log rotator */ + WarnOpenInput(msg_path); + draining = 0; + continue; + } + /* Nothing left to read, so we must wait. */ + struct inotify_event event; + while (1) { + int n = read(i_fd, &event, sizeof(event)); + if (n <= 0) { + if (errno == EINTR) + continue; + else + Die("inotify: %s\n", strerror(errno)); + } else + break; + } + if (event.mask & IN_MOVE_SELF) { + /* The file has been renamed. Before switching + * to the new one, we process any remaining + * content of this file. + */ + draining = 1; + } + } +} + +int main(int argc, char **argv) { + int result; + struct passwd *user; + prog_name = argv[0]; + + if (argc == 2 && strcmp(argv[1], "--test") == 0) + testing = 1; + else if (argc == 2 && strcmp(argv[1], "--filter") == 0) + filter = 1; + else if (argc == 2 && strcmp(argv[1], "--fifo") == 0) { + fifo = 1; + } else if (argc != 1) { + fprintf(stderr, + "usage: %s [single-flag]\n" + "flags (for testing only):\n" + "--fifo\tinput is fifo \"fifo\", output is stdout\n" + "--filter\tinput is stdin, output is stdout\n" + "--test\trun self-test\n", + prog_name); + exit(1); + } + + metrics_library = CMetricsLibraryNew(); + CMetricsLibraryInit(metrics_library); + + crash_reporter_command = testing ? + "./warn_collector_test_reporter.sh" : + "/sbin/crash_reporter --kernel_warning"; + + /* When filtering with --filter (for development) use stdin for input. + * Otherwise read input from a file or a fifo. + */ + yyin_fd = fileno(stdin); + if (testing) { + msg_path = "messages"; + warn_dump_path = "warning"; + } + if (fifo) { + msg_path = "fifo"; + } + if (!filter) { + WarnOpenInput(msg_path); + } + + /* Create directory for dump file. Still need to be root here. */ + unlink(warn_dump_path); + if (!testing && !fifo && !filter) { + rmdir(warn_dump_dir); + result = mkdir(warn_dump_dir, 0755); + if (result < 0) + Die("could not create %s: %s\n", + warn_dump_dir, strerror(errno)); + } + + if (0) { + /* TODO(semenzato): put this back in once we decide it's safe + * to make /var/spool/crash rwxrwxrwx root, or use a different + * owner and setuid for the crash reporter as well. + */ + + /* Get low privilege uid, gid. */ + user = getpwnam("chronos"); + if (user == NULL) + Die("getpwnam failed\n"); + + /* Change dump directory ownership. */ + if (chown(warn_dump_dir, user->pw_uid, user->pw_gid) < 0) + Die("chown: %s\n", strerror(errno)); + + /* Drop privileges. */ + if (setuid(user->pw_uid) < 0) { + Die("setuid: %s\n", strerror(errno)); + } + } + + /* Go! */ + return yylex(); +} + +/* Flex should really know not to generate these functions. + */ +void UnusedFunctionWarningSuppressor(void) { + yyunput(0, 0); +} diff --git a/warn_collector_test.c b/warn_collector_test.c new file mode 100644 index 0000000..7ebe0a8 --- /dev/null +++ b/warn_collector_test.c @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Test driver for the warn_collector daemon. + */ +#include <stdlib.h> + +int main(int ac, char **av) { + int status = system("exec \"${SRC}\"/warn_collector_test.sh"); + return status < 0 ? EXIT_FAILURE : WEXITSTATUS(status); +} diff --git a/warn_collector_test.sh b/warn_collector_test.sh new file mode 100755 index 0000000..a5af16c --- /dev/null +++ b/warn_collector_test.sh @@ -0,0 +1,90 @@ +#! /bin/bash + +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test for warn_collector. Run the warn collector in the background, emulate +# the kernel by appending lines to the log file "messages", and observe the log +# of the (fake) crash reporter each time is run by the warn collector daemon. + +set -e + +fail() { + printf '[ FAIL ] %b\n' "$*" + exit 1 +} + +if [[ -z ${SYSROOT} ]]; then + fail "SYSROOT must be set for this test to work" +fi +: ${OUT:=${PWD}} +cd "${OUT}" +PATH=${OUT}:${PATH} +TESTLOG="${OUT}/warn-test-log" + +echo "Testing: $(which warn_collector)" + +cleanup() { + # Kill daemon (if started) on exit + kill % +} + +check_log() { + local n_expected=$1 + if [[ ! -f ${TESTLOG} ]]; then + fail "${TESTLOG} was not created" + fi + if [[ $(wc -l < "${TESTLOG}") -ne ${n_expected} ]]; then + fail "expected ${n_expected} lines in ${TESTLOG}, found this instead: +$(<"${TESTLOG}")" + fi + if egrep -qv '^[0-9a-f]{8}' "${TESTLOG}"; then + fail "found bad lines in ${TESTLOG}: +$(<"${TESTLOG}")" + fi +} + +rm -f "${TESTLOG}" +cp "${SRC}/warn_collector_test_reporter.sh" . +cp "${SRC}/TEST_WARNING" . +cp TEST_WARNING messages + +# Start the collector daemon. With the --test option, the daemon reads input +# from ./messages, writes the warning into ./warning, and invokes +# ./warn_collector_test_reporter.sh to report the warning. +warn_collector --test & +trap cleanup EXIT + +# After a while, check that the first warning has been collected. +sleep 1 +check_log 1 + +# Add the same warning to messages, verify that it is NOT collected +cat TEST_WARNING >> messages +sleep 1 +check_log 1 + +# Add a slightly different warning to messages, check that it is collected. +sed s/intel_dp.c/intel_xx.c/ < TEST_WARNING >> messages +sleep 1 +check_log 2 + +# Emulate log rotation, add a warning, and check. +mv messages messages.1 +sed s/intel_dp.c/intel_xy.c/ < TEST_WARNING > messages +sleep 2 +check_log 3 + +# Success! +exit 0 diff --git a/warn_collector_test_reporter.sh b/warn_collector_test_reporter.sh new file mode 100755 index 0000000..b6096ed --- /dev/null +++ b/warn_collector_test_reporter.sh @@ -0,0 +1,27 @@ +#! /bin/sh + +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Replacement for the crash reporter, for testing. Log the first line of the +# "warning" file, which by convention contains the warning hash, and remove the +# file. + +set -e + +exec 1>> warn-test-log +exec 2>> warn-test-log + +head -1 warning +rm warning |