diff --git a/compiler-rt/cmake/config-ix.cmake b/compiler-rt/cmake/config-ix.cmake --- a/compiler-rt/cmake/config-ix.cmake +++ b/compiler-rt/cmake/config-ix.cmake @@ -238,6 +238,7 @@ set(ALL_FUZZER_SUPPORTED_ARCH ${X86_64} ${ARM64}) endif() +set(ALL_GWP_ASAN_SUPPORTED_ARCH ${X86} ${X86_64} ${ARM64} ${ARM32}) if(APPLE) set(ALL_LSAN_SUPPORTED_ARCH ${X86} ${X86_64} ${MIPS64} ${ARM64}) else() @@ -437,6 +438,9 @@ list_intersect(DFSAN_SUPPORTED_ARCH ALL_DFSAN_SUPPORTED_ARCH SANITIZER_COMMON_SUPPORTED_ARCH) + list_intersect(GWP_ASAN_SUPPORTED_ARCH + ALL_GWP_ASAN_SUPPORTED_ARCH + SANITIZER_COMMON_SUPPORTED_ARCH) list_intersect(LSAN_SUPPORTED_ARCH ALL_LSAN_SUPPORTED_ARCH SANITIZER_COMMON_SUPPORTED_ARCH) @@ -505,6 +509,7 @@ filter_available_targets(XRAY_SUPPORTED_ARCH ${ALL_XRAY_SUPPORTED_ARCH}) filter_available_targets(SHADOWCALLSTACK_SUPPORTED_ARCH ${ALL_SHADOWCALLSTACK_SUPPORTED_ARCH}) + filter_available_targets(GWP_ASAN_SUPPORTED_ARCH ${ALL_GWP_ASAN_SUPPORTED_ARCH}) endif() if (MSVC) @@ -532,7 +537,7 @@ set(OS_NAME "${CMAKE_SYSTEM_NAME}") endif() -set(ALL_SANITIZERS asan;dfsan;msan;hwasan;tsan;safestack;cfi;scudo;ubsan_minimal) +set(ALL_SANITIZERS asan;dfsan;msan;hwasan;tsan;safestack;cfi;scudo;ubsan_minimal;gwp_asan) set(COMPILER_RT_SANITIZERS_TO_BUILD all CACHE STRING "sanitizers to build if supported on the target (all;${ALL_SANITIZERS})") list_replace(COMPILER_RT_SANITIZERS_TO_BUILD all "${ALL_SANITIZERS}") @@ -676,3 +681,13 @@ else() set(COMPILER_RT_HAS_SHADOWCALLSTACK FALSE) endif() + +# Note: Fuchsia and Windows are not currently supported by GWP-ASan. Support +# is planned for these platforms. Darwin is also not supported due to TLS +# calling malloc on first use. +if (GWP_ASAN_SUPPORTED_ARCH AND OS_NAME MATCHES "Android|Linux") + set(COMPILER_RT_HAS_GWP_ASAN TRUE) +else() + set(COMPILER_RT_HAS_GWP_ASAN FALSE) +endif() +pythonize_bool(COMPILER_RT_HAS_GWP_ASAN) diff --git a/compiler-rt/include/CMakeLists.txt b/compiler-rt/include/CMakeLists.txt --- a/compiler-rt/include/CMakeLists.txt +++ b/compiler-rt/include/CMakeLists.txt @@ -5,6 +5,7 @@ sanitizer/common_interface_defs.h sanitizer/coverage_interface.h sanitizer/dfsan_interface.h + sanitizer/gwp_asan_interface.h sanitizer/hwasan_interface.h sanitizer/linux_syscall_hooks.h sanitizer/lsan_interface.h diff --git a/compiler-rt/include/sanitizer/gwp_asan_interface.h b/compiler-rt/include/sanitizer/gwp_asan_interface.h new file mode 100644 --- /dev/null +++ b/compiler-rt/include/sanitizer/gwp_asan_interface.h @@ -0,0 +1,41 @@ +//===-- sanitizer/gwp_asan_interface.h --------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef SANITIZER_GWP_ASAN_INTERFACE_H +#define SANITIZER_GWP_ASAN_INTERFACE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// See GuardedPoolAllocator::shouldSample() for more details. Returns 'false' if +// the GPA is not initialised. +bool __gwp_asan_pointer_is_mine(void *Ptr); + +// See GuardedPoolAllocator::allocate() for more details. Returns nullptr if +// the GPA is not initialised. +void *__gwp_asan_allocate(std::size_t Size); + +// See GuardedPoolAllocator::deallocate() for more details. +void __gwp_asan_deallocate(void *Ptr); + +// See GuardedPoolAllocator::getSize() for more details. Returns 0 if the GPA is +// not initialised or the pointer is not owned by the GPA. +std::size_t __gwp_asan_get_size(const void *Ptr); + +// See GuardedPoolAllocator::maximumAllocationSize() for more details. Returns 0 +// if the GPA is not initialised. +std::size_t __gwp_asan_maximum_allocation_size(void); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // SANITIZER_GWP_ASAN_INTERFACE_H diff --git a/compiler-rt/lib/gwp_asan/CMakeLists.txt b/compiler-rt/lib/gwp_asan/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/CMakeLists.txt @@ -0,0 +1,66 @@ +add_compiler_rt_component(gwp_asan) + +include_directories(..) + +set(GWP_ASAN_SOURCES + guarded_pool_allocator.cpp + guarded_pool_allocator_posix.cpp + random.cpp +) + +set(GWP_ASAN_HEADERS + guarded_pool_allocator.h + mutex.h + options.h + options.inc + random.h +) + +# Disable RTTI and exception support, as we want these libraries to be +# C-compatible. Regular C source files can be linked against the generated +# GwpAsan libraries using the Clang C compiler. +set(GWP_ASAN_CFLAGS -fno-rtti -fno-exceptions) + +# Options parsing support is optional. GwpAsan is totally independent of +# sanitizer_common, the options parser is not. This is an optional library +# that can be used by an allocator to automatically parse GwpAsan options from +# the environment variable GWP_ASAN_FLAGS, but the allocator can choose to +# implement its own options parsing and populate the Options struct itself. +set(GWP_ASAN_OPTIONS_PARSER_SOURCES + optional/options_parser.cpp +) +set(GWP_ASAN_OPTIONS_PARSER_HEADERS + optional/options_parser.h +) +set(GWP_ASAN_OPTIONS_PARSER_CFLAGS + ${GWP_ASAN_CFLAGS} + ${SANITIZER_COMMON_CFLAGS}) + +if (COMPILER_RT_HAS_GWP_ASAN) + foreach(arch ${GWP_ASAN_SUPPORTED_ARCH}) + add_compiler_rt_runtime( + clang_rt.gwp_asan + STATIC + ARCHS ${arch} + SOURCES ${GWP_ASAN_SOURCES} + ADDITIONAL_HEADERS ${GWP_ASAN_HEADERS} + CFLAGS ${GWP_ASAN_CFLAGS} + PARENT_TARGET gwp_asan + ) + endforeach() + + add_compiler_rt_object_libraries(RTGwpAsan + ARCHS ${GWP_ASAN_SUPPORTED_ARCH} + SOURCES ${GWP_ASAN_SOURCES} + ADDITIONAL_HEADERS ${GWP_ASAN_HEADERS} + CFLAGS ${GWP_ASAN_CFLAGS}) + + # Note: If you choose to add this as an object library, ensure you also + # include the sanitizer_common flag parsing object lib + # 'RTSanitizerCommonNoTermination'. + add_compiler_rt_object_libraries(RTGwpAsanOptionsParser + ARCHS ${GWP_ASAN_SUPPORTED_ARCH} + SOURCES ${GWP_ASAN_OPTIONS_PARSER_SOURCES} + ADDITIONAL_HEADERS ${GWP_ASAN_OPTIONS_PARSER_HEADERS} + CFLAGS ${GWP_ASAN_OPTIONS_PARSER_CFLAGS}) +endif() diff --git a/compiler-rt/lib/gwp_asan/definitions.h b/compiler-rt/lib/gwp_asan/definitions.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/definitions.h @@ -0,0 +1,34 @@ +//===-- gwp_asan_definitions.h ----------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_DEFINITIONS_H_ +#define GWP_ASAN_DEFINITIONS_H_ + +#define TLS_INITIAL_EXEC thread_local __attribute__((tls_model("initial-exec"))) + +#ifdef LIKELY +# undef LIKELY +#endif // defined(LIKELY) +#define LIKELY(X) __builtin_expect(!!(X), 1) + +#ifdef UNLIKELY +# undef UNLIKELY +#endif // defined(UNLIKELY) +#define UNLIKELY(X) __builtin_expect(!!(X), 0) + +#ifdef ALWAYS_INLINE +# undef ALWAYS_INLINE +#endif // defined(ALWAYS_INLINE) +#define ALWAYS_INLINE inline __attribute__((always_inline)) + +#ifdef ALIGNED +# undef ALIGNED +#endif // defined(ALIGNED) +#define ALIGNED(X) __attribute__((aligned(X))) + +#endif // GWP_ASAN_DEFINITIONS_H_ diff --git a/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h @@ -0,0 +1,256 @@ +//===-- guarded_pool_allocator.h --------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_GUARDED_POOL_ALLOCATOR_H_ +#define GWP_ASAN_GUARDED_POOL_ALLOCATOR_H_ + +#include "gwp_asan/definitions.h" +#include "gwp_asan/mutex.h" +#include "gwp_asan/options.h" +#include "gwp_asan/random.h" + +#include +#include +#include + +namespace gwp_asan { +// This class is the primary implementation of the allocator portion of GWP- +// ASan. It is the sole owner of the pool of sequentially allocated guarded +// slots. It should always be treated as a singleton. + +// Functions in the public interface of this class are thread-compatible until +// init() is called, at which point they become thread-safe (unless specified +// otherwise). +class GuardedPoolAllocator { +public: + static constexpr uint64_t kInvalidThreadID = UINT64_MAX; + + enum class Error { + UNKNOWN, + USE_AFTER_FREE, + DOUBLE_FREE, + INVALID_FREE, + BUFFER_OVERFLOW, + BUFFER_UNDERFLOW + }; + + struct AllocationMetadata { + // Maximum number of stack trace frames to collect for allocations + frees. + // TODO(hctim): Implement stack frame compression, a-la Chromium. + // Currently the maximum stack frames is one, as we don't collect traces. + static constexpr std::size_t kMaximumStackFrames = 1; + + // Records the given allocation metadata into this struct. In the future, + // this will collect the allocation trace as well. + void RecordAllocation(uintptr_t Addr, std::size_t Size); + + // Record that this allocation is now deallocated. In future, this will + // collect the deallocation trace as well. + void RecordDeallocation(); + + struct CallSiteInfo { + // The backtrace to the allocation/deallocation. If the first value is + // zero, we did not collect a trace. + uintptr_t Trace[kMaximumStackFrames] = {}; + // The thread ID for this trace, or kInvalidThreadID if not available. + uint64_t ThreadID = kInvalidThreadID; + }; + + // The address of this allocation. + uintptr_t Addr = 0; + // Represents the actual size of the allocation. + std::size_t Size = 0; + + CallSiteInfo AllocationTrace; + CallSiteInfo DeallocationTrace; + + // Whether this allocation has been deallocated yet. + bool IsDeallocated = false; + }; + + // During program startup, we must ensure that memory allocations do not land + // in this allocation pool if the allocator decides to runtime-disable + // GWP-ASan. The constructor value-initialises the class such that if no + // further initialisation takes place, calls to shouldSample() and + // pointerIsMine() will return false. + constexpr GuardedPoolAllocator(){}; + GuardedPoolAllocator(const GuardedPoolAllocator &) = delete; + GuardedPoolAllocator &operator=(const GuardedPoolAllocator &) = delete; + + // Note: This class is expected to be a singleton for the lifetime of the + // program. If this object is initialised, it will leak the guarded page pool + // and metadata allocations during destruction. We can't clean up these areas + // as this may cause a use-after-free on shutdown. + ~GuardedPoolAllocator() = default; + + // Initialise the rest of the members of this class. Create the allocation + // pool using the provided options. See options.inc for runtime configuration + // options. + void init(const options::Options &Opts); + + // Return whether the allocation should be randomly chosen for sampling. + ALWAYS_INLINE bool shouldSample() { + // NextSampleCounter == 0 means we "should regenerate the counter". + // == 1 means we "should sample this allocation". + if (UNLIKELY(NextSampleCounter == 0)) + NextSampleCounter = + (random::getRandomUnsigned64() % AdjustedSampleRate) + 1; + + // GuardedPagePool != 0 if GWP-ASan is enabled. + return UNLIKELY(NextSampleCounter-- == 1) && LIKELY(GuardedPagePool != 0); + } + + // Returns whether the provided pointer is a current sampled allocation that + // is owned by this pool. + ALWAYS_INLINE bool pointerIsMine(const void *Ptr) const { + uintptr_t P = reinterpret_cast(Ptr); + return GuardedPagePool <= P && P < GuardedPagePoolEnd; + } + + // Allocate memory in a guarded slot, and return a pointer to the new + // allocation. Returns nullptr if the pool is empty, the requested size is too + // large for this pool to handle, or the requested size is zero. + void *allocate(std::size_t Size); + + // Deallocate memory in a guarded slot. The provided pointer must have been + // allocated using this pool. This will set the guarded slot as inaccessible. + void deallocate(void *Ptr); + + // Returns the size of the allocation at Ptr. + std::size_t getSize(const void *Ptr); + + // Returns the largest allocation that is supported by this pool. Any + // allocations larger than this should go to the regular system allocator. + std::size_t maximumAllocationSize() const; + + // Dumps an error report (including allocation and deallocation stack traces). + // An optional error may be provided if the caller knows what the error is + // ahead of time. This is primarily a helper function to locate the static + // singleton pointer and call the internal version of this function. This + // method is never thread safe, and should only be called when fatal errors + // occur. + static void reportError(uintptr_t AccessPtr, Error Error = Error::UNKNOWN); + +private: + static constexpr std::size_t kInvalidSlotID = SIZE_MAX; + + // These functions anonymously map memory or change the permissions of mapped + // memory into this process in a platform- specific way. Pointer and size + // arguments are expected to be page-aligned. These functions will never + // return on error, instead electing to kill the calling process on failure. + // Note that memory is initially mapped inaccessible. In order for RW + // mappings, call mapMemory() followed by markReadWrite() on the returned + // pointer. + void *mapMemory(std::size_t Size) const; + void markReadWrite(void *Ptr, std::size_t Size) const; + void markInaccessible(void *Ptr, std::size_t Size) const; + + // Get the current thread ID, or kInvalidThreadID if failure. Note: This + // implementation is platform-specific. + static uint64_t getThreadID(); + + // Get the page size from the platform-specific implementation. Only needs to + // be called once, and the result should be cached in PageSize in this class. + static std::size_t getPlatformPageSize(); + + // Install the SIGSEGV crash handler for printing use-after-free and heap- + // buffer-{under|over}flow exceptions. This is platform specific as even + // though POSIX and Windows both support registering handlers through + // signal(), we have to use platform-specific signal handlers to obtain the + // address that caused the SIGSEGV exception. + static void installSignalHandlers(); + + // Returns the index of the slot that this pointer resides in. If the pointer + // is not owned by this pool, the result is undefined. + std::size_t addrToSlot(uintptr_t Ptr) const; + + // Returns the address of to the N-th guarded slot. + uintptr_t slotToAddr(std::size_t N) const; + + // Returns a pointer to the metadata for the owned pointer. If the pointer is + // not owned by this pool, the result is undefined. + AllocationMetadata *addrToMetadata(uintptr_t Ptr) const; + + // Returns the address of the page that this pointer resides in. + uintptr_t getPageAddr(uintptr_t Ptr) const; + + // Gets the nearest slot to the provided address. + std::size_t getNearestSlot(uintptr_t Ptr) const; + + // Returns whether the provided pointer is a guard page or not. The pointer + // must be within memory owned by this pool, else the result is undefined. + bool isGuardPage(uintptr_t Ptr) const; + + // Reserve a slot for a new guarded allocation. Returns kInvalidSlotID if no + // slot is available to be reserved. + std::size_t reserveSlot(); + + // Unreserve the guarded slot. + void freeSlot(std::size_t SlotIndex); + + // Returns the offset (in bytes) between the start of a guarded slot and where + // the start of the allocation should take place. Determined using the size of + // the allocation and the options provided at init-time. + uintptr_t allocationSlotOffset(std::size_t AllocationSize); + + // Returns the diagnosis for an unknown error. If the diagnosis is not + // Error::INVALID_FREE or Error::UNKNOWN, the metadata for the slot + // responsible for the error is placed in *Meta. + Error diagnoseUnknownError(uintptr_t AccessPtr, AllocationMetadata **Meta); + + void reportErrorInternal(uintptr_t AccessPtr, Error Error); + + // Cached page size for this system in bytes. + std::size_t PageSize = 0; + + // A mutex to protect the guarded slot and metadata pool for this class. + Mutex PoolMutex; + // The number of guarded slots that this pool holds. + std::size_t MaxSimultaneousAllocations = 0; + // Record the number allocations that we've sampled. We store this amount so + // that we don't randomly choose to recycle a slot that previously had an + // allocation before all the slots have been utilised. + std::size_t NumSampledAllocations = 0; + // Pointer to the pool of guarded slots. Note that this points to the start of + // the pool (which is a guard page), not a pointer to the first guarded page. + uintptr_t GuardedPagePool = UINTPTR_MAX; + uintptr_t GuardedPagePoolEnd = 0; + // Pointer to the allocation metadata (allocation/deallocation stack traces), + // if any. + AllocationMetadata *Metadata = nullptr; + + // Pointer to an array of free slot indexes. + std::size_t *FreeSlots = nullptr; + // The current length of the list of free slots. + std::size_t FreeSlotsLength = 0; + + // See options.{h, inc} for more information. + bool PerfectlyRightAlign = false; + + // Printf function supplied by the implementing allocator. We can't (in + // general) use printf() from the cstdlib as it may malloc(), causing infinite + // recursion. + options::Printf_t Printf = nullptr; + + // The adjusted sample rate for allocation sampling. Default *must* be + // nonzero, as dynamic initialisation may call malloc (e.g. from libstdc++) + // before GPA::init() is called. This would cause an error in shouldSample(), + // where we would calculate modulo zero. This value is set UINT64_MAX, as when + // GWP-ASan is disabled, we wish to never spend wasted cycles recalculating + // the sample rate. + ALIGNED(8) uint64_t AdjustedSampleRate = UINT64_MAX; + // Thread-local decrementing counter that indicates that a given allocation + // should be sampled when it reaches zero. + static TLS_INITIAL_EXEC uint64_t NextSampleCounter; +}; + +static_assert(std::is_trivially_destructible::value, + "GuardedPoolAllocator must be trivially destructible."); +} // namespace gwp_asan + +#endif // GWP_ASAN_GUARDED_POOL_ALLOCATOR_H_ diff --git a/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp @@ -0,0 +1,450 @@ +//===-- guarded_pool_allocator.cpp ------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "gwp_asan/guarded_pool_allocator.h" + +#include "gwp_asan/gwp_asan_interface_internal.h" +#include "gwp_asan/options.h" + +#include +#include +#include +#include +#include + +using AllocationMetadata = gwp_asan::GuardedPoolAllocator::AllocationMetadata; +using Error = gwp_asan::GuardedPoolAllocator::Error; + +namespace gwp_asan { +namespace { +// Forward declare the pointer to the singleton version of this class. +// Instantiated during initialisation, this allows the signal handler +// to find this class in order to deduce the root cause of failures. Must not be +// referenced by users outside this translation unit, in order to avoid +// init-order-fiasco. +GuardedPoolAllocator *SingletonPtr = nullptr; +} // anonymous namespace + +// Gets the singleton implementation of this class. Thread-compatible until +// init() is called, thread-safe afterwards. +GuardedPoolAllocator *getSingleton() { return SingletonPtr; } + +void GuardedPoolAllocator::AllocationMetadata::RecordAllocation( + uintptr_t AllocAddr, std::size_t AllocSize) { + Addr = AllocAddr; + Size = AllocSize; + IsDeallocated = false; + + // TODO(hctim): Implement stack trace collection. + AllocationTrace.ThreadID = getThreadID(); + DeallocationTrace.ThreadID = kInvalidThreadID; + AllocationTrace.Trace[0] = 0; + DeallocationTrace.Trace[0] = 0; +} + +void GuardedPoolAllocator::AllocationMetadata::RecordDeallocation() { + IsDeallocated = true; + // TODO(hctim): Implement stack trace collection. + DeallocationTrace.ThreadID = getThreadID(); +} + +void GuardedPoolAllocator::init(const options::Options &Opts) { + // Note: We return from the constructor here if GWP-ASan is not available. + // This will stop heap-allocation of class members, as well as mmap() of the + // guarded slots. + if (!Opts.Enabled || Opts.SampleRate == 0 || + Opts.MaxSimultaneousAllocations == 0) + return; + + // TODO(hctim): Add a death unit test for this. + if (SingletonPtr) { + (*SingletonPtr->Printf)( + "GWP-ASan Error: init() has already been called.\n"); + exit(EXIT_FAILURE); + } + + if (Opts.SampleRate < 0) { + Opts.Printf("GWP-ASan Error: SampleRate is < 0.\n"); + exit(EXIT_FAILURE); + } + + if (Opts.MaxSimultaneousAllocations < 0) { + Opts.Printf("GWP-ASan Error: MaxSimultaneousAllocations is < 0.\n"); + exit(EXIT_FAILURE); + } + + SingletonPtr = this; + + MaxSimultaneousAllocations = Opts.MaxSimultaneousAllocations; + + PageSize = getPlatformPageSize(); + + PerfectlyRightAlign = Opts.PerfectlyRightAlign; + Printf = Opts.Printf; + + std::size_t PoolBytesRequired = + PageSize * (1 + MaxSimultaneousAllocations) + + MaxSimultaneousAllocations * maximumAllocationSize(); + void *GuardedPoolMemory = mapMemory(PoolBytesRequired); + + std::size_t BytesRequired = MaxSimultaneousAllocations * sizeof(*Metadata); + Metadata = reinterpret_cast(mapMemory(BytesRequired)); + markReadWrite(Metadata, BytesRequired); + + // Allocate memory and set up the free pages queue. + BytesRequired = MaxSimultaneousAllocations * sizeof(*FreeSlots); + FreeSlots = reinterpret_cast(mapMemory(BytesRequired)); + markReadWrite(FreeSlots, BytesRequired); + + // Multiply the sample rate by 2 to give a good, fast approximation for (1 / + // SampleRate) chance of sampling. + if (Opts.SampleRate != 1) + AdjustedSampleRate = Opts.SampleRate * 2; + else + AdjustedSampleRate = 1; + + GuardedPagePool = reinterpret_cast(GuardedPoolMemory); + GuardedPagePoolEnd = + reinterpret_cast(GuardedPoolMemory) + PoolBytesRequired; + + // Ensure that signal handlers are installed as late as possible, as the class + // is not thread-safe until init() is finished, and thus a SIGSEGV may cause a + // race to members if recieved during init(). + if (Opts.InstallSignalHandlers) + installSignalHandlers(); +} + +void *GuardedPoolAllocator::allocate(std::size_t Size) { + if (Size == 0 || Size > maximumAllocationSize()) + return nullptr; + + ScopedLock L(&PoolMutex); + + size_t Index = reserveSlot(); + if (Index == kInvalidSlotID) + return nullptr; + + uintptr_t Ptr = slotToAddr(Index); + Ptr += allocationSlotOffset(Size); + AllocationMetadata *Meta = addrToMetadata(Ptr); + + // If a slot is multiple pages in size, and the allocation takes up a single + // page, we can improve overflow detection by leaving the unused pages as + // unmapped. + markReadWrite(reinterpret_cast(getPageAddr(Ptr)), Size); + + Meta->RecordAllocation(Ptr, Size); + + return reinterpret_cast(Ptr); +} + +void GuardedPoolAllocator::deallocate(void *Ptr) { + assert(pointerIsMine(Ptr) && "Pointer is not mine!"); + uintptr_t UPtr = reinterpret_cast(Ptr); + uintptr_t SlotStart = slotToAddr(addrToSlot(UPtr)); + AllocationMetadata *Meta = addrToMetadata(UPtr); + if (Meta->Addr != UPtr) { + reportError(UPtr, Error::INVALID_FREE); + exit(EXIT_FAILURE); + } + + ScopedLock L(&PoolMutex); + + if (Meta->IsDeallocated) { + reportError(UPtr, Error::DOUBLE_FREE); + exit(EXIT_FAILURE); + } + + Meta->RecordDeallocation(); + + markInaccessible(reinterpret_cast(SlotStart), + maximumAllocationSize()); + freeSlot(addrToSlot(UPtr)); +} + +std::size_t GuardedPoolAllocator::getSize(const void *Ptr) { + assert(pointerIsMine(Ptr)); + ScopedLock L(&PoolMutex); + AllocationMetadata *Meta = addrToMetadata(reinterpret_cast(Ptr)); + assert(Meta->Addr == reinterpret_cast(Ptr)); + return Meta->Size; +} + +std::size_t GuardedPoolAllocator::maximumAllocationSize() const { + // TODO(hctim): When this changes, ensure to update corresponding values in + // test/gwp_asan/interface_helpers.h + return PageSize; +} + +AllocationMetadata *GuardedPoolAllocator::addrToMetadata(uintptr_t Ptr) const { + assert(pointerIsMine(reinterpret_cast(Ptr))); + return &Metadata[addrToSlot(Ptr)]; +} + +std::size_t GuardedPoolAllocator::addrToSlot(uintptr_t Ptr) const { + std::size_t ByteOffsetFromPoolStart = Ptr - GuardedPagePool; + return ByteOffsetFromPoolStart / (maximumAllocationSize() + PageSize); +} + +uintptr_t GuardedPoolAllocator::slotToAddr(std::size_t N) const { + return GuardedPagePool + (PageSize * (1 + N)) + (maximumAllocationSize() * N); +} + +uintptr_t GuardedPoolAllocator::getPageAddr(uintptr_t Ptr) const { + return Ptr & ~(static_cast(PageSize) - 1); +} + +bool GuardedPoolAllocator::isGuardPage(uintptr_t Ptr) const { + std::size_t PageOffsetFromPoolStart = (Ptr - GuardedPagePool) / PageSize; + std::size_t PagesPerSlot = maximumAllocationSize() / PageSize; + return (PageOffsetFromPoolStart % (PagesPerSlot + 1)) == 0; +} + +std::size_t GuardedPoolAllocator::reserveSlot() { + // Avoid potential reuse of a slot before we have made at least a single + // allocation in each slot. Helps with our use-after-free detection. + if (NumSampledAllocations < MaxSimultaneousAllocations) + return NumSampledAllocations++; + + if (FreeSlotsLength == 0) + return kInvalidSlotID; + + std::size_t ReservedIndex = random::getRandomUnsigned64() % FreeSlotsLength; + std::size_t SlotIndex = FreeSlots[ReservedIndex]; + FreeSlots[ReservedIndex] = FreeSlots[--FreeSlotsLength]; + return SlotIndex; +} + +void GuardedPoolAllocator::freeSlot(std::size_t SlotIndex) { + assert(FreeSlotsLength < MaxSimultaneousAllocations); + FreeSlots[FreeSlotsLength++] = SlotIndex; +} + +uintptr_t GuardedPoolAllocator::allocationSlotOffset(std::size_t Size) { + assert(Size > 0); + + bool ShouldRightAlign = random::getRandomUnsigned64() % 2 == 0; + if (!ShouldRightAlign) + return 0; + + uintptr_t Offset = maximumAllocationSize(); + if (!PerfectlyRightAlign) { + if (Size == 3) + Size = 4; + else if (Size > 4 && Size <= 8) + Size = 8; + else if (Size > 8 && (Size % 16) != 0) + Size += 16 - (Size % 16); + } + Offset -= Size; + return Offset; +} + +void GuardedPoolAllocator::reportError(uintptr_t AccessPtr, Error Error) { + if (SingletonPtr) + SingletonPtr->reportErrorInternal(AccessPtr, Error); +} + +std::size_t GuardedPoolAllocator::getNearestSlot(uintptr_t Ptr) const { + if (Ptr <= GuardedPagePool + PageSize) + return 0; + if (Ptr > GuardedPagePoolEnd - PageSize) + return MaxSimultaneousAllocations - 1; + + if (!isGuardPage(Ptr)) + return addrToSlot(Ptr); + + if (Ptr % PageSize <= PageSize / 2) + return addrToSlot(Ptr - PageSize); // Round down. + return addrToSlot(Ptr + PageSize); // Round up. +} + +Error GuardedPoolAllocator::diagnoseUnknownError(uintptr_t AccessPtr, + AllocationMetadata **Meta) { + // Let's try and figure out what the source of this error is. + if (isGuardPage(AccessPtr)) { + std::size_t Slot = getNearestSlot(AccessPtr); + AllocationMetadata *SlotMeta = addrToMetadata(slotToAddr(Slot)); + + // Ensure that this slot was allocated once upon a time. + if (!SlotMeta->Addr) + return Error::UNKNOWN; + *Meta = SlotMeta; + + if (SlotMeta->Addr < AccessPtr) + return Error::BUFFER_OVERFLOW; + return Error::BUFFER_UNDERFLOW; + } + + // Access wasn't a guard page, check for use-after-free. + AllocationMetadata *SlotMeta = addrToMetadata(AccessPtr); + if (SlotMeta->IsDeallocated) { + *Meta = SlotMeta; + return Error::USE_AFTER_FREE; + } + + // If we have reached here, the error is still unknown. There is no metadata + // available. + return Error::UNKNOWN; +} + +// Prints the provided error and metadata information. Returns true if there is +// additional context that can be provided, false otherwise (i.e. returns false +// if Error == {UNKNOWN, INVALID_FREE without metadata}). +bool printErrorType(Error Error, uintptr_t AccessPtr, AllocationMetadata *Meta, + options::Printf_t Printf) { + switch (Error) { + case Error::UNKNOWN: + Printf("GWP-ASan couldn't automatically determine the source of the " + "memory error when accessing 0x%zx. It was likely caused by a wild " + "memory access into the GWP-ASan pool.\n", + AccessPtr); + return false; + case Error::USE_AFTER_FREE: + Printf("Use after free occurred when accessing memory at: 0x%zx\n", + AccessPtr); + break; + case Error::DOUBLE_FREE: + Printf("Double free occurred when trying to free memory at: 0x%zx\n", + AccessPtr); + break; + case Error::INVALID_FREE: + Printf( + "Invalid (wild) free occurred when trying to free memory at: 0x%zx\n", + AccessPtr); + // It's possible for an invalid free to fall onto a slot that has never been + // allocated. If this is the case, there is no valid metadata. + if (Meta == nullptr) + return false; + break; + case Error::BUFFER_OVERFLOW: + Printf("Buffer overflow occurred when accessing memory at: 0x%zx\n", + AccessPtr); + break; + case Error::BUFFER_UNDERFLOW: + Printf("Buffer underflow occurred when accessing memory at: 0x%zx\n", + AccessPtr); + break; + } + + Printf("0x%zx is ", AccessPtr); + if (AccessPtr < Meta->Addr) + Printf("located %zu bytes to the left of a %zu-byte allocation located at " + "0x%zx\n", + Meta->Addr - AccessPtr, Meta->Size, Meta->Addr); + else if (AccessPtr > Meta->Addr) + Printf("located %zu bytes to the right of a %zu-byte allocation located at " + "0x%zx\n", + AccessPtr - Meta->Addr, Meta->Size, Meta->Addr); + else + Printf("a %zu-byte allocation\n", Meta->Size); + return true; +} + +void printThreadInformation(Error Error, uintptr_t AccessPtr, + AllocationMetadata *Meta, + options::Printf_t Printf) { + Printf("0x%zx was allocated by thread ", AccessPtr); + if (Meta->AllocationTrace.ThreadID == UINT64_MAX) + Printf("UNKNOWN.\n"); + else + Printf("%zu.\n", Meta->AllocationTrace.ThreadID); + + if (Error == Error::USE_AFTER_FREE || Error == Error::DOUBLE_FREE) { + Printf("0x%zx was freed by thread ", AccessPtr); + if (Meta->AllocationTrace.ThreadID == UINT64_MAX) + Printf("UNKNOWN.\n"); + else + Printf("%zu.\n", Meta->AllocationTrace.ThreadID); + } +} + +struct ScopedEndOfReportDecorator { + ScopedEndOfReportDecorator(options::Printf_t Printf) : Printf(Printf) {} + ~ScopedEndOfReportDecorator() { Printf("*** End GWP-ASan report ***\n"); } + options::Printf_t Printf; +}; + +void GuardedPoolAllocator::reportErrorInternal(uintptr_t AccessPtr, + Error Error) { + if (!pointerIsMine(reinterpret_cast(AccessPtr))) { + return; + } + + // Attempt to prevent races to re-use the same slot that triggered this error. + // This does not guarantee that there are no races, because another thread can + // take the locks during the time that the signal handler is being called. + PoolMutex.tryLock(); + + Printf("*** GWP-ASan detected a memory error ***\n"); + ScopedEndOfReportDecorator Decorator(Printf); + + AllocationMetadata *Meta = nullptr; + + if (Error == Error::UNKNOWN) { + Error = diagnoseUnknownError(AccessPtr, &Meta); + } else { + std::size_t Slot = getNearestSlot(AccessPtr); + Meta = addrToMetadata(slotToAddr(Slot)); + // Ensure that this slot has been previously allocated. + if (!Meta->Addr) + Meta = nullptr; + } + + // Print the error information, and if there is no valid metadata, stop here. + if (!printErrorType(Error, AccessPtr, Meta, Printf)) { + return; + } + + // Ensure that we have a valid metadata pointer from this point forward. + if (Meta == nullptr) { + Printf("GWP-ASan internal unreachable error. Metadata is not null.\n"); + return; + } + + printThreadInformation(Error, AccessPtr, Meta, Printf); + // TODO(hctim): Implement stack unwinding here. Ask the caller to provide us + // with the base pointer, and we unwind the stack to give a stack trace for + // the access. + // TODO(hctim): Implement dumping here of allocation/deallocation traces. +} + +TLS_INITIAL_EXEC uint64_t GuardedPoolAllocator::NextSampleCounter = 0; +} // namespace gwp_asan + +bool __gwp_asan_pointer_is_mine(void *Ptr) { + if (!gwp_asan::SingletonPtr) + return false; + return gwp_asan::SingletonPtr->pointerIsMine(Ptr); +} + +void *__gwp_asan_allocate(std::size_t Size) { + if (!gwp_asan::SingletonPtr) + return nullptr; + return gwp_asan::SingletonPtr->allocate(Size); +} + +void __gwp_asan_deallocate(void *Ptr) { + assert(gwp_asan::SingletonPtr && + "GPA uninitialised in __gwp_asan_deallocate()"); + gwp_asan::SingletonPtr->deallocate(Ptr); +} + +std::size_t __gwp_asan_get_size(const void *Ptr) { + if (!gwp_asan::SingletonPtr) + return 0; + if (!gwp_asan::SingletonPtr->pointerIsMine(Ptr)) + return 0; + return gwp_asan::SingletonPtr->getSize(Ptr); +} + +std::size_t __gwp_asan_maximum_allocation_size(void) { + if (!gwp_asan::SingletonPtr) + return 0; + return gwp_asan::SingletonPtr->maximumAllocationSize(); +} diff --git a/compiler-rt/lib/gwp_asan/guarded_pool_allocator_posix.cpp b/compiler-rt/lib/gwp_asan/guarded_pool_allocator_posix.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/guarded_pool_allocator_posix.cpp @@ -0,0 +1,96 @@ +//===-- guarded_pool_allocator_posix.cpp ------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "gwp_asan/guarded_pool_allocator.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace gwp_asan { + +void *GuardedPoolAllocator::mapMemory(size_t Size) const { + void *Ptr = + mmap(nullptr, Size, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + + if (Ptr == MAP_FAILED) { + Printf("Failed to map guarded pool allocator memory, errno: %d\n", errno); + Printf(" mmap(nullptr, %zu, ...) failed.\n", Size); + exit(EXIT_FAILURE); + } + return Ptr; +} + +void GuardedPoolAllocator::markReadWrite(void *Ptr, size_t Size) const { + if (mprotect(Ptr, Size, PROT_READ | PROT_WRITE) != 0) { + Printf("Failed to set guarded pool allocator memory at as RW, errno: %d\n", + errno); + Printf(" mprotect(%p, %zu, RW) failed.\n", Ptr, Size); + exit(EXIT_FAILURE); + } +} + +void GuardedPoolAllocator::markInaccessible(void *Ptr, size_t Size) const { + // mmap() a PROT_NONE page over the address to release it to the system, if + // we used mprotect() here the system would count pages in the quarantine + // against the RSS. + if (mmap(Ptr, Size, PROT_NONE, MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE, -1, + 0) == MAP_FAILED) { + Printf("Failed to set guarded pool allocator memory as inaccessible, " + "errno: %d\n", + errno); + Printf(" mmap(%p, %zu, NONE, ...) failed.\n", Ptr, Size); + exit(EXIT_FAILURE); + } +} + +std::size_t GuardedPoolAllocator::getPlatformPageSize() { + return sysconf(_SC_PAGESIZE); +} + +struct sigaction PreviousHandler; + +static void sigSegvHandler(int sig, siginfo_t *info, void *ucontext) { + gwp_asan::GuardedPoolAllocator::reportError( + reinterpret_cast(info->si_addr)); + + // Process any previous handlers. + if (PreviousHandler.sa_flags & SA_SIGINFO) { + PreviousHandler.sa_sigaction(sig, info, ucontext); + } else if (PreviousHandler.sa_handler == SIG_IGN || + PreviousHandler.sa_handler == SIG_DFL) { + // If the previous handler was the default handler, or was ignoring this + // signal, install the default handler and re-raise the signal in order to + // get a core dump and terminate this process. + signal(SIGSEGV, SIG_DFL); + raise(SIGSEGV); + } else { + PreviousHandler.sa_handler(sig); + } +} + +void GuardedPoolAllocator::installSignalHandlers() { + struct sigaction Action; + Action.sa_sigaction = sigSegvHandler; + Action.sa_flags = SA_SIGINFO; + sigaction(SIGSEGV, &Action, &PreviousHandler); +} + +uint64_t GuardedPoolAllocator::getThreadID() { +#ifdef SYS_gettid + return syscall(SYS_gettid); +#else + return kInvalidThreadID; +#endif +} + +} // namespace gwp_asan diff --git a/compiler-rt/lib/gwp_asan/gwp_asan_interface_internal.h b/compiler-rt/lib/gwp_asan/gwp_asan_interface_internal.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/gwp_asan_interface_internal.h @@ -0,0 +1,37 @@ +//===-- gwp_asan_interface_internal.h ---------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_INTERFACE_INTERNAL_H_ +#define GWP_ASAN_INTERFACE_INTERNAL_H_ + +#include + +#define INTERFACE_ATTRIBUTE __attribute__((visibility("default"))) + +extern "C" { +// See GuardedPoolAllocator::shouldSample() for more details. Returns 'false' if +// the GPA is not initialised. +INTERFACE_ATTRIBUTE bool __gwp_asan_pointer_is_mine(void *Ptr); + +// See GuardedPoolAllocator::allocate() for more details. Returns nullptr if +// the GPA is not initialised. +INTERFACE_ATTRIBUTE void *__gwp_asan_allocate(std::size_t Size); + +// See GuardedPoolAllocator::deallocate() for more details. +INTERFACE_ATTRIBUTE void __gwp_asan_deallocate(void *Ptr); + +// See GuardedPoolAllocator::getSize() for more details. Returns 0 if the GPA is +// not initialised or the pointer is not owned by the GPA. +INTERFACE_ATTRIBUTE std::size_t __gwp_asan_get_size(const void *Ptr); + +// See GuardedPoolAllocator::maximumAllocationSize() for more details. Returns 0 +// if the GPA is not initialised. +INTERFACE_ATTRIBUTE std::size_t __gwp_asan_maximum_allocation_size(void); +} + +#endif // GWP_ASAN_INTERFACE_INTERNAL_H_ diff --git a/compiler-rt/lib/gwp_asan/mutex.h b/compiler-rt/lib/gwp_asan/mutex.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/mutex.h @@ -0,0 +1,94 @@ +//===-- mutex.h -------------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// TODO(hctim): Move this implementation and Scudo's implementation into a +// unified base implementation. We shouldn't need to have separate +// implementations for this. + +#ifndef GWP_ASAN_MUTEX_H_ +#define GWP_ASAN_MUTEX_H_ + +#include "gwp_asan/definitions.h" + +#include + +#ifdef __unix__ +#include +#endif // defined(__unix__) + +namespace gwp_asan { +class Mutex { +public: + void lock(); + bool tryLock(); + void unlock(); + +private: + void yieldProcessor(uint8_t Count); + void yieldPlatform(); + + void lockSlow(); + + bool Locked = false; +}; + +class ScopedLock { +public: + explicit ScopedLock(Mutex *Mx) : Mu(Mx) { Mu->lock(); } + ~ScopedLock() { Mu->unlock(); } + +private: + Mutex *Mu; +}; + +ALWAYS_INLINE void Mutex::lock() { + if (tryLock()) + return; + lockSlow(); +} + +ALWAYS_INLINE bool Mutex::tryLock() { + return !__atomic_exchange_n(&Locked, true, __ATOMIC_ACQUIRE); +} + +ALWAYS_INLINE void Mutex::unlock() { + __atomic_store_n(&Locked, false, __ATOMIC_RELEASE); +} + +ALWAYS_INLINE void Mutex::yieldProcessor(uint8_t Count) { +#if defined(__i386__) || defined(__x86_64__) + asm volatile("" ::: "memory"); + for (uint8_t i = 0; i < Count; ++i) + asm volatile("pause"); +#elif defined(__aarch64__) || defined(__arm__) + asm volatile("" ::: "memory"); + for (uint8_t i = 0; I < Count; ++i) + asm volatile("yield"); +#endif + asm volatile("" ::: "memory"); +} + +ALWAYS_INLINE void Mutex::lockSlow() { + for (uint32_t i = 0;; ++i) { + if (i < 10) + yieldProcessor(10); + else + yieldPlatform(); + + if (!__atomic_load_n(&Locked, __ATOMIC_RELAXED) && tryLock()) + return; + } +} + +#ifdef __unix__ +ALWAYS_INLINE void Mutex::yieldPlatform() { sched_yield(); } +#endif // defined(__unix__) + +} // namespace gwp_asan + +#endif // GWP_ASAN_MUTEX_H_ diff --git a/compiler-rt/lib/gwp_asan/optional/options_parser.h b/compiler-rt/lib/gwp_asan/optional/options_parser.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/optional/options_parser.h @@ -0,0 +1,35 @@ +//===-- options_parser.h ----------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_OPTIONAL_OPTIONS_PARSER_H_ +#define GWP_ASAN_OPTIONAL_OPTIONS_PARSER_H_ + +// Include sanitizer_common first as gwp_asan/options.h transiently includes +// definitions.h, which overwrites some of the definitions in sanitizer_common. +#include "sanitizer_common/sanitizer_common.h" + +#include "gwp_asan/options.h" + +namespace gwp_asan { +namespace options { + +// Parse the options from the GWP_ASAN_FLAGS environment variable. +void initOptions(); +// Returns a pointer to the initialised options. Call initOptions() prior to +// calling this function. +Options *getOptions(); + +} // namespace options +} // namespace gwp_asan + +extern "C" { +SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE const char * +__gwp_asan_default_options(); +} + +#endif // GWP_ASAN_OPTIONAL_OPTIONS_PARSER_H_ diff --git a/compiler-rt/lib/gwp_asan/optional/options_parser.cpp b/compiler-rt/lib/gwp_asan/optional/options_parser.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/optional/options_parser.cpp @@ -0,0 +1,86 @@ +//===-- options_parser.cpp --------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "gwp_asan/optional/options_parser.h" + +#include +#include +#include + +#include "gwp_asan/options.h" +#include "sanitizer_common/sanitizer_common.h" +#include "sanitizer_common/sanitizer_flag_parser.h" +#include "sanitizer_common/sanitizer_flags.h" + +namespace gwp_asan { +namespace options { +void registerGwpAsanFlags(__sanitizer::FlagParser *parser, Options *o) { +#define GWP_ASAN_OPTION(Type, Name, DefaultValue, Description) \ + RegisterFlag(parser, #Name, Description, &o->Name); +#include "gwp_asan/options.inc" +#undef GWP_ASAN_OPTION +} + +const char *getCompileDefinitionGwpAsanDefaultOptions() { +#ifdef GWP_ASAN_DEFAULT_OPTIONS + return SANITIZER_STRINGIFY(GWP_ASAN_DEFAULT_OPTIONS); +#else + return ""; +#endif +} + +const char *getGwpAsanDefaultOptions() { + return (__gwp_asan_default_options) ? __gwp_asan_default_options() : ""; +} + +void initOptions() { + Options *o = getOptions(); + o->setDefaults(); + + __sanitizer::FlagParser Parser; + registerGwpAsanFlags(&Parser, o); + + // Override from compile definition. + Parser.ParseString(getCompileDefinitionGwpAsanDefaultOptions()); + + // Override from user-specified string. + Parser.ParseString(getGwpAsanDefaultOptions()); + + // Override from environment. + Parser.ParseString(__sanitizer::GetEnv("GWP_ASAN_OPTIONS")); + + __sanitizer::InitializeCommonFlags(); + + // Sanity checks and default settings for the parameters. + if (o->Enabled && o->MaxSimultaneousAllocations <= 0) { + __sanitizer::Printf( + "GWP-ASan ERROR: MaxSimultaneousAllocations must be >= 0 when " + "GWP-ASan is enabled.\n"); + exit(EXIT_FAILURE); + } + + if (o->Enabled && o->SampleRate < 1) { + __sanitizer::Printf("GWP-ASan ERROR: SampleRate must be >= 1 when " + "GWP-ASan is enabled.\n"); + exit(EXIT_FAILURE); + } + + o->Printf = __sanitizer::Printf; +} + +Options *getOptions() { + static Options GwpAsanFlags; + return &GwpAsanFlags; +} + +} // namespace options +} // namespace gwp_asan + +SANITIZER_INTERFACE_WEAK_DEF(const char *, __gwp_asan_default_options, void) { + return ""; +} diff --git a/compiler-rt/lib/gwp_asan/options.h b/compiler-rt/lib/gwp_asan/options.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/options.h @@ -0,0 +1,41 @@ +//===-- options.h -----------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_OPTIONS_H_ +#define GWP_ASAN_OPTIONS_H_ + +namespace gwp_asan { +namespace options { +// The function pointer type for printf(). Follows the standard format from the +// sanitizers library. If the supported allocator exposes printing via a +// different function signature, please provide a wrapper which has this +// printf() signature, and pass the wrapper instead. +typedef void (*Printf_t)(const char *Format, ...); + +struct Options { + Printf_t Printf = nullptr; + + // Read the options from the included definitions file. +#define GWP_ASAN_OPTION(Type, Name, DefaultValue, Description) \ + Type Name = DefaultValue; +#include "gwp_asan/options.inc" +#undef GWP_ASAN_OPTION + + void setDefaults() { +#define GWP_ASAN_OPTION(Type, Name, DefaultValue, Description) \ + Name = DefaultValue; +#include "gwp_asan/options.inc" +#undef GWP_ASAN_OPTION + + Printf = nullptr; + } +}; +} // namespace options +} // namespace gwp_asan + +#endif // GWP_ASAN_OPTIONS_H_ diff --git a/compiler-rt/lib/gwp_asan/options.inc b/compiler-rt/lib/gwp_asan/options.inc new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/options.inc @@ -0,0 +1,50 @@ +//===-- options.inc ---------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +// Please ensure that this file is kept up to date with llvm/docs/GwpAsan.rst. + +#ifndef GWP_ASAN_OPTION +#error "Define GWP_ASAN_OPTION prior to including this file!" +#endif + +GWP_ASAN_OPTION(bool, Enabled, true, "Is GWP-ASan enabled? Defaults to true.") + +GWP_ASAN_OPTION( + bool, PerfectlyRightAlign, false, + "When allocations are right-aligned, should we perfectly align them up to " + "the page boundary? By default (false), we round up allocation size to the " + "nearest power of two (2, 4, 8, 16) up to a maximum of 16-byte alignment " + "for performance reasons. Setting this to true can find single byte " + "buffer-overflows at the cost of performance, and may be incompatible with " + "some architectures.") + +GWP_ASAN_OPTION( + int, MaxSimultaneousAllocations, 16, + "Number of usable guarded slots in the allocation pool. Defaults to 16.") + +// Note that this is intentionally a signed long long due to sanitizer_common +// implementation details. The __sanitizer::FlagHandler<>::Parse() calls +// __sanitizer::internal_simple_strtoll() to do the string->integer conversion, +// and supports a signed, 64-bit number as its maximum. +GWP_ASAN_OPTION( + long long, SampleRate, 5000, + "The probability (1 / SampleRate) that an allocation is selected for " + "GWP-ASan sampling. Default is 5000. Sample rates up to 2^63 are " + "supported.") + +GWP_ASAN_OPTION( + bool, InstallSignalHandlers, true, + "Install GWP-ASan signal handlers for SIGSEGV. This allows " + "better error reports by providing stack traces for " + "allocation/deallocation when reporting a memory error. These handlers " + "are generally installed during dynamic loading, and will ensure that the " + "previously installed signal handlers will be called. User programs that " + "install SIGSEGV handlers should ensure that any previously installed " + "signal handlers are also called. Note, if the previously installed signal " + "handler is SIG_IGN, we terminate the process after dumping the error " + "report.") diff --git a/compiler-rt/lib/gwp_asan/random.h b/compiler-rt/lib/gwp_asan/random.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/random.h @@ -0,0 +1,28 @@ +//===-- random.h ------------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_RANDOM_H_ +#define GWP_ASAN_RANDOM_H_ + +#include + +namespace gwp_asan { +namespace random { +// TODO(hctim): This may have significant overhead for platforms where +// 64-bit arithmetic is emulated. Do we need less than a 2^32 chance of +// sampling? +// xorshift128+ (64-bit output). Avoids multiplication. +uint64_t getRandomUnsigned64(); + +// xorshift* (64-bit output). This is primarily used to seed the xorshift128+ +// generator. +uint64_t xorShiftStar64(); +} // namespace random +} // namespace gwp_asan + +#endif // GWP_ASAN_RANDOM_H_ diff --git a/compiler-rt/lib/gwp_asan/random.cpp b/compiler-rt/lib/gwp_asan/random.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/random.cpp @@ -0,0 +1,43 @@ +//===-- random.cpp ----------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "gwp_asan/random.h" + +#include + +namespace gwp_asan { +namespace random { +uint64_t getRandomUnsigned64() { + static thread_local uint64_t RandomStateA = + static_cast(time(nullptr)); + static thread_local uint64_t RandomStateB = xorShiftStar64(); + + uint64_t A = RandomStateA; + const uint64_t B = RandomStateB; + RandomStateA = B; + + A ^= A << 23; + A ^= A >> 17; + A ^= B ^ (B >> 26); + + RandomStateB = A; + return A + B; +} + +uint64_t xorShiftStar64() { + static thread_local uint64_t RandomState = + static_cast(time(nullptr)); + uint64_t A = RandomState; + A ^= A >> 12; + A ^= A << 25; + A ^= A >> 27; + RandomState = A; + return A * 0x2545F4914F6CDD1D; +} +} // namespace random +} // namespace gwp_asan diff --git a/compiler-rt/lib/scudo/CMakeLists.txt b/compiler-rt/lib/scudo/CMakeLists.txt --- a/compiler-rt/lib/scudo/CMakeLists.txt +++ b/compiler-rt/lib/scudo/CMakeLists.txt @@ -30,6 +30,15 @@ RTSanitizerCommonNoTermination RTSanitizerCommonLibc RTInterception) + +if (COMPILER_RT_HAS_GWP_ASAN) + # Currently, Scudo uses the GwpAsan flag parser. This backs onto the flag + # parsing mechanism of sanitizer_common. Once Scudo has its own flag parsing, + # and parses GwpAsan options, you can remove this dependency. + list(APPEND SCUDO_MINIMAL_OBJECT_LIBS RTGwpAsan RTGwpAsanOptionsParser) + list(APPEND SCUDO_CFLAGS -DGWP_ASAN_HOOKS) +endif() + set(SCUDO_OBJECT_LIBS ${SCUDO_MINIMAL_OBJECT_LIBS}) set(SCUDO_DYNAMIC_LIBS ${SCUDO_MINIMAL_DYNAMIC_LIBS}) diff --git a/compiler-rt/lib/scudo/scudo_allocator.cpp b/compiler-rt/lib/scudo/scudo_allocator.cpp --- a/compiler-rt/lib/scudo/scudo_allocator.cpp +++ b/compiler-rt/lib/scudo/scudo_allocator.cpp @@ -25,6 +25,11 @@ #include "sanitizer_common/sanitizer_allocator_interface.h" #include "sanitizer_common/sanitizer_quarantine.h" +#ifdef GWP_ASAN_HOOKS +# include "gwp_asan/guarded_pool_allocator.h" +# include "gwp_asan/optional/options_parser.h" +#endif // GWP_ASAN_HOOKS + #include #include @@ -213,6 +218,10 @@ return reinterpret_cast(TSD->QuarantineCachePlaceHolder); } +#ifdef GWP_ASAN_HOOKS +static gwp_asan::GuardedPoolAllocator GuardedAlloc; +#endif // GWP_ASAN_HOOKS + struct Allocator { static const uptr MaxAllowedMallocSize = FIRST_32_SECOND_64(2UL << 30, 1ULL << 40); @@ -291,6 +300,14 @@ void *allocate(uptr Size, uptr Alignment, AllocType Type, bool ForceZeroContents = false) { initThreadMaybe(); + +#ifdef GWP_ASAN_HOOKS + if (UNLIKELY(GuardedAlloc.shouldSample())) { + if (void *Ptr = GuardedAlloc.allocate(Size)) + return Ptr; + } +#endif // GWP_ASAN_HOOKS + if (UNLIKELY(Alignment > MaxAlignment)) { if (AllocatorMayReturnNull()) return nullptr; @@ -434,6 +451,14 @@ __sanitizer_free_hook(Ptr); if (UNLIKELY(!Ptr)) return; + +#ifdef GWP_ASAN_HOOKS + if (UNLIKELY(GuardedAlloc.pointerIsMine(Ptr))) { + GuardedAlloc.deallocate(Ptr); + return; + } +#endif // GWP_ASAN_HOOKS + if (UNLIKELY(!Chunk::isAligned(Ptr))) dieWithMessage("misaligned pointer when deallocating address %p\n", Ptr); UnpackedHeader Header; @@ -463,6 +488,18 @@ // size still fits in the chunk. void *reallocate(void *OldPtr, uptr NewSize) { initThreadMaybe(); + +#ifdef GWP_ASAN_HOOKS + if (UNLIKELY(GuardedAlloc.pointerIsMine(OldPtr))) { + size_t OldSize = GuardedAlloc.getSize(OldPtr); + void *NewPtr = allocate(NewSize, MinAlignment, FromMalloc); + if (NewPtr) + memcpy(NewPtr, OldPtr, (NewSize < OldSize) ? NewSize : OldSize); + GuardedAlloc.deallocate(OldPtr); + return NewPtr; + } +#endif // GWP_ASAN_HOOKS + if (UNLIKELY(!Chunk::isAligned(OldPtr))) dieWithMessage("misaligned address when reallocating address %p\n", OldPtr); @@ -504,6 +541,12 @@ initThreadMaybe(); if (UNLIKELY(!Ptr)) return 0; + +#ifdef GWP_ASAN_HOOKS + if (UNLIKELY(GuardedAlloc.pointerIsMine(Ptr))) + return GuardedAlloc.getSize(Ptr); +#endif // GWP_ASAN_HOOKS + UnpackedHeader Header; Chunk::loadHeader(Ptr, &Header); // Getting the usable size of a chunk only makes sense if it's allocated. @@ -626,6 +669,10 @@ void initScudo() { Instance.init(); +#ifdef GWP_ASAN_HOOKS + gwp_asan::options::initOptions(); + GuardedAlloc.init(*gwp_asan::options::getOptions()); +#endif // GWP_ASAN_HOOKS } void ScudoTSD::init() { diff --git a/compiler-rt/test/gwp_asan/CMakeLists.txt b/compiler-rt/test/gwp_asan/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/CMakeLists.txt @@ -0,0 +1,30 @@ +set(GWP_ASAN_LIT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +set(GWP_ASAN_LIT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}) + +set(GWP_ASAN_TESTSUITES) + +set(GWP_ASAN_TEST_DEPS ${SANITIZER_COMMON_LIT_TEST_DEPS} gwp_asan scudo) + +configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.in + ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg + ) + +set(GWP_ASAN_TEST_ARCH ${GWP_ASAN_SUPPORTED_ARCH}) +foreach(arch ${GWP_ASAN_TEST_ARCH}) + set(GWP_ASAN_TEST_TARGET_ARCH ${arch}) + string(TOLOWER "-${arch}" GWP_ASAN_TEST_CONFIG_SUFFIX) + get_test_cc_for_arch(${arch} GWP_ASAN_TEST_TARGET_CC GWP_ASAN_TEST_TARGET_CFLAGS) + string(TOUPPER ${arch} ARCH_UPPER_CASE) + set(CONFIG_NAME ${ARCH_UPPER_CASE}${OS_NAME}Config) + + configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.in + ${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_NAME}/lit.site.cfg) + list(APPEND GWP_ASAN_TESTSUITES ${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_NAME}) +endforeach() + +add_lit_testsuite(check-gwp_asan "Running the GWP-ASan tests" + ${GWP_ASAN_TESTSUITES} + DEPENDS ${GWP_ASAN_TEST_DEPS}) +set_target_properties(check-gwp_asan PROPERTIES FOLDER "Compiler-RT Misc") diff --git a/compiler-rt/test/gwp_asan/alignment_power_of_two.cpp b/compiler-rt/test/gwp_asan/alignment_power_of_two.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/alignment_power_of_two.cpp @@ -0,0 +1,30 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %run %t + +#include +#include +#include + +#include "interface_helpers.h" + +int main() { + std::vector> AllocSizeToAlignment = { + {1, 1}, {2, 2}, {3, 4}, {4, 4}, {5, 8}, {7, 8}, + {8, 8}, {9, 16}, {15, 16}, {16, 16}, {17, 16}, {31, 16}, + {32, 16}, {33, 16}, {4095, 4096}, {4096, 4096}, + }; + + for (const auto &KV : AllocSizeToAlignment) { + void *Ptr = malloc(KV.first); + + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + + if (reinterpret_cast(Ptr) % KV.second != 0) + exit(EXIT_FAILURE); + + free(Ptr); + } + return 0; +} diff --git a/compiler-rt/test/gwp_asan/allocation_methods.cpp b/compiler-rt/test/gwp_asan/allocation_methods.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/allocation_methods.cpp @@ -0,0 +1,40 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation/memory access/deallocation works +// as expected. + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %run %t + +#include + +#include "interface_helpers.h" + +int main() { + void *Ptr = calloc(1, sizeof(char)); + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + free(Ptr); + + Ptr = malloc(1); + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + free(Ptr); + + char *CPtr = new char; + if (!__internal::pointerIsMine(CPtr)) + exit(EXIT_FAILURE); + delete CPtr; + + CPtr = new char[2]; + if (!__internal::pointerIsMine(CPtr)) + exit(EXIT_FAILURE); + delete[] CPtr; + + Ptr = malloc(1); + Ptr = realloc(Ptr, 2); + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + free(Ptr); + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/allocator_fallback.cpp b/compiler-rt/test/gwp_asan/allocator_fallback.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/allocator_fallback.cpp @@ -0,0 +1,28 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation/memory access/deallocation works +// as expected and we didn't accidentally break the supporting allocator. + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=1 %run %t +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=2 %run %t +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=11 %run %t +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=12 %run %t +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=13 %run %t + +#include + +int main() { + void* Pointers[16]; + for (unsigned i = 0; i < 16; ++i) { + char *Ptr = reinterpret_cast(malloc(1 << i)); + Pointers[i] = Ptr; + *Ptr = 0; + Ptr[(1 << i) - 1] = 0; + } + + for (unsigned i = 0; i < 16; ++i) { + free(Pointers[i]); + } + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/double_delete.cpp b/compiler-rt/test/gwp_asan/double_delete.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/double_delete.cpp @@ -0,0 +1,15 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Double free occurred when trying to free memory at: + +#include + +int main() { + char *Ptr = new char; + delete Ptr; + delete Ptr; + return 0; +} diff --git a/compiler-rt/test/gwp_asan/double_deletea.cpp b/compiler-rt/test/gwp_asan/double_deletea.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/double_deletea.cpp @@ -0,0 +1,15 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Double free occurred when trying to free memory at: + +#include + +int main() { + char *Ptr = new char[50]; + delete[] Ptr; + delete[] Ptr; + return 0; +} diff --git a/compiler-rt/test/gwp_asan/double_free.cpp b/compiler-rt/test/gwp_asan/double_free.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/double_free.cpp @@ -0,0 +1,15 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Double free occurred when trying to free memory at: + +#include + +int main() { + void *Ptr = malloc(10); + free(Ptr); + free(Ptr); + return 0; +} diff --git a/compiler-rt/test/gwp_asan/heap_buffer_overflow.cpp b/compiler-rt/test/gwp_asan/heap_buffer_overflow.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/heap_buffer_overflow.cpp @@ -0,0 +1,18 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Buffer overflow occurred when accessing memory at: +// CHECK: is located {{[0-9]+}} bytes to the right + +#include + +#include "page_size.h" + +int main() { + char *Ptr = + reinterpret_cast(malloc(pageSize())); + volatile char x = *(Ptr + pageSize()); + return 0; +} diff --git a/compiler-rt/test/gwp_asan/heap_buffer_underflow.cpp b/compiler-rt/test/gwp_asan/heap_buffer_underflow.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/heap_buffer_underflow.cpp @@ -0,0 +1,18 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Buffer underflow occurred when accessing memory at: +// CHECK: is located 1 bytes to the left + +#include + +#include "page_size.h" + +int main() { + char *Ptr = + reinterpret_cast(malloc(pageSize())); + volatile char x = *(Ptr - 1); + return 0; +} diff --git a/compiler-rt/test/gwp_asan/interface_helpers.h b/compiler-rt/test/gwp_asan/interface_helpers.h new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/interface_helpers.h @@ -0,0 +1,81 @@ +//===-- interface_helpers.h -------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef GWP_ASAN_INTERFACE_HELPERS_H_ +#define GWP_ASAN_INTERFACE_HELPERS_H_ + +#include + +#include +#include +#include + +namespace __internal { +namespace { +std::mutex InitMutex; +bool IsInitialised = false; + +// For lazy-initialised GWP-ASan, it may take a malloc() call through the +// regular allocator in order to initialise GWP-ASan. We allocate a buffer that +// is larger than the maximum supported buffer in GWP-ASan in order to ensure +// that this "init allocation" isn't sampled. + +const unsigned kAllocSizeLargerThanCanBeSampled = 0x10001; +void maybeInit() { + std::unique_lock L(InitMutex); + if (IsInitialised) + return; + + void *alloc = malloc(kAllocSizeLargerThanCanBeSampled); + free(alloc); + IsInitialised = true; +} +} // anonymous namespace + +bool pointerIsMine(void *Ptr) { + maybeInit(); + return __gwp_asan_pointer_is_mine(Ptr); +} + +void *allocate(std::size_t Size) { + maybeInit(); + void *Ptr = __gwp_asan_allocate(Size); + if (!Ptr) { + fprintf(stderr, "GWP-ASan FAILURE. Could not allocate.\n"); + exit(EXIT_FAILURE); + } + return Ptr; +} + +void deallocate(void *Ptr) { + maybeInit(); + if (!__gwp_asan_pointer_is_mine(Ptr)) { + fprintf(stderr, "GWP-ASan FAILURE. Deallocating a non-owned ptr.\n"); + exit(EXIT_FAILURE); + } + __gwp_asan_deallocate(Ptr); +} + +std::size_t getSize(void *Ptr) { + maybeInit(); + std::size_t Size = __gwp_asan_get_size(Ptr); + if (Size == 0) { + fprintf(stderr, "GWP-ASan FAILURE. Called getSize() on a non-owned ptr.\n"); + exit(EXIT_FAILURE); + } + return Size; +} + +std::size_t maximumAllocationSize() { + maybeInit(); + return __gwp_asan_maximum_allocation_size(); +} + +} // namespace __internal + +#endif // GWP_ASAN_INTERFACE_HELPERS_H_ diff --git a/compiler-rt/test/gwp_asan/interface_test.cpp b/compiler-rt/test/gwp_asan/interface_test.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/interface_test.cpp @@ -0,0 +1,40 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %run %t + +#include "interface_helpers.h" + +#include +#include + +int main() { + std::size_t MaxAllocSize = __internal::maximumAllocationSize(); + for (unsigned AllocSize = 1; AllocSize <= MaxAllocSize; AllocSize <<= 1) { + void *Ptr = __internal::allocate(AllocSize); + + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + + if (AllocSize != __internal::getSize(Ptr)) + exit(EXIT_FAILURE); + + __internal::deallocate(Ptr); + } + + // Test interoperability between guarded allocs and malloc/free. + void *Ptr = __internal::allocate(1); + free(Ptr); + + Ptr = malloc(1); + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + __internal::deallocate(Ptr); + + Ptr = __internal::allocate(1); + Ptr = realloc(Ptr, 2); + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + free(Ptr); + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/interface_test_failures.cpp b/compiler-rt/test/gwp_asan/interface_test_failures.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/interface_test_failures.cpp @@ -0,0 +1,63 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t + +#include "interface_helpers.h" + +#include +#include + +// RUN: not %run %t 1 2>&1 | FileCheck %s --check-prefix=E1 +// E1: GWP-ASan FAILURE. Could not allocate. + +// RUN: not %run %t 2 2>&1 | FileCheck %s --check-prefix=E2 +// E2: GWP-ASan FAILURE. Deallocating a non-owned ptr. + +// RUN: %env_no_sample_options= not %run %t 3 2>&1 | \ +// RUN: FileCheck %s --check-prefix=E3 +// E3: GWP-ASan FAILURE. Deallocating a non-owned ptr. + +// RUN: not %run %t 4 2>&1 | FileCheck %s --check-prefix=E4 +// E4: GWP-ASan FAILURE. Called getSize() on a non-owned ptr. + +// RUN: %env_no_sample_options= not %run %t 5 2>&1 | \ +// RUN: FileCheck %s --check-prefix=E5 +// E5: GWP-ASan FAILURE. Called getSize() on a non-owned ptr. + +// RUN: %env_no_sample_options= not %run %t 6 + +int main(int argc, char **argv) { + std::size_t MaxAllocSize = __internal::maximumAllocationSize(); + if (argc < 2) + return 0; + + int TestNum = atoi(argv[1]); + void *Ptr = nullptr; + + switch (TestNum) { + case 1: + // Allocate more than available. + __internal::allocate(MaxAllocSize + 1); + return 0; + case 2: + __internal::deallocate(nullptr); + return 0; + case 3: + Ptr = malloc(1); + __internal::deallocate(Ptr); + return 0; + case 4: + __internal::getSize(nullptr); + return 0; + case 5: + Ptr = malloc(1); + __internal::getSize(Ptr); + return 0; + case 6: + Ptr = malloc(1); + if (__internal::pointerIsMine(Ptr)) + return 0; + return 1; + default: + return 0; + } +} diff --git a/compiler-rt/test/gwp_asan/invalid_free_left.cpp b/compiler-rt/test/gwp_asan/invalid_free_left.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/invalid_free_left.cpp @@ -0,0 +1,16 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Invalid (wild) free occurred when trying to free memory at: +// CHECK: is located 1 bytes to the left of + +#include + +int main() { + char *Ptr = + reinterpret_cast(malloc(1)); + free(Ptr - 1); + return 0; +} diff --git a/compiler-rt/test/gwp_asan/invalid_free_right.cpp b/compiler-rt/test/gwp_asan/invalid_free_right.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/invalid_free_right.cpp @@ -0,0 +1,16 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: not %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Invalid (wild) free occurred when trying to free memory at: +// CHECK: is located {{[0-9]+}} bytes to the right + +#include + +int main() { + char *Ptr = + reinterpret_cast(malloc(1)); + free(Ptr + 1); + return 0; +} diff --git a/compiler-rt/test/gwp_asan/lit.cfg b/compiler-rt/test/gwp_asan/lit.cfg new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/lit.cfg @@ -0,0 +1,51 @@ +# -*- Python -*- + +import os + +# Setup config name. +config.name = 'GWP-ASan' + config.name_suffix + +# Setup source root. +config.test_source_root = os.path.dirname(__file__) + +# Test suffixes. +config.suffixes = ['.c', '.cc', '.cpp', '.test'] + +# C & CXX flags. +c_flags = ([config.target_cflags]) + +# Android doesn't want -lrt. +if not config.android: + c_flags += ["-lrt"] + +cxx_flags = (c_flags + config.cxx_mode_flags + ["-std=c++11"]) + +gwp_asan_flags = ["-fsanitize=scudo"] + +def build_invocation(compile_flags): + return " " + " ".join([config.clang] + compile_flags) + " " + +# Add substitutions. +config.substitutions.append(("%clang ", build_invocation(c_flags))) +config.substitutions.append(("%clang_gwp_asan ", build_invocation(c_flags + gwp_asan_flags))) +config.substitutions.append(("%clangxx_gwp_asan ", build_invocation(cxx_flags + gwp_asan_flags))) + +# Platform-specific default GWP_ASAN for lit tests. Ensure that GWP-ASan is +# enabled and that it samples every allocation. +default_gwp_asan_options = 'Enabled=1:SampleRate=1' + +config.environment['GWP_ASAN_OPTIONS'] = default_gwp_asan_options +default_gwp_asan_options += ':' +config.substitutions.append(('%env_gwp_asan_options=', + 'env GWP_ASAN_OPTIONS=' + default_gwp_asan_options)) + +# Add a configuration that defaults to an almost-zero chance of sampling (1 in +# a quintillion). If you ever spuriously fail a GWP-ASan unit test due to this, +# buy a lottery ticket. +no_sample_rate_options = default_gwp_asan_options + 'SampleRate=1000000000000000000:' +config.substitutions.append(('%env_no_sample_options=', + 'env GWP_ASAN_OPTIONS=' + no_sample_rate_options)) + +# GWP-ASan tests are currently supported on Linux only. +if config.host_os not in ['Linux']: + config.unsupported = True diff --git a/compiler-rt/test/gwp_asan/lit.site.cfg.in b/compiler-rt/test/gwp_asan/lit.site.cfg.in new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/lit.site.cfg.in @@ -0,0 +1,11 @@ +@LIT_SITE_CFG_IN_HEADER@ + +config.name_suffix = "@GWP_ASAN_TEST_CONFIG_SUFFIX@" +config.target_arch = "@GWP_ASAN_TEST_TARGET_ARCH@" +config.target_cflags = "@GWP_ASAN_TEST_TARGET_CFLAGS@" + +# Load common config for all compiler-rt lit tests. +lit_config.load_config(config, "@COMPILER_RT_BINARY_DIR@/test/lit.common.configured") + +# Load tool-specific config that would do the real work. +lit_config.load_config(config, "@GWP_ASAN_LIT_SOURCE_DIR@/lit.cfg") diff --git a/compiler-rt/test/gwp_asan/no_reuse_ahead_of_time.cpp b/compiler-rt/test/gwp_asan/no_reuse_ahead_of_time.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/no_reuse_ahead_of_time.cpp @@ -0,0 +1,46 @@ +// REQUIRES: gwp_asan + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=2 %run %t 2 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=3 %run %t 3 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=8 %run %t 8 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=16 %run %t 16 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=32 %run %t 32 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=64 %run %t 64 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=128 %run %t 128 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=129 %run %t 129 + +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=7 not %run %t 8 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=8 not %run %t 9 + +// This test ensures that our slots are not reused ahead of time. We increase +// the use-after-free detection by not reusing slots until all of them have been +// allocated. This is done by always using the slots from left-to-right in the +// pool before we used each slot once, at which point random selection takes +// over. + +#include +#include + +#include "interface_helpers.h" + +int main(int argc, char **argv) { + int NumSlots = atoi(argv[1]); + uintptr_t LastAlloc = 0; + + for (unsigned i = 0; i < NumSlots; ++i) { + uintptr_t NextAlloc = reinterpret_cast(__internal::allocate(1)); + + if (!__internal::pointerIsMine(reinterpret_cast(NextAlloc))) + exit(EXIT_FAILURE); + + if (NextAlloc <= LastAlloc) + exit(EXIT_FAILURE); + + free(reinterpret_cast(LastAlloc)); + LastAlloc = NextAlloc; + } + + free(reinterpret_cast(LastAlloc)); + return 0; +} diff --git a/compiler-rt/test/gwp_asan/num_usable_slots.cpp b/compiler-rt/test/gwp_asan/num_usable_slots.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/num_usable_slots.cpp @@ -0,0 +1,45 @@ +// REQUIRES: gwp_asan + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=1 not %run %t 1 \ +// RUN: 2>&1 | FileCheck %s +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=2 not %run %t 2 \ +// RUN: 2>&1 | FileCheck %s +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=3 not %run %t 3 \ +// RUN: 2>&1 | FileCheck %s +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=129 not %run %t 129 \ +// RUN: 2>&1 | FileCheck %s + +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=7 not %run %t 8 2>&1 \ +// RUN: | FileCheck %s --check-prefix=CHECK2 +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=8 not %run %t 9 2>&1 \ +// RUN: | FileCheck %s --check-prefix=CHECK2 + +// This test ensures that our allocations use all of the available +// slots. + +#include + +#include "interface_helpers.h" + +// CHECK: Next alloc should fail. +// CHECK-NEXT: GWP-ASan FAILURE. Could not allocate. + +// CHECK2-NOT: Next alloc should fail. +// CHECK2: GWP-ASan FAILURE. Could not allocate. + +int main(int argc, char **argv) { + int NumSlots = atoi(argv[1]); + + for (unsigned i = 0; i < NumSlots; ++i) { + void *Ptr = __internal::allocate(1); + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + } + + // Now, we have exhausted the pool. The next allocation should fail. + fprintf(stderr, "Next alloc should fail.\n"); + void *Ptr = __internal::allocate(1); + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/page_size.h b/compiler-rt/test/gwp_asan/page_size.h new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/page_size.h @@ -0,0 +1,11 @@ +#ifndef PAGE_SIZE_ +#define PAGE_SIZE_ + +#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) +# include +unsigned pageSize() { + return sysconf(_SC_PAGESIZE); +} +#endif + +#endif // PAGE_SIZE_ diff --git a/compiler-rt/test/gwp_asan/repeated_alloc.cpp b/compiler-rt/test/gwp_asan/repeated_alloc.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/repeated_alloc.cpp @@ -0,0 +1,19 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation/memory access/deallocation works +// as expected. + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %run %t + +int main() { + for (unsigned i = 1; i < 0x100000; i <<= 1) { + char *Ptr = new char[i]; + + for (unsigned j = 0; j < i; ++j) { + *(Ptr + j) = 0x0; + } + + delete[] Ptr; + } + return 0; +} diff --git a/compiler-rt/test/gwp_asan/reuse_quarantine.cpp b/compiler-rt/test/gwp_asan/reuse_quarantine.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/reuse_quarantine.cpp @@ -0,0 +1,27 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation/memory access/deallocation works +// as expected. + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=1 %run %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=2 %run %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=127 %run %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=128 %run %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=129 %run %t + +#include + +#include "interface_helpers.h" + +int main() { + for (unsigned i = 0; i < 128; ++i) { + void *Ptr = __internal::allocate(1); + + if (!__internal::pointerIsMine(Ptr)) + exit(EXIT_FAILURE); + + free(Ptr); + } + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/thread_contention.cpp b/compiler-rt/test/gwp_asan/thread_contention.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/thread_contention.cpp @@ -0,0 +1,71 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation works as expected under threading +// conditions. + +// Note: Compilation of and are extremely expensive for +// non-opt builds of clang. We attempt to minimise this by making a single +// compilation and everything else is decided at runtime. +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=1024 %run %t 1024 + +// Intentionally allocate and free all slots before thrashing. This means we +// get the random slot selection mechanism. +// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=1024 %run %t 1024 RNG + +#include +#include +#include + +#include "interface_helpers.h" + +void asyncMalloc(void *Pointers[], int NumAllocs, + std::atomic *StartingGun) { + while (!StartingGun->load()) { + } + + for (int i = 0; i < NumAllocs; ++i) + Pointers[i] = malloc(__internal::maximumAllocationSize()); +} + +int main(int argc, char **argv) { + if (argc < 2) + exit(EXIT_FAILURE); + int NumAllocsTotal = atoi(argv[1]); + int NumAllocsForThread2 = NumAllocsTotal - (NumAllocsTotal / 2); + + // Optionally, allocate and free all the possible allocations so that PRNG + // selection of the slot takes place. + if (argc > 2) { + for (int i = 0; i < NumAllocsTotal; ++i) { + void *Ptr = malloc(1); + free(Ptr); + } + } + + // Each thread gets half of this as a thread-local section to save into. + void **Pointers = new void *[NumAllocsTotal]; + + std::atomic StartingGun{false}; + + std::thread T1(asyncMalloc, Pointers, NumAllocsTotal / 2, &StartingGun); + std::thread T2(asyncMalloc, &Pointers[NumAllocsTotal / 2], + NumAllocsForThread2, &StartingGun); + + StartingGun.store(true); + + T1.join(); + T2.join(); + + for (unsigned i = 0; i < NumAllocsTotal; ++i) { + for (unsigned j = i + 1; j < NumAllocsTotal; ++j) { + if (Pointers[i] == Pointers[j]) { + // Pointer was dual-allocated. + exit(EXIT_FAILURE); + } + } + free(Pointers[i]); + } + delete[] Pointers; + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/thread_contention_with_release.cpp b/compiler-rt/test/gwp_asan/thread_contention_with_release.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/thread_contention_with_release.cpp @@ -0,0 +1,84 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation and deallocation works as expected +// under threading conditions. + +// Note: Compilation of and are extremely expensive for +// non-opt builds of clang. We attempt to minimise this by making a single +// compilation and everything else is decided at runtime. +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=4 %run %t 4 + +// Intentionally allocate and free all slots before thrashing. This means we +// get the random slot selection mechanism. +// RUN: %env_no_sample_options=MaxSimultaneousAllocations=4 %run %t 4 RNG + +#include +#include +#include +#include +#include + +#include "interface_helpers.h" + +constexpr unsigned NUM_ITERATIONS = 10000; + +void task(std::atomic *StartingGun) { + while (!StartingGun->load()) { + } + + // Get ourselves a new allocation. + for (int i = 0; i < NUM_ITERATIONS; ++i) { + volatile char *Ptr = reinterpret_cast( + __internal::allocate(__internal::maximumAllocationSize())); + // Do any other threads have access to this page? + if (*Ptr != 0) + exit(EXIT_FAILURE); + + // Mark the page as from malloc. Wait to see if another thread also takes + // this page. + *Ptr = 'A'; + std::this_thread::sleep_for(std::chrono::nanoseconds(10000)); + + // Check we still own the page. + if (*Ptr != 'A') + exit(EXIT_FAILURE); + + // And now release it. + *Ptr = 0; + free(const_cast(Ptr)); + } +} + +int main(int argc, char **argv) { + if (argc < 2) + exit(EXIT_FAILURE); + // Note that NumThreads == MaxSimultaneousAllocations. + int NumThreads = atoi(argv[1]); + + // Optionally, allocate and free all the possible allocations so that PRNG + // selection of the slot takes place. + if (argc > 2) { + for (int i = 0; i < NumThreads; ++i) { + void *Ptr = __internal::allocate(1); + __internal::deallocate(Ptr); + } + } + + std::atomic StartingGun{false}; + std::vector Threads; + + if (std::thread::hardware_concurrency() < NumThreads) { + NumThreads = std::thread::hardware_concurrency(); + } + + for (int i = 0; i < NumThreads; ++i) { + Threads.emplace_back(task, &StartingGun); + } + + StartingGun.store(true); + + for (auto &T : Threads) + T.join(); + + return 0; +} diff --git a/compiler-rt/test/gwp_asan/use_after_delete.cpp b/compiler-rt/test/gwp_asan/use_after_delete.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/use_after_delete.cpp @@ -0,0 +1,18 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Use after free occurred when accessing memory at: + +#include + +int main() { + char *Ptr = new char; + + *Ptr = 0x0; + + delete Ptr; + volatile char x = *Ptr; + return 0; +} diff --git a/compiler-rt/test/gwp_asan/use_after_deletea.cpp b/compiler-rt/test/gwp_asan/use_after_deletea.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/use_after_deletea.cpp @@ -0,0 +1,20 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Use after free occurred when accessing memory at: + +#include + +int main() { + char *Ptr = new char[10]; + + for (unsigned i = 0; i < 10; ++i) { + *(Ptr + i) = 0x0; + } + + delete[] Ptr; + volatile char x = *Ptr; + return 0; +} diff --git a/compiler-rt/test/gwp_asan/use_after_free.cpp b/compiler-rt/test/gwp_asan/use_after_free.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/use_after_free.cpp @@ -0,0 +1,20 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Use after free occurred when accessing memory at: + +#include + +int main() { + char *Ptr = reinterpret_cast(malloc(10)); + + for (unsigned i = 0; i < 10; ++i) { + *(Ptr + i) = 0x0; + } + + free(Ptr); + volatile char x = *Ptr; + return 0; +} diff --git a/compiler-rt/test/lit.common.cfg b/compiler-rt/test/lit.common.cfg --- a/compiler-rt/test/lit.common.cfg +++ b/compiler-rt/test/lit.common.cfg @@ -239,6 +239,9 @@ if config.can_symbolize: config.available_features.add('can-symbolize') +if config.gwp_asan: + config.available_features.add('gwp_asan') + lit.util.usePlatformSdkOnDarwin(config, lit_config) if config.host_os == 'Darwin': diff --git a/compiler-rt/test/lit.common.configured.in b/compiler-rt/test/lit.common.configured.in --- a/compiler-rt/test/lit.common.configured.in +++ b/compiler-rt/test/lit.common.configured.in @@ -41,6 +41,7 @@ set_default("android_serial", "@ANDROID_SERIAL_FOR_TESTING@") set_default("android_files_to_push", []) set_default("have_rpc_xdr_h", @HAVE_RPC_XDR_H@) +set_default("gwp_asan", @COMPILER_RT_HAS_GWP_ASAN_PYBOOL@) config.available_features.add('target-is-%s' % config.target_arch) if config.enable_per_target_runtime_dir: diff --git a/compiler-rt/test/scudo/lit.cfg b/compiler-rt/test/scudo/lit.cfg --- a/compiler-rt/test/scudo/lit.cfg +++ b/compiler-rt/test/scudo/lit.cfg @@ -49,6 +49,13 @@ # Android defaults to abort_on_error=1, which doesn't work for us. default_scudo_opts = 'abort_on_error=0' +# Disable GWP-ASan for scudo internal tests. +if config.gwp_asan: + gwp_asan_options = 'Enabled=0' + config.environment['GWP_ASAN_OPTIONS'] = gwp_asan_options + config.substitutions.append(('%env_gwp_asan_options=', + 'env GWP_ASAN_OPTIONS=' + gwp_asan_options)) + if default_scudo_opts: config.environment['SCUDO_OPTIONS'] = default_scudo_opts default_scudo_opts += ':' diff --git a/llvm/docs/GwpAsan.rst b/llvm/docs/GwpAsan.rst new file mode 100644 --- /dev/null +++ b/llvm/docs/GwpAsan.rst @@ -0,0 +1,189 @@ +======== +GWP-ASan +======== + +.. contents:: + :local: + :depth: 2 + +Introduction +============ + +GWP-ASan is a sampled allocator framework that assists in finding use-after-free +and heap-buffer-overflow bugs in production environments. It informally is a +recursive acronym, "**G**\WP-ASan **W**\ill **P**\rovide **A**\llocation +**SAN**\ity". + +GWP-ASan is based on the classic +`Electric Fence Malloc Debugger `_, with a +key adaptation. Notably, we only choose a very small percentage of allocations +to sample, and apply guard pages to these sampled allocations only. The sampling +is small enough to allow us to have very low performance overhead. + +There is a small, tunable memory overhead that is fixed for the lifetime of the +process. This is approximately ~60KiB per process using the default settings, +depending on the average size of your allocations. Future improvements should +drastically reduce this amount. + +GWP-ASan vs. ASan +================= + +Unlike `AddressSanitizer `_, +GWP-ASan does not induce a significant performance overhead. ASan often requires +the use of dedicated canaries to be viable in production environments, and as +such is often impractical. + +GWP-ASan is only capable of finding a subset of the memory issues detected by +ASan. Furthermore, GWP-ASan's bug detection capabilities are only probabilistic. +As such, we recommend using ASan over GWP-ASan in testing, as well as anywhere +else that guaranteed error detection is more valuable than the 2x execution +slowdown/binary size bloat. For the majority of production environments, this +impact is too high, and GWP-ASan proves extremely useful. + +Design +====== + +**Please note:** The implementation of GWP-ASan is largely in-flux, and these +details are subject to change. There are currently other implementations of +GWP-ASan, such as the implementation featured in +`Chromium `_. The +long-term support goal is to ensure feature-parity where reasonble, and to +support compiler-rt as the reference implementation. + +Allocator Support +----------------- + +GWP-ASan is not a replacement for a traditional allocator. Instead, it works by +inserting stubs into an existing allocator to redirect allocations when they're +chosen to be sampled. These stubs are generally implemented in the implementaion +of ``malloc()``, ``free()`` and ``realloc()``. The stubs are extremely small, +which makes using GWP-ASan in most allocators fairly trivial. The stubs follow +the same general pattern (example ``malloc()`` pseudocode below): + +.. code:: cpp + #ifdef INSTALL_GWP_ASAN_STUBS + gwp_asan::GuardedPoolAllocator GWPASanAllocator; + #endif + + void* YourAllocator::malloc(..) { + #ifdef INSTALL_GWP_ASAN_STUBS + if (GWPASanAllocator.shouldSample(..)) + return GWPASanAllocator.allocate(..); + #endif + + // ... the rest of your allocator code here. + } + +Then, all the supported allocator needs to do is compile with +``-DINSTALL_GWP_ASAN_STUBS`` and link against the GWP-ASan library! + +Guarded Allocation Pool +----------------------- + +The core of GWP-ASan is the guarded allocation pool. Each sampled allocation is +backed using its own *guarded* slot, which may consist of one or more accessible +pages. Each guarded slot is surrounded by two *guard* pages, which are mapped as +inaccessible. We create a contiguous buffer of this ``guard_page | guarded_slot +| guard_page`` pattern, which we call the *guarded allocation pool*. + +Buffer Underflow/Overflow Detection +----------------------------------- + +We gain buffer-overflow and buffer-underflow through these guard pages. When a +memory access overruns the allocated buffer, it will touch the inaccessible +guard page, causing memory exception. This exception is caught and handled by +the internal crash handler. Because each allocation is recorded with metadata +about where (and by what thread) it was allocated and deallocated, we can +provide helpful information that will help identify the root cause of the bug. + +In order to increase our detection of overflows, we randomly align half of the +allocations to the right hand side of the guarded slot. + +Use after Free Detection +------------------------ + +The guarded allocation pool also provide use-after-free detection. Whenever a +sampled allocation is deallocated, we map the guard page as inaccessible. Any +memory accesses after deallocation will thus trigger the crash handler, and we +can provide useful information about the source of the error. + +Please note that the use-after-free detection for a sampled allocation is +transient. We reuse inaccessible slots on-demand as we have a fixed number of +them, and wish to avoid starving the allocation pool to solely catch +use-after-frees. We ranomly choose an inaccessible slot to reuse in order to +provide a chance of detecting long-lived use-after-frees. + +Usage +===== + +Currently, the only allocator that supports GWP-ASan is the +`Scudo Hardened Allocator `_. +Building compiler-rt will install GWP-ASan into your local version of Scudo, and +any binary built with Scudo will also have GWP-ASan enabled by default. +Instructions on using Scudo for your applications can be found on the Scudo wiki +page. + +Options +------- + +GWP-ASan is configured on a per-allocator basis. We provide a default +implementation of configuration that is used by Scudo. Several aspects of +GWP-ASan can be configured on a per process basis through the following ways: + +- at compile time, by defining ``GWP_ASAN_DEFAULT_OPTIONS`` to the options + string you want set by default; + +- by defining a ``__gwp_asan_default_options`` function in one's program that + returns the options string to be parsed. Said function must have the following + prototype: ``extern "C" const char* __gwp_asan_default_options(void)``, with a + default visibility. This will override the compile time define; + +- through the environment variable ``GWP_ASAN_OPTIONS``, containing the options string + to be parsed. Options defined this way will override any definition made + through ``__gwp_asan_default_options``. + +The options string follows a syntax similar to ASan, where distinct options +can be assigned in the same string, separated by colons. + +For example, using the environment variable: + +.. code:: console + + GWP_ASAN_OPTIONS="MaxSimultaneousAllocations=16:SampleRate=5000" ./a.out + +Or using the function: + +.. code:: cpp + + extern "C" const char *__gwp_asan_default_options() { + return "MaxSimultaneousAllocations=16:SampleRate=5000"; + } + +The following options are available: + ++----------------------------+---------+--------------------------------------------------------------------------------+ +| Option | Default | Description | ++----------------------------+---------+--------------------------------------------------------------------------------+ +| Enabled | true | Is GWP-ASan enabled? | ++----------------------------+---------+--------------------------------------------------------------------------------+ +| PerfectlyRightAlign | false | When allocations are right-aligned, should we perfectly align them up to the | +| | | page boundary? By default (false), we round up allocation size to the nearest | +| | | power of two (2, 4, 8, 16) up to a maximum of 16-byte alignment for | +| | | performance reasons. Setting this to true can find single byte | +| | | buffer-overflows at the cost of performance, and may be incompatible with | +| | | some architectures. | ++----------------------------+---------+--------------------------------------------------------------------------------+ +| MaxSimultaneousAllocations | 16 | Number of simultaneously-guarded allocations available in the pool. | ++----------------------------+---------+--------------------------------------------------------------------------------+ +| SampleRate | 5000 | The probability (1 / SampleRate) that a page is selected for GWP-ASan | +| | | sampling. Sample rates up to 2^63 are supported. | ++----------------------------+---------+--------------------------------------------------------------------------------+ +| InstallSignalHandlers | true | Install GWP-ASan signal handlers for SIGSEGV. This allows better error reports | +| | | by providing stack traces for allocation/deallocation when reporting a | +| | | memory error. These handlers are generally installed during dynamic loading, | +| | | and will ensure that the previously installed signal handlers will be called. | +| | | User programs that install SIGSEGV handlers should ensure that any previously | +| | | installed signal handlers are also called. Note, if the previously installed | +| | | signal handler is SIG_IGN, we terminate the process after dumping the error | +| | | report. | ++----------------------------+---------+--------------------------------------------------------------------------------+