blob: 8c7810571b41029df0206c2dffc7d9552511e414 [file]
// 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_PARTITION_FREELIST_ENTRY_H_
#define PARTITION_ALLOC_PARTITION_FREELIST_ENTRY_H_
#include <utility>
#include "partition_alloc/buildflags.h"
#include "partition_alloc/partition_alloc_constants.h"
#include "partition_alloc/slot_start.h"
// Pool-offset encoding has better security characteristics, but requires
// contiguous pool hence limited to 64-bit systems.
#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
#include "partition_alloc/pool_offset_freelist.h"
#else
#include "partition_alloc/encoded_next_freelist.h"
#endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
namespace partition_alloc::internal {
// Freelist entries are encoded for security reasons. See
// //base/allocator/partition_allocator/PartitionAlloc.md
// and |Transform()| for the rationale and mechanism, respectively.
class FreelistEntry {
#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
using EncodedPtr = EncodedPoolOffset;
#else
using EncodedPtr = EncodedFreelistPtr;
#endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
constexpr explicit FreelistEntry(std::nullptr_t)
: encoded_next_(EncodedPtr(nullptr))
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
,
shadow_(encoded_next_.Inverted())
#endif
{
}
template <typename... Args>
explicit FreelistEntry(FreelistEntry* next, Args&&... args)
: encoded_next_(EncodedPtr(next, std::forward<Args>(args)...))
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
,
shadow_(encoded_next_.Inverted())
#endif
{
}
// For testing only.
template <typename... Args>
FreelistEntry(void* next, Args&&... args, bool make_shadow_match)
: encoded_next_(EncodedPtr(next, std::forward<Args>(args)...))
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
,
shadow_(make_shadow_match ? encoded_next_.Inverted() : 12345)
#endif
{
}
public:
~FreelistEntry() = delete;
// Emplaces the freelist entry at the beginning of the given slot span, and
// initializes it as null-terminated.
PA_ALWAYS_INLINE static FreelistEntry* EmplaceAndInitNull(
void* slot_start_tagged) {
// |slot_start_tagged| is MTE-tagged.
auto* entry = new (slot_start_tagged) FreelistEntry(nullptr);
return entry;
}
PA_ALWAYS_INLINE static FreelistEntry* EmplaceAndInitNull(
UntaggedSlotStart slot_start) {
return EmplaceAndInitNull(slot_start.Tag().ToObject());
}
// Emplaces the freelist entry at the beginning of the given slot span, and
// initializes it with the given |next| pointer, but encoded.
//
// This freelist is built for the purpose of thread-cache. This means that we
// can't perform a check that this and the next pointer belong to the same
// super page, as thread-cache spans may chain slots across super pages.
template <typename... Args>
PA_ALWAYS_INLINE static FreelistEntry* EmplaceAndInitForThreadCache(
UntaggedSlotStart slot_start,
FreelistEntry* next,
Args&&... args) {
auto* entry = new (slot_start.Tag().ToObject())
FreelistEntry(next, std::forward<Args>(args)...);
return entry;
}
// Emplaces the freelist entry at the beginning of the given slot span, and
// initializes it with the given |next| pointer.
//
// This is for testing purposes only! |make_shadow_match| allows you to choose
// if the shadow matches the next pointer properly or is trash.
template <typename... Args>
PA_ALWAYS_INLINE static void EmplaceAndInitForTest(uintptr_t slot_start,
void* next,
Args&&... args,
bool make_shadow_match) {
new (UntaggedSlotStart::Unchecked(slot_start).Tag().ToObject())
FreelistEntry(next, std::forward<Args>(args)..., make_shadow_match);
}
void CorruptNextForTesting(uintptr_t v) {
// We just need a value that can never be a valid value here.
encoded_next_.Override(EncodedPtr::Transform(v));
}
// Puts `slot_size` on the stack before crashing in case of memory
// corruption. Meant to be used to report the failed allocation size.
template <typename... Args>
PA_ALWAYS_INLINE FreelistEntry* GetNextForThreadCache(size_t slot_size,
Args&&... args) const {
return GetNextInternal</*for_thread_cache=*/true>(
slot_size, std::forward<Args>(args)...);
}
template <typename... Args>
PA_ALWAYS_INLINE FreelistEntry* GetNext(size_t slot_size,
Args&&... args) const {
return GetNextInternal</*for_thread_cache=*/false>(
slot_size, std::forward<Args>(args)...);
}
PA_NOINLINE void CheckFreeList(size_t slot_size) const {
for (auto* entry = this; entry; entry = entry->GetNext(slot_size)) {
// `GetNext()` calls `IsWellFormed()`.
}
}
PA_NOINLINE void CheckFreeListForThreadCache(size_t slot_size) const {
for (auto* entry = this; entry;
entry = entry->GetNextForThreadCache(slot_size)) {
// `GetNextForThreadCache()` calls `IsWellFormed()`.
}
}
template <typename... Args>
PA_ALWAYS_INLINE void SetNext(FreelistEntry* entry, Args&&... args) {
// SetNext() is either called on the freelist head, when provisioning new
// slots, or when GetNext() has been called before, no need to pass the
// size.
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
// Regular freelists always point to an entry within the same super page.
//
// This is most likely a PartitionAlloc bug if this triggers.
if (entry &&
(SlotStart::Unchecked(this).Untag().value() & kSuperPageBaseMask) !=
(SlotStart::Unchecked(entry).Untag().value() & kSuperPageBaseMask))
[[unlikely]] {
FreelistCorruptionDetected(0);
}
#endif // PA_BUILDFLAG(DCHECKS_ARE_ON)
encoded_next_ = EncodedPtr(entry, std::forward<Args>(args)...);
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
shadow_ = encoded_next_.Inverted();
#endif
}
// Zeroes out |this| before returning the slot. The pointer to this memory
// will be returned to the user (caller of Alloc()), thus can't have internal
// data.
PA_ALWAYS_INLINE uintptr_t ClearForAllocation() {
encoded_next_.Override(0);
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
shadow_ = 0;
#endif
return SlotStart::Unchecked(this).Untag().value();
}
PA_ALWAYS_INLINE constexpr bool IsEncodedNextPtrZero() const {
return !encoded_next_;
}
private:
template <bool for_thread_cache, typename... Args>
PA_ALWAYS_INLINE FreelistEntry* GetNextInternal(size_t slot_size,
Args&&... args) const {
// GetNext() can be called on discarded memory, in which case
// |encoded_next_| is 0, and none of the checks apply. Don't prefetch
// nullptr either.
if (IsEncodedNextPtrZero()) {
return nullptr;
}
auto* ret = encoded_next_.Decode(slot_size, std::forward<Args>(args)...);
if (!IsWellFormed<for_thread_cache>(this, ret)) [[unlikely]] {
// Put the corrupted data on the stack, it may give us more information
// about what kind of corruption that was.
PA_DEBUG_DATA_ON_STACK("first",
static_cast<size_t>(encoded_next_.encoded_));
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
PA_DEBUG_DATA_ON_STACK("second", static_cast<size_t>(shadow_));
#endif
FreelistCorruptionDetected(slot_size);
}
// In real-world profiles, the load of |encoded_next_| above is responsible
// for a large fraction of the allocation cost. However, we cannot
// anticipate it enough since it is accessed right after we know its
// address.
//
// In the case of repeated allocations, we can prefetch the access that will
// be done at the *next* allocation, which will touch *ret, prefetch it.
PA_PREFETCH(ret);
return ret;
}
template <bool for_thread_cache>
PA_ALWAYS_INLINE static bool IsWellFormed(const FreelistEntry* here,
const FreelistEntry* next) {
// Don't allow the freelist to be blindly followed to any location.
// Checks following constraints:
// - `here->shadow_` must match an inversion of `here->next_` (if present).
// - `next` mustn't point inside the super page metadata area.
// - Unless this is a thread-cache freelist, `here` and `next` must belong
// to the same super page (as a matter of fact, they must belong to the
// same slot span, but that'd be too expensive to check here).
// - `next` is marked as free in the free slot bitmap (if present).
const uintptr_t here_address = SlotStart::Unchecked(here).Untag().value();
const uintptr_t next_address = SlotStart::Unchecked(next).Untag().value();
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
bool shadow_ptr_ok = here->encoded_next_.Inverted() == here->shadow_;
#else
constexpr bool shadow_ptr_ok = true;
#endif
// This is necessary but not sufficient when quarantine is enabled, see
// SuperPagePayloadBegin() in partition_page.h. However we don't want to
// fetch anything from the root in this function.
const bool not_in_metadata =
(next_address & kSuperPageOffsetMask) >= PartitionPageSize();
if constexpr (for_thread_cache) {
return shadow_ptr_ok & not_in_metadata;
}
const bool same_super_page = (here_address & kSuperPageBaseMask) ==
(next_address & kSuperPageBaseMask);
return shadow_ptr_ok & same_super_page & not_in_metadata;
}
EncodedPtr encoded_next_;
// This is intended to detect unintentional corruptions of the freelist.
// These can happen due to a Use-after-Free, or overflow of the previous
// allocation in the slot span.
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
uintptr_t shadow_;
#endif
};
// Assertions that are agnostic to the implementation of the freelist.
static_assert(BucketIndexLookup::kMinBucketSize >=
sizeof(partition_alloc::internal::FreelistEntry),
"Need enough space for freelist entries in the smallest slot");
} // namespace partition_alloc::internal
#endif // PARTITION_ALLOC_PARTITION_FREELIST_ENTRY_H_