blob: 5a9d3df00296f6b2cf761309fd6a3988f2ca776e [file] [log] [blame]
// Copyright 2018 The Chromium 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 "components/gwp_asan/crash_handler/crash_handler.h"
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/no_destructor.h"
#include "base/process/process_metrics.h"
#include "base/strings/stringprintf.h"
#include "base/test/gtest_util.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "build/build_config.h"
#include "components/gwp_asan/client/guarded_page_allocator.h"
#include "components/gwp_asan/common/crash_key_name.h"
#include "components/gwp_asan/crash_handler/crash.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/crashpad/crashpad/client/annotation.h"
#include "third_party/crashpad/crashpad/client/crash_report_database.h"
#include "third_party/crashpad/crashpad/client/crashpad_client.h"
#include "third_party/crashpad/crashpad/handler/handler_main.h"
#include "third_party/crashpad/crashpad/snapshot/minidump/process_snapshot_minidump.h"
#include "third_party/crashpad/crashpad/tools/tool_support.h"
namespace gwp_asan {
namespace internal {
namespace {
class GwpAsanTest : public testing::Test {};
constexpr size_t kAllocationSize = 902;
constexpr int kSuccess = 0;
constexpr size_t kTotalPages = AllocatorState::kGpaMaxPages;
int HandlerMainAdaptor(int argc, char* argv[]) {
crashpad::UserStreamDataSources user_stream_data_sources;
user_stream_data_sources.push_back(
std::make_unique<gwp_asan::UserStreamDataSource>());
return crashpad::HandlerMain(argc, argv, &user_stream_data_sources);
}
// Child process that runs the crashpad handler.
MULTIPROCESS_TEST_MAIN(CrashpadHandler) {
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
// Remove the --test-child-process argument from argv and launch crashpad.
#if defined(OS_WIN)
std::vector<wchar_t*> argv;
for (auto& arg : cmd_line->argv()) {
if (arg.find(L"test-child-process") == std::string::npos)
argv.push_back(const_cast<wchar_t*>(arg.c_str()));
}
crashpad::ToolSupport::Wmain(argv.size(), argv.data(), HandlerMainAdaptor);
#else
std::vector<char*> argv;
for (auto& arg : cmd_line->argv()) {
if (arg.find("test-child-process") == std::string::npos)
argv.push_back(const_cast<char*>(arg.c_str()));
}
HandlerMainAdaptor(argv.size(), argv.data());
#endif
return 0;
}
// Child process that launches the crashpad handler and then crashes.
MULTIPROCESS_TEST_MAIN(CrashingProcess) {
base::NoDestructor<GuardedPageAllocator> gpa;
gpa->Init(AllocatorState::kGpaMaxPages, kTotalPages);
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
base::FilePath directory = cmd_line->GetSwitchValuePath("directory");
CHECK(!directory.empty());
std::string test_name = cmd_line->GetSwitchValueASCII("test-name");
CHECK(!test_name.empty());
static char gpa_addr_buf[32];
snprintf(gpa_addr_buf, sizeof(gpa_addr_buf), "%zx",
gpa->GetCrashKeyAddress());
static crashpad::Annotation gpa_annotation(
crashpad::Annotation::Type::kString, kGpaCrashKey, gpa_addr_buf);
gpa_annotation.SetSize(strlen(gpa_addr_buf));
base::FilePath metrics_dir(FILE_PATH_LITERAL(""));
std::map<std::string, std::string> annotations;
std::vector<std::string> arguments;
arguments.push_back("--test-child-process=CrashpadHandler");
crashpad::CrashpadClient* client = new crashpad::CrashpadClient();
bool handler = client->StartHandler(/* handler */ cmd_line->GetProgram(),
/* database */ directory,
/* metrics_dir */ metrics_dir,
/* url */ "",
/* annotations */ annotations,
/* arguments */ arguments,
/* restartable */ false,
/* asynchronous_start */ false);
if (!handler) {
LOG(ERROR) << "Crash handler failed to launch";
return kSuccess;
}
if (test_name == "UseAfterFree") {
void* ptr = gpa->Allocate(kAllocationSize);
gpa->Deallocate(ptr);
*(int*)ptr = 0;
} else if (test_name == "DoubleFree") {
void* ptr = gpa->Allocate(kAllocationSize);
gpa->Deallocate(ptr);
gpa->Deallocate(ptr);
} else if (test_name == "Underflow") {
void* ptr = gpa->Allocate(kAllocationSize);
for (size_t i = 0; i < base::GetPageSize(); i++)
((unsigned char*)ptr)[-i] = 0;
} else if (test_name == "Overflow") {
void* ptr = gpa->Allocate(kAllocationSize);
for (size_t i = 0; i <= base::GetPageSize(); i++)
((unsigned char*)ptr)[i] = 0;
} else if (test_name == "UnrelatedException") {
__builtin_trap();
} else {
LOG(ERROR) << "Unknown test name " << test_name;
}
LOG(ERROR) << "This return should never be reached.";
return kSuccess;
}
class CrashHandlerTest : public base::MultiProcessTest {
protected:
// Launch a child process and wait for it to crash. Set |gwp_asan_found_| if a
// GWP-ASan data was found and if so, read it into |proto_|.
void SetUp() final {
base::ScopedTempDir database_dir;
ASSERT_TRUE(database_dir.CreateUniqueTempDir());
ASSERT_TRUE(runTestProcess(
database_dir.GetPath(),
::testing::UnitTest::GetInstance()->current_test_info()->name()));
bool minidump_found;
readGwpAsanStreamFromCrash(database_dir.GetPath(), &minidump_found,
&gwp_asan_found_, &proto_);
ASSERT_TRUE(minidump_found);
}
// Launch a second process that installs a crashpad handler and causes an
// exception of type test_name, then validate that it exited successfully.
// crashpad is initialized to write to the given database directory.
bool runTestProcess(const base::FilePath& database_dir,
const char* test_name) {
base::CommandLine cmd_line =
base::GetMultiProcessTestChildBaseCommandLine();
cmd_line.AppendSwitchPath("directory", database_dir);
cmd_line.AppendSwitchASCII("test-name", test_name);
base::LaunchOptions options;
#if defined(OS_WIN)
options.start_hidden = true;
#endif // defined(OS_WIN)
base::Process process =
base::SpawnMultiProcessTestChild("CrashingProcess", cmd_line, options);
int exit_code = -1;
EXPECT_TRUE(process.WaitForExitWithTimeout(
TestTimeouts::action_max_timeout(), &exit_code));
EXPECT_NE(exit_code, kSuccess);
return (exit_code != kSuccess);
}
// Given a directory with a single crashpad exception, read and parse the
// minidump and identify whether it has a GWP-ASan stream. If it successfully
// found a minidump, it writes true to minidump_found. If it sucessfully found
// a GWP-ASan stream in the minidump, it writes true to gwp_asan_found and
// parses the protobuf into into proto_out.
void readGwpAsanStreamFromCrash(const base::FilePath& database_dir,
bool* minidump_found,
bool* gwp_asan_found,
gwp_asan::Crash* proto_out) {
*minidump_found = *gwp_asan_found = false;
auto database =
crashpad::CrashReportDatabase::InitializeWithoutCreating(database_dir);
std::vector<crashpad::CrashReportDatabase::Report> reports;
ASSERT_EQ(database->GetPendingReports(&reports),
crashpad::CrashReportDatabase::kNoError);
ASSERT_EQ(reports.size(), 1U);
crashpad::FileReader minidump_file_reader;
ASSERT_TRUE(minidump_file_reader.Open(reports[0].file_path));
crashpad::ProcessSnapshotMinidump minidump_process_snapshot;
ASSERT_TRUE(minidump_process_snapshot.Initialize(&minidump_file_reader));
*minidump_found = true;
auto custom_streams = minidump_process_snapshot.CustomMinidumpStreams();
for (auto* stream : custom_streams) {
if (stream->stream_type() == static_cast<crashpad::MinidumpStreamType>(
kGwpAsanMinidumpStreamType)) {
ASSERT_TRUE(proto_out->ParseFromArray(stream->data().data(),
stream->data().size()));
*gwp_asan_found = true;
return;
}
}
}
void checkProto(Crash_ErrorType error_type, bool has_deallocation) {
EXPECT_TRUE(proto_.has_error_type());
EXPECT_EQ(proto_.error_type(), error_type);
EXPECT_TRUE(proto_.has_allocation_address());
EXPECT_TRUE(proto_.has_allocation_size());
EXPECT_EQ(proto_.allocation_size(), kAllocationSize);
EXPECT_TRUE(proto_.has_allocation());
EXPECT_TRUE(proto_.allocation().has_thread_id());
EXPECT_NE(proto_.allocation().thread_id(), base::kInvalidThreadId);
EXPECT_GT(proto_.allocation().stack_trace_size(), 0);
EXPECT_EQ(proto_.has_deallocation(), has_deallocation);
if (has_deallocation) {
EXPECT_TRUE(proto_.deallocation().has_thread_id());
EXPECT_NE(proto_.deallocation().thread_id(), base::kInvalidThreadId);
EXPECT_EQ(proto_.allocation().thread_id(),
proto_.deallocation().thread_id());
EXPECT_GT(proto_.deallocation().stack_trace_size(), 0);
}
EXPECT_TRUE(proto_.has_region_start());
EXPECT_TRUE(proto_.has_region_size());
EXPECT_EQ(proto_.region_start() & (base::GetPageSize() - 1), 0U);
EXPECT_EQ(proto_.region_size(),
base::GetPageSize() * (2 * kTotalPages + 1));
}
gwp_asan::Crash proto_;
bool gwp_asan_found_;
};
#if defined(ADDRESS_SANITIZER) && defined(OS_WIN)
// ASan intercepts crashes and crashpad doesn't have a chance to see them.
#define MAYBE_DISABLED(name) DISABLED_ ##name
#else
#define MAYBE_DISABLED(name) name
#endif // defined(ADDRESS_SANITIZER) && defined(OS_WIN)
TEST_F(CrashHandlerTest, MAYBE_DISABLED(UseAfterFree)) {
ASSERT_TRUE(gwp_asan_found_);
checkProto(Crash_ErrorType_USE_AFTER_FREE, true);
}
TEST_F(CrashHandlerTest, MAYBE_DISABLED(DoubleFree)) {
ASSERT_TRUE(gwp_asan_found_);
checkProto(Crash_ErrorType_DOUBLE_FREE, true);
}
TEST_F(CrashHandlerTest, MAYBE_DISABLED(Underflow)) {
ASSERT_TRUE(gwp_asan_found_);
checkProto(Crash_ErrorType_BUFFER_UNDERFLOW, false);
}
TEST_F(CrashHandlerTest, MAYBE_DISABLED(Overflow)) {
ASSERT_TRUE(gwp_asan_found_);
checkProto(Crash_ErrorType_BUFFER_OVERFLOW, false);
}
TEST_F(CrashHandlerTest, MAYBE_DISABLED(UnrelatedException)) {
ASSERT_FALSE(gwp_asan_found_);
}
} // namespace
} // namespace internal
} // namespace gwp_asan