// 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.

#ifndef COMPONENTS_GWP_ASAN_CLIENT_GUARDED_PAGE_ALLOCATOR_H_
#define COMPONENTS_GWP_ASAN_CLIENT_GUARDED_PAGE_ALLOCATOR_H_

#include <atomic>
#include <map>
#include <memory>
#include <string>
#include <vector>

#include "base/compiler_specific.h"
#include "base/functional/callback.h"
#include "base/gtest_prod_util.h"
#include "base/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "build/build_config.h"
#include "components/gwp_asan/client/export.h"
#include "components/gwp_asan/client/gwp_asan.h"
#include "components/gwp_asan/common/allocator_state.h"

namespace gwp_asan {
namespace internal {
// This class encompasses the allocation and deallocation logic on top of the
// AllocatorState. Its members are not inspected or used by the crash handler.
//
// This class makes use of dynamically-sized arrays like std::vector<> to only
// allocate as much memory as we need; however, they only reserve memory at
// initialization-time so there is no risk of malloc reentrancy.
class GWP_ASAN_EXPORT GuardedPageAllocator {
 public:
  // Number of consecutive allocations that fail due to lack of available pages
  // before we call the OOM callback.
  static constexpr size_t kOutOfMemoryCount = 100;
  // Default maximum alignment for all returned allocations.
  static constexpr size_t kGpaAllocAlignment = 16;

  // Callback used to report the allocator running out of memory, reports the
  // number of successful allocations before running out of memory.
  using OutOfMemoryCallback = base::OnceCallback<void(size_t)>;

  // Does not allocate any memory for the allocator, to finish initializing call
  // Init().
  GuardedPageAllocator();

  GuardedPageAllocator(const GuardedPageAllocator&) = delete;
  GuardedPageAllocator& operator=(const GuardedPageAllocator&) = delete;

  // Configures this allocator to allocate up to `settings.max_alloced_pages`
  // pages at a time, holding metadata for up to `settings.num_metadata`
  // allocations, from a pool of `settings.total_pages` pages, where:
  //   1 <= max_alloced_pages <= num_metadata <= kMaxMetadata
  //   num_metadata <= total_pages <= kMaxSlots
  //
  // The OOM callback is called the first time the allocator fails to allocate
  // kOutOfMemoryCount allocations consecutively due to lack of memory.
  [[nodiscard]] bool Init(const AllocatorSettings& settings,
                          OutOfMemoryCallback oom_callback,
                          bool is_partition_alloc);

  // On success, returns a pointer to size bytes of page-guarded memory. On
  // failure, returns nullptr. The allocation is not guaranteed to be
  // zero-filled. Failure can occur if memory could not be mapped or protected,
  // or if all guarded pages are already allocated.
  //
  // The align parameter specifies a power of two to align the allocation up to.
  // It must be less than or equal to the allocation size. If it's left as zero
  // it will default to the default alignment the allocator chooses.
  //
  // The type parameter should only be set for PartitionAlloc allocations.
  //
  // Preconditions: Init() must have been called.
  void* Allocate(size_t size, size_t align = 0, const char* type = nullptr);

  // Deallocates memory pointed to by ptr. ptr must have been previously
  // returned by a call to Allocate.
  void Deallocate(void* ptr);

  // Returns the size requested when ptr was allocated. ptr must have been
  // previously returned by a call to Allocate, and not have been deallocated.
  size_t GetRequestedSize(const void* ptr) const;

  // Retrieves the textual address of the shared allocator state required by the
  // crash handler.
  std::string GetCrashKey() const;

  // Returns internal memory used by the allocator (required for sanitization
  // on supported platforms.)
  std::vector<std::pair<void*, size_t>> GetInternalMemoryRegions();

  // Returns true if ptr points to memory managed by this class.
  inline bool PointerIsMine(const void* ptr) const {
    return state_.PointerIsMine(reinterpret_cast<uintptr_t>(ptr));
  }

  void DestructForTesting();

 private:
  // Virtual base class representing a free list of entries T.
  template <typename T>
  class FreeList {
   public:
    FreeList() = default;
    virtual ~FreeList() = default;
    virtual void Initialize(T max_entries) = 0;
    virtual void Initialize(T max_entries, std::vector<T>&& free_list) = 0;
    virtual bool Allocate(T* out, const char* type) = 0;
    virtual void Free(T entry) = 0;
  };

  // Manages a free list of slot or metadata indices in the range
  // [0, max_entries). Access to SimpleFreeList objects must be synchronized.
  //
  // SimpleFreeList is specifically designed to pre-allocate data in Initialize
  // so that it never recurses into malloc/free during Allocate/Free.
  template <typename T>
  class SimpleFreeList final : public FreeList<T> {
   public:
    ~SimpleFreeList() final = default;
    void Initialize(T max_entries) final;
    void Initialize(T max_entries, std::vector<T>&& free_list) final;
    bool Allocate(T* out, const char* type) final;
    void Free(T entry) final;

   private:
    std::vector<T> free_list_;

    // Number of used entries. This counter ensures all free entries are used
    // before starting to use random eviction.
    T num_used_entries_ = 0;
    T max_entries_ = 0;
  };

