// 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/common/allocator_state.h"

#include <algorithm>

#include "base/bits.h"
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/memory/page_size.h"
#include "base/strings/stringprintf.h"
#include "base/threading/platform_thread.h"
#include "build/build_config.h"

namespace gwp_asan {
namespace internal {

AllocatorState::AllocatorState() = default;

AllocatorState::GetMetadataReturnType AllocatorState::GetMetadataForAddress(
    uintptr_t exception_address,
    const SlotMetadata* metadata_arr,
    const MetadataIdx* slot_to_metadata,
    MetadataIdx* metadata_idx,
    std::string* error) const {
  CHECK(IsValid());
  CHECK(PointerIsMine(exception_address));

  AllocatorState::SlotIdx slot_idx = GetNearestSlot(exception_address);
  if (slot_idx >= total_reserved_pages) {
    *error = base::StringPrintf("Bad slot index %u >= %zu", slot_idx,
                                total_reserved_pages);
    return GetMetadataReturnType::kErrorBadSlot;
  }

  AllocatorState::MetadataIdx index = UNSAFE_TODO(slot_to_metadata[slot_idx]);
  if (index == kInvalidMetadataIdx)
    return GetMetadataReturnType::kGwpAsanCrashWithMissingMetadata;

  if (index >= num_metadata) {
    *error =
        base::StringPrintf("Bad metadata index %u >= %zu", index, num_metadata);
    return GetMetadataReturnType::kErrorBadMetadataIndex;
  }

  if (GetNearestSlot(UNSAFE_TODO(metadata_arr[index]).alloc_ptr) != slot_idx) {
    *error = base::StringPrintf(
        "Outdated metadata index %u: slot for %zx does not match %zx", index,
        UNSAFE_TODO(metadata_arr[index]).alloc_ptr, exception_address);
    return GetMetadataReturnType::kErrorOutdatedMetadataIndex;
  }

  *metadata_idx = index;
  return GetMetadataReturnType::kGwpAsanCrash;
}

bool AllocatorState::IsValid() const {
  if (!page_size || page_size != base::GetPageSize())
    return false;

  if (total_requested_pages == 0 || total_requested_pages > kMaxRequestedSlots)
    return false;

  if (total_reserved_pages == 0 || total_reserved_pages > kMaxReservedSlots ||
      total_reserved_pages < total_requested_pages)
    return false;

  if (num_metadata == 0 ||
      num_metadata > std::min(kMaxMetadata, total_requested_pages))
    return false;

  if (pages_base_addr % page_size != 0 || pages_end_addr % page_size != 0 ||
      first_page_addr % page_size != 0)
    return false;

  if (pages_base_addr >= pages_end_addr)
    return false;

  if (first_page_addr != pages_base_addr + page_size ||
      pages_end_addr - pages_base_addr !=
          page_size * (total_reserved_pages * 2 + 1))
    return false;

  if (!metadata_addr || !slot_to_metadata_addr)
    return false;

  return true;
}

uintptr_t AllocatorState::GetPageAddr(uintptr_t addr) const {
  const uintptr_t addr_mask = ~(page_size - 1ULL);
  return addr & addr_mask;
}

uintptr_t AllocatorState::GetNearestValidPage(uintptr_t addr) const {
  if (addr < first_page_addr)
    return first_page_addr;
  const uintptr_t last_page_addr = pages_end_addr - 2 * page_size;
  if (addr > last_page_addr)
    return last_page_addr;

  uintptr_t offset = addr - first_page_addr;
  // If addr is already on a valid page, just return addr.
  if ((offset >> base::bits::Log2Floor(page_size)) % 2 == 0)
    return addr;

  // ptr points to a guard page, so get nearest valid page.
  const size_t kHalfPageSize = page_size / 2;
  if ((offset >> base::bits::Log2Floor(kHalfPageSize)) % 2 == 0) {
    return addr - kHalfPageSize;  // Round down.
  }
  return addr + kHalfPageSize;  // Round up.
}

AllocatorState::SlotIdx AllocatorState::GetNearestSlot(uintptr_t addr) const {
  return AddrToSlot(GetPageAddr(GetNearestValidPage(addr)));
}

AllocatorState::ErrorType AllocatorState::GetErrorType(uintptr_t addr,
                                                       bool allocated,
                                                       bool deallocated) const {
  if (free_invalid_address)
    return ErrorType::kFreeInvalidAddress;
  if (!allocated)
    return ErrorType::kUnknown;
  if (double_free_address)
    return ErrorType::kDoubleFree;
  if (deallocated)
    return ErrorType::kUseAfterFree;
  if (addr < first_page_addr)
    return ErrorType::kBufferUnderflow;
  const uintptr_t last_page_addr = pages_end_addr - 2 * page_size;
  if (addr > last_page_addr)
    return ErrorType::kBufferOverflow;
  const uintptr_t offset = addr - first_page_addr;

  // If we hit this condition, it means we crashed on accessing an allocation
  // even though it's currently allocated [there is a if(deallocated) return
  // earlier.] This can happen when a use-after-free causes a crash and another
  // thread manages to allocate the page in another thread before it's stopped.
  // This can happen with low sampling frequencies and high parallel allocator
  // usage.
  if ((offset >> base::bits::Log2Floor(page_size)) % 2 == 0) {
    LOG(WARNING) << "Hit impossible error condition, likely caused by a racy "
                    "use-after-free";
    return ErrorType::kUnknown;
  }

  const size_t kHalfPageSize = page_size / 2;
  return (offset >> base::bits::Log2Floor(kHalfPageSize)) % 2 == 0
             ? ErrorType::kBufferOverflow
             : ErrorType::kBufferUnderflow;
}

uintptr_t AllocatorState::SlotToAddr(AllocatorState::SlotIdx slot) const {
  DCHECK_LE(slot, kMaxReservedSlots);
  return first_page_addr + 2 * slot * page_size;
}

AllocatorState::SlotIdx AllocatorState::AddrToSlot(uintptr_t addr) const {
  DCHECK_EQ(addr % page_size, 0ULL);
  uintptr_t offset = addr - first_page_addr;
  DCHECK_EQ((offset >> base::bits::Log2Floor(page_size)) % 2, 0ULL);
  size_t slot = (offset >> base::bits::Log2Floor(page_size)) / 2;
  DCHECK_LE(slot, kMaxReservedSlots);
  return static_cast<SlotIdx>(slot);
}

AllocatorState::SlotMetadata::SlotMetadata() = default;

}  // namespace internal
}  // namespace gwp_asan
