// 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 <map>
#include <memory>
#include <string>
#include <vector>

#include "base/bind_helpers.h"
#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/client/crashpad_info.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 {

constexpr size_t kAllocationSize = 902;
constexpr int kSuccess = 0;
constexpr size_t kTotalPages = AllocatorState::kMaxSlots;

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) {
#if defined(OS_MACOSX)
  // Disable the system crash reporter from inspecting this crash (it is slow
  // and causes test timeouts.)
  crashpad::CrashpadInfo::GetCrashpadInfo()
      ->set_system_crash_reporter_forwarding(crashpad::TriState::kDisabled);
#endif

  base::NoDestructor<GuardedPageAllocator> gpa;
  gpa->Init(AllocatorState::kMaxMetadata, AllocatorState::kMaxMetadata,
            kTotalPages, base::DoNothing(), false);

  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());
  std::string allocator = cmd_line->GetSwitchValueASCII("allocator");

  const char* annotation_name;
  if (allocator == "malloc") {
    annotation_name = kMallocCrashKey;
  } else if (allocator == "partitionalloc") {
    annotation_name = kPartitionAllocCrashKey;
  } else {
    LOG(ERROR) << "Unknown allocator";
    return kSuccess;
  }

  std::string gpa_addr = gpa->GetCrashKey();
  static crashpad::Annotation gpa_annotation(
      crashpad::Annotation::Type::kString, annotation_name,
      const_cast<char*>(gpa_addr.c_str()));
  gpa_annotation.SetSize(gpa_addr.size());

  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 if (test_name == "FreeInvalidAddress") {
    void* ptr = gpa->Allocate(kAllocationSize);
    uintptr_t bad_address = reinterpret_cast<uintptr_t>(ptr) + 1;
    gpa->Deallocate(reinterpret_cast<void*>(bad_address));
  } else if (test_name == "MissingMetadata") {
    // Consume all allocations/metadata
    void* ptrs[AllocatorState::kMaxMetadata];
    for (size_t i = 0; i < AllocatorState::kMaxMetadata; i++)
      ptrs[i] = gpa->Allocate(1);

    gpa->Deallocate(ptrs[0]);

    // Take the remaining metadata slot with an allocation on a different page.
    while (1) {
      void* new_alloc = gpa->Allocate(1);
      if (new_alloc != ptrs[0])
        break;
      gpa->Deallocate(new_alloc);
    }

    // Cause a crash accessing an allocation that no longer has metadata
    // associated with it.
    *(uint8_t*)(ptrs[0]) = 0;
  } else {
    LOG(ERROR) << "Unknown test name " << test_name;
  }

  LOG(ERROR) << "This return should never be reached.";
  return kSuccess;
}

class CrashHandlerTest : public base::MultiProcessTest,
                         public testing::WithParamInterface<const char*> {
 protected:
  CrashHandlerTest() : allocator_(GetParam()) {}

  // 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());

    // Remove the parameterized test suffix from the test name.
    std::string test_name(
        testing::UnitTest::GetInstance()->current_test_info()->name());
    size_t separator = test_name.find("/");
    ASSERT_NE(separator, std::string::npos);
    test_name.erase(separator);

    ASSERT_TRUE(runTestProcess(database_dir.GetPath(), test_name.c_str()));

    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);
    cmd_line.AppendSwitchASCII("allocator", allocator_);

    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(WaitForMultiprocessTestChildExit(
        process, 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(),
              static_cast<uint64_t>(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(),
                static_cast<uint64_t>(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));

    EXPECT_TRUE(proto_.has_missing_metadata());
    EXPECT_FALSE(proto_.missing_metadata());

    EXPECT_TRUE(proto_.has_allocator());
    if (allocator_ == "malloc")
      EXPECT_EQ(proto_.allocator(), Crash_Allocator_MALLOC);
    else if (allocator_ == "partitionalloc")
      EXPECT_EQ(proto_.allocator(), Crash_Allocator_PARTITIONALLOC);
    else
      ASSERT_TRUE(false) << "Unknown allocator name";
  }

  gwp_asan::Crash proto_;
  std::string allocator_;
  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_P(CrashHandlerTest, MAYBE_DISABLED(UseAfterFree)) {
  ASSERT_TRUE(gwp_asan_found_);
  checkProto(Crash_ErrorType_USE_AFTER_FREE, true);
}

TEST_P(CrashHandlerTest, MAYBE_DISABLED(DoubleFree)) {
  ASSERT_TRUE(gwp_asan_found_);
  checkProto(Crash_ErrorType_DOUBLE_FREE, true);
}

TEST_P(CrashHandlerTest, MAYBE_DISABLED(Underflow)) {
  ASSERT_TRUE(gwp_asan_found_);
  checkProto(Crash_ErrorType_BUFFER_UNDERFLOW, false);
}

TEST_P(CrashHandlerTest, MAYBE_DISABLED(Overflow)) {
  ASSERT_TRUE(gwp_asan_found_);
  checkProto(Crash_ErrorType_BUFFER_OVERFLOW, false);
}

TEST_P(CrashHandlerTest, MAYBE_DISABLED(FreeInvalidAddress)) {
  ASSERT_TRUE(gwp_asan_found_);
  checkProto(Crash_ErrorType_FREE_INVALID_ADDRESS, false);
  EXPECT_TRUE(proto_.has_free_invalid_address());
}

TEST_P(CrashHandlerTest, MAYBE_DISABLED(MissingMetadata)) {
  ASSERT_TRUE(gwp_asan_found_);

  ASSERT_TRUE(proto_.has_missing_metadata());
  EXPECT_TRUE(proto_.missing_metadata());

  EXPECT_FALSE(proto_.has_error_type());
  EXPECT_FALSE(proto_.has_allocation_address());
  EXPECT_FALSE(proto_.has_allocation_size());
  EXPECT_FALSE(proto_.has_allocation());
  EXPECT_FALSE(proto_.has_deallocation());
  EXPECT_FALSE(proto_.has_free_invalid_address());
  EXPECT_TRUE(proto_.has_region_start());
  EXPECT_TRUE(proto_.has_region_size());
}

TEST_P(CrashHandlerTest, MAYBE_DISABLED(UnrelatedException)) {
  ASSERT_FALSE(gwp_asan_found_);
}

INSTANTIATE_TEST_SUITE_P(VaryAllocator,
                         CrashHandlerTest,
                         testing::Values("malloc", "partitionalloc"));

}  // namespace

}  // namespace internal
}  // namespace gwp_asan
