// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/gwp_asan/client/sampling_malloc_shims.h"

#include <stdlib.h>

#include <cstdlib>
#include <memory>
#include <string>

#include "base/compiler_specific.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/page_size.h"
#include "base/strings/string_number_conversions.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/crash/core/common/crash_key.h"
#include "components/gwp_asan/client/guarded_page_allocator.h"
#include "components/gwp_asan/client/gwp_asan.h"
#include "components/gwp_asan/common/crash_key_name.h"
#include "partition_alloc/buildflags.h"
#include "partition_alloc/shim/allocator_shim.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"

// These tests install global allocator shims so they are not safe to run in
// multi-threaded contexts. Instead they're implemented as multi-process tests.

#if BUILDFLAG(IS_WIN)
#include <malloc.h>
static size_t GetUsableSize(void* mem) {
  return _msize(mem);
}
#elif BUILDFLAG(IS_APPLE)
#include <malloc/malloc.h>
static size_t GetUsableSize(void* mem) {
  return malloc_size(mem);
}
#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
#include <malloc.h>
static size_t GetUsableSize(void* mem) {
  return malloc_usable_size(mem);
}
#endif

namespace gwp_asan {
namespace internal {

extern GuardedPageAllocator& GetMallocGpaForTesting();

namespace {

constexpr size_t kSamplingFrequency = 5;
// Number of loop iterations required to hit a sampled allocation.
// The probability of not hitting a sample allocation in kLoopIterations
// is (1 - 1/kSamplingFrequency)^kLoopIterations. In this case that is
// (4/5)^100 < 3*10^-10.
constexpr size_t kLoopIterations = 100;

constexpr int kSuccess = 0;
constexpr int kFailure = 1;
constexpr int kSamplingMaxSize = 16;

static constexpr size_t kMaxMetadata = 2048;
static constexpr size_t kMaxRequestedSlots = 8192;

class SamplingMallocShimsTest : public base::MultiProcessTest {
 public:
  static void multiprocessTestSetup() {
#if BUILDFLAG(IS_APPLE)
    allocator_shim::InitializeAllocatorShim();
#endif  // BUILDFLAG(IS_APPLE)
    crash_reporter::InitializeCrashKeys();
    CHECK(InstallMallocHooks(
        AllocatorSettings{
            .max_allocated_pages = kMaxMetadata,
            .num_metadata = kMaxMetadata,
            .total_pages = kMaxRequestedSlots,
            .sampling_frequency = kSamplingFrequency,
            .sampling_min_size = 1,
            .sampling_max_size = std::numeric_limits<int>::max(),
        },
        base::DoNothing()));
  }

  static void multiprocessTestSetupWithSamplingMaxSize() {
#if BUILDFLAG(IS_APPLE)
    allocator_shim::InitializeAllocatorShim();
#endif  // BUILDFLAG(IS_APPLE)
    crash_reporter::InitializeCrashKeys();
    CHECK(InstallMallocHooks(
        AllocatorSettings{.max_allocated_pages = kMaxMetadata,
                          .num_metadata = kMaxMetadata,
                          .total_pages = kMaxRequestedSlots,
                          .sampling_frequency = kSamplingFrequency,
                          .sampling_min_size = 1,
                          .sampling_max_size = kSamplingMaxSize},
        base::DoNothing()));
  }

 protected:
  void runTest(const char* name) {
    base::Process process = SpawnChild(name);
    int exit_code = -1;
    ASSERT_TRUE(WaitForMultiprocessTestChildExit(
        process, TestTimeouts::action_max_timeout(), &exit_code));
    EXPECT_EQ(exit_code, kSuccess);
  }
};

// Return whether some of the allocations returned by the calling the allocate
// parameter are sampled to the guarded allocator. Keep count of failures
// encountered.
bool allocationCheck(std::function<void*(void)> allocate,
                     std::function<void(void*)> free,
                     int* failures) {
  size_t guarded = 0;
  size_t unguarded = 0;
  for (size_t i = 0; i < kLoopIterations; i++) {
    std::unique_ptr<void, decltype(free)> alloc(allocate(), free);
    EXPECT_NE(alloc.get(), nullptr);
    if (!alloc.get()) {
      *failures += 1;
      return false;
    }

    if (GetMallocGpaForTesting().PointerIsMine(alloc.get()))
      guarded++;
    else
      unguarded++;
  }

  if (guarded > 0 && unguarded > 0)
    return true;

  *failures += 1;
  return false;
}

MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    BasicFunctionality,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  const size_t page_size = base::GetPageSize();
  int failures = 0;

