diff --git a/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h --- a/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h +++ b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.h @@ -133,17 +133,55 @@ // allocations larger than this should go to the regular system allocator. size_t maximumAllocationSize() const; + // Get the current thread ID, or kInvalidThreadID if failure. Note: This + // implementation is platform-specific. + static uint64_t getThreadID(); + // Dumps an error report (including allocation and deallocation stack traces). // An optional error may be provided if the caller knows what the error is // ahead of time. This is primarily a helper function to locate the static // singleton pointer and call the internal version of this function. This // method is never thread safe, and should only be called when fatal errors - // occur. - static void reportError(uintptr_t AccessPtr, Error E = Error::UNKNOWN); - - // Get the current thread ID, or kInvalidThreadID if failure. Note: This - // implementation is platform-specific. - static uint64_t getThreadID(); + // occur. Calling this function permanently disables the allocator, and so + // should only be done when reporting a crash. + static void dumpReport(uintptr_t AccessPtr); + + // Return the type of memory error that occurred at `AccessPtr`. This function + // may return Error::UNKNOWN if the error is non-deducible. This method is + // never thread safe, and should only be called by a crash handler (or other + // stop-the-world process). + static Error diagnoseError(uintptr_t AccessPtr); + + // Describe the error `E` that occured at `AccessPtr`, and place the resulting + // string in `Buffer`. This function returns the number of bytes that would + // have been written to `Buffer` (excluding the null terminator) if space was + // available. A return value of `BufferSize` or more indicates that the output + // is truncated. This function returns zero on error (e.g. `AccessPtr` doesn't + // correspond to an allocation by the GPA singleton). This method is never + // thread safe, and should only be called by a crash handler (or other + // stop-the-world process). + static size_t getErrorString(uintptr_t AccessPtr, Error E, char *Buffer, + size_t BufferSize); + + // For the provided `AccessPtr` and error `E`, describe the allocation that + // most likely caused it, and place the resulting string in `Buffer`. This + // function returns the number of bytes that would have been written to + // `Buffer` (excluding the null terminator) if space was available. A return + // value of `BufferSize` or more indicates that the output is truncated. This + // function returns zero on error (e.g. `AccessPtr` doesn't correspond to an + // allocation by the GPA singleto). This method is never thread safe, and + // should only be called by a crash handler (or other stop-the-world process). + static size_t getAllocationString(uintptr_t AccessPtr, Error E, char *Buffer, + size_t BufferSize); + + // Copy the metadata for the provided allocation from the GPA singleton into + // `*Meta`. This may be used during crashes to get the stack trace and thread + // number of an allocation. This function returns false if the GPA singleton + // doesn't exist (i.e. GWP-ASan wansn't initialised or was disabled), or the + // provided pointer is outside of the GWP-ASan range. This method is never + // thread safe, and should only be called by a crash handler (or other + // stop-the-world process). + static bool getMetadata(uintptr_t Ptr, AllocationMetadata *Meta); private: static constexpr size_t kInvalidSlotID = SIZE_MAX; @@ -208,7 +246,20 @@ // responsible for the error is placed in *Meta. Error diagnoseUnknownError(uintptr_t AccessPtr, AllocationMetadata **Meta); - void reportErrorInternal(uintptr_t AccessPtr, Error E); + // Internal implementations of interface functions, please see there for more + // information. + void dumpReportInternal(uintptr_t AccessPtr); + size_t getErrorStringInternal(uintptr_t AccessPtr, Error E, char *Buffer, + size_t BufferSize); + size_t getAllocationStringInternal(uintptr_t AccessPtr, Error E, char *Buffer, + size_t BufferSize); + Error diagnoseErrorInternal(uintptr_t AccessPtr); + bool getMetadataInternal(uintptr_t Ptr, AllocationMetadata *Meta); + + // Used by INVALID_FREE and DOUBLE_FREE reporting to ensure that the `si_addr` + // field for a SEGV handler is set to the correct address, so that the fault + // can be deduced properly. + void trapOnAddress(uintptr_t Address, Error E); // Cached page size for this system in bytes. size_t PageSize = 0; @@ -252,6 +303,14 @@ // the sample rate. uint32_t AdjustedSampleRate = UINT32_MAX; + // The type of an internal failure. For INVALID_FREE and DOUBLE_FREE, these + // errors are detected internally in GWP-ASan. We terminate the process with + // SEGV when these errors occur. We still need to be able to deduce what + // caused the failure after the fact. If an internally-detected error occurs, + // the type of the error will be set to `FailureType`. This can be later + // retrieved by a call to `diagnoseError()`. + Error FailureType = Error::UNKNOWN; + // Pack the thread local variables into a struct to ensure that they're in // the same cache line for performance reasons. These are the most touched // variables in GWP-ASan. diff --git a/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp --- a/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp +++ b/compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -215,14 +216,29 @@ return reinterpret_cast(Ptr); } +void GuardedPoolAllocator::trapOnAddress(uintptr_t Address, Error E) { + FailureType = E; + + // If the page containing the address isn't currently marked an inacessible, + // do that, so that we can trap on the address provided. + void *Page = reinterpret_cast(getPageAddr(Address)); + markInaccessible(Page, getPlatformPageSize()); + + // And now trap on the pointer. + char *Ptr = reinterpret_cast(Address); + volatile char ThisReadShouldSegv = *Ptr; + (void)(ThisReadShouldSegv); // Ignore unused variable warning. +} + void GuardedPoolAllocator::deallocate(void *Ptr) { assert(pointerIsMine(Ptr) && "Pointer is not mine!"); uintptr_t UPtr = reinterpret_cast(Ptr); uintptr_t SlotStart = slotToAddr(addrToSlot(UPtr)); AllocationMetadata *Meta = addrToMetadata(UPtr); if (Meta->Addr != UPtr) { - reportError(UPtr, Error::INVALID_FREE); - exit(EXIT_FAILURE); + // If multiple errors occur at the same time, use the first one. + ScopedLock L(PoolMutex); + trapOnAddress(UPtr, Error::INVALID_FREE); } // Intentionally scope the mutex here, so that other threads can access the @@ -230,8 +246,7 @@ { ScopedLock L(PoolMutex); if (Meta->IsDeallocated) { - reportError(UPtr, Error::DOUBLE_FREE); - exit(EXIT_FAILURE); + trapOnAddress(UPtr, Error::DOUBLE_FREE); } // Ensure that the deallocation is recorded before marking the page as @@ -324,9 +339,9 @@ return Offset; } -void GuardedPoolAllocator::reportError(uintptr_t AccessPtr, Error E) { +void GuardedPoolAllocator::dumpReport(uintptr_t AccessPtr) { if (SingletonPtr) - SingletonPtr->reportErrorInternal(AccessPtr, E); + SingletonPtr->dumpReportInternal(AccessPtr); } size_t GuardedPoolAllocator::getNearestSlot(uintptr_t Ptr) const { @@ -343,8 +358,13 @@ return addrToSlot(Ptr + PageSize); // Round up. } -Error GuardedPoolAllocator::diagnoseUnknownError(uintptr_t AccessPtr, - AllocationMetadata **Meta) { +Error GuardedPoolAllocator::diagnoseErrorInternal(uintptr_t AccessPtr) { + if (!pointerIsMine(reinterpret_cast(AccessPtr))) + return Error::UNKNOWN; + + if (FailureType != Error::UNKNOWN) + return FailureType; + // Let's try and figure out what the source of this error is. if (isGuardPage(AccessPtr)) { size_t Slot = getNearestSlot(AccessPtr); @@ -353,7 +373,6 @@ // 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; @@ -363,82 +382,73 @@ // 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. - *Meta = nullptr; + // If we have reached here, the error is still unknown. return Error::UNKNOWN; } namespace { -// Prints the provided error and metadata information. -void printErrorType(Error E, uintptr_t AccessPtr, AllocationMetadata *Meta, - options::Printf_t Printf, uint64_t ThreadID) { - // Print using intermediate strings. Platforms like Android don't like when - // you print multiple times to the same line, as there may be a newline - // appended to a log file automatically per Printf() call. - const char *ErrorString; +// Stores the stringified error into the provided buffer. Returns the number of +// bytes that would have been written to the buffer (excluding null terminator) +// if it were large enough. A return value of `BufferSize` or greater indicates +// the result was truncated. +size_t errorString(Error E, char *Buffer, size_t BufferSize) { switch (E) { case Error::UNKNOWN: - ErrorString = "GWP-ASan couldn't automatically determine the source of " - "the memory error. It was likely caused by a wild memory " - "access into the GWP-ASan pool. The error occurred"; + return snprintf(Buffer, BufferSize, + "GWP-ASan couldn't automatically determine the source of " + "the memory error. It was likely caused by a wild memory " + "access into the GWP-ASan pool."); break; case Error::USE_AFTER_FREE: - ErrorString = "Use after free"; + return snprintf(Buffer, BufferSize, "Use after free"); break; case Error::DOUBLE_FREE: - ErrorString = "Double free"; + return snprintf(Buffer, BufferSize, "Double free"); break; case Error::INVALID_FREE: - ErrorString = "Invalid (wild) free"; + return snprintf(Buffer, BufferSize, "Invalid (wild) free"); break; case Error::BUFFER_OVERFLOW: - ErrorString = "Buffer overflow"; + return snprintf(Buffer, BufferSize, "Buffer overflow"); break; case Error::BUFFER_UNDERFLOW: - ErrorString = "Buffer underflow"; + return snprintf(Buffer, BufferSize, "Buffer underflow"); break; } +} - constexpr size_t kDescriptionBufferLen = 128; - char DescriptionBuffer[kDescriptionBufferLen]; - if (Meta) { - if (E == Error::USE_AFTER_FREE) { - snprintf(DescriptionBuffer, kDescriptionBufferLen, - "(%zu byte%s into a %zu-byte allocation at 0x%zx)", - AccessPtr - Meta->Addr, (AccessPtr - Meta->Addr == 1) ? "" : "s", - Meta->Size, Meta->Addr); - } else if (AccessPtr < Meta->Addr) { - snprintf(DescriptionBuffer, kDescriptionBufferLen, - "(%zu byte%s to the left of a %zu-byte allocation at 0x%zx)", - Meta->Addr - AccessPtr, (Meta->Addr - AccessPtr == 1) ? "" : "s", - Meta->Size, Meta->Addr); - } else if (AccessPtr > Meta->Addr) { - snprintf(DescriptionBuffer, kDescriptionBufferLen, - "(%zu byte%s to the right of a %zu-byte allocation at 0x%zx)", - AccessPtr - Meta->Addr, (AccessPtr - Meta->Addr == 1) ? "" : "s", - Meta->Size, Meta->Addr); - } else { - snprintf(DescriptionBuffer, kDescriptionBufferLen, - "(a %zu-byte allocation)", Meta->Size); - } +// Stores the string that describes the relation between `AccessPtr` and `Meta` +// for the given error `E` into the provided buffer. Returns the number of +// bytes that would have been written to the buffer (excluding null terminator) +// if it were large enough. A return value of `BufferSize` or greater indicates +// the result was truncated. +size_t allocationErrorRelationString(uintptr_t AccessPtr, Error E, + AllocationMetadata *Meta, char *Buffer, + size_t BufferSize) { + assert(Meta && + "Metadata to allocationErrorRelationString() should be non-null."); + if (E == Error::USE_AFTER_FREE) { + return snprintf( + Buffer, BufferSize, "%zu byte%s into a %zu-byte allocation at 0x%zx", + AccessPtr - Meta->Addr, (AccessPtr - Meta->Addr == 1) ? "" : "s", + Meta->Size, Meta->Addr); + } else if (AccessPtr < Meta->Addr) { + return snprintf(Buffer, BufferSize, + "%zu byte%s to the left of a %zu-byte allocation at 0x%zx", + Meta->Addr - AccessPtr, + (Meta->Addr - AccessPtr == 1) ? "" : "s", Meta->Size, + Meta->Addr); + } else if (AccessPtr > Meta->Addr) { + return snprintf(Buffer, BufferSize, + "%zu byte%s to the right of a %zu-byte allocation at 0x%zx", + AccessPtr - Meta->Addr, + (AccessPtr - Meta->Addr == 1) ? "" : "s", Meta->Size, + Meta->Addr); } - - // Possible number of digits of a 64-bit number: ceil(log10(2^64)) == 20. Add - // a null terminator, and round to the nearest 8-byte boundary. - constexpr size_t kThreadBufferLen = 24; - char ThreadBuffer[kThreadBufferLen]; - if (ThreadID == GuardedPoolAllocator::kInvalidThreadID) - snprintf(ThreadBuffer, kThreadBufferLen, ""); - else - snprintf(ThreadBuffer, kThreadBufferLen, "%" PRIu64, ThreadID); - - Printf("%s at 0x%zx %s by thread %s here:\n", ErrorString, AccessPtr, - DescriptionBuffer, ThreadBuffer); + return snprintf(Buffer, BufferSize, "a %zu-byte allocation", Meta->Size); } void printAllocDeallocTraces(uintptr_t AccessPtr, AllocationMetadata *Meta, @@ -484,7 +494,7 @@ }; } // anonymous namespace -void GuardedPoolAllocator::reportErrorInternal(uintptr_t AccessPtr, Error E) { +void GuardedPoolAllocator::dumpReportInternal(uintptr_t AccessPtr) { if (!pointerIsMine(reinterpret_cast(AccessPtr))) { return; } @@ -500,9 +510,9 @@ AllocationMetadata *Meta = nullptr; - if (E == Error::UNKNOWN) { - E = diagnoseUnknownError(AccessPtr, &Meta); - } else { + Error E = diagnoseErrorInternal(AccessPtr); + + if (E != Error::UNKNOWN) { size_t Slot = getNearestSlot(AccessPtr); Meta = addrToMetadata(slotToAddr(Slot)); // Ensure that this slot has been previously allocated. @@ -510,9 +520,42 @@ Meta = nullptr; } - // Print the error information. + // Print the error information. Grab 512B worth of stack space, which should + // be more than enough for the allocation and error details. + constexpr unsigned kBufferSize = 512; + char *Buffer = static_cast(alloca(kBufferSize)); + size_t BytesWritten = errorString(E, Buffer, kBufferSize); + if (BytesWritten > kBufferSize) + BytesWritten = kBufferSize; + + if (E == Error::UNKNOWN) { + BytesWritten += snprintf(Buffer + BytesWritten, kBufferSize - BytesWritten, + " The error occurred at 0x%zx (", AccessPtr); + } else { + BytesWritten += snprintf(Buffer + BytesWritten, kBufferSize - BytesWritten, + " at 0x%zx (", AccessPtr); + } + + if (BytesWritten > kBufferSize) + BytesWritten = kBufferSize; + + if (Meta) + BytesWritten += allocationErrorRelationString( + AccessPtr, E, Meta, Buffer + BytesWritten, kBufferSize - BytesWritten); + + if (BytesWritten > kBufferSize) + BytesWritten = kBufferSize; + uint64_t ThreadID = getThreadID(); - printErrorType(E, AccessPtr, Meta, Printf, ThreadID); + if (ThreadID == GuardedPoolAllocator::kInvalidThreadID) + snprintf(Buffer + BytesWritten, kBufferSize - BytesWritten, + ") by thread here:"); + else + snprintf(Buffer + BytesWritten, kBufferSize - BytesWritten, + ") by thread %" PRIu64 " here:", ThreadID); + + Printf("%s\n", Buffer); + if (Backtrace) { static constexpr unsigned kMaximumStackFramesForCrashTrace = 512; uintptr_t Trace[kMaximumStackFramesForCrashTrace]; @@ -527,6 +570,81 @@ printAllocDeallocTraces(AccessPtr, Meta, Printf, PrintBacktrace); } +size_t GuardedPoolAllocator::getErrorString(uintptr_t AccessPtr, Error E, + char *Buffer, size_t BufferSize) { + if (SingletonPtr) + return SingletonPtr->getErrorStringInternal(AccessPtr, E, Buffer, + BufferSize); + return 0; +} + +size_t GuardedPoolAllocator::getAllocationString(uintptr_t AccessPtr, Error E, + char *Buffer, + size_t BufferSize) { + if (SingletonPtr) + return SingletonPtr->getAllocationStringInternal(AccessPtr, E, Buffer, + BufferSize); + return 0; +} + +Error GuardedPoolAllocator::diagnoseError(uintptr_t AccessPtr) { + if (SingletonPtr) + return SingletonPtr->diagnoseErrorInternal(AccessPtr); + return Error::UNKNOWN; +} + +size_t GuardedPoolAllocator::getErrorStringInternal(uintptr_t AccessPtr, + Error E, char *Buffer, + size_t BufferSize) { + if (!pointerIsMine(reinterpret_cast(AccessPtr))) + return 0; + + return errorString(E, Buffer, BufferSize); +} + +size_t GuardedPoolAllocator::getAllocationStringInternal(uintptr_t AccessPtr, + Error E, char *Buffer, + size_t BufferSize) { + if (!pointerIsMine(reinterpret_cast(AccessPtr))) + return 0; + + AllocationMetadata *Meta = nullptr; + + if (E == Error::UNKNOWN) + return 0; + + size_t Slot = getNearestSlot(AccessPtr); + Meta = addrToMetadata(slotToAddr(Slot)); + // Ensure that this slot has been previously allocated. + if (!Meta->Addr) + return 0; + + return allocationErrorRelationString(AccessPtr, E, Meta, Buffer, BufferSize); +} + +bool GuardedPoolAllocator::getMetadataInternal(uintptr_t Ptr, + AllocationMetadata *Meta) { + if (!pointerIsMine(reinterpret_cast(Ptr))) + return false; + + size_t Slot = getNearestSlot(Ptr); + AllocationMetadata *Allocation = addrToMetadata(slotToAddr(Slot)); + + // Ensure that this slot has been previously allocated. + if (!Allocation->Addr) + return false; + + *Meta = *Allocation; + return true; +} + +bool GuardedPoolAllocator::getMetadata(uintptr_t Ptr, + AllocationMetadata *Meta) { + if (SingletonPtr) + return SingletonPtr->getMetadataInternal(Ptr, Meta); + return false; +} + TLS_INITIAL_EXEC GuardedPoolAllocator::ThreadLocalPackedVariables GuardedPoolAllocator::ThreadLocals; diff --git a/compiler-rt/lib/gwp_asan/platform_specific/guarded_pool_allocator_posix.cpp b/compiler-rt/lib/gwp_asan/platform_specific/guarded_pool_allocator_posix.cpp --- a/compiler-rt/lib/gwp_asan/platform_specific/guarded_pool_allocator_posix.cpp +++ b/compiler-rt/lib/gwp_asan/platform_specific/guarded_pool_allocator_posix.cpp @@ -60,7 +60,7 @@ struct sigaction PreviousHandler; static void sigSegvHandler(int sig, siginfo_t *info, void *ucontext) { - gwp_asan::GuardedPoolAllocator::reportError( + gwp_asan::GuardedPoolAllocator::dumpReport( reinterpret_cast(info->si_addr)); // Process any previous handlers. diff --git a/compiler-rt/lib/gwp_asan/tests/CMakeLists.txt b/compiler-rt/lib/gwp_asan/tests/CMakeLists.txt --- a/compiler-rt/lib/gwp_asan/tests/CMakeLists.txt +++ b/compiler-rt/lib/gwp_asan/tests/CMakeLists.txt @@ -4,8 +4,9 @@ ${COMPILER_RT_UNITTEST_CFLAGS} ${COMPILER_RT_GTEST_CFLAGS} -I${COMPILER_RT_SOURCE_DIR}/lib/ - -O2 - -g) + -g + -fno-optimize-sibling-calls # Used in crash_handler_api.cpp (GetAllocation()) +) file(GLOB GWP_ASAN_HEADERS ../*.h) set(GWP_ASAN_UNITTESTS @@ -14,6 +15,7 @@ backtrace.cpp basic.cpp compression.cpp + crash_handler_api.cpp driver.cpp mutex_test.cpp slot_reuse.cpp diff --git a/compiler-rt/lib/gwp_asan/tests/crash_handler_api.cpp b/compiler-rt/lib/gwp_asan/tests/crash_handler_api.cpp new file mode 100644 --- /dev/null +++ b/compiler-rt/lib/gwp_asan/tests/crash_handler_api.cpp @@ -0,0 +1,356 @@ +//===-- crash_handler_api.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 + +#include "gwp_asan/guarded_pool_allocator.h" +#include "gwp_asan/tests/harness.h" + +using Error = gwp_asan::GuardedPoolAllocator::Error; +using GuardedPoolAllocator = gwp_asan::GuardedPoolAllocator; +using AllocationMetadata = gwp_asan::GuardedPoolAllocator::AllocationMetadata; + +// We also disable sibling call optimisations in the build file to ensure that +// the call inside this function doesn't get optimised to a jump. +__attribute__((noinline)) void *GetAllocation(GuardedPoolAllocator *GPA, + size_t Size) { + return GPA->allocate(Size); +} +__attribute__((noinline)) void GetAllocation_End() {} + +// We also disable sibling call optimisations in the build file to ensure that +// the call inside this function doesn't get optimised to a jump. +__attribute__((noinline)) void DoDeallocate(GuardedPoolAllocator *GPA, + void *Ptr) { + GPA->deallocate(Ptr); +} +__attribute__((noinline)) void DoDeallocate_End() {} + +// It's impossible to determine the exact location of the call instruction +// within a C++ function - so we allow our frames to occur in a range. If the +// frame lies between [Start - End] (inclusive), we determine the frame to be +// found. +struct FrameRange { + FrameRange(uintptr_t Start, uintptr_t End) : Start(Start), End(End) {} + uintptr_t Start; + uintptr_t End; +}; + +// A null value (or zero), in general means "don't check this value". +struct ExpectedAPIResult { + Error ErrorType; + // There is no null value for `ErrorType`, so indicate whether we want to + // check this value or not. + bool CheckErrorType = false; + + const char *ErrorNeedle = nullptr; + const char *AllocationNeedle = nullptr; + + // Results from the AllocationMetadata. + bool HasValidMetadata = true; + uintptr_t AllocationAddress = 0; + size_t AllocationSize = 0; + + // Required frames below should be in order (but can be non-adjacent). If the + // required frames are [A, B], then the following matches will succeed: + // - [A, B] + // - [X, Y, A, B] + // - [A, B, X, Y] + // - [X, A, Y, B] + // - [A, A, B, A] + // and the following will fail: + // - [B, A] + // - [X, B, Y, A] + std::vector RequiredAllocationFrames; + std::vector RequiredDeallocationFrames; + + bool IsDeallocated; + bool CheckIsDeallocated = false; +}; + +// Create an ExpectedAPIResult that has the correct required +// allocation/deallocation frames for use with `GetAllocate()` and +// `DoDeallocate()`. Place the allocated pointer in `*Ptr`. +ExpectedAPIResult createTest(GuardedPoolAllocator *GPA, size_t Size, Error E, + void **Ptr, bool WillBeDeallocated) { + ExpectedAPIResult Result; + Result.ErrorType = E; + Result.CheckErrorType = true; + + *Ptr = GetAllocation(GPA, Size); + EXPECT_NE(*Ptr, nullptr); + + Result.AllocationAddress = reinterpret_cast(*Ptr); + Result.AllocationSize = Size; + + Result.RequiredAllocationFrames = { + FrameRange(reinterpret_cast(&GetAllocation), + reinterpret_cast(&GetAllocation_End))}; + + if (WillBeDeallocated) { + Result.RequiredDeallocationFrames = { + FrameRange(reinterpret_cast(&DoDeallocate), + reinterpret_cast(&DoDeallocate_End))}; + Result.IsDeallocated = true; + Result.CheckIsDeallocated = true; + } else { + Result.IsDeallocated = false; + Result.CheckIsDeallocated = true; + } + return Result; +} + +void CheckFrames(const std::vector &Frames, + uint8_t *CompressedTrace, size_t CompressedTraceSize) { + uintptr_t *Buffer = static_cast( + alloca(AllocationMetadata::kMaxTraceLengthToCollect * sizeof(uintptr_t))); + + size_t NumUncompressedFrames = gwp_asan::compression::unpack( + CompressedTrace, CompressedTraceSize, Buffer, + AllocationMetadata::kMaxTraceLengthToCollect); + EXPECT_GE(NumUncompressedFrames, Frames.size()); + + size_t FramesMatched = 0; + for (size_t i = 0; i < NumUncompressedFrames && FramesMatched < Frames.size(); + ++i) { + if (Buffer[i] >= Frames[FramesMatched].Start && + Buffer[i] <= Frames[FramesMatched].End) + ++FramesMatched; + } + + EXPECT_EQ(FramesMatched, Frames.size()); +} + +ExpectedAPIResult *ExpectedResult = nullptr; +sigjmp_buf SigJumpBuf; +bool CrashHandlerExecuted; + +void CheckExpectedResult(uintptr_t FaultAddress) { + assert(ExpectedResult && "ExpectedAPIResult should be non-null"); + CrashHandlerExecuted = true; + + Error Err = GuardedPoolAllocator::diagnoseError(FaultAddress); + + if (ExpectedResult->CheckErrorType) + EXPECT_EQ(ExpectedResult->ErrorType, Err); + + constexpr unsigned kBufferSize = 0x1000; + char *Buffer = static_cast(alloca(kBufferSize)); + if (ExpectedResult->ErrorNeedle) { + EXPECT_NE(0u, GuardedPoolAllocator::getErrorString(FaultAddress, Err, + Buffer, kBufferSize)); + EXPECT_NE(nullptr, strstr(Buffer, ExpectedResult->ErrorNeedle)) + << "Expected ErrorNeedle not found: \"" << ExpectedResult->ErrorNeedle + << "\" in string: \"" << Buffer << "\""; + } + + if (ExpectedResult->AllocationNeedle) { + EXPECT_NE(0u, GuardedPoolAllocator::getAllocationString( + FaultAddress, Err, Buffer, kBufferSize)); + EXPECT_NE(nullptr, strstr(Buffer, ExpectedResult->AllocationNeedle)) + << "Expected AllocationNeedle not found: \"" + << ExpectedResult->AllocationNeedle << "\" in string: \"" << Buffer + << "\""; + } + + AllocationMetadata Meta; + EXPECT_EQ(ExpectedResult->HasValidMetadata, + GuardedPoolAllocator::getMetadata(FaultAddress, &Meta)); + + if (!ExpectedResult->HasValidMetadata) + return; + + if (ExpectedResult->AllocationAddress) + EXPECT_EQ(Meta.Addr, ExpectedResult->AllocationAddress); + if (ExpectedResult->AllocationSize) + EXPECT_EQ(Meta.Size, ExpectedResult->AllocationSize); + + if (!ExpectedResult->RequiredAllocationFrames.empty()) + CheckFrames(ExpectedResult->RequiredAllocationFrames, + Meta.AllocationTrace.CompressedTrace, + Meta.AllocationTrace.TraceSize); + + if (!ExpectedResult->RequiredDeallocationFrames.empty()) + CheckFrames(ExpectedResult->RequiredDeallocationFrames, + Meta.DeallocationTrace.CompressedTrace, + Meta.DeallocationTrace.TraceSize); + + if (ExpectedResult->CheckIsDeallocated) + EXPECT_EQ(Meta.IsDeallocated, ExpectedResult->IsDeallocated); +} + +void segvHandler(int sig, siginfo_t *info, void *ucontext) { + CheckExpectedResult(reinterpret_cast(info->si_addr)); + siglongjmp(SigJumpBuf, 1); +} + +void CheckAPIOnCrash(ExpectedAPIResult *APIResult) { + CrashHandlerExecuted = false; + ExpectedResult = APIResult; + struct sigaction Action; + Action.sa_sigaction = segvHandler; + Action.sa_flags = SA_SIGINFO; + sigaction(SIGSEGV, &Action, nullptr); +} + +TEST_F(NoSignalHandlerGuardedPoolAllocator, UnknownErrorIfNotAllocated) { + constexpr unsigned kBufferSize = 0x10; + char Buffer[kBufferSize]; + + void *Ptr = malloc(1); + while (GPA.pointerIsMine(Ptr)) { + free(Ptr); + Ptr = malloc(1); + } + + uintptr_t IntPtr = reinterpret_cast(Ptr); + + EXPECT_EQ(Error::UNKNOWN, GuardedPoolAllocator::diagnoseError(0)); + EXPECT_EQ(Error::UNKNOWN, GuardedPoolAllocator::diagnoseError(IntPtr)); + + EXPECT_EQ(0u, GuardedPoolAllocator::getErrorString(0, Error::UNKNOWN, Buffer, + kBufferSize)); + EXPECT_EQ(0u, GuardedPoolAllocator::getErrorString(IntPtr, Error::UNKNOWN, + Buffer, kBufferSize)); + + EXPECT_EQ(0u, GuardedPoolAllocator::getAllocationString( + IntPtr, Error::UNKNOWN, Buffer, kBufferSize)); + EXPECT_EQ(0u, GuardedPoolAllocator::getAllocationString( + IntPtr, Error::UNKNOWN, Buffer, kBufferSize)); + + AllocationMetadata Meta; + EXPECT_FALSE(GuardedPoolAllocator::getMetadata(IntPtr, &Meta)); + EXPECT_FALSE(GuardedPoolAllocator::getMetadata(IntPtr, &Meta)); +} + +TEST_F(NoSignalHandlerGuardedPoolAllocator, DoubleFree) { + void *Ptr; + ExpectedAPIResult ExpectedResult = createTest( + &GPA, 1, Error::DOUBLE_FREE, &Ptr, /* WillBeDeallocated */ true); + ExpectedResult.ErrorNeedle = "Double free"; + ExpectedResult.AllocationNeedle = "a 1-byte allocation"; + CheckAPIOnCrash(&ExpectedResult); + + DoDeallocate(&GPA, Ptr); + if (!sigsetjmp(SigJumpBuf, 1)) + DoDeallocate(&GPA, Ptr); + EXPECT_TRUE(CrashHandlerExecuted); +} + +TEST_F(NoSignalHandlerGuardedPoolAllocator, InvalidFreeCheckAlign) { + void *Ptr; + ExpectedAPIResult ExpectedResult = createTest( + &GPA, 1, Error::INVALID_FREE, &Ptr, /* WillBeDeallocated */ false); + ExpectedResult.ErrorNeedle = "Invalid (wild) free"; + ExpectedResult.AllocationNeedle = "1 byte to the"; + CheckAPIOnCrash(&ExpectedResult); + if (!sigsetjmp(SigJumpBuf, 1)) + DoDeallocate(&GPA, static_cast(Ptr) + 1); + EXPECT_TRUE(CrashHandlerExecuted); +} + +TEST_F(NoSignalHandlerGuardedPoolAllocator, InvalidFreeCheckSize) { + void *Ptr; + ExpectedAPIResult ExpectedResult = createTest( + &GPA, 1, Error::INVALID_FREE, &Ptr, /* WillBeDeallocated */ false); + ExpectedResult.AllocationNeedle = "of a 1-byte allocation"; + CheckAPIOnCrash(&ExpectedResult); + if (!sigsetjmp(SigJumpBuf, 1)) + DoDeallocate(&GPA, static_cast(Ptr) + 1); + EXPECT_TRUE(CrashHandlerExecuted); +} + +TEST_F(NoSignalHandlerGuardedPoolAllocator, UseAfterFree) { + void *Ptr; + ExpectedAPIResult ExpectedResult = createTest( + &GPA, 1, Error::USE_AFTER_FREE, &Ptr, /* WillBeDeallocated */ true); + ExpectedResult.ErrorNeedle = "Use after free"; + CheckAPIOnCrash(&ExpectedResult); + + DoDeallocate(&GPA, Ptr); + if (!sigsetjmp(SigJumpBuf, 1)) + *(static_cast(Ptr)) = 0; + EXPECT_TRUE(CrashHandlerExecuted); +} + +constexpr unsigned kMaximumAlignmentPadding = 16; + +TEST_F(NoSignalHandlerGuardedPoolAllocator, BufferOverflow) { + void *Ptr; + constexpr unsigned kAllocationSize = 10; + ExpectedAPIResult ExpectedResult = + createTest(&GPA, kAllocationSize, Error::BUFFER_OVERFLOW, &Ptr, + /* WillBeDeallocated */ false); + + uintptr_t IntPtr = reinterpret_cast(Ptr); + // Keep trying until we have a right-aligned allocation. + while (IntPtr % GPA.maximumAllocationSize() < kMaximumAlignmentPadding) { + GPA.deallocate(Ptr); + Ptr = GetAllocation(&GPA, kAllocationSize); + EXPECT_NE(Ptr, nullptr); + IntPtr = reinterpret_cast(Ptr); + ExpectedResult.AllocationAddress = IntPtr; + } + + ExpectedResult.ErrorNeedle = "Buffer overflow"; + ExpectedResult.AllocationNeedle = + "bytes to the right of a 10-byte allocation"; + CheckAPIOnCrash(&ExpectedResult); + + if (!sigsetjmp(SigJumpBuf, 1)) + *(static_cast(Ptr) + kMaximumAlignmentPadding) = 0; + EXPECT_TRUE(CrashHandlerExecuted); +} + +TEST_F(NoSignalHandlerGuardedPoolAllocator, BufferUnderflow) { + void *Ptr; + constexpr unsigned kAllocationSize = 10; + ExpectedAPIResult ExpectedResult = + createTest(&GPA, kAllocationSize, Error::BUFFER_UNDERFLOW, &Ptr, + /* WillBeDeallocated */ false); + + uintptr_t IntPtr = reinterpret_cast(Ptr); + // Keep trying until we have a left-aligned allocation. + while (IntPtr % GPA.maximumAllocationSize() > kMaximumAlignmentPadding) { + GPA.deallocate(Ptr); + Ptr = GetAllocation(&GPA, kAllocationSize); + EXPECT_NE(Ptr, nullptr); + IntPtr = reinterpret_cast(Ptr); + ExpectedResult.AllocationAddress = IntPtr; + } + + ExpectedResult.ErrorNeedle = "Buffer underflow"; + ExpectedResult.AllocationNeedle = "bytes to the left of a 10-byte allocation"; + CheckAPIOnCrash(&ExpectedResult); + + if (!sigsetjmp(SigJumpBuf, 1)) + *(static_cast(Ptr) - kMaximumAlignmentPadding) = 0; + EXPECT_TRUE(CrashHandlerExecuted); +} + +// Simulate an internal error (or a hardware failure) where the touched +// allocation doesn't correspond to an allocation. +TEST_F(NoSignalHandlerGuardedPoolAllocator, UnknownInternalError) { + void *Ptr = GPA.allocate(1); + ExpectedAPIResult ExpectedResult; + ExpectedResult.ErrorType = Error::UNKNOWN; + ExpectedResult.CheckErrorType = true; + ExpectedResult.HasValidMetadata = false; + ExpectedResult.ErrorNeedle = + "GWP-ASan couldn't automatically determine the source of the memory " + "error. It was likely caused by a wild memory access into the GWP-ASan " + "pool."; + CheckAPIOnCrash(&ExpectedResult); + + if (!sigsetjmp(SigJumpBuf, 1)) + *(static_cast(Ptr) + GPA.maximumAllocationSize() * 2) = 0; + EXPECT_TRUE(CrashHandlerExecuted); +} diff --git a/compiler-rt/lib/gwp_asan/tests/harness.h b/compiler-rt/lib/gwp_asan/tests/harness.h --- a/compiler-rt/lib/gwp_asan/tests/harness.h +++ b/compiler-rt/lib/gwp_asan/tests/harness.h @@ -81,4 +81,20 @@ gwp_asan::GuardedPoolAllocator GPA; }; +class NoSignalHandlerGuardedPoolAllocator : public ::testing::Test { +public: + NoSignalHandlerGuardedPoolAllocator() { + gwp_asan::options::Options Opts; + Opts.setDefaults(); + Opts.InstallSignalHandlers = false; + + Opts.Printf = gwp_asan::test::getPrintfFunction(); + Opts.Backtrace = gwp_asan::options::getBacktraceFunction(); + GPA.init(Opts); + } + +protected: + gwp_asan::GuardedPoolAllocator GPA; +}; + #endif // GWP_ASAN_TESTS_HARNESS_H_ diff --git a/compiler-rt/test/gwp_asan/double_delete.cpp b/compiler-rt/test/gwp_asan/double_delete.cpp --- a/compiler-rt/test/gwp_asan/double_delete.cpp +++ b/compiler-rt/test/gwp_asan/double_delete.cpp @@ -1,6 +1,6 @@ // REQUIRES: gwp_asan // RUN: %clangxx_gwp_asan %s -o %t -// RUN: not %run %t 2>&1 | FileCheck %s +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s // CHECK: GWP-ASan detected a memory error // CHECK: Double free at 0x{{[a-f0-9]+}} (a 1-byte allocation) diff --git a/compiler-rt/test/gwp_asan/double_deletea.cpp b/compiler-rt/test/gwp_asan/double_deletea.cpp --- a/compiler-rt/test/gwp_asan/double_deletea.cpp +++ b/compiler-rt/test/gwp_asan/double_deletea.cpp @@ -1,6 +1,6 @@ // REQUIRES: gwp_asan // RUN: %clangxx_gwp_asan %s -o %t -// RUN: not %run %t 2>&1 | FileCheck %s +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s // CHECK: GWP-ASan detected a memory error // CHECK: Double free at 0x{{[a-f0-9]+}} (a 50-byte allocation) diff --git a/compiler-rt/test/gwp_asan/double_free.cpp b/compiler-rt/test/gwp_asan/double_free.cpp --- a/compiler-rt/test/gwp_asan/double_free.cpp +++ b/compiler-rt/test/gwp_asan/double_free.cpp @@ -1,6 +1,6 @@ // REQUIRES: gwp_asan // RUN: %clangxx_gwp_asan %s -o %t -// RUN: not %run %t 2>&1 | FileCheck %s +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s #include diff --git a/compiler-rt/test/gwp_asan/invalid_free_left.cpp b/compiler-rt/test/gwp_asan/invalid_free_left.cpp --- a/compiler-rt/test/gwp_asan/invalid_free_left.cpp +++ b/compiler-rt/test/gwp_asan/invalid_free_left.cpp @@ -1,6 +1,6 @@ // REQUIRES: gwp_asan // RUN: %clangxx_gwp_asan %s -o %t -// RUN: not %run %t 2>&1 | FileCheck %s +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s // CHECK: GWP-ASan detected a memory error // CHECK: Invalid (wild) free at 0x{{[a-f0-9]+}} (1 byte to the left of a diff --git a/compiler-rt/test/gwp_asan/invalid_free_right.cpp b/compiler-rt/test/gwp_asan/invalid_free_right.cpp --- a/compiler-rt/test/gwp_asan/invalid_free_right.cpp +++ b/compiler-rt/test/gwp_asan/invalid_free_right.cpp @@ -1,6 +1,6 @@ // REQUIRES: gwp_asan // RUN: %clangxx_gwp_asan %s -o %t -// RUN: not %run %t 2>&1 | FileCheck %s +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s // CHECK: GWP-ASan detected a memory error // CHECK: Invalid (wild) free at 0x{{[a-f0-9]+}} (1 byte to the right of a diff --git a/compiler-rt/test/gwp_asan/realloc.cpp b/compiler-rt/test/gwp_asan/realloc.cpp --- a/compiler-rt/test/gwp_asan/realloc.cpp +++ b/compiler-rt/test/gwp_asan/realloc.cpp @@ -1,6 +1,6 @@ // REQUIRES: gwp_asan // RUN: %clangxx_gwp_asan %s -o %t -DTEST_MALLOC -// RUN: not %run %t 2>&1 | FileCheck %s --check-prefix CHECK-MALLOC +// RUN: %expect_crash %run %t 2>&1 | FileCheck %s --check-prefix CHECK-MALLOC // Check both C++98 and C. // RUN: %clangxx_gwp_asan -std=c++98 %s -o %t -DTEST_FREE