Commit a6258684 authored by Mitch Phillips's avatar Mitch Phillips
Browse files

[GWP-ASan] Crash Handler API.

Summary:
Forewarning: This patch looks big in #LOC changed. I promise it's not that bad, it just moves a lot of content from one file to another. I've gone ahead and left inline comments on Phabricator for sections where this has happened.

This patch:
 1. Introduces the crash handler API (crash_handler_api.h).
 2. Moves information required for out-of-process crash handling into an AllocatorState. This is a trivially-copied POD struct that designed to be recovered from a deceased process, and used by the crash handler to create a GWP-ASan report (along with the other trivially-copied Metadata struct).
 3. Implements the crash handler API using the AllocatorState and Metadata.
 4. Adds tests for the crash handler.
 5. Reimplements the (now optionally linked by the supporting allocator) in-process crash handler (i.e. the segv handler) using the new crash handler API.
 6. Minor updates Scudo & Scudo Standalone to fix compatibility.
 7. Changed capitalisation of errors (e.g. /s/Use after free/Use After Free).

Reviewers: cryptoad, eugenis, jfb

Reviewed By: eugenis

Subscribers: merge_guards_bot, pcc, jfb, dexonsmith, mgorny, cryptoad, #sanitizers, llvm-commits

Tags: #sanitizers, #llvm

Differential Revision: https://reviews.llvm.org/D73557
parent 7464e8d6
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -3,14 +3,20 @@ add_compiler_rt_component(gwp_asan)
include_directories(..)

set(GWP_ASAN_SOURCES
  common.cpp
  crash_handler.cpp
  platform_specific/common_posix.cpp
  platform_specific/guarded_pool_allocator_posix.cpp
  platform_specific/mutex_posix.cpp
  platform_specific/utilities_posix.cpp
  guarded_pool_allocator.cpp
  random.cpp
  stack_trace_compressor.cpp
)

