blob: a97863e022b9cdd81c4ee40602a8ff86ad960636 [file] [log] [blame]
// Copyright 2018 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/crashpad_agent/crashpad_agent.h"
#include <fuchsia/crash/cpp/fidl.h>
#include <fuchsia/mem/cpp/fidl.h>
#include <lib/fdio/spawn.h>
#include <lib/fsl/vmo/strings.h>
#include <lib/gtest/real_loop_fixture.h>
#include <lib/sys/cpp/testing/service_directory_provider.h>
#include <lib/syslog/cpp/logger.h>
#include <lib/zx/job.h>
#include <lib/zx/port.h>
#include <lib/zx/process.h>
#include <lib/zx/thread.h>
#include <stdint.h>
#include <zircon/errors.h>
#include <zircon/time.h>
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "src/developer/crashpad_agent/config.h"
#include "src/developer/crashpad_agent/tests/stub_crash_server.h"
#include "src/developer/crashpad_agent/tests/stub_feedback_data_provider.h"
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/lib/files/scoped_temp_dir.h"
#include "src/lib/fxl/logging.h"
#include "third_party/googletest/googlemock/include/gmock/gmock.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"
namespace fuchsia {
namespace crash {
namespace {
// We keep the local Crashpad database size under a certain value. As we want to
// check the produced attachments in the database, we should set the size to be
// at least the total size for a single report so that it does not get cleaned
// up before we are able to inspect its attachments.
// For now, a single report should take up to 1MB.
constexpr uint64_t kMaxTotalReportSizeInKb = 1024u;
constexpr bool alwaysReturnSuccess = true;
constexpr bool alwaysReturnFailure = false;
// Unit-tests the implementation of the fuchsia.crash.Analyzer FIDL interface.
//
// This does not test the environment service. It directly instantiates the
// class, without connecting through FIDL.
class CrashpadAgentTest : public gtest::RealLoopFixture {
public:
void SetUp() override {
// The underlying agent is initialized with a default config, but can
// be reset via ResetAgent() if a different config is necessary.
ResetAgent(Config{/*local_crashpad_database_path=*/database_path_.path(),
/*max_crashpad_database_size_in_kb=*/
kMaxTotalReportSizeInKb,
/*enable_upload_to_crash_server=*/true,
/*crash_server_url=*/
std::make_unique<std::string>(kStubCrashServerUrl)},
std::make_unique<StubCrashServer>(alwaysReturnSuccess));
}
protected:
// Resets the underlying agent using the given |config| and |crash_server|.
void ResetAgent(Config config,
std::unique_ptr<StubCrashServer> crash_server) {
FXL_CHECK(config.enable_upload_to_crash_server ^ !crash_server);
crash_server_ = std::move(crash_server);
// "attachments" should be kept in sync with the value defined in
// //crashpad/client/crash_report_database_generic.cc
attachments_dir_ =
files::JoinPath(config.local_crashpad_database_path, "attachments");
agent_ = CrashpadAgent::TryCreate(
dispatcher(), service_directory_provider_.service_directory(),
std::move(config), std::move(crash_server_));
FXL_CHECK(agent_);
}
// Resets the underlying agent using the given |config|.
void ResetAgent(Config config) {
FXL_CHECK(!config.enable_upload_to_crash_server);
return ResetAgent(std::move(config), /*crash_server=*/nullptr);
}
// Resets the underlying stub feedback data provider and registers it in the
// |service_directory_provider_|.
//
// This can only be done once per test as ServiceDirectoryProvider does not
// allow overridding a service. Hence why it is not in the SetUp().
void ResetFeedbackDataProvider(
std::unique_ptr<StubFeedbackDataProvider> stub_feedback_data_provider) {
stub_feedback_data_provider_ = std::move(stub_feedback_data_provider);
if (stub_feedback_data_provider_) {
FXL_CHECK(service_directory_provider_.AddService(
stub_feedback_data_provider_->GetHandler()) == ZX_OK);
}
}
// Checks that there is:
// * only one set of attachments
// * the set of attachment filenames matches the concatenation of
// |expected_extra_attachments| and feedback_attachment_keys_
// * no attachment is empty
// in the local Crashpad database.
void CheckAttachments(
const std::vector<std::string>& expected_extra_attachments = {}) {
const std::vector<std::string> subdirs = GetAttachmentSubdirs();
// We expect a single crash report to have been generated.
ASSERT_EQ(subdirs.size(), 1u);
// We expect as attachments the ones returned by the feedback::DataProvider
// and the extra ones specific to the crash analysis flow under test.
std::vector<std::string> expected_attachments = expected_extra_attachments;
expected_attachments.insert(
expected_attachments.begin(),
stub_feedback_data_provider_->attachment_keys().begin(),
stub_feedback_data_provider_->attachment_keys().end());
std::vector<std::string> attachments;
const std::string report_attachments_dir =
files::JoinPath(attachments_dir_, subdirs[0]);
ASSERT_TRUE(files::ReadDirContents(report_attachments_dir, &attachments));
RemoveCurrentDirectory(&attachments);
EXPECT_THAT(attachments,
testing::UnorderedElementsAreArray(expected_attachments));
for (const std::string& attachment : attachments) {
uint64_t size;
ASSERT_TRUE(files::GetFileSize(
files::JoinPath(report_attachments_dir, attachment), &size));
EXPECT_GT(size, 0u) << "attachment file '" << attachment
<< "' shouldn't be empty";
}
}
// Returns all the attachment subdirectories under the over-arching attachment
// directory. Each subdirectory corresponds to one local crash report.
std::vector<std::string> GetAttachmentSubdirs() {
std::vector<std::string> subdirs;
FXL_CHECK(files::ReadDirContents(attachments_dir_, &subdirs));
RemoveCurrentDirectory(&subdirs);
return subdirs;
}
// Runs one crash analysis. Useful to test shared logic among all crash
// analysis flows.
//
// |attachment| allows to control the lower bound of the size of the report.
//
// Today we use the kernel panic flow because it requires fewer arguments to
// set up.
Analyzer_OnKernelPanicCrashLog_Result RunOneCrashAnalysis(
const std::string& attachment) {
fuchsia::mem::Buffer crash_log;
FXL_CHECK(fsl::VmoFromString(attachment, &crash_log));
Analyzer_OnKernelPanicCrashLog_Result out_result;
bool has_out_result = false;
agent_->OnKernelPanicCrashLog(
std::move(crash_log),
[&out_result,
&has_out_result](Analyzer_OnKernelPanicCrashLog_Result result) {
out_result = std::move(result);
has_out_result = true;
});
RunLoopUntil([&has_out_result] { return has_out_result; });
return out_result;
}
// Runs one crash analysis. Useful to test shared logic among all crash
// analysis flows.
//
// Today we use the kernel panic flow because it requires fewer arguments to
// set up.
Analyzer_OnKernelPanicCrashLog_Result RunOneCrashAnalysis() {
return RunOneCrashAnalysis("irrelevant, just not empty");
}
uint64_t total_num_feedback_data_provider_bindings() {
return stub_feedback_data_provider_->total_num_bindings();
}
size_t current_num_feedback_data_provider_bindings() {
return stub_feedback_data_provider_->current_num_bindings();
}
std::unique_ptr<CrashpadAgent> agent_;
files::ScopedTempDir database_path_;
std::unique_ptr<StubCrashServer> crash_server_;
private:
void RemoveCurrentDirectory(std::vector<std::string>* dirs) {
dirs->erase(std::remove(dirs->begin(), dirs->end(), "."), dirs->end());
}
::sys::testing::ServiceDirectoryProvider service_directory_provider_;
std::unique_ptr<StubFeedbackDataProvider> stub_feedback_data_provider_;
std::string attachments_dir_;
};
TEST_F(CrashpadAgentTest, OnNativeException_C_Basic) {
// We create a parent job and a child job. The child job will spawn the
// crashing program and analyze the crash. The parent job is just here to
// swallow the exception potentially bubbling up from the child job once the
// exception has been handled by the test agent (today this is the case as the
// Crashpad exception handler RESUME_TRY_NEXTs the thread).
zx::job parent_job;
zx::port parent_exception_port;
zx::job job;
zx::port exception_port;
zx::process process;
zx::thread thread;
// Create the child jobs of the current job now so we can bind to the
// exception port before spawning the crashing program.
zx::unowned_job current_job(zx_job_default());
ASSERT_EQ(zx::job::create(*current_job, 0, &parent_job), ZX_OK);
ASSERT_EQ(zx::port::create(0u, &parent_exception_port), ZX_OK);
ASSERT_EQ(zx_task_bind_exception_port(parent_job.get(),
parent_exception_port.get(), 0u, 0u),
ZX_OK);
ASSERT_EQ(zx::job::create(parent_job, 0, &job), ZX_OK);
ASSERT_EQ(zx::port::create(0u, &exception_port), ZX_OK);
ASSERT_EQ(
zx_task_bind_exception_port(job.get(), exception_port.get(), 0u, 0u),
ZX_OK);
// Create child process using our utility program `crasher` that will crash on
// startup.
const char* argv[] = {"crasher", nullptr};
char err_msg[FDIO_SPAWN_ERR_MSG_MAX_LENGTH];
ASSERT_EQ(fdio_spawn_etc(job.get(), FDIO_SPAWN_CLONE_ALL,
"/pkg/bin/crasher_exe", argv, nullptr, 0, nullptr,
process.reset_and_get_address(), err_msg),
ZX_OK)
<< err_msg;
// Wait up to 1s for the exception to be thrown. We need the process and
// thread to be blocked in the exception for Crashpad to analyze them.
zx_port_packet_t packet;
ASSERT_EQ(exception_port.wait(zx::deadline_after(zx::sec(1)), &packet),
ZX_OK);
ASSERT_TRUE(ZX_PKT_IS_EXCEPTION(packet.type));
// Get the one thread from the child process.
zx_koid_t thread_ids[1];
size_t num_ids;
ASSERT_EQ(process.get_info(ZX_INFO_PROCESS_THREADS, thread_ids,
sizeof(zx_koid_t), &num_ids, nullptr),
ZX_OK);
ASSERT_EQ(num_ids, 1u);
ASSERT_EQ(process.get_child(thread_ids[0], ZX_RIGHT_SAME_RIGHTS, &thread),
ZX_OK);
// Test crash analysis.
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
Analyzer_OnNativeException_Result out_result;
bool has_out_result = false;
agent_->OnNativeException(
std::move(process), std::move(thread), std::move(exception_port),
[&out_result, &has_out_result](Analyzer_OnNativeException_Result result) {
out_result = std::move(result);
has_out_result = true;
});
RunLoopUntil([&has_out_result] { return has_out_result; });
EXPECT_TRUE(out_result.is_response());
CheckAttachments();
// The parent job just swallows the exception, i.e. not RESUME_TRY_NEXT it,
// to not trigger the real agent attached to the root job.
thread.resume_from_exception(
parent_exception_port,
0u /*no options to mark the exception as handled*/);
// We kill the job so that it doesn't try to reschedule the process, which
// would crash again, but this time would be handled by the real agent
// attached to the root job as the exception has already been handled by the
// parent and child jobs.
job.kill();
}
TEST_F(CrashpadAgentTest, OnManagedRuntimeException_Dart_Basic) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
GenericException exception = {};
const std::string type = "FileSystemException";
std::copy(type.begin(), type.end(), exception.type.data());
const std::string message = "cannot open file";
std::copy(message.begin(), message.end(), exception.message.data());
ASSERT_TRUE(fsl::VmoFromString("#0", &exception.stack_trace));
ManagedRuntimeException dart_exception;
dart_exception.set_dart(std::move(exception));
Analyzer_OnManagedRuntimeException_Result out_result;
bool has_out_result = false;
agent_->OnManagedRuntimeException(
"component_url", std::move(dart_exception),
[&out_result,
&has_out_result](Analyzer_OnManagedRuntimeException_Result result) {
out_result = std::move(result);
has_out_result = true;
});
RunLoopUntil([&has_out_result] { return has_out_result; });
EXPECT_TRUE(out_result.is_response());
CheckAttachments({"DartError"});
}
TEST_F(CrashpadAgentTest, OnManagedRuntimeException_UnknownLanguage_Basic) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
UnknownException exception;
ASSERT_TRUE(fsl::VmoFromString("#0", &exception.data));
ManagedRuntimeException unknown_exception;
unknown_exception.set_unknown_(std::move(exception));
Analyzer_OnManagedRuntimeException_Result out_result;
bool has_out_result = false;
agent_->OnManagedRuntimeException(
"component_url", std::move(unknown_exception),
[&out_result,
&has_out_result](Analyzer_OnManagedRuntimeException_Result result) {
out_result = std::move(result);
has_out_result = true;
});
RunLoopUntil([&has_out_result] { return has_out_result; });
EXPECT_TRUE(out_result.is_response());
CheckAttachments({"data"});
}
TEST_F(CrashpadAgentTest, OnKernelPanicCrashLog_Basic) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
fuchsia::mem::Buffer crash_log;
ASSERT_TRUE(fsl::VmoFromString("ZIRCON KERNEL PANIC", &crash_log));
Analyzer_OnKernelPanicCrashLog_Result out_result;
bool has_out_result = false;
agent_->OnKernelPanicCrashLog(
std::move(crash_log), [&out_result, &has_out_result](
Analyzer_OnKernelPanicCrashLog_Result result) {
out_result = std::move(result);
has_out_result = true;
});
RunLoopUntil([&has_out_result] { return has_out_result; });
EXPECT_TRUE(out_result.is_response());
CheckAttachments({"kernel_panic_crash_log"});
}
TEST_F(CrashpadAgentTest, PruneDatabase_ZeroSize) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
// We reset the agent with a max database size of 0, meaning reports will
// get cleaned up before the end of the |agent_| call.
ResetAgent(Config{/*local_crashpad_database_path=*/database_path_.path(),
/*max_crashpad_database_size_in_kb=*/0u,
/*enable_upload_to_crash_server=*/false,
/*crash_server_url=*/nullptr});
// We generate a crash report.
EXPECT_TRUE(RunOneCrashAnalysis().is_response());
// We check that all the attachments have been cleaned up.
EXPECT_TRUE(GetAttachmentSubdirs().empty());
}
std::string GenerateString(const uint64_t string_size_in_kb) {
std::string str;
for (size_t i = 0; i < string_size_in_kb * 1024; ++i) {
str.push_back(static_cast<char>(i % 128));
}
return str;
}
TEST_F(CrashpadAgentTest, PruneDatabase_SizeForOneReport) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
// We reset the agent with a max database size equivalent to the expected
// size of a report plus the value of an especially large attachment.
const uint64_t crash_log_size_in_kb = 2u * kMaxTotalReportSizeInKb;
const std::string large_string = GenerateString(crash_log_size_in_kb);
ResetAgent(
Config{/*local_crashpad_database_path=*/database_path_.path(),
/*max_crashpad_database_size_in_kb=*/kMaxTotalReportSizeInKb +
crash_log_size_in_kb,
/*enable_upload_to_crash_server=*/false,
/*crash_server_url=*/nullptr});
// We generate a first crash report.
EXPECT_TRUE(RunOneCrashAnalysis(large_string).is_response());
// We check that only one set of attachments is there.
const std::vector<std::string> attachment_subdirs = GetAttachmentSubdirs();
ASSERT_EQ(attachment_subdirs.size(), 1u);
// We sleep for one second to guarantee a different creation time for the
// next crash report.
zx::nanosleep(zx::deadline_after(zx::sec(1)));
// We generate a new crash report.
EXPECT_TRUE(RunOneCrashAnalysis(large_string).is_response());
// We check that only one set of attachments is there and that it is a
// different directory than previously (the directory name is the local crash
// report ID).
const std::vector<std::string> new_attachment_subdirs =
GetAttachmentSubdirs();
EXPECT_EQ(new_attachment_subdirs.size(), 1u);
EXPECT_THAT(
new_attachment_subdirs,
testing::Not(testing::UnorderedElementsAreArray(attachment_subdirs)));
}
TEST_F(CrashpadAgentTest, AnalysisFailOnFailedUpload) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
ResetAgent(Config{/*local_crashpad_database_path=*/database_path_.path(),
/*max_crashpad_database_size_in_kb=*/
kMaxTotalReportSizeInKb,
/*enable_upload_to_crash_server=*/true,
/*crash_server_url=*/
std::make_unique<std::string>(kStubCrashServerUrl)},
std::make_unique<StubCrashServer>(alwaysReturnFailure));
EXPECT_TRUE(RunOneCrashAnalysis().is_err());
}
TEST_F(CrashpadAgentTest, AnalysisSucceedOnNoUpload) {
ResetFeedbackDataProvider(std::make_unique<StubFeedbackDataProvider>());
ResetAgent(Config{/*local_crashpad_database_path=*/database_path_.path(),
/*max_crashpad_database_size_in_kb=*/
kMaxTotalReportSizeInKb,
/*enable_upload_to_crash_server=*/false,
/*crash_server_url=*/nullptr});
EXPECT_TRUE(RunOneCrashAnalysis().is_response());
}
TEST_F(CrashpadAgentTest, AnalysisSucceedOnNoFeedbackAttachments) {
ResetFeedbackDataProvider(
std::make_unique<StubFeedbackDataProviderReturnsNoAttachment>());
EXPECT_TRUE(RunOneCrashAnalysis().is_response());
CheckAttachments({"kernel_panic_crash_log"});
}
TEST_F(CrashpadAgentTest, AnalysisSucceedOnNoFeedbackAnnotations) {
ResetFeedbackDataProvider(
std::make_unique<StubFeedbackDataProviderReturnsNoAnnotation>());
EXPECT_TRUE(RunOneCrashAnalysis().is_response());
}
TEST_F(CrashpadAgentTest, AnalysisSucceedOnNoFeedbackData) {
ResetFeedbackDataProvider(
std::make_unique<StubFeedbackDataProviderReturnsNoData>());
EXPECT_TRUE(RunOneCrashAnalysis().is_response());
CheckAttachments({"kernel_panic_crash_log"});
}
TEST_F(CrashpadAgentTest, OneFeedbackDataProviderConnectionPerAnalysis) {
// We use a stub that returns no data as we are not interested in the
// payload, just the number of different connections to the stub.
ResetFeedbackDataProvider(
std::make_unique<StubFeedbackDataProviderReturnsNoData>());
const size_t num_calls = 5u;
std::vector<Analyzer_OnKernelPanicCrashLog_Result> out_results;
for (size_t i = 0; i < num_calls; i++) {
fuchsia::mem::Buffer crash_log;
FXL_CHECK(fsl::VmoFromString("irrelevant, just not empty", &crash_log));
agent_->OnKernelPanicCrashLog(
std::move(crash_log),
[&out_results](Analyzer_OnKernelPanicCrashLog_Result result) {
out_results.push_back(std::move(result));
});
}
RunLoopUntil(
[&out_results, num_calls] { return out_results.size() == num_calls; });
EXPECT_EQ(total_num_feedback_data_provider_bindings(), num_calls);
// The unbinding is asynchronous so we need to run the loop until all the
// outstanding connections are actually close in the stub.
RunLoopUntil(
[this] { return current_num_feedback_data_provider_bindings() == 0u; });
}
} // namespace
} // namespace crash
} // namespace fuchsia
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
syslog::InitLogger({"crash", "test"});
return RUN_ALL_TESTS();
}