  EXPECT_TRUE(
      allocationCheck([&] { return malloc(page_size); }, &free, &failures));
  EXPECT_TRUE(
      allocationCheck([&] { return calloc(1, page_size); }, &free, &failures));
  EXPECT_TRUE(allocationCheck([&] { return realloc(nullptr, page_size); },
                              &free, &failures));

#if BUILDFLAG(IS_WIN)
  EXPECT_TRUE(allocationCheck([&] { return _aligned_malloc(123, 16); },
                              &_aligned_free, &failures));
  EXPECT_TRUE(
      allocationCheck([&] { return _aligned_realloc(nullptr, 123, 16); },
                      &_aligned_free, &failures));
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_POSIX)
  EXPECT_TRUE(allocationCheck(
      [&]() -> void* {
        void* ptr;
        if (posix_memalign(&ptr, page_size, page_size))
          return nullptr;
        return ptr;
      },
      &free, &failures));
#endif  // BUILDFLAG(IS_POSIX)

  EXPECT_TRUE(allocationCheck([&] { return std::malloc(page_size); },
                              &std::free, &failures));
  EXPECT_TRUE(allocationCheck([&] { return std::calloc(1, page_size); },
                              &std::free, &failures));
  EXPECT_TRUE(allocationCheck([&] { return std::realloc(nullptr, page_size); },
                              &std::free, &failures));

  EXPECT_TRUE(allocationCheck([] { return new int; },
                              [](void* ptr) { delete (int*)ptr; }, &failures));
  EXPECT_TRUE(allocationCheck([] { return new int[4]; },
                              [](void* ptr) { delete[](int*) ptr; },
                              &failures));

  if (failures)
    return kFailure;

  EXPECT_FALSE(
      allocationCheck([&] { return malloc(page_size + 1); }, &free, &failures));

  // Make sure exactly 1 negative test case was hit.
  if (failures == 1)
    return kSuccess;

  return kFailure;
}

// Flaky on Mac: https://crbug.com/1087372
#if BUILDFLAG(IS_APPLE)
#define MAYBE_BasicFunctionality DISABLED_BasicFunctionality
#else
#define MAYBE_BasicFunctionality BasicFunctionality
#endif
TEST_F(SamplingMallocShimsTest, MAYBE_BasicFunctionality) {
  runTest("BasicFunctionality");
}

MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    Realloc,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  void* alloc = GetMallocGpaForTesting().Allocate(base::GetPageSize());
  CHECK_NE(alloc, nullptr);

  constexpr unsigned char kFillChar = 0xff;
  UNSAFE_TODO(memset(alloc, kFillChar, base::GetPageSize()));

  unsigned char* new_alloc =
      static_cast<unsigned char*>(realloc(alloc, base::GetPageSize() + 1));
  CHECK_NE(alloc, new_alloc);
  CHECK_EQ(GetMallocGpaForTesting().PointerIsMine(new_alloc), false);

  for (size_t i = 0; i < base::GetPageSize(); i++)
    UNSAFE_TODO(CHECK_EQ(new_alloc[i], kFillChar));

  free(new_alloc);

  return kSuccess;
}

TEST_F(SamplingMallocShimsTest, Realloc) {
  runTest("Realloc");
}

MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    Calloc,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  for (size_t i = 0; i < kLoopIterations; i++) {
    unsigned char* alloc =
        static_cast<unsigned char*>(calloc(base::GetPageSize(), 1));
    CHECK_NE(alloc, nullptr);

    if (GetMallocGpaForTesting().PointerIsMine(alloc)) {
      for (size_t j = 0; j < base::GetPageSize(); j++)
        UNSAFE_TODO(CHECK_EQ(alloc[j], 0U));
      free(alloc);
      return kSuccess;
    }

    free(alloc);
  }

  return kFailure;
}

TEST_F(SamplingMallocShimsTest, Calloc) {
  runTest("Calloc");
}