set(GWP_ASAN_HEADERS
  common.h
  crash_handler.h
  definitions.h
  guarded_pool_allocator.h
  mutex.h
@@ -18,6 +24,7 @@ set(GWP_ASAN_HEADERS
  options.inc
  random.h
  stack_trace_compressor.h
  utilities.h
)

# Ensure that GWP-ASan meets the delegated requirements of some supporting
@@ -50,6 +57,9 @@ set(GWP_ASAN_BACKTRACE_HEADERS
  options.h
  options.inc
)
set(GWP_ASAN_SEGV_HANDLER_HEADERS
  optional/segv_handler.h
  options.h)

set(GWP_ASAN_OPTIONS_PARSER_CFLAGS
    ${GWP_ASAN_CFLAGS}
@@ -94,6 +104,11 @@ if (COMPILER_RT_HAS_GWP_ASAN)
      SOURCES optional/backtrace_linux_libc.cpp
      ADDITIONAL_HEADERS ${GWP_ASAN_BACKTRACE_HEADERS}
      CFLAGS ${GWP_ASAN_CFLAGS})
  add_compiler_rt_object_libraries(RTGwpAsanSegvHandler
      ARCHS ${GWP_ASAN_SUPPORTED_ARCH}
      SOURCES optional/segv_handler_posix.cpp
      ADDITIONAL_HEADERS ${GWP_ASAN_SEGV_HANDLER_HEADERS}
      CFLAGS ${GWP_ASAN_CFLAGS})
  add_compiler_rt_object_libraries(RTGwpAsanBacktraceSanitizerCommon
      ARCHS ${GWP_ASAN_SUPPORTED_ARCH}
      SOURCES optional/backtrace_sanitizer_common.cpp
+100 −0
Original line number Diff line number Diff line
//===-- common.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/common.h"
#include "gwp_asan/stack_trace_compressor.h"

#include <assert.h>

using AllocationMetadata = gwp_asan::AllocationMetadata;
using Error = gwp_asan::Error;

namespace gwp_asan {

const char *ErrorToString(const Error &E) {
  switch (E) {
  case Error::UNKNOWN:
    return "Unknown";
  case Error::USE_AFTER_FREE:
    return "Use After Free";
  case Error::DOUBLE_FREE:
    return "Double Free";
  case Error::INVALID_FREE:
    return "Invalid (Wild) Free";
  case Error::BUFFER_OVERFLOW:
    return "Buffer Overflow";
  case Error::BUFFER_UNDERFLOW:
    return "Buffer Underflow";
  }
  __builtin_trap();
}

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

  AllocationTrace.ThreadID = getThreadID();
  DeallocationTrace.TraceSize = 0;
  DeallocationTrace.ThreadID = kInvalidThreadID;
}

void AllocationMetadata::RecordDeallocation() {
  IsDeallocated = true;
  DeallocationTrace.ThreadID = getThreadID();
}

void AllocationMetadata::CallSiteInfo::RecordBacktrace(
    options::Backtrace_t Backtrace) {
  TraceSize = 0;
  if (!Backtrace)
    return;

  uintptr_t UncompressedBuffer[kMaxTraceLengthToCollect];
  size_t BacktraceLength =
      Backtrace(UncompressedBuffer, kMaxTraceLengthToCollect);
  TraceSize =
      compression::pack(UncompressedBuffer, BacktraceLength, CompressedTrace,
                        AllocationMetadata::kStackFrameStorageBytes);
}

size_t AllocatorState::maximumAllocationSize() const { return PageSize; }

uintptr_t AllocatorState::slotToAddr(size_t N) const {
  return GuardedPagePool + (PageSize * (1 + N)) + (maximumAllocationSize() * N);
}

bool AllocatorState::isGuardPage(uintptr_t Ptr) const {
  assert(pointerIsMine(reinterpret_cast<void *>(Ptr)));
  size_t PageOffsetFromPoolStart = (Ptr - GuardedPagePool) / PageSize;
  size_t PagesPerSlot = maximumAllocationSize() / PageSize;
  return (PageOffsetFromPoolStart % (PagesPerSlot + 1)) == 0;
}

static size_t addrToSlot(const AllocatorState *State, uintptr_t Ptr) {
  size_t ByteOffsetFromPoolStart = Ptr - State->GuardedPagePool;
  return ByteOffsetFromPoolStart /
         (State->maximumAllocationSize() + State->PageSize);
}

size_t AllocatorState::getNearestSlot(uintptr_t Ptr) const {
  if (Ptr <= GuardedPagePool + PageSize)
    return 0;
  if (Ptr > GuardedPagePoolEnd - PageSize)
    return MaxSimultaneousAllocations - 1;

  if (!isGuardPage(Ptr))
    return addrToSlot(this, Ptr);

  if (Ptr % PageSize <= PageSize / 2)
    return addrToSlot(this, Ptr - PageSize); // Round down.
  return addrToSlot(this, Ptr + PageSize);   // Round up.
}

} // namespace gwp_asan
+125 −0
Original line number Diff line number Diff line
//===-- common.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
//
//===----------------------------------------------------------------------===//

// This file contains code that is common between the crash handler and the
// GuardedPoolAllocator.

#ifndef GWP_ASAN_COMMON_H_
#define GWP_ASAN_COMMON_H_

#include "gwp_asan/definitions.h"
#include "gwp_asan/options.h"

#include <stddef.h>
#include <stdint.h>

namespace gwp_asan {
enum class Error {
  UNKNOWN,
  USE_AFTER_FREE,
  DOUBLE_FREE,
  INVALID_FREE,
  BUFFER_OVERFLOW,
  BUFFER_UNDERFLOW
};

const char *ErrorToString(const Error &E);

static constexpr uint64_t kInvalidThreadID = UINT64_MAX;
// Get the current thread ID, or kInvalidThreadID if failure. Note: This
// implementation is platform-specific.
uint64_t getThreadID();

// This struct contains all the metadata recorded about a single allocation made
// by GWP-ASan. If `AllocationMetadata.Addr` is zero, the metadata is non-valid.
struct AllocationMetadata {
  // The number of bytes used to store a compressed stack frame. On 64-bit
  // platforms, assuming a compression ratio of 50%, this should allow us to
  // store ~64 frames per trace.
  static constexpr size_t kStackFrameStorageBytes = 256;

  // Maximum number of stack frames to collect on allocation/deallocation. The
  // actual number of collected frames may be less than this as the stack
  // frames are compressed into a fixed memory range.
  static constexpr size_t kMaxTraceLengthToCollect = 128;

  // Records the given allocation metadata into this struct.
  void RecordAllocation(uintptr_t Addr, size_t Size);
  // Record that this allocation is now deallocated.
  void RecordDeallocation();

  struct CallSiteInfo {
    // Record the current backtrace to this callsite.
    void RecordBacktrace(options::Backtrace_t Backtrace);

    // The compressed backtrace to the allocation/deallocation.
    uint8_t CompressedTrace[kStackFrameStorageBytes];
    // The thread ID for this trace, or kInvalidThreadID if not available.
    uint64_t ThreadID = kInvalidThreadID;
    // The size of the compressed trace (in bytes). Zero indicates that no
    // trace was collected.
    size_t TraceSize = 0;
  };

  // The address of this allocation. If zero, the rest of this struct isn't
  // valid, as the allocation has never occurred.
  uintptr_t Addr = 0;
  // Represents the actual size of the allocation.
  size_t Size = 0;

  CallSiteInfo AllocationTrace;
  CallSiteInfo DeallocationTrace;

  // Whether this allocation has been deallocated yet.
  bool IsDeallocated = false;
};

// This holds the state that's shared between the GWP-ASan allocator and the
// crash handler. This, in conjunction with the Metadata array, forms the entire
// set of information required for understanding a GWP-ASan crash.
struct AllocatorState {
  // Returns whether the provided pointer is a current sampled allocation that
  // is owned by this pool.
  GWP_ASAN_ALWAYS_INLINE bool pointerIsMine(const void *Ptr) const {
    uintptr_t P = reinterpret_cast<uintptr_t>(Ptr);
    return P < GuardedPagePoolEnd && GuardedPagePool <= P;
  }

  // Returns the address of the N-th guarded slot.
  uintptr_t slotToAddr(size_t N) const;

  // Returns the largest allocation that is supported by this pool.
  size_t maximumAllocationSize() const;

  // Gets the nearest slot to the provided address.
  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;

  // The number of guarded slots that this pool holds.
  size_t MaxSimultaneousAllocations = 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 = 0;
  uintptr_t GuardedPagePoolEnd = 0;

  // Cached page size for this system in bytes.
  size_t PageSize = 0;

  // The type and address of an internally-detected failure. For INVALID_FREE
  // and DOUBLE_FREE, these errors are detected in GWP-ASan, which will set
  // these values and terminate the process.
  Error FailureType = Error::UNKNOWN;
  uintptr_t FailureAddress = 0;
};

} // namespace gwp_asan
#endif // GWP_ASAN_COMMON_H_
+147 −0
Original line number Diff line number Diff line
//===-- crash_handler_interface.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/common.h"
#include "gwp_asan/stack_trace_compressor.h"

#include <assert.h>

using AllocationMetadata = gwp_asan::AllocationMetadata;
using Error = gwp_asan::Error;

#ifdef __cplusplus
extern "C" {
#endif

bool __gwp_asan_error_is_mine(const gwp_asan::AllocatorState *State,
                              uintptr_t ErrorPtr) {
  assert(State && "State should not be nullptr.");
  if (State->FailureType != Error::UNKNOWN && State->FailureAddress != 0)
    return true;

  return ErrorPtr < State->GuardedPagePoolEnd &&
         State->GuardedPagePool <= ErrorPtr;
}

uintptr_t
__gwp_asan_get_internal_crash_address(const gwp_asan::AllocatorState *State) {
  return State->FailureAddress;
}

static const AllocationMetadata *
addrToMetadata(const gwp_asan::AllocatorState *State,
               const AllocationMetadata *Metadata, uintptr_t Ptr) {
  // Note - Similar implementation in guarded_pool_allocator.cpp.
  return &Metadata[State->getNearestSlot(Ptr)];
}

gwp_asan::Error
__gwp_asan_diagnose_error(const gwp_asan::AllocatorState *State,
                          const gwp_asan::AllocationMetadata *Metadata,
                          uintptr_t ErrorPtr) {
  if (!__gwp_asan_error_is_mine(State, ErrorPtr))
    return Error::UNKNOWN;

  if (State->FailureType != Error::UNKNOWN)
    return State->FailureType;

  // Let's try and figure out what the source of this error is.
  if (State->isGuardPage(ErrorPtr)) {
    size_t Slot = State->getNearestSlot(ErrorPtr);
    const AllocationMetadata *SlotMeta =
        addrToMetadata(State, Metadata, State->slotToAddr(Slot));

    // Ensure that this slot was allocated once upon a time.
    if (!SlotMeta->Addr)
      return Error::UNKNOWN;

    if (SlotMeta->Addr < ErrorPtr)
      return Error::BUFFER_OVERFLOW;
    return Error::BUFFER_UNDERFLOW;
  }

  // Access wasn't a guard page, check for use-after-free.
  const AllocationMetadata *SlotMeta =
      addrToMetadata(State, Metadata, ErrorPtr);
  if (SlotMeta->IsDeallocated) {
    return Error::USE_AFTER_FREE;
  }

  // If we have reached here, the error is still unknown.
  return Error::UNKNOWN;
}

const gwp_asan::AllocationMetadata *
__gwp_asan_get_metadata(const gwp_asan::AllocatorState *State,
                        const gwp_asan::AllocationMetadata *Metadata,
                        uintptr_t ErrorPtr) {
  if (!__gwp_asan_error_is_mine(State, ErrorPtr))
    return nullptr;

  if (ErrorPtr >= State->GuardedPagePoolEnd ||
      State->GuardedPagePool > ErrorPtr)
    return nullptr;

  const AllocationMetadata *Meta = addrToMetadata(State, Metadata, ErrorPtr);
  if (Meta->Addr == 0)
    return nullptr;

  return Meta;
}

uintptr_t __gwp_asan_get_allocation_address(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta) {
  return AllocationMeta->Addr;
}

size_t __gwp_asan_get_allocation_size(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta) {
  return AllocationMeta->Size;
}

uint64_t __gwp_asan_get_allocation_thread_id(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta) {
  return AllocationMeta->AllocationTrace.ThreadID;
}

size_t __gwp_asan_get_allocation_trace(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta, uintptr_t *Buffer,
    size_t BufferLen) {
  return gwp_asan::compression::unpack(
      AllocationMeta->AllocationTrace.CompressedTrace,
      AllocationMeta->AllocationTrace.TraceSize, Buffer, BufferLen);
}

bool __gwp_asan_is_deallocated(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta) {
  return AllocationMeta->IsDeallocated;
}

uint64_t __gwp_asan_get_deallocation_thread_id(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta) {
  return AllocationMeta->DeallocationTrace.ThreadID;
}

size_t __gwp_asan_get_deallocation_trace(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta, uintptr_t *Buffer,
    size_t BufferLen) {
  return gwp_asan::compression::unpack(
      AllocationMeta->DeallocationTrace.CompressedTrace,
      AllocationMeta->DeallocationTrace.TraceSize, Buffer, BufferLen);
}

#ifdef __cplusplus
} // extern "C"
#endif
+132 −0
Original line number Diff line number Diff line
//===-- crash_handler_interface.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
//
//===----------------------------------------------------------------------===//

// This file contains interface functions that can be called by an in-process or
// out-of-process crash handler after the process has terminated. Functions in
// this interface are never thread safe. For an in-process crash handler, the
// handler should call GuardedPoolAllocator::disable() to stop any other threads
// from retrieving new GWP-ASan allocations, which may corrupt the metadata.
#ifndef GWP_ASAN_INTERFACE_H_
#define GWP_ASAN_INTERFACE_H_

#include "gwp_asan/common.h"

#ifdef __cplusplus
extern "C" {
#endif

// When a process crashes, there are three possible outcomes:
//  1. The crash is unrelated to GWP-ASan - in which case this function returns
//     false.
//  2. The crash is internally detected within GWP-ASan itself (e.g. a
//     double-free bug is caught in GuardedPoolAllocator::deallocate(), and
//     GWP-ASan will terminate the process). In this case - this function
//     returns true.
//  3. The crash is caused by a memory error at `AccessPtr` that's caught by the
//     system, but GWP-ASan is responsible for the allocation. In this case -
//     the function also returns true.
// This function takes an optional `AccessPtr` parameter. If the pointer that
// was attempted to be accessed is available, you should provide it here. In the
// case of some internally-detected errors, the crash may manifest as an abort
// or trap may or may not have an associated pointer. In these cases, the
// pointer can be obtained by a call to __gwp_asan_get_internal_crash_address.
bool __gwp_asan_error_is_mine(const gwp_asan::AllocatorState *State,
                              uintptr_t ErrorPtr = 0u);

// Diagnose and return the type of error that occurred at `ErrorPtr`. If
// `ErrorPtr` is unrelated to GWP-ASan, or if the error type cannot be deduced,
// this function returns Error::UNKNOWN.
gwp_asan::Error
__gwp_asan_diagnose_error(const gwp_asan::AllocatorState *State,
                          const gwp_asan::AllocationMetadata *Metadata,
                          uintptr_t ErrorPtr);

// For internally-detected errors (double free, invalid free), this function
// returns the pointer that the error occurred at. If the error is unrelated to
// GWP-ASan, or if the error was caused by a non-internally detected failure,
// this function returns zero.
uintptr_t
__gwp_asan_get_internal_crash_address(const gwp_asan::AllocatorState *State);

// Returns a pointer to the metadata for the allocation that's responsible for
// the crash. This metadata should not be dereferenced directly due to API
// compatibility issues, but should be instead passed to functions below for
// information retrieval. Returns nullptr if there is no metadata available for
// this crash.
const gwp_asan::AllocationMetadata *
__gwp_asan_get_metadata(const gwp_asan::AllocatorState *State,
                        const gwp_asan::AllocationMetadata *Metadata,
                        uintptr_t ErrorPtr);

// +---------------------------------------------------------------------------+
// | Error Information Functions                                               |
// +---------------------------------------------------------------------------+
// Functions below return information about the type of error that was caught by
// GWP-ASan, or information about the allocation that caused the error. These
// functions generally take an `AllocationMeta` argument, which should be
// retrieved via. __gwp_asan_get_metadata.

// Returns the start of the allocation whose metadata is in `AllocationMeta`.
uintptr_t __gwp_asan_get_allocation_address(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta);

// Returns the size of the allocation whose metadata is in `AllocationMeta`
size_t __gwp_asan_get_allocation_size(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta);

// Returns the Thread ID that allocated the memory that caused the error at
// `ErrorPtr`. This function may not be called if __gwp_asan_has_metadata()
// returns false.
uint64_t __gwp_asan_get_allocation_thread_id(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta);

// Retrieve the allocation trace for the allocation whose metadata is in
// `AllocationMeta`, and place it into the provided `Buffer` that has at least
// `BufferLen` elements. This function returns the number of frames that would
// have been written into `Buffer` if the space was available (i.e. however many
// frames were stored by GWP-ASan). A return value greater than `BufferLen`
// indicates that the trace was truncated when storing to `Buffer`.
size_t __gwp_asan_get_allocation_trace(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta, uintptr_t *Buffer,
    size_t BufferLen);

// Returns whether the allocation whose metadata is in `AllocationMeta` has been
// deallocated. This function may not be called if __gwp_asan_has_metadata()
// returns false.
bool __gwp_asan_is_deallocated(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta);

// Returns the Thread ID that deallocated the memory whose metadata is in
// `AllocationMeta`. This function may not be called if
// __gwp_asan_is_deallocated() returns false.
uint64_t __gwp_asan_get_deallocation_thread_id(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta);

// Retrieve the deallocation trace for the allocation whose metadata is in
// `AllocationMeta`, and place it into the provided `Buffer` that has at least
// `BufferLen` elements. This function returns the number of frames that would
// have been written into `Buffer` if the space was available (i.e. however many
// frames were stored by GWP-ASan). A return value greater than `BufferLen`
// indicates that the trace was truncated when storing to `Buffer`. This
// function may not be called if __gwp_asan_is_deallocated() returns false.
size_t __gwp_asan_get_deallocation_trace(
    const gwp_asan::AllocatorState *State,
    const gwp_asan::AllocationMetadata *AllocationMeta, uintptr_t *Buffer,
    size_t BufferLen);

#ifdef __cplusplus
} // extern "C"
#endif

#endif // GWP_ASAN_INTERFACE_H_
Loading