[GWP-ASan] Integration with Scudo [5].

Summary:
See D60593 for further information.

This patch adds GWP-ASan support to the Scudo hardened allocator. It also
implements end-to-end integration tests using Scudo as the backing allocator.
The tests include crash handling for buffer over/underflow as well as
use-after-free detection.

Reviewers: vlad.tsyrklevich, cryptoad

Reviewed By: vlad.tsyrklevich, cryptoad

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

Tags: #sanitizers, #llvm

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

llvm-svn: 363584
This commit is contained in:
Mitch Phillips 2019-06-17 17:45:34 +00:00
parent 0cbf37af1e
commit 21184ec5c4
21 changed files with 335 additions and 5 deletions

View File

@ -30,6 +30,15 @@ set(SCUDO_MINIMAL_OBJECT_LIBS
RTSanitizerCommonNoTermination
RTSanitizerCommonLibc
RTInterception)
if (COMPILER_RT_HAS_GWP_ASAN)
# Currently, Scudo uses the GwpAsan flag parser. This backs onto the flag
# parsing mechanism of sanitizer_common. Once Scudo has its own flag parsing,
# and parses GwpAsan options, you can remove this dependency.
list(APPEND SCUDO_MINIMAL_OBJECT_LIBS RTGwpAsan RTGwpAsanOptionsParser)
list(APPEND SCUDO_CFLAGS -DGWP_ASAN_HOOKS)
endif()
set(SCUDO_OBJECT_LIBS ${SCUDO_MINIMAL_OBJECT_LIBS})
set(SCUDO_DYNAMIC_LIBS ${SCUDO_MINIMAL_DYNAMIC_LIBS})

View File

