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,10 @@ 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 (default=False)?" OFF) +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,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 potential +# TLS allocations on first-use. +if (COMPILER_RT_HAS_SANITIZER_COMMON AND 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 + mutex_posix.cpp + random.cpp +) + +set(GWP_ASAN_HEADERS + allocation_metadata.h + guarded_pool_allocator.h + mutex.h + options.h + options.inc + random.h +) + +# Runtime environment support is optional. GwpAsan is totally independent of +# sanitizer_common, the runtime env parser is not. This is an optional library +# that can be used by an allocator to automatically parse GwpAsan flags from the +# environment variable GWP_ASAN_FLAGS, but the allocator can choose to implement +# its own flag parsing and populate the Options struct themselves. +set(GWP_ASAN_FLAG_PARSER_SOURCES + optional/runtime_env_flag_parser.cpp +) +set(GWP_ASAN_FLAG_PARSER_HEADERS + optional/runtime_env_flag_parser.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) +set(GWP_ASAN_FLAG_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(RTGwpAsanFlagParser + ARCHS ${GWP_ASAN_SUPPORTED_ARCH} + SOURCES ${GWP_ASAN_FLAG_PARSER_SOURCES} + ADDITIONAL_HEADERS ${GWP_ASAN_FLAG_PARSER_HEADERS} + CFLAGS ${GWP_ASAN_FLAG_PARSER_CFLAGS}) +endif() diff --git a/compiler-rt/lib/gwp_asan/allocation_metadata.h b/compiler-rt/lib/gwp_asan/allocation_metadata.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/allocation_metadata.h @@ -0,0 +1,43 @@ +//===-- allocation_metadata.h ---------------------------------------------===// +// +// 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_ALLOCATION_METADATA_H_ +#define GWP_ASAN_ALLOCATION_METADATA_H_ + +#include +#include + +namespace gwp_asan { +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] = { 0lu }; + // 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 size requested by the caller. + std::size_t Size = 0; + + CallSiteInfo AllocationTrace; + CallSiteInfo DeallocationTrace; + + // Whether this allocation has been deallocated yet. + bool IsDeallocated = false; +}; +}; // namespace gwp_asan + +#endif // GWP_ASAN_ALLOCATION_METADATA_H_ 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,39 @@ +//===-- 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 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/allocation_metadata.h" +#include "gwp_asan/definitions.h" +#include "gwp_asan/mutex.h" +#include "gwp_asan/options.h" +#include "gwp_asan/random.h" + +#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. +class GuardedPoolAllocator { +public: + enum class GwpAsanError { + UNKNOWN, + USE_AFTER_FREE, + DOUBLE_FREE, + INVALID_FREE, + BUFFER_OVERFLOW, + BUFFER_UNDERFLOW + }; + + // During program startup, we must ensure that memory allocations do not land + // in this allocation pool until we're completely ready. The constructor + // ensures that shouldSample() and pointerIsMine() will always return false + // before this class has been fully initialised. Every call to non-static + // methods of this class should be guarded by a call to either of these + // functions before init() has been called. + constexpr GuardedPoolAllocator(){}; + GuardedPoolAllocator(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 racy 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() % + __atomic_load_n(&AdjustedSampleRate, __ATOMIC_RELAXED)) + + 1; + } + + return NextSampleCounter-- == 1 && + __atomic_load_n(&IsInitialised, __ATOMIC_RELAXED); + } + + // 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 GetGuardedPagePool() <= P && P < GetGuardedPagePoolEnd(); + } + + // 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, + GwpAsanError Error = GwpAsanError::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 unmapMemory(void *Ptr, 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. + 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(); + + ALWAYS_INLINE uintptr_t GetGuardedPagePool() const { + return __atomic_load_n(&GuardedPagePool, __ATOMIC_RELAXED); + } + + ALWAYS_INLINE uintptr_t GetGuardedPagePoolEnd() const { + return __atomic_load_n(&GuardedPagePoolEnd, __ATOMIC_RELAXED); + } + + // 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 a pointer 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 page that this pointer resides in. + uintptr_t getPageAddr(uintptr_t Ptr) const; + + // Gets the nearest slot the 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 guarded slot Returns true if there was a slot to be reserved, and + // writes the available index to the provided parameter. Returns false if no + // slots are available. + bool reserveSlot(std::size_t *PageIndex); + + // Unreserve the guarded slot. + void freeSlot(std::size_t PageIndex); + + // Uses the allocation strategy from the runtime flags and the allocation + // size provided to update where the allocation should be made. + // AllocationPtr should be passed to this function as a pointer to the + // slot in which the allocation should live. + void applyAllocationStrategy(std::size_t Size, uintptr_t *AllocationPtr); + + // Try-lock all the mutexes in this class. This is used to attempt to stop any + // race wherein a use-after-free has been triggered (and we're reporting the + // error), and another thread attempts to make a new allocation in the same + // slot. + void tryLockAllMutexes(); + + // Diagnose an unknown error. Place the type of the error in *Error, and + // return a pointer to the metadata for the slot where the fault manifests. + // May return nullptr if the error was unable to be diagnosed, or if it + // resulted in GwpAsanError::INVALID_FREE or GwpAsanError::UNKNOWN. + AllocationMetadata *diagnoseUnknownError(uintptr_t AccessPtr, + GwpAsanError *Error); + + NORETURN void reportErrorAndDieInternal(uintptr_t AccessPtr, + GwpAsanError Error); + +private: + // Whether this class has been initialised. This means the class is ready to + // accept calls to allocate() and deallocate(). Lazy initialisation may not + // have taken place even when this is true. + bool IsInitialised = false; + + // 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 of currently used slots. We store this amount so that we + // don't potentially randomly choose to recycle a slot that previously had an + // allocation before all the slots have been utilised. + std::size_t NumUsedSlots = 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 - 1, as + // when GWP-ASan is disabled, we wish to never spend wasted cycles + // recalcualting the sample rate. + ALIGNED(8) uint64_t AdjustedSampleRate = UINT64_MAX - 1; + // 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,372 @@ +//===-- 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/allocation_metadata.h" +#include "gwp_asan/options.h" + +#include +#include +#include +#include +#include + +namespace gwp_asan { +void GuardedPoolAllocator::init(const options::Options &Opts) { + SingletonPtr = this; + + // 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) + return; + + 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); + BytesRequired += PageSize - (BytesRequired % PageSize); + Metadata = reinterpret_cast(mapMemory(BytesRequired)); + markReadWrite(Metadata, BytesRequired); + + // Allocate memory and set up the free pages queue. + BytesRequired = NumGuardedSlots * sizeof(std::size_t); + BytesRequired += PageSize - (BytesRequired % PageSize); + FreeSlots = reinterpret_cast(mapMemory(BytesRequired)); + markReadWrite(FreeSlots, BytesRequired); + FreeSlotsLength = NumGuardedSlots; + for (std::size_t i = 0; i < NumGuardedSlots; ++i) + FreeSlots[i] = i; + + // 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); + + if (Opts.InstallSignalHandlers) + installSignalHandlers(); + + // Store these as the last values. This ensures that all calls to + // isPointerMine() return false until the allocator is completely ready. + __atomic_store_n(&GuardedPagePool, + reinterpret_cast(GuardedPoolMemory), + __ATOMIC_RELAXED); + __atomic_store_n(&GuardedPagePoolEnd, + reinterpret_cast(GuardedPoolMemory) + + PoolBytesRequired, + __ATOMIC_RELAXED); + __atomic_store_n(&IsInitialised, true, __ATOMIC_RELAXED); +} + +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); + markReadWrite(reinterpret_cast(Ptr), maximumAllocationSize()); + + AllocationMetadata *Meta = addrToMetadata(Ptr); + + Meta->Size = Size; + applyAllocationStrategy(Size, &Ptr); + + 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] = 0lu; + Meta->DeallocationTrace.Trace[0] = 0lu; + + return reinterpret_cast(Ptr); +} + +void GuardedPoolAllocator::deallocate(void *Ptr) { + assert(pointerIsMine(Ptr) && "Pointer is not mine!"); + uintptr_t UPtr = reinterpret_cast(Ptr); + uintptr_t Page = getPageAddr(UPtr); + AllocationMetadata *Meta = addrToMetadata(UPtr); + if (Meta->Addr != UPtr) + reportErrorAndDie(UPtr, GwpAsanError::INVALID_FREE); + + ScopedLock L(&PoolMutex); + + if (Meta->IsDeallocated) + reportErrorAndDie(UPtr, GwpAsanError::DOUBLE_FREE); + + Meta->IsDeallocated = true; + Meta->DeallocationTrace.ThreadID = getThreadID(); + + markInaccessible(reinterpret_cast(Page), 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 - GetGuardedPagePool(); + return ByteOffsetFromPoolStart / (maximumAllocationSize() + PageSize); +} + +uintptr_t GuardedPoolAllocator::slotToAddr(std::size_t N) const { + return GetGuardedPagePool() + (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 { + return ((Ptr - GetGuardedPagePool()) / PageSize) % 2 == 0; +} + +bool GuardedPoolAllocator::reserveSlot(std::size_t *PageIndex) { + if (FreeSlotsLength == 0) + return false; + + size_t ReservedIndex; + if (NumUsedSlots < NumGuardedSlots) { + ReservedIndex = NumUsedSlots++; + } else { + ReservedIndex = Random::getRandomUnsigned64() % FreeSlotsLength; + } + + *PageIndex = FreeSlots[ReservedIndex]; + + FreeSlots[ReservedIndex] = FreeSlots[--FreeSlotsLength]; + return true; +} + +void GuardedPoolAllocator::freeSlot(std::size_t PageIndex) { + assert(FreeSlotsLength < NumGuardedSlots); + FreeSlots[FreeSlotsLength++] = PageIndex; +} + +void GuardedPoolAllocator::applyAllocationStrategy(std::size_t Size, + uintptr_t *AllocationPtr) { + assert(Size > 0); + + bool ShouldRightAlign = addrToSlot(*AllocationPtr) % 2; + + if (!PerfectlyRightAlign) { + if (Size == 3) + Size = 4; + else if (Size > 4 && Size <= 8) + Size = 8; + else if (Size > 8 && (Size & 15) != 0) + Size += 16 - (Size & 15); + } + + if (ShouldRightAlign) + *AllocationPtr += maximumAllocationSize() - Size; +} + +NORETURN void GuardedPoolAllocator::reportErrorAndDie(uintptr_t AccessPtr, + GwpAsanError Error) { + if (SingletonPtr == nullptr) + exit(EXIT_FAILURE); + SingletonPtr->reportErrorAndDieInternal(AccessPtr, Error); +} + +std::size_t GuardedPoolAllocator::getNearestSlot(uintptr_t Ptr) const { + if (Ptr <= GetGuardedPagePool() + PageSize) + return 0; + else if (Ptr > GetGuardedPagePoolEnd() - 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. +} + +AllocationMetadata * +GuardedPoolAllocator::diagnoseUnknownError(uintptr_t AccessPtr, + GwpAsanError *Error) { + // Let's try and figure out what the source of this error is. + if (isGuardPage(AccessPtr)) { + std::size_t Slot = getNearestSlot(AccessPtr); + AllocationMetadata *Meta = addrToMetadata(slotToAddr(Slot)); + + // Ensure that this slot was allocated once upon a time. + if (!Meta->Addr) + return nullptr; + + bool SlotRoundedDown = (Meta->Addr < AccessPtr); + if (SlotRoundedDown) + *Error = GwpAsanError::BUFFER_OVERFLOW; + else + *Error = GwpAsanError::BUFFER_UNDERFLOW; + + return Meta; + } else { + // Access wasn't a guard page, check for use-after-free. + AllocationMetadata *Meta = addrToMetadata(AccessPtr); + if (Meta->IsDeallocated) { + *Error = GwpAsanError::USE_AFTER_FREE; + return Meta; + } + } + + // If we have reached here, the error is still unknown. There is no metadata + // available. + return nullptr; +} + +void printErrorTypeAndMaybeDie(GuardedPoolAllocator::GwpAsanError Error, + uintptr_t AccessPtr, AllocationMetadata *Meta, + options::Printf_t Printf) { + switch (Error) { + case GuardedPoolAllocator::GwpAsanError::UNKNOWN: + (*Printf)( + "The cause of the error is indeterminate. GWP-ASan couldn't " + "automatically determine the source of this memory error. " + "Note that this is almost definitely *not* a bug in GWP-ASan " + "itself. It was likely either caused by a memory bug for a non-sampled " + "allocation, or a wild memory access into the GWP-ASan pool.\n"); + (*Printf)("When trying to access memory at: 0x%zx\n", AccessPtr); + exit(EXIT_FAILURE); + case GuardedPoolAllocator::GwpAsanError::USE_AFTER_FREE: + (*Printf)("Use after free occured when accessing memory at: 0x%zx\n", + AccessPtr); + break; + case GuardedPoolAllocator::GwpAsanError::DOUBLE_FREE: + (*Printf)("Double free occured when trying to free memory at: 0x%zx\n", + AccessPtr); + break; + case GuardedPoolAllocator::GwpAsanError::INVALID_FREE: + (*Printf)( + "Invalid (wild) free occured 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 GuardedPoolAllocator::GwpAsanError::BUFFER_OVERFLOW: + (*Printf)("Buffer overflow occured when accessing memory at: 0x%zx\n", + AccessPtr); + break; + case GuardedPoolAllocator::GwpAsanError::BUFFER_UNDERFLOW: + (*Printf)("Buffer underflow occured when accessing memory at: 0x%zx\n", + AccessPtr); + break; + } + + (*Printf)("0x%zx is located ", AccessPtr); + if (AccessPtr < Meta->Addr) + (*Printf)("%zu bytes to the left ", Meta->Addr - AccessPtr); + else + (*Printf)("%zu bytes to the right ", AccessPtr - Meta->Addr); + (*Printf)("of a %zu-byte allocation located at 0x%zx\n", Meta->Size, + Meta->Addr); +} + +void printThreadInformation(GuardedPoolAllocator::GwpAsanError 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 == GuardedPoolAllocator::GwpAsanError::USE_AFTER_FREE || + Error == GuardedPoolAllocator::GwpAsanError::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, + GwpAsanError Error) { + // 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. + tryLockAllMutexes(); + + (*Printf)("*** GWP-ASan detected a memory error ***\n"); + + AllocationMetadata *Meta = nullptr; + + if (!pointerIsMine(reinterpret_cast(AccessPtr))) { + Error = GwpAsanError::UNKNOWN; + } else if (Error == GwpAsanError::UNKNOWN) { + Meta = diagnoseUnknownError(AccessPtr, &Error); + } 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); +} + +void GuardedPoolAllocator::tryLockAllMutexes() { PoolMutex.tryLock(); } + +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,97 @@ +//===-- 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::unmapMemory(void *Ptr, size_t Size) const { + if (munmap(Ptr, Size) != 0) { + (*Printf)("Failed to unmap guarded pool allocator memory, errno: %d\n", + errno); + (*Printf)(" munmap(%p, %zu) failed.\n", Ptr, Size); + exit(EXIT_FAILURE); + } +} + +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, 0, + 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); +} + +static void sigSegvHandler(int sig, siginfo_t *info, void *ucontext) { + if (sig != SIGSEGV) + return; + + if (info == nullptr) + exit(EXIT_FAILURE); + + gwp_asan::GuardedPoolAllocator::reportErrorAndDie( + reinterpret_cast(info->si_addr)); +} + +void GuardedPoolAllocator::installSignalHandlers() { + struct sigaction Action; + Action.sa_sigaction = sigSegvHandler; + Action.sa_flags = SA_SIGINFO; + sigaction(SIGSEGV, &Action, NULL); +} + +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,86 @@ +//===-- 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 implementaiton. 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 + +namespace gwp_asan { +class Mutex { +public: + void lock(); + bool tryLock(); + void unlock(); + +private: + void yieldProcessor(uint8_t Count); + void yieldPlatform(); + + void lockSlow(); + + bool Semaphore = false; +}; + +class ScopedLock { +public: + ScopedLock(Mutex *Mu) : Mutex(Mu) { Mutex->lock(); } + ~ScopedLock() { Mutex->unlock(); } + +private: + Mutex *Mutex; +}; + +ALWAYS_INLINE void Mutex::lock() { + if (tryLock()) + return; + lockSlow(); +} + +ALWAYS_INLINE bool Mutex::tryLock() { + return __atomic_exchange_n(&Semaphore, true, __ATOMIC_ACQUIRE) == false; +} + +ALWAYS_INLINE void Mutex::unlock() { + __atomic_store_n(&Semaphore, 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(&Semaphore, __ATOMIC_RELAXED) == false && tryLock()) + return; + } +} + +} // namespace gwp_asan + +#endif // GWP_ASAN_MUTEX_H_ diff --git a/compiler-rt/lib/gwp_asan/mutex_posix.cpp b/compiler-rt/lib/gwp_asan/mutex_posix.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/mutex_posix.cpp @@ -0,0 +1,15 @@ +//===-- mutex_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/mutex.h" + +#include + +namespace gwp_asan { +void Mutex::yieldPlatform() { sched_yield(); } +} // namespace gwp_asan diff --git a/compiler-rt/lib/gwp_asan/optional/runtime_env_flag_parser.h b/compiler-rt/lib/gwp_asan/optional/runtime_env_flag_parser.h new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/optional/runtime_env_flag_parser.h @@ -0,0 +1,32 @@ +//===-- runtime_env_flag_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_RUNTIME_ENV_FLAG_PARSER_H_ +#define GWP_ASAN_OPTIONAL_RUNTIME_ENV_FLAG_PARSER_H_ + +#include "gwp_asan/options.h" +#include "sanitizer_common/sanitizer_common.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 initFlags() 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_RUNTIME_ENV_FLAG_PARSER_H_ diff --git a/compiler-rt/lib/gwp_asan/optional/runtime_env_flag_parser.cpp b/compiler-rt/lib/gwp_asan/optional/runtime_env_flag_parser.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/optional/runtime_env_flag_parser.cpp @@ -0,0 +1,103 @@ +//===-- runtime_env_flag_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 +#include +#include + +#include "gwp_asan/optional/runtime_env_flag_parser.h" +#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 { +int sanitizerPrintfWrapper(const char *Format, ...) { + va_list args; + va_start(args, Format); + __sanitizer::VariadicPrintf(Format, args); + va_end(args); + return 0; +} + +static Options GWPAsanFlags; // Use via getOptions(). + +static 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 +} + +static const char *getCompileDefinitionGWPAsanDefaultOptions() { +#ifdef GWP_ASAN_DEFAULT_OPTIONS + return SANITIZER_STRINGIFY(GWP_ASAN_DEFAULT_OPTIONS); +#else + return ""; +#endif +} + +static const char *getGWPAsanDefaultOptions() { + return (&__gwp_asan_default_options) ? __gwp_asan_default_options() : ""; +} + + +void initOptions() { + __sanitizer::SetCommonFlagsDefaults(); + { + __sanitizer::CommonFlags cf; + cf.CopyFrom(*__sanitizer::common_flags()); + cf.exitcode = 1; + OverrideCommonFlags(cf); + } + Options *o = getOptions(); + o->setDefaults(); + + __sanitizer::FlagParser Parser; + registerGWPAsanFlags(&Parser, o); + RegisterCommonFlags(&Parser); + + // 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 = sanitizerPrintfWrapper; +} + +Options *getOptions() { return &GWPAsanFlags; } + +} // namespace options +} // namespace gwp_asan + +#if !SANITIZER_SUPPORTS_WEAK_HOOKS +SANITIZER_INTERFACE_WEAK_DEF(const char *, __gwp_asan_default_options, void) { + return ""; +} +#endif 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 for the +// C library. If the supported allocator exposes printing via a different +// function signature, please provide a wrapper which has the normal printf() +// signature, and pass the wrapper instead. +typedef int (*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,36 @@ +//===-- 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 +// +//===----------------------------------------------------------------------===// + +#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.") + +GWP_ASAN_OPTION( + int, NumUsableGuardedSlots, 16, + "Number of usable guarded slots in the allocation pool. Defaults to 16.") + +GWP_ASAN_OPTION( + unsigned long, SampleRate, 5000, + "The probability (1 / SampleRate) that an allocation is selected for " + "GWP-ASan sampling. Default is 5000. Sample rates up to 1 / (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 use-after-free.") 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,20 @@ 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 RTGwpAsanFlagParser) + list(APPEND SCUDO_CFLAGS -DGWP_ASAN_HOOKS) + + # TODO(hctim): Why do we require the atomics library here? When compiling i386 + # versions of Scudo, the linker fails to find __atomic_load_8 (used by + # GPA::shouldSample() to atomically load GPA::AdjustedSampleRate). + list(APPEND SCUDO_DYNAMIC_LINK_FLAGS -latomic) +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/runtime_env_flag_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,28 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=1 %run %t + +#include +#include +#include +#include + +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& P : AllocSizeToAlignment) { + char *Ptr = reinterpret_cast(malloc(P.first)); + if (reinterpret_cast(Ptr) % P.second != 0) { + exit(EXIT_FAILURE); + } + + // Normally causes buffer overflow, but alignment should prevent this. + volatile char x = *(Ptr + P.second - 1); + free(Ptr); + } + return 0; +} diff --git a/compiler-rt/test/gwp_asan/alignment_static.cpp b/compiler-rt/test/gwp_asan/alignment_static.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/test/gwp_asan/alignment_static.cpp @@ -0,0 +1,25 @@ +// REQUIRES: gwp_asan +// RUN: %clangxx_gwp_asan %s -o %t +// RUN: %env_gwp_asan_options= %run %t 4 +// RUN: %env_gwp_asan_options= %run %t 8 +// RUN: %env_gwp_asan_options= %run %t 16 + +#include +#include + +int main(int argc, char** argv) { + if (argc < 2) + exit(EXIT_FAILURE); + + int alignment = atoi(argv[1]); + + char *Ptr = reinterpret_cast(malloc(alignment - 1)); + + if (reinterpret_cast(Ptr) % alignment != 0) + exit(EXIT_FAILURE); + + // Normally causes buffer overflow, but alignment should prevent this. + volatile char x = *(Ptr + alignment - 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,29 @@ +// 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=NumUsableGuardedPages=1 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=2 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=15 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=16 %run %t +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=17 %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; + for (unsigned j = 0; j < i; ++j) { + *(Ptr + j) = 0x0; + } + } + + 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 occured 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 occured 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 occured 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 occured 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 occured 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 occured 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 occured 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/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,15 @@ +// 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; + *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=NumUsableGuardedPages=1 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=2 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=8 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=16 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=32 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=64 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=128 not %run %t 2>&1 | FileCheck %s +// RUN: %env_gwp_asan_options=NumUsableGuardedPages=129 not %run %t 2>&1 | FileCheck %s + +// CHECK: GWP-ASan detected a memory error +// CHECK: Buffer overflow occured 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/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 occured 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 occured 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 occured 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,190 @@ +======== +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's 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* page. Each guarded page is +surrounded by two *guard* pages, which are mapped as inaccessible. We create a +contiguous buffer of this ``guard_page | guarded_page | 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 generally align each sampled +allocation to the right hand side of the guard page. This means that a 1-byte +overflow will touch the guard page and trigger the crash handler. We can also +optionally choose to left-align sampled allocations, which increases the chance +of finding buffer underflows at the cost of reduced buffer overflow detection. + +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 guarded allocation is only +emphemeral. 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_asandefault_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? | ++------------------------+--------------------+---------------------------------------------------------------------------------+ +| RightAlignAll | false | Should we right-align all allocations? By default (false), we left-align | +| | | even-sized allocations, and right-align odd-sized allocations. | ++------------------------+--------------------+---------------------------------------------------------------------------------+ +| AlignmentStrategy | PowerOfTwo | 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. | ++------------------------+--------------------+---------------------------------------------------------------------------------+ +| NumUsableGuardedPages | 16 | Number of usable guarded pages in the allocation pool. | ++------------------------+--------------------+---------------------------------------------------------------------------------+ +| SampleRate | 5000 | The probability (1 / SampleRate) that a page is selected for GWP-ASan | +| | | sampling. Sample rates up to 1 / (2^64) are supported, values higher than | +| | | this are truncated. | ++------------------------+--------------------+---------------------------------------------------------------------------------+ +| InstallSignalHandlers | true | Install GWP-ASan signal handlers for SIGSEGV. This allows better error reports | +| | | by providing stack traces for allocation/deallocation when reporting a | +| | | use-after-free. | ++------------------------+--------------------+---------------------------------------------------------------------------------+