367 lines
14 KiB
C++
367 lines
14 KiB
C++
|
|
// Copyright 2022 The Chromium Authors
|
||
|
|
// Use of this source code is governed by a BSD-style license that can be
|
||
|
|
// found in the LICENSE file.
|
||
|
|
|
||
|
|
#include "base/memory/raw_ptr_asan_service.h"
|
||
|
|
|
||
|
|
#if BUILDFLAG(USE_ASAN_BACKUP_REF_PTR)
|
||
|
|
|
||
|
|
#include <sanitizer/allocator_interface.h>
|
||
|
|
#include <sanitizer/asan_interface.h>
|
||
|
|
#include <stdarg.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include "base/check_op.h"
|
||
|
|
#include "base/compiler_specific.h"
|
||
|
|
#include "base/debug/asan_service.h"
|
||
|
|
#include "base/immediate_crash.h"
|
||
|
|
#include "base/logging.h"
|
||
|
|
#include "base/memory/raw_ptr.h"
|
||
|
|
#include "base/memory/raw_ptr_asan_bound_arg_tracker.h"
|
||
|
|
#include "base/memory/raw_ptr_asan_hooks.h"
|
||
|
|
#include "base/process/process.h"
|
||
|
|
#include "base/strings/stringprintf.h"
|
||
|
|
#include "base/task/thread_pool/thread_group.h"
|
||
|
|
#include "third_party/abseil-cpp/absl/base/attributes.h"
|
||
|
|
|
||
|
|
namespace base {
|
||
|
|
|
||
|
|
RawPtrAsanService RawPtrAsanService::instance_;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
// https://github.com/llvm/llvm-project/blob/b84673b3f424882c4c1961fb2c49b6302b68f344/compiler-rt/lib/asan/asan_mapping.h#L154
|
||
|
|
constexpr size_t kShadowScale = 3;
|
||
|
|
// https://github.com/llvm/llvm-project/blob/b84673b3f424882c4c1961fb2c49b6302b68f344/compiler-rt/lib/asan/asan_allocator.cpp#L143
|
||
|
|
constexpr size_t kChunkHeaderSize = 16;
|
||
|
|
// https://github.com/llvm/llvm-project/blob/b84673b3f424882c4c1961fb2c49b6302b68f344/compiler-rt/lib/asan/asan_internal.h#L138
|
||
|
|
constexpr uint8_t kAsanHeapLeftRedzoneMagic = 0xfa;
|
||
|
|
// https://github.com/llvm/llvm-project/blob/b84673b3f424882c4c1961fb2c49b6302b68f344/compiler-rt/lib/asan/asan_internal.h#L145
|
||
|
|
constexpr uint8_t kAsanUserPoisonedMemoryMagic = 0xf7;
|
||
|
|
|
||
|
|
// Intentionally use thread-local-storage here. Making this sequence-local
|
||
|
|
// doesn't prevent sharing of PendingReport contents between unrelated tasks, so
|
||
|
|
// we keep this at a lower-level and avoid introducing additional assumptions
|
||
|
|
// about Chrome's sequence model.
|
||
|
|
ABSL_CONST_INIT thread_local RawPtrAsanService::PendingReport pending_report;
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
// Mark the first eight bytes of every allocation's header as "user poisoned".
|
||
|
|
// This allows us to filter out allocations made before BRP-ASan is activated.
|
||
|
|
// The change shouldn't reduce the regular ASan coverage.
|
||
|
|
|
||
|
|
// static
|
||
|
|
NO_SANITIZE("address")
|
||
|
|
void RawPtrAsanService::MallocHook(const volatile void* ptr, size_t size) {
|
||
|
|
uint8_t* header =
|
||
|
|
static_cast<uint8_t*>(const_cast<void*>(ptr)) - kChunkHeaderSize;
|
||
|
|
*RawPtrAsanService::GetInstance().GetShadow(header) =
|
||
|
|
kAsanUserPoisonedMemoryMagic;
|
||
|
|
}
|
||
|
|
|
||
|
|
NO_SANITIZE("address")
|
||
|
|
bool RawPtrAsanService::IsSupportedAllocation(void* allocation_start) const {
|
||
|
|
uint8_t* header = static_cast<uint8_t*>(allocation_start) - kChunkHeaderSize;
|
||
|
|
return *GetShadow(header) == kAsanUserPoisonedMemoryMagic;
|
||
|
|
}
|
||
|
|
|
||
|
|
NO_SANITIZE("address")
|
||
|
|
void RawPtrAsanService::Configure(
|
||
|
|
EnableDereferenceCheck enable_dereference_check,
|
||
|
|
EnableExtractionCheck enable_extraction_check,
|
||
|
|
EnableInstantiationCheck enable_instantiation_check) {
|
||
|
|
CHECK_EQ(mode_, Mode::kUninitialized);
|
||
|
|
|
||
|
|
Mode new_mode = enable_dereference_check || enable_extraction_check ||
|
||
|
|
enable_instantiation_check
|
||
|
|
? Mode::kEnabled
|
||
|
|
: Mode::kDisabled;
|
||
|
|
if (new_mode == Mode::kEnabled) {
|
||
|
|
// The constants we use aren't directly exposed by the API, so
|
||
|
|
// validate them at runtime as carefully as possible.
|
||
|
|
size_t shadow_scale;
|
||
|
|
__asan_get_shadow_mapping(&shadow_scale, &shadow_offset_);
|
||
|
|
CHECK_EQ(shadow_scale, kShadowScale);
|
||
|
|
|
||
|
|
uint8_t* dummy_alloc = new uint8_t;
|
||
|
|
CHECK_EQ(*GetShadow(dummy_alloc - kChunkHeaderSize),
|
||
|
|
kAsanHeapLeftRedzoneMagic);
|
||
|
|
|
||
|
|
__asan_poison_memory_region(dummy_alloc, 1);
|
||
|
|
CHECK_EQ(*GetShadow(dummy_alloc), kAsanUserPoisonedMemoryMagic);
|
||
|
|
delete dummy_alloc;
|
||
|
|
|
||
|
|
__sanitizer_install_malloc_and_free_hooks(MallocHook, FreeHook);
|
||
|
|
debug::AsanService::GetInstance()->AddErrorCallback(ErrorReportCallback);
|
||
|
|
internal::InstallRawPtrHooks(base::internal::GetRawPtrAsanHooks());
|
||
|
|
|
||
|
|
is_dereference_check_enabled_ = !!enable_dereference_check;
|
||
|
|
is_extraction_check_enabled_ = !!enable_extraction_check;
|
||
|
|
is_instantiation_check_enabled_ = !!enable_instantiation_check;
|
||
|
|
}
|
||
|
|
|
||
|
|
mode_ = new_mode;
|
||
|
|
}
|
||
|
|
|
||
|
|
uint8_t* RawPtrAsanService::GetShadow(void* ptr) const {
|
||
|
|
return reinterpret_cast<uint8_t*>(
|
||
|
|
(reinterpret_cast<uintptr_t>(ptr) >> kShadowScale) + shadow_offset_);
|
||
|
|
}
|
||
|
|
|
||
|
|
// static
|
||
|
|
void RawPtrAsanService::SetPendingReport(ReportType type,
|
||
|
|
const volatile void* ptr) {
|
||
|
|
// The actual ASan crash may occur at an offset from the pointer passed
|
||
|
|
// here, so track the whole region.
|
||
|
|
void* region_base;
|
||
|
|
size_t region_size;
|
||
|
|
__asan_locate_address(const_cast<void*>(ptr), nullptr, 0, ®ion_base,
|
||
|
|
®ion_size);
|
||
|
|
|
||
|
|
pending_report = {type, reinterpret_cast<uintptr_t>(region_base),
|
||
|
|
region_size};
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
enum class ProtectionStatus {
|
||
|
|
kNotProtected,
|
||
|
|
kManualAnalysisRequired,
|
||
|
|
kProtected,
|
||
|
|
};
|
||
|
|
|
||
|
|
const char* ProtectionStatusToString(ProtectionStatus status) {
|
||
|
|
switch (status) {
|
||
|
|
case ProtectionStatus::kNotProtected:
|
||
|
|
return "NOT PROTECTED";
|
||
|
|
case ProtectionStatus::kManualAnalysisRequired:
|
||
|
|
return "MANUAL ANALYSIS REQUIRED";
|
||
|
|
case ProtectionStatus::kProtected:
|
||
|
|
return "PROTECTED";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ASan doesn't have an API to get the current thread's identifier.
|
||
|
|
// We have to create a dummy allocation to determine it.
|
||
|
|
int GetCurrentThreadId() {
|
||
|
|
int* dummy = new int;
|
||
|
|
int id = -1;
|
||
|
|
__asan_get_alloc_stack(dummy, nullptr, 0, &id);
|
||
|
|
delete dummy;
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
// static
|
||
|
|
void RawPtrAsanService::ErrorReportCallback(const char* report, bool*) {
|
||
|
|
if (strcmp(__asan_get_report_description(), "heap-use-after-free") != 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
struct {
|
||
|
|
ProtectionStatus protection_status;
|
||
|
|
const char* crash_details;
|
||
|
|
const char* protection_details;
|
||
|
|
} crash_info;
|
||
|
|
|
||
|
|
uintptr_t ptr = reinterpret_cast<uintptr_t>(__asan_get_report_address());
|
||
|
|
uintptr_t bound_arg_ptr = RawPtrAsanBoundArgTracker::GetProtectedArgPtr(ptr);
|
||
|
|
if (pending_report.allocation_base <= ptr &&
|
||
|
|
ptr < pending_report.allocation_base + pending_report.allocation_size) {
|
||
|
|
bool is_supported_allocation =
|
||
|
|
RawPtrAsanService::GetInstance().IsSupportedAllocation(
|
||
|
|
reinterpret_cast<void*>(pending_report.allocation_base));
|
||
|
|
switch (pending_report.type) {
|
||
|
|
case ReportType::kDereference: {
|
||
|
|
if (is_supported_allocation) {
|
||
|
|
crash_info = {ProtectionStatus::kProtected,
|
||
|
|
"This crash occurred while a raw_ptr<T> object "
|
||
|
|
"containing a dangling pointer was being dereferenced.",
|
||
|
|
"MiraclePtr is expected to make this crash "
|
||
|
|
"non-exploitable once fully enabled."};
|
||
|
|
} else {
|
||
|
|
crash_info = {ProtectionStatus::kNotProtected,
|
||
|
|
"This crash occurred while accessing a region that was "
|
||
|
|
"allocated before MiraclePtr was activated.",
|
||
|
|
"This crash is still exploitable with MiraclePtr."};
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case ReportType::kExtraction: {
|
||
|
|
if (is_supported_allocation && bound_arg_ptr) {
|
||
|
|
crash_info = {
|
||
|
|
ProtectionStatus::kProtected,
|
||
|
|
"This crash occurred inside a callback where a raw_ptr<T> "
|
||
|
|
"pointing to the same region was bound to one of the arguments.",
|
||
|
|
"MiraclePtr is expected to make this crash non-exploitable once "
|
||
|
|
"fully enabled."};
|
||
|
|
} else if (is_supported_allocation) {
|
||
|
|
crash_info = {
|
||
|
|
ProtectionStatus::kManualAnalysisRequired,
|
||
|
|
"A pointer to the same region was extracted from a raw_ptr<T> "
|
||
|
|
"object prior to this crash.",
|
||
|
|
"To determine the protection status, enable extraction warnings "
|
||
|
|
"and check whether the raw_ptr<T> object can be destroyed or "
|
||
|
|
"overwritten between the extraction and use."};
|
||
|
|
} else {
|
||
|
|
crash_info = {ProtectionStatus::kNotProtected,
|
||
|
|
"This crash occurred while accessing a region that was "
|
||
|
|
"allocated before MiraclePtr was activated.",
|
||
|
|
"This crash is still exploitable with MiraclePtr."};
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case ReportType::kInstantiation: {
|
||
|
|
crash_info = {ProtectionStatus::kNotProtected,
|
||
|
|
"A pointer to an already freed region was assigned to a "
|
||
|
|
"raw_ptr<T> object, which may lead to memory corruption.",
|
||
|
|
"This crash is still exploitable with MiraclePtr."};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (bound_arg_ptr) {
|
||
|
|
// Note - this branch comes second to avoid hiding invalid instantiations,
|
||
|
|
// as we still consider it to be an error to instantiate a raw_ptr<T> from
|
||
|
|
// an invalid T* even if that T* is guaranteed to be quarantined.
|
||
|
|
bool is_supported_allocation =
|
||
|
|
RawPtrAsanService::GetInstance().IsSupportedAllocation(
|
||
|
|
reinterpret_cast<void*>(bound_arg_ptr));
|
||
|
|
if (is_supported_allocation) {
|
||
|
|
crash_info = {
|
||
|
|
ProtectionStatus::kProtected,
|
||
|
|
"This crash occurred inside a callback where a raw_ptr<T> pointing "
|
||
|
|
"to the same region was bound to one of the arguments.",
|
||
|
|
"MiraclePtr is expected to make this crash non-exploitable once "
|
||
|
|
"fully enabled."};
|
||
|
|
} else {
|
||
|
|
crash_info = {ProtectionStatus::kNotProtected,
|
||
|
|
"This crash occurred while accessing a region that was "
|
||
|
|
"allocated before MiraclePtr was activated.",
|
||
|
|
"This crash is still exploitable with MiraclePtr."};
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
crash_info = {
|
||
|
|
ProtectionStatus::kNotProtected,
|
||
|
|
"No raw_ptr<T> access to this region was detected prior to this crash.",
|
||
|
|
"This crash is still exploitable with MiraclePtr."};
|
||
|
|
}
|
||
|
|
|
||
|
|
// The race condition check below may override the protection status.
|
||
|
|
if (crash_info.protection_status != ProtectionStatus::kNotProtected) {
|
||
|
|
int free_thread_id = -1;
|
||
|
|
__asan_get_free_stack(reinterpret_cast<void*>(ptr), nullptr, 0,
|
||
|
|
&free_thread_id);
|
||
|
|
if (free_thread_id != GetCurrentThreadId()) {
|
||
|
|
crash_info.protection_status = ProtectionStatus::kManualAnalysisRequired;
|
||
|
|
crash_info.protection_details =
|
||
|
|
"The \"use\" and \"free\" threads don't match. This crash is likely "
|
||
|
|
"to have been caused by a race condition that is mislabeled as a "
|
||
|
|
"use-after-free. Make sure that the \"free\" is sequenced after the "
|
||
|
|
"\"use\" (e.g. both are on the same sequence, or the \"free\" is in "
|
||
|
|
"a task posted after the \"use\"). Otherwise, the crash is still "
|
||
|
|
"exploitable with MiraclePtr.";
|
||
|
|
} else if (internal::ThreadGroup::CurrentThreadHasGroup()) {
|
||
|
|
// We need to be especially careful with ThreadPool threads. Otherwise,
|
||
|
|
// we might miss false-positives where the "use" and "free" happen on
|
||
|
|
// different sequences but the same thread by chance.
|
||
|
|
crash_info.protection_status = ProtectionStatus::kManualAnalysisRequired;
|
||
|
|
crash_info.protection_details =
|
||
|
|
"This crash occurred in the thread pool. The sequence which invoked "
|
||
|
|
"the \"free\" is unknown, so the crash may have been caused by a "
|
||
|
|
"race condition that is mislabeled as a use-after-free. Make sure "
|
||
|
|
"that the \"free\" is sequenced after the \"use\" (e.g. both are on "
|
||
|
|
"the same sequence, or the \"free\" is in a task posted after the "
|
||
|
|
"\"use\"). Otherwise, the crash is still exploitable with "
|
||
|
|
"MiraclePtr.";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
debug::AsanService::GetInstance()->Log(
|
||
|
|
"\nMiraclePtr Status: %s\n%s\n%s\n"
|
||
|
|
"Refer to "
|
||
|
|
"https://chromium.googlesource.com/chromium/src/+/main/base/memory/"
|
||
|
|
"raw_ptr.md for details.",
|
||
|
|
ProtectionStatusToString(crash_info.protection_status),
|
||
|
|
crash_info.crash_details, crash_info.protection_details);
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
enum class MessageLevel {
|
||
|
|
kWarning,
|
||
|
|
kError,
|
||
|
|
};
|
||
|
|
|
||
|
|
const char* LevelToString(MessageLevel level) {
|
||
|
|
switch (level) {
|
||
|
|
case MessageLevel::kWarning:
|
||
|
|
return "WARNING";
|
||
|
|
case MessageLevel::kError:
|
||
|
|
return "ERROR";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Prints AddressSanitizer-like custom error messages.
|
||
|
|
void Log(MessageLevel level,
|
||
|
|
uintptr_t address,
|
||
|
|
const char* type,
|
||
|
|
const char* description) {
|
||
|
|
#if __has_builtin(__builtin_extract_return_addr) && \
|
||
|
|
__has_builtin(__builtin_return_address)
|
||
|
|
void* pc = __builtin_extract_return_addr(__builtin_return_address(0));
|
||
|
|
#else
|
||
|
|
void* pc = nullptr;
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#if __has_builtin(__builtin_frame_address)
|
||
|
|
void* bp = __builtin_frame_address(0);
|
||
|
|
#else
|
||
|
|
void* bp = nullptr;
|
||
|
|
#endif
|
||
|
|
|
||
|
|
void* local_stack;
|
||
|
|
void* sp = &local_stack;
|
||
|
|
|
||
|
|
debug::AsanService::GetInstance()->Log(
|
||
|
|
"=================================================================\n"
|
||
|
|
"==%d==%s: MiraclePtr: %s on address %p at pc %p bp %p sp %p",
|
||
|
|
Process::Current().Pid(), LevelToString(level), type, address, pc, bp,
|
||
|
|
sp);
|
||
|
|
__sanitizer_print_stack_trace();
|
||
|
|
__asan_describe_address(reinterpret_cast<void*>(address));
|
||
|
|
debug::AsanService::GetInstance()->Log(
|
||
|
|
"%s\n"
|
||
|
|
"=================================================================",
|
||
|
|
description);
|
||
|
|
}
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
void RawPtrAsanService::WarnOnDanglingExtraction(
|
||
|
|
const volatile void* ptr) const {
|
||
|
|
Log(MessageLevel::kWarning, reinterpret_cast<uintptr_t>(ptr),
|
||
|
|
"dangling-pointer-extraction",
|
||
|
|
"A regular ASan report will follow if the extracted pointer is "
|
||
|
|
"dereferenced later.\n"
|
||
|
|
"Otherwise, it is still likely a bug to rely on the address of an "
|
||
|
|
"already freed allocation.\n"
|
||
|
|
"Refer to "
|
||
|
|
"https://chromium.googlesource.com/chromium/src/+/main/base/memory/"
|
||
|
|
"raw_ptr.md for details.");
|
||
|
|
}
|
||
|
|
|
||
|
|
void RawPtrAsanService::CrashOnDanglingInstantiation(
|
||
|
|
const volatile void* ptr) const {
|
||
|
|
Log(MessageLevel::kError, reinterpret_cast<uintptr_t>(ptr),
|
||
|
|
"dangling-pointer-instantiation",
|
||
|
|
"This crash occurred due to an attempt to assign a dangling pointer to a "
|
||
|
|
"raw_ptr<T> variable, which might lead to use-after-free.\n"
|
||
|
|
"Note that this report might be a false positive if at the moment of the "
|
||
|
|
"crash another raw_ptr<T> is guaranteed to keep the allocation alive.\n"
|
||
|
|
"Refer to "
|
||
|
|
"https://chromium.googlesource.com/chromium/src/+/main/base/memory/"
|
||
|
|
"raw_ptr.md for details.");
|
||
|
|
base::ImmediateCrash();
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace base
|
||
|
|
|
||
|
|
#endif // BUILDFLAG(USE_ASAN_BACKUP_REF_PTR)
|