  // Manages a free list of slot indices especially for PartitionAlloc.
  // Allocate() is type-aware so that once a page has been used to allocate
  // a given partition, it never reallocates an object of a different type on
  // that page. Access to this object must be synchronized.
  //
  // PartitionAllocSlotFreeList can perform malloc/free during Allocate/Free,
  // so it is not safe to use with malloc hooks!
  //
  // TODO(vtsyrklevich): Right now we allocate slots to partitions on a
  // first-come first-serve basis, this makes it likely that all slots will be
  // used up by common types first. Set aside a fixed amount of slots (~5%) for
  // one-off partitions so that we make sure to sample rare types as well.
  class PartitionAllocSlotFreeList final
      : public FreeList<AllocatorState::SlotIdx> {
   public:
    PartitionAllocSlotFreeList();
    ~PartitionAllocSlotFreeList() final;
    void Initialize(AllocatorState::SlotIdx max_entries) final;
    void Initialize(AllocatorState::SlotIdx max_entries,
                    std::vector<AllocatorState::SlotIdx>&& free_list) final;
    bool Allocate(AllocatorState::SlotIdx* out, const char* type) final;
    void Free(AllocatorState::SlotIdx entry) final;

   private:
    std::vector<const char*> type_mapping_;
    std::map<const char*, std::vector<AllocatorState::SlotIdx>> free_list_;
    std::vector<AllocatorState::SlotIdx> initial_free_list_;

    // Number of used entries. This counter ensures all free entries are used
    // before starting to use random eviction.
    AllocatorState::SlotIdx num_used_entries_ = 0;
    AllocatorState::SlotIdx max_entries_ = 0;
  };

  // Unmaps memory allocated by this class, if Init was called.
  ~GuardedPageAllocator();

  // Allocates/deallocates the virtual memory used for allocations.
  void* MapRegion();
  void UnmapRegion();

  // Provide a hint for MapRegion() on where to place the GWP-ASan region.
  void* MapRegionHint() const;

  // Returns the size of the virtual memory region used to store allocations.
  size_t RegionSize() const;

  // Mark page read-write.
  void MarkPageReadWrite(void*);

  // Mark page inaccessible and decommit the memory from use to save memory
  // used by the quarantine.
  void MarkPageInaccessible(void*);

  // On success, returns true and writes the reserved indices to |slot| and
  // |metadata_idx|. Otherwise returns false if no allocations are available.
  bool ReserveSlotAndMetadata(AllocatorState::SlotIdx* slot,
                              AllocatorState::MetadataIdx* metadata_idx,
                              const char* type) LOCKS_EXCLUDED(lock_);

  // Marks the specified slot and metadata as unreserved.
  void FreeSlotAndMetadata(AllocatorState::SlotIdx slot,
                           AllocatorState::MetadataIdx metadata_idx)
      LOCKS_EXCLUDED(lock_);

  // Record the metadata for an allocation or deallocation for a given metadata
  // index.
  ALWAYS_INLINE
  void RecordAllocationMetadata(AllocatorState::MetadataIdx metadata_idx,
                                size_t size,
                                void* ptr);
  ALWAYS_INLINE void RecordDeallocationMetadata(
      AllocatorState::MetadataIdx metadata_idx);

  // Allocator state shared with with the crash analyzer.
  AllocatorState state_;

  // Lock that synchronizes allocating/freeing slots between threads.
  base::Lock lock_;

  std::unique_ptr<FreeList<AllocatorState::SlotIdx>> free_slots_
      GUARDED_BY(lock_);
  SimpleFreeList<AllocatorState::MetadataIdx> free_metadata_ GUARDED_BY(lock_);

  // Number of currently-allocated pages.
  size_t num_alloced_pages_ GUARDED_BY(lock_) = 0;
  // Max number of concurrent allocations.
  size_t max_alloced_pages_ = 0;

  // Array of metadata (e.g. stack traces) for allocations.
  // TODO(vtsyrklevich): Use an std::vector<> here as well.
  std::unique_ptr<AllocatorState::SlotMetadata[]> metadata_;

  // Maps a slot index to a metadata index (or kInvalidMetadataIdx if no such
  // mapping exists.)
  std::vector<AllocatorState::MetadataIdx> slot_to_metadata_idx_;

  // Maintain a count of total allocations and consecutive failed allocations
  // to report allocator OOM.
  size_t total_allocations_ GUARDED_BY(lock_) = 0;
  size_t consecutive_oom_hits_ GUARDED_BY(lock_) = 0;
  bool oom_hit_ GUARDED_BY(lock_) = false;
  OutOfMemoryCallback oom_callback_;

  bool is_partition_alloc_ = false;

  friend class BaseGpaTest;
  friend class BaseCrashAnalyzerTest;
  FRIEND_TEST_ALL_PREFIXES(CrashAnalyzerTest, InternalError);
  FRIEND_TEST_ALL_PREFIXES(CrashAnalyzerTest, StackTraceCollection);
};

}  // namespace internal
}  // namespace gwp_asan

#endif  // COMPONENTS_GWP_ASAN_CLIENT_GUARDED_PAGE_ALLOCATOR_H_