// GetCrashKeyValue() operates on a per-component basis, can't read the crash
// key from the gwp_asan_client component in a component build.
#if !defined(COMPONENT_BUILD)
MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    CrashKey,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  if (crash_reporter::GetCrashKeyValue(kMallocCrashKey) !=
      GetMallocGpaForTesting().GetCrashKey()) {
    return kFailure;
  }

  return kSuccess;
}

TEST_F(SamplingMallocShimsTest, CrashKey) {
  runTest("CrashKey");
}
#endif  // !defined(COMPONENT_BUILD)

// malloc_usable_size() is not currently used/shimmed on Android.
#if !BUILDFLAG(IS_ANDROID)
MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    GetSizeEstimate,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  constexpr size_t kAllocationSize = 123;
  for (size_t i = 0; i < kLoopIterations; i++) {
    std::unique_ptr<void, decltype(&free)> alloc(malloc(kAllocationSize), free);
    CHECK_NE(alloc.get(), nullptr);

    size_t alloc_sz = GetUsableSize(alloc.get());
    if (GetMallocGpaForTesting().PointerIsMine(alloc.get()))
      CHECK_EQ(alloc_sz, kAllocationSize);
    else
      CHECK_GE(alloc_sz, kAllocationSize);
  }

  return kSuccess;
}

TEST_F(SamplingMallocShimsTest, GetSizeEstimate) {
  runTest("GetSizeEstimate");
}
#endif

#if BUILDFLAG(IS_WIN)
MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    AlignedRealloc,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  // Exercise the _aligned_* shims and ensure that we handle them stably.
  constexpr size_t kAllocationSize = 123;
  constexpr size_t kAllocationAlignment = 64;
  for (size_t i = 0; i < kLoopIterations; i++) {
    void* ptr = _aligned_malloc(kAllocationSize, kAllocationAlignment);
    CHECK(ptr);
    ptr = _aligned_realloc(ptr, kAllocationSize * 2, kAllocationAlignment);
    CHECK(ptr);
    _aligned_free(ptr);
  }

  return kSuccess;
}

TEST_F(SamplingMallocShimsTest, AlignedRealloc) {
  runTest("AlignedRealloc");
}
#endif  // BUILDFLAG(IS_WIN)

// PartitionAlloc-Everywhere does not support batch_malloc / batch_free.
#if BUILDFLAG(IS_APPLE) && !PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    BatchFree,
    SamplingMallocShimsTest::multiprocessTestSetup) {
  void* ptrs[kMaxMetadata + 1];
  for (size_t i = 0; i < kMaxMetadata; i++) {
    UNSAFE_TODO(ptrs[i]) = GetMallocGpaForTesting().Allocate(16);
    UNSAFE_TODO(CHECK(ptrs[i]));
  }
  // Check that all GPA allocations were consumed.
  CHECK_EQ(GetMallocGpaForTesting().Allocate(16), nullptr);

  ptrs[kMaxMetadata] =
      malloc_zone_malloc(malloc_default_zone(), 16);
  CHECK(ptrs[kMaxMetadata]);

  malloc_zone_batch_free(malloc_default_zone(), ptrs,
                         kMaxMetadata + 1);

  // Check that GPA allocations were freed.
  CHECK(GetMallocGpaForTesting().Allocate(16));

  return kSuccess;
}

TEST_F(SamplingMallocShimsTest, BatchFree) {
  runTest("BatchFree");
}

#endif  // BUILDFLAG(IS_APPLE) && !PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)

MULTIPROCESS_TEST_MAIN_WITH_SETUP(
    SamplingRange,
    SamplingMallocShimsTest::multiprocessTestSetupWithSamplingMaxSize) {
  for (size_t i = 0; i < kLoopIterations; i++) {
    unsigned char* alloc =
        static_cast<unsigned char*>(malloc(kSamplingMaxSize * 2));
    CHECK_NE(alloc, nullptr);

    if (GetMallocGpaForTesting().PointerIsMine(alloc)) {
      free(alloc);
      return kFailure;
    }

    free(alloc);
  }

  return kSuccess;
}

TEST_F(SamplingMallocShimsTest, SamplingRange) {
  runTest("SamplingRange");
}

}  // namespace

}  // namespace internal
}  // namespace gwp_asan
