| // 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/exception_broker/exception_broker.h" |
| |
| #include <fuchsia/feedback/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/sys/cpp/testing/service_directory_provider.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls/exception.h> |
| |
| #include <type_traits> |
| |
| #include <garnet/public/lib/fostr/fidl/fuchsia/exception/formatting.h> |
| #include <gtest/gtest.h> |
| #include <third_party/crashpad/snapshot/minidump/process_snapshot_minidump.h> |
| #include <third_party/crashpad/util/file/string_file.h> |
| |
| #include "src/developer/exception_broker/tests/crasher_wrapper.h" |
| #include "src/lib/fsl/handles/object_info.h" |
| #include "src/lib/fxl/test/test_settings.h" |
| #include "src/lib/syslog/cpp/logger.h" |
| |
| namespace fuchsia { |
| namespace exception { |
| |
| inline void ToString(const ExceptionType& value, std::ostream* os) { *os << value; } |
| |
| namespace { |
| |
| // ExceptionBroker unit test ----------------------------------------------------------------------- |
| // |
| // This test is meant to verify that the exception broker does the correct thing depending on the |
| // configuration. The main objective of this test is to verify that the connected crash reporter and |
| // exception handlers actually receive the exception from the broker. |
| |
| 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_; |
| }; |
| |
| // Test Setup -------------------------------------------------------------------------------------- |
| // |
| // Necessary elements for a fidl test to run. The ServiceDirectoryProvider is meant to mock the |
| // environment from which a process gets its services. This is the way we "inject" in our stub |
| // crash reporter instead of the real one. |
| |
| struct TestContext { |
| async::Loop loop; |
| sys::testing::ServiceDirectoryProvider services; |
| std::unique_ptr<StubCrashReporter> crash_reporter; |
| }; |
| |
| std::unique_ptr<TestContext> CreateTestContext() { |
| std::unique_ptr<TestContext> context(new TestContext{ |
| .loop = async::Loop(&kAsyncLoopConfigAttachToCurrentThread), |
| .services = sys::testing::ServiceDirectoryProvider{}, |
| .crash_reporter = std::make_unique<StubCrashReporter>(), |
| }); |
| |
| return context; |
| } |
| |
| // Runs a loop until |condition| is true. Does this by stopping every |step| to check the condition. |
| // If |condition| is never true, the thread will never leave this cycle. |
| // The test harness has to be able to handle this "hanging" case. |
| void RunUntil(TestContext* context, fit::function<bool()> condition, |
| zx::duration step = zx::msec(10)) { |
| while (!condition()) { |
| context->loop.Run(zx::deadline_after(step)); |
| } |
| } |
| |
| bool RetrieveExceptionContext(ExceptionContext* pe) { |
| // Create a process that crashes and obtain the relevant handles and exception. |
| // By the time |SpawnCrasher| has returned, the process 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); |
| } |
| |
| ExceptionInfo ExceptionContextToExceptionInfo(const ExceptionContext& pe) { |
| // Translate the exception to the fidl format. |
| ExceptionInfo exception_info; |
| exception_info.process_koid = pe.exception_info.pid; |
| exception_info.thread_koid = pe.exception_info.tid; |
| exception_info.type = static_cast<ExceptionType>(pe.exception_info.type); |
| |
| return exception_info; |
| } |
| |
| // Utilities --------------------------------------------------------------------------------------- |
| |
| inline void ValidateReport(const fuchsia::feedback::CrashReport& report, bool validate_minidump) { |
| ASSERT_TRUE(report.has_program_name()); |
| |
| ASSERT_TRUE(report.has_specific_report()); |
| const fuchsia::feedback::SpecificCrashReport& specific_report = report.specific_report(); |
| |
| ASSERT_TRUE(specific_report.is_native()); |
| const fuchsia::feedback::NativeCrashReport& native_report = specific_report.native(); |
| |
| // If the broker could not get a minidump, it will not send a mem buffer. |
| if (!validate_minidump) { |
| ASSERT_FALSE(native_report.has_minidump()); |
| return; |
| } |
| |
| EXPECT_EQ(report.program_name(), "crasher"); |
| |
| ASSERT_TRUE(native_report.has_minidump()); |
| const zx::vmo& minidump_vmo = native_report.minidump().vmo; |
| |
| uint64_t vmo_size; |
| ASSERT_EQ(minidump_vmo.get_size(&vmo_size), ZX_OK); |
| |
| auto buf = std::make_unique<uint8_t[]>(vmo_size); |
| ASSERT_EQ(minidump_vmo.read(buf.get(), 0, vmo_size), ZX_OK); |
| |
| // Read the vmo back into a file writer/reader interface. |
| crashpad::StringFile string_file; |
| string_file.Write(buf.get(), vmo_size); |
| |
| // Move the cursor to the beggining of the file. |
| ASSERT_EQ(string_file.Seek(0, SEEK_SET), 0); |
| |
| // We verify that the minidump snapshot can validly read the file. |
| crashpad::ProcessSnapshotMinidump minidump_snapshot; |
| ASSERT_TRUE(minidump_snapshot.Initialize(&string_file)); |
| } |
| |
| // Tests ------------------------------------------------------------------------------------------- |
| |
| TEST(ExceptionBroker, CallingMultipleExceptions) { |
| auto test_context = CreateTestContext(); |
| |
| // We add the service we're injecting. |
| test_context->services.AddService(test_context->crash_reporter->GetHandler()); |
| |
| auto broker = ExceptionBroker::Create(test_context->loop.dispatcher(), |
| test_context->services.service_directory()); |
| ASSERT_TRUE(broker); |
| |
| // We create multiple exceptions. |
| ExceptionContext excps[3]; |
| ASSERT_TRUE(RetrieveExceptionContext(excps + 0)); |
| ASSERT_TRUE(RetrieveExceptionContext(excps + 1)); |
| ASSERT_TRUE(RetrieveExceptionContext(excps + 2)); |
| |
| // Get the fidl representation of the exception. |
| ExceptionInfo infos[3]; |
| infos[0] = ExceptionContextToExceptionInfo(excps[0]); |
| infos[1] = ExceptionContextToExceptionInfo(excps[1]); |
| infos[2] = ExceptionContextToExceptionInfo(excps[2]); |
| |
| // It's not easy to pass array references to lambdas. |
| bool cb_call0 = false; |
| bool cb_call1 = false; |
| bool cb_call2 = false; |
| broker->OnException(std::move(excps[0].exception), infos[0], [&cb_call0]() { cb_call0 = true; }); |
| broker->OnException(std::move(excps[1].exception), infos[1], [&cb_call1]() { cb_call1 = true; }); |
| broker->OnException(std::move(excps[2].exception), infos[2], [&cb_call2]() { cb_call2 = true; }); |
| |
| // There should be many connections opened. |
| ASSERT_EQ(broker->connections().size(), 3u); |
| |
| // We wait until the crash reporter has received all exceptions. |
| RunUntil(test_context.get(), |
| [&test_context]() { return test_context->crash_reporter->reports().size() == 3u; }); |
| |
| EXPECT_TRUE(cb_call0); |
| EXPECT_TRUE(cb_call1); |
| EXPECT_TRUE(cb_call2); |
| |
| // All connections should be killed now. |
| EXPECT_EQ(broker->connections().size(), 0u); |
| |
| auto& reports = test_context->crash_reporter->reports(); |
| ValidateReport(reports[0], true); |
| ValidateReport(reports[1], true); |
| ValidateReport(reports[2], true); |
| |
| // Process limbo should be empty. |
| ASSERT_EQ(broker->limbo_manager().limbo().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. |
| excps[0].job.kill(); |
| excps[1].job.kill(); |
| excps[2].job.kill(); |
| } |
| |
| TEST(ExceptionBroker, NoConnection) { |
| // We don't inject a stub service. This will make connecting to the service fail. |
| auto test_context = CreateTestContext(); |
| |
| auto broker = ExceptionBroker::Create(test_context->loop.dispatcher(), |
| test_context->services.service_directory()); |
| ASSERT_TRUE(broker); |
| |
| // Create the exception. |
| ExceptionContext exception; |
| ASSERT_TRUE(RetrieveExceptionContext(&exception)); |
| ExceptionInfo info = ExceptionContextToExceptionInfo(exception); |
| |
| bool called = false; |
| broker->OnException(std::move(exception.exception), info, [&called]() { called = true; }); |
| |
| // There should be an outgoing connection. |
| ASSERT_EQ(broker->connections().size(), 1u); |
| |
| RunUntil(test_context.get(), [&broker]() { return broker->connections().empty(); }); |
| ASSERT_TRUE(called); |
| |
| // The stub shouldn't be called. |
| ASSERT_EQ(test_context->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(); |
| |
| // Process limbo should be empty. |
| ASSERT_EQ(broker->limbo_manager().limbo().size(), 0u); |
| } |
| |
| TEST(ExceptionBroker, GettingInvalidVMO) { |
| auto test_context = CreateTestContext(); |
| test_context->services.AddService(test_context->crash_reporter->GetHandler()); |
| |
| auto broker = ExceptionBroker::Create(test_context->loop.dispatcher(), |
| test_context->services.service_directory()); |
| ASSERT_TRUE(broker); |
| |
| // We create a bogus exception, which will fail to create a valid VMO. |
| bool called = false; |
| ExceptionInfo info = {}; |
| broker->OnException({}, info, [&called]() { called = true; }); |
| |
| ASSERT_EQ(broker->connections().size(), 1u); |
| RunUntil(test_context.get(), [&broker]() { return broker->connections().empty(); }); |
| ASSERT_TRUE(called); |
| |
| ASSERT_EQ(test_context->crash_reporter->reports().size(), 1u); |
| auto& report = test_context->crash_reporter->reports().front(); |
| |
| ValidateReport(report, false); |
| } |
| |
| } // namespace |
| } // namespace exception |
| } // namespace fuchsia |
| |
| |