diff --git a/compiler-rt/CMakeLists.txt b/compiler-rt/CMakeLists.txt --- a/compiler-rt/CMakeLists.txt +++ b/compiler-rt/CMakeLists.txt @@ -277,6 +277,11 @@ option(COMPILER_RT_USE_BUILTINS_LIBRARY "Use compiler-rt builtins instead of libgcc" ${DEFAULT_COMPILER_RT_USE_BUILTINS_LIBRARY}) +option(GWP_ASAN_SCUDO_HOOKS + "When building scudo, should we install GWP-ASan hooks?" + COMPILER_RT_HAS_GWP_ASAN) +pythonize_bool(GWP_ASAN_SCUDO_HOOKS) + include(config-ix) #================================ 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 @@ -232,6 +232,7 @@ set(ALL_DFSAN_SUPPORTED_ARCH ${X86_64} ${MIPS64} ${ARM64}) set(ALL_FUZZER_SUPPORTED_ARCH ${X86_64} ${ARM64}) +set(ALL_GWP_ASAN_SUPPORTED_ARCH ${X86} ${X86_64} ${ARM64} ${ARM32}) if(APPLE) set(ALL_LSAN_SUPPORTED_ARCH ${X86} ${X86_64} ${MIPS64} ${ARM64}) else() @@ -431,6 +432,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) @@ -498,6 +502,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) @@ -525,7 +530,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}") @@ -663,3 +668,12 @@ 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() 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,48 @@ +//===-- 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_ + +#if defined(_WIN64) +// 64-bit Windows uses LLP64 data model. +typedef unsigned long long uptr; // NOLINT +typedef signed long long sptr; // NOLINT +#else +typedef unsigned long uptr; // NOLINT +typedef signed long sptr; // NOLINT +#endif // defined(_WIN64) + +#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 NORETURN +# undef NORETURN +#endif // defined(NORETURN) +#define NORETURN __attribute__((noreturn)) + +#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,248 @@ +//===-- 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. +// This class is **thread-hostile** until init() is completely finished. Any +// implementing allocator must ensure that init() is called in a thread-safe +// manner, before any calls to public methods are made. +class GuardedPoolAllocator { +public: + 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 MaximumStackFrames = 1; + + struct CallSiteInfo { + // The backtrace to the allocation/deallocation. If the first value is + // zero, we did not collect a trace. + uintptr_t Trace[MaximumStackFrames] = {}; + // The thread ID for this trace, or UINT64_MAX if not available. + uint64_t ThreadID = UINT64_MAX; + }; + + // 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) const; + + // 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) + // and exits the program. 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 helps solve the static initialisation order problem. + NORETURN static void reportErrorAndDie(uintptr_t AccessPtr, + Error Error = Error::UNKNOWN); + +private: + // 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 UINT64_MAX 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, and place the slot number into + // *SlotIndex. Returns false if there are no remaining slots, true otherwise. + bool reserveSlot(std::size_t *SlotIndex); + + // 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); + + NORETURN void reportErrorAndDieInternal(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 NumGuardedSlots = 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; + + // Pointer to the singleton version of this class. Automatically assigned + // during initialisation, this allows the signal handler to find this class + // in order to deduce the root cause of failures. + static GuardedPoolAllocator *SingletonPtr; +}; + +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,370 @@ +//===-- 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/options.h" + +#include +#include +#include +#include +#include + +using AllocationMetadata = gwp_asan::GuardedPoolAllocator::AllocationMetadata; +using Error = gwp_asan::GuardedPoolAllocator::Error; + +namespace gwp_asan { +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) + 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); + } + + SingletonPtr = this; + + NumGuardedSlots = Opts.NumUsableGuardedSlots; + + PageSize = getPlatformPageSize(); + + PerfectlyRightAlign = Opts.PerfectlyRightAlign; + Printf = Opts.Printf; + + std::size_t PoolBytesRequired = PageSize * (1 + NumGuardedSlots) + + NumGuardedSlots * maximumAllocationSize(); + void *GuardedPoolMemory = mapMemory(PoolBytesRequired); + + // Round up memory mappings to the nearest page size. + std::size_t BytesRequired = NumGuardedSlots * sizeof(AllocationMetadata); + Metadata = reinterpret_cast(mapMemory(BytesRequired)); + markReadWrite(Metadata, BytesRequired); + + // Allocate memory and set up the free pages queue. + BytesRequired = NumGuardedSlots * sizeof(std::size_t); + 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) + __atomic_store_n(&AdjustedSampleRate, Opts.SampleRate * 2, + __ATOMIC_RELAXED); + else + __atomic_store_n(&AdjustedSampleRate, 1, __ATOMIC_RELAXED); + + 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; + if (reserveSlot(&Index) == false) + 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->Size = Size; + Meta->Addr = Ptr; + Meta->IsDeallocated = false; + Meta->AllocationTrace.ThreadID = getThreadID(); + Meta->DeallocationTrace.ThreadID = UINT64_MAX; + + // TODO(hctim): Implement stack trace collection. + Meta->AllocationTrace.Trace[0] = 0; + Meta->DeallocationTrace.Trace[0] = 0; + + 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) + reportErrorAndDie(UPtr, Error::INVALID_FREE); + + ScopedLock L(&PoolMutex); + + if (Meta->IsDeallocated) + reportErrorAndDie(UPtr, Error::DOUBLE_FREE); + + Meta->IsDeallocated = true; + Meta->DeallocationTrace.ThreadID = getThreadID(); + + markInaccessible(reinterpret_cast(SlotStart), + maximumAllocationSize()); + freeSlot(addrToSlot(UPtr)); +} + +std::size_t GuardedPoolAllocator::getSize(const void *Ptr) const { + assert(pointerIsMine(Ptr)); + AllocationMetadata *Meta = addrToMetadata(reinterpret_cast(Ptr)); + assert(Meta->Addr == reinterpret_cast(Ptr)); + return Meta->Size; +} + +std::size_t GuardedPoolAllocator::maximumAllocationSize() const { + 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; +} + +bool GuardedPoolAllocator::reserveSlot(std::size_t *SlotIndex) { + // 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 < NumGuardedSlots) { + *SlotIndex = NumSampledAllocations++; + return true; + } + + if (FreeSlotsLength == 0) + return false; + + std::size_t ReservedIndex = Random::getRandomUnsigned64() % FreeSlotsLength; + *SlotIndex = FreeSlots[ReservedIndex]; + FreeSlots[ReservedIndex] = FreeSlots[--FreeSlotsLength]; + return true; +} + +void GuardedPoolAllocator::freeSlot(std::size_t SlotIndex) { + assert(FreeSlotsLength < NumGuardedSlots); + 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; +} + +NORETURN void GuardedPoolAllocator::reportErrorAndDie(uintptr_t AccessPtr, + Error Error) { + assert(SingletonPtr != nullptr); + SingletonPtr->reportErrorAndDieInternal(AccessPtr, Error); +} + +std::size_t GuardedPoolAllocator::getNearestSlot(uintptr_t Ptr) const { + if (Ptr <= GuardedPagePool + PageSize) + return 0; + if (Ptr > GuardedPagePoolEnd - PageSize) + return NumGuardedSlots - 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; + else + 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; +} + +void printErrorTypeAndMaybeDie(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); + exit(EXIT_FAILURE); + 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) + exit(EXIT_FAILURE); + 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 ", Meta->Addr - AccessPtr); + else if (AccessPtr > Meta->Addr) + Printf("located %zu bytes to the right of ", AccessPtr - Meta->Addr); + Printf("a %zu-byte allocation located at 0x%zx\n", Meta->Size, Meta->Addr); +} + +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); + } +} + +NORETURN void +GuardedPoolAllocator::reportErrorAndDieInternal(uintptr_t AccessPtr, + Error Error) { + if (!pointerIsMine(reinterpret_cast(AccessPtr))) { + Printf("Segmentation fault\n"); + exit(EXIT_FAILURE); + } + + // 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"); + + 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, die here. + printErrorTypeAndMaybeDie(Error, AccessPtr, Meta, Printf); + + // 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"); + exit(EXIT_FAILURE); + } + + 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. + + exit(EXIT_FAILURE); +} + +TLS_INITIAL_EXEC uint64_t GuardedPoolAllocator::NextSampleCounter = 0; +GuardedPoolAllocator *GuardedPoolAllocator::SingletonPtr = nullptr; + +} // namespace gwp_asan 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,102 @@ +//===-- 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); +} + +void (*PreviousSignumHandler)(int) = nullptr; +void (*PreviousSigactionHandler)(int, siginfo_t *, void *) = nullptr; + +static void sigSegvHandler(int sig, siginfo_t *info, void *ucontext) { + if (sig != SIGSEGV) + return; + + if (info == nullptr) + return; + + gwp_asan::GuardedPoolAllocator::reportErrorAndDie( + reinterpret_cast(info->si_addr)); + + if (PreviousSignumHandler) + PreviousSignumHandler(sig); + if (PreviousSigactionHandler) + PreviousSigactionHandler(sig, info, ucontext); +} + +void GuardedPoolAllocator::installSignalHandlers() { + struct sigaction OldHandler; + if (sigaction(SIGSEGV, nullptr, &OldHandler) == 0) { + if (OldHandler.sa_flags & SA_SIGINFO) + PreviousSigactionHandler = OldHandler.sa_sigaction; + else + PreviousSignumHandler = OldHandler.sa_handler; + } + + struct sigaction Action; + Action.sa_sigaction = sigSegvHandler; + Action.sa_flags = SA_SIGINFO; + sigaction(SIGSEGV, &Action, nullptr); +} + +uint64_t GuardedPoolAllocator::getThreadID() { +#ifdef SYS_gettid + return syscall(SYS_gettid); +#else + return UINT64_MAX; +#endif +} + +} // namespace gwp_asan 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: + 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->NumUsableGuardedSlots <= 0) { + __sanitizer::Printf( + "GWP-ASan ERROR: NumUsableGuardedSlots 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,43 @@ +//===-- 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_ + +#include "definitions.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,48 @@ +//===-- 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 + +#ifndef GWP_ASAN_DEFINITIONS_H_ +#error "Include gwp_asan/definitions.h 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, NumUsableGuardedSlots, 16, + "Number of usable guarded slots in the allocation pool. Defaults to 16.") + +GWP_ASAN_OPTION( + uptr, 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.") 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,54 @@ +//===-- 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 + +#include "gwp_asan/definitions.h" + +namespace gwp_asan { + +class Random { +public: + // 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. + static ALWAYS_INLINE uint64_t getRandomUnsigned64() { + 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; + } + + // xorshift* (64-bit output). This is primarily used to seed the xorshift128+ + // generator. + static ALWAYS_INLINE uint64_t xorShiftStar64() { + uint64_t A = RandomStateA; + A ^= A >> 12; + A ^= A << 25; + A ^= A >> 27; + RandomStateA = A; + return A * 0x2545F4914F6CDD1D; + } + +private: + static TLS_INITIAL_EXEC uint64_t RandomStateA; + static TLS_INITIAL_EXEC uint64_t RandomStateB; +}; +} // 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,17 @@ +//===-- 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 { +TLS_INITIAL_EXEC uint64_t Random::RandomStateA = + static_cast(time(nullptr)); +TLS_INITIAL_EXEC uint64_t Random::RandomStateB = xorShiftStar64(); +} // namespace gwp_asan diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_common.h b/compiler-rt/lib/sanitizer_common/sanitizer_common.h --- a/compiler-rt/lib/sanitizer_common/sanitizer_common.h +++ b/compiler-rt/lib/sanitizer_common/sanitizer_common.h @@ -22,6 +22,8 @@ #include "sanitizer_list.h" #include "sanitizer_mutex.h" +#include + #if defined(_MSC_VER) && !defined(__clang__) extern "C" void _ReadWriteBarrier(); #pragma intrinsic(_ReadWriteBarrier) @@ -186,6 +188,7 @@ bool ColorizeReports(); void RemoveANSIEscapeSequencesFromString(char *buffer); void Printf(const char *format, ...); +void VariadicPrintf(const char *format, va_list ap); void Report(const char *format, ...); void SetPrintfAndReportCallback(void (*callback)(const char *)); #define VReport(level, ...) \ diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_printf.cc b/compiler-rt/lib/sanitizer_common/sanitizer_printf.cc --- a/compiler-rt/lib/sanitizer_common/sanitizer_printf.cc +++ b/compiler-rt/lib/sanitizer_common/sanitizer_printf.cc @@ -322,6 +322,10 @@ va_end(args); } +void VariadicPrintf(const char *format, va_list ap) { + SharedPrintfCode(false, format, ap); +} + // Like Printf, but prints the current PID before the output string. FORMAT(1, 2) void Report(const char *format, ...) { 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 (GWP_ASAN_SCUDO_HOOKS) + # 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,55 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=1 %run %t + +#include +#include +#include +#include +#include + +// Note, statically defined in order to avoid calling malloc() to store this +// data. Consider this a map where element n => element n+1, and we are testing +// that an allocation of size [n] rounds up to the nearest [n+1]-byte boundary. +int 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 +}; + +int main() { + for (unsigned i = 0; i < sizeof(AllocSizeToAlignment) / sizeof(int); i += 2) { + // Each allocation has a 50/50 chance of being left/right aligned. As we + // want to test both instances, and have no way of directly controlling + // left/right alignment, we test each allocation multiple times. We have a + // 0.195% chance of all allocations being left or right aligned when we do + // 10 tests. + for (unsigned j = 0; j < 10; ++j) { + char *Ptr = reinterpret_cast(malloc(AllocSizeToAlignment[i])); + if (reinterpret_cast(Ptr) % AllocSizeToAlignment[i + 1] != 0) { + fprintf(stderr, + "Guarded pointer %p with size %i is not %i-byte aligned.\n", + Ptr, AllocSizeToAlignment[i], AllocSizeToAlignment[i + 1]); + exit(EXIT_FAILURE); + } + + // Normally causes buffer overflow, but alignment should prevent this. + volatile char x = *(Ptr + AllocSizeToAlignment[i + 1] - 1); + 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. + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=1 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=2 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=11 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=12 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=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() { + char *Ptr = reinterpret_cast(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,17 @@ +// 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: 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,17 @@ +// 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: 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/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,17 @@ +// 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 + +#include "page_size.h" + +int main() { + char *Ptr = reinterpret_cast(malloc(pageSize())); + 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,17 @@ +// 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 + +#include "page_size.h" + +int main() { + char *Ptr = reinterpret_cast(malloc(pageSize())); + free(Ptr + pageSize()); + 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,44 @@ +# -*- 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)) + +# 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/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,68 @@ +// REQUIRES: gwp_asan + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=1 %run %t 1 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=2 %run %t 2 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=3 %run %t 3 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=8 %run %t 8 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=16 %run %t 16 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=32 %run %t 32 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=64 %run %t 64 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=128 %run %t 128 2>&1 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=129 %run %t 129 2>&1 + +// This test massages the GPA to reveal internal data that we test for. In +// particular, this test ensures that our allocations use all of the available +// slots. It relies on the fact that the 1st allocation -> ${NumGuardedSlots}th +// allocation are sequential, before PRNG selection of the guarded slots takes +// place. +// TODO(hctim): Update this test when the slot size is different to a single +// page. + +#include +#include +#include + +#include "page_size.h" + +char *SampledAllocs[129] = {}; + +int main(int argc, char** argv) { + int NumSlots = atoi(argv[1]); + + uintptr_t PageSize = pageSize(); + + SampledAllocs[0] = reinterpret_cast(malloc(1)); + char *FirstGuardedSlot = reinterpret_cast( + reinterpret_cast(SampledAllocs[0]) & ~(PageSize - 1)); + + for (unsigned i = 1; i < NumSlots; ++i) { + char *Ptr = reinterpret_cast(malloc(PageSize)); + char *Slot = reinterpret_cast(reinterpret_cast(Ptr) & + ~(PageSize - 1)); + if (Slot != FirstGuardedSlot + (i * 2 * PageSize)) { + fprintf( + stderr, + "Allocation number %i (%p) was either not in the guarded pool, or it " + "is reusing a slot before all slots have been used once.\n", + i, Ptr); + exit(EXIT_FAILURE); + } + SampledAllocs[i] = Ptr; + } + + // Now, we have exhausted the pool. Check to see that the next allocation goes + // to the system allocator. + char *Ptr = reinterpret_cast(malloc(PageSize)); + // The system allocator doesn't have use-after-free READ detection. If this + // was a sampled allocation, this would be caught. + free(Ptr); + volatile char x = *Ptr; + + // Clean up allocations. + for (unsigned i = 0; SampledAllocs[i] != nullptr; ++i) { + free(SampledAllocs[i]); + } + + 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,21 @@ +#ifndef PAGE_SIZE_ +#define PAGE_SIZE_ + +#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) +# include +unsigned pageSize() { + return sysconf(_SC_PAGESIZE); +} +#endif + +#if defined (_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +unsigned pageSize() { + SYSTEM_INFO system_info; + GetSystemInfo(&system_info); + return system_info.dwPageSize; +} +#endif + +#endif // PAGE_SIZE_ diff --git a/compiler-rt/test/gwp_asan/pattern_calloc_free.cpp b/compiler-rt/test/gwp_asan/pattern_calloc_free.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/pattern_calloc_free.cpp @@ -0,0 +1,21 @@ +// 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 + +int main() { + for (unsigned i = 1; i < 0x100000; i <<= 1) { + char *Ptr = reinterpret_cast(calloc(i, sizeof(char))); + + for (unsigned j = 0; j < i; ++j) { + *(Ptr + j) = 0x0; + } + + free(Ptr); + } + return 0; +} diff --git a/compiler-rt/test/gwp_asan/pattern_malloc_free.cpp b/compiler-rt/test/gwp_asan/pattern_malloc_free.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/pattern_malloc_free.cpp @@ -0,0 +1,21 @@ +// 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 + +int main() { + for (unsigned i = 1; i < 0x100000; i <<= 1) { + char *Ptr = reinterpret_cast(malloc(i)); + + for (unsigned j = 0; j < i; ++j) { + *(Ptr + j) = 0x0; + } + + free(Ptr); + } + return 0; +} diff --git a/compiler-rt/test/gwp_asan/pattern_new_delete.cpp b/compiler-rt/test/gwp_asan/pattern_new_delete.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/pattern_new_delete.cpp @@ -0,0 +1,13 @@ +// 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() { + char *Ptr = new char; + *Ptr = 0x0; + delete Ptr; + return 0; +} diff --git a/compiler-rt/test/gwp_asan/pattern_newa_deletea.cpp b/compiler-rt/test/gwp_asan/pattern_newa_deletea.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/pattern_newa_deletea.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/pattern_realloc_free.cpp b/compiler-rt/test/gwp_asan/pattern_realloc_free.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/pattern_realloc_free.cpp @@ -0,0 +1,20 @@ +// 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 + +int main() { + char *Ptr = reinterpret_cast(realloc(nullptr, 1)); + for (unsigned i = 1; i < 0x100000; i <<= 1) { + Ptr = reinterpret_cast(realloc(Ptr, i)); + + for (unsigned j = 0; j < i; ++j) { + *(Ptr + j) = 0x0; + } + } + 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,35 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation/memory access/deallocation works +// as expected. + +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=1 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=2 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=8 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=16 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=32 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=64 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=128 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=129 not %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 of a + +#include + +#include "page_size.h" + +int main() { + unsigned PageSize = pageSize(); + for (unsigned i = 0; i < 128; ++i) { + char *Ptr = reinterpret_cast(malloc(PageSize)); + *Ptr = 0x0; + free(Ptr); + } + + char *Ptr = reinterpret_cast(malloc(PageSize)); + volatile char x = *(Ptr + PageSize); + + 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,73 @@ +// REQUIRES: gwp_asan +// This test ensures that normal allocation/memory access/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_gwp_asan_options=NumUsableGuardedSlots=32 %t 32 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=127 %t 127 +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=1028 %t 1028 + +// Intentionally allocate and free all slots before thrashing. This means we +// get the random slot selection mechanism. +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=32 %t 32 RNG +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=127 %t 127 RNG +// RUN: %env_gwp_asan_options=NumUsableGuardedSlots=1028 %t 1028 RNG + +#include +#include +#include + +#include "page_size.h" + +void asyncMalloc(void *Pointers[], int NumAllocationsLeft, + std::atomic *StartingGun) { + while (!StartingGun->load()) { + } + + for (int i = NumAllocationsLeft; i > 0; --i) + Pointers[i - 1] = malloc(pageSize()); +} + +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); + } + } + } + + 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: not %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: not %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: not %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_scudo_hooks: + 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_scudo_hooks", @GWP_ASAN_SCUDO_HOOKS_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,12 @@ # 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. +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,188 @@ +======== +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 individual 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 choose the inaccessible slot to reuse at random 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="NumUsableGuardedPages=16:SampleRate=5000" ./a.out + +Or using the function: + +.. code:: cpp + + extern "C" const char *__gwp_asan_default_options() { + return "NumUsableGuardedPages=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. | ++------------------------+--------------------+---------------------------------------------------------------------------------+ +| NumUsableGuardedSlots | 16 | Number of usable guarded slots in the allocation 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. | ++------------------------+--------------------+---------------------------------------------------------------------------------+