| // 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. |
| |
| #ifndef PARTITION_ALLOC_PARTITION_ROOT_H_ |
| #define PARTITION_ALLOC_PARTITION_ROOT_H_ |
| |
| // DESCRIPTION |
| // PartitionRoot::Alloc() and PartitionRoot::Free() are approximately analogous |
| // to malloc() and free(). |
| // |
| // The main difference is that a PartitionRoot object must be supplied to these |
| // functions, representing a specific "heap partition" that will be used to |
| // satisfy the allocation. Different partitions are guaranteed to exist in |
| // separate address spaces, including being separate from the main system |
| // heap. If the contained objects are all freed, physical memory is returned to |
| // the system but the address space remains reserved. See PartitionAlloc.md for |
| // other security properties PartitionAlloc provides. |
| // |
| // THE ONLY LEGITIMATE WAY TO OBTAIN A PartitionRoot IS THROUGH THE |
| // PartitionAllocator classes. To minimize the instruction count to the fullest |
| // extent possible, the PartitionRoot is really just a header adjacent to other |
| // data areas provided by the allocator class. |
| // |
| // The constraints for PartitionRoot::Alloc() are: |
| // - Multi-threaded use against a single partition is ok; locking is handled. |
| // - Allocations of any arbitrary size can be handled (subject to a limit of |
| // INT_MAX bytes for security reasons). |
| // - Bucketing is by approximate size, for example an allocation of 4000 bytes |
| // might be placed into a 4096-byte bucket. Bucket sizes are chosen to try and |
| // keep worst-case waste to ~10%. |
| |
| #include <atomic> |
| #include <cstddef> |
| #include <cstdint> |
| #include <limits> |
| #include <utility> |
| |
| #include "partition_alloc/address_pool_manager_types.h" |
| #include "partition_alloc/allocation_guard.h" |
| #include "partition_alloc/bucket_lookup.h" |
| #include "partition_alloc/build_config.h" |
| #include "partition_alloc/buildflags.h" |
| #include "partition_alloc/in_slot_metadata.h" |
| #include "partition_alloc/page_allocator.h" |
| #include "partition_alloc/partition_address_space.h" |
| #include "partition_alloc/partition_alloc-inl.h" |
| #include "partition_alloc/partition_alloc_allocation_data.h" |
| #include "partition_alloc/partition_alloc_base/bits.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/algorithm.h" |
| #include "partition_alloc/partition_alloc_base/cxx_wrapper/optional.h" |
| #include "partition_alloc/partition_alloc_base/export_template.h" |
| #include "partition_alloc/partition_alloc_base/no_destructor.h" |
| #include "partition_alloc/partition_alloc_base/notreached.h" |
| #include "partition_alloc/partition_alloc_base/thread_annotations.h" |
| #include "partition_alloc/partition_alloc_base/time/time.h" |
| #include "partition_alloc/partition_alloc_check.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_alloc_hooks.h" |
| #include "partition_alloc/partition_bucket.h" |
| #include "partition_alloc/partition_cookie.h" |
| #include "partition_alloc/partition_dcheck_helper.h" |
| #include "partition_alloc/partition_direct_map_extent.h" |
| #include "partition_alloc/partition_freelist_entry.h" |
| #include "partition_alloc/partition_lock.h" |
| #include "partition_alloc/partition_oom.h" |
| #include "partition_alloc/partition_page.h" |
| #include "partition_alloc/reservation_offset_table.h" |
| #include "partition_alloc/scheduler_loop_quarantine.h" |
| #include "partition_alloc/slot_start.h" |
| #include "partition_alloc/tagging.h" |
| #include "partition_alloc/thread_cache.h" |
| #include "partition_alloc/thread_isolation/thread_isolation.h" |
| |
| // When a memory tool is replacing malloc to keep aligned behaviour working we |
| // use window's aligned_malloc and aligned_free, but otherwise we need memalign. |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| #if PA_BUILDFLAG(PA_COMPILER_MSVC) |
| #include <malloc.h> |
| #else |
| #include <stdlib.h> |
| #endif // PA_BUILDFLAG(PA_COMPILER_MSVC) |
| #endif // defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| |
| namespace partition_alloc::internal { |
| |
| // We want this size to be big enough that we have time to start up other |
| // scripts _before_ we wrap around. |
| static constexpr size_t kAllocInfoSize = 1 << 24; |
| |
| struct AllocInfo { |
| std::atomic<size_t> index{0}; |
| struct { |
| uintptr_t addr; |
| size_t size; |
| } allocs[kAllocInfoSize] = {}; |
| }; |
| |
| // Represents the detailed size information for a given requested allocation |
| // size. |
| struct BucketSizeDetails { |
| uint16_t bucket_index; |
| size_t slot_size; |
| }; |
| |
| #if PA_BUILDFLAG(RECORD_ALLOC_INFO) |
| extern AllocInfo g_allocs; |
| |
| void RecordAllocOrFree(uintptr_t addr, size_t size); |
| #endif // PA_BUILDFLAG(RECORD_ALLOC_INFO) |
| } // namespace partition_alloc::internal |
| |
| namespace partition_alloc { |
| |
| namespace internal { |
| // Avoid including partition_address_space.h from this .h file, by moving the |
| // call to IsManagedByPartitionAllocBRPPool into the .cc file. |
| #if PA_BUILDFLAG(DCHECKS_ARE_ON) |
| PA_COMPONENT_EXPORT(PARTITION_ALLOC) |
| void DCheckIfManagedByPartitionAllocBRPPool(uintptr_t address); |
| #else |
| PA_ALWAYS_INLINE void DCheckIfManagedByPartitionAllocBRPPool( |
| uintptr_t address) {} |
| #endif |
| |
| #if PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| class PartitionRootEnumerator; |
| #endif |
| |
| class BatchFreeQueue; |
| |
| } // namespace internal |
| |
| // Bit flag constants used to purge memory. See PartitionRoot::PurgeMemory. |
| // |
| // In order to support bit operations like `flag_a | flag_b`, the old-fashioned |
| // enum (+ surrounding named struct) is used instead of enum class. |
| struct PurgeFlags { |
| enum : int { |
| // Decommitting the ring list of empty slot spans is reasonably fast. |
| kDecommitEmptySlotSpans = 1 << 0, |
| // Discarding unused system pages is slower, because it involves walking all |
| // freelists in all active slot spans of all buckets_ >= system page |
| // size. It often frees a similar amount of memory to decommitting the empty |
| // slot spans, though. |
| kDiscardUnusedSystemPages = 1 << 1, |
| // Aggressively reclaim memory. This is meant to be used in low-memory |
| // situations, not for periodic memory reclaiming. |
| kAggressiveReclaim = 1 << 2, |
| // Limit the total duration of reclaim to 2ms, then return even if reclaim |
| // is incomplete. |
| kLimitDuration = 1 << 3, |
| }; |
| }; |
| |
| // Options struct used to configure PartitionRoot and PartitionAllocator. |
| struct PartitionOptions { |
| // 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 PartitionOptions(); |
| inline constexpr PartitionOptions(const PartitionOptions& other); |
| inline PA_CONSTEXPR_DTOR ~PartitionOptions(); |
| |
| enum class AllowToggle : uint8_t { |
| kDisallowed, |
| kAllowed, |
| }; |
| enum class EnableToggle : uint8_t { |
| kDisabled, |
| kEnabled, |
| }; |
| |
| // Expose the enum arms directly at the level of `PartitionOptions`, |
| // since the variant names are already sufficiently descriptive. |
| static constexpr auto kAllowed = AllowToggle::kAllowed; |
| static constexpr auto kDisallowed = AllowToggle::kDisallowed; |
| static constexpr auto kDisabled = EnableToggle::kDisabled; |
| static constexpr auto kEnabled = EnableToggle::kEnabled; |
| |
| // Partitions with a thread cache cannot be destroyed. |
| EnableToggle thread_cache = kDisabled; |
| size_t thread_cache_index = internal::kInvalidThreadCacheIndex; |
| EnableToggle use_cookie_if_supported = kEnabled; |
| EnableToggle backup_ref_ptr = kDisabled; |
| AllowToggle use_configurable_pool = kDisallowed; |
| |
| // TODO(https://crbug.com/371135823): Remove after the investigation. |
| size_t backup_ref_ptr_extra_extras_size = 0; |
| |
| // Configuration for the global quarantine branch. Used when a thread local |
| // instance is not available. |
| internal::SchedulerLoopQuarantineConfig |
| scheduler_loop_quarantine_global_config; |
| // Configuration for the thread-local quarantine branch. Associated with |
| // each `ThreadCache` instance. |
| internal::SchedulerLoopQuarantineConfig |
| scheduler_loop_quarantine_thread_local_config; |
| // Configuration for the AMSC quarantine branch. Used when |
| // `FreeFlags::kSchedulerLoopQuarantineForAdvancedMemorySafetyChecks` is |
| // specified. |
| internal::SchedulerLoopQuarantineConfig |
| scheduler_loop_quarantine_for_advanced_memory_safety_checks_config; |
| |
| // As the name implies, this is not a security measure, as there is no |
| // guarantee that memorys has been zeroed out when handed back to the |
| // application, or when free() returns. This is intended to improve the |
| // compression ratio of freed memory inside partially allocated pages (due to |
| // fragmentation). |
| EnableToggle eventually_zero_freed_memory = kDisabled; |
| |
| struct { |
| EnableToggle enabled = kDisabled; |
| EnableToggle random_memory_tagging = kDisabled; |
| TagViolationReportingMode reporting_mode = |
| TagViolationReportingMode::kUndefined; |
| } memory_tagging; |
| #if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION) |
| ThreadIsolationOption thread_isolation; |
| #endif |
| |
| EnableToggle free_with_size = kDisabled; |
| EnableToggle strict_free_size_check = kEnabled; |
| }; |
| |
| constexpr PartitionOptions::PartitionOptions() = default; |
| constexpr PartitionOptions::PartitionOptions(const PartitionOptions& other) = |
| default; |
| PA_CONSTEXPR_DTOR PartitionOptions::~PartitionOptions() = default; |
| |
| // When/if free lists should be "straightened" when calling |
| // PartitionRoot::PurgeMemory(..., accounting_only=false). |
| enum class StraightenLargerSlotSpanFreeListsMode { |
| kNever, |
| kOnlyWhenUnprovisioning, |
| kAlways, |
| }; |
| |
| // Never instantiate a PartitionRoot directly, instead use |
| // PartitionAllocator. |
| class alignas(64) PA_COMPONENT_EXPORT(PARTITION_ALLOC) PartitionRoot { |
| public: |
| using SlotSpanMetadata = internal::SlotSpanMetadata; |
| using Bucket = internal::PartitionBucket; |
| using FreeListEntry = internal::FreelistEntry; |
| using SuperPageExtentEntry = internal::PartitionSuperPageExtentEntry; |
| using DirectMapExtent = internal::PartitionDirectMapExtent; |
| |
| enum class BucketDistribution : uint8_t { kNeutral, kDenser }; |
| |
| // Root settings_ accessed on fast paths. |
| // |
| // Careful! PartitionAlloc's performance is sensitive to its layout. Please |
| // put the fast-path objects in the struct below. |
| struct alignas(internal::kPartitionCachelineSize) Settings { |
| // Chromium-style: Complex constructor needs an explicit out-of-line |
| // constructor. |
| Settings(); |
| |
| // It's important to default to the 'neutral' distribution, otherwise a |
| // switch from 'dense' -> 'neutral' would leave some buckets_ with dirty |
| // memory forever, since no memory would be allocated from these, their |
| // freelist would typically not be empty, making these unreclaimable. |
| BucketDistribution bucket_distribution = BucketDistribution::kNeutral; |
| |
| bool with_thread_cache = false; |
| size_t thread_cache_index = internal::kInvalidThreadCacheIndex; |
| |
| #if PA_BUILDFLAG(USE_PARTITION_COOKIE) |
| bool use_cookie = true; |
| #else |
| static constexpr bool use_cookie = false; |
| #endif // PA_BUILDFLAG(USE_PARTITION_COOKIE) |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| bool brp_enabled_ = false; |
| size_t in_slot_metadata_size = 0; |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| internal::pool_handle pool_handle = internal::pool_handle::kNullPoolHandle; |
| #if PA_BUILDFLAG(HAS_64_BIT_POINTERS) |
| internal::PoolOffsetLookup offset_lookup; |
| #endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS) |
| internal::ReservationOffsetTable reservation_offset_table; |
| |
| bool eventually_zero_freed_memory = false; |
| internal::SchedulerLoopQuarantineConfig |
| scheduler_loop_quarantine_thread_local_config; |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| bool memory_tagging_enabled_ = false; |
| bool use_random_memory_tagging_ = false; |
| TagViolationReportingMode memory_tagging_reporting_mode_ = |
| TagViolationReportingMode::kUndefined; |
| #endif // PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| #if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION) |
| ThreadIsolationOption thread_isolation; |
| #endif |
| |
| #if PA_CONFIG(EXTRAS_REQUIRED) |
| uint32_t extras_size = 0; |
| #else |
| // Teach the compiler that code can be optimized in builds that use no |
| // extras. |
| static inline constexpr uint32_t extras_size = 0; |
| #endif // PA_CONFIG(EXTRAS_REQUIRED) |
| |
| #if PA_CONFIG(MOVE_METADATA_OUT_OF_GIGACAGE) |
| std::ptrdiff_t metadata_offset_ = 0; |
| #endif |
| |
| bool enable_free_with_size = false; |
| bool enable_strict_free_size_check = true; |
| }; |
| |
| Settings settings_; |
| |
| // Not used on the fastest path (thread cache allocations), but on the fast |
| // path of the central allocator. |
| alignas(internal::kPartitionCachelineSize) internal::Lock lock_; |
| |
| // Add last bucket as sentinel. |
| Bucket buckets_[BucketIndexLookup::kNumBuckets] = {}; |
| Bucket sentinel_bucket_{}; |
| |
| // All fields below this comment are not accessed on the fast path. |
| bool initialized_ = false; |
| |
| // Bookkeeping. |
| // - total_size_of_super_pages_ - total virtual address space for normal |
| // bucket |
| // super pages |
| // - total_size_of_direct_mapped_pages_ - total virtual address space for |
| // direct-map regions |
| // - total_size_of_committed_pages_ - total committed pages for slots (doesn't |
| // include metadata, bitmaps (if any), or any data outside or regions |
| // described in #1 and #2) |
| // Invariant: total_size_of_allocated_bytes_ <= |
| // total_size_of_committed_pages_ < |
| // total_size_of_super_pages_ + |
| // total_size_of_direct_mapped_pages_. |
| // Invariant: total_size_of_committed_pages_ <= max_size_of_committed_pages_. |
| // Invariant: total_size_of_allocated_bytes_ <= max_size_of_allocated_bytes_. |
| // Invariant: max_size_of_allocated_bytes_ <= max_size_of_committed_pages_. |
| // Since all operations on the atomic variables have relaxed semantics, we |
| // don't check these invariants with DCHECKs. |
| std::atomic<size_t> total_size_of_committed_pages_{0}; |
| std::atomic<size_t> max_size_of_committed_pages_{0}; |
| std::atomic<size_t> total_size_of_super_pages_{0}; |
| std::atomic<size_t> total_size_of_direct_mapped_pages_{0}; |
| std::atomic<size_t> total_size_of_allocated_bytes_{0}; |
| std::atomic<size_t> max_size_of_allocated_bytes_{0}; |
| // Atomic, because system calls can be made without the lock held. |
| std::atomic<uint64_t> syscall_count_; |
| std::atomic<uint64_t> syscall_total_time_ns_; |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| // TODO(https://crbug.com/475729821): Remove PDFium reference to this variable |
| // and rename this to `total_size_of_brp_quarantined_bytes_`. |
| std::atomic<size_t> total_size_of_brp_quarantined_bytes{0}; |
| std::atomic<size_t> total_count_of_brp_quarantined_slots_{0}; |
| std::atomic<size_t> cumulative_size_of_brp_quarantined_bytes_{0}; |
| std::atomic<size_t> cumulative_count_of_brp_quarantined_slots_{0}; |
| #endif |
| // Slot span memory which has been provisioned, and is currently unused as |
| // it's part of an empty SlotSpan. This is not clean memory, since it has |
| // either been used for a memory allocation, and/or contains freelist |
| // entries. But it might have been moved to swap. Note that all this memory |
| // can be decommitted at any time. |
| size_t empty_slot_spans_dirty_bytes_ |
| PA_GUARDED_BY(internal::PartitionRootLock(this)) = 0; |
| |
| // Only tolerate up to |total_size_of_committed_pages_ >> |
| // max_empty_slot_spans_dirty_bytes_shift_| dirty bytes in empty slot |
| // spans. That is, the default value of 3 tolerates up to 1/8. Since |
| // |empty_slot_spans_dirty_bytes_| is never strictly larger than |
| // total_size_of_committed_pages_, setting this to 0 removes the cap. This is |
| // useful to make tests deterministic and easier to reason about. |
| int max_empty_slot_spans_dirty_bytes_shift_ = 3; |
| |
| uintptr_t next_super_page_ = 0; |
| uintptr_t next_partition_page_ = 0; |
| uintptr_t next_partition_page_end_ = 0; |
| SuperPageExtentEntry* current_extent_ = nullptr; |
| SuperPageExtentEntry* first_extent_ = nullptr; |
| DirectMapExtent* direct_map_list_ |
| PA_GUARDED_BY(internal::PartitionRootLock(this)) = nullptr; |
| SlotSpanMetadata* global_empty_slot_span_ring_ |
| [internal::kMaxEmptySlotSpanRingSize] PA_GUARDED_BY( |
| internal::PartitionRootLock(this)) = {}; |
| int16_t global_empty_slot_span_ring_index_ |
| PA_GUARDED_BY(internal::PartitionRootLock(this)) = 0; |
| int16_t global_empty_slot_span_ring_size_ |
| PA_GUARDED_BY(internal::PartitionRootLock(this)) = |
| internal::kDefaultEmptySlotSpanRingSize; |
| static_assert(BucketIndexLookup::kNumBuckets < |
| std::numeric_limits<uint16_t>::max()); |
| |
| // Integrity check = ~reinterpret_cast<uintptr_t>(this). |
| uintptr_t inverted_self_ = 0; |
| |
| // A lock which is hold during thread cache construction. |
| // Any (de)allocation code path should not try to `Acquire()` this lock to |
| // prevent deadlocks. Instead, `TryAcquire()`. |
| internal::Lock thread_cache_construction_lock_; |
| |
| size_t scheduler_loop_quarantine_branch_capacity_in_bytes_ = 0; |
| internal::SchedulerLoopQuarantineRoot scheduler_loop_quarantine_root_; |
| internal::GlobalSchedulerLoopQuarantineBranch scheduler_loop_quarantine_; |
| internal::GlobalSchedulerLoopQuarantineBranch |
| scheduler_loop_quarantine_for_advanced_memory_safety_checks_; |
| |
| static constexpr internal::base::TimeDelta kMaxPurgeDuration = |
| internal::base::Milliseconds(2); |
| // Not overriding the global one to only change it for this partition. |
| internal::base::TimeTicks (*now_maybe_overridden_for_testing_)() = |
| internal::base::TimeTicks::Now; |
| |
| PartitionRoot(); |
| explicit PartitionRoot(PartitionOptions opts); |
| |
| // TODO(tasak): remove ~PartitionRoot() after confirming all tests |
| // don't need ~PartitionRoot(). |
| ~PartitionRoot(); |
| |
| // This will unreserve any space in the pool that the PartitionRoot is |
| // using. This is needed because many tests create and destroy many |
| // PartitionRoots over the lifetime of a process, which can exhaust the |
| // pool and cause tests to fail. |
| void DestructForTesting() |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| |
| void DecommitEmptySlotSpansForTesting(); |
| |
| // Public API |
| // |
| // Allocates out of the given bucket. Properly, this function should probably |
| // be in PartitionBucket, but because the implementation needs to be inlined |
| // for performance, and because it needs to inspect SlotSpanMetadata, |
| // it becomes impossible to have it in PartitionBucket as this causes a |
| // cyclical dependency on SlotSpanMetadata function implementations. |
| // |
| // Moving it a layer lower couples PartitionRoot and PartitionBucket, but |
| // preserves the layering of the includes. |
| void Init(PartitionOptions) PA_LOCKS_EXCLUDED(lock_); |
| |
| void EnableThreadCacheIfSupported() |
| PA_LOCKS_EXCLUDED(thread_cache_construction_lock_, lock_); |
| |
| PA_ALWAYS_INLINE static PartitionRoot* FromSlotSpanMetadata( |
| const SlotSpanMetadata* slot_span); |
| |
| // These two functions work unconditionally for normal buckets_. |
| // For direct map, they only work for the first super page of a reservation, |
| // (see partition_alloc_constants.h for the direct map allocation layout). |
| // In particular, the functions always work for a pointer to the start of a |
| // reservation. |
| PA_ALWAYS_INLINE static PartitionRoot* FromFirstSuperPage( |
| uintptr_t super_page); |
| PA_ALWAYS_INLINE static PartitionRoot* FromAddrInFirstSuperpage( |
| uintptr_t address); |
| |
| PA_ALWAYS_INLINE void DecreaseTotalSizeOfAllocatedBytes(uintptr_t slot_start, |
| size_t len); |
| PA_ALWAYS_INLINE void IncreaseTotalSizeOfAllocatedBytes(uintptr_t addr, |
| size_t len, |
| size_t raw_size); |
| PA_ALWAYS_INLINE void IncreaseCommittedPages(size_t len); |
| PA_ALWAYS_INLINE void DecreaseCommittedPages(size_t len); |
| PA_ALWAYS_INLINE void DecommitSystemPagesForData( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| PA_ALWAYS_INLINE void RecommitSystemPagesForData( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| |
| template <bool already_locked> |
| PA_ALWAYS_INLINE bool TryRecommitSystemPagesForDataInternal( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging); |
| |
| // TryRecommitSystemPagesForDataWithAcquiringLock() locks this root internally |
| // before invoking DecommitEmptySlotSpans(), which needs the lock. So the root |
| // must not be locked when invoking this method. |
| PA_ALWAYS_INLINE bool TryRecommitSystemPagesForDataWithAcquiringLock( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) |
| PA_LOCKS_EXCLUDED(internal::PartitionRootLock(this)); |
| |
| // TryRecommitSystemPagesForDataLocked() doesn't lock this root internally |
| // before invoking DecommitEmptySlotSpans(), which needs the lock. So the root |
| // must have been already locked when invoking this method. |
| PA_ALWAYS_INLINE bool TryRecommitSystemPagesForDataLocked( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| |
| [[noreturn]] PA_NOINLINE void OutOfMemory(size_t size); |
| |
| // Returns a pointer aligned on |alignment|, or nullptr. |
| // |
| // |alignment| has to be a power of two and a multiple of sizeof(void*) (as in |
| // posix_memalign() for POSIX systems). The returned pointer may include |
| // padding, and can be passed to |Free()| later. |
| // |
| // NOTE: This is incompatible with anything that adds extras before the |
| // returned pointer, such as in-slot metadata. |
| template <AllocFlags flags = AllocFlags::kNone> |
| PA_NOINLINE void* AlignedAlloc(size_t alignment, size_t requested_size) { |
| return AlignedAllocInline<flags>(alignment, requested_size); |
| } |
| PA_ALWAYS_INLINE size_t GetAdjustedSizeForAlignment(size_t alignment, |
| size_t requested_size); |
| template <AllocFlags flags = AllocFlags::kNone> |
| PA_ALWAYS_INLINE void* AlignedAllocInline(size_t alignment, |
| size_t requested_size); |
| |
| // PartitionAlloc supports multiple partitions, and hence multiple callers to |
| // these functions. Setting PA_ALWAYS_INLINE bloats code, and can be |
| // detrimental to performance, for instance if multiple callers are hot (by |
| // increasing cache footprint). Set PA_NOINLINE on the "basic" top-level |
| // functions to mitigate that for "vanilla" callers. |
| // |
| // |type_name == nullptr|: ONLY FOR TESTS except internal uses. |
| // You should provide |type_name| to make debugging easier. |
| template <AllocFlags flags = AllocFlags::kNone> |
| PA_NOINLINE PA_MALLOC_FN void* Alloc(size_t requested_size, |
| const char* type_name = nullptr) { |
| return AllocInline<flags>(requested_size, type_name); |
| } |
| template <AllocFlags flags = AllocFlags::kNone> |
| PA_ALWAYS_INLINE PA_MALLOC_FN void* AllocInline( |
| size_t requested_size, |
| const char* type_name = nullptr) { |
| return AllocInternal<flags>(requested_size, internal::PartitionPageSize(), |
| type_name); |
| } |
| |
| // AllocInternal exposed for testing. |
| template <AllocFlags flags = AllocFlags::kNone> |
| PA_NOINLINE PA_MALLOC_FN void* AllocInternalForTesting( |
| size_t requested_size, |
| size_t slot_span_alignment, |
| const char* type_name) { |
| return AllocInternal<flags>(requested_size, slot_span_alignment, type_name); |
| } |
| |
| template <AllocFlags alloc_flags = AllocFlags::kNone, |
| FreeFlags free_flags = FreeFlags::kNone> |
| PA_NOINLINE void* Realloc(void* ptr, size_t new_size, const char* type_name) { |
| return ReallocInline<alloc_flags, free_flags>(ptr, new_size, type_name); |
| } |
| template <AllocFlags alloc_flags = AllocFlags::kNone, |
| FreeFlags free_flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE void* ReallocInline(void* ptr, |
| size_t new_size, |
| const char* type_name); |
| |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_NOINLINE void Free(void* object) { |
| FreeInline<flags>(object); |
| } |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_NOINLINE void FreeWithSize(void* object, size_t size) { |
| FreeWithSizeInline<flags>(object, size); |
| } |
| |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_NOINLINE void AlignedFree(void* object) { |
| // Normally kAlignedFree is a no-op call into Free, but with memory tools it |
| // will instead remap to the appropriate system aligned free call. |
| constexpr FreeFlags kMaybeAlignedFreeForMemoryTool = |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| FreeFlags::kAlignedFreeForMemoryTool; |
| #else |
| FreeFlags::kNone; |
| #endif // defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| FreeInline<flags | kMaybeAlignedFreeForMemoryTool>(object); |
| } |
| |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE void FreeInline(void* object); |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE void FreeWithSizeInline(void* object, size_t size); |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE void FreeWithSizeAndAlignmentInline(void* object, |
| size_t size, |
| size_t alignment); |
| // |object| must be a non-null pointer. |
| PA_ALWAYS_INLINE std::pair<internal::SlotStart, internal::SlotSpanMetadata*> |
| GetSlotStartAndSlotSpanFromAddress(void* object); |
| |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_NOINLINE static void FreeInUnknownRoot(void* object) { |
| FreeInlineInUnknownRoot<flags>(object); |
| } |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE static void FreeInlineInUnknownRoot(void* object); |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE static void FreeWithSizeInlineInUnknownRoot(void* object, |
| size_t size); |
| template <FreeFlags flags = FreeFlags::kNone> |
| PA_ALWAYS_INLINE static void FreeWithSizeAndAlignmentInlineInUnknownRoot( |
| void* object, |
| size_t size, |
| size_t alignment); |
| // |object| must be a non-null pointer. |
| PA_ALWAYS_INLINE static PartitionRoot* GetRootFromAddressInFirstSuperpage( |
| void* object); |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void FreeNoHooksImmediate(internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span); |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void FreeWithSizeNoHooksImmediate( |
| internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span, |
| size_t size); |
| // Immediately frees the pointer bypassing the quarantine. `slot_start` is the |
| // beginning of the slot that contains `object`. |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void FreeNoHooksImmediateInternal( |
| internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span, |
| const internal::BucketSizeDetails& size_details); |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| // Actual free operation on BRP dequarantine. |
| PA_ALWAYS_INLINE static void FreeAfterBRPQuarantine( |
| internal::UntaggedSlotStart slot_start, |
| size_t slot_size); |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| PA_ALWAYS_INLINE size_t GetSlotUsableSize(const SlotSpanMetadata* slot_span) { |
| return AdjustSizeForExtrasSubtract(slot_span->GetUtilizedSlotSize()); |
| } |
| |
| // This function attempts to compute the slot_span's usable size without |
| // touching `slot_span`, but if it fails it will fall back on |
| // GetSlotUsableSize(slot_span). |
| PA_ALWAYS_INLINE size_t |
| GetSlotUsableSize(const internal::BucketSizeDetails& size_details, |
| SlotSpanMetadata* slot_span); |
| |
| PA_ALWAYS_INLINE static size_t GetUsableSize(const void* ptr); |
| |
| PA_ALWAYS_INLINE PageAccessibilityConfiguration |
| GetPageAccessibility(bool request_tagging) const; |
| PA_ALWAYS_INLINE PageAccessibilityConfiguration |
| PageAccessibilityWithThreadIsolationIfEnabled( |
| PageAccessibilityConfiguration::Permissions) const; |
| |
| PA_ALWAYS_INLINE size_t |
| AllocationCapacityFromSlotStart(internal::UntaggedSlotStart slot_start) const; |
| PA_ALWAYS_INLINE size_t |
| AllocationCapacityFromRequestedSize(size_t size) const; |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| PA_ALWAYS_INLINE static internal::InSlotMetadata* |
| InSlotMetadataPointerFromSlotStartAndSize( |
| internal::UntaggedSlotStart slot_start, |
| size_t slot_size); |
| PA_ALWAYS_INLINE internal::InSlotMetadata* |
| InSlotMetadataPointerFromObjectForTesting(void* object) const; |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| PA_ALWAYS_INLINE bool IsMemoryTaggingEnabled() const; |
| PA_ALWAYS_INLINE bool UseRandomMemoryTagging() const; |
| PA_ALWAYS_INLINE TagViolationReportingMode |
| memory_tagging_reporting_mode() const; |
| |
| // Frees memory from this partition, if possible, by decommitting pages or |
| // even entire slot spans. `flags` is an OR of base::PartitionPurgeFlags. |
| // Caller is responsible to persist `purge_state` when calling this |
| // periodically. |
| // For single-time use, prefer one-param version. |
| void PurgeMemory(int flags, PurgeState& purge_state); |
| PA_ALWAYS_INLINE void PurgeMemory(int flags) { |
| PurgeState purge_state; |
| PurgeMemory(flags, purge_state); |
| } |
| |
| // Reduces the size of the empty slot spans ring, until the dirty size is <= |
| // |limit|. |
| void ShrinkEmptySlotSpansRing(size_t limit) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| |
| void DumpStats(const char* partition_name, |
| bool is_light_dump, |
| bool populate_discardable_bytes, |
| PartitionStatsDumper* partition_stats_dumper); |
| |
| static void DeleteForTesting(PartitionRoot* partition_root); |
| void ResetForTesting(bool allow_leaks); |
| void ResetBookkeepingForTesting(); |
| void SetGlobalEmptySlotSpanRingIndexForTesting(int16_t index); |
| |
| PA_ALWAYS_INLINE BucketDistribution GetBucketDistribution() const { |
| return settings_.bucket_distribution; |
| } |
| |
| static uint16_t SizeToBucketIndex(size_t size, |
| BucketDistribution bucket_distribution); |
| |
| PA_ALWAYS_INLINE internal::BucketSizeDetails SlotSpanToBucketSizeDetails( |
| SlotSpanMetadata* slot_span) const; |
| |
| PA_ALWAYS_INLINE internal::BucketSizeDetails SizeToBucketSizeDetails( |
| size_t requested_size, |
| SlotSpanMetadata* slot_span) const; |
| |
| PA_ALWAYS_INLINE void FreeInSlotSpan(internal::UntaggedSlotStart slot_start, |
| SlotSpanMetadata* slot_span) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| |
| // Frees memory, with |slot_start| as returned by |RawAlloc()|. |
| PA_ALWAYS_INLINE void RawFree(internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span) |
| PA_LOCKS_EXCLUDED(internal::PartitionRootLock(this)); |
| |
| PA_ALWAYS_INLINE void RawFreeWithThreadCache( |
| internal::SlotStart slot_start, |
| const internal::BucketSizeDetails& size_details, |
| SlotSpanMetadata* slot_span); |
| |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| // Sets a new MTE tag on the slot. This must not be called when an object |
| // enters BRP quarantine because it might cause a race with |raw_ptr|'s |
| // ref-count decrement. (crbug.com/357526108) |
| PA_ALWAYS_INLINE void RetagSlotIfNeeded( |
| internal::UntaggedSlotStart slot_start_ptr, |
| size_t slot_size); |
| #endif |
| |
| // This is safe to do because we are switching to a bucket distribution with |
| // more buckets_, meaning any allocations we have done before the switch are |
| // guaranteed to have a bucket under the new distribution when they are |
| // eventually deallocated. We do not need synchronization here. |
| void SwitchToDenserBucketDistribution() { |
| settings_.bucket_distribution = BucketDistribution::kDenser; |
| } |
| // Switching back to the less dense bucket distribution is ok during tests. |
| // At worst, we end up with deallocations that are sent to a bucket that we |
| // cannot allocate from, which will not cause problems besides wasting |
| // memory. |
| void ResetBucketDistributionForTesting() { |
| settings_.bucket_distribution = BucketDistribution::kNeutral; |
| } |
| |
| ThreadCache* thread_cache_for_testing() const { |
| return settings_.with_thread_cache |
| ? ThreadCache::Get(settings_.thread_cache_index) |
| : nullptr; |
| } |
| size_t get_total_size_of_committed_pages() const { |
| return total_size_of_committed_pages_.load(std::memory_order_relaxed); |
| } |
| size_t get_max_size_of_committed_pages() const { |
| return max_size_of_committed_pages_.load(std::memory_order_relaxed); |
| } |
| |
| size_t get_total_size_of_allocated_bytes() const { |
| // Since this is only used for bookkeeping, we don't care if the value is |
| // stale, so no need to get a lock here. |
| return total_size_of_allocated_bytes_.load(std::memory_order_relaxed); |
| } |
| |
| size_t get_max_size_of_allocated_bytes() const { |
| // Since this is only used for bookkeeping, we don't care if the value is |
| // stale, so no need to get a lock here. |
| return max_size_of_allocated_bytes_.load(std::memory_order_relaxed); |
| } |
| |
| internal::pool_handle ChoosePool() const { return settings_.pool_handle; } |
| #if PA_BUILDFLAG(HAS_64_BIT_POINTERS) |
| PA_ALWAYS_INLINE const internal::PoolOffsetLookup& GetOffsetLookup() const { |
| return settings_.offset_lookup; |
| } |
| #endif // PA_BUILDFLAG(HAS_64_BIT_POINTERS) |
| PA_ALWAYS_INLINE const internal::ReservationOffsetTable& |
| GetReservationOffsetTable() const { |
| return settings_.reservation_offset_table; |
| } |
| |
| PA_ALWAYS_INLINE static PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR size_t |
| GetDirectMapMetadataAndGuardPagesSize() { |
| // Because we need to fake a direct-map region to look like a super page, we |
| // need to allocate more pages around the payload: |
| // - The first partition page is a combination of metadata and guard region. |
| // - We also add a trailing guard page. In most cases, a system page would |
| // suffice. But on 32-bit systems when BRP is on, we need a partition page |
| // to match granularity of the BRP pool bitmap. For cosistency, we'll use |
| // a partition page everywhere, which is cheap as it's uncommitted address |
| // space anyway. |
| return 2 * internal::PartitionPageSize(); |
| } |
| |
| PA_ALWAYS_INLINE static PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR size_t |
| GetDirectMapSlotSize(size_t raw_size) { |
| // Caller must check that the size is not above the MaxDirectMapped() |
| // limit before calling. This also guards against integer overflow in the |
| // calculation here. |
| PA_DCHECK(raw_size <= internal::MaxDirectMapped()); |
| return partition_alloc::internal::base::bits::AlignUp( |
| raw_size, internal::SystemPageSize()); |
| } |
| |
| PA_ALWAYS_INLINE static size_t GetDirectMapReservationSize( |
| size_t padded_raw_size) { |
| // Caller must check that the size is not above the MaxDirectMapped() |
| // limit before calling. This also guards against integer overflow in the |
| // calculation here. |
| PA_DCHECK(padded_raw_size <= internal::MaxDirectMapped()); |
| return partition_alloc::internal::base::bits::AlignUp( |
| padded_raw_size + GetDirectMapMetadataAndGuardPagesSize(), |
| internal::DirectMapAllocationGranularity()); |
| } |
| |
| PA_ALWAYS_INLINE bool IsDirectMapped( |
| partition_alloc::internal::SlotSpanMetadata* slot_span) const { |
| return IsDirectMappedBucket(slot_span->bucket); |
| } |
| |
| PA_ALWAYS_INLINE size_t AdjustSize0IfNeeded(size_t size) const { |
| // On macOS and iOS, PartitionGetSizeEstimate() is used for two purposes: |
| // as a zone dispatcher and as an underlying implementation of |
| // malloc_size(3). As a zone dispatcher, zero has a special meaning of |
| // "doesn't belong to this zone". When extras fill out the entire slot, |
| // the usable size is 0, thus confusing the zone dispatcher. |
| // |
| // To save ourselves a branch on this hot path, we could eliminate this |
| // check at compile time for cases not listed above. The #if statement would |
| // be rather complex. Then there is also the fear of the unknown. The |
| // existing cases were discovered through obscure, painful-to-debug crashes. |
| // Better save ourselves trouble with not-yet-discovered cases. |
| if (size == 0) [[unlikely]] { |
| return 1; |
| } |
| return size; |
| } |
| |
| // Adjusts the size by adding extras. Also include the 0->1 adjustment if |
| // needed. |
| PA_ALWAYS_INLINE size_t AdjustSizeForExtrasAdd(size_t size) const { |
| size = AdjustSize0IfNeeded(size); |
| PA_DCHECK(size + settings_.extras_size >= size); |
| return size + settings_.extras_size; |
| } |
| |
| // Adjusts the size by subtracing extras. Doesn't include the 0->1 adjustment, |
| // which leads to an asymmetry with AdjustSizeForExtrasAdd, but callers of |
| // AdjustSizeForExtrasSubtract either expect the adjustment to be included, or |
| // are indifferent. |
| PA_ALWAYS_INLINE size_t AdjustSizeForExtrasSubtract(size_t size) const { |
| return size - settings_.extras_size; |
| } |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| bool brp_enabled() const { return settings_.brp_enabled_; } |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| // When a SlotSpan becomes empty, the allocator tries to avoid reusing it |
| // immediately, to help with fragmentation. At this point, it becomes dirty |
| // committed memory, which we want to minimize. This could be decommitted |
| // immediately, but that would imply doing a lot of system calls. In |
| // particular, for single-slot SlotSpans, a malloc() / free() loop would cause |
| // a *lot* of system calls. |
| // |
| // As an intermediate step, empty SlotSpans are placed into a per-partition |
| // global ring buffer, giving the newly-empty SlotSpan a chance to be reused |
| // before getting decommitted. A new entry (i.e. a newly empty SlotSpan) |
| // taking the place used by a previous one will lead the previous SlotSpan to |
| // be decommitted immediately, provided that it is still empty. |
| // |
| // Increasing the ring size means giving more time for reuse to happen, at the |
| // cost of possibly increasing peak committed memory usage (and increasing the |
| // size of PartitionRoot a bit, since the ring buffer is there). Note that the |
| // ring buffer doesn't necessarily contain an empty SlotSpan, as SlotSpans are |
| // *not* removed from it when reused. So the ring buffer really is a buffer |
| // of *possibly* empty SlotSpans. |
| // |
| // In all cases, PartitionRoot::PurgeMemory() with the |
| // PurgeFlags::kDecommitEmptySlotSpans flag will eagerly decommit all entries |
| // in the ring buffer, so with periodic purge enabled, this typically happens |
| // every few seconds. |
| void AdjustSlotSpanRing(int16_t ring_size, int dirty_bytes_shift) { |
| max_empty_slot_spans_dirty_bytes_shift_ = dirty_bytes_shift; |
| // ShrinkEmptySlotSpansRing() will iterate through |
| // kMaxEmptySlotSpanRingSize, so no need to free empty pages now. |
| ::partition_alloc::internal::ScopedGuard guard{ |
| internal::PartitionRootLock(this)}; |
| global_empty_slot_span_ring_size_ = ring_size; |
| if (global_empty_slot_span_ring_index_ >= ring_size) { |
| global_empty_slot_span_ring_index_ = 0; |
| } |
| } |
| |
| // To make tests deterministic, it is necessary to uncap the amount of memory |
| // waste incurred by empty slot spans. Otherwise, the size of various |
| // freelists, and committed memory becomes harder to reason about (and |
| // brittle) with a single thread, and non-deterministic with several. |
| void UncapEmptySlotSpanMemoryForTesting() { |
| max_empty_slot_spans_dirty_bytes_shift_ = 0; |
| } |
| |
| // Enables/disables the free list straightening for larger slot spans in |
| // PurgeMemory(). |
| static void SetStraightenLargerSlotSpanFreeListsMode( |
| StraightenLargerSlotSpanFreeListsMode new_value); |
| // Enables/disables the free list sorting for smaller slot spans in |
| // PurgeMemory(). |
| static void SetSortSmallerSlotSpanFreeListsEnabled(bool new_value); |
| // Enables/disables the sorting of active slot spans in PurgeMemory(). |
| static void SetSortActiveSlotSpansEnabled(bool new_value); |
| |
| static StraightenLargerSlotSpanFreeListsMode |
| GetStraightenLargerSlotSpanFreeListsMode() { |
| return straighten_larger_slot_span_free_lists_; |
| } |
| |
| void ReconfigureSchedulerLoopQuarantineForCurrentThread( |
| const internal::SchedulerLoopQuarantineConfig& config) { |
| ThreadCache* thread_cache = this->EnsureThreadCache(); |
| PA_CHECK(ThreadCache::IsValid(thread_cache)); |
| thread_cache->GetSchedulerLoopQuarantineBranch().Configure( |
| scheduler_loop_quarantine_root_, config); |
| } |
| |
| #if PA_CONFIG(MOVE_METADATA_OUT_OF_GIGACAGE) |
| PA_ALWAYS_INLINE std::ptrdiff_t MetadataOffset() const { |
| return settings_.metadata_offset_; |
| } |
| #else |
| PA_ALWAYS_INLINE |
| PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR |
| size_t MetadataOffset() const { return internal::SystemPageSize(); } |
| #endif // PA_CONFIG(MOVE_METADATA_OUT_OF_GIGACAGE) |
| |
| PA_NOINLINE static void CheckMetadataIntegrity(const void* object); |
| |
| private: |
| static inline StraightenLargerSlotSpanFreeListsMode |
| straighten_larger_slot_span_free_lists_ = |
| StraightenLargerSlotSpanFreeListsMode::kOnlyWhenUnprovisioning; |
| static inline bool sort_smaller_slot_span_free_lists_ = true; |
| static inline bool sort_active_slot_spans_ = false; |
| |
| // Common path of Free() and FreeInUnknownRoot(). Returns |
| // true if the caller should return immediately. |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE static bool FreeProlog(void* object, |
| const PartitionRoot* root); |
| |
| // |buckets_| has `BucketIndexLookup::kNumBuckets` elements, but we |
| // sometimes access it at index `BucketIndexLookup::kNumBuckets`, which is |
| // occupied by the sentinel bucket. The correct layout is enforced by a |
| // static_assert() in partition_root.cc, so this is fine. However, UBSAN is |
| // correctly pointing out that there is an out-of-bounds access, so disable it |
| // for these accesses. |
| // |
| // See crbug.com/1150772 for an instance of Clusterfuzz / UBSAN detecting |
| // this. |
| PA_NO_SANITIZE("undefined") |
| PA_ALWAYS_INLINE const Bucket& bucket_at(size_t i) const { |
| PA_DCHECK(i <= BucketIndexLookup::kNumBuckets); |
| return PA_UNSAFE_TODO(buckets_[i]); |
| } |
| |
| // Returns whether a |bucket| from |this| root is direct-mapped. This function |
| // does not touch |bucket|, contrary to PartitionBucket::is_direct_mapped(). |
| // |
| // This is meant to be used in hot paths, and particularly *before* going into |
| // the thread cache fast path. Indeed, real-world profiles show that accessing |
| // an allocation's bucket is responsible for a sizable fraction of *total* |
| // deallocation time. This can be understood because |
| // - All deallocations have to access the bucket to know whether it is |
| // direct-mapped. If not (vast majority of allocations), it can go through |
| // the fast path, i.e. thread cache. |
| // - The bucket is relatively frequently written to, by *all* threads |
| // (e.g. every time a slot span becomes full or empty), so accessing it will |
| // result in some amount of cacheline ping-pong. |
| PA_ALWAYS_INLINE bool IsDirectMappedBucket(const Bucket* bucket) const { |
| // All regular allocations are associated with a bucket in the |buckets_| |
| // array. A range check is then sufficient to identify direct-mapped |
| // allocations. |
| bool ret = !(bucket >= this->buckets_ && bucket <= &this->sentinel_bucket_); |
| PA_DCHECK(ret == bucket->is_direct_mapped()); |
| return ret; |
| } |
| |
| // Same as |Alloc()|, but allows specifying |slot_span_alignment|. It |
| // has to be a multiple of partition page size, greater than 0 and no greater |
| // than kMaxSupportedAlignment. If it equals exactly 1 partition page, no |
| // special action is taken as PartitionAlloc naturally guarantees this |
| // alignment, otherwise a sub-optimal allocation strategy is used to |
| // guarantee the higher-order alignment. |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE PA_MALLOC_FN void* AllocInternal(size_t requested_size, |
| size_t slot_span_alignment, |
| const char* type_name); |
| |
| // Same as |AllocInternal()|, but don't handle allocation hooks. |
| template <AllocFlags flags = AllocFlags::kNone> |
| PA_ALWAYS_INLINE PA_MALLOC_FN void* AllocInternalNoHooks( |
| size_t requested_size, |
| size_t slot_span_alignment); |
| // Allocates a memory slot, without initializing extras. |
| // |
| // - |flags| are as in Alloc(). |
| // - |raw_size| accommodates for extras on top of Alloc()'s |
| // |requested_size|. |
| // - |usable_size|, |slot_size| and |is_already_zeroed| are output only. |
| // Note, |usable_size| is guaranteed to be no smaller than Alloc()'s |
| // |requested_size|, and no larger than |slot_size|. |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE internal::UntaggedSlotStart RawAlloc( |
| Bucket* bucket, |
| size_t raw_size, |
| size_t slot_span_alignment, |
| size_t* usable_size, |
| size_t* slot_size, |
| bool* is_already_zeroed); |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE internal::UntaggedSlotStart AllocFromBucket( |
| Bucket* bucket, |
| size_t raw_size, |
| size_t slot_span_alignment, |
| size_t* usable_size, |
| size_t* slot_size, |
| bool* is_already_zeroed) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| |
| // We use this to make MEMORY_TOOL_REPLACES_ALLOCATOR behave the same for max |
| // size as other alloc code. |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE static bool AllocWithMemoryToolProlog(size_t size) { |
| if (size > partition_alloc::internal::MaxDirectMapped()) { |
| if constexpr (ContainsFlags(flags, AllocFlags::kReturnNull)) { |
| // Early return indicating not to proceed with allocation |
| return false; |
| } |
| PA_CHECK(false); |
| } |
| return true; // Allocation should proceed |
| } |
| |
| bool TryReallocInPlaceForNormalBuckets(void* object, |
| SlotSpanMetadata* slot_span, |
| size_t new_size) |
| PA_LOCKS_EXCLUDED(thread_cache_construction_lock_); |
| bool TryReallocInPlaceForDirectMap(SlotSpanMetadata* slot_span, |
| size_t requested_size) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| void DecommitEmptySlotSpans() |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| PA_ALWAYS_INLINE void RawFreeLocked(internal::UntaggedSlotStart slot_start, |
| SlotSpanMetadata* slot_span) |
| PA_EXCLUSIVE_LOCKS_REQUIRED(internal::PartitionRootLock(this)); |
| ThreadCache* MaybeInitThreadCache() |
| PA_LOCKS_EXCLUDED(thread_cache_construction_lock_); |
| ThreadCache* ForceInitThreadCache() |
| PA_LOCKS_EXCLUDED(thread_cache_construction_lock_); |
| |
| // May return an invalid thread cache. |
| PA_ALWAYS_INLINE ThreadCache* GetOrCreateThreadCache(); |
| PA_ALWAYS_INLINE ThreadCache* GetThreadCache(); |
| // Similar to `GetOrCreateThreadCache()`, but this creates a new thread cache |
| // with `ForceInitThreadCache()`. This can be slow since it acquires a lock, |
| // and hence with a risk of deadlock. |
| // Must NOT be used inside (de)allocation code path. |
| PA_ALWAYS_INLINE ThreadCache* EnsureThreadCache(); |
| |
| PA_ALWAYS_INLINE internal::SchedulerLoopQuarantineRoot& |
| GetSchedulerLoopQuarantineRoot(); |
| |
| PA_ALWAYS_INLINE AllocationNotificationData |
| CreateAllocationNotificationData(void* object, |
| size_t size, |
| const char* type_name) const; |
| PA_ALWAYS_INLINE static FreeNotificationData |
| CreateDefaultFreeNotificationData(void* address); |
| PA_ALWAYS_INLINE FreeNotificationData |
| CreateFreeNotificationData(void* address) const; |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| PA_NOINLINE void QuarantineForBrp(const SlotSpanMetadata* slot_span, |
| internal::SlotStart slot_start); |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| #if PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| static internal::Lock& GetEnumeratorLock(); |
| |
| PartitionRoot* PA_GUARDED_BY(GetEnumeratorLock()) next_root = nullptr; |
| PartitionRoot* PA_GUARDED_BY(GetEnumeratorLock()) prev_root = nullptr; |
| |
| friend class internal::PartitionRootEnumerator; |
| #endif // PA_CONFIG(USE_PARTITION_ROOT_ENUMERATOR) |
| |
| std::atomic<uint64_t> intended_leak_size_; |
| |
| friend class ThreadCache; |
| friend class internal::BatchFreeQueue; |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| friend class internal::InSlotMetadata; |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| template <bool> |
| friend class internal::SchedulerLoopQuarantineBranch; |
| }; |
| |
| namespace internal { |
| |
| PA_ALWAYS_INLINE ::partition_alloc::internal::Lock& PartitionRootLock( |
| PartitionRoot* root) { |
| return root->lock_; |
| } |
| |
| class ScopedSyscallTimer { |
| public: |
| #if PA_CONFIG(COUNT_SYSCALL_TIME) |
| explicit ScopedSyscallTimer(PartitionRoot* root) |
| : root_(root), tick_(base::TimeTicks::Now()) {} |
| |
| ~ScopedSyscallTimer() { |
| root_->syscall_count_.fetch_add(1, std::memory_order_relaxed); |
| |
| int64_t elapsed_nanos = (base::TimeTicks::Now() - tick_).InNanoseconds(); |
| if (elapsed_nanos > 0) { |
| root_->syscall_total_time_ns_.fetch_add( |
| static_cast<uint64_t>(elapsed_nanos), std::memory_order_relaxed); |
| } |
| } |
| |
| private: |
| PartitionRoot* root_; |
| const base::TimeTicks tick_; |
| #else |
| explicit ScopedSyscallTimer(PartitionRoot* root) { |
| root->syscall_count_.fetch_add(1, std::memory_order_relaxed); |
| } |
| #endif |
| }; |
| |
| |
| } // namespace internal |
| |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE internal::UntaggedSlotStart PartitionRoot::AllocFromBucket( |
| Bucket* bucket, |
| size_t raw_size, |
| size_t slot_span_alignment, |
| size_t* usable_size, |
| size_t* slot_size, |
| bool* is_already_zeroed) { |
| PA_DCHECK((slot_span_alignment >= internal::PartitionPageSize()) && |
| internal::base::bits::HasSingleBit(slot_span_alignment)); |
| SlotSpanMetadata* slot_span = bucket->active_slot_spans_head; |
| // There always must be a slot span on the active list (could be a sentinel). |
| PA_DCHECK(slot_span); |
| // Check that it isn't marked full, which could only be true if the span was |
| // removed from the active list. |
| PA_DCHECK(!slot_span->marked_full); |
| |
| // Should check validity, but later in this function. |
| internal::UntaggedSlotStart slot_start = |
| internal::SlotStart::Unchecked(slot_span->get_freelist_head()).Untag(); |
| |
| // Use the fast path when a slot is readily available on the free list of the |
| // first active slot span. However, fall back to the slow path if a |
| // higher-order alignment is requested, because an inner slot of an existing |
| // slot span is unlikely to satisfy it. |
| if (slot_span_alignment <= internal::PartitionPageSize() && |
| slot_start.value()) [[likely]] { |
| *is_already_zeroed = false; |
| // This is a fast path, avoid calling GetSlotUsableSize() in Release builds |
| // as it is costlier. Copy its small bucket path instead. |
| *usable_size = AdjustSizeForExtrasSubtract(bucket->slot_size); |
| PA_DCHECK(*usable_size == GetSlotUsableSize(slot_span)); |
| |
| // If these DCHECKs fire, you probably corrupted memory. |
| PA_CHECK(DeducedRootIsValid(slot_span)); |
| |
| // All large allocations must go through the slow path to correctly update |
| // the size metadata. |
| PA_DCHECK(!slot_span->CanStoreRawSize()); |
| PA_DCHECK(!slot_span->bucket->is_direct_mapped()); |
| |
| void* entry = slot_span->PopForAlloc(bucket->slot_size); |
| |
| PA_DCHECK(internal::SlotStart::Unchecked(entry).Untag() == slot_start); |
| |
| PA_DCHECK(slot_span->bucket == bucket); |
| } else { |
| // Should check validity, but later in this function. |
| slot_start = internal::UntaggedSlotStart::Unchecked( |
| bucket->SlowPathAlloc(this, flags, raw_size, slot_span_alignment, |
| &slot_span, is_already_zeroed)); |
| if (!slot_start) [[unlikely]] { |
| return internal::UntaggedSlotStart(); |
| } |
| PA_DCHECK(slot_span == SlotSpanMetadata::FromSlotStart(slot_start, this)); |
| PA_CHECK(DeducedRootIsValid(slot_span)); |
| // For direct mapped allocations, |bucket| is the sentinel. |
| PA_DCHECK((slot_span->bucket == bucket) || |
| (slot_span->bucket->is_direct_mapped() && |
| (bucket == &sentinel_bucket_))); |
| |
| *usable_size = GetSlotUsableSize(slot_span); |
| } |
| |
| slot_start.Check(this); |
| |
| *slot_size = slot_span->bucket->slot_size; |
| return slot_start; |
| } |
| |
| AllocationNotificationData PartitionRoot::CreateAllocationNotificationData( |
| void* object, |
| size_t size, |
| const char* type_name) const { |
| AllocationNotificationData notification_data(object, size, type_name); |
| |
| if (IsMemoryTaggingEnabled()) { |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| notification_data.SetMteReportingMode(memory_tagging_reporting_mode()); |
| #endif |
| } |
| |
| return notification_data; |
| } |
| |
| FreeNotificationData PartitionRoot::CreateDefaultFreeNotificationData( |
| void* address) { |
| return FreeNotificationData(address); |
| } |
| |
| FreeNotificationData PartitionRoot::CreateFreeNotificationData( |
| void* address) const { |
| FreeNotificationData notification_data = |
| CreateDefaultFreeNotificationData(address); |
| |
| if (IsMemoryTaggingEnabled()) { |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| notification_data.SetMteReportingMode(memory_tagging_reporting_mode()); |
| #endif |
| } |
| |
| return notification_data; |
| } |
| |
| // static |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE bool PartitionRoot::FreeProlog(void* object, |
| const PartitionRoot* root) { |
| static_assert(AreValidFlags(flags)); |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| if constexpr (!ContainsFlags(flags, FreeFlags::kNoMemoryToolOverride)) { |
| #if PA_BUILDFLAG(PA_COMPILER_MSVC) |
| if (ContainsFlags(flags, FreeFlags::kAlignedFreeForMemoryTool)) { |
| _aligned_free(object); |
| } else { |
| free(object); |
| } |
| #else // !PA_BUILDFLAG(PA_COMPILER_MSVC) |
| free(object); |
| #endif // PA_BUILDFLAG(PA_COMPILER_MSVC) |
| return true; |
| } |
| #else // !defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| // If the memory tool is not replacing the allocator, then the |
| // kAlignedFreeForMemoryTool flag is unused and should not be passed. |
| static_assert(!ContainsFlags(flags, FreeFlags::kAlignedFreeForMemoryTool)); |
| #endif // defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| |
| if (!object) [[unlikely]] { |
| return true; |
| } |
| |
| if constexpr (ContainsFlags(flags, FreeFlags::kNoHooks)) { |
| return false; |
| } |
| |
| if (PartitionAllocHooks::AreHooksEnabled()) { |
| // A valid |root| might not be available if this function is called from |
| // |FreeInUnknownRoot| and not deducible if object originates from |
| // an override hook. |
| // TODO(crbug.com/40152647): See if we can make the root available more |
| // reliably or even make this function non-static. |
| auto notification_data = root ? root->CreateFreeNotificationData(object) |
| : CreateDefaultFreeNotificationData(object); |
| PartitionAllocHooks::FreeObserverHookIfEnabled(notification_data); |
| if (PartitionAllocHooks::FreeOverrideHookIfEnabled(object)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| PA_ALWAYS_INLINE bool PartitionRoot::IsMemoryTaggingEnabled() const { |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| return settings_.memory_tagging_enabled_; |
| #else |
| return false; |
| #endif // PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| } |
| |
| PA_ALWAYS_INLINE bool PartitionRoot::UseRandomMemoryTagging() const { |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| return settings_.use_random_memory_tagging_; |
| #else |
| return false; |
| #endif // PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| } |
| |
| PA_ALWAYS_INLINE TagViolationReportingMode |
| PartitionRoot::memory_tagging_reporting_mode() const { |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| return settings_.memory_tagging_reporting_mode_; |
| #else |
| return TagViolationReportingMode::kUndefined; |
| #endif // PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| } |
| |
| // static |
| PA_ALWAYS_INLINE PartitionRoot* |
| PartitionRoot::GetRootFromAddressInFirstSuperpage(void* object) { |
| PA_DCHECK(object); |
| // Fetch the root from the address, and not SlotSpanMetadata. This is |
| // important, as obtaining it from SlotSpanMetadata is a slow operation |
| // (looking into the metadata area, and following a pointer), which can induce |
| // cache coherency traffic (since they're read on every free(), and written to |
| // on any malloc()/free() that is not a hit in the thread cache). This way we |
| // change the critical path from object -> slot_span -> root into two |
| // *parallel* ones: |
| // 1. object -> root |
| // 2. object -> slot_span (inside FreeInline) |
| uintptr_t object_addr = |
| internal::SlotStart::Unchecked(object).Untag().value(); |
| return FromAddrInFirstSuperpage(object_addr); |
| } |
| |
| // static |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeInlineInUnknownRoot(void* object) { |
| bool early_return = FreeProlog<flags>(object, nullptr); |
| if (early_return) { |
| return; |
| } |
| // FreeProlog ensures the object is not nullptr. |
| PA_DCHECK(object); |
| |
| auto* root = GetRootFromAddressInFirstSuperpage(object); |
| root->FreeInline<flags | FreeFlags::kNoHooks>(object); |
| } |
| |
| // static |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeWithSizeInlineInUnknownRoot( |
| void* object, |
| size_t size) { |
| bool early_return = FreeProlog<flags>(object, nullptr); |
| if (early_return) { |
| return; |
| } |
| // FreeProlog ensures the object is not nullptr. |
| PA_DCHECK(object); |
| |
| auto* root = GetRootFromAddressInFirstSuperpage(object); |
| root->FreeWithSizeInline<flags | FreeFlags::kNoHooks>(object, size); |
| } |
| |
| // static |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void |
| PartitionRoot::FreeWithSizeAndAlignmentInlineInUnknownRoot(void* object, |
| size_t size, |
| size_t alignment) { |
| bool early_return = FreeProlog<flags>(object, nullptr); |
| if (early_return) { |
| return; |
| } |
| // FreeProlog ensures the object is not nullptr. |
| PA_DCHECK(object); |
| |
| auto* root = GetRootFromAddressInFirstSuperpage(object); |
| root->FreeWithSizeAndAlignmentInline<flags | FreeFlags::kNoHooks>( |
| object, size, alignment); |
| } |
| |
| PA_ALWAYS_INLINE std::pair<internal::SlotStart, internal::SlotSpanMetadata*> |
| PartitionRoot::GetSlotStartAndSlotSpanFromAddress(void* object) { |
| PA_DCHECK(object); |
| |
| // On some platforms, malloc() interception is fragile. For example, on |
| // Android, malloc() interception is more fragile than on other platforms, |
| // as we use wrapped symbols. However, the pools allow us to quickly tell |
| // that a pointer was allocated with PartitionAlloc. |
| // |
| // This is a crash to detect imperfect symbol interception. However, we can |
| // forward allocations we don't own to the system malloc() implementation in |
| // these rare cases, assuming that some remain. |
| // |
| // On platforms with ENABLE_SYSTEM_FREE_FALLBACK, this is already checked in |
| // PartitionFree() in the shim. |
| #if PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) && \ |
| !PA_BUILDFLAG(ENABLE_SYSTEM_FREE_FALLBACK) |
| uintptr_t object_addr = |
| internal::SlotStart::Unchecked(object).Untag().value(); |
| PA_CHECK(IsManagedByPartitionAlloc(object_addr)); |
| #endif |
| |
| internal::SlotStart slot_start = internal::SlotStart::Checked(object, this); |
| SlotSpanMetadata* slot_span = |
| SlotSpanMetadata::FromSlotStart(slot_start.Untag(), this); |
| PA_DCHECK(PartitionRoot::FromSlotSpanMetadata(slot_span) == this); |
| PA_DCHECK(slot_span == |
| SlotSpanMetadata::FromSlotStart(slot_start.Untag(), this)); |
| |
| return {slot_start, slot_span}; |
| } |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeInline(void* object) { |
| // The correct PartitionRoot might not be deducible if the |object| originates |
| // from an override hook. |
| bool early_return = FreeProlog<flags>(object, this); |
| if (early_return) { |
| return; |
| } |
| // FreeProlog ensures the object is not nullptr. |
| PA_DCHECK(object); |
| |
| // Almost all calls to FreeNoNooks() will end up writing to |*object|. |
| PA_PREFETCH_FOR_WRITE(object); |
| auto [slot_start, slot_span] = GetSlotStartAndSlotSpanFromAddress(object); |
| // We are going to read from |*slot_span| in all branches, but haven't |
| // done it yet. |
| PA_PREFETCH(slot_span); |
| FreeNoHooksImmediate<flags>(slot_start, slot_span); |
| } |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeWithSizeInline(void* object, |
| size_t size) { |
| if (!settings_.enable_free_with_size) { |
| FreeInline<flags>(object); |
| return; |
| } |
| // The correct PartitionRoot might not be deducible if the |object| originates |
| // from an override hook. |
| bool early_return = FreeProlog<flags>(object, this); |
| if (early_return) { |
| return; |
| } |
| // FreeProlog ensures the object is not nullptr. |
| PA_DCHECK(object); |
| |
| // Almost all calls to FreeWithSizeNoHooks() will end up writing to |*object|. |
| PA_PREFETCH_FOR_WRITE(object); |
| auto [slot_start, slot_span] = GetSlotStartAndSlotSpanFromAddress(object); |
| FreeWithSizeNoHooksImmediate<flags>(slot_start, slot_span, size); |
| } |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeWithSizeAndAlignmentInline( |
| void* object, |
| size_t size, |
| size_t alignment) { |
| if (!settings_.enable_free_with_size) { |
| FreeInline<flags>(object); |
| return; |
| } |
| // The correct PartitionRoot might not be deducible if the |object| originates |
| // from an override hook. |
| bool early_return = FreeProlog<flags>(object, this); |
| if (early_return) { |
| return; |
| } |
| // FreeProlog ensures the object is not nullptr. |
| PA_DCHECK(object); |
| |
| // Almost all calls to FreeWithSizeNoHooks() will end up writing to |*object|. |
| PA_PREFETCH_FOR_WRITE(object); |
| auto [slot_start, slot_span] = GetSlotStartAndSlotSpanFromAddress(object); |
| |
| auto adjusted_size = GetAdjustedSizeForAlignment(alignment, size); |
| // Overflow check. adjusted_size must be larger or equal to the original size. |
| PA_CHECK(adjusted_size >= size); |
| FreeWithSizeNoHooksImmediate<flags>(slot_start, slot_span, adjusted_size); |
| } |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeNoHooksImmediateInternal( |
| internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span, |
| const internal::BucketSizeDetails& size_details) { |
| // The thread cache is added "in the middle" of the main allocator, that is: |
| // - After all the cookie/in-slot metadata management |
| // - Before the "raw" allocator. |
| // |
| // On the deallocation side: |
| // 1. Check cookie/in-slot metadata, adjust the pointer |
| // 2. Deallocation |
| // a. Return to the thread cache if possible. If it succeeds, return. |
| // b. Otherwise, call the "raw" allocator <-- Locking |
| PA_DCHECK(slot_start); |
| PA_DCHECK(slot_span); |
| PA_DCHECK(DeducedRootIsValid(slot_span)); |
| |
| // Layout inside the slot: |
| // |...object...|[empty]|[cookie]|[unused]|[metadata]| |
| // <--------(a)---------> |
| // <--(b)---> + <---(b)----> |
| // <-------------(c)-------------> + <---(c)----> |
| // (a) usable_size |
| // (b) extras |
| // (c) utilized_slot_size |
| // |
| // Note: in-slot metadata and cookie can be 0-sized. |
| // |
| // For more context, see the other "Layout inside the slot" comment inside |
| // AllocInternalNoHooks(). |
| |
| #if PA_BUILDFLAG(USE_PARTITION_COOKIE) |
| if (settings_.use_cookie) { |
| // Verify the cookie after the allocated region. |
| // If this assert fires, you probably corrupted memory. |
| const size_t usable_size = GetSlotUsableSize(size_details, slot_span); |
| internal::PartitionCookieCheckValue( |
| PA_UNSAFE_TODO(slot_start.ToObject() + usable_size), usable_size); |
| } |
| #endif // PA_BUILDFLAG(USE_PARTITION_COOKIE) |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| if (brp_enabled()) [[likely]] { |
| auto* ref_count = InSlotMetadataPointerFromSlotStartAndSize( |
| slot_start.Untag(), size_details.slot_size); |
| // If there are no more references to the allocation, it can be freed |
| // immediately. Otherwise, defer the operation and zap the memory to turn |
| // potential use-after-free issues into unexploitable crashes. Zapping must |
| // complete before we clear kMemoryHeldByAllocatorBit in |
| // ReleaseFromAllocator(), otherwise another thread may allocate and start |
| // using the slot in the middle of zapping. |
| bool was_zapped = false; |
| if (!ref_count->IsAliveWithNoKnownRefs()) [[unlikely]] { |
| was_zapped = true; |
| QuarantineForBrp(slot_span, slot_start); |
| } |
| |
| if (!(ref_count->ReleaseFromAllocator(slot_start.Untag(), slot_span))) |
| [[unlikely]] { |
| PA_CHECK(was_zapped); |
| total_size_of_brp_quarantined_bytes.fetch_add( |
| slot_span->GetSlotSizeForBookkeeping(), std::memory_order_relaxed); |
| total_count_of_brp_quarantined_slots_.fetch_add( |
| 1, std::memory_order_relaxed); |
| cumulative_size_of_brp_quarantined_bytes_.fetch_add( |
| slot_span->GetSlotSizeForBookkeeping(), std::memory_order_relaxed); |
| cumulative_count_of_brp_quarantined_slots_.fetch_add( |
| 1, std::memory_order_relaxed); |
| |
| if constexpr (ContainsFlags(flags, FreeFlags::kSchedulerLoopQuarantine)) { |
| // This flag is to be read on `FreeAfterBRPQuarantine()`. |
| ref_count->SetQuarantineRequest(); |
| } |
| return; |
| } |
| } |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| // memset() can be really expensive. |
| #if PA_BUILDFLAG(EXPENSIVE_DCHECKS_ARE_ON) |
| internal::DebugMemset(slot_start.ToObject(), internal::kFreedByte, |
| slot_span->GetUtilizedSlotSize()); |
| #endif // PA_BUILDFLAG(EXPENSIVE_DCHECKS_ARE_ON) |
| |
| if constexpr (ContainsFlags(flags, FreeFlags::kIntendedLeak)) { |
| // Must not enable `thread_cache` and `brp` to use `kIntendedLeak`. |
| PA_CHECK(!settings_.with_thread_cache); |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| PA_CHECK(!brp_enabled()); |
| #endif |
| intended_leak_size_.fetch_add(size_details.slot_size); |
| return; // Leak |
| } else if constexpr (ContainsFlags(flags, |
| FreeFlags::kSchedulerLoopQuarantine)) { |
| ThreadCache* thread_cache = GetThreadCache(); |
| if (ThreadCache::IsValid(thread_cache)) [[likely]] { |
| thread_cache->GetSchedulerLoopQuarantineBranch().Quarantine( |
| slot_start, slot_span, size_details); |
| } else { |
| scheduler_loop_quarantine_.Quarantine(slot_start, slot_span, |
| size_details); |
| } |
| return; |
| } else if constexpr ( |
| ContainsFlags( |
| flags, |
| FreeFlags::kSchedulerLoopQuarantineForAdvancedMemorySafetyChecks)) { |
| scheduler_loop_quarantine_for_advanced_memory_safety_checks_.Quarantine( |
| slot_start, slot_span, size_details); |
| return; |
| } |
| |
| RawFreeWithThreadCache(slot_start, size_details, slot_span); |
| } |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeNoHooksImmediate( |
| internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span) { |
| auto size_details = SlotSpanToBucketSizeDetails(slot_span); |
| FreeNoHooksImmediateInternal<flags>(slot_start, slot_span, size_details); |
| } |
| |
| template <FreeFlags flags> |
| PA_ALWAYS_INLINE void PartitionRoot::FreeWithSizeNoHooksImmediate( |
| internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span, |
| size_t size) { |
| auto size_details = SizeToBucketSizeDetails(size, slot_span); |
| FreeNoHooksImmediateInternal<flags>(slot_start, slot_span, size_details); |
| } |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| // static |
| PA_ALWAYS_INLINE void PartitionRoot::FreeAfterBRPQuarantine( |
| internal::UntaggedSlotStart slot_start, |
| size_t slot_size) { |
| auto* slot_span = internal::SlotSpanMetadata::FromSlotStart(slot_start); |
| auto* root = PartitionRoot::FromSlotSpanMetadata(slot_span); |
| // Currently, InSlotMetadata is allocated when BRP is used. |
| PA_DCHECK(root->brp_enabled()); |
| PA_DCHECK(!PartitionRoot::InSlotMetadataPointerFromSlotStartAndSize( |
| slot_start, slot_span->bucket->slot_size) |
| ->IsAlive()); |
| |
| // Iterating over the entire slot can be really expensive. |
| #if PA_BUILDFLAG(EXPENSIVE_DCHECKS_ARE_ON) |
| auto hook = PartitionAllocHooks::GetQuarantineOverrideHook(); |
| // If we have a hook the object segment is not necessarily filled |
| // with |kQuarantinedByte|. |
| if (!hook) [[likely]] { |
| unsigned char* object = slot_start.Tag().ToObject(); |
| for (size_t i = 0; i < root->GetSlotUsableSize(slot_span); ++i) { |
| PA_UNSAFE_TODO(PA_DCHECK(object[i] == internal::kQuarantinedByte)); |
| } |
| } |
| internal::DebugMemset(slot_start.Tag().ToObject(), internal::kFreedByte, |
| slot_span->GetUtilizedSlotSize()); |
| #endif // PA_BUILDFLAG(EXPENSIVE_DCHECKS_ARE_ON) |
| |
| root->total_size_of_brp_quarantined_bytes.fetch_sub( |
| slot_span->GetSlotSizeForBookkeeping(), std::memory_order_relaxed); |
| root->total_count_of_brp_quarantined_slots_.fetch_sub( |
| 1, std::memory_order_relaxed); |
| |
| internal::InSlotMetadata* metadata = |
| internal::InSlotMetadataPointer(slot_start.value(), slot_size); |
| auto size_details = internal::BucketSizeDetails{ |
| .bucket_index = |
| SizeToBucketIndex(slot_size, root->GetBucketDistribution()), |
| .slot_size = slot_size, |
| }; |
| PA_DCHECK(slot_size == slot_span->bucket->slot_size); |
| |
| // `FreeFlags::kSchedulerLoopQuarantine` was used for the original `Free()` |
| // call. Send the allocation to yet another quarantine. |
| if (metadata->PopQuarantineRequest()) { |
| ThreadCache* thread_cache = root->GetThreadCache(); |
| if (ThreadCache::IsValid(thread_cache)) [[likely]] { |
| thread_cache->GetSchedulerLoopQuarantineBranch().Quarantine( |
| slot_start.Tag(), slot_span, size_details); |
| } else { |
| root->scheduler_loop_quarantine_.Quarantine(slot_start.Tag(), slot_span, |
| size_details); |
| } |
| } else { |
| root->RawFreeWithThreadCache(slot_start.Tag(), size_details, slot_span); |
| } |
| } |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| PA_ALWAYS_INLINE void PartitionRoot::FreeInSlotSpan( |
| internal::UntaggedSlotStart slot_start, |
| SlotSpanMetadata* slot_span) { |
| return slot_span->Free(slot_start.value(), this); |
| } |
| |
| #if PA_CONFIG(IS_NONCLANG_MSVC) |
| // MSVC only supports inline assembly on x86. This preprocessor directive |
| // is intended to be a replacement for the same. |
| // |
| // TODO(crbug.com/40234441): Make sure inlining doesn't degrade this into |
| // a no-op or similar. The documentation doesn't say. |
| #pragma optimize("", off) |
| #endif |
| PA_ALWAYS_INLINE void PartitionRoot::RawFree(internal::SlotStart slot_start, |
| SlotSpanMetadata* slot_span) { |
| void* ptr = slot_start.ToObject(); |
| // At this point we are about to acquire the lock, so we try to minimize the |
| // risk of blocking inside the locked section. |
| // |
| // For allocations that are not direct-mapped, there will always be a store at |
| // the beginning of |*slot_start|, to link the freelist. This is why there is |
| // a prefetch of it at the beginning of the free() path. |
| // |
| // However, the memory which is being freed can be very cold (for instance |
| // during browser shutdown, when various caches are finally completely freed), |
| // and so moved to either compressed memory or swap. This means that touching |
| // it here can cause a major page fault. This is in turn will cause |
| // descheduling of the thread *while locked*. Since we don't have priority |
| // inheritance locks on most platforms, avoiding long locked periods relies on |
| // the OS having proper priority boosting. There is evidence |
| // (crbug.com/1228523) that this is not always the case on Windows, and a very |
| // low priority background thread can block the main one for a long time, |
| // leading to hangs. |
| // |
| // To mitigate that, make sure that we fault *before* locking. Note that this |
| // is useless for direct-mapped allocations (which are very rare anyway), and |
| // that this path is *not* taken for thread cache bucket purge (since it calls |
| // RawFreeLocked()). This is intentional, as the thread cache is purged often, |
| // and the memory has a consequence the memory has already been touched |
| // recently (to link the thread cache freelist). |
| *static_cast<volatile uintptr_t*>(ptr) = 0; |
| // Note: even though we write to slot_start + sizeof(void*) as well, due to |
| // alignment constraints, the two locations are always going to be in the same |
| // OS page. No need to write to the second one as well. |
| // |
| // Do not move the store above inside the locked section. |
| #if !(PA_CONFIG(IS_NONCLANG_MSVC)) |
| __asm__ __volatile__("" : : "r"(slot_start) : "memory"); |
| #endif |
| // This is done for memory usage (by improving the compression ratio of heap |
| // pages), not for security, so we care more about being affordable than |
| // prompt. This is done after the thread cache, so most deallocation do not |
| // end up here. Nevertheless, we do not need to memset() direct-mapped |
| // allocations, as they are released right away. And single-slot slot spans |
| // are also excluded, because they can be entirely decommitted once leaving |
| // the global ring. |
| // |
| // This is done before acquiring the lock, to prevent page faults causing |
| // issues there. |
| if (settings_.eventually_zero_freed_memory && |
| !IsDirectMappedBucket(slot_span->bucket) && |
| slot_span->bucket->get_slots_per_span() > 1) { |
| internal::SecureMemset(ptr, 0, GetSlotUsableSize(slot_span)); |
| } |
| |
| DecreaseTotalSizeOfAllocatedBytes(slot_start.value(), |
| slot_span->GetSlotSizeForBookkeeping()); |
| |
| ::partition_alloc::internal::ScopedGuard guard{ |
| internal::PartitionRootLock(this)}; |
| FreeInSlotSpan(slot_start.Untag(), slot_span); |
| } |
| #if PA_CONFIG(IS_NONCLANG_MSVC) |
| #pragma optimize("", on) |
| #endif |
| |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| PA_ALWAYS_INLINE void PartitionRoot::RetagSlotIfNeeded( |
| internal::UntaggedSlotStart slot_start, |
| size_t slot_size) { |
| void* slot_start_ptr = slot_start.Tag().ToObject(); |
| // This branch is |likely| because HAS_MEMORY_TAGGING build flag is true for |
| // arm64 Android devices and only a small portion of them will have memory |
| // tagging enabled. |
| if (!IsMemoryTaggingEnabled()) [[likely]] { |
| return; |
| } |
| |
| if (slot_size <= internal::kMaxMemoryTaggingSize) [[likely]] { |
| if (UseRandomMemoryTagging()) { |
| // Exclude the previous tag so that immediate use after free is detected |
| // 100% of the time. |
| uint8_t previous_tag = internal::ExtractTagFromPtr(slot_start_ptr); |
| internal::TagMemoryRangeRandomly(slot_start_ptr, slot_size, |
| 1 << previous_tag); |
| } else { |
| internal::TagMemoryRangeIncrement(slot_start_ptr, slot_size); |
| } |
| } |
| } |
| #endif // PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| |
| PA_ALWAYS_INLINE void PartitionRoot::RawFreeWithThreadCache( |
| internal::SlotStart slot_start, |
| const internal::BucketSizeDetails& size_details, |
| SlotSpanMetadata* slot_span) { |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| RetagSlotIfNeeded(slot_start.Untag(), size_details.slot_size); |
| slot_start = slot_start.Untag().Tag(); |
| #endif |
| |
| // `[[likely]]`: performance-sensitive partitions have a thread cache, |
| // direct-mapped allocations are uncommon. |
| ThreadCache* thread_cache = GetThreadCache(); |
| // TODO(crbug.com/467243745): Once `ThreadCache::largest_active_bucket_index_` |
| // becomes a per-class variable, remove the initialization check in `IsValid` |
| // and reuse the `bucket_index > largest_active_bucket_index_` within |
| // `MaybePutInCache`. |
| if (ThreadCache::IsValid(thread_cache) && |
| (size_details.slot_size <= BucketIndexLookup::kMaxBucketSize)) |
| [[likely]] { |
| PA_DCHECK(!IsDirectMappedBucket(slot_span->bucket)); |
| std::optional<size_t> slot_size = thread_cache->MaybePutInCache( |
| slot_start.Untag(), size_details.bucket_index); |
| if (slot_size.has_value()) [[likely]] { |
| // This is a fast path, avoid calling GetSlotUsableSize() in Release |
| // builds as it is costlier. Copy its small bucket path instead. |
| PA_DCHECK(!slot_span->CanStoreRawSize()); |
| size_t usable_size = AdjustSizeForExtrasSubtract(slot_size.value()); |
| PA_DCHECK(usable_size == GetSlotUsableSize(slot_span)); |
| thread_cache->RecordDeallocation(usable_size); |
| return; |
| } |
| } |
| |
| if (ThreadCache::IsValid(thread_cache)) [[likely]] { |
| // Accounting must be done outside `RawFree()`, as it's also called from |
| // the thread cache. We would double-count otherwise. |
| // |
| // GetSlotUsableSize() will always give the correct result, and we are in |
| // a slow path here (since the thread cache case returned earlier). |
| size_t usable_size = GetSlotUsableSize(slot_span); |
| thread_cache->RecordDeallocation(usable_size); |
| } |
| RawFree(slot_start, slot_span); |
| } |
| |
| PA_ALWAYS_INLINE void PartitionRoot::RawFreeLocked( |
| internal::UntaggedSlotStart slot_start, |
| SlotSpanMetadata* slot_span) { |
| // 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(!IsDirectMappedBucket(slot_span->bucket)); |
| DecreaseTotalSizeOfAllocatedBytes(slot_start.value(), |
| slot_span->GetSlotSizeForBookkeeping()); |
| FreeInSlotSpan(slot_start, slot_span); |
| } |
| |
| PA_ALWAYS_INLINE PartitionRoot* PartitionRoot::FromSlotSpanMetadata( |
| const SlotSpanMetadata* slot_span) { |
| auto* extent_entry = reinterpret_cast<SuperPageExtentEntry*>( |
| reinterpret_cast<uintptr_t>(slot_span) & internal::SystemPageBaseMask()); |
| return extent_entry->root; |
| } |
| |
| PA_ALWAYS_INLINE PartitionRoot* PartitionRoot::FromFirstSuperPage( |
| uintptr_t super_page) { |
| PA_DCHECK(internal::ReservationOffsetTable::Get(super_page) |
| .IsReservationStart(super_page)); |
| // Slow |
| const std::ptrdiff_t offset = internal::GetMetadataOffsetFromAddr(super_page); |
| auto* extent_entry = internal::PartitionSuperPageToExtent(super_page, offset); |
| PartitionRoot* root = extent_entry->root; |
| PA_DCHECK(root->inverted_self_ == ~reinterpret_cast<uintptr_t>(root)); |
| return root; |
| } |
| |
| PA_ALWAYS_INLINE PartitionRoot* PartitionRoot::FromAddrInFirstSuperpage( |
| uintptr_t address) { |
| uintptr_t super_page = address & internal::kSuperPageBaseMask; |
| PA_DCHECK(internal::ReservationOffsetTable::Get(super_page) |
| .IsReservationStart(super_page)); |
| return FromFirstSuperPage(super_page); |
| } |
| |
| PA_ALWAYS_INLINE void PartitionRoot::IncreaseTotalSizeOfAllocatedBytes( |
| uintptr_t addr, |
| size_t len, |
| size_t raw_size) { |
| // |total_size_of_allocated_bytes_| is only for debugging/stats, so relaxed |
| // memory order is sufficient. |
| size_t previous_total_size_of_allocated_bytes = |
| total_size_of_allocated_bytes_.fetch_add(len, std::memory_order_relaxed); |
| size_t new_total_size_of_allocated_bytes = |
| previous_total_size_of_allocated_bytes + len; |
| |
| size_t expected, desired; |
| do { |
| expected = max_size_of_allocated_bytes_.load(std::memory_order_relaxed); |
| desired = std::max(expected, new_total_size_of_allocated_bytes); |
| } while (!max_size_of_allocated_bytes_.compare_exchange_weak( |
| expected, desired, std::memory_order_relaxed, std::memory_order_relaxed)); |
| |
| #if PA_BUILDFLAG(RECORD_ALLOC_INFO) |
| partition_alloc::internal::RecordAllocOrFree(addr | 0x01, raw_size); |
| #endif // PA_BUILDFLAG(RECORD_ALLOC_INFO) |
| } |
| |
| PA_ALWAYS_INLINE void PartitionRoot::DecreaseTotalSizeOfAllocatedBytes( |
| uintptr_t addr, |
| size_t len) { |
| // An underflow here means we've miscounted |total_size_of_allocated_bytes_| |
| // somewhere. |
| // |total_size_of_allocated_bytes_| is only for debugging/stats, so relaxed |
| // memory order is sufficient. |
| size_t previous_total_size_of_allocated_bytes = |
| total_size_of_allocated_bytes_.fetch_sub(len, std::memory_order_relaxed); |
| PA_DCHECK(previous_total_size_of_allocated_bytes >= len); |
| #if PA_BUILDFLAG(RECORD_ALLOC_INFO) |
| partition_alloc::internal::RecordAllocOrFree(addr | 0x00, len); |
| #endif // PA_BUILDFLAG(RECORD_ALLOC_INFO) |
| } |
| |
| PA_ALWAYS_INLINE void PartitionRoot::IncreaseCommittedPages(size_t len) { |
| const auto old_total = |
| total_size_of_committed_pages_.fetch_add(len, std::memory_order_relaxed); |
| |
| const auto new_total = old_total + len; |
| |
| // This function is called quite frequently; to avoid performance problems, we |
| // don't want to hold a lock here, so we use compare and exchange instead. |
| size_t expected = |
| max_size_of_committed_pages_.load(std::memory_order_relaxed); |
| size_t desired; |
| do { |
| desired = std::max(expected, new_total); |
| } while (!max_size_of_committed_pages_.compare_exchange_weak( |
| expected, desired, std::memory_order_relaxed, std::memory_order_relaxed)); |
| } |
| |
| PA_ALWAYS_INLINE void PartitionRoot::DecreaseCommittedPages(size_t len) { |
| total_size_of_committed_pages_.fetch_sub(len, std::memory_order_relaxed); |
| } |
| |
| PA_ALWAYS_INLINE void PartitionRoot::DecommitSystemPagesForData( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition) { |
| internal::ScopedSyscallTimer timer{this}; |
| DecommitSystemPages(address, length, accessibility_disposition); |
| DecreaseCommittedPages(length); |
| } |
| |
| // Not unified with TryRecommitSystemPagesForData() to preserve error codes. |
| PA_ALWAYS_INLINE void PartitionRoot::RecommitSystemPagesForData( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) { |
| internal::ScopedSyscallTimer timer{this}; |
| |
| auto page_accessibility = GetPageAccessibility(request_tagging); |
| bool ok = TryRecommitSystemPages(address, length, page_accessibility, |
| accessibility_disposition); |
| if (!ok) [[unlikely]] { |
| // Decommit some memory and retry. The alternative is crashing. |
| DecommitEmptySlotSpans(); |
| RecommitSystemPages(address, length, page_accessibility, |
| accessibility_disposition); |
| } |
| |
| IncreaseCommittedPages(length); |
| } |
| |
| template <bool already_locked> |
| PA_ALWAYS_INLINE bool PartitionRoot::TryRecommitSystemPagesForDataInternal( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) { |
| internal::ScopedSyscallTimer timer{this}; |
| |
| auto page_accessibility = GetPageAccessibility(request_tagging); |
| bool ok = TryRecommitSystemPages(address, length, page_accessibility, |
| accessibility_disposition); |
| if (!ok) [[unlikely]] { |
| { |
| // Decommit some memory and retry. The alternative is crashing. |
| if constexpr (!already_locked) { |
| ::partition_alloc::internal::ScopedGuard guard( |
| internal::PartitionRootLock(this)); |
| DecommitEmptySlotSpans(); |
| } else { |
| internal::PartitionRootLock(this).AssertAcquired(); |
| DecommitEmptySlotSpans(); |
| } |
| } |
| ok = TryRecommitSystemPages(address, length, page_accessibility, |
| accessibility_disposition); |
| } |
| |
| if (ok) { |
| IncreaseCommittedPages(length); |
| } |
| |
| return ok; |
| } |
| |
| PA_ALWAYS_INLINE bool |
| PartitionRoot::TryRecommitSystemPagesForDataWithAcquiringLock( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) { |
| return TryRecommitSystemPagesForDataInternal<false>( |
| address, length, accessibility_disposition, request_tagging); |
| } |
| |
| PA_ALWAYS_INLINE |
| bool PartitionRoot::TryRecommitSystemPagesForDataLocked( |
| uintptr_t address, |
| size_t length, |
| PageAccessibilityDisposition accessibility_disposition, |
| bool request_tagging) { |
| return TryRecommitSystemPagesForDataInternal<true>( |
| address, length, accessibility_disposition, request_tagging); |
| } |
| |
| PA_ALWAYS_INLINE size_t PartitionRoot::GetSlotUsableSize( |
| const internal::BucketSizeDetails& size_details, |
| SlotSpanMetadata* slot_span) { |
| if (size_details.slot_size <= kThreadCacheLargeSizeThreshold) [[likely]] { |
| PA_DCHECK(!slot_span->CanStoreRawSize()); |
| auto usable_size = AdjustSizeForExtrasSubtract(size_details.slot_size); |
| PA_DCHECK(usable_size == GetSlotUsableSize(slot_span)); |
| return usable_size; |
| } |
| return GetSlotUsableSize(slot_span); |
| } |
| |
| // static |
| // |
| // Returns the size available to the app. It can be equal or higher than the |
| // requested size. If higher, the overage won't exceed what's actually usable |
| // by the app without a risk of running out of an allocated region or into |
| // PartitionAlloc's internal data. Used as malloc_usable_size and malloc_size. |
| // |
| // |ptr| should preferably point to the beginning of an object returned from |
| // malloc() et al., but it doesn't have to. crbug.com/1292646 shows an example |
| // where this isn't the case. Note, an inner object pointer won't work for |
| // direct map, unless it is within the first partition page. |
| PA_ALWAYS_INLINE size_t PartitionRoot::GetUsableSize(const void* ptr) { |
| // malloc_usable_size() is expected to handle NULL gracefully and return 0. |
| if (!ptr) { |
| return 0; |
| } |
| const std::ptrdiff_t offset = |
| internal::GetMetadataOffsetFromAddr(internal::ObjectInnerPtr2Addr(ptr)); |
| auto* slot_span = SlotSpanMetadata::FromObjectInnerPtr(ptr, offset); |
| auto* root = FromSlotSpanMetadata(slot_span); |
| return root->GetSlotUsableSize(slot_span); |
| } |
| |
| // Returns the page configuration to use when mapping slot spans for a given |
| // partition root. ReadWriteTagged is used on MTE-enabled systems for |
| // PartitionRoots supporting it. |
| PA_ALWAYS_INLINE PageAccessibilityConfiguration |
| PartitionRoot::GetPageAccessibility(bool request_tagging) const { |
| PageAccessibilityConfiguration::Permissions permissions = |
| PageAccessibilityConfiguration::kReadWrite; |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| if (IsMemoryTaggingEnabled() && request_tagging) { |
| permissions = PageAccessibilityConfiguration::kReadWriteTagged; |
| } |
| #endif |
| #if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION) |
| return PageAccessibilityConfiguration(permissions, |
| settings_.thread_isolation); |
| #else |
| return PageAccessibilityConfiguration(permissions); |
| #endif |
| } |
| |
| PA_ALWAYS_INLINE PageAccessibilityConfiguration |
| PartitionRoot::PageAccessibilityWithThreadIsolationIfEnabled( |
| PageAccessibilityConfiguration::Permissions permissions) const { |
| #if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION) |
| return PageAccessibilityConfiguration(permissions, |
| settings_.thread_isolation); |
| #endif |
| return PageAccessibilityConfiguration(permissions); |
| } |
| |
| // Return the capacity of the underlying slot (adjusted for extras). This |
| // doesn't mean this capacity is readily available. It merely means that if |
| // a new allocation (or realloc) happened with that returned value, it'd use |
| // the same amount of underlying memory. |
| PA_ALWAYS_INLINE size_t PartitionRoot::AllocationCapacityFromSlotStart( |
| internal::UntaggedSlotStart slot_start) const { |
| auto* slot_span = SlotSpanMetadata::FromSlotStart(slot_start, this); |
| return AdjustSizeForExtrasSubtract(slot_span->bucket->slot_size); |
| } |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| PA_ALWAYS_INLINE internal::InSlotMetadata* |
| PartitionRoot::InSlotMetadataPointerFromSlotStartAndSize( |
| internal::UntaggedSlotStart slot_start, |
| size_t slot_size) { |
| return internal::InSlotMetadataPointer(slot_start.value(), slot_size); |
| } |
| |
| PA_ALWAYS_INLINE internal::InSlotMetadata* |
| PartitionRoot::InSlotMetadataPointerFromObjectForTesting(void* object) const { |
| auto slot_start = internal::SlotStart::Unchecked(object).Untag(); |
| auto* slot_span = SlotSpanMetadata::FromSlotStart(slot_start, this); |
| return InSlotMetadataPointerFromSlotStartAndSize( |
| slot_start, slot_span->bucket->slot_size); |
| } |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| // static |
| PA_ALWAYS_INLINE uint16_t |
| PartitionRoot::SizeToBucketIndex(size_t size, |
| BucketDistribution bucket_distribution) { |
| switch (bucket_distribution) { |
| case BucketDistribution::kNeutral: |
| return BucketIndexLookup::GetIndexForNeutralBuckets(size); |
| case BucketDistribution::kDenser: |
| return BucketIndexLookup::GetIndexForDenserBuckets(size); |
| } |
| PA_NOTREACHED(); |
| } |
| |
| PA_ALWAYS_INLINE internal::BucketSizeDetails |
| PartitionRoot::SlotSpanToBucketSizeDetails(SlotSpanMetadata* slot_span) const { |
| return internal::BucketSizeDetails{ |
| .bucket_index = static_cast<uint16_t>(slot_span->bucket - this->buckets_), |
| .slot_size = slot_span->bucket->slot_size, |
| }; |
| } |
| |
| PA_ALWAYS_INLINE internal::BucketSizeDetails |
| PartitionRoot::SizeToBucketSizeDetails(size_t requested_size, |
| SlotSpanMetadata* slot_span) const { |
| auto raw_size = AdjustSizeForExtrasAdd(requested_size); |
| if (raw_size <= BucketIndexLookup::kMaxBucketSize) [[likely]] { |
| // For non-direct-mapped allocations, `bucket_index` and `slot_size` are |
| // determined without using `slot_span`. |
| auto bucket_index = |
| SizeToBucketIndex(raw_size, this->GetBucketDistribution()); |
| auto slot_size = BucketIndexLookup::GetBucketSize(bucket_index); |
| if (settings_.enable_strict_free_size_check) { |
| // TODO(crbug.com/410190984): Remove this prefetch & CHECKS once the |
| // PA_CHECK of the given size against the slot span metadata is replaced |
| // with a PA_DCHECK. |
| PA_PREFETCH(slot_span); |
| PA_CHECK(bucket_index == |
| static_cast<uint16_t>(slot_span->bucket - this->buckets_)); |
| PA_CHECK(slot_size == slot_span->bucket->slot_size); |
| } else { |
| PA_DCHECK(bucket_index == |
| static_cast<uint16_t>(slot_span->bucket - this->buckets_)); |
| PA_DCHECK(slot_size == slot_span->bucket->slot_size); |
| } |
| return internal::BucketSizeDetails{ |
| .bucket_index = bucket_index, |
| .slot_size = slot_size, |
| }; |
| } |
| // For direct-mapped allocations, `slot_size` is derived from `slot_span`. |
| // `bucket_index` is also populated from `slot_span` although its value is |
| // invalid and SHOULD NOT be used. |
| return SlotSpanToBucketSizeDetails(slot_span); |
| } |
| |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE void* PartitionRoot::AllocInternal(size_t requested_size, |
| size_t slot_span_alignment, |
| const char* type_name) { |
| static_assert(AreValidFlags(flags)); |
| PA_DCHECK((slot_span_alignment >= internal::PartitionPageSize()) && |
| internal::base::bits::HasSingleBit(slot_span_alignment)); |
| static_assert(!ContainsFlags( |
| flags, AllocFlags::kMemoryShouldBeTaggedForMte)); // Internal only. |
| |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| if constexpr (!ContainsFlags(flags, AllocFlags::kNoMemoryToolOverride)) { |
| if (!PartitionRoot::AllocWithMemoryToolProlog<flags>(requested_size)) { |
| // Early return if AllocWithMemoryToolProlog returns false |
| return nullptr; |
| } |
| void* result = nullptr; |
| // Taken from base::AlignedAlloc implementation. |
| if constexpr (ContainsFlags(flags, |
| AllocFlags::kAlignedAllocForMemoryTool)) { |
| #if PA_BUILDFLAG(PA_COMPILER_MSVC) |
| result = _aligned_malloc(requested_size, slot_span_alignment); |
| #elif PA_BUILDFLAG(IS_ANDROID) |
| // Android technically supports posix_memalign(), but does not expose it |
| // in the current version of the library headers used by Chromium. |
| // Luckily, memalign() on Android returns pointers which can safely be |
| // used with free(), so we can use it instead. Issue filed to document |
| // this: http://code.google.com/p/android/issues/detail?id=35391 |
| result = memalign(slot_span_alignment, requested_size); |
| #else |
| int ret = posix_memalign(&result, slot_span_alignment, requested_size); |
| if (ret != 0) { |
| result = nullptr; |
| } |
| #endif // PA_BUILDFLAG(PA_COMPILER_MSVC) |
| // Aligned alloc functions don't have the `calloc` behavior of zeroing |
| // the allocated memory, so we need to do it manually. |
| if constexpr (ContainsFlags(flags, AllocFlags::kZeroFill)) { |
| if (result) { |
| // SAFETY: `result` is non-null and `requested_size` is the size of |
| // the allocation, so this is a valid range to zero out. |
| PA_UNSAFE_BUFFERS(memset(result, 0, requested_size)); |
| } |
| } |
| } else { |
| constexpr bool zero_fill = ContainsFlags(flags, AllocFlags::kZeroFill); |
| result = zero_fill ? calloc(1, requested_size) : malloc(requested_size); |
| } |
| if constexpr (!ContainsFlags(flags, AllocFlags::kReturnNull)) { |
| PA_CHECK(result); |
| } |
| return result; |
| } |
| #else // !defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| // If `MEMORY_TOOL_REPLACES_ALLOCATOR` is not defined, |
| // `kAlignedAllocForMemoryTool` should not be passed to `AllocInternal`. |
| static_assert(!ContainsFlags(flags, AllocFlags::kAlignedAllocForMemoryTool)); |
| #endif // defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| |
| constexpr bool no_hooks = ContainsFlags(flags, AllocFlags::kNoHooks); |
| bool hooks_enabled; |
| |
| if constexpr (!no_hooks) { |
| PA_DCHECK(initialized_); |
| void* object = nullptr; |
| hooks_enabled = PartitionAllocHooks::AreHooksEnabled(); |
| if (hooks_enabled) { |
| auto additional_flags = AllocFlags::kNone; |
| #if PA_BUILDFLAG(HAS_MEMORY_TAGGING) |
| if (IsMemoryTaggingEnabled()) { |
| additional_flags |= AllocFlags::kMemoryShouldBeTaggedForMte; |
| } |
| #endif |
| // The override hooks will return false if it can't handle the request, |
| // i.e. due to unsupported flags. In this case, we forward the allocation |
| // request to the default mechanisms. |
| // TODO(crbug.com/40152647): See if we can make the forwarding more |
| // verbose to ensure that this situation doesn't go unnoticed. |
| if (PartitionAllocHooks::AllocationOverrideHookIfEnabled( |
| &object, flags | additional_flags, requested_size, type_name)) { |
| PartitionAllocHooks::AllocationObserverHookIfEnabled( |
| CreateAllocationNotificationData(object, requested_size, |
| type_name)); |
| return object; |
| } |
| } |
| } |
| |
| void* const object = |
| AllocInternalNoHooks<flags>(requested_size, slot_span_alignment); |
| |
| if constexpr (!no_hooks) { |
| if (hooks_enabled) [[unlikely]] { |
| PartitionAllocHooks::AllocationObserverHookIfEnabled( |
| CreateAllocationNotificationData(object, requested_size, type_name)); |
| } |
| } |
| |
| return object; |
| } |
| |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE void* PartitionRoot::AllocInternalNoHooks( |
| size_t requested_size, |
| size_t slot_span_alignment) { |
| static_assert(AreValidFlags(flags)); |
| |
| // The thread cache is added "in the middle" of the main allocator, that is: |
| // - After all the cookie/in-slot metadata management |
| // - Before the "raw" allocator. |
| // |
| // That is, the general allocation flow is: |
| // 1. Adjustment of requested size to make room for extras |
| // 2. Allocation: |
| // a. Call to the thread cache, if it succeeds, go to step 3. |
| // b. Otherwise, call the "raw" allocator <-- Locking |
| // 3. Handle cookie/in-slot metadata, zero allocation if required |
| |
| size_t raw_size = AdjustSizeForExtrasAdd(requested_size); |
| PA_CHECK(raw_size >= requested_size); // check for overflows |
| |
| // We should avoid calling `GetBucketDistribution()` repeatedly in the |
| // same function, since the bucket distribution can change underneath |
| // us. If we pass this changed value to `SizeToBucketIndex()` in the |
| // same allocation request, we'll get inconsistent state. |
| uint16_t bucket_index = |
| SizeToBucketIndex(raw_size, this->GetBucketDistribution()); |
| size_t usable_size; |
| bool is_already_zeroed = false; |
| internal::UntaggedSlotStart slot_start; |
| size_t slot_size = 0; |
| |
| auto* thread_cache = GetOrCreateThreadCache(); |
| |
| // Don't use thread cache if higher order alignment is requested, because the |
| // thread cache will not be able to satisfy it. |
| // |
| // `[[likely]]`: performance-sensitive partitions use the thread cache. |
| if (ThreadCache::IsValid(thread_cache) && |
| slot_span_alignment <= internal::PartitionPageSize()) [[likely]] { |
| // Note: getting slot_size from the thread cache rather than by |
| // `buckets_[bucket_index].slot_size` to avoid touching `buckets_` on the |
| // fast path. |
| slot_start = thread_cache->GetFromCache(bucket_index, &slot_size); |
| |
| // `[[likely]]`: median hit rate in the thread cache is 95%, from metrics. |
| if (slot_start.value()) [[likely]] { |
| // This follows the logic of SlotSpanMetadata::GetUsableSize for small |
| // buckets_, which is too expensive to call here. |
| // Keep it in sync! |
| usable_size = AdjustSizeForExtrasSubtract(slot_size); |
| |
| #if PA_BUILDFLAG(DCHECKS_ARE_ON) |
| // Make sure that the allocated pointer comes from the same place it would |
| // for a non-thread cache allocation. |
| SlotSpanMetadata* slot_span = |
| SlotSpanMetadata::FromSlotStart(slot_start, this); |
| PA_DCHECK(DeducedRootIsValid(slot_span)); |
| PA_DCHECK(slot_span->bucket == &bucket_at(bucket_index)); |
| PA_DCHECK(slot_span->bucket->slot_size == slot_size); |
| PA_DCHECK(usable_size == GetSlotUsableSize(slot_span)); |
| // All large allocations must go through the RawAlloc path to correctly |
| // set |usable_size|. |
| PA_DCHECK(!slot_span->CanStoreRawSize()); |
| PA_DCHECK(!slot_span->bucket->is_direct_mapped()); |
| #endif |
| } else { |
| slot_start = RawAlloc<flags>(PA_UNSAFE_TODO(buckets_ + bucket_index), |
| raw_size, slot_span_alignment, &usable_size, |
| &slot_size, &is_already_zeroed); |
| } |
| } else { |
| slot_start = RawAlloc<flags>(PA_UNSAFE_TODO(buckets_ + bucket_index), |
| raw_size, slot_span_alignment, &usable_size, |
| &slot_size, &is_already_zeroed); |
| } |
| |
| if (!slot_start.value()) [[unlikely]] { |
| return nullptr; |
| } |
| |
| if (ThreadCache::IsValid(thread_cache)) [[likely]] { |
| thread_cache->RecordAllocation(usable_size); |
| } |
| |
| // Layout inside the slot: |
| // |...object...|[empty]|[cookie]|[unused]|[metadata]| |
| // <----(a)-----> |
| // <--------(b)---------> |
| // <--(c)---> + <---(c)----> |
| // <----(d)-----> + <--(d)---> + <---(d)----> |
| // <-------------(e)-------------> + <---(e)----> |
| // <-----------------------(f)-----------------------> |
| // (a) requested_size |
| // (b) usable_size |
| // (c) extras |
| // (d) raw_size |
| // (e) utilized_slot_size |
| // (f) slot_size |
| // |
| // Notes: |
| // - Cookie exists only in the PA_BUILDFLAG(DCHECKS_ARE_ON) case. |
| // - Think of raw_size as the minimum size required internally to satisfy |
| // the allocation request (i.e. requested_size + extras) |
| // - At most one "empty" or "unused" space can occur at a time. They occur |
| // when slot_size is larger than raw_size. "unused" applies only to large |
| // allocations (direct-mapped and single-slot slot spans) and "empty" only |
| // to small allocations. |
| // Why either-or, one might ask? We make an effort to put the trailing |
| // cookie as close to data as possible to catch overflows (often |
| // off-by-one), but that's possible only if we have enough space in metadata |
| // to save raw_size, i.e. only for large allocations. For small allocations, |
| // we have no other choice than putting the cookie at the very end of the |
| // slot, thus creating the "empty" space. |
| // - Unlike "unused", "empty" counts towards usable_size, because the app can |
| // query for it and use this space without a need for reallocation. |
| // - In-slot metadata may or may not exist in the slot. Currently it exists |
| // only when BRP is used. |
| // - If slot_start is not SystemPageSize()-aligned (possible only for small |
| // allocations), in-slot metadata is stored at the end of the slot. |
| // Otherwise it is stored in a special table placed after the super page |
| // metadata. For simplicity, the space for in-slot metadata is still |
| // reserved at the end of the slot, even though redundant. |
| |
| void* object = slot_start.Tag().ToObject(); |
| |
| // Add the cookie after the allocation. |
| #if PA_BUILDFLAG(USE_PARTITION_COOKIE) |
| if (settings_.use_cookie) { |
| internal::PartitionCookieWriteValue( |
| PA_UNSAFE_TODO(static_cast<unsigned char*>(object) + usable_size)); |
| } |
| #endif // PA_BUILDFLAG(USE_PARTITION_COOKIE) |
| |
| // Fill the region kUninitializedByte (on debug builds, if not requested to 0) |
| // or 0 (if requested and not 0 already). |
| constexpr bool zero_fill = ContainsFlags(flags, AllocFlags::kZeroFill); |
| // `[[likely]]`: operator new() calls malloc(), not calloc(). |
| if constexpr (!zero_fill) { |
| // memset() can be really expensive. |
| #if PA_BUILDFLAG(EXPENSIVE_DCHECKS_ARE_ON) |
| internal::DebugMemset(object, internal::kUninitializedByte, usable_size); |
| #endif |
| } else if (!is_already_zeroed) { |
| PA_UNSAFE_TODO(memset(object, 0, usable_size)); |
| } |
| |
| #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| if (brp_enabled()) [[likely]] { |
| auto* ref_count = |
| new (InSlotMetadataPointerFromSlotStartAndSize(slot_start, slot_size)) |
| internal::InSlotMetadata(); |
| #if PA_CONFIG(IN_SLOT_METADATA_STORE_REQUESTED_SIZE) |
| ref_count->SetRequestedSize(requested_size); |
| #else |
| (void)ref_count; |
| #endif |
| } |
| #endif // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT) |
| |
| return object; |
| } |
| |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE internal::UntaggedSlotStart PartitionRoot::RawAlloc( |
| Bucket* bucket, |
| size_t raw_size, |
| size_t slot_span_alignment, |
| size_t* usable_size, |
| size_t* slot_size, |
| bool* is_already_zeroed) { |
| internal::UntaggedSlotStart slot_start; |
| { |
| ::partition_alloc::internal::ScopedGuard guard{ |
| internal::PartitionRootLock(this)}; |
| slot_start = |
| AllocFromBucket<flags>(bucket, raw_size, slot_span_alignment, |
| usable_size, slot_size, is_already_zeroed); |
| } |
| |
| if (slot_start.value()) [[likely]] { |
| IncreaseTotalSizeOfAllocatedBytes(slot_start.value(), *slot_size, raw_size); |
| } |
| |
| return slot_start; |
| } |
| |
| PA_ALWAYS_INLINE size_t |
| PartitionRoot::GetAdjustedSizeForAlignment(size_t alignment, |
| size_t requested_size) { |
| // Aligned allocation support relies on the natural alignment guarantees of |
| // PartitionAlloc. Specifically, it relies on the fact that slots within a |
| // slot span are aligned to slot size, from the beginning of the span. |
| // |
| // For alignments <=PartitionPageSize(), the code below adjusts the request |
| // size to be a power of two, no less than alignment. Since slot spans are |
| // aligned to PartitionPageSize(), which is also a power of two, this will |
| // automatically guarantee alignment on the adjusted size boundary, thanks to |
| // the natural alignment described above. |
| // |
| // For alignments >PartitionPageSize(), we need to pass the request down the |
| // stack to only give us a slot span aligned to this more restrictive |
| // boundary. In the current implementation, this code path will always |
| // allocate a new slot span and hand us the first slot, so no need to adjust |
| // the request size. As a consequence, allocating many small objects with |
| // such a high alignment can cause a non-negligable fragmentation, |
| // particularly if these allocations are back to back. |
| // TODO(bartekn): We should check that this is not causing issues in practice. |
| // |
| // This relies on the fact that there are no extras before the allocation, as |
| // they'd shift the returned allocation from the beginning of the slot, thus |
| // messing up alignment. Extras after the allocation are acceptable, but they |
| // have to be taken into account in the request size calculation to avoid |
| // crbug.com/1185484. |
| |
| // This is mandated by |posix_memalign()|, so should never fire. |
| PA_CHECK(internal::base::bits::HasSingleBit(alignment)); |
| // Catch unsupported alignment requests early. |
| PA_CHECK(alignment <= internal::kMaxSupportedAlignment); |
| |
| // Memory returned by the regular allocator *always* respects |kAlignment|, |
| // which is a power of two, and any valid alignment is also a power of two. |
| // So we can use the requested_size as is. |
| if (alignment <= internal::kAlignment) { |
| return requested_size; |
| } |
| size_t raw_size = AdjustSizeForExtrasAdd(requested_size); |
| |
| size_t adjusted_size = requested_size; |
| if (alignment <= internal::PartitionPageSize()) { |
| // Handle cases such as size = 16, alignment = 64. |
| // Wastes memory when a large alignment is requested with a small size, but |
| // this is hard to avoid, and should not be too common. |
| if (raw_size < alignment) [[unlikely]] { |
| raw_size = alignment; |
| } else { |
| // PartitionAlloc only guarantees alignment for power-of-two sized |
| // allocations. To make sure this applies here, round up the allocation |
| // size. |
| raw_size = |
| static_cast<size_t>(1) |
| << (int{sizeof(size_t) * 8} - |
| partition_alloc::internal::base::bits::CountlZero(raw_size - 1)); |
| } |
| PA_DCHECK(internal::base::bits::HasSingleBit(raw_size)); |
| // Adjust back, because AllocInternalNoHooks/Alloc will adjust it again. |
| adjusted_size = AdjustSizeForExtrasSubtract(raw_size); |
| } |
| return adjusted_size; |
| } |
| |
| template <AllocFlags flags> |
| PA_ALWAYS_INLINE void* PartitionRoot::AlignedAllocInline( |
| size_t alignment, |
| size_t requested_size) { |
| auto adjusted_size = GetAdjustedSizeForAlignment(alignment, requested_size); |
| |
| // Overflow check. adjusted_size must be larger or equal to requested_size. |
| if (adjusted_size < requested_size) [[unlikely]] { |
| if constexpr (ContainsFlags(flags, AllocFlags::kReturnNull)) { |
| return nullptr; |
| } |
| // OutOfMemoryDeathTest.AlignedAlloc requires |
| // base::TerminateBecauseOutOfMemory (invoked by |
| // PartitionExcessiveAllocationSize). |
| internal::PartitionExcessiveAllocationSize(requested_size); |
| // internal::PartitionExcessiveAllocationSize(size) causes OOM_CRASH. |
| PA_NOTREACHED(); |
| } |
| |
| // Slot spans are naturally aligned on partition page size, but make sure you |
| // don't pass anything less, because it'll mess up callee's calculations. |
| size_t slot_span_alignment = |
| std::max(alignment, internal::PartitionPageSize()); |
| constexpr AllocFlags kMaybeAlignedAllocForMemoryTool = |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| AllocFlags::kAlignedAllocForMemoryTool; |
| #else |
| AllocFlags::kNone; |
| #endif |
| void* object = AllocInternal<flags | kMaybeAlignedAllocForMemoryTool>( |
| adjusted_size, slot_span_alignment, nullptr); |
| |
| // |alignment| is a power of two, but the compiler doesn't necessarily know |
| // that. A regular % operation is very slow, make sure to use the equivalent, |
| // faster form. |
| // No need to MTE-untag, as it doesn't change alignment. |
| PA_CHECK(!(reinterpret_cast<uintptr_t>(object) & (alignment - 1))); |
| |
| return object; |
| } |
| |
| template <AllocFlags alloc_flags, FreeFlags free_flags> |
| void* PartitionRoot::ReallocInline(void* ptr, |
| size_t new_size, |
| const char* type_name) { |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| if (!PartitionRoot::AllocWithMemoryToolProlog<alloc_flags>(new_size)) { |
| // Early return if AllocWithMemoryToolProlog returns false |
| return nullptr; |
| } |
| void* result = realloc(ptr, new_size); |
| if constexpr (!ContainsFlags(alloc_flags, AllocFlags::kReturnNull)) { |
| PA_CHECK(result); |
| } |
| return result; |
| #else |
| if (!ptr) [[unlikely]] { |
| return AllocInternal<alloc_flags>(new_size, internal::PartitionPageSize(), |
| type_name); |
| } |
| |
| if (!new_size) [[unlikely]] { |
| FreeInUnknownRoot<free_flags>(ptr); |
| return nullptr; |
| } |
| |
| if (new_size > internal::MaxDirectMapped()) { |
| if constexpr (ContainsFlags(alloc_flags, AllocFlags::kReturnNull)) { |
| return nullptr; |
| } |
| internal::PartitionExcessiveAllocationSize(new_size); |
| } |
| |
| constexpr bool no_hooks = ContainsFlags(alloc_flags, AllocFlags::kNoHooks); |
| const bool hooks_enabled = PartitionAllocHooks::AreHooksEnabled(); |
| bool overridden = false; |
| size_t old_usable_size = 0; |
| if (!no_hooks && hooks_enabled) [[unlikely]] { |
| overridden = PartitionAllocHooks::ReallocOverrideHookIfEnabled( |
| &old_usable_size, ptr); |
| } |
| if (!overridden) [[likely]] { |
| // |ptr| may have been allocated in another root. |
| SlotSpanMetadata* slot_span = SlotSpanMetadata::FromSlotStart( |
| internal::SlotStart::Unchecked(ptr).Untag()); |
| auto* old_root = PartitionRoot::FromSlotSpanMetadata(slot_span); |
| bool success = false; |
| bool tried_in_place_for_direct_map = false; |
| { |
| ::partition_alloc::internal::ScopedGuard guard{ |
| internal::PartitionRootLock(old_root)}; |
| PA_CHECK(DeducedRootIsValid(slot_span)); |
| old_usable_size = old_root->GetSlotUsableSize(slot_span); |
| |
| if (slot_span->bucket->is_direct_mapped()) [[unlikely]] { |
| 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 (!no_hooks && hooks_enabled) [[unlikely]] { |
| PartitionAllocHooks::ReallocObserverHookIfEnabled( |
| CreateFreeNotificationData(ptr), |
| CreateAllocationNotificationData(ptr, new_size, type_name)); |
| } |
| return ptr; |
| } |
| |
| if (!tried_in_place_for_direct_map) [[likely]] { |
| if (old_root->TryReallocInPlaceForNormalBuckets(ptr, slot_span, |
| new_size)) { |
| return ptr; |
| } |
| } |
| } |
| |
| #if PA_BUILDFLAG(REALLOC_GROWTH_FACTOR_MITIGATION) |
| // Some nVidia drivers have a performance bug where they repeatedly realloc a |
| // buffer with a small 4144 byte increment instead of using a growth factor to |
| // amortize the cost of a memcpy. To work around this, we apply a growth |
| // factor to the new size to avoid this issue. This workaround is only |
| // intended to be used for Skia bots, and is not intended to be a general |
| // solution. |
| if (new_size > old_usable_size) { |
| // 1.5x growth factor. |
| // Note that in case of integer overflow, the std::max ensures that the |
| // new_size is at least as large as the old_usable_size. |
| new_size = std::max(new_size, old_usable_size * 3 / 2); |
| } |
| #endif |
| |
| // This realloc cannot be resized in-place. Sadness. |
| void* ret = AllocInternal<alloc_flags>( |
| new_size, internal::PartitionPageSize(), type_name); |
| if (!ret) { |
| if constexpr (ContainsFlags(alloc_flags, AllocFlags::kReturnNull)) { |
| return nullptr; |
| } |
| internal::PartitionExcessiveAllocationSize(new_size); |
| } |
| |
| PA_UNSAFE_TODO(memcpy(ret, ptr, std::min(old_usable_size, new_size))); |
| FreeInUnknownRoot<free_flags>( |
| ptr); // Implicitly protects the old ptr on MTE systems. |
| return ret; |
| #endif |
| } |
| |
| // Return the capacity of the underlying slot (adjusted for extras) that'd be |
| // used to satisfy a request of |size|. This doesn't mean this capacity would be |
| // readily available. It merely means that if an allocation happened with that |
| // returned value, it'd use the same amount of underlying memory as the |
| // allocation with |size|. |
| PA_ALWAYS_INLINE size_t |
| PartitionRoot::AllocationCapacityFromRequestedSize(size_t size) const { |
| #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) |
| return size; |
| #else |
| PA_DCHECK(PartitionRoot::initialized_); |
| size = AdjustSizeForExtrasAdd(size); |
| auto& bucket = bucket_at(SizeToBucketIndex(size, GetBucketDistribution())); |
| PA_DCHECK(!bucket.slot_size || bucket.slot_size >= size); |
| PA_DCHECK(!(bucket.slot_size % internal::kAlignment)); |
| |
| if (!bucket.is_direct_mapped()) [[likely]] { |
| size = bucket.slot_size; |
| } else if (size > internal::MaxDirectMapped()) { |
| // Too large to allocate => return the size unchanged. |
| } else { |
| size = GetDirectMapSlotSize(size); |
| } |
| size = AdjustSizeForExtrasSubtract(size); |
| return size; |
| #endif |
| } |
| |
| ThreadCache* PartitionRoot::GetOrCreateThreadCache() |
| PA_LOCKS_EXCLUDED(thread_cache_construction_lock_) { |
| ThreadCache* thread_cache = nullptr; |
| if (settings_.with_thread_cache) [[likely]] { |
| thread_cache = ThreadCache::Get(settings_.thread_cache_index); |
| if (!ThreadCache::IsValid(thread_cache)) [[unlikely]] { |
| thread_cache = MaybeInitThreadCache(); |
| } |
| } |
| return thread_cache; |
| } |
| |
| ThreadCache* PartitionRoot::GetThreadCache() { |
| if (settings_.with_thread_cache) [[likely]] { |
| return ThreadCache::Get(settings_.thread_cache_index); |
| } |
| return nullptr; |
| } |
| |
| ThreadCache* PartitionRoot::EnsureThreadCache() |
| PA_LOCKS_EXCLUDED(thread_cache_construction_lock_) { |
| ThreadCache* thread_cache = nullptr; |
| if (settings_.with_thread_cache) [[likely]] { |
| thread_cache = ThreadCache::Get(settings_.thread_cache_index); |
| if (!ThreadCache::IsValid(thread_cache)) [[unlikely]] { |
| thread_cache = ForceInitThreadCache(); |
| } |
| } |
| return thread_cache; |
| } |
| |
| internal::SchedulerLoopQuarantineRoot& |
| PartitionRoot::GetSchedulerLoopQuarantineRoot() { |
| return scheduler_loop_quarantine_root_; |
| } |
| |
| // Explicitly declare common template instantiations to reduce compile time. |
| #define EXPORT_TEMPLATE \ |
| extern template PA_EXPORT_TEMPLATE_DECLARE( \ |
| PA_COMPONENT_EXPORT(PARTITION_ALLOC)) |
| EXPORT_TEMPLATE void* PartitionRoot::Alloc<AllocFlags::kNone>(size_t, |
| const char*); |
| EXPORT_TEMPLATE void* PartitionRoot::Alloc<AllocFlags::kReturnNull>( |
| size_t, |
| const char*); |
| EXPORT_TEMPLATE void* |
| PartitionRoot::Realloc<AllocFlags::kNone, FreeFlags::kNone>(void*, |
| size_t, |
| const char*); |
| EXPORT_TEMPLATE void* |
| PartitionRoot::Realloc<AllocFlags::kReturnNull, FreeFlags::kNone>(void*, |
| size_t, |
| const char*); |
| EXPORT_TEMPLATE void* PartitionRoot::AlignedAlloc<AllocFlags::kNone>(size_t, |
| size_t); |
| #undef EXPORT_TEMPLATE |
| |
| #if PA_BUILDFLAG(IS_APPLE) && PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| PA_COMPONENT_EXPORT(PARTITION_ALLOC) |
| void PartitionAllocMallocHookOnBeforeForkInParent(); |
| PA_COMPONENT_EXPORT(PARTITION_ALLOC) |
| void PartitionAllocMallocHookOnAfterForkInParent(); |
| PA_COMPONENT_EXPORT(PARTITION_ALLOC) |
| void PartitionAllocMallocHookOnAfterForkInChild(); |
| #endif // PA_BUILDFLAG(IS_APPLE) && PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| |
| } // namespace partition_alloc |
| |
| #endif // PARTITION_ALLOC_PARTITION_ROOT_H_ |