blob: 07c59be0ffac05f4610e391537d0d9f72fb7cbfb [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/gwp_asan/client/sampling_allocator_shims.h"
#include "base/allocator/allocator_shim.h"
#include "base/allocator/buildflags.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_math.h"
#include "base/process/process_metrics.h"
#include "base/rand_util.h"
#include "build/build_config.h"
#include "components/gwp_asan/client/crash_key.h"
#include "components/gwp_asan/client/export.h"
#include "components/gwp_asan/client/guarded_page_allocator.h"
#if defined(OS_MACOSX)
// TODO(https://crbug.com/829078): thread_local is not currently supported on
// macOS; however, it works correctly on other platforms and is noticeably
// faster.
#error "macOS does not support thread_local"
#endif
namespace gwp_asan {
namespace internal {
namespace {
using base::allocator::AllocatorDispatch;
// Class that encapsulates the current sampling state. Sampling is performed
// using a counter stored in thread-local storage.
class SamplingState {
public:
constexpr SamplingState() {}
void Init(size_t sampling_frequency) {
DCHECK_GT(sampling_frequency, 0U);
sampling_frequency_ = sampling_frequency;
}
// Return true if this allocation should be sampled.
ALWAYS_INLINE bool Sample() {
// For a new thread the initial TLS value will be zero, we do not want to
// sample on zero as it will always sample the first allocation on thread
// creation and heavily bias allocations towards that particular call site.
//
// Instead, use zero to mean 'get a new counter value' and one to mean
// that this allocation should be sampled.
static thread_local size_t tls_counter = 0;
size_t samples_left = tls_counter;
if (UNLIKELY(!samples_left))
samples_left = NextSample();
tls_counter = samples_left - 1;
return (samples_left == 1);
}
private:
// Sample a single allocations in every chunk of |sampling_frequency_|
// allocations.
//
// TODO(https://crbug.com/919207): Replace with std::geometric_distribution
// once the LLVM floating point codegen issue in the linked bug is fixed.
size_t NextSample() {
size_t random = base::RandInt(1, sampling_frequency_ + 1);
size_t next_sample = increment_ + random;
increment_ = sampling_frequency_ + 1 - random;
return next_sample;
}
size_t sampling_frequency_ = 0;
// Stores the number of allocations we need to skip to reach the end of the
// current chunk of |sampling_frequency_| allocations.
size_t increment_ = 0;
};
// By being implemented as a global with inline method definitions, method calls
// and member acceses are inlined and as efficient as possible in the
// performance-sensitive allocation hot-path.
//
// Note that this optimization has not been benchmarked. However since it is
// easy to do there is no reason to pay the extra cost.
SamplingState sampling_state;
// The global allocator singleton used by the shims. Implemented as a global
// pointer instead of a function-local static to avoid initialization checks
// for every access.
GuardedPageAllocator* gpa = nullptr;
void* AllocFn(const AllocatorDispatch* self, size_t size, void* context) {
if (UNLIKELY(sampling_state.Sample()))
if (void* allocation = gpa->Allocate(size))
return allocation;
return self->next->alloc_function(self->next, size, context);
}
void* AllocZeroInitializedFn(const AllocatorDispatch* self,
size_t n,
size_t size,
void* context) {
if (UNLIKELY(sampling_state.Sample())) {
base::CheckedNumeric<size_t> checked_total = size;
checked_total *= n;
if (UNLIKELY(!checked_total.IsValid()))
return nullptr;
size_t total_size = checked_total.ValueOrDie();
if (void* allocation = gpa->Allocate(total_size)) {
memset(allocation, 0, total_size);
return allocation;
}
}
return self->next->alloc_zero_initialized_function(self->next, n, size,
context);
}
void* AllocAlignedFn(const AllocatorDispatch* self,
size_t alignment,
size_t size,
void* context) {
if (UNLIKELY(sampling_state.Sample()))
if (void* allocation = gpa->Allocate(size, alignment))
return allocation;
return self->next->alloc_aligned_function(self->next, alignment, size,
context);
}
void* ReallocFn(const AllocatorDispatch* self,
void* address,
size_t size,
void* context) {
if (UNLIKELY(!address))
return AllocFn(self, size, context);
if (LIKELY(!gpa->PointerIsMine(address)))
return self->next->realloc_function(self->next, address, size, context);
if (!size) {
gpa->Deallocate(address);
return nullptr;
}
void* new_alloc = gpa->Allocate(size);
if (!new_alloc)
new_alloc = self->next->alloc_function(self->next, size, context);
if (!new_alloc)
return nullptr;
memcpy(new_alloc, address, std::min(size, gpa->GetRequestedSize(address)));
gpa->Deallocate(address);
return new_alloc;
}
void FreeFn(const AllocatorDispatch* self, void* address, void* context) {
if (UNLIKELY(gpa->PointerIsMine(address)))
return gpa->Deallocate(address);
self->next->free_function(self->next, address, context);
}
size_t GetSizeEstimateFn(const AllocatorDispatch* self,
void* address,
void* context) {
if (UNLIKELY(gpa->PointerIsMine(address)))
return gpa->GetRequestedSize(address);
return self->next->get_size_estimate_function(self->next, address, context);
}
unsigned BatchMallocFn(const AllocatorDispatch* self,
size_t size,
void** results,
unsigned num_requested,
void* context) {
// The batch_malloc() routine is esoteric and only accessible for the system
// allocator's zone, GWP-ASan interception is not provided.
return self->next->batch_malloc_function(self->next, size, results,
num_requested, context);
}
void BatchFreeFn(const AllocatorDispatch* self,
void** to_be_freed,
unsigned num_to_be_freed,
void* context) {
// A batch_free() hook is implemented because it is imperative that we never
// call free() with a GWP-ASan allocation.
for (size_t i = 0; i < num_to_be_freed; i++) {
if (UNLIKELY(gpa->PointerIsMine(to_be_freed[i]))) {
// If this batch includes guarded allocations, call free() on all of the
// individual allocations to ensure the guarded allocations are handled
// correctly.
for (size_t j = 0; j < num_to_be_freed; j++)
FreeFn(self, to_be_freed[j], context);
return;
}
}
self->next->batch_free_function(self->next, to_be_freed, num_to_be_freed,
context);
}
void FreeDefiniteSizeFn(const AllocatorDispatch* self,
void* address,
size_t size,
void* context) {
if (UNLIKELY(gpa->PointerIsMine(address))) {
// TODO(vtsyrklevich): Perform this check in GuardedPageAllocator and report
// failed checks using the same pipeline.
CHECK_EQ(size, gpa->GetRequestedSize(address));
gpa->Deallocate(address);
return;
}
self->next->free_definite_size_function(self->next, address, size, context);
}
static void* AlignedMallocFn(const AllocatorDispatch* self,
size_t size,
size_t alignment,
void* context) {
if (UNLIKELY(sampling_state.Sample()))
if (void* allocation = gpa->Allocate(size, alignment))
return allocation;
return self->next->aligned_malloc_function(self->next, size, alignment,
context);
}
static void* AlignedReallocFn(const AllocatorDispatch* self,
void* address,
size_t size,
size_t alignment,
void* context) {
if (UNLIKELY(!address))
return AlignedMallocFn(self, size, alignment, context);
if (LIKELY(!gpa->PointerIsMine(address)))
return self->next->aligned_realloc_function(self->next, address, size,
alignment, context);
if (!size) {
gpa->Deallocate(address);
return nullptr;
}
void* new_alloc = gpa->Allocate(size, alignment);
if (!new_alloc)
new_alloc = self->next->aligned_malloc_function(self->next, size, alignment,
context);
if (!new_alloc)
return nullptr;
memcpy(new_alloc, address, std::min(size, gpa->GetRequestedSize(address)));
gpa->Deallocate(address);
return new_alloc;
}
static void AlignedFreeFn(const AllocatorDispatch* self,
void* address,
void* context) {
if (UNLIKELY(gpa->PointerIsMine(address)))
return gpa->Deallocate(address);
self->next->aligned_free_function(self->next, address, context);
}
AllocatorDispatch g_allocator_dispatch = {
&AllocFn,
&AllocZeroInitializedFn,
&AllocAlignedFn,
&ReallocFn,
&FreeFn,
&GetSizeEstimateFn,
&BatchMallocFn,
&BatchFreeFn,
&FreeDefiniteSizeFn,
&AlignedMallocFn,
&AlignedReallocFn,
&AlignedFreeFn,
nullptr /* next */
};
} // namespace
// We expose the allocator singleton for unit tests.
GWP_ASAN_EXPORT GuardedPageAllocator& GetGpaForTesting() {
return *gpa;
}
void InstallAllocatorHooks(size_t max_allocated_pages,
size_t total_pages,
size_t sampling_frequency) {
#if BUILDFLAG(USE_ALLOCATOR_SHIM)
gpa = new GuardedPageAllocator();
gpa->Init(max_allocated_pages, total_pages);
RegisterAllocatorAddress(gpa->GetCrashKeyAddress());
sampling_state.Init(sampling_frequency);
base::allocator::InsertAllocatorDispatch(&g_allocator_dispatch);
#else
ignore_result(g_allocator_dispatch);
ignore_result(gpa);
DLOG(WARNING) << "base::allocator shims are unavailable for GWP-ASan.";
#endif // BUILDFLAG(USE_ALLOCATOR_SHIM)
}
} // namespace internal
} // namespace gwp_asan