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

#ifndef PARTITION_ALLOC_POOL_OFFSET_FREELIST_H_
#define PARTITION_ALLOC_POOL_OFFSET_FREELIST_H_

#include <cstddef>
#include <cstdint>

#include "partition_alloc/build_config.h"
#include "partition_alloc/buildflags.h"
#include "partition_alloc/partition_address_space.h"
#include "partition_alloc/partition_alloc-inl.h"
#include "partition_alloc/partition_alloc_base/compiler_specific.h"
#include "partition_alloc/partition_alloc_check.h"
#include "partition_alloc/partition_alloc_config.h"
#include "partition_alloc/partition_alloc_constants.h"
#include "partition_alloc/tagging.h"

#if !PA_BUILDFLAG(PA_ARCH_CPU_BIG_ENDIAN)
#include "partition_alloc/reverse_bytes.h"
#endif

#if !PA_BUILDFLAG(HAS_64_BIT_POINTERS)
#error Pool-Offset Freelist is supported only on 64-bit system.
#endif  // !PA_BUILDFLAG(HAS_64_BIT_POINTERS)

namespace partition_alloc::internal {

// Defined in "partition_freelist_entry.cc".
[[noreturn]] PA_NOINLINE PA_COMPONENT_EXPORT(
    PARTITION_ALLOC) void FreelistCorruptionDetected(size_t slot_size);

using PoolInfo = PartitionAddressSpace::PoolInfo;

class FreelistEntry;

// Encoding to store the entries as pool offsets. In a scenario that an attacker
// has a write primitive anywhere within the pool, they would not be able to
// corrupt the freelist in a way that would allow them to break out of the pool.
class EncodedPoolOffset {
#if PA_BUILDFLAG(PA_ARCH_CPU_BIG_ENDIAN)
  static constexpr uintptr_t kEncodeedNullptr = ~uintptr_t{0};
#else
  static constexpr uintptr_t kEncodeedNullptr = uintptr_t{0};
#endif

  PA_ALWAYS_INLINE constexpr explicit EncodedPoolOffset(std::nullptr_t)
      : encoded_(kEncodeedNullptr) {}
  PA_ALWAYS_INLINE explicit EncodedPoolOffset(void* ptr)
      // The encoded pointer stays MTE-tagged.
      : encoded_(Encode(ptr)) {}
  // Similar to above, but faster with known pool.
  PA_ALWAYS_INLINE explicit EncodedPoolOffset(
      void* ptr,
      const PoolOffsetLookup& offset_lookup)
      : encoded_(Encode(ptr, offset_lookup)) {}

  PA_ALWAYS_INLINE constexpr uintptr_t Inverted() const { return ~encoded_; }

  PA_ALWAYS_INLINE constexpr void Override(uintptr_t encoded) {
    encoded_ = encoded;
  }

  PA_ALWAYS_INLINE constexpr explicit operator bool() const { return encoded_; }

  // Transform() works the same in both directions, so can be used for
  // encoding and decoding.
  PA_ALWAYS_INLINE static constexpr uintptr_t Transform(uintptr_t offset) {
    // We use bswap on little endian as a fast transformation for two reasons:
    // 1) The offset is a canonical address, possibly pointing to valid memory,
    //    whereas, on 64 bit, the swapped offset is very unlikely to be a
    //    canonical address. Therefore, if an object is freed and its vtable is
    //    used where the attacker doesn't get the chance to run allocations
    //    between the free and use, the vtable dereference is likely to fault.
    // 2) If the attacker has a linear buffer overflow and elects to try and
    //    corrupt a freelist pointer, partial pointer overwrite attacks are
    //    thwarted.
    // For big endian, similar guarantees are arrived at with a negation.
#if PA_BUILDFLAG(PA_ARCH_CPU_BIG_ENDIAN)
    uintptr_t transformed = ~offset;
#else
    uintptr_t transformed = ReverseBytes(offset);
#endif
    return transformed;
  }

  // Determines the containing pool of `ptr` and returns `ptr`
  // represented as a tagged offset into that pool.
  PA_ALWAYS_INLINE static uintptr_t Encode(void* ptr) {
    if (!ptr) {
      return kEncodeedNullptr;
    }
    uintptr_t address = SlotStartPtr2Addr(ptr);
    PoolInfo pool_info = PartitionAddressSpace::GetPoolInfo(address);
    // Save a MTE tag as well as an offset.
    uintptr_t tagged_offset =
        reinterpret_cast<uintptr_t>(ptr) & (kPtrTagMask | ~pool_info.base_mask);
    return Transform(tagged_offset);
  }

  // Similar to above, but faster with known pool.
  PA_ALWAYS_INLINE static uintptr_t Encode(
      void* ptr,
      const PoolOffsetLookup& offset_lookup) {
    if (!ptr) {
      return kEncodeedNullptr;
    }
    // Save a MTE tag as well as an offset.
    return Transform(offset_lookup.GetTaggedOffset(ptr));
  }

  // Given `pool_info`, decodes a `tagged_offset` into a tagged pointer.
  PA_ALWAYS_INLINE FreelistEntry* Decode(size_t slot_size) const {
    PoolInfo pool_info = GetPoolInfo(SlotStartPtr2Addr(this));
    uintptr_t tagged_offset = Transform(encoded_);

    // `tagged_offset` must not have bits set in the pool base mask, except MTE
    // tag.
    if (tagged_offset & pool_info.base_mask & ~kPtrTagMask) {
      FreelistCorruptionDetected(slot_size);
    }

    // We assume `tagged_offset` contains a proper MTE tag.
    return reinterpret_cast<FreelistEntry*>(pool_info.base | tagged_offset);
  }

  // Given `pool_info`, decodes a `tagged_offset` into a tagged pointer.
  PA_ALWAYS_INLINE FreelistEntry* Decode(
      size_t slot_size,
      const PoolOffsetLookup& offset_lookup) const {
    uintptr_t tagged_offset = Transform(encoded_);

    // `tagged_offset` must not have bits set in the pool base mask, except MTE
    // tag.
    if (!offset_lookup.IsValidTaggedOffset(tagged_offset)) {
      FreelistCorruptionDetected(slot_size);
    }

    // We assume `tagged_offset` contains a proper MTE tag.
    return static_cast<FreelistEntry*>(offset_lookup.GetPointer(tagged_offset));
  }

  uintptr_t encoded_;

  friend FreelistEntry;
};

}  // namespace partition_alloc::internal

#endif  // PARTITION_ALLOC_POOL_OFFSET_FREELIST_H_
