blob: 7e567cf1176740db4c5926c945c2279d4c875521 [file]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "partition_alloc/slot_start.h"
#ifndef PARTITION_ALLOC_THREAD_CACHE_H_
#define PARTITION_ALLOC_THREAD_CACHE_H_
#include <atomic>
#include <cstdint>
#include <limits>
#include <memory>
#include "partition_alloc/bucket_lookup.h"
#include "partition_alloc/build_config.h"
#include "partition_alloc/buildflags.h"
#include "partition_alloc/partition_alloc-inl.h"
#include "partition_alloc/partition_alloc_base/compiler_specific.h"
#include "partition_alloc/partition_alloc_base/component_export.h"
#include "partition_alloc/partition_alloc_base/cxx_wrapper/optional.h"
#include "partition_alloc/partition_alloc_base/thread_annotations.h"
#include "partition_alloc/partition_alloc_base/time/time.h"
#include "partition_alloc/partition_alloc_config.h"
#include "partition_alloc/partition_alloc_constants.h"
#include "partition_alloc/partition_alloc_forward.h"
#include "partition_alloc/partition_freelist_entry.h"
#include "partition_alloc/partition_lock.h"
#include "partition_alloc/partition_stats.h"
#include "partition_alloc/partition_tls.h"
#include "partition_alloc/scheduler_loop_quarantine.h"
#if PA_BUILDFLAG(PA_ARCH_CPU_X86_64) && PA_BUILDFLAG(HAS_64_BIT_POINTERS)
#include "partition_alloc/partition_alloc_base/cxx_wrapper/algorithm.h"
#endif
namespace partition_alloc {
class ThreadCache;
namespace tools {
// This is used from ThreadCacheInspector, which runs in a different process. It
// scans the process memory looking for the two needles, to locate the thread
// cache registry instance.
//
// These two values were chosen randomly, and in particular neither is a valid
// pointer on most 64 bit architectures.
#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
constexpr uintptr_t kNeedle1 = 0xe69e32f3ad9ea63;
constexpr uintptr_t kNeedle2 = 0x9615ee1c5eb14caf;
#else
constexpr uintptr_t kNeedle1 = 0xe69e32f3;
constexpr uintptr_t kNeedle2 = 0x9615ee1c;
#endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
// This array contains, in order:
// - kNeedle1
// - &ThreadCacheRegistry::Instance()
// - kNeedle2
//
// It is refererenced in the thread cache constructor to make sure it is not
// removed by the compiler. It is also not const to make sure it ends up in
// .data.
constexpr size_t kThreadCacheNeedleArraySize = 4;
extern uintptr_t kThreadCacheNeedleArray[kThreadCacheNeedleArraySize];
class HeapDumper;
class ThreadCacheInspector;
} // namespace tools
namespace internal {
extern PA_COMPONENT_EXPORT(PARTITION_ALLOC) PartitionTlsKey g_thread_cache_key;
constexpr inline size_t kMaxThreadCacheIndex = 4;
constexpr inline size_t kDefaultRootThreadCacheIndex = 0;
constexpr inline size_t kInvalidThreadCacheIndex = static_cast<size_t>(-1);
#if PA_CONFIG(THREAD_CACHE_FAST_TLS)
extern PA_COMPONENT_EXPORT(
PARTITION_ALLOC) thread_local ThreadCache* g_thread_caches
[kMaxThreadCacheIndex];
#endif
#if PA_CONFIG(THREAD_CACHE_FAST_TLS)
// Represents a special index value within the thread-local `g_thread_caches`
// array used to mark a "tombstone" state. Marking only at a specific index is
// sufficient because all ThreadCaches for a single thread are deallocated
// together when the thread terminates.
constexpr inline size_t kThreadCacheTombstoneIndex = 0;
#endif
// Represents an index of the array (g_thread_caches, PartitionTls)
// storing the thread cache for SchedulerLoopQuarantine.
constexpr inline size_t kThreadCacheQuarantineIndex = 0;
} // namespace internal
// Global registry of all ThreadCache instances.
//
// This class cannot allocate in the (Un)registerThreadCache() functions, as
// they are called from ThreadCache constructor, which is from within the
// allocator. However the other members can allocate.
class PA_COMPONENT_EXPORT(PARTITION_ALLOC) ThreadCacheRegistry {
public:
static ThreadCacheRegistry& Instance();
// Do not instantiate.
//
// Several things are surprising here:
// - The constructor is public even though this is intended to be a singleton:
// we cannot use a "static local" variable in |Instance()| as this is
// reached too early during CRT initialization on Windows, meaning that
// static local variables don't work (as they call into the uninitialized
// runtime). To sidestep that, we use a regular global variable in the .cc,
// which is fine as this object's constructor is constexpr.
// - Marked inline so that the chromium style plugin doesn't complain that a
// "complex constructor" has an inline body. This warning is disabled when
// the constructor is explicitly marked "inline". Note that this is a false
// positive of the plugin, since constexpr implies inline.
inline constexpr ThreadCacheRegistry();
void RegisterThreadCache(ThreadCache* cache);
void UnregisterThreadCache(ThreadCache* cache);
// Prints statistics for all thread caches, or this thread's only.
void DumpStats(bool my_thread_only, ThreadCacheStats* stats, size_t index);
// Purge() this thread's cache, and asks the other ones to trigger Purge() at
// a later point (during a deallocation).
void PurgeAll();
// Runs `PurgeAll` and updates the next interval which
// `GetPeriodicPurgeNextIntervalInMicroseconds` returns.
//
// Note that it's a caller's responsibility to invoke this member function
// periodically with an appropriate interval. This function does not schedule
// any task nor timer.
void RunPeriodicPurge();
// Returns the appropriate interval to invoke `RunPeriodicPurge` next time.
int64_t GetPeriodicPurgeNextIntervalInMicroseconds() const;
// Controls the thread cache size, by setting the multiplier to a value above
// or below |ThreadCache::kDefaultMultiplier|.
void SetThreadCacheMultiplier(float multiplier);
void SetLargestActiveBucketIndex(uint16_t largest_active_bucket_index);
static internal::Lock& GetLock() { return Instance().lock_; }
// Purges all thread caches *now*. This is completely thread-unsafe, and
// should only be called in a post-fork() handler.
void ForcePurgeAllThreadAfterForkUnsafe();
void ResetForTesting();
static constexpr internal::base::TimeDelta kMinPurgeInterval =
internal::base::Seconds(1);
static constexpr internal::base::TimeDelta kMaxPurgeInterval =
internal::base::Minutes(1);
static constexpr internal::base::TimeDelta kDefaultPurgeInterval =
2 * kMinPurgeInterval;
static constexpr size_t kMinCachedMemoryForPurgingBytes = 500 * 1024;
private:
friend class tools::ThreadCacheInspector;
friend class tools::HeapDumper;
// Not using base::Lock as the object's constructor must be constexpr.
internal::Lock lock_;
ThreadCache* list_head_ PA_GUARDED_BY(GetLock()) = nullptr;
bool periodic_purge_is_initialized_ = false;
internal::base::TimeDelta periodic_purge_next_interval_;
uint16_t largest_active_bucket_index_ =
BucketIndexLookup::GetIndexForNeutralBuckets(
kThreadCacheDefaultSizeThreshold);
};
constexpr ThreadCacheRegistry::ThreadCacheRegistry() = default;
#if PA_CONFIG(THREAD_CACHE_ENABLE_STATISTICS)
#define PA_INCREMENT_COUNTER(counter) ++counter
#else
#define PA_INCREMENT_COUNTER(counter) \
do { \
} while (0)
#endif // PA_CONFIG(THREAD_CACHE_ENABLE_STATISTICS)
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
namespace internal {
class ReentrancyGuard {
public:
explicit ReentrancyGuard(bool& flag) : flag_(flag) {
PA_CHECK(!flag_);
flag_ = true;
}
~ReentrancyGuard() { flag_ = false; }
private:
bool& flag_;
};
} // namespace internal
#define PA_REENTRANCY_GUARD(x) \
internal::ReentrancyGuard guard { \
x \
}
#else // PA_BUILDFLAG(DCHECKS_ARE_ON)
#define PA_REENTRANCY_GUARD(x) \
do { \
} while (0)
#endif // PA_BUILDFLAG(DCHECKS_ARE_ON)
// Per-thread cache. *Not* threadsafe, must only be accessed from a single
// thread.
//
// In practice, this is easily enforced as long as only |instance| is
// manipulated, as it is a thread_local member. As such, any
// |ThreadCache::instance->*()| call will necessarily be done from a single
// thread.
class PA_COMPONENT_EXPORT(PARTITION_ALLOC) ThreadCache {
public:
struct Bucket {
internal::FreelistEntry* freelist_head = nullptr;
// Want to keep sizeof(Bucket) small, using small types.
uint8_t count = 0;
std::atomic<uint8_t> limit{}; // Can be changed from another thread.
uint16_t slot_size = 0;
Bucket();
};
// Initializes the thread cache for |root|. May allocate, so should be called
// with the thread cache disabled on the partition side, and without the
// partition lock held.
//
// May only be called by a single PartitionRoot.
static void Init(PartitionRoot* root);
static bool IsInitialized();
static void DeleteForTesting();
// Deletes existing thread cache and creates a new one for |root|.
static void SwapForTesting(PartitionRoot* root, size_t index);
// Removes the tombstone marker that would be returned by Get() otherwise.
static void RemoveTombstoneForTesting();
// Can be called several times, must be called before any ThreadCache
// interactions.
static void EnsureThreadSpecificDataInitialized();
static ThreadCache* Get(size_t index) {
PA_DCHECK(index < internal::kMaxThreadCacheIndex);
#if PA_CONFIG(THREAD_CACHE_FAST_TLS)
return PA_UNSAFE_TODO(internal::g_thread_caches[index]);
#else
// This region isn't MTE-tagged.
auto* ptr = reinterpret_cast<ThreadCache*>(
internal::PartitionTlsGet(internal::g_thread_cache_key));
// TODO(crbug.com/467243745): Eliminate the `IsValidPtr` check. Improve
// `IsValidPtr` to also validate against `nullptr + index` and `kTombstone +
// index`.
if (!ThreadCache::IsValidPtr(ptr)) [[unlikely]] {
return nullptr;
}
return PA_UNSAFE_TODO(ptr + index);
#endif
}
// Get ThreadCache for SchedulerLoopQurantine, which is stored in index 0.
static ThreadCache* EnsureAndGetForQuarantine();
// Returns true if the given pointer is not nullptr or kTombstone.
static bool IsValidPtr(ThreadCache* tcache) {
// Do not MTE-untag, as it'd mess up the sentinel value.
return reinterpret_cast<uintptr_t>(tcache) & kTombstoneMask;
}
// Returns true if the ThreadCache* from ThreadCache::Get() is valid
// and initialized.
static bool IsValid(ThreadCache* tcache) {
#if PA_CONFIG(THREAD_CACHE_FAST_TLS)
// `g_thread_caches[index]` has valid pointers only if the ThreadCache
// object is initialized.
return IsValidPtr(tcache);
#else
// Even if the array of ThreadCache is allocated, the ThreadCache object
// may not be initialized, and thus check `root_` to know if initialized.
// We use pointer arithmetic to directly inspect the memory for `root_`, as
// accessing `tcache->root_` is UB before the ThreadCache object's lifetime
// begins (i.e., between memset(0) and placement new).
return tcache && PA_UNSAFE_TODO(*reinterpret_cast<uintptr_t*>(
(reinterpret_cast<uint8_t*>(tcache) +
offsetof(ThreadCache, root_))));
#endif
}
static bool IsTombstone() {
#if PA_CONFIG(THREAD_CACHE_FAST_TLS)
void* ptr = PA_UNSAFE_TODO(
internal::g_thread_caches[internal::kThreadCacheTombstoneIndex]);
#else
void* ptr = internal::PartitionTlsGet(internal::g_thread_cache_key);
#endif
// Do not MTE-untag, as it'd mess up the sentinel value.
return reinterpret_cast<uintptr_t>(ptr) == kTombstone;
}
// Create a new ThreadCache associated with |root|.
// Must be called without the partition locked, as this may allocate.
static ThreadCache* Create(PartitionRoot* root, size_t index);
~ThreadCache();
// Disallow copy and move.
ThreadCache(const ThreadCache&) = delete;
ThreadCache(const ThreadCache&&) = delete;
ThreadCache& operator=(const ThreadCache&) = delete;
// Tries to put a slot at |slot_start| into the cache.
// The slot comes from the bucket at index |bucket_index| from the partition
// this cache is for.
//
// Returns the slot size if the insertion succeeds, `nullopt` otherwise.
// Insertion can fail either because the cache is full or the
// allocation was too large.
PA_ALWAYS_INLINE std::optional<size_t> MaybePutInCache(
internal::UntaggedSlotStart slot_start,
size_t bucket_index);
// Tries to allocate a memory slot from the cache.
// Returns 0 on failure.
//
// Has the same behavior as RawAlloc(), that is: no cookie nor ref-count
// handling. Sets |slot_size| to the allocated size upon success.
PA_ALWAYS_INLINE internal::UntaggedSlotStart GetFromCache(size_t bucket_index,
size_t* slot_size);
// Asks this cache to trigger |Purge()| at a later point. Can be called from
// any thread.
void SetShouldPurge();
// Empties the cache.
// The Partition lock must *not* be held when calling this.
// Must be called from the thread this cache is for.
void Purge();
// Amount of cached memory for this thread's cache, in bytes.
size_t CachedMemory() const;
void AccumulateStats(ThreadCacheStats* stats) const;
// Purge the thread cache of the current thread, if one exists.
static void PurgeCurrentThread();
const ThreadAllocStats& thread_alloc_stats() const {
return thread_alloc_stats_;
}
size_t bucket_count_for_testing(size_t index) const {
return PA_UNSAFE_TODO(buckets_[index]).count;
}
internal::base::PlatformThreadId thread_id() const { return thread_id_; }
// Sets the maximum size of allocations that may be cached by the thread
// cache. This applies to all threads. However, the maximum size is bounded by
// |kLargeSizeThreshold|.
static void SetLargestCachedSize(size_t size);
// Cumulative stats about *all* allocations made on the `root_` partition on
// this thread, that is not only the allocations serviced by the thread cache,
// but all allocations, including large and direct-mapped ones. This should in
// theory be split into a separate PerThread data structure, but the thread
// cache is the only per-thread data we have as of now.
//
// TODO(lizeb): Investigate adding a proper per-thread data structure.
PA_ALWAYS_INLINE void RecordAllocation(size_t size);
PA_ALWAYS_INLINE void RecordDeallocation(size_t size);
void ResetPerThreadAllocationStatsForTesting();
// Fill 1 / kBatchFillRatio * bucket.limit slots at a time.
static constexpr uint16_t kBatchFillRatio = 8;
// Limit for the smallest bucket will be kDefaultMultiplier *
// kSmallBucketBaseCount by default.
static constexpr float kDefaultMultiplier = 2.;
static constexpr uint8_t kSmallBucketBaseCount = 64;
static constexpr size_t kDefaultSizeThreshold =
kThreadCacheDefaultSizeThreshold;
static constexpr size_t kLargeSizeThreshold = kThreadCacheLargeSizeThreshold;
static constexpr uint16_t kBucketCount =
BucketIndexLookup::GetIndexForNeutralBuckets(
kThreadCacheLargeSizeThreshold) +
1;
static_assert(
kBucketCount <= BucketIndexLookup::kNumBuckets,
"Cannot have more cached buckets than what the allocator supports");
const ThreadCache* prev_for_testing() const
PA_EXCLUSIVE_LOCKS_REQUIRED(ThreadCacheRegistry::GetLock()) {
return prev_;
}
const ThreadCache* next_for_testing() const
PA_EXCLUSIVE_LOCKS_REQUIRED(ThreadCacheRegistry::GetLock()) {
return next_;
}
ThreadCacheStats& stats_for_testing() { return stats_; }
PartitionRoot* GetRoot();
Bucket& bucket_for_testing(size_t index) {
return PA_UNSAFE_TODO(buckets_[index]);
}
void ClearBucketForTesting(Bucket& bucket, size_t limit) {
ClearBucket(bucket, limit);
}
internal::ThreadBoundSchedulerLoopQuarantineBranch&
GetSchedulerLoopQuarantineBranch() {
return scheduler_loop_quarantine_branch_;
}
// Returns true if the given address is in the thread cache's freelist.
// Otherwise, returns false.
bool IsInFreelist(internal::UntaggedSlotStart address,
size_t bucket_index,
size_t& position);
private:
friend class tools::HeapDumper;
friend class tools::ThreadCacheInspector;
static_assert(sizeof(Bucket) <= 2 * sizeof(void*), "Keep Bucket small.");
explicit ThreadCache(PartitionRoot* root);
static void Delete(void* thread_caches_ptr);
static void* operator new(size_t count);
static void operator delete(void* ptr);
void PurgeInternal();
// Fills a bucket from the central allocator.
void FillBucket(size_t bucket_index);
// Empties the |bucket| until there are at most |limit| objects in it.
void ClearBucket(Bucket& bucket, size_t limit);
PA_ALWAYS_INLINE void PutInBucket(Bucket& bucket,
internal::UntaggedSlotStart slot_start);
void ResetForTesting();
// Releases the entire freelist starting at |head| to the root.
void FreeAfter(internal::FreelistEntry* head, size_t slot_size);
static void SetGlobalLimits(PartitionRoot* root, float multiplier);
// On some architectures, ThreadCache::Get() can be called and return
// something after the thread cache has been destroyed. In this case, we set
// it to this value, to signal that the thread is being terminated, and the
// thread cache should not be used.
//
// This happens in particular on Windows, during program termination.
//
// We choose 0x1 as the value as it is an invalid pointer value, since it is
// not aligned, and too low. Also, checking !(ptr & kTombstoneMask) checks for
// nullptr and kTombstone at the same time.
static constexpr uintptr_t kTombstone = 0x1;
static constexpr uintptr_t kTombstoneMask = ~kTombstone;
static uint8_t global_limits_[kBucketCount];
// Index of the largest active bucket. Not all processes/platforms will use
// all buckets, as using larger buckets increases the memory footprint.
//
// TODO(lizeb): Investigate making this per-thread rather than static, to
// improve locality, and open the door to per-thread settings.
static uint16_t largest_active_bucket_index_;
// These are at the beginning as they're accessed for each allocation.
uint32_t cached_memory_ = 0;
std::atomic<bool> should_purge_;
#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
const internal::PoolOffsetLookup offset_lookup_;
#endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
ThreadCacheStats stats_;
ThreadAllocStats thread_alloc_stats_;
// Buckets are quite big, though each is only 2 pointers.
Bucket buckets_[kBucketCount];
// Cold data below.
PartitionRoot* const root_;
const internal::base::PlatformThreadId thread_id_;
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
bool is_in_thread_cache_ = false;
#endif
// Intrusive list since ThreadCacheRegistry::RegisterThreadCache() cannot
// allocate.
ThreadCache* next_ PA_GUARDED_BY(ThreadCacheRegistry::GetLock());
ThreadCache* prev_ PA_GUARDED_BY(ThreadCacheRegistry::GetLock());
// Thread-Local version of `PartitionRoot::scheduler_loop_quarantine_branch_`.
internal::ThreadBoundSchedulerLoopQuarantineBranch
scheduler_loop_quarantine_branch_;
friend class ThreadCacheRegistry;
friend class PartitionAllocThreadCacheTest;
friend class tools::ThreadCacheInspector;
};
PA_ALWAYS_INLINE std::optional<size_t> ThreadCache::MaybePutInCache(
internal::UntaggedSlotStart slot_start,
size_t bucket_index) {
PA_REENTRANCY_GUARD(is_in_thread_cache_);
PA_INCREMENT_COUNTER(stats_.cache_fill_count);
if (bucket_index > largest_active_bucket_index_) [[unlikely]] {
PA_INCREMENT_COUNTER(stats_.cache_fill_misses);
return std::nullopt;
}
auto& bucket = PA_UNSAFE_TODO(buckets_[bucket_index]);
PA_DCHECK(bucket.count != 0 || bucket.freelist_head == nullptr);
PutInBucket(bucket, slot_start);
cached_memory_ += bucket.slot_size;
PA_INCREMENT_COUNTER(stats_.cache_fill_hits);
// Relaxed ordering: we don't care about having an up-to-date or consistent
// value, just want it to not change while we are using it, hence using
// relaxed ordering, and loading into a local variable. Without it, we are
// gambling that the compiler would not issue multiple loads.
uint8_t limit = bucket.limit.load(std::memory_order_relaxed);
// Batched deallocation, amortizing lock acquisitions.
if (bucket.count > limit) [[unlikely]] {
ClearBucket(bucket, limit / 2);
}
if (should_purge_.load(std::memory_order_relaxed)) [[unlikely]] {
PurgeInternal();
}
return bucket.slot_size;
}
PA_ALWAYS_INLINE internal::UntaggedSlotStart ThreadCache::GetFromCache(
size_t bucket_index,
size_t* slot_size) {
#if PA_CONFIG(THREAD_CACHE_ALLOC_STATS)
stats_.allocs_per_bucket_[bucket_index]++;
#endif
PA_REENTRANCY_GUARD(is_in_thread_cache_);
PA_INCREMENT_COUNTER(stats_.alloc_count);
// Only handle "small" allocations.
if (bucket_index > largest_active_bucket_index_) [[unlikely]] {
PA_INCREMENT_COUNTER(stats_.alloc_miss_too_large);
PA_INCREMENT_COUNTER(stats_.alloc_misses);
return internal::UntaggedSlotStart();
}
auto& bucket = PA_UNSAFE_TODO(buckets_[bucket_index]);
if (bucket.freelist_head) [[likely]] {
PA_INCREMENT_COUNTER(stats_.alloc_hits);
} else {
PA_DCHECK(bucket.count == 0);
PA_INCREMENT_COUNTER(stats_.alloc_miss_empty);
PA_INCREMENT_COUNTER(stats_.alloc_misses);
FillBucket(bucket_index);
// Very unlikely, means that the central allocator is out of memory. Let it
// deal with it (may return 0, may crash).
if (!bucket.freelist_head) [[unlikely]] {
return internal::UntaggedSlotStart();
}
}
PA_DCHECK(bucket.count != 0);
internal::FreelistEntry* entry = bucket.freelist_head;
// TODO(lizeb): Consider removing once crbug.com/1382658 is fixed.
#if PA_BUILDFLAG(IS_CHROMEOS) && PA_BUILDFLAG(PA_ARCH_CPU_X86_64) && \
PA_BUILDFLAG(HAS_64_BIT_POINTERS)
// x86_64 architecture now supports 57 bits of address space, as of Ice Lake
// for Intel. However Chrome OS systems do not ship with kernel support for
// it, but with 48 bits, so all canonical addresses have the upper 16 bits
// zeroed (17 in practice, since the upper half of address space is reserved
// by the kernel).
constexpr uintptr_t kCanonicalPointerMask = (1ULL << 48) - 1;
PA_CHECK(!(reinterpret_cast<uintptr_t>(entry) & ~kCanonicalPointerMask));
#endif // PA_BUILDFLAG(IS_CHROMEOS) && PA_BUILDFLAG(PA_ARCH_CPU_X86_64) &&
// PA_BUILDFLAG(HAS_64_BIT_POINTERS)
// Passes the bucket size to |GetNextForThreadCache()|, so that in case of
// freelist corruption, we know the bucket size that lead to the crash,
// helping to narrow down the search for culprit. |bucket| was touched just
// now, so this does not introduce another cache miss.
#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
internal::FreelistEntry* next =
entry->GetNextForThreadCache(bucket.slot_size, offset_lookup_);
#else
internal::FreelistEntry* next =
entry->GetNextForThreadCache(bucket.slot_size);
#endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
PA_DCHECK(entry != next);
bucket.count--;
PA_DCHECK(bucket.count != 0 || !next);
bucket.freelist_head = next;
*slot_size = bucket.slot_size;
PA_DCHECK(cached_memory_ >= bucket.slot_size);
cached_memory_ -= bucket.slot_size;
return internal::SlotStart::Unchecked(entry).Untag();
}
PA_ALWAYS_INLINE void ThreadCache::PutInBucket(
Bucket& bucket,
internal::UntaggedSlotStart slot_start) {
#if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY) && \
PA_BUILDFLAG(PA_ARCH_CPU_X86_64) && PA_BUILDFLAG(HAS_64_BIT_POINTERS)
// We see freelist corruption crashes happening in the wild. These are likely
// due to out-of-bounds accesses in the previous slot, or to a Use-After-Free
// somewhere in the code.
//
// The issue is that we detect the UaF far away from the place where it
// happens. As a consequence, we should try to make incorrect code crash as
// early as possible. Poisoning memory at free() time works for UaF, but it
// was seen in the past to incur a high performance cost.
//
// Here, only poison the current cacheline, which we are touching anyway.
// TODO(lizeb): Make sure this does not hurt performance.
// Everything below requires this alignment.
static_assert(internal::kAlignment == 16, "");
// The pointer is always 16 bytes aligned, so its start address is always == 0
// % 16. Its distance to the next cacheline is
// `64 - ((slot_start & 63) / 16) * 16`
static_assert(
internal::kPartitionCachelineSize == 64,
"The computation below assumes that cache lines are 64 bytes long.");
int distance_to_next_cacheline_in_16_bytes =
4 - ((slot_start.value() >> 4) & 3);
int slot_size_remaining_in_16_bytes = bucket.slot_size / 16;
slot_size_remaining_in_16_bytes = std::min(
slot_size_remaining_in_16_bytes, distance_to_next_cacheline_in_16_bytes);
static const uint32_t poison_16_bytes[4] = {0xbadbad00, 0xbadbad00,
0xbadbad00, 0xbadbad00};
#if !(PA_BUILDFLAG(IS_WIN) && defined(COMPONENT_BUILD)) && \
PA_HAS_BUILTIN(__builtin_assume_aligned)
void* slot_start_tagged = __builtin_assume_aligned(
slot_start.Tag().ToObject(), internal::kAlignment);
#else
// TODO(crbug.com/40262684): std::assume_aligned introduce an additional
// dependency: _libcpp_verbose_abort(const char*, ...). It will cause
// "undefined symbol" error when linking allocator_shim.dll.
void* slot_start_tagged = slot_start.Tag().ToObject();
#endif
uint32_t* address_aligned = static_cast<uint32_t*>(slot_start_tagged);
for (int i = 0; i < slot_size_remaining_in_16_bytes; i++) {
// Clang will expand the memcpy to a 16-byte write (movups on x86).
PA_UNSAFE_TODO(
memcpy(address_aligned, poison_16_bytes, sizeof(poison_16_bytes)));
PA_UNSAFE_TODO(address_aligned += 4);
}
#endif // PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY) &&
// PA_BUILDFLAG(PA_ARCH_CPU_X86_64) && PA_BUILDFLAG(HAS_64_BIT_POINTERS)
#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
auto* entry = internal::FreelistEntry::EmplaceAndInitForThreadCache(
slot_start, bucket.freelist_head, offset_lookup_);
#else
auto* entry = internal::FreelistEntry::EmplaceAndInitForThreadCache(
slot_start, bucket.freelist_head);
#endif
bucket.freelist_head = entry;
bucket.count++;
}
PA_ALWAYS_INLINE void ThreadCache::RecordAllocation(size_t size) {
thread_alloc_stats_.alloc_count++;
thread_alloc_stats_.alloc_total_size += size;
}
PA_ALWAYS_INLINE void ThreadCache::RecordDeallocation(size_t size) {
thread_alloc_stats_.dealloc_count++;
thread_alloc_stats_.dealloc_total_size += size;
}
} // namespace partition_alloc
#endif // PARTITION_ALLOC_THREAD_CACHE_H_