@ -25,6 +25,11 @@
#include "sanitizer_common/sanitizer_allocator_interface.h"
#include "sanitizer_common/sanitizer_quarantine.h"
#ifdef GWP_ASAN_HOOKS
# include "gwp_asan/guarded_pool_allocator.h"
# include "gwp_asan/optional/options_parser.h"
#endif // GWP_ASAN_HOOKS
#include <errno.h>
#include <string.h>
@ -213,6 +218,10 @@ QuarantineCacheT *getQuarantineCache(ScudoTSD *TSD) {
return reinterpret_cast<QuarantineCacheT *>(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 @@ struct Allocator {
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 @@ struct Allocator {
__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 @@ struct Allocator {
// 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 @@ struct Allocator {
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 @@ static BackendT &getBackend() {
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() {

View File

@ -6,7 +6,8 @@ set(GWP_ASAN_TESTSUITES)
set(GWP_ASAN_UNITTEST_DEPS)
set(GWP_ASAN_TEST_DEPS
${SANITIZER_COMMON_LIT_TEST_DEPS}
gwp_asan)
gwp_asan
scudo)
# Longstanding issues in the Android test runner means that compiler-rt unit
# tests don't work on Android due to libc++ link-time issues. Looks like the

View File

@ -0,0 +1,15 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Double free occurred when trying to free memory at:
#include <cstdlib>
int main() {
char *Ptr = new char;
delete Ptr;
delete Ptr;
return 0;
}

View File

@ -0,0 +1,15 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Double free occurred when trying to free memory at:
#include <cstdlib>
int main() {
char *Ptr = new char[50];
delete[] Ptr;
delete[] Ptr;
return 0;
}

View File

@ -0,0 +1,15 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Double free occurred when trying to free memory at:
#include <cstdlib>
int main() {
void *Ptr = malloc(10);
free(Ptr);
free(Ptr);
return 0;
}

View File

@ -1,4 +0,0 @@
// Exists to simply stop warnings about lit not discovering any tests here.
// RUN: %clang %s -o %s.o
int main() { return 0; }

View File

@ -0,0 +1,18 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Buffer overflow occurred when accessing memory at:
// CHECK: is located {{[0-9]+}} bytes to the right
#include <cstdlib>
#include "page_size.h"
int main() {
char *Ptr =
reinterpret_cast<char *>(malloc(pageSize()));
volatile char x = *(Ptr + pageSize());
return 0;
}

View File

@ -0,0 +1,18 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Buffer underflow occurred when accessing memory at:
// CHECK: is located 1 bytes to the left
#include <cstdlib>
#include "page_size.h"
int main() {
char *Ptr =
reinterpret_cast<char *>(malloc(pageSize()));
volatile char x = *(Ptr - 1);
return 0;
}

View File

@ -0,0 +1,16 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Invalid (wild) free occurred when trying to free memory at:
// CHECK: is located 1 bytes to the left of
#include <cstdlib>
int main() {
char *Ptr =
reinterpret_cast<char *>(malloc(1));
free(Ptr - 1);
return 0;
}

View File

@ -0,0 +1,16 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Invalid (wild) free occurred when trying to free memory at:
// CHECK: is located 1 bytes to the right
#include <cstdlib>
int main() {
char *Ptr =
reinterpret_cast<char *>(malloc(1));
free(Ptr + 1);
return 0;
}

View File

@ -20,11 +20,24 @@ if not config.android:
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']:

View File

@ -0,0 +1,13 @@
#ifndef PAGE_SIZE_
#define PAGE_SIZE_
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
# include <unistd.h>
unsigned pageSize() {
return sysconf(_SC_PAGESIZE);
}
#else
# error "GWP-ASan is not supported on this platform."
#endif
#endif // PAGE_SIZE_

View File

@ -0,0 +1,44 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t -DTEST_MALLOC
// RUN: not %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
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s --check-prefix CHECK-FREE
// RUN: cp %s %t.c && %clang_gwp_asan %t.c -o %t -DTEST_FREE
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s --check-prefix CHECK-FREE
// Ensure GWP-ASan stub implementation of realloc() in Scudo works to-spec. In
// particular, the behaviour regarding realloc of size zero is interesting, as
// it's defined as free().
#include <stdlib.h>
int main() {
#if defined(TEST_MALLOC)
// realloc(nullptr, size) is equivalent to malloc(size).
char *Ptr = reinterpret_cast<char *>(realloc(nullptr, 1));
*Ptr = 0;
// Trigger an INVALID_FREE to the right.
free(Ptr + 1);
// CHECK-MALLOC: GWP-ASan detected a memory error
// CHECK-MALLOC: Invalid (wild) free occurred when trying to free memory at:
// CHECK-MALLOC: is located 1 bytes to the right of a 1-byte allocation
#elif defined(TEST_FREE)
char *Ptr = (char *) malloc(1);
// realloc(ptr, 0) is equivalent to free(ptr) and must return nullptr. Note
// that this is only the specification in C++98 and C.
if (realloc(Ptr, 0) != NULL) {
}
// Trigger a USE_AFTER_FREE.
*Ptr = 0;
// CHECK-FREE: GWP-ASan detected a memory error
// CHECK-FREE: Use after free occurred when accessing memory at:
// CHECK-FREE: is a 1-byte allocation
#endif
return 0;
}

View File

@ -0,0 +1,28 @@
// REQUIRES: gwp_asan
// This test ensures that normal allocation/memory access/deallocation works
// as expected and we didn't accidentally break the supporting allocator.
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=1 %run %t
// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=2 %run %t
// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=11 %run %t
// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=12 %run %t
// RUN: %env_gwp_asan_options=MaxSimultaneousAllocations=13 %run %t
#include <cstdlib>
int main() {
void* Pointers[16];
for (unsigned i = 0; i < 16; ++i) {
char *Ptr = reinterpret_cast<char*>(malloc(1 << i));
Pointers[i] = Ptr;
*Ptr = 0;
Ptr[(1 << i) - 1] = 0;
}
for (unsigned i = 0; i < 16; ++i) {
free(Pointers[i]);
}
return 0;
}

View File

@ -0,0 +1,18 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Use after free occurred when accessing memory at:
#include <cstdlib>
int main() {
char *Ptr = new char;
*Ptr = 0x0;
delete Ptr;
volatile char x = *Ptr;
return 0;
}

View File

@ -0,0 +1,20 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Use after free occurred when accessing memory at:
#include <cstdlib>
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;
}

View File

@ -0,0 +1,20 @@
// REQUIRES: gwp_asan
// RUN: %clangxx_gwp_asan %s -o %t
// RUN: %expect_crash %run %t 2>&1 | FileCheck %s
// CHECK: GWP-ASan detected a memory error
// CHECK: Use after free occurred when accessing memory at:
#include <cstdlib>
int main() {
char *Ptr = reinterpret_cast<char *>(malloc(10));
for (unsigned i = 0; i < 10; ++i) {
*(Ptr + i) = 0x0;
}
free(Ptr);
volatile char x = *Ptr;
return 0;
}

View File

@ -239,6 +239,9 @@ if config.use_lld:
if config.can_symbolize:
config.available_features.add('can-symbolize')
if config.gwp_asan:
config.available_features.add('gwp_asan')
lit.util.usePlatformSdkOnDarwin(config, lit_config)
if config.host_os == 'Darwin':

View File

@ -41,6 +41,7 @@ set_default("android_ndk_version", @ANDROID_NDK_VERSION@)
set_default("android_serial", "@ANDROID_SERIAL_FOR_TESTING@")
set_default("android_files_to_push", [])
set_default("have_rpc_xdr_h", @HAVE_RPC_XDR_H@)
set_default("gwp_asan", @COMPILER_RT_HAS_GWP_ASAN_PYBOOL@)
config.available_features.add('target-is-%s' % config.target_arch)
if config.enable_per_target_runtime_dir:

View File

@ -49,6 +49,10 @@ if config.android:
# Android defaults to abort_on_error=1, which doesn't work for us.
default_scudo_opts = 'abort_on_error=0'
# Disable GWP-ASan for scudo internal tests.
if config.gwp_asan:
config.environment['GWP_ASAN_OPTIONS'] = 'Enabled=0'
if default_scudo_opts:
config.environment['SCUDO_OPTIONS'] = default_scudo_opts
default_scudo_opts += ':'