blob: 50ebe869fa797c0852b2b73ef80edcfba226bb7a [file] [log] [blame]
// 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