| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef BASE_DEBUG_ALLOCATION_TRACE_H_ |
| #define BASE_DEBUG_ALLOCATION_TRACE_H_ |
| |
| #include <algorithm> |
| #include <array> |
| #include <atomic> |
| #include <cstdint> |
| |
| #include "base/allocator/dispatcher/subsystem.h" |
| #include "base/allocator/partition_allocator/pointers/raw_ptr_exclusion.h" |
| #include "base/base_export.h" |
| #include "base/bits.h" |
| #include "base/compiler_specific.h" |
| #include "base/debug/debugging_buildflags.h" |
| #include "base/debug/stack_trace.h" |
| #include "build/build_config.h" |
| |
| namespace base::debug::tracer { |
| |
| // Number of traces that can be stored. This number must be a power of two to |
| // allow for fast computation of modulo. |
| constexpr size_t kMaximumNumberOfMemoryOperationTraces = (1 << 15); |
| // Number of frames stored for each operation. Probably the lower frames |
| // represent the memory allocation system. Hence, we store more frames to |
| // increase chances of having a meaningful trace of the path that caused the |
| // allocation or free. |
| constexpr size_t kStackTraceSize = 16; |
| |
| // The type of an operation stored in the recorder. |
| enum class OperationType { |
| // The state of an operation record before calling any of the initialization |
| // functions. |
| kNone = 0, |
| // The record represents an allocation operation. |
| kAllocation, |
| // The record represents a free operation. |
| kFree, |
| }; |
| |
| using StackTraceContainer = std::array<const void*, kStackTraceSize>; |
| |
| // The record for a single operation. A record can represent any type of |
| // operation, allocation or free, but not at the same time. |
| // |
| // A record protects itself from concurrent initializations. If a thread B calls |
| // any of the Initialize*-functions while another thread A is currently |
| // initializing, B's invocations shall immediately return |false| without |
| // interfering with thread A. |
| struct BASE_EXPORT OperationRecord { |
| // Is the record currently being taken? |
| bool IsRecording() const; |
| |
| OperationType GetOperationType() const; |
| // The address allocated or freed. |
| const void* GetAddress() const; |
| // Number of allocated bytes. Returns 0 for free operations. |
| size_t GetSize() const; |
| // The stacktrace as taken by the Initialize*-functions. |
| const StackTraceContainer& GetStackTrace() const; |
| |
| // Initialize the record with data for another operation. Data from any |
| // previous operation will be silently overwritten. These functions are |
| // declared ALWAYS_INLINE to minimize pollution of the recorded stack trace. |
| // |
| // Both functions return false in case no record was taken, i.e. if another |
| // thread is capturing. |
| ALWAYS_INLINE bool InitializeFree(const void* freed_address) { |
| return InitializeOperationRecord(freed_address, 0, OperationType::kFree); |
| } |
| |
| ALWAYS_INLINE bool InitializeAllocation(const void* allocated_address, |
| size_t allocated_size) { |
| return InitializeOperationRecord(allocated_address, allocated_size, |
| OperationType::kAllocation); |
| } |
| |
| private: |
| // Initialize a record with the given data. Return true if the record was |
| // initialized successfully, false if no record was taken, i.e. if another |
| // thread is capturing. |
| ALWAYS_INLINE bool InitializeOperationRecord(const void* address, |
| size_t size, |
| OperationType operation_type); |
| ALWAYS_INLINE void StoreStackTrace(); |
| |
| // The stack trace taken in one of the Initialize* functions. |
| StackTraceContainer stack_trace_ = {}; |
| // The number of allocated bytes. |
| size_t size_ = 0; |
| // The address that was allocated or freed. |
| // We use a raw C++ pointer instead of base::raw_ptr for performance |
| // reasons. |
| // - In the recorder we only store pointers, we never allocate or free on |
| // our own. |
| // - Storing is the hot path. base::raw_ptr::operator== may perform sanity |
| // checks which do not make sense in our case (otherwise the allocated |
| // address would have been quirky) |
| RAW_PTR_EXCLUSION const void* address_ = nullptr; |
| // The type of the operation that was performed. In the course of making a |
| // record, this value is reset to |OperationType::kNone| and later set to |
| // the operation type specific value, so if the process crashes whilst writing |
| // the record, it's marked as empty. To prevent the compiler from optimizing |
| // away the initial reset, this value is marked as volatile. |
| volatile OperationType operation_type_ = OperationType::kNone; |
| // Is the record currently being taken from another thread? Used to prevent |
| // concurrent writes to the same record. |
| // |
| // The value is mutable since pre C++20 there is no const getter in |
| // atomic_flag. All ways to get the value involve setting it. |
| // TODO(https://crbug.com/1284275): Remove mutable and make IsRecording() use |
| // atomic_flag::test(); |
| mutable std::atomic_flag is_recording_ = ATOMIC_FLAG_INIT; |
| }; |
| |
| ALWAYS_INLINE bool OperationRecord::InitializeOperationRecord( |
| const void* address, |
| size_t size, |
| OperationType operation_type) { |
| if (is_recording_.test_and_set(std::memory_order_acquire)) { |
| return false; |
| } |
| |
| operation_type_ = operation_type; |
| StoreStackTrace(); |
| address_ = address; |
| size_ = size; |
| |
| is_recording_.clear(std::memory_order_release); |
| |
| return true; |
| } |
| |
| ALWAYS_INLINE void OperationRecord::StoreStackTrace() { |
| stack_trace_.fill(nullptr); |
| |
| #if BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS) |
| // Currently we limit ourselves to use TraceStackFramePointers. We know that |
| // TraceStackFramePointers has an acceptable performance impact on Android. |
| base::debug::TraceStackFramePointers(&stack_trace_[0], stack_trace_.size(), |
| 0); |
| #elif BUILDFLAG(IS_LINUX) |
| // Use base::debug::CollectStackTrace as an alternative for tests on Linux. We |
| // still have a check in /base/debug/debug.gni to prevent that |
| // AllocationStackTraceRecorder is enabled accidentally on Linux. |
| base::debug::CollectStackTrace(&stack_trace_[0], stack_trace_.size()); |
| #else |
| #error "No supported stack tracer found." |
| #endif |
| } |
| |
| // The recorder which holds entries for past memory operations. |
| // |
| // The memory image of the recorder will be copied into the crash-handler. |
| // Therefore, it must not hold any references to external data which are vital |
| // for proper functioning. |
| // |
| // It is important that the recorder itself does not allocate to prevent |
| // recursive calls and save as much runtime overhead as possible. |
| // |
| // Therefore, records are stored in a preallocated buffer with a compile time |
| // constant maximum size, see |kMaximumNumberOfMemoryOperationTraces|. Once all |
| // records have been used, old records will be overwritten (fifo-style). |
| // |
| // The recorder works in an multithreaded environment without external locking. |
| // Concurrent writes are prevented by two means: |
| // 1 - We atomically increment and calculate the effective index of the record |
| // to be written. |
| // 2 - If this entry is still being used (the recording thread didn't finish |
| // yet), we go back to step 1 |
| // Currently we do not enforce separate cache lines for each entry, which means |
| // false sharing can occur. On the other hand, with 64 byte cachelines a clean |
| // separation would introduce some 3*64 - sizeof(OperationRecord) = 40 bytes of |
| // padding per entry. |
| // |
| // Note: As a process might be terminated for whatever reason while stack |
| // traces are being written, the recorded data may contain some garbage. |
| // |
| // TODO(https://crbug.com/1419908): Evaluate the impact of the shared cache |
| // lines between entries. |
| struct BASE_EXPORT AllocationTraceRecorder { |
| // Verify that the image of a recorder copied into the crashpad handler is |
| // still valid. IsValid compares the bytes of prologue and epilogue to |
| // expected values. |
| // |
| // Note: This is intended to protect from programming errors like using a |
| // wrong address or not copying the full recorder into the crash handler. |
| // |
| // TODO(https://crbug.com/1419908): Develop a full integration test which |
| // makes this IsValid check obsolete. |
| bool IsValid() const; |
| |
| // The allocation event observer interface. See the dispatcher for further |
| // details. The functions are marked NO_INLINE. All other functions called but |
| // the one taking the call stack are marked ALWAYS_INLINE. This way we ensure |
| // the number of frames recorded from these functions is fixed. |
| |
| // Handle all allocation events. |
| NOINLINE void OnAllocation( |
| const void* allocated_address, |
| size_t allocated_size, |
| base::allocator::dispatcher::AllocationSubsystem subsystem, |
| const char* type); |
| |
| // Handle all free events. |
| NOINLINE void OnFree(const void* freed_address); |
| |
| // Access functions to retrieve the current content of the recorder. |
| // Note: Since the recorder is usually updated upon each allocation or free, |
| // it is important to prevent updates if you want to read the entries at any |
| // point. |
| |
| // Get the current number of entries stored in the recorder. When the |
| // recorder has reached its maximum capacity, it always returns |
| // |GetMaximumNumberOfTraces()|. |
| size_t size() const; |
| |
| // Access the record of an operation by index. Oldest operation is always |
| // accessible at index 0, latest operation at |size()-1|. |
| // Note: Since a process might have crashed while a trace is being written, |
| // especially the last records might be corrupted. |
| const OperationRecord& operator[](size_t idx) const; |
| |
| constexpr size_t GetMaximumNumberOfTraces() const { |
| return kMaximumNumberOfMemoryOperationTraces; |
| } |
| |
| private: |
| // The guards are short byte sequences which are stored in the beginning and |
| // at the end of AllocationTraceRecorder. They are used to ensure the memory |
| // image copied to crashpad handler is valid. |
| static constexpr uint64_t kMemoryGuard = 0x5A55A55A55A55A5A; |
| |
| ALWAYS_INLINE size_t GetNextIndex(); |
| |
| ALWAYS_INLINE static constexpr size_t WrapIdxIfNeeded(size_t idx); |
| |
| // Used to ensure validity after copying memory-image into crashpad-handler. |
| // It is declared volatile to prevent the compiler from taking shortcuts |
| // when checking the guards, since they are never written explicitly. |
| uint64_t const volatile prologue_ = kMemoryGuard; |
| // The actual container. |
| std::array<OperationRecord, kMaximumNumberOfMemoryOperationTraces> |
| alloc_trace_buffer_; |
| // The total number of records that have been taken so far. Note that this |
| // might be greater than |kMaximumNumberOfMemoryOperationTraces| since we |
| // overwrite oldest items. |
| std::atomic<size_t> total_number_of_records_ = 0; |
| // Used to ensure validity after copying memory-image into crashpad-handler. |
| // It is declared volatile to prevent the compiler from taking shortcuts |
| // when checking the guards, since they are never written explicitly. |
| uint64_t const volatile epilogue_ = kMemoryGuard; |
| }; |
| |
| ALWAYS_INLINE constexpr size_t AllocationTraceRecorder::WrapIdxIfNeeded( |
| size_t idx) { |
| // Wrapping around counter, e.g. for BUFFER_SIZE = 256, the counter will |
| // wrap around when reaching 256. To enable the compiler to emit more |
| // optimized code we assert |kMaximumNumberOfMemoryOperationTraces| is a power |
| // of two . |
| static_assert( |
| base::bits::IsPowerOfTwo(kMaximumNumberOfMemoryOperationTraces), |
| "kMaximumNumberOfMemoryOperationTraces should be a power of 2 to " |
| "allow for fast modulo operation."); |
| |
| return idx % kMaximumNumberOfMemoryOperationTraces; |
| } |
| |
| ALWAYS_INLINE size_t AllocationTraceRecorder::GetNextIndex() { |
| const auto raw_idx = |
| total_number_of_records_.fetch_add(1, std::memory_order_relaxed); |
| return WrapIdxIfNeeded(raw_idx); |
| } |
| |
| } // namespace base::debug::tracer |
| |
| #endif // BASE_DEBUG_ALLOCATION_TRACE_H_ |