Skip to content

Commit

Permalink
[GWP-ASan] Add generic unwinders and structure backtrace output.
Browse files Browse the repository at this point in the history
Summary:
Adds two flavours of generic unwinder and all the supporting cruft. If the
supporting allocator is okay with bringing in sanitizer_common, they can use
the fast frame-pointer based unwinder from sanitizer_common. Otherwise, we also
provide the backtrace() libc-based unwinder as well. Of course, the allocator
can always specify its own unwinder and unwinder-symbolizer.

The slightly changed output format is exemplified in the first comment on this
patch. It now better incorporates backtrace information, and displays
allocation details on the second line.

Reviewers: eugenis, vlad.tsyrklevich

Reviewed By: eugenis, vlad.tsyrklevich

Subscribers: srhines, kubamracek, mgorny, cryptoad, #sanitizers, llvm-commits, morehouse

Tags: #sanitizers, #llvm

Differential Revision: https://reviews.llvm.org/D63841

llvm-svn: 364941
  • Loading branch information
hctim committed Jul 2, 2019
1 parent cb1a5a7 commit 7339ca2
Show file tree
Hide file tree
Showing 24 changed files with 434 additions and 137 deletions.
41 changes: 24 additions & 17 deletions compiler-rt/lib/gwp_asan/CMakeLists.txt
Expand Up @@ -23,6 +23,10 @@ set(GWP_ASAN_HEADERS
# parts of the C++ standard library.
set(GWP_ASAN_CFLAGS -fno-rtti -fno-exceptions -nostdinc++ -pthread)
append_list_if(COMPILER_RT_HAS_FPIC_FLAG -fPIC GWP_ASAN_CFLAGS)
append_list_if(COMPILER_RT_HAS_OMIT_FRAME_POINTER_FLAG -fno-omit-frame-pointer
GWP_ASAN_CFLAGS)
append_list_if(COMPILER_RT_HAS_OMIT_FRAME_POINTER_FLAG
-mno-omit-leaf-frame-pointer GWP_ASAN_CFLAGS)

# Remove -stdlib= which is unused when passing -nostdinc++.
string(REGEX REPLACE "-stdlib=[a-zA-Z+]*" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
Expand All @@ -40,6 +44,12 @@ set(GWP_ASAN_OPTIONS_PARSER_HEADERS
options.h
options.inc
)
set(GWP_ASAN_BACKTRACE_HEADERS
optional/backtrace.h
options.h
options.inc
)

set(GWP_ASAN_OPTIONS_PARSER_CFLAGS
${GWP_ASAN_CFLAGS}
${SANITIZER_COMMON_CFLAGS})
Expand Down Expand Up @@ -68,29 +78,26 @@ if (COMPILER_RT_HAS_GWP_ASAN)

# Note: If you choose to add this as an object library, ensure you also
# include the sanitizer_common flag parsing object lib (generally
# 'RTSanitizerCommonNoTermination').
# 'RTSanitizerCommonNoTermination'). Also, you'll need to either implement
# your own backtrace support (see optional/backtrace.h), or choose between one
# of the pre-implemented backtrace support options (see below).
add_compiler_rt_object_libraries(RTGwpAsanOptionsParser
ARCHS ${GWP_ASAN_SUPPORTED_ARCH}
SOURCES ${GWP_ASAN_OPTIONS_PARSER_SOURCES}
ADDITIONAL_HEADERS ${GWP_ASAN_OPTIONS_PARSER_HEADERS}
CFLAGS ${GWP_ASAN_OPTIONS_PARSER_CFLAGS})

# Ensure that the build for the options parser succeeds, as
# 'RTGwpAsanOptionsParser' may not be built if it's not needed. This library
# has only a very small amount of logic, all of the testable components are
# exercised in the sanitizer_common test suite.
foreach(arch ${GWP_ASAN_SUPPORTED_ARCH})
add_compiler_rt_runtime(
clang_rt.gwp_asan_options_parser
STATIC
ARCHS ${arch}
SOURCES ${GWP_ASAN_OPTIONS_PARSER_SOURCES}
ADDITIONAL_HEADERS ${GWP_ASAN_OPTIONS_PARSER_HEADERS}
CFLAGS ${GWP_ASAN_OPTIONS_PARSER_CFLAGS}
OBJECT_LIBS ${GWP_ASAN_OPTIONS_PARSER_OBJECT_LIBS}
PARENT_TARGET gwp_asan
)
endforeach()
# As above, build the pre-implemented optional backtrace support libraries.
add_compiler_rt_object_libraries(RTGwpAsanBacktraceLibc
ARCHS ${GWP_ASAN_SUPPORTED_ARCH}
SOURCES optional/backtrace_linux_libc.cpp
ADDITIONAL_HEADERS ${GWP_ASAN_BACKTRACE_HEADERS}
CFLAGS ${GWP_ASAN_CFLAGS})
add_compiler_rt_object_libraries(RTGwpAsanBacktraceSanitizerCommon
ARCHS ${GWP_ASAN_SUPPORTED_ARCH}
SOURCES optional/backtrace_sanitizer_common.cpp
ADDITIONAL_HEADERS ${GWP_ASAN_BACKTRACE_HEADERS}
CFLAGS ${GWP_ASAN_CFLAGS} ${SANITIZER_COMMON_CFLAGS})
endif()

if(COMPILER_RT_INCLUDE_TESTS)
Expand Down
210 changes: 128 additions & 82 deletions compiler-rt/lib/gwp_asan/guarded_pool_allocator.cpp
Expand Up @@ -11,6 +11,8 @@
#include "gwp_asan/options.h"

#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
Expand All @@ -26,32 +28,59 @@ namespace {
// referenced by users outside this translation unit, in order to avoid
// init-order-fiasco.
GuardedPoolAllocator *SingletonPtr = nullptr;

class ScopedBoolean {
public:
ScopedBoolean(bool &B) : Bool(B) { Bool = true; }
~ScopedBoolean() { Bool = false; }

private:
bool &Bool;
};

void defaultPrintStackTrace(uintptr_t *Trace, options::Printf_t Printf) {
if (Trace[0] == 0)
Printf(" <unknown (does your allocator support backtracing?)>\n");

for (size_t i = 0; Trace[i] != 0; ++i) {
Printf(" #%zu 0x%zx in <unknown>\n", i, Trace[i]);
}
Printf("\n");
}
} // anonymous namespace

// Gets the singleton implementation of this class. Thread-compatible until
// init() is called, thread-safe afterwards.
GuardedPoolAllocator *getSingleton() { return SingletonPtr; }

void GuardedPoolAllocator::AllocationMetadata::RecordAllocation(
uintptr_t AllocAddr, size_t AllocSize) {
uintptr_t AllocAddr, size_t AllocSize, options::Backtrace_t Backtrace) {
Addr = AllocAddr;
Size = AllocSize;
IsDeallocated = false;

// TODO(hctim): Implement stack trace collection.
// TODO(hctim): Ask the caller to provide the thread ID, so we don't waste
// other thread's time getting the thread ID under lock.
AllocationTrace.ThreadID = getThreadID();
DeallocationTrace.ThreadID = kInvalidThreadID;
AllocationTrace.Trace[0] = 0;
if (Backtrace)
Backtrace(AllocationTrace.Trace, kMaximumStackFrames);
else
AllocationTrace.Trace[0] = 0;
DeallocationTrace.Trace[0] = 0;
}

void GuardedPoolAllocator::AllocationMetadata::RecordDeallocation() {
void GuardedPoolAllocator::AllocationMetadata::RecordDeallocation(
options::Backtrace_t Backtrace) {
IsDeallocated = true;
// TODO(hctim): Implement stack trace collection. Ensure that the unwinder is
// not called if we have our recursive flag called, otherwise non-reentrant
// unwinders may deadlock.
// Ensure that the unwinder is not called if the recursive flag is set,
// otherwise non-reentrant unwinders may deadlock.
if (Backtrace && !ThreadLocals.RecursiveGuard) {
ScopedBoolean B(ThreadLocals.RecursiveGuard);
Backtrace(DeallocationTrace.Trace, kMaximumStackFrames);
} else {
DeallocationTrace.Trace[0] = 0;
}
DeallocationTrace.ThreadID = getThreadID();
}

Expand Down Expand Up @@ -93,6 +122,11 @@ void GuardedPoolAllocator::init(const options::Options &Opts) {

PerfectlyRightAlign = Opts.PerfectlyRightAlign;
Printf = Opts.Printf;
Backtrace = Opts.Backtrace;
if (Opts.PrintBacktrace)
PrintBacktrace = Opts.PrintBacktrace;
else
PrintBacktrace = defaultPrintStackTrace;

size_t PoolBytesRequired =
PageSize * (1 + MaxSimultaneousAllocations) +
Expand Down Expand Up @@ -126,17 +160,6 @@ void GuardedPoolAllocator::init(const options::Options &Opts) {
installSignalHandlers();
}

namespace {
class ScopedBoolean {
public:
ScopedBoolean(bool &B) : Bool(B) { Bool = true; }
~ScopedBoolean() { Bool = false; }

private:
bool &Bool;
};
} // anonymous namespace

void *GuardedPoolAllocator::allocate(size_t Size) {
// GuardedPagePoolEnd == 0 when GWP-ASan is disabled. If we are disabled, fall
// back to the supporting allocator.
Expand Down Expand Up @@ -169,7 +192,7 @@ void *GuardedPoolAllocator::allocate(size_t Size) {
// unmapped.
markReadWrite(reinterpret_cast<void *>(getPageAddr(Ptr)), Size);

Meta->RecordAllocation(Ptr, Size);
Meta->RecordAllocation(Ptr, Size, Backtrace);

return reinterpret_cast<void *>(Ptr);
}
Expand All @@ -196,7 +219,7 @@ void GuardedPoolAllocator::deallocate(void *Ptr) {
// Ensure that the deallocation is recorded before marking the page as
// inaccessible. Otherwise, a racy use-after-free will have inconsistent
// metadata.
Meta->RecordDeallocation();
Meta->RecordDeallocation(Backtrace);
}

markInaccessible(reinterpret_cast<void *>(SlotStart),
Expand Down Expand Up @@ -328,85 +351,109 @@ Error GuardedPoolAllocator::diagnoseUnknownError(uintptr_t AccessPtr,

// If we have reached here, the error is still unknown. There is no metadata
// available.
*Meta = nullptr;
return Error::UNKNOWN;
}

// Prints the provided error and metadata information. Returns true if there is
// additional context that can be provided, false otherwise (i.e. returns false
// if Error == {UNKNOWN, INVALID_FREE without metadata}).
bool printErrorType(Error E, uintptr_t AccessPtr, AllocationMetadata *Meta,
options::Printf_t Printf) {
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;
switch (E) {
case Error::UNKNOWN:
Printf("GWP-ASan couldn't automatically determine the source of the "
"memory error when accessing 0x%zx. It was likely caused by a wild "
"memory access into the GWP-ASan pool.\n",
AccessPtr);
return false;
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 occured";
break;
case Error::USE_AFTER_FREE:
Printf("Use after free occurred when accessing memory at: 0x%zx\n",
AccessPtr);
ErrorString = "Use after free";
break;
case Error::DOUBLE_FREE:
Printf("Double free occurred when trying to free memory at: 0x%zx\n",
AccessPtr);
ErrorString = "Double free";
break;
case Error::INVALID_FREE:
Printf(
"Invalid (wild) free occurred when trying to free memory at: 0x%zx\n",
AccessPtr);
// It's possible for an invalid free to fall onto a slot that has never been
// allocated. If this is the case, there is no valid metadata.
if (Meta == nullptr)
return false;
ErrorString = "Invalid (wild) free";
break;
case Error::BUFFER_OVERFLOW:
Printf("Buffer overflow occurred when accessing memory at: 0x%zx\n",
AccessPtr);
ErrorString = "Buffer overflow";
break;
case Error::BUFFER_UNDERFLOW:
Printf("Buffer underflow occurred when accessing memory at: 0x%zx\n",
AccessPtr);
ErrorString = "Buffer underflow";
break;
}

Printf("0x%zx is ", AccessPtr);
if (AccessPtr < Meta->Addr)
Printf("located %zu bytes to the left of a %zu-byte allocation located at "
"0x%zx\n",
Meta->Addr - AccessPtr, Meta->Size, Meta->Addr);
else if (AccessPtr > Meta->Addr)
Printf("located %zu bytes to the right of a %zu-byte allocation located at "
"0x%zx\n",
AccessPtr - Meta->Addr, Meta->Size, Meta->Addr);
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);
}
}

// 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, "<unknown>");
else
Printf("a %zu-byte allocation\n", Meta->Size);
return true;
snprintf(ThreadBuffer, kThreadBufferLen, "%" PRIu64, ThreadID);

Printf("%s at 0x%zx %s by thread %s here:\n", ErrorString, AccessPtr,
DescriptionBuffer, ThreadBuffer);
}

void printThreadInformation(Error E, 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);
void printAllocDeallocTraces(uintptr_t AccessPtr, AllocationMetadata *Meta,
options::Printf_t Printf,
options::PrintBacktrace_t PrintBacktrace) {
assert(Meta != nullptr && "Metadata is non-null for printAllocDeallocTraces");

if (E == Error::USE_AFTER_FREE || E == Error::DOUBLE_FREE) {
Printf("0x%zx was freed by thread ", AccessPtr);
if (Meta->AllocationTrace.ThreadID == UINT64_MAX)
Printf("UNKNOWN.\n");
if (Meta->IsDeallocated) {
if (Meta->DeallocationTrace.ThreadID ==
GuardedPoolAllocator::kInvalidThreadID)
Printf("0x%zx was deallocated by thread <unknown> here:\n", AccessPtr);
else
Printf("%zu.\n", Meta->AllocationTrace.ThreadID);
Printf("0x%zx was deallocated by thread %zu here:\n", AccessPtr,
Meta->DeallocationTrace.ThreadID);

PrintBacktrace(Meta->DeallocationTrace.Trace, Printf);
}

if (Meta->AllocationTrace.ThreadID == GuardedPoolAllocator::kInvalidThreadID)
Printf("0x%zx was allocated by thread <unknown> here:\n", Meta->Addr);
else
Printf("0x%zx was allocated by thread %zu here:\n", Meta->Addr,
Meta->AllocationTrace.ThreadID);

PrintBacktrace(Meta->AllocationTrace.Trace, Printf);
}

struct ScopedEndOfReportDecorator {
ScopedEndOfReportDecorator(options::Printf_t Printf) : Printf(Printf) {}
~ScopedEndOfReportDecorator() { Printf("*** End GWP-ASan report ***\n"); }
options::Printf_t Printf;
};
} // anonymous namespace

void GuardedPoolAllocator::reportErrorInternal(uintptr_t AccessPtr, Error E) {
if (!pointerIsMine(reinterpret_cast<void *>(AccessPtr))) {
Expand Down Expand Up @@ -434,22 +481,21 @@ void GuardedPoolAllocator::reportErrorInternal(uintptr_t AccessPtr, Error E) {
Meta = nullptr;
}

// Print the error information, and if there is no valid metadata, stop here.
if (!printErrorType(E, AccessPtr, Meta, Printf)) {
return;
}
// Print the error information.
uint64_t ThreadID = getThreadID();
printErrorType(E, AccessPtr, Meta, Printf, ThreadID);
if (Backtrace) {
static constexpr unsigned kMaximumStackFramesForCrashTrace = 128;
uintptr_t Trace[kMaximumStackFramesForCrashTrace];
Backtrace(Trace, kMaximumStackFramesForCrashTrace);

// Ensure that we have a valid metadata pointer from this point forward.
if (Meta == nullptr) {
Printf("GWP-ASan internal unreachable error. Metadata is not null.\n");
return;
PrintBacktrace(Trace, Printf);
} else {
Printf(" <unknown (does your allocator support backtracing?)>\n\n");
}

printThreadInformation(E, 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.
if (Meta)
printAllocDeallocTraces(AccessPtr, Meta, Printf, PrintBacktrace);
}

TLS_INITIAL_EXEC
Expand Down

0 comments on commit 7339ca2

Please sign in to comment.