| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. |
| |
| #include "src/developer/forensics/exceptions/handler/crash_reporter.h" |
| |
| #include <fuchsia/feedback/cpp/fidl.h> |
| #include <fuchsia/sys/internal/cpp/fidl.h> |
| #include <fuchsia/sys2/cpp/fidl.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/process.h> |
| #include <lib/zx/thread.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls/exception.h> |
| #include <zircon/types.h> |
| |
| #include <memory> |
| #include <type_traits> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/developer/forensics/exceptions/handler/component_lookup.h" |
| #include "src/developer/forensics/exceptions/tests/crasher_wrapper.h" |
| #include "src/developer/forensics/testing/gmatchers.h" |
| #include "src/developer/forensics/testing/gpretty_printers.h" |
| #include "src/developer/forensics/testing/unit_test_fixture.h" |
| #include "src/lib/fostr/fidl/fuchsia/exception/formatting.h" |
| #include "src/lib/fsl/handles/object_info.h" |
| #include "src/lib/fxl/test/test_settings.h" |
| #include "third_party/crashpad/snapshot/minidump/process_snapshot_minidump.h" |
| #include "third_party/crashpad/util/file/string_file.h" |
| |
| namespace forensics { |
| namespace exceptions { |
| namespace handler { |
| |
| inline void ToString(const fuchsia::exception::ExceptionType& value, std::ostream* os) { |
| *os << value; |
| } |
| |
| namespace { |
| |
| using fuchsia::exception::ExceptionInfo; |
| using fuchsia::exception::ExceptionType; |
| using fuchsia::exception::ProcessException; |
| using testing::UnorderedElementsAreArray; |
| |
| constexpr zx::duration kDefaultTimeout{zx::duration::infinite()}; |
| |
| class StubCrashReporter : public fuchsia::feedback::CrashReporter { |
| public: |
| void File(fuchsia::feedback::CrashReport report, FileCallback callback) { |
| reports_.push_back(std::move(report)); |
| |
| fuchsia::feedback::CrashReporter_File_Result result; |
| result.set_response({}); |
| callback(std::move(result)); |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::feedback::CrashReporter> GetHandler() { |
| return [this](fidl::InterfaceRequest<fuchsia::feedback::CrashReporter> request) { |
| bindings_.AddBinding(this, std::move(request)); |
| }; |
| } |
| |
| const std::vector<fuchsia::feedback::CrashReport>& reports() const { return reports_; } |
| |
| private: |
| std::vector<fuchsia::feedback::CrashReport> reports_; |
| |
| fidl::BindingSet<fuchsia::feedback::CrashReporter> bindings_; |
| }; |
| |
| class StubCrashIntrospectV1 : public fuchsia::sys::internal::CrashIntrospect { |
| public: |
| struct ComponentInfo { |
| std::string url; |
| std::vector<std::string> realm_path; |
| std::string name; |
| }; |
| |
| void FindComponentByThreadKoid(uint64_t thread_koid, FindComponentByThreadKoidCallback callback) { |
| using namespace fuchsia::sys::internal; |
| if (tids_to_component_infos_.find(thread_koid) == tids_to_component_infos_.end()) { |
| callback(CrashIntrospect_FindComponentByThreadKoid_Result::WithErr(ZX_ERR_NOT_FOUND)); |
| } else { |
| const auto& info = tids_to_component_infos_[thread_koid]; |
| |
| SourceIdentity source_identity; |
| source_identity.set_component_url(info.url) |
| .set_realm_path(info.realm_path) |
| .set_component_name(info.name); |
| |
| callback(CrashIntrospect_FindComponentByThreadKoid_Result::WithResponse( |
| CrashIntrospect_FindComponentByThreadKoid_Response(std::move(source_identity)))); |
| } |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::sys::internal::CrashIntrospect> GetHandler() { |
| return [this](fidl::InterfaceRequest<fuchsia::sys::internal::CrashIntrospect> request) { |
| bindings_.AddBinding(this, std::move(request)); |
| }; |
| } |
| |
| void AddThreadKoidToComponentInfo(uint64_t thread_koid, ComponentInfo component_info) { |
| tids_to_component_infos_[thread_koid] = component_info; |
| } |
| |
| private: |
| std::map<uint64_t, ComponentInfo> tids_to_component_infos_; |
| |
| fidl::BindingSet<fuchsia::sys::internal::CrashIntrospect> bindings_; |
| }; |
| |
| class StubCrashIntrospectV2 : public fuchsia::sys2::CrashIntrospect { |
| public: |
| struct ComponentInfo { |
| std::string url; |
| std::string moniker; |
| }; |
| void FindComponentByThreadKoid(uint64_t thread_koid, FindComponentByThreadKoidCallback callback) { |
| using namespace fuchsia::sys2; |
| if (tids_to_component_infos_.find(thread_koid) == tids_to_component_infos_.end()) { |
| callback(CrashIntrospect_FindComponentByThreadKoid_Result::WithErr( |
| fuchsia::component::Error::RESOURCE_NOT_FOUND)); |
| } else { |
| const auto& info = tids_to_component_infos_[thread_koid]; |
| |
| ComponentCrashInfo crash_info; |
| crash_info.set_url(info.url).set_moniker(info.moniker); |
| |
| callback(CrashIntrospect_FindComponentByThreadKoid_Result::WithResponse( |
| CrashIntrospect_FindComponentByThreadKoid_Response(std::move(crash_info)))); |
| } |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::sys2::CrashIntrospect> GetHandler() { |
| return [this](fidl::InterfaceRequest<fuchsia::sys2::CrashIntrospect> request) { |
| bindings_.AddBinding(this, std::move(request)); |
| }; |
| } |
| |
| void AddThreadKoidToComponentInfo(uint64_t thread_koid, ComponentInfo component_info) { |
| tids_to_component_infos_[thread_koid] = component_info; |
| } |
| |
| private: |
| std::map<uint64_t, ComponentInfo> tids_to_component_infos_; |
| |
| fidl::BindingSet<fuchsia::sys2::CrashIntrospect> bindings_; |
| }; |
| |
| class HandlerTest : public UnitTestFixture { |
| public: |
| void HandleException( |
| zx::exception exception, zx::duration component_lookup_timeout, |
| CrashReporter::SendCallback callback = [](::fidl::StringPtr moniker) {}) { |
| handler_ = std::make_unique<CrashReporter>(dispatcher(), services(), component_lookup_timeout); |
| |
| zx::process process; |
| exception.get_process(&process); |
| |
| zx::thread thread; |
| exception.get_thread(&thread); |
| |
| handler_->Send(std::move(exception), std::move(process), std::move(thread), |
| std::move(callback)); |
| RunLoopUntilIdle(); |
| } |
| |
| void HandleException( |
| zx::process process, zx::thread thread, zx::duration component_lookup_timeout, |
| CrashReporter::SendCallback callback = [](::fidl::StringPtr moniker) {}) { |
| handler_ = std::make_unique<CrashReporter>(dispatcher(), services(), component_lookup_timeout); |
| |
| handler_->Send(zx::exception{}, std::move(process), std::move(thread), std::move(callback)); |
| RunLoopUntilIdle(); |
| } |
| |
| void SetUpCrashReporter() { InjectServiceProvider(&crash_reporter_); } |
| void SetUpCrashIntrospect() { |
| InjectServiceProvider(&introspect_v1_); |
| InjectServiceProvider(&introspect_v2_); |
| } |
| |
| const StubCrashReporter& crash_reporter() const { return crash_reporter_; } |
| |
| StubCrashIntrospectV1& introspect_v1() { return introspect_v1_; } |
| const StubCrashIntrospectV1& introspect_v1() const { return introspect_v1_; } |
| |
| StubCrashIntrospectV2& introspect_v2() { return introspect_v2_; } |
| const StubCrashIntrospectV2& introspect_v2() const { return introspect_v2_; } |
| |
| private: |
| std::unique_ptr<CrashReporter> handler_{nullptr}; |
| |
| StubCrashReporter crash_reporter_; |
| StubCrashIntrospectV1 introspect_v1_; |
| StubCrashIntrospectV2 introspect_v2_; |
| }; |
| |
| bool RetrieveExceptionContext(ExceptionContext* pe) { |
| // Create a process that crashes and obtain the relevant handles and exception. |
| // By the time |SpawnCrasher| has returned, the thread has already thrown an exception. |
| if (!SpawnCrasher(pe)) |
| return false; |
| |
| // We mark the exception to be handled. We need this because we pass on the exception to the |
| // handler, which will resume it before we get the control back. If we don't mark it as handled, |
| // the exception will bubble out of our environment. |
| return MarkExceptionAsHandled(pe); |
| } |
| |
| // Utilities --------------------------------------------------------------------------------------- |
| |
| inline void ValidateCrashSignature(const fuchsia::feedback::CrashReport& report, |
| const std::string& crash_signature) { |
| ASSERT_TRUE(report.has_crash_signature()); |
| EXPECT_EQ(report.crash_signature(), crash_signature); |
| } |
| |
| inline void ValidateCrashReport(const fuchsia::feedback::CrashReport& report, |
| const std::string& expected_program_name, |
| const std::string& expected_process_name, |
| const zx_koid_t expected_process_koid, |
| const std::string& expected_thread_name, |
| const zx_koid_t expected_thread_koid, |
| const std::map<std::string, std::string>& expected_annotations) { |
| ASSERT_TRUE(report.has_program_name()); |
| EXPECT_EQ(report.program_name(), expected_program_name); |
| |
| ASSERT_TRUE(report.has_specific_report()); |
| ASSERT_TRUE(report.specific_report().is_native()); |
| EXPECT_EQ(report.specific_report().native().process_name(), expected_process_name); |
| EXPECT_EQ(report.specific_report().native().process_koid(), expected_process_koid); |
| EXPECT_EQ(report.specific_report().native().thread_name(), expected_thread_name); |
| EXPECT_EQ(report.specific_report().native().thread_koid(), expected_thread_koid); |
| |
| if (!expected_annotations.empty()) { |
| ASSERT_TRUE(report.has_annotations()); |
| |
| // Infer the type of |matchers|. |
| auto matchers = std::vector({MatchesAnnotation("", "")}); |
| matchers.clear(); |
| |
| for (const auto& [k, v] : expected_annotations) { |
| matchers.push_back(MatchesAnnotation(k.c_str(), v.c_str())); |
| } |
| |
| EXPECT_THAT(report.annotations(), UnorderedElementsAreArray(matchers)); |
| } |
| } |
| |
| TEST_F(HandlerTest, NoIntrospectConnection) { |
| SetUpCrashReporter(); |
| |
| // Create the exception. |
| ExceptionContext exception; |
| ASSERT_TRUE(RetrieveExceptionContext(&exception)); |
| |
| bool called = false; |
| std::optional<std::string> out_moniker{std::nullopt}; |
| HandleException(std::move(exception.exception), kDefaultTimeout, |
| [&called, &out_moniker](const ::fidl::StringPtr moniker) { |
| called = true; |
| if (moniker.has_value()) { |
| out_moniker = moniker.value(); |
| } |
| }); |
| |
| ASSERT_TRUE(called); |
| ASSERT_FALSE(out_moniker.has_value()); |
| EXPECT_EQ(crash_reporter().reports().size(), 1u); |
| |
| // We kill the jobs. This kills the underlying process. We do this so that the crashed process |
| // doesn't get rescheduled. Otherwise the exception on the crash program would bubble out of our |
| // environment and create noise on the overall system. |
| exception.job.kill(); |
| } |
| |
| TEST_F(HandlerTest, NoCrashReporterConnectionV1) { |
| SetUpCrashIntrospect(); |
| |
| // Create the exception. |
| ExceptionContext exception; |
| ASSERT_TRUE(RetrieveExceptionContext(&exception)); |
| |
| zx::thread thread; |
| ASSERT_EQ(exception.exception.get_thread(&thread), ZX_OK); |
| const zx_koid_t thread_koid = fsl::GetKoid(thread.get()); |
| |
| const std::string kComponentUrl = "component_url"; |
| const std::vector<std::string> kRealmPath = {"realm", "path"}; |
| const std::string kComponentName = "component_name"; |
| introspect_v1().AddThreadKoidToComponentInfo(thread_koid, StubCrashIntrospectV1::ComponentInfo{ |
| .url = kComponentUrl, |
| .realm_path = kRealmPath, |
| .name = kComponentName, |
| }); |
| |
| bool called = false; |
| std::optional<std::string> out_moniker{std::nullopt}; |
| HandleException(std::move(exception.exception), kDefaultTimeout, |
| [&called, &out_moniker](const ::fidl::StringPtr moniker) { |
| called = true; |
| if (moniker.has_value()) { |
| out_moniker = moniker.value(); |
| } |
| }); |
| |
| ASSERT_TRUE(called); |
| ASSERT_TRUE(out_moniker.has_value()); |
| EXPECT_EQ(out_moniker.value(), "realm/path/component_name"); |
| |
| // The stub shouldn't be called. |
| EXPECT_EQ(crash_reporter().reports().size(), 0u); |
| |
| // We kill the jobs. This kills the underlying process. We do this so that the crashed process |
| // doesn't get rescheduled. Otherwise the exception on the crash program would bubble out of our |
| // environment and create noise on the overall system. |
| exception.job.kill(); |
| } |
| |
| TEST_F(HandlerTest, NoCrashReporterConnectionV2) { |
| SetUpCrashIntrospect(); |
| |
| // Create the exception. |
| ExceptionContext exception; |
| ASSERT_TRUE(RetrieveExceptionContext(&exception)); |
| |
| zx::thread thread; |
| ASSERT_EQ(exception.exception.get_thread(&thread), ZX_OK); |
| const zx_koid_t thread_koid = fsl::GetKoid(thread.get()); |
| |
| const std::string kComponentUrl = "component_url"; |
| const std::string kComponentMoniker = "/realm/path/component_name"; |
| introspect_v2().AddThreadKoidToComponentInfo(thread_koid, StubCrashIntrospectV2::ComponentInfo{ |
| .url = kComponentUrl, |
| .moniker = kComponentMoniker, |
| }); |
| |
| bool called = false; |
| std::optional<std::string> out_moniker{std::nullopt}; |
| HandleException(std::move(exception.exception), kDefaultTimeout, |
| [&called, &out_moniker](const ::fidl::StringPtr moniker) { |
| called = true; |
| if (moniker.has_value()) { |
| out_moniker = moniker.value(); |
| } |
| }); |
| |
| ASSERT_TRUE(called); |
| ASSERT_TRUE(out_moniker.has_value()); |
| EXPECT_EQ(out_moniker.value(), "realm/path/component_name"); |
| |
| // The stub shouldn't be called. |
| EXPECT_EQ(crash_reporter().reports().size(), 0u); |
| |
| // We kill the jobs. This kills the underlying process. We do this so that the crashed process |
| // doesn't get rescheduled. Otherwise the exception on the crash program would bubble out of our |
| // environment and create noise on the overall system. |
| exception.job.kill(); |
| } |
| |
| TEST_F(HandlerTest, NoExceptionV1) { |
| SetUpCrashReporter(); |
| SetUpCrashIntrospect(); |
| |
| // Create the exception. |
| ExceptionContext exception; |
| ASSERT_TRUE(RetrieveExceptionContext(&exception)); |
| |
| zx::process process; |
| ASSERT_EQ(exception.exception.get_process(&process), ZX_OK); |
| const std::string process_name = fsl::GetObjectName(process.get()); |
| const zx_koid_t process_koid = fsl::GetKoid(process.get()); |
| |
| zx::thread thread; |
| ASSERT_EQ(exception.exception.get_thread(&thread), ZX_OK); |
| const std::string thread_name = fsl::GetObjectName(thread.get()); |
| const zx_koid_t thread_koid = fsl::GetKoid(thread.get()); |
| |
| const std::string kComponentUrl = "component_url"; |
| const std::vector<std::string> kRealmPath = {"realm", "path"}; |
| const std::string kComponentName = "component_name"; |
| introspect_v1().AddThreadKoidToComponentInfo(thread_koid, StubCrashIntrospectV1::ComponentInfo{ |
| .url = kComponentUrl, |
| .realm_path = kRealmPath, |
| .name = kComponentName, |
| }); |
| exception.exception.reset(); |
| |
| bool called = false; |
| std::optional<std::string> out_moniker{std::nullopt}; |
| HandleException(std::move(process), std::move(thread), zx::duration::infinite(), |
| [&called, &out_moniker](const ::fidl::StringPtr moniker) { |
| called = true; |
| if (moniker.has_value()) { |
| out_moniker = moniker.value(); |
| } |
| }); |
| |
| ASSERT_TRUE(called); |
| ASSERT_TRUE(out_moniker.has_value()); |
| EXPECT_EQ(out_moniker.value(), "realm/path/component_name"); |
| |
| ASSERT_EQ(crash_reporter().reports().size(), 1u); |
| auto& report = crash_reporter().reports().front(); |
| |
| ValidateCrashReport(report, kComponentUrl, process_name, process_koid, thread_name, thread_koid, |
| { |
| {"crash.realm-path", "/realm/path"}, |
| }); |
| ValidateCrashSignature(report, "fuchsia-no-minidump-exception-expired"); |
| |
| // We kill the jobs. This kills the underlying process. We do this so that the crashed process |
| // doesn't get rescheduled. Otherwise the exception on the crash program would bubble out of our |
| // environment and create noise on the overall system. |
| exception.job.kill(); |
| } |
| |
| TEST_F(HandlerTest, NoExceptionV2) { |
| SetUpCrashReporter(); |
| SetUpCrashIntrospect(); |
| |
| // Create the exception. |
| ExceptionContext exception; |
| ASSERT_TRUE(RetrieveExceptionContext(&exception)); |
| |
| zx::process process; |
| ASSERT_EQ(exception.exception.get_process(&process), ZX_OK); |
| const std::string process_name = fsl::GetObjectName(process.get()); |
| const zx_koid_t process_koid = fsl::GetKoid(process.get()); |
| |
| zx::thread thread; |
| ASSERT_EQ(exception.exception.get_thread(&thread), ZX_OK); |
| const std::string thread_name = fsl::GetObjectName(thread.get()); |
| const zx_koid_t thread_koid = fsl::GetKoid(thread.get()); |
| |
| const std::string kComponentUrl = "component_url"; |
| const std::string kComponentMoniker = "/realm/path/component_name"; |
| introspect_v2().AddThreadKoidToComponentInfo(thread_koid, StubCrashIntrospectV2::ComponentInfo{ |
| .url = kComponentUrl, |
| .moniker = kComponentMoniker, |
| }); |
| exception.exception.reset(); |
| |
| bool called = false; |
| std::optional<std::string> out_moniker{std::nullopt}; |
| HandleException(std::move(process), std::move(thread), zx::duration::infinite(), |
| [&called, &out_moniker](const ::fidl::StringPtr moniker) { |
| called = true; |
| if (moniker.has_value()) { |
| out_moniker = moniker.value(); |
| } |
| }); |
| |
| ASSERT_TRUE(called); |
| ASSERT_TRUE(out_moniker.has_value()); |
| EXPECT_EQ(out_moniker.value(), "realm/path/component_name"); |
| |
| ASSERT_EQ(crash_reporter().reports().size(), 1u); |
| auto& report = crash_reporter().reports().front(); |
| |
| ValidateCrashReport(report, kComponentUrl, process_name, process_koid, thread_name, thread_koid, |
| {}); |
| ValidateCrashSignature(report, "fuchsia-no-minidump-exception-expired"); |
| |
| // We kill the jobs. This kills the underlying process. We do this so that the crashed process |
| // doesn't get rescheduled. Otherwise the exception on the crash program would bubble out of our |
| // environment and create noise on the overall system. |
| exception.job.kill(); |
| } |
| |
| } // namespace |
| } // namespace handler |
| } // namespace exceptions |
| } // namespace forensics |