// Copyright (c) 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef BASE_ALLOCATOR_PARTITION_ALLOCATOR_PARTITION_PAGE_H_ #define BASE_ALLOCATOR_PARTITION_ALLOCATOR_PARTITION_PAGE_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_freelist_entry.h" namespace base { namespace internal { struct PartitionRootBase; // Some notes on page states. A page can be in one of four major states: // 1) Active. // 2) Full. // 3) Empty. // 4) Decommitted. // An active page has available free slots. A full page has no free slots. An // empty page has no free slots, and a decommitted page is an empty page that // had its backing memory released back to the system. // There are two linked lists tracking the pages. The "active page" list is an // approximation of a list of active pages. It is an approximation because // full, empty and decommitted pages may briefly be present in the list until // we next do a scan over it. // The "empty page" list is an accurate list of pages which are either empty // or decommitted. // // The significant page transitions are: // - free() will detect when a full page has a slot free()'d and immediately // return the page to the head of the active list. // - free() will detect when a page is fully emptied. It _may_ add it to the // empty list or it _may_ leave it on the active list until a future list scan. // - malloc() _may_ scan the active page list in order to fulfil the request. // If it does this, full, empty and decommitted pages encountered will be // booted out of the active list. If there are no suitable active pages found, // an empty or decommitted page (if one exists) will be pulled from the empty // list on to the active list. // // TODO(ajwong): Evaluate if this should be named PartitionSlotSpanMetadata or // similar. If so, all uses of the term "page" in comments, member variables, // local variables, and documentation that refer to this concept should be // updated. struct PartitionPage { PartitionFreelistEntry* freelist_head; PartitionPage* next_page; PartitionBucket* bucket; // Deliberately signed, 0 for empty or decommitted page, -n for full pages: int16_t num_allocated_slots; uint16_t num_unprovisioned_slots; uint16_t page_offset; int16_t empty_cache_index; // -1 if not in the empty cache. // Public API // Note the matching Alloc() functions are in PartitionPage. BASE_EXPORT NOINLINE void FreeSlowPath(); ALWAYS_INLINE void Free(void* ptr); void Decommit(PartitionRootBase* root); void DecommitIfPossible(PartitionRootBase* root); // Pointer manipulation functions. These must be static as the input |page| // pointer may be the result of an offset calculation and therefore cannot // be trusted. The objective of these functions is to sanitize this input. ALWAYS_INLINE static void* ToPointer(const PartitionPage* page); ALWAYS_INLINE static PartitionPage* FromPointerNoAlignmentCheck(void* ptr); ALWAYS_INLINE static PartitionPage* FromPointer(void* ptr); ALWAYS_INLINE const size_t* get_raw_size_ptr() const; ALWAYS_INLINE size_t* get_raw_size_ptr() { return const_cast( const_cast(this)->get_raw_size_ptr()); } ALWAYS_INLINE size_t get_raw_size() const; ALWAYS_INLINE void set_raw_size(size_t size); ALWAYS_INLINE void Reset(); // TODO(ajwong): Can this be made private? https://crbug.com/787153 BASE_EXPORT static PartitionPage* get_sentinel_page(); // Page State accessors. // Note that it's only valid to call these functions on pages found on one of // the page lists. Specifically, you can't call these functions on full pages // that were detached from the active list. // // This restriction provides the flexibity for some of the status fields to // be repurposed when a page is taken off a list. See the negation of // |num_allocated_slots| when a full page is removed from the active list // for an example of such repurposing. ALWAYS_INLINE bool is_active() const; ALWAYS_INLINE bool is_full() const; ALWAYS_INLINE bool is_empty() const; ALWAYS_INLINE bool is_decommitted() const; private: // g_sentinel_page is used as a sentinel to indicate that there is no page // in the active page list. We can use nullptr, but in that case we need // to add a null-check branch to the hot allocation path. We want to avoid // that. // // Note, this declaration is kept in the header as opposed to an anonymous // namespace so the getter can be fully inlined. static PartitionPage sentinel_page_; }; static_assert(sizeof(PartitionPage) <= kPageMetadataSize, "PartitionPage must be able to fit in a metadata slot"); ALWAYS_INLINE char* PartitionSuperPageToMetadataArea(char* ptr) { uintptr_t pointer_as_uint = reinterpret_cast(ptr); DCHECK(!(pointer_as_uint & kSuperPageOffsetMask)); // The metadata area is exactly one system page (the guard page) into the // super page. return reinterpret_cast(pointer_as_uint + kSystemPageSize); } ALWAYS_INLINE PartitionPage* PartitionPage::FromPointerNoAlignmentCheck( void* ptr) { uintptr_t pointer_as_uint = reinterpret_cast(ptr); char* super_page_ptr = reinterpret_cast(pointer_as_uint & kSuperPageBaseMask); uintptr_t partition_page_index = (pointer_as_uint & kSuperPageOffsetMask) >> kPartitionPageShift; // Index 0 is invalid because it is the metadata and guard area and // the last index is invalid because it is a guard page. DCHECK(partition_page_index); DCHECK(partition_page_index < kNumPartitionPagesPerSuperPage - 1); PartitionPage* page = reinterpret_cast( PartitionSuperPageToMetadataArea(super_page_ptr) + (partition_page_index << kPageMetadataShift)); // Partition pages in the same slot span can share the same page object. // Adjust for that. size_t delta = page->page_offset << kPageMetadataShift; page = reinterpret_cast(reinterpret_cast(page) - delta); return page; } // Resturns start of the slot span for the PartitionPage. ALWAYS_INLINE void* PartitionPage::ToPointer(const PartitionPage* page) { uintptr_t pointer_as_uint = reinterpret_cast(page); uintptr_t super_page_offset = (pointer_as_uint & kSuperPageOffsetMask); // A valid |page| must be past the first guard System page and within // the following metadata region. DCHECK(super_page_offset > kSystemPageSize); // Must be less than total metadata region. DCHECK(super_page_offset < kSystemPageSize + (kNumPartitionPagesPerSuperPage * kPageMetadataSize)); uintptr_t partition_page_index = (super_page_offset - kSystemPageSize) >> kPageMetadataShift; // Index 0 is invalid because it is the superpage extent metadata and the // last index is invalid because the whole PartitionPage is set as guard // pages for the metadata region. DCHECK(partition_page_index); DCHECK(partition_page_index < kNumPartitionPagesPerSuperPage - 1); uintptr_t super_page_base = (pointer_as_uint & kSuperPageBaseMask); void* ret = reinterpret_cast( super_page_base + (partition_page_index << kPartitionPageShift)); return ret; } ALWAYS_INLINE PartitionPage* PartitionPage::FromPointer(void* ptr) { PartitionPage* page = PartitionPage::FromPointerNoAlignmentCheck(ptr); // Checks that the pointer is a multiple of bucket size. DCHECK(!((reinterpret_cast(ptr) - reinterpret_cast(PartitionPage::ToPointer(page))) % page->bucket->slot_size)); return page; } ALWAYS_INLINE const size_t* PartitionPage::get_raw_size_ptr() const { // For single-slot buckets which span more than one partition page, we // have some spare metadata space to store the raw allocation size. We // can use this to report better statistics. if (bucket->slot_size <= kMaxSystemPagesPerSlotSpan * kSystemPageSize) return nullptr; DCHECK((bucket->slot_size % kSystemPageSize) == 0); DCHECK(bucket->is_direct_mapped() || bucket->get_slots_per_span() == 1); const PartitionPage* the_next_page = this + 1; return reinterpret_cast(&the_next_page->freelist_head); } ALWAYS_INLINE size_t PartitionPage::get_raw_size() const { const size_t* ptr = get_raw_size_ptr(); if (UNLIKELY(ptr != nullptr)) return *ptr; return 0; } ALWAYS_INLINE void PartitionPage::Free(void* ptr) { // If these asserts fire, you probably corrupted memory. #if DCHECK_IS_ON() size_t slot_size = this->bucket->slot_size; size_t raw_size = get_raw_size(); if (raw_size) slot_size = raw_size; PartitionCookieCheckValue(ptr); PartitionCookieCheckValue(reinterpret_cast(ptr) + slot_size - kCookieSize); memset(ptr, kFreedByte, slot_size); #endif DCHECK(this->num_allocated_slots); // TODO(palmer): See if we can afford to make this a CHECK. // FIX FIX FIX // DCHECK(!freelist_head || PartitionRootBase::IsValidPage( // PartitionPage::FromPointer(freelist_head))); CHECK(ptr != freelist_head); // Catches an immediate double free. // Look for double free one level deeper in debug. DCHECK(!freelist_head || ptr != internal::PartitionFreelistEntry::Transform( freelist_head->next)); internal::PartitionFreelistEntry* entry = static_cast(ptr); entry->next = internal::PartitionFreelistEntry::Transform(freelist_head); freelist_head = entry; --this->num_allocated_slots; if (UNLIKELY(this->num_allocated_slots <= 0)) { FreeSlowPath(); } else { // All single-slot allocations must go through the slow path to // correctly update the size metadata. DCHECK(get_raw_size() == 0); } } ALWAYS_INLINE bool PartitionPage::is_active() const { DCHECK(this != get_sentinel_page()); DCHECK(!page_offset); return (num_allocated_slots > 0 && (freelist_head || num_unprovisioned_slots)); } ALWAYS_INLINE bool PartitionPage::is_full() const { DCHECK(this != get_sentinel_page()); DCHECK(!page_offset); bool ret = (num_allocated_slots == bucket->get_slots_per_span()); if (ret) { DCHECK(!freelist_head); DCHECK(!num_unprovisioned_slots); } return ret; } ALWAYS_INLINE bool PartitionPage::is_empty() const { DCHECK(this != get_sentinel_page()); DCHECK(!page_offset); return (!num_allocated_slots && freelist_head); } ALWAYS_INLINE bool PartitionPage::is_decommitted() const { DCHECK(this != get_sentinel_page()); DCHECK(!page_offset); bool ret = (!num_allocated_slots && !freelist_head); if (ret) { DCHECK(!num_unprovisioned_slots); DCHECK(empty_cache_index == -1); } return ret; } ALWAYS_INLINE void PartitionPage::set_raw_size(size_t size) { size_t* raw_size_ptr = get_raw_size_ptr(); if (UNLIKELY(raw_size_ptr != nullptr)) *raw_size_ptr = size; } ALWAYS_INLINE void PartitionPage::Reset() { DCHECK(this->is_decommitted()); num_unprovisioned_slots = bucket->get_slots_per_span(); DCHECK(num_unprovisioned_slots); next_page = nullptr; } } // namespace internal } // namespace base #endif // BASE_ALLOCATOR_PARTITION_ALLOCATOR_PARTITION_PAGE_H_