blob: dbe87e472b5e4937a8e3bf9c51aa34d52033b202 [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.
#include "partition_alloc/scheduler_loop_quarantine.h"
#include <atomic>
#include "partition_alloc/internal_allocator.h"
#include "partition_alloc/partition_alloc_base/compiler_specific.h"
#include "partition_alloc/partition_alloc_check.h"
#include "partition_alloc/partition_page.h"
#include "partition_alloc/partition_root.h"
#include "partition_alloc/scheduler_loop_quarantine_support.h"
#include "partition_alloc/slot_start.h"
#include "partition_alloc/thread_cache.h"
namespace partition_alloc::internal {
namespace {
// Utility to disable thread safety analysis when we know it is safe.
class PA_SCOPED_LOCKABLE FakeScopedGuard {
public:
PA_ALWAYS_INLINE explicit FakeScopedGuard(Lock& lock)
PA_EXCLUSIVE_LOCK_FUNCTION(lock) {}
// For some reason, defaulting this causes a thread safety annotation failure.
PA_ALWAYS_INLINE
~FakeScopedGuard() // NOLINT(modernize-use-equals-default)
PA_UNLOCK_FUNCTION() {}
};
// Utility classes to lock only if a condition is met.
template <bool thread_bound>
using ScopedGuardIfNeeded =
std::conditional_t<thread_bound, FakeScopedGuard, ScopedGuard>;
// When set to `true`, all the branches stop purging. It helps to reduce
// shutdown hangs.
std::atomic_bool g_no_purge = false;
} // namespace
// Utility class to process batched-free operation.
class BatchFreeQueue {
public:
PA_ALWAYS_INLINE explicit BatchFreeQueue(PartitionRoot* root) : root_(root) {}
PA_ALWAYS_INLINE ~BatchFreeQueue() { Purge(); }
PA_ALWAYS_INLINE void Queue(UntaggedSlotStart slot_start) {
auto* slot_span = SlotSpanMetadata::FromSlotStart(slot_start, root_);
// Direct-mapped deallocation releases then re-acquires the lock. The caller
// may not expect that, but we never call this function on direct-mapped
// allocations.
PA_DCHECK(!root_->IsDirectMapped(slot_span));
Entry& entry = queue_[size_];
++size_;
entry = {
.slot_start = slot_start,
.slot_span = slot_span,
};
if (size_ == kQueueSize) {
Purge();
}
}
PA_ALWAYS_INLINE void Purge() {
if (!size_) {
return;
}
for (size_t i = 0; i < size_; ++i) {
Entry& entry = queue_[i];
// Make sure that we fault *before* locking. See `PartitionRoot::RawFree`
// for detailed performance reasons.
auto* object = entry.slot_start.Tag().ToObject<volatile uintptr_t>();
*object = 0;
// Also we are going to write into |*slot_span|.
PA_PREFETCH_FOR_WRITE(entry.slot_span);
}
internal::ScopedGuard guard(internal::PartitionRootLock(root_));
do {
--size_;
Entry& entry = queue_[size_];
root_->RawFreeLocked(entry.slot_start, entry.slot_span);
} while (size_);
}
private:
struct Entry {
UntaggedSlotStart slot_start;
SlotSpanMetadata* slot_span;
};
PartitionRoot* const root_;
size_t size_ = 0;
constexpr static size_t kQueueSize = 16;
std::array<Entry, kQueueSize> queue_;
};
template <bool thread_bound>
SchedulerLoopQuarantineBranch<thread_bound>::SchedulerLoopQuarantineBranch(
PartitionRoot* allocator_root,
ThreadCache* tcache)
: allocator_root_(allocator_root), tcache_(tcache) {
PA_CHECK(allocator_root);
if constexpr (kThreadBound) {
PA_CHECK(tcache_);
} else {
PA_CHECK(!tcache_);
}
}
template <bool thread_bound>
SchedulerLoopQuarantineBranch<thread_bound>::~SchedulerLoopQuarantineBranch() {
Destroy();
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::Configure(
SchedulerLoopQuarantineRoot& root,
const SchedulerLoopQuarantineConfig& config) {
// Note the Quarantine could be paused here because scoped-opt outs are not
// aware of the feature being enabled or disabled.
PA_CHECK(allocator_root_ == &root.allocator_root_);
if constexpr (kThreadBound) {
PA_CHECK(tcache_->GetRoot() == &root.allocator_root_);
}
ScopedGuardIfNeeded<kThreadBound> guard(lock_);
config_for_testing_ = config;
if (enable_quarantine_) {
// Already enabled, explicitly purging an existing instance.
PurgeInternal(0);
PA_CHECK(slots_.empty());
slots_.shrink_to_fit();
}
root_ = &root;
enable_quarantine_ = config.enable_quarantine;
enable_zapping_ = config.enable_zapping;
leak_on_destruction_ = config.leak_on_destruction;
branch_capacity_in_bytes_ = config.branch_capacity_in_bytes;
// This bucket index can be invalid if "Neutral" distribution is in use,
// but value here is only for comparison and should be safe.
largest_bucket_index_ =
BucketIndexLookup::GetIndexForDenserBuckets(config.max_quarantine_size);
PA_CHECK(largest_bucket_index_ < BucketIndexLookup::kNumBuckets);
PA_UNSAFE_TODO(PA_CHECK(&allocator_root_->buckets_[largest_bucket_index_] <=
&allocator_root_->sentinel_bucket_));
}
template <bool thread_bound>
bool SchedulerLoopQuarantineBranch<thread_bound>::IsQuarantinedForTesting(
void* object) {
ScopedGuardIfNeeded<kThreadBound> guard(lock_);
UntaggedSlotStart slot_start = SlotStart::Unchecked(object).Untag();
for (const auto& slot : slots_) {
if (slot.slot_start.Untag() == slot_start) {
return true;
}
}
return false;
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::SetCapacityInBytes(
size_t capacity_in_bytes) {
branch_capacity_in_bytes_.store(capacity_in_bytes, std::memory_order_relaxed);
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::Purge() {
ScopedGuardIfNeeded<kThreadBound> guard(lock_);
PurgeInternal(0);
slots_.shrink_to_fit();
PA_DCHECK(slots_.capacity() == 0);
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::Destroy() {
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
being_destructed_ = true;
#endif // PA_BUILDFLAG(DCHECKS_ARE_ON)
if (!leak_on_destruction_) {
Purge();
}
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::Quarantine(
SlotStart slot_start,
SlotSpanMetadata* slot_span,
const internal::BucketSizeDetails& size_details) {
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
PA_DCHECK(!being_destructed_);
#endif // PA_BUILDFLAG(DCHECKS_ARE_ON)
if (!enable_quarantine_ || pause_quarantine_) [[unlikely]] {
return allocator_root_->RawFreeWithThreadCache(slot_start, size_details,
slot_span);
}
if (size_details.slot_size > BucketIndexLookup::kMaxBucketSize ||
largest_bucket_index_ < size_details.bucket_index) [[unlikely]] {
// The allocation is direct-mapped or larger than `largest_bucket_index_`.
return allocator_root_->RawFreeWithThreadCache(slot_start, size_details,
slot_span);
}
PA_DCHECK(!allocator_root_->IsDirectMapped(slot_span));
PA_DCHECK(
slot_span->bucket >= PA_UNSAFE_TODO(&allocator_root_->buckets_[0]) &&
slot_span->bucket <=
PA_UNSAFE_TODO(&allocator_root_->buckets_[largest_bucket_index_]));
const size_t slot_size = size_details.slot_size;
const size_t capacity_in_bytes =
branch_capacity_in_bytes_.load(std::memory_order_relaxed);
if (capacity_in_bytes < slot_size) [[unlikely]] {
// Even if this branch dequarantines all entries held by it, this entry
// cannot fit within the capacity.
allocator_root_->RawFreeWithThreadCache(slot_start, size_details,
slot_span);
root_->quarantine_miss_count_.fetch_add(1u, std::memory_order_relaxed);
return;
}
ScopedGuardIfNeeded<kThreadBound> guard(lock_);
// Dequarantine some entries as required.
PurgeInternal(capacity_in_bytes - slot_size);
// Put the entry onto the list.
branch_size_in_bytes_ += slot_size;
slots_.push_back({
.slot_start = slot_start,
.bucket_index = size_details.bucket_index,
});
// Swap randomly so that the quarantine list remain shuffled.
// This is not uniformly random, but sufficiently random.
const size_t random_index = random_.RandUint32() % slots_.size();
std::swap(slots_[random_index], slots_.back());
// Update stats (not locked).
root_->count_.fetch_add(1, std::memory_order_relaxed);
root_->size_in_bytes_.fetch_add(slot_size, std::memory_order_relaxed);
root_->cumulative_count_.fetch_add(1, std::memory_order_relaxed);
root_->cumulative_size_in_bytes_.fetch_add(slot_size,
std::memory_order_relaxed);
if (enable_zapping_) {
internal::SecureMemset(slot_start.ToObject(), internal::kFreedByte,
slot_size);
}
}
template <bool thread_bound>
PA_ALWAYS_INLINE void
SchedulerLoopQuarantineBranch<thread_bound>::PurgeInternal(
size_t target_size_in_bytes,
[[maybe_unused]] bool for_destruction) {
if (g_no_purge.load(std::memory_order_relaxed)) {
return;
}
int64_t freed_count = 0;
int64_t freed_size_in_bytes = 0;
BatchFreeQueue queue(allocator_root_);
// Dequarantine some entries as required.
while (target_size_in_bytes < branch_size_in_bytes_) {
PA_DCHECK(!slots_.empty());
// As quarantined entries are shuffled, picking last entry is equivalent
// to picking random entry.
SlotStart slot_start = slots_.back().slot_start;
const size_t bucket_index = slots_.back().bucket_index;
size_t slot_size = 0;
#if PA_BUILDFLAG(HAS_MEMORY_TAGGING)
allocator_root_->RetagSlotIfNeeded(slot_start.Untag(), slot_size);
slot_start = slot_start.Untag().Tag();
#endif
if constexpr (!kThreadBound) {
// Assuming that ThreadCache is not available as this is not thread-bound.
// Going to `RawFree()` directly.
slot_size = BucketIndexLookup::GetBucketSize(bucket_index);
queue.Queue(slot_start.Untag());
} else {
// Unless during its destruction, we can assume ThreadCache is valid
// because this branch is embedded inside ThreadCache.
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
PA_DCHECK(being_destructed_ ||
ThreadCache::IsValid(allocator_root_->GetThreadCache()));
PA_DCHECK(being_destructed_ ||
allocator_root_->GetThreadCache() == tcache_);
#endif // PA_BUILDFLAG(DCHECKS_ARE_ON)
std::optional<size_t> slot_size_opt =
tcache_->MaybePutInCache(slot_start.Untag(), bucket_index);
if (slot_size_opt.has_value()) [[likely]] {
slot_size = slot_size_opt.value();
// This is a fast path, avoid calling GetSlotUsableSize() in Release
// builds as it is costlier. Copy its small bucket path instead.
const size_t usable_size =
allocator_root_->AdjustSizeForExtrasSubtract(slot_size);
#if PA_BUILDFLAG(DCHECKS_ARE_ON)
auto* slot_span = SlotSpanMetadata::FromSlotStart(slot_start.Untag(),
allocator_root_);
PA_DCHECK(!slot_span->CanStoreRawSize());
PA_DCHECK(usable_size == allocator_root_->GetSlotUsableSize(slot_span));
#endif
tcache_->RecordDeallocation(usable_size);
// Now ThreadCache is responsible for freeing the allocation.
} else {
// ThreadCache refused to take ownership of the allocation, hence we
// free it.
slot_size = BucketIndexLookup::GetBucketSize(bucket_index);
const size_t usable_size =
allocator_root_->AdjustSizeForExtrasSubtract(slot_size);
tcache_->RecordDeallocation(usable_size);
queue.Queue(slot_start.Untag());
}
}
++freed_count;
PA_DCHECK(slot_size > 0);
freed_size_in_bytes += slot_size;
branch_size_in_bytes_ -= slot_size;
slots_.pop_back();
}
root_->size_in_bytes_.fetch_sub(freed_size_in_bytes,
std::memory_order_relaxed);
root_->count_.fetch_sub(freed_count, std::memory_order_relaxed);
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::AllowScanlessPurge() {
PA_DCHECK(kThreadBound);
// Always thread-bound; no need to lock.
FakeScopedGuard guard(lock_);
PA_CHECK(disallow_scanless_purge_ > 0);
--disallow_scanless_purge_;
if (disallow_scanless_purge_ == 0) {
// Now scanless purge is allowed. Purging at this timing is more performance
// efficient.
PurgeInternal(0);
}
}
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::DisallowScanlessPurge() {
PA_DCHECK(kThreadBound);
// Always thread-bound; no need to lock.
FakeScopedGuard guard(lock_);
++disallow_scanless_purge_;
PA_CHECK(disallow_scanless_purge_ > 0); // Overflow check.
}
// static
template <bool thread_bound>
void SchedulerLoopQuarantineBranch<thread_bound>::DangerouslyDisablePurge() {
g_no_purge.store(true, std::memory_order_relaxed);
}
template <bool thread_bound>
const SchedulerLoopQuarantineConfig&
SchedulerLoopQuarantineBranch<thread_bound>::GetConfigurationForTesting() {
return config_for_testing_;
}
template class PA_EXPORT_TEMPLATE_DEFINE(PA_COMPONENT_EXPORT(PARTITION_ALLOC))
SchedulerLoopQuarantineBranch<false>;
template class PA_EXPORT_TEMPLATE_DEFINE(PA_COMPONENT_EXPORT(PARTITION_ALLOC))
SchedulerLoopQuarantineBranch<true>;
} // namespace partition_alloc::internal