| // 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 "base/allocator/partition_allocator/partition_root.h" |
| |
| #include <cstdint> |
| |
| #include "base/allocator/partition_allocator/freeslot_bitmap.h" |
| #include "base/allocator/partition_allocator/oom.h" |
| #include "base/allocator/partition_allocator/page_allocator.h" |
| #include "base/allocator/partition_allocator/partition_address_space.h" |
| #include "base/allocator/partition_allocator/partition_alloc_base/bits.h" |
| #include "base/allocator/partition_allocator/partition_alloc_base/compiler_specific.h" |
| #include "base/allocator/partition_allocator/partition_alloc_base/component_export.h" |
| #include "base/allocator/partition_allocator/partition_alloc_base/debug/debugging_buildflags.h" |
| #include "base/allocator/partition_allocator/partition_alloc_base/thread_annotations.h" |
| #include "base/allocator/partition_allocator/partition_alloc_buildflags.h" |
| #include "base/allocator/partition_allocator/partition_alloc_check.h" |
| #include "base/allocator/partition_allocator/partition_alloc_config.h" |
| #include "base/allocator/partition_allocator/partition_alloc_constants.h" |
| #include "base/allocator/partition_allocator/partition_bucket.h" |
| #include "base/allocator/partition_allocator/partition_cookie.h" |
| #include "base/allocator/partition_allocator/partition_oom.h" |
| #include "base/allocator/partition_allocator/partition_page.h" |
| #include "base/allocator/partition_allocator/partition_ref_count.h" |
| #include "base/allocator/partition_allocator/pkey.h" |
| #include "base/allocator/partition_allocator/reservation_offset_table.h" |
| #include "base/allocator/partition_allocator/tagging.h" |
| #include "build/build_config.h" |
| |
| #if PA_CONFIG(ENABLE_MAC11_MALLOC_SIZE_HACK) && BUILDFLAG(IS_APPLE) |
| #include "base/allocator/partition_allocator/partition_alloc_base/mac/mac_util.h" |
| #endif // PA_CONFIG(ENABLE_MAC11_MALLOC_SIZE_HACK) && BUILDFLAG(IS_APPLE) |
| |
| #if BUILDFLAG(USE_STARSCAN) |
| #include "base/allocator/partition_allocator/starscan/pcscan.h" |
| #endif |
| |
| #if !BUILDFLAG(HAS_64_BIT_POINTERS) |
| #include "base/allocator/partition_allocator/address_pool_manager_bitmap.h" |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| #include <windows.h> |
| #include "wow64apiset.h" |
| #endif |
| |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) |
| #include <pthread.h> |
| #endif |
| |
| namespace partition_alloc::internal { |
| |
| #if BUILDFLAG(RECORD_ALLOC_INFO) |
| // Even if this is not hidden behind a BUILDFLAG, it should not use any memory |
| // when recording is disabled, since it ends up in the .bss section. |
| AllocInfo g_allocs = {}; |
| |
| void RecordAllocOrFree(uintptr_t addr, size_t size) { |
| g_allocs.allocs[g_allocs.index.fetch_add(1, std::memory_order_relaxed) % |
| kAllocInfoSize] = {addr, size}; |
| } |
| #endif // BUILDFLAG(RECORD_ALLOC_INFO) |
| |
| #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| PtrPosWithinAlloc IsPtrWithinSameAlloc(uintptr_t orig_address, |
| uintptr_t test_address, |
| size_t type_size) { |
| // Required for pointers right past an allocation. See |
| // |PartitionAllocGetSlotStartInBRPPool()|. |
| uintptr_t adjusted_address = |
| orig_address - kPartitionPastAllocationAdjustment; |
| PA_DCHECK(IsManagedByNormalBucketsOrDirectMap(adjusted_address)); |
| DCheckIfManagedByPartitionAllocBRPPool(adjusted_address); |
| |
| uintptr_t slot_start = PartitionAllocGetSlotStartInBRPPool(adjusted_address); |
| // Don't use |adjusted_address| beyond this point at all. It was needed to |
| // pick the right slot, but now we're dealing with very concrete addresses. |
| // Zero it just in case, to catch errors. |
| adjusted_address = 0; |
| |
| auto* slot_span = SlotSpanMetadata<ThreadSafe>::FromSlotStart(slot_start); |
| auto* root = PartitionRoot<ThreadSafe>::FromSlotSpan(slot_span); |
| // Double check that ref-count is indeed present. |
| PA_DCHECK(root->brp_enabled()); |
| |
| uintptr_t object_addr = root->SlotStartToObjectAddr(slot_start); |
| uintptr_t object_end = object_addr + slot_span->GetUsableSize(root); |
| if (test_address < object_addr || object_end < test_address) { |
| return PtrPosWithinAlloc::kFarOOB; |
| #if BUILDFLAG(BACKUP_REF_PTR_POISON_OOB_PTR) |
| } else if (object_end - type_size < test_address) { |
| // Not even a single element of the type referenced by the pointer can fit |
| // between the pointer and the end of the object. |
| return PtrPosWithinAlloc::kAllocEnd; |
| #endif |
| } else { |
| return PtrPosWithinAlloc::kInBounds; |
| } |
| } |
| #endif // BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| } // namespace partition_alloc::internal |
| |
| namespace partition_alloc { |
| |
| #if PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| |
| namespace { |
| |
| internal::Lock g_root_enumerator_lock; |
| } |
| |
| template <bool thread_safe> |
| internal::Lock& PartitionRoot<thread_safe>::GetEnumeratorLock() { |
| return g_root_enumerator_lock; |
| } |
| |
| namespace internal { |
| |
| class PartitionRootEnumerator { |
| public: |
| using EnumerateCallback = void (*)(ThreadSafePartitionRoot* root, |
| bool in_child); |
| enum EnumerateOrder { |
| kNormal, |
| kReverse, |
| }; |
| |
| static PartitionRootEnumerator& Instance() { |
| static PartitionRootEnumerator instance; |
| return instance; |
| } |
| |
| void Enumerate(EnumerateCallback callback, |
| bool in_child, |
| EnumerateOrder order) PA_NO_THREAD_SAFETY_ANALYSIS { |
| if (order == kNormal) { |
| ThreadSafePartitionRoot* root; |
| for (root = Head(partition_roots_); root != nullptr; |
| root = root->next_root) { |
| callback(root, in_child); |
| } |
| } else { |
| PA_DCHECK(order == kReverse); |
| ThreadSafePartitionRoot* root; |
| for (root = Tail(partition_roots_); root != nullptr; |
| root = root->prev_root) { |
| callback(root, in_child); |
| } |
| } |
| } |
| |
| void Register(ThreadSafePartitionRoot* root) { |
| internal::ScopedGuard guard(ThreadSafePartitionRoot::GetEnumeratorLock()); |
| root->next_root = partition_roots_; |
| root->prev_root = nullptr; |
| if (partition_roots_) { |
| partition_roots_->prev_root = root; |
| } |
| partition_roots_ = root; |
| } |
| |
| void Unregister(ThreadSafePartitionRoot* root) { |
| internal::ScopedGuard guard(ThreadSafePartitionRoot::GetEnumeratorLock()); |
| ThreadSafePartitionRoot* prev = root->prev_root; |
| ThreadSafePartitionRoot* next = root->next_root; |
| if (prev) { |
| PA_DCHECK(prev->next_root == root); |
| prev->next_root = next; |
| } else { |
| PA_DCHECK(partition_roots_ == root); |
| partition_roots_ = next; |
| } |
| if (next) { |
| PA_DCHECK(next->prev_root == root); |
| next->prev_root = prev; |
| } |
| root->next_root = nullptr; |
| root->prev_root = nullptr; |
| } |
| |
| private: |
| constexpr PartitionRootEnumerator() = default; |
| |
| ThreadSafePartitionRoot* Head(ThreadSafePartitionRoot* roots) { |
| return roots; |
| } |
| |
| ThreadSafePartitionRoot* Tail(ThreadSafePartitionRoot* roots) |
| PA_NO_THREAD_SAFETY_ANALYSIS { |
| if (!roots) { |
| return nullptr; |
| } |
| ThreadSafePartitionRoot* node = roots; |
| for (; node->next_root != nullptr; node = node->next_root) |
| ; |
| return node; |
| } |
| |
| ThreadSafePartitionRoot* partition_roots_ |
| PA_GUARDED_BY(ThreadSafePartitionRoot::GetEnumeratorLock()) = nullptr; |
| }; |
| |
| } // namespace internal |
| |
| #endif // PA_USE_PARTITION_ROOT_ENUMERATOR |
| |
| #if BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| |
| namespace { |
| |
| #if PA_CONFIG(HAS_ATFORK_HANDLER) |
| |
| void LockRoot(PartitionRoot<internal::ThreadSafe>* root, |
| bool) PA_NO_THREAD_SAFETY_ANALYSIS { |
| PA_DCHECK(root); |
| root->lock_.Acquire(); |
| } |
| |
| // PA_NO_THREAD_SAFETY_ANALYSIS: acquires the lock and doesn't release it, by |
| // design. |
| void BeforeForkInParent() PA_NO_THREAD_SAFETY_ANALYSIS { |
| // ThreadSafePartitionRoot::GetLock() is private. So use |
| // g_root_enumerator_lock here. |
| g_root_enumerator_lock.Acquire(); |
| internal::PartitionRootEnumerator::Instance().Enumerate( |
| LockRoot, false, |
| internal::PartitionRootEnumerator::EnumerateOrder::kNormal); |
| |
| ThreadCacheRegistry::GetLock().Acquire(); |
| } |
| |
| template <typename T> |
| void UnlockOrReinit(T& lock, bool in_child) PA_NO_THREAD_SAFETY_ANALYSIS { |
| // Only re-init the locks in the child process, in the parent can unlock |
| // normally. |
| if (in_child) { |
| lock.Reinit(); |
| } else { |
| lock.Release(); |
| } |
| } |
| |
| void UnlockOrReinitRoot(PartitionRoot<internal::ThreadSafe>* root, |
| bool in_child) PA_NO_THREAD_SAFETY_ANALYSIS { |
| UnlockOrReinit(root->lock_, in_child); |
| } |
| |
| void ReleaseLocks(bool in_child) PA_NO_THREAD_SAFETY_ANALYSIS { |
| // In reverse order, even though there are no lock ordering dependencies. |
| UnlockOrReinit(ThreadCacheRegistry::GetLock(), in_child); |
| internal::PartitionRootEnumerator::Instance().Enumerate( |
| UnlockOrReinitRoot, in_child, |
| internal::PartitionRootEnumerator::EnumerateOrder::kReverse); |
| |
| // ThreadSafePartitionRoot::GetLock() is private. So use |
| // g_root_enumerator_lock here. |
| UnlockOrReinit(g_root_enumerator_lock, in_child); |
| } |
| |
| void AfterForkInParent() { |
| ReleaseLocks(/* in_child = */ false); |
| } |
| |
| void AfterForkInChild() { |
| ReleaseLocks(/* in_child = */ true); |
| // Unsafe, as noted in the name. This is fine here however, since at this |
| // point there is only one thread, this one (unless another post-fork() |
| // handler created a thread, but it would have needed to allocate, which would |
| // have deadlocked the process already). |
| // |
| // If we don't reclaim this memory, it is lost forever. Note that this is only |
| // really an issue if we fork() a multi-threaded process without calling |
| // exec() right away, which is discouraged. |
| ThreadCacheRegistry::Instance().ForcePurgeAllThreadAfterForkUnsafe(); |
| } |
| #endif // PA_CONFIG(HAS_ATFORK_HANDLER) |
| |
| std::atomic<bool> g_global_init_called; |
| void PartitionAllocMallocInitOnce() { |
| bool expected = false; |
| // No need to block execution for potential concurrent initialization, merely |
| // want to make sure this is only called once. |
| if (!g_global_init_called.compare_exchange_strong(expected, true)) { |
| return; |
| } |
| |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) |
| // When fork() is called, only the current thread continues to execute in the |
| // child process. If the lock is held, but *not* by this thread when fork() is |
| // called, we have a deadlock. |
| // |
| // The "solution" here is to acquire the lock on the forking thread before |
| // fork(), and keep it held until fork() is done, in the parent and the |
| // child. To clean up memory, we also must empty the thread caches in the |
| // child, which is easier, since no threads except for the current one are |
| // running right after the fork(). |
| // |
| // This is not perfect though, since: |
| // - Multiple pre/post-fork() handlers can be registered, they are then run in |
| // LIFO order for the pre-fork handler, and FIFO order for the post-fork |
| // one. So unless we are the first to register a handler, if another handler |
| // allocates, then we deterministically deadlock. |
| // - pthread handlers are *not* called when the application calls clone() |
| // directly, which is what Chrome does to launch processes. |
| // |
| // However, no perfect solution really exists to make threads + fork() |
| // cooperate, but deadlocks are real (and fork() is used in DEATH_TEST()s), |
| // and other malloc() implementations use the same techniques. |
| int err = |
| pthread_atfork(BeforeForkInParent, AfterForkInParent, AfterForkInChild); |
| PA_CHECK(err == 0); |
| #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| } // namespace |
| |
| #if BUILDFLAG(IS_APPLE) |
| void PartitionAllocMallocHookOnBeforeForkInParent() { |
| BeforeForkInParent(); |
| } |
| |
| void PartitionAllocMallocHookOnAfterForkInParent() { |
| AfterForkInParent(); |
| } |
| |
| void PartitionAllocMallocHookOnAfterForkInChild() { |
| AfterForkInChild(); |
| } |
| #endif // BUILDFLAG(IS_APPLE) |
| |
| #endif // BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| |
| namespace internal { |
| |
| namespace { |
| // 64 was chosen arbitrarily, as it seems like a reasonable trade-off between |
| // performance and purging opportunity. Higher value (i.e. smaller slots) |
| // wouldn't necessarily increase chances of purging, but would result in |
| // more work and larger |slot_usage| array. Lower value would probably decrease |
| // chances of purging. Not empirically tested. |
| constexpr size_t kMaxPurgeableSlotsPerSystemPage = 64; |
| PA_ALWAYS_INLINE PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR size_t |
| MinPurgeableSlotSize() { |
| return SystemPageSize() / kMaxPurgeableSlotsPerSystemPage; |
| } |
| } // namespace |
| |
| template <bool thread_safe> |
| static size_t PartitionPurgeSlotSpan( |
| internal::SlotSpanMetadata<thread_safe>* slot_span, |
| bool discard) { |
| auto* root = PartitionRoot<thread_safe>::FromSlotSpan(slot_span); |
| const internal::PartitionBucket<thread_safe>* bucket = slot_span->bucket; |
| size_t slot_size = bucket->slot_size; |
| |
| if (slot_size < MinPurgeableSlotSize() || !slot_span->num_allocated_slots) { |
| return 0; |
| } |
| |
| size_t bucket_num_slots = bucket->get_slots_per_span(); |
| size_t discardable_bytes = 0; |
| |
| if (slot_span->CanStoreRawSize()) { |
| uint32_t utilized_slot_size = static_cast<uint32_t>( |
| RoundUpToSystemPage(slot_span->GetUtilizedSlotSize())); |
| discardable_bytes = bucket->slot_size - utilized_slot_size; |
| if (discardable_bytes && discard) { |
| uintptr_t slot_span_start = |
| internal::SlotSpanMetadata<thread_safe>::ToSlotSpanStart(slot_span); |
| uintptr_t committed_data_end = slot_span_start + utilized_slot_size; |
| ScopedSyscallTimer timer{root}; |
| DiscardSystemPages(committed_data_end, discardable_bytes); |
| } |
| return discardable_bytes; |
| } |
| |
| #if defined(PAGE_ALLOCATOR_CONSTANTS_ARE_CONSTEXPR) |
| constexpr size_t kMaxSlotCount = |
| (PartitionPageSize() * kMaxPartitionPagesPerRegularSlotSpan) / |
| MinPurgeableSlotSize(); |
| #elif BUILDFLAG(IS_APPLE) || (BUILDFLAG(IS_LINUX) && defined(ARCH_CPU_ARM64)) |
| // It's better for slot_usage to be stack-allocated and fixed-size, which |
| // demands that its size be constexpr. On IS_APPLE and Linux on arm64, |
| // PartitionPageSize() is always SystemPageSize() << 2, so regardless of |
| // what the run time page size is, kMaxSlotCount can always be simplified |
| // to this expression. |
| constexpr size_t kMaxSlotCount = |
| 4 * kMaxPurgeableSlotsPerSystemPage * |
| internal::kMaxPartitionPagesPerRegularSlotSpan; |
| PA_CHECK(kMaxSlotCount == (PartitionPageSize() * |
| internal::kMaxPartitionPagesPerRegularSlotSpan) / |
| MinPurgeableSlotSize()); |
| #endif |
| PA_DCHECK(bucket_num_slots <= kMaxSlotCount); |
| PA_DCHECK(slot_span->num_unprovisioned_slots < bucket_num_slots); |
| size_t num_slots = bucket_num_slots - slot_span->num_unprovisioned_slots; |
| char slot_usage[kMaxSlotCount]; |
| #if !BUILDFLAG(IS_WIN) |
| // The last freelist entry should not be discarded when using OS_WIN. |
| // DiscardVirtualMemory makes the contents of discarded memory undefined. |
| size_t last_slot = static_cast<size_t>(-1); |
| #endif |
| memset(slot_usage, 1, num_slots); |
| uintptr_t slot_span_start = |
| SlotSpanMetadata<thread_safe>::ToSlotSpanStart(slot_span); |
| // First, walk the freelist for this slot span and make a bitmap of which |
| // slots are not in use. |
| for (PartitionFreelistEntry* entry = slot_span->get_freelist_head(); entry; |
| /**/) { |
| size_t slot_number = |
| bucket->GetSlotNumber(SlotStartPtr2Addr(entry) - slot_span_start); |
| PA_DCHECK(slot_number < num_slots); |
| slot_usage[slot_number] = 0; |
| #if !BUILDFLAG(IS_WIN) |
| // If we have a slot where the encoded next pointer is 0, we can actually |
| // discard that entry because touching a discarded page is guaranteed to |
| // return the original content or 0. (Note that this optimization won't be |
| // effective on big-endian machines because the masking function is |
| // negation.) |
| if (entry->IsEncodedNextPtrZero()) { |
| last_slot = slot_number; |
| } |
| #endif |
| entry = entry->GetNext(slot_size); |
| } |
| |
| // If the slot(s) at the end of the slot span are not in used, we can truncate |
| // them entirely and rewrite the freelist. |
| size_t truncated_slots = 0; |
| while (!slot_usage[num_slots - 1]) { |
| truncated_slots++; |
| num_slots--; |
| PA_DCHECK(num_slots); |
| } |
| // First, do the work of calculating the discardable bytes. Don't actually |
| // discard anything unless the discard flag was passed in. |
| if (truncated_slots) { |
| size_t unprovisioned_bytes = 0; |
| uintptr_t begin_addr = slot_span_start + (num_slots * slot_size); |
| uintptr_t end_addr = begin_addr + (slot_size * truncated_slots); |
| |
| // The slots that do not contain discarded pages should not be included to |
| // |truncated_slots|. Detects those slots and fixes |truncated_slots| and |
| // |num_slots| accordingly. |
| uintptr_t rounded_up_truncatation_begin_addr = |
| RoundUpToSystemPage(begin_addr); |
| while (begin_addr + slot_size <= rounded_up_truncatation_begin_addr) { |
| begin_addr += slot_size; |
| PA_DCHECK(truncated_slots); |
| --truncated_slots; |
| ++num_slots; |
| } |
| begin_addr = rounded_up_truncatation_begin_addr; |
| |
| // We round the end address here up and not down because we're at the end of |
| // a slot span, so we "own" all the way up the page boundary. |
| end_addr = RoundUpToSystemPage(end_addr); |
| PA_DCHECK(end_addr <= slot_span_start + bucket->get_bytes_per_span()); |
| if (begin_addr < end_addr) { |
| unprovisioned_bytes = end_addr - begin_addr; |
| discardable_bytes += unprovisioned_bytes; |
| } |
| if (unprovisioned_bytes && discard) { |
| PA_DCHECK(truncated_slots > 0); |
| size_t new_unprovisioned_slots = |
| truncated_slots + slot_span->num_unprovisioned_slots; |
| PA_DCHECK(new_unprovisioned_slots <= bucket->get_slots_per_span()); |
| slot_span->num_unprovisioned_slots = new_unprovisioned_slots; |
| |
| // Rewrite the freelist. |
| internal::PartitionFreelistEntry* head = nullptr; |
| internal::PartitionFreelistEntry* back = head; |
| size_t num_new_entries = 0; |
| for (size_t slot_index = 0; slot_index < num_slots; ++slot_index) { |
| if (slot_usage[slot_index]) { |
| continue; |
| } |
| |
| auto* entry = PartitionFreelistEntry::EmplaceAndInitNull( |
| slot_span_start + (slot_size * slot_index)); |
| if (!head) { |
| head = entry; |
| back = entry; |
| } else { |
| back->SetNext(entry); |
| back = entry; |
| } |
| num_new_entries++; |
| #if !BUILDFLAG(IS_WIN) |
| last_slot = slot_index; |
| #endif |
| } |
| |
| slot_span->SetFreelistHead(head); |
| |
| PA_DCHECK(num_new_entries == num_slots - slot_span->num_allocated_slots); |
| |
| #if BUILDFLAG(USE_FREESLOT_BITMAP) |
| FreeSlotBitmapReset(slot_span_start + (slot_size * num_slots), end_addr, |
| slot_size); |
| #endif |
| |
| // Discard the memory. |
| ScopedSyscallTimer timer{root}; |
| DiscardSystemPages(begin_addr, unprovisioned_bytes); |
| } |
| } |
| |
| if (slot_size < SystemPageSize()) { |
| // Returns here because implementing the following steps for smaller slot |
| // size will need a complicated logic and make the code messy. |
| return discardable_bytes; |
| } |
| |
| // Next, walk the slots and for any not in use, consider which system pages |
| // are no longer needed. We can release any system pages back to the system as |
| // long as we don't interfere with a freelist pointer or an adjacent used |
| // slot. |
| for (size_t i = 0; i < num_slots; ++i) { |
| if (slot_usage[i]) { |
| continue; |
| } |
| |
| // The first address we can safely discard is just after the freelist |
| // pointer. There's one quirk: if the freelist pointer is actually nullptr, |
| // we can discard that pointer value too. |
| uintptr_t begin_addr = slot_span_start + (i * slot_size); |
| uintptr_t end_addr = begin_addr + slot_size; |
| |
| bool can_discard_free_list_pointer = false; |
| #if !BUILDFLAG(IS_WIN) |
| if (i != last_slot) { |
| begin_addr += sizeof(internal::PartitionFreelistEntry); |
| } else { |
| can_discard_free_list_pointer = true; |
| } |
| #else |
| begin_addr += sizeof(internal::PartitionFreelistEntry); |
| #endif |
| |
| uintptr_t rounded_up_begin_addr = RoundUpToSystemPage(begin_addr); |
| uintptr_t rounded_down_begin_addr = RoundDownToSystemPage(begin_addr); |
| end_addr = RoundDownToSystemPage(end_addr); |
| |
| // |rounded_up_begin_addr| could be greater than |end_addr| only if slot |
| // size was less than system page size, or if free list pointer crossed the |
| // page boundary. Neither is possible here. |
| PA_DCHECK(rounded_up_begin_addr <= end_addr); |
| |
| if (rounded_down_begin_addr < rounded_up_begin_addr && i != 0 && |
| !slot_usage[i - 1] && can_discard_free_list_pointer) { |
| // This slot contains a partial page in the beginning. The rest of that |
| // page is contained in the slot[i-1], which is also discardable. |
| // Therefore we can discard this page. |
| begin_addr = rounded_down_begin_addr; |
| } else { |
| begin_addr = rounded_up_begin_addr; |
| } |
| |
| if (begin_addr < end_addr) { |
| size_t partial_slot_bytes = end_addr - begin_addr; |
| discardable_bytes += partial_slot_bytes; |
| if (discard) { |
| ScopedSyscallTimer timer{root}; |
| DiscardSystemPages(begin_addr, partial_slot_bytes); |
| } |
| } |
| } |
| |
| return discardable_bytes; |
| } |
| |
| template <bool thread_safe> |
| static void PartitionPurgeBucket( |
| internal::PartitionBucket<thread_safe>* bucket) { |
| if (bucket->active_slot_spans_head != |
| internal::SlotSpanMetadata<thread_safe>::get_sentinel_slot_span()) { |
| for (internal::SlotSpanMetadata<thread_safe>* slot_span = |
| bucket->active_slot_spans_head; |
| slot_span; slot_span = slot_span->next_slot_span) { |
| PA_DCHECK( |
| slot_span != |
| internal::SlotSpanMetadata<thread_safe>::get_sentinel_slot_span()); |
| PartitionPurgeSlotSpan(slot_span, true); |
| } |
| } |
| } |
| |
| template <bool thread_safe> |
| static void PartitionDumpSlotSpanStats( |
| PartitionBucketMemoryStats* stats_out, |
| internal::SlotSpanMetadata<thread_safe>* slot_span) { |
| uint16_t bucket_num_slots = slot_span->bucket->get_slots_per_span(); |
| |
| if (slot_span->is_decommitted()) { |
| ++stats_out->num_decommitted_slot_spans; |
| return; |
| } |
| |
| stats_out->discardable_bytes += PartitionPurgeSlotSpan(slot_span, false); |
| |
| if (slot_span->CanStoreRawSize()) { |
| stats_out->active_bytes += static_cast<uint32_t>(slot_span->GetRawSize()); |
| } else { |
| stats_out->active_bytes += |
| (slot_span->num_allocated_slots * stats_out->bucket_slot_size); |
| } |
| stats_out->active_count += slot_span->num_allocated_slots; |
| |
| size_t slot_span_bytes_resident = RoundUpToSystemPage( |
| (bucket_num_slots - slot_span->num_unprovisioned_slots) * |
| stats_out->bucket_slot_size); |
| stats_out->resident_bytes += slot_span_bytes_resident; |
| if (slot_span->is_empty()) { |
| stats_out->decommittable_bytes += slot_span_bytes_resident; |
| ++stats_out->num_empty_slot_spans; |
| } else if (slot_span->is_full()) { |
| ++stats_out->num_full_slot_spans; |
| } else { |
| PA_DCHECK(slot_span->is_active()); |
| ++stats_out->num_active_slot_spans; |
| } |
| } |
| |
| template <bool thread_safe> |
| static void PartitionDumpBucketStats( |
| PartitionBucketMemoryStats* stats_out, |
| const internal::PartitionBucket<thread_safe>* bucket) { |
| PA_DCHECK(!bucket->is_direct_mapped()); |
| stats_out->is_valid = false; |
| // If the active slot span list is empty (== |
| // internal::SlotSpanMetadata::get_sentinel_slot_span()), the bucket might |
| // still need to be reported if it has a list of empty, decommitted or full |
| // slot spans. |
| if (bucket->active_slot_spans_head == |
| internal::SlotSpanMetadata<thread_safe>::get_sentinel_slot_span() && |
| !bucket->empty_slot_spans_head && !bucket->decommitted_slot_spans_head && |
| !bucket->num_full_slot_spans) { |
| return; |
| } |
| |
| memset(stats_out, '\0', sizeof(*stats_out)); |
| stats_out->is_valid = true; |
| stats_out->is_direct_map = false; |
| stats_out->num_full_slot_spans = |
| static_cast<size_t>(bucket->num_full_slot_spans); |
| stats_out->bucket_slot_size = bucket->slot_size; |
| uint16_t bucket_num_slots = bucket->get_slots_per_span(); |
| size_t bucket_useful_storage = stats_out->bucket_slot_size * bucket_num_slots; |
| stats_out->allocated_slot_span_size = bucket->get_bytes_per_span(); |
| stats_out->active_bytes = bucket->num_full_slot_spans * bucket_useful_storage; |
| stats_out->active_count = bucket->num_full_slot_spans * bucket_num_slots; |
| stats_out->resident_bytes = |
| bucket->num_full_slot_spans * stats_out->allocated_slot_span_size; |
| |
| for (internal::SlotSpanMetadata<thread_safe>* slot_span = |
| bucket->empty_slot_spans_head; |
| slot_span; slot_span = slot_span->next_slot_span) { |
| PA_DCHECK(slot_span->is_empty() || slot_span->is_decommitted()); |
| PartitionDumpSlotSpanStats(stats_out, slot_span); |
| } |
| for (internal::SlotSpanMetadata<thread_safe>* slot_span = |
| bucket->decommitted_slot_spans_head; |
| slot_span; slot_span = slot_span->next_slot_span) { |
| PA_DCHECK(slot_span->is_decommitted()); |
| PartitionDumpSlotSpanStats(stats_out, slot_span); |
| } |
| |
| if (bucket->active_slot_spans_head != |
| internal::SlotSpanMetadata<thread_safe>::get_sentinel_slot_span()) { |
| for (internal::SlotSpanMetadata<thread_safe>* slot_span = |
| bucket->active_slot_spans_head; |
| slot_span; slot_span = slot_span->next_slot_span) { |
| PA_DCHECK( |
| slot_span != |
| internal::SlotSpanMetadata<thread_safe>::get_sentinel_slot_span()); |
| PartitionDumpSlotSpanStats(stats_out, slot_span); |
| } |
| } |
| } |
| |
| #if BUILDFLAG(PA_DCHECK_IS_ON) |
| void DCheckIfManagedByPartitionAllocBRPPool(uintptr_t address) { |
| PA_DCHECK(IsManagedByPartitionAllocBRPPool(address)); |
| } |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PKEYS) |
| void PartitionAllocPkeyInit(int pkey) { |
| PkeySettings::settings.enabled = true; |
| PartitionAddressSpace::InitPkeyPool(pkey); |
| // Call TagGlobalsWithPkey last since we might not have write permissions to |
| // to memory tagged with `pkey` at this point. |
| TagGlobalsWithPkey(pkey); |
| } |
| #endif // BUILDFLAG(ENABLE_PKEYS) |
| |
| } // namespace internal |
| |
| template <bool thread_safe> |
| [[noreturn]] PA_NOINLINE void PartitionRoot<thread_safe>::OutOfMemory( |
| size_t size) { |
| const size_t virtual_address_space_size = |
| total_size_of_super_pages.load(std::memory_order_relaxed) + |
| total_size_of_direct_mapped_pages.load(std::memory_order_relaxed); |
| #if !defined(ARCH_CPU_64_BITS) |
| const size_t uncommitted_size = |
| virtual_address_space_size - |
| total_size_of_committed_pages.load(std::memory_order_relaxed); |
| |
| // Check whether this OOM is due to a lot of super pages that are allocated |
| // but not committed, probably due to http://crbug.com/421387. |
| if (uncommitted_size > internal::kReasonableSizeOfUnusedPages) { |
| internal::PartitionOutOfMemoryWithLotsOfUncommitedPages(size); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| // If true then we are running on 64-bit Windows. |
| BOOL is_wow_64 = FALSE; |
| // Intentionally ignoring failures. |
| IsWow64Process(GetCurrentProcess(), &is_wow_64); |
| // 32-bit address space on Windows is typically either 2 GiB (on 32-bit |
| // Windows) or 4 GiB (on 64-bit Windows). 2.8 and 1.0 GiB are just rough |
| // guesses as to how much address space PA can consume (note that code, |
| // stacks, and other allocators will also consume address space). |
| const size_t kReasonableVirtualSize = (is_wow_64 ? 2800 : 1024) * 1024 * 1024; |
| // Make it obvious whether we are running on 64-bit Windows. |
| PA_DEBUG_DATA_ON_STACK("is_wow_64", static_cast<size_t>(is_wow_64)); |
| #else |
| constexpr size_t kReasonableVirtualSize = |
| // 1.5GiB elsewhere, since address space is typically 3GiB. |
| (1024 + 512) * 1024 * 1024; |
| #endif |
| if (virtual_address_space_size > kReasonableVirtualSize) { |
| internal::PartitionOutOfMemoryWithLargeVirtualSize( |
| virtual_address_space_size); |
| } |
| #endif // #if !defined(ARCH_CPU_64_BITS) |
| |
| // Out of memory can be due to multiple causes, such as: |
| // - Out of virtual address space in the desired pool |
| // - Out of commit due to either our process, or another one |
| // - Excessive allocations in the current process |
| // |
| // Saving these values make it easier to distinguish between these. See the |
| // documentation in PA_CONFIG(DEBUG_DATA_ON_STACK) on how to get these from |
| // minidumps. |
| PA_DEBUG_DATA_ON_STACK("va_size", virtual_address_space_size); |
| PA_DEBUG_DATA_ON_STACK("alloc", get_total_size_of_allocated_bytes()); |
| PA_DEBUG_DATA_ON_STACK("commit", get_total_size_of_committed_pages()); |
| PA_DEBUG_DATA_ON_STACK("size", size); |
| |
| if (internal::g_oom_handling_function) { |
| (*internal::g_oom_handling_function)(size); |
| } |
| OOM_CRASH(size); |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::DecommitEmptySlotSpans() { |
| ShrinkEmptySlotSpansRing(0); |
| // Just decommitted everything, and holding the lock, should be exactly 0. |
| PA_DCHECK(empty_slot_spans_dirty_bytes == 0); |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::DestructForTesting() { |
| // We need to destruct the thread cache before we unreserve any of the super |
| // pages below, which we currently are not doing. So, we should only call |
| // this function on PartitionRoots without a thread cache. |
| PA_CHECK(!flags.with_thread_cache); |
| auto pool_handle = ChoosePool(); |
| #if BUILDFLAG(ENABLE_PKEYS) |
| // The pages managed by pkey will be free-ed at UninitPKeyForTesting(). |
| // Don't invoke FreePages() for the pages. |
| if (pool_handle == internal::kPkeyPoolHandle) { |
| return; |
| } |
| PA_DCHECK(pool_handle < internal::kNumPools); |
| #else |
| PA_DCHECK(pool_handle <= internal::kNumPools); |
| #endif |
| |
| auto* curr = first_extent; |
| while (curr != nullptr) { |
| auto* next = curr->next; |
| uintptr_t address = SuperPagesBeginFromExtent(curr); |
| size_t size = |
| internal::kSuperPageSize * curr->number_of_consecutive_super_pages; |
| #if !BUILDFLAG(HAS_64_BIT_POINTERS) |
| internal::AddressPoolManager::GetInstance().MarkUnused(pool_handle, address, |
| size); |
| #endif |
| internal::AddressPoolManager::GetInstance().UnreserveAndDecommit( |
| pool_handle, address, size); |
| curr = next; |
| } |
| } |
| |
| #if PA_CONFIG(ENABLE_MAC11_MALLOC_SIZE_HACK) |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::EnableMac11MallocSizeHackForTesting() { |
| flags.mac11_malloc_size_hack_enabled_ = true; |
| } |
| #endif // PA_CONFIG(ENABLE_MAC11_MALLOC_SIZE_HACK) |
| |
| #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) && !BUILDFLAG(HAS_64_BIT_POINTERS) |
| namespace { |
| std::atomic<bool> g_reserve_brp_guard_region_called; |
| // An address constructed by repeating `kQuarantinedByte` shouldn't never point |
| // to valid memory. Preemptively reserve a memory region around that address and |
| // make it inaccessible. Not needed for 64-bit platforms where the address is |
| // guaranteed to be non-canonical. Safe to call multiple times. |
| void ReserveBackupRefPtrGuardRegionIfNeeded() { |
| bool expected = false; |
| // No need to block execution for potential concurrent initialization, merely |
| // want to make sure this is only called once. |
| if (!g_reserve_brp_guard_region_called.compare_exchange_strong(expected, |
| true)) { |
| return; |
| } |
| |
| size_t alignment = internal::PageAllocationGranularity(); |
| uintptr_t requested_address; |
| memset(&requested_address, internal::kQuarantinedByte, |
| sizeof(requested_address)); |
| requested_address = RoundDownToPageAllocationGranularity(requested_address); |
| |
| // Request several pages so that even unreasonably large C++ objects stay |
| // within the inaccessible region. If some of the pages can't be reserved, |
| // it's still preferable to try and reserve the rest. |
| for (size_t i = 0; i < 4; ++i) { |
| [[maybe_unused]] uintptr_t allocated_address = |
| AllocPages(requested_address, alignment, alignment, |
| PageAccessibilityConfiguration( |
| PageAccessibilityConfiguration::kInaccessible), |
| PageTag::kPartitionAlloc); |
| requested_address += alignment; |
| } |
| } |
| } // namespace |
| #endif // BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) && |
| // !BUILDFLAG(HAS_64_BIT_POINTERS) |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::Init(PartitionOptions opts) { |
| { |
| #if BUILDFLAG(IS_APPLE) |
| // Needed to statically bound page size, which is a runtime constant on |
| // apple OSes. |
| PA_CHECK((internal::SystemPageSize() == (size_t{1} << 12)) || |
| (internal::SystemPageSize() == (size_t{1} << 14))); |
| #elif BUILDFLAG(IS_LINUX) && defined(ARCH_CPU_ARM64) |
| // Check runtime pagesize. Though the code is currently the same, it is |
| // not merged with the IS_APPLE case above as a 1 << 16 case needs to be |
| // added here in the future, to allow 64 kiB pagesize. That is only |
| // supported on Linux on arm64, not on IS_APPLE, but not yet present here |
| // as the rest of the partition allocator does not currently support it. |
| PA_CHECK((internal::SystemPageSize() == (size_t{1} << 12)) || |
| (internal::SystemPageSize() == (size_t{1} << 14))); |
| #endif |
| |
| ::partition_alloc::internal::ScopedGuard guard{lock_}; |
| if (initialized) { |
| return; |
| } |
| |
| // Swaps out the active no-op tagging intrinsics with MTE-capable ones, if |
| // running on the right hardware. |
| ::partition_alloc::internal::InitializeMTESupportIfNeeded(); |
| |
| #if BUILDFLAG(HAS_64_BIT_POINTERS) |
| // Reserve address space for partition alloc. |
| internal::PartitionAddressSpace::Init(); |
| #endif |
| |
| #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) && !BUILDFLAG(HAS_64_BIT_POINTERS) |
| ReserveBackupRefPtrGuardRegionIfNeeded(); |
| #endif |
| |
| flags.allow_aligned_alloc = |
| opts.aligned_alloc == PartitionOptions::AlignedAlloc::kAllowed; |
| flags.allow_cookie = opts.cookie == PartitionOptions::Cookie::kAllowed; |
| #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| flags.brp_enabled_ = |
| opts.backup_ref_ptr == PartitionOptions::BackupRefPtr::kEnabled; |
| flags.brp_zapping_enabled_ = |
| opts.backup_ref_ptr_zapping == |
| PartitionOptions::BackupRefPtrZapping::kEnabled; |
| PA_CHECK(!flags.brp_zapping_enabled_ || flags.brp_enabled_); |
| #if PA_CONFIG(ENABLE_MAC11_MALLOC_SIZE_HACK) && BUILDFLAG(IS_APPLE) |
| flags.mac11_malloc_size_hack_enabled_ = |
| flags.brp_enabled_ && internal::base::mac::IsOS11(); |
| #endif // PA_CONFIG(ENABLE_MAC11_MALLOC_SIZE_HACK) && BUILDFLAG(IS_APPLE) |
| #else // BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| PA_CHECK(opts.backup_ref_ptr == PartitionOptions::BackupRefPtr::kDisabled); |
| #endif // BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| flags.use_configurable_pool = |
| (opts.use_configurable_pool == |
| PartitionOptions::UseConfigurablePool::kIfAvailable) && |
| IsConfigurablePoolAvailable(); |
| PA_DCHECK(!flags.use_configurable_pool || IsConfigurablePoolAvailable()); |
| |
| // brp_enabled() is not supported in the configurable pool because |
| // BRP requires objects to be in a different Pool. |
| PA_CHECK(!(flags.use_configurable_pool && brp_enabled())); |
| |
| #if BUILDFLAG(ENABLE_PKEYS) |
| // BRP and pkey mode use different pools, so they can't be enabled at the |
| // same time. |
| PA_CHECK(opts.pkey == internal::kDefaultPkey || |
| opts.backup_ref_ptr == PartitionOptions::BackupRefPtr::kDisabled); |
| flags.pkey = opts.pkey; |
| #endif |
| |
| // Ref-count messes up alignment needed for AlignedAlloc, making this |
| // option incompatible. However, except in the |
| // PUT_REF_COUNT_IN_PREVIOUS_SLOT case. |
| #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) && \ |
| !BUILDFLAG(PUT_REF_COUNT_IN_PREVIOUS_SLOT) |
| PA_CHECK(!flags.allow_aligned_alloc || !flags.brp_enabled_); |
| #endif |
| |
| #if PA_CONFIG(EXTRAS_REQUIRED) |
| flags.extras_size = 0; |
| flags.extras_offset = 0; |
| |
| if (flags.allow_cookie) { |
| flags.extras_size += internal::kPartitionCookieSizeAdjustment; |
| } |
| |
| if (brp_enabled()) { |
| // TODO(tasak): In the PUT_REF_COUNT_IN_PREVIOUS_SLOT case, ref-count is |
| // stored out-of-line for single-slot slot spans, so no need to |
| // add/subtract its size in this case. |
| flags.extras_size += internal::kPartitionRefCountSizeAdjustment; |
| flags.extras_offset += internal::kPartitionRefCountOffsetAdjustment; |
| } |
| if (opts.add_dummy_ref_count == |
| PartitionOptions::AddDummyRefCount::kEnabled) { |
| // AddDummyRefCount will increase the size to simulate adding |
| // PartitionRefCount, but non of the BRP logic will run. |
| PA_CHECK(!brp_enabled()); |
| flags.extras_size += internal::kPartitionRefCountSizeAdjustment; |
| } |
| #endif // PA_CONFIG(EXTRAS_REQUIRED) |
| |
| // Re-confirm the above PA_CHECKs, by making sure there are no |
| // pre-allocation extras when AlignedAlloc is allowed. Post-allocation |
| // extras are ok. |
| PA_CHECK(!flags.allow_aligned_alloc || !flags.extras_offset); |
| |
| flags.quarantine_mode = |
| #if BUILDFLAG(USE_STARSCAN) |
| (opts.quarantine == PartitionOptions::Quarantine::kDisallowed |
| ? QuarantineMode::kAlwaysDisabled |
| : QuarantineMode::kDisabledByDefault); |
| #else |
| QuarantineMode::kAlwaysDisabled; |
| #endif // BUILDFLAG(USE_STARSCAN) |
| |
| // We mark the sentinel slot span as free to make sure it is skipped by our |
| // logic to find a new active slot span. |
| memset(&sentinel_bucket, 0, sizeof(sentinel_bucket)); |
| sentinel_bucket.active_slot_spans_head = |
| SlotSpan::get_sentinel_slot_span_non_const(); |
| |
| // This is a "magic" value so we can test if a root pointer is valid. |
| inverted_self = ~reinterpret_cast<uintptr_t>(this); |
| |
| // Set up the actual usable buckets first. |
| constexpr internal::BucketIndexLookup lookup{}; |
| size_t bucket_index = 0; |
| while (lookup.bucket_sizes()[bucket_index] != |
| internal::kInvalidBucketSize) { |
| buckets[bucket_index].Init(lookup.bucket_sizes()[bucket_index]); |
| bucket_index++; |
| } |
| PA_DCHECK(bucket_index < internal::kNumBuckets); |
| |
| // Remaining buckets are not usable, and not real. |
| for (size_t index = bucket_index; index < internal::kNumBuckets; index++) { |
| // Cannot init with size 0 since it computes 1 / size, but make sure the |
| // bucket is invalid. |
| buckets[index].Init(internal::kInvalidBucketSize); |
| buckets[index].active_slot_spans_head = nullptr; |
| PA_DCHECK(!buckets[index].is_valid()); |
| } |
| |
| #if !PA_CONFIG(THREAD_CACHE_SUPPORTED) |
| // TLS in ThreadCache not supported on other OSes. |
| flags.with_thread_cache = false; |
| #else |
| ThreadCache::EnsureThreadSpecificDataInitialized(); |
| flags.with_thread_cache = |
| (opts.thread_cache == PartitionOptions::ThreadCache::kEnabled); |
| |
| if (flags.with_thread_cache) { |
| ThreadCache::Init(this); |
| } |
| #endif // !PA_CONFIG(THREAD_CACHE_SUPPORTED) |
| |
| #if PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| internal::PartitionRootEnumerator::Instance().Register(this); |
| #endif |
| |
| initialized = true; |
| } |
| |
| // Called without the lock, might allocate. |
| #if BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| PartitionAllocMallocInitOnce(); |
| #endif |
| |
| #if BUILDFLAG(ENABLE_PKEYS) |
| if (flags.pkey != internal::kDefaultPkey) { |
| internal::PartitionAllocPkeyInit(flags.pkey); |
| } |
| #endif |
| } |
| |
| template <bool thread_safe> |
| PartitionRoot<thread_safe>::~PartitionRoot() { |
| #if BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| PA_CHECK(!flags.with_thread_cache) |
| << "Must not destroy a partition with a thread cache"; |
| #endif // BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| |
| #if PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| if (initialized) { |
| internal::PartitionRootEnumerator::Instance().Unregister(this); |
| } |
| #endif // PA_CONFIG(USE_PARTITION_ALLOC_ENUMERATOR) |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::EnableThreadCacheIfSupported() { |
| #if PA_CONFIG(THREAD_CACHE_SUPPORTED) |
| ::partition_alloc::internal::ScopedGuard guard{lock_}; |
| PA_CHECK(!flags.with_thread_cache); |
| // By the time we get there, there may be multiple threads created in the |
| // process. Since `with_thread_cache` is accessed without a lock, it can |
| // become visible to another thread before the effects of |
| // `internal::ThreadCacheInit()` are visible. To prevent that, we fake thread |
| // cache creation being in-progress while this is running. |
| // |
| // This synchronizes with the acquire load in `MaybeInitThreadCacheAndAlloc()` |
| // to ensure that we don't create (and thus use) a ThreadCache before |
| // ThreadCache::Init()'s effects are visible. |
| int before = |
| thread_caches_being_constructed_.fetch_add(1, std::memory_order_acquire); |
| PA_CHECK(before == 0); |
| ThreadCache::Init(this); |
| thread_caches_being_constructed_.fetch_sub(1, std::memory_order_release); |
| flags.with_thread_cache = true; |
| #endif // PA_CONFIG(THREAD_CACHE_SUPPORTED) |
| } |
| |
| template <bool thread_safe> |
| bool PartitionRoot<thread_safe>::TryReallocInPlaceForDirectMap( |
| internal::SlotSpanMetadata<thread_safe>* slot_span, |
| size_t requested_size) { |
| PA_DCHECK(slot_span->bucket->is_direct_mapped()); |
| // Slot-span metadata isn't MTE-tagged. |
| PA_DCHECK( |
| internal::IsManagedByDirectMap(reinterpret_cast<uintptr_t>(slot_span))); |
| |
| size_t raw_size = AdjustSizeForExtrasAdd(requested_size); |
| auto* extent = DirectMapExtent::FromSlotSpan(slot_span); |
| size_t current_reservation_size = extent->reservation_size; |
| // Calculate the new reservation size the way PartitionDirectMap() would, but |
| // skip the alignment, because this call isn't requesting it. |
| size_t new_reservation_size = GetDirectMapReservationSize(raw_size); |
| |
| // If new reservation would be larger, there is nothing we can do to |
| // reallocate in-place. |
| if (new_reservation_size > current_reservation_size) { |
| return false; |
| } |
| |
| // Don't reallocate in-place if new reservation size would be less than 80 % |
| // of the current one, to avoid holding on to too much unused address space. |
| // Make this check before comparing slot sizes, as even with equal or similar |
| // slot sizes we can save a lot if the original allocation was heavily padded |
| // for alignment. |
| if ((new_reservation_size >> internal::SystemPageShift()) * 5 < |
| (current_reservation_size >> internal::SystemPageShift()) * 4) { |
| return false; |
| } |
| |
| // Note that the new size isn't a bucketed size; this function is called |
| // whenever we're reallocating a direct mapped allocation, so calculate it |
| // the way PartitionDirectMap() would. |
| size_t new_slot_size = GetDirectMapSlotSize(raw_size); |
| if (new_slot_size < internal::kMinDirectMappedDownsize) { |
| return false; |
| } |
| |
| // Past this point, we decided we'll attempt to reallocate without relocating, |
| // so we have to honor the padding for alignment in front of the original |
| // allocation, even though this function isn't requesting any alignment. |
| |
| // bucket->slot_size is the currently committed size of the allocation. |
| size_t current_slot_size = slot_span->bucket->slot_size; |
| size_t current_usable_size = slot_span->GetUsableSize(this); |
| uintptr_t slot_start = SlotSpan::ToSlotSpanStart(slot_span); |
| // This is the available part of the reservation up to which the new |
| // allocation can grow. |
| size_t available_reservation_size = |
| current_reservation_size - extent->padding_for_alignment - |
| PartitionRoot<thread_safe>::GetDirectMapMetadataAndGuardPagesSize(); |
| #if BUILDFLAG(PA_DCHECK_IS_ON) |
| uintptr_t reservation_start = slot_start & internal::kSuperPageBaseMask; |
| PA_DCHECK(internal::IsReservationStart(reservation_start)); |
| PA_DCHECK(slot_start + available_reservation_size == |
| reservation_start + current_reservation_size - |
| GetDirectMapMetadataAndGuardPagesSize() + |
| internal::PartitionPageSize()); |
| #endif |
| |
| if (new_slot_size == current_slot_size) { |
| // No need to move any memory around, but update size and cookie below. |
| // That's because raw_size may have changed. |
| } else if (new_slot_size < current_slot_size) { |
| // Shrink by decommitting unneeded pages and making them inaccessible. |
| size_t decommit_size = current_slot_size - new_slot_size; |
| DecommitSystemPagesForData(slot_start + new_slot_size, decommit_size, |
| PageAccessibilityDisposition::kRequireUpdate); |
| // Since the decommited system pages are still reserved, we don't need to |
| // change the entries for decommitted pages in the reservation offset table. |
| } else if (new_slot_size <= available_reservation_size) { |
| // Grow within the actually reserved address space. Just need to make the |
| // pages accessible again. |
| size_t recommit_slot_size_growth = new_slot_size - current_slot_size; |
| RecommitSystemPagesForData(slot_start + current_slot_size, |
| recommit_slot_size_growth, |
| PageAccessibilityDisposition::kRequireUpdate); |
| // The recommited system pages had been already reserved and all the |
| // entries in the reservation offset table (for entire reservation_size |
| // region) have been already initialized. |
| |
| #if BUILDFLAG(PA_DCHECK_IS_ON) |
| memset(reinterpret_cast<void*>(slot_start + current_slot_size), |
| internal::kUninitializedByte, recommit_slot_size_growth); |
| #endif |
| } else { |
| // We can't perform the realloc in-place. |
| // TODO: support this too when possible. |
| return false; |
| } |
| |
| DecreaseTotalSizeOfAllocatedBytes(reinterpret_cast<uintptr_t>(slot_span), |
| slot_span->bucket->slot_size); |
| slot_span->SetRawSize(raw_size); |
| slot_span->bucket->slot_size = new_slot_size; |
| IncreaseTotalSizeOfAllocatedBytes(reinterpret_cast<uintptr_t>(slot_span), |
| slot_span->bucket->slot_size, raw_size); |
| |
| // Always record in-place realloc() as free()+malloc() pair. |
| // |
| // The early returns above (`return false`) will fall back to free()+malloc(), |
| // so this is consistent. |
| auto* thread_cache = GetOrCreateThreadCache(); |
| if (ThreadCache::IsValid(thread_cache)) { |
| thread_cache->RecordDeallocation(current_usable_size); |
| thread_cache->RecordAllocation(slot_span->GetUsableSize(this)); |
| } |
| |
| #if BUILDFLAG(PA_DCHECK_IS_ON) |
| // Write a new trailing cookie. |
| if (flags.allow_cookie) { |
| auto* object = static_cast<unsigned char*>(SlotStartToObject(slot_start)); |
| internal::PartitionCookieWriteValue(object + |
| slot_span->GetUsableSize(this)); |
| } |
| #endif |
| |
| return true; |
| } |
| |
| template <bool thread_safe> |
| bool PartitionRoot<thread_safe>::TryReallocInPlaceForNormalBuckets( |
| void* object, |
| SlotSpan* slot_span, |
| size_t new_size) { |
| uintptr_t slot_start = ObjectToSlotStart(object); |
| PA_DCHECK(internal::IsManagedByNormalBuckets(slot_start)); |
| |
| // TODO: note that tcmalloc will "ignore" a downsizing realloc() unless the |
| // new size is a significant percentage smaller. We could do the same if we |
| // determine it is a win. |
| if (AllocationCapacityFromRequestedSize(new_size) != |
| AllocationCapacityFromSlotStart(slot_start)) { |
| return false; |
| } |
| size_t current_usable_size = slot_span->GetUsableSize(this); |
| |
| // Trying to allocate |new_size| would use the same amount of underlying |
| // memory as we're already using, so re-use the allocation after updating |
| // statistics (and cookie, if present). |
| if (slot_span->CanStoreRawSize()) { |
| #if BUILDFLAG(PUT_REF_COUNT_IN_PREVIOUS_SLOT) && BUILDFLAG(PA_DCHECK_IS_ON) |
| internal::PartitionRefCount* old_ref_count; |
| if (brp_enabled()) { |
| old_ref_count = internal::PartitionRefCountPointer(slot_start); |
| } |
| #endif // BUILDFLAG(PUT_REF_COUNT_IN_PREVIOUS_SLOT) && |
| // BUILDFLAG(PA_DCHECK_IS_ON) |
| size_t new_raw_size = AdjustSizeForExtrasAdd(new_size); |
| slot_span->SetRawSize(new_raw_size); |
| #if BUILDFLAG(PUT_REF_COUNT_IN_PREVIOUS_SLOT) && BUILDFLAG(PA_DCHECK_IS_ON) |
| if (brp_enabled()) { |
| internal::PartitionRefCount* new_ref_count = |
| internal::PartitionRefCountPointer(slot_start); |
| PA_DCHECK(new_ref_count == old_ref_count); |
| } |
| #endif // BUILDFLAG(PUT_REF_COUNT_IN_PREVIOUS_SLOT) && |
| // BUILDFLAG(PA_DCHECK_IS_ON) |
| #if BUILDFLAG(PA_DCHECK_IS_ON) |
| // Write a new trailing cookie only when it is possible to keep track |
| // raw size (otherwise we wouldn't know where to look for it later). |
| if (flags.allow_cookie) { |
| internal::PartitionCookieWriteValue(static_cast<unsigned char*>(object) + |
| slot_span->GetUsableSize(this)); |
| } |
| #endif // BUILDFLAG(PA_DCHECK_IS_ON) |
| } |
| |
| // Always record a realloc() as a free() + malloc(), even if it's in |
| // place. When we cannot do it in place (`return false` above), the allocator |
| // falls back to free()+malloc(), so this is consistent. |
| ThreadCache* thread_cache = GetOrCreateThreadCache(); |
| if (PA_LIKELY(ThreadCache::IsValid(thread_cache))) { |
| thread_cache->RecordDeallocation(current_usable_size); |
| thread_cache->RecordAllocation(slot_span->GetUsableSize(this)); |
| } |
| |
| return object; |
| } |
| |
| template <bool thread_safe> |
| void* PartitionRoot<thread_safe>::ReallocWithFlags(unsigned int flags, |
| void* ptr, |
| size_t new_size, |
| const char* type_name) { |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| CHECK_MAX_SIZE_OR_RETURN_NULLPTR(new_size, flags); |
| void* result = realloc(ptr, new_size); |
| PA_CHECK(result || flags & AllocFlags::kReturnNull); |
| return result; |
| #else |
| bool no_hooks = flags & AllocFlags::kNoHooks; |
| if (PA_UNLIKELY(!ptr)) { |
| return no_hooks |
| ? AllocWithFlagsNoHooks(flags, new_size, |
| internal::PartitionPageSize()) |
| : AllocWithFlagsInternal( |
| flags, new_size, internal::PartitionPageSize(), type_name); |
| } |
| |
| if (PA_UNLIKELY(!new_size)) { |
| Free(ptr); |
| return nullptr; |
| } |
| |
| if (new_size > internal::MaxDirectMapped()) { |
| if (flags & AllocFlags::kReturnNull) { |
| return nullptr; |
| } |
| internal::PartitionExcessiveAllocationSize(new_size); |
| } |
| |
| const bool hooks_enabled = PartitionAllocHooks::AreHooksEnabled(); |
| bool overridden = false; |
| size_t old_usable_size; |
| if (PA_UNLIKELY(!no_hooks && hooks_enabled)) { |
| overridden = PartitionAllocHooks::ReallocOverrideHookIfEnabled( |
| &old_usable_size, ptr); |
| } |
| if (PA_LIKELY(!overridden)) { |
| // |ptr| may have been allocated in another root. |
| SlotSpan* slot_span = SlotSpan::FromObject(ptr); |
| auto* old_root = PartitionRoot::FromSlotSpan(slot_span); |
| bool success = false; |
| bool tried_in_place_for_direct_map = false; |
| { |
| ::partition_alloc::internal::ScopedGuard guard{old_root->lock_}; |
| // TODO(crbug.com/1257655): See if we can afford to make this a CHECK. |
| PA_DCHECK(IsValidSlotSpan(slot_span)); |
| old_usable_size = slot_span->GetUsableSize(old_root); |
| |
| if (PA_UNLIKELY(slot_span->bucket->is_direct_mapped())) { |
| tried_in_place_for_direct_map = true; |
| // We may be able to perform the realloc in place by changing the |
| // accessibility of memory pages and, if reducing the size, decommitting |
| // them. |
| success = old_root->TryReallocInPlaceForDirectMap(slot_span, new_size); |
| } |
| } |
| if (success) { |
| if (PA_UNLIKELY(!no_hooks && hooks_enabled)) { |
| PartitionAllocHooks::ReallocObserverHookIfEnabled(ptr, ptr, new_size, |
| type_name); |
| } |
| return ptr; |
| } |
| |
| if (PA_LIKELY(!tried_in_place_for_direct_map)) { |
| if (old_root->TryReallocInPlaceForNormalBuckets(ptr, slot_span, |
| new_size)) { |
| return ptr; |
| } |
| } |
| } |
| |
| // This realloc cannot be resized in-place. Sadness. |
| void* ret = |
| no_hooks ? AllocWithFlagsNoHooks(flags, new_size, |
| internal::PartitionPageSize()) |
| : AllocWithFlagsInternal( |
| flags, new_size, internal::PartitionPageSize(), type_name); |
| if (!ret) { |
| if (flags & AllocFlags::kReturnNull) { |
| return nullptr; |
| } |
| internal::PartitionExcessiveAllocationSize(new_size); |
| } |
| |
| memcpy(ret, ptr, std::min(old_usable_size, new_size)); |
| Free(ptr); // Implicitly protects the old ptr on MTE systems. |
| return ret; |
| #endif |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::PurgeMemory(int flags) { |
| { |
| ::partition_alloc::internal::ScopedGuard guard{lock_}; |
| #if BUILDFLAG(USE_STARSCAN) |
| // Avoid purging if there is PCScan task currently scheduled. Since pcscan |
| // takes snapshot of all allocated pages, decommitting pages here (even |
| // under the lock) is racy. |
| // TODO(bikineev): Consider rescheduling the purging after PCScan. |
| if (PCScan::IsInProgress()) { |
| return; |
| } |
| #endif // BUILDFLAG(USE_STARSCAN) |
| |
| if (flags & PurgeFlags::kDecommitEmptySlotSpans) { |
| DecommitEmptySlotSpans(); |
| } |
| if (flags & PurgeFlags::kDiscardUnusedSystemPages) { |
| for (Bucket& bucket : buckets) { |
| if (bucket.slot_size == internal::kInvalidBucketSize) { |
| continue; |
| } |
| |
| if (bucket.slot_size >= internal::MinPurgeableSlotSize()) { |
| internal::PartitionPurgeBucket(&bucket); |
| } else { |
| bucket.SortSlotSpanFreelists(); |
| } |
| |
| // Do it at the end, as the actions above change the status of slot |
| // spans (e.g. empty -> decommitted). |
| bucket.MaintainActiveList(); |
| |
| if (sort_active_slot_spans_) { |
| bucket.SortActiveSlotSpans(); |
| } |
| } |
| } |
| } |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::ShrinkEmptySlotSpansRing(size_t limit) { |
| int16_t index = global_empty_slot_span_ring_index; |
| int16_t starting_index = index; |
| while (empty_slot_spans_dirty_bytes > limit) { |
| SlotSpan* slot_span = global_empty_slot_span_ring[index]; |
| // The ring is not always full, may be nullptr. |
| if (slot_span) { |
| slot_span->DecommitIfPossible(this); |
| global_empty_slot_span_ring[index] = nullptr; |
| } |
| index += 1; |
| // Walk through the entirety of possible slots, even though the last ones |
| // are unused, if global_empty_slot_span_ring_size is smaller than |
| // kMaxFreeableSpans. It's simpler, and does not cost anything, since all |
| // the pointers are going to be nullptr. |
| if (index == internal::kMaxFreeableSpans) { |
| index = 0; |
| } |
| |
| // Went around the whole ring, since this is locked, |
| // empty_slot_spans_dirty_bytes should be exactly 0. |
| if (index == starting_index) { |
| PA_DCHECK(empty_slot_spans_dirty_bytes == 0); |
| // Metrics issue, don't crash, return. |
| break; |
| } |
| } |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::DumpStats(const char* partition_name, |
| bool is_light_dump, |
| PartitionStatsDumper* dumper) { |
| static const size_t kMaxReportableDirectMaps = 4096; |
| // Allocate on the heap rather than on the stack to avoid stack overflow |
| // skirmishes (on Windows, in particular). Allocate before locking below, |
| // otherwise when PartitionAlloc is malloc() we get reentrancy issues. This |
| // inflates reported values a bit for detailed dumps though, by 16kiB. |
| std::unique_ptr<uint32_t[]> direct_map_lengths; |
| if (!is_light_dump) { |
| direct_map_lengths = |
| std::unique_ptr<uint32_t[]>(new uint32_t[kMaxReportableDirectMaps]); |
| } |
| PartitionBucketMemoryStats bucket_stats[internal::kNumBuckets]; |
| size_t num_direct_mapped_allocations = 0; |
| PartitionMemoryStats stats = {0}; |
| |
| stats.syscall_count = syscall_count.load(std::memory_order_relaxed); |
| stats.syscall_total_time_ns = |
| syscall_total_time_ns.load(std::memory_order_relaxed); |
| |
| // Collect data with the lock held, cannot allocate or call third-party code |
| // below. |
| { |
| ::partition_alloc::internal::ScopedGuard guard{lock_}; |
| PA_DCHECK(total_size_of_allocated_bytes <= max_size_of_allocated_bytes); |
| |
| stats.total_mmapped_bytes = |
| total_size_of_super_pages.load(std::memory_order_relaxed) + |
| total_size_of_direct_mapped_pages.load(std::memory_order_relaxed); |
| stats.total_committed_bytes = |
| total_size_of_committed_pages.load(std::memory_order_relaxed); |
| stats.max_committed_bytes = |
| max_size_of_committed_pages.load(std::memory_order_relaxed); |
| stats.total_allocated_bytes = total_size_of_allocated_bytes; |
| stats.max_allocated_bytes = max_size_of_allocated_bytes; |
| #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| stats.total_brp_quarantined_bytes = |
| total_size_of_brp_quarantined_bytes.load(std::memory_order_relaxed); |
| stats.total_brp_quarantined_count = |
| total_count_of_brp_quarantined_slots.load(std::memory_order_relaxed); |
| stats.cumulative_brp_quarantined_bytes = |
| cumulative_size_of_brp_quarantined_bytes.load( |
| std::memory_order_relaxed); |
| stats.cumulative_brp_quarantined_count = |
| cumulative_count_of_brp_quarantined_slots.load( |
| std::memory_order_relaxed); |
| #endif |
| |
| size_t direct_mapped_allocations_total_size = 0; |
| for (size_t i = 0; i < internal::kNumBuckets; ++i) { |
| const Bucket* bucket = &bucket_at(i); |
| // Don't report the pseudo buckets that the generic allocator sets up in |
| // order to preserve a fast size->bucket map (see |
| // PartitionRoot::Init() for details). |
| if (!bucket->is_valid()) { |
| bucket_stats[i].is_valid = false; |
| } else { |
| internal::PartitionDumpBucketStats(&bucket_stats[i], bucket); |
| } |
| if (bucket_stats[i].is_valid) { |
| stats.total_resident_bytes += bucket_stats[i].resident_bytes; |
| stats.total_active_bytes += bucket_stats[i].active_bytes; |
| stats.total_active_count += bucket_stats[i].active_count; |
| stats.total_decommittable_bytes += bucket_stats[i].decommittable_bytes; |
| stats.total_discardable_bytes += bucket_stats[i].discardable_bytes; |
| } |
| } |
| |
| for (DirectMapExtent* extent = direct_map_list; |
| extent && num_direct_mapped_allocations < kMaxReportableDirectMaps; |
| extent = extent->next_extent, ++num_direct_mapped_allocations) { |
| PA_DCHECK(!extent->next_extent || |
| extent->next_extent->prev_extent == extent); |
| size_t slot_size = extent->bucket->slot_size; |
| direct_mapped_allocations_total_size += slot_size; |
| if (is_light_dump) { |
| continue; |
| } |
| direct_map_lengths[num_direct_mapped_allocations] = slot_size; |
| } |
| |
| stats.total_resident_bytes += direct_mapped_allocations_total_size; |
| stats.total_active_bytes += direct_mapped_allocations_total_size; |
| stats.total_active_count += num_direct_mapped_allocations; |
| |
| stats.has_thread_cache = flags.with_thread_cache; |
| if (stats.has_thread_cache) { |
| ThreadCacheRegistry::Instance().DumpStats( |
| true, &stats.current_thread_cache_stats); |
| ThreadCacheRegistry::Instance().DumpStats(false, |
| &stats.all_thread_caches_stats); |
| } |
| } |
| |
| // Do not hold the lock when calling |dumper|, as it may allocate. |
| if (!is_light_dump) { |
| for (auto& stat : bucket_stats) { |
| if (stat.is_valid) { |
| dumper->PartitionsDumpBucketStats(partition_name, &stat); |
| } |
| } |
| |
| for (size_t i = 0; i < num_direct_mapped_allocations; ++i) { |
| uint32_t size = direct_map_lengths[i]; |
| |
| PartitionBucketMemoryStats mapped_stats = {}; |
| mapped_stats.is_valid = true; |
| mapped_stats.is_direct_map = true; |
| mapped_stats.num_full_slot_spans = 1; |
| mapped_stats.allocated_slot_span_size = size; |
| mapped_stats.bucket_slot_size = size; |
| mapped_stats.active_bytes = size; |
| mapped_stats.active_count = 1; |
| mapped_stats.resident_bytes = size; |
| dumper->PartitionsDumpBucketStats(partition_name, &mapped_stats); |
| } |
| } |
| dumper->PartitionDumpTotals(partition_name, &stats); |
| } |
| |
| // static |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::DeleteForTesting( |
| PartitionRoot* partition_root) { |
| if (partition_root->flags.with_thread_cache) { |
| ThreadCache::SwapForTesting(nullptr); |
| partition_root->flags.with_thread_cache = false; |
| } |
| |
| partition_root->DestructForTesting(); // IN-TEST |
| |
| delete partition_root; |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::ResetForTesting(bool allow_leaks) { |
| if (flags.with_thread_cache) { |
| ThreadCache::SwapForTesting(nullptr); |
| flags.with_thread_cache = false; |
| } |
| |
| ::partition_alloc::internal::ScopedGuard guard(lock_); |
| |
| #if BUILDFLAG(PA_DCHECK_IS_ON) |
| if (!allow_leaks) { |
| unsigned num_allocated_slots = 0; |
| for (Bucket& bucket : buckets) { |
| if (bucket.active_slot_spans_head != |
| internal::SlotSpanMetadata<thread_safe>::get_sentinel_slot_span()) { |
| for (internal::SlotSpanMetadata<thread_safe>* slot_span = |
| bucket.active_slot_spans_head; |
| slot_span; slot_span = slot_span->next_slot_span) { |
| num_allocated_slots += slot_span->num_allocated_slots; |
| } |
| } |
| // Full slot spans are nowhere. Need to see bucket.num_full_slot_spans |
| // to count the number of full slot spans' slots. |
| if (bucket.num_full_slot_spans) { |
| num_allocated_slots += |
| bucket.num_full_slot_spans * bucket.get_slots_per_span(); |
| } |
| } |
| PA_DCHECK(num_allocated_slots == 0); |
| |
| // Check for direct-mapped allocations. |
| PA_DCHECK(!direct_map_list); |
| } |
| #endif |
| |
| DestructForTesting(); // IN-TEST |
| |
| #if PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| if (initialized) { |
| internal::PartitionRootEnumerator::Instance().Unregister(this); |
| } |
| #endif // PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| |
| for (Bucket& bucket : buckets) { |
| bucket.active_slot_spans_head = |
| SlotSpan::get_sentinel_slot_span_non_const(); |
| bucket.empty_slot_spans_head = nullptr; |
| bucket.decommitted_slot_spans_head = nullptr; |
| bucket.num_full_slot_spans = 0; |
| } |
| |
| next_super_page = 0; |
| next_partition_page = 0; |
| next_partition_page_end = 0; |
| current_extent = nullptr; |
| first_extent = nullptr; |
| |
| direct_map_list = nullptr; |
| for (auto& entity : global_empty_slot_span_ring) { |
| entity = nullptr; |
| } |
| |
| global_empty_slot_span_ring_index = 0; |
| global_empty_slot_span_ring_size = internal::kDefaultEmptySlotSpanRingSize; |
| initialized = false; |
| } |
| |
| template <bool thread_safe> |
| void PartitionRoot<thread_safe>::ResetBookkeepingForTesting() { |
| ::partition_alloc::internal::ScopedGuard guard{lock_}; |
| max_size_of_allocated_bytes = total_size_of_allocated_bytes; |
| max_size_of_committed_pages.store(total_size_of_committed_pages); |
| } |
| |
| template <> |
| ThreadCache* PartitionRoot<internal::ThreadSafe>::MaybeInitThreadCache() { |
| auto* tcache = ThreadCache::Get(); |
| // See comment in `EnableThreadCacheIfSupport()` for why this is an acquire |
| // load. |
| if (ThreadCache::IsTombstone(tcache) || |
| thread_caches_being_constructed_.load(std::memory_order_acquire)) { |
| // Two cases: |
| // 1. Thread is being terminated, don't try to use the thread cache, and |
| // don't try to resurrect it. |
| // 2. Someone, somewhere is currently allocating a thread cache. This may |
| // be us, in which case we are re-entering and should not create a thread |
| // cache. If it is not us, then this merely delays thread cache |
| // construction a bit, which is not an issue. |
| return nullptr; |
| } |
| |
| // There is no per-thread ThreadCache allocated here yet, and this partition |
| // has a thread cache, allocate a new one. |
| // |
| // The thread cache allocation itself will not reenter here, as it sidesteps |
| // the thread cache by using placement new and |RawAlloc()|. However, |
| // internally to libc, allocations may happen to create a new TLS |
| // variable. This would end up here again, which is not what we want (and |
| // likely is not supported by libc). |
| // |
| // To avoid this sort of reentrancy, increase the count of thread caches that |
| // are currently allocating a thread cache. |
| // |
| // Note that there is no deadlock or data inconsistency concern, since we do |
| // not hold the lock, and has such haven't touched any internal data. |
| int before = |
| thread_caches_being_constructed_.fetch_add(1, std::memory_order_relaxed); |
| PA_CHECK(before < std::numeric_limits<int>::max()); |
| tcache = ThreadCache::Create(this); |
| thread_caches_being_constructed_.fetch_sub(1, std::memory_order_relaxed); |
| |
| return tcache; |
| } |
| |
| template <> |
| void PartitionRoot<internal::ThreadSafe>::EnableSortActiveSlotSpans() { |
| sort_active_slot_spans_ = true; |
| } |
| |
| template struct PA_COMPONENT_EXPORT(PARTITION_ALLOC) |
| PartitionRoot<internal::ThreadSafe>; |
| |
| static_assert(offsetof(PartitionRoot<internal::ThreadSafe>, sentinel_bucket) == |
| offsetof(PartitionRoot<internal::ThreadSafe>, buckets) + |
| internal::kNumBuckets * |
| sizeof(PartitionRoot<internal::ThreadSafe>::Bucket), |
| "sentinel_bucket must be just after the regular buckets."); |
| |
| static_assert( |
| offsetof(PartitionRoot<internal::ThreadSafe>, lock_) >= 64, |
| "The lock should not be on the same cacheline as the read-mostly flags"); |
| |
| } // namespace partition_alloc |