[MTE] Add a HWASan-style tag dump to tombstones.

We already dump the tags in the regigster dump section by appending the
tag to the memory address. You only get 2 granules before each register
and 13 after.

The HWASan-style tag dump is extremely useful for debugging, as it gives
a pretty comprehensive overview of the memory subsystem. It also
provides enough context bytes (256) to give you a reasonable intuition
about a particular bug.

The tag dump shows up only if PTRACE_PEEKTAGS returns at least one value
in the 256 requested. If the start of end of the region is untagged,
it's omitted. The tag dump looks like this:

Change-Id: Icc33fb97542d9b1fa3ae9e58aba34d524c6ba7b5

---
Memory tags around the fault address (0x60000704414d340), one tag per 16 bytes:
      0x704414d000: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d100: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d200: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
    =>0x704414d300: 0  0  0  0 [2] 2  0  0  0  0  0  0  0  0  0  0
      0x704414d400: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d500: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d600: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d700: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d800: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414d900: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
      0x704414da00: 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
---

Bug: 183992164
Test: atest debuggerd_test on MTE+QEMU and sunfish.

Change-Id: I8d5842e4803ca30b407e866c99eef56f2cb36600
This commit is contained in:
Mitch Phillips 2021-04-19 09:59:17 -07:00
parent a08d328864
commit 5ddcea2924
5 changed files with 273 additions and 19 deletions

View file

@ -58,6 +58,7 @@
#include <scoped_minijail.h>
#include "debuggerd/handler.h"
#include "libdebuggerd/utility.h"
#include "protocol.h"
#include "tombstoned/tombstoned.h"
#include "util.h"
@ -526,6 +527,8 @@ TEST_P(SizeParamCrasherTest, mte_uaf) {
std::vector<std::string> log_sources(2);
ConsumeFd(std::move(output_fd), &log_sources[0]);
logcat_collector.Collect(&log_sources[1]);
// Tag dump only available in the tombstone, not logcat.
ASSERT_MATCH(log_sources[0], "Memory tags around the fault address");
for (const auto& result : log_sources) {
ASSERT_MATCH(result, R"(signal 11 \(SIGSEGV\))");
@ -597,6 +600,12 @@ TEST_P(SizeParamCrasherTest, mte_overflow) {
ConsumeFd(std::move(output_fd), &log_sources[0]);
logcat_collector.Collect(&log_sources[1]);
// Tag dump only in tombstone, not logcat, and tagging is not used for
// overflow protection in the scudo secondary (guard pages are used instead).
if (GetParam() < 0x10000) {
ASSERT_MATCH(log_sources[0], "Memory tags around the fault address");
}
for (const auto& result : log_sources) {
ASSERT_MATCH(result, R"(signal 11 \(SIGSEGV\))");
ASSERT_MATCH(result, R"(Cause: \[MTE\]: Buffer Overflow, 0 bytes right of a )" +
@ -637,6 +646,7 @@ TEST_P(SizeParamCrasherTest, mte_underflow) {
std::to_string(GetParam()) + R"(-byte allocation)");
ASSERT_MATCH(result, R"((^|\s)allocated by thread .*
#00 pc)");
ASSERT_MATCH(result, "Memory tags around the fault address");
#else
GTEST_SKIP() << "Requires aarch64";
#endif
@ -686,6 +696,9 @@ TEST_F(CrasherTest, mte_multiple_causes) {
ConsumeFd(std::move(output_fd), &log_sources[0]);
logcat_collector.Collect(&log_sources[1]);
// Tag dump only in the tombstone, not logcat.
ASSERT_MATCH(log_sources[0], "Memory tags around the fault address");
for (const auto& result : log_sources) {
ASSERT_MATCH(result, R"(signal 11 \(SIGSEGV\))");
ASSERT_THAT(result, HasSubstr("Note: multiple potential causes for this crash were detected, "
@ -706,21 +719,26 @@ TEST_F(CrasherTest, mte_multiple_causes) {
#if defined(__aarch64__)
static uintptr_t CreateTagMapping() {
uintptr_t mapping =
reinterpret_cast<uintptr_t>(mmap(nullptr, getpagesize(), PROT_READ | PROT_WRITE | PROT_MTE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
if (reinterpret_cast<void*>(mapping) == MAP_FAILED) {
// Some of the MTE tag dump tests assert that there is an inaccessible page to the left and right
// of the PROT_MTE page, so map three pages and set the two guard pages to PROT_NONE.
size_t page_size = getpagesize();
void* mapping = mmap(nullptr, page_size * 3, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
uintptr_t mapping_uptr = reinterpret_cast<uintptr_t>(mapping);
if (mapping == MAP_FAILED) {
return 0;
}
__asm__ __volatile__(".arch_extension mte; stg %0, [%0]"
:
: "r"(mapping + (1ULL << 56))
: "memory");
return mapping;
mprotect(reinterpret_cast<void*>(mapping_uptr + page_size), page_size,
PROT_READ | PROT_WRITE | PROT_MTE);
// Stripe the mapping, where even granules get tag '1', and odd granules get tag '0'.
for (uintptr_t offset = 0; offset < page_size; offset += 2 * kTagGranuleSize) {
uintptr_t tagged_addr = mapping_uptr + page_size + offset + (1ULL << 56);
__asm__ __volatile__(".arch_extension mte; stg %0, [%0]" : : "r"(tagged_addr) : "memory");
}
return mapping_uptr + page_size;
}
#endif
TEST_F(CrasherTest, mte_tag_dump) {
TEST_F(CrasherTest, mte_register_tag_dump) {
#if defined(__aarch64__)
if (!mte_supported()) {
GTEST_SKIP() << "Requires MTE";
@ -753,6 +771,107 @@ TEST_F(CrasherTest, mte_tag_dump) {
#endif
}
TEST_F(CrasherTest, mte_fault_tag_dump_front_truncated) {
#if defined(__aarch64__)
if (!mte_supported()) {
GTEST_SKIP() << "Requires MTE";
}
int intercept_result;
unique_fd output_fd;
StartProcess([&]() {
SetTagCheckingLevelSync();
volatile char* p = reinterpret_cast<char*>(CreateTagMapping());
p[0] = 0; // Untagged pointer, tagged memory.
});
StartIntercept(&output_fd);
FinishCrasher();
AssertDeath(SIGSEGV);
FinishIntercept(&intercept_result);
ASSERT_EQ(1, intercept_result) << "tombstoned reported failure";
std::string result;
ConsumeFd(std::move(output_fd), &result);
ASSERT_MATCH(result, R"(Memory tags around the fault address.*
\s*=>0x[0-9a-f]+000:\[1\] 0 1 0)");
#else
GTEST_SKIP() << "Requires aarch64";
#endif
}
TEST_F(CrasherTest, mte_fault_tag_dump) {
#if defined(__aarch64__)
if (!mte_supported()) {
GTEST_SKIP() << "Requires MTE";
}
int intercept_result;
unique_fd output_fd;
StartProcess([&]() {
SetTagCheckingLevelSync();
volatile char* p = reinterpret_cast<char*>(CreateTagMapping());
p[320] = 0; // Untagged pointer, tagged memory.
});
StartIntercept(&output_fd);
FinishCrasher();
AssertDeath(SIGSEGV);
FinishIntercept(&intercept_result);
ASSERT_EQ(1, intercept_result) << "tombstoned reported failure";
std::string result;
ConsumeFd(std::move(output_fd), &result);
ASSERT_MATCH(result, R"(Memory tags around the fault address.*
\s*0x[0-9a-f]+: 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
\s*=>0x[0-9a-f]+: 1 0 1 0 \[1\] 0 1 0 1 0 1 0 1 0 1 0
\s*0x[0-9a-f]+: 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
)");
#else
GTEST_SKIP() << "Requires aarch64";
#endif
}
TEST_F(CrasherTest, mte_fault_tag_dump_rear_truncated) {
#if defined(__aarch64__)
if (!mte_supported()) {
GTEST_SKIP() << "Requires MTE";
}
int intercept_result;
unique_fd output_fd;
StartProcess([&]() {
SetTagCheckingLevelSync();
size_t page_size = getpagesize();
volatile char* p = reinterpret_cast<char*>(CreateTagMapping());
p[page_size - kTagGranuleSize * 2] = 0; // Untagged pointer, tagged memory.
});
StartIntercept(&output_fd);
FinishCrasher();
AssertDeath(SIGSEGV);
FinishIntercept(&intercept_result);
ASSERT_EQ(1, intercept_result) << "tombstoned reported failure";
std::string result;
ConsumeFd(std::move(output_fd), &result);
ASSERT_MATCH(result, R"(Memory tags around the fault address)");
ASSERT_MATCH(result,
R"(\s*0x[0-9a-f]+: 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
\s*=>0x[0-9a-f]+: 1 0 1 0 1 0 1 0 1 0 1 0 1 0 \[1\] 0
)"); // Ensure truncation happened and there's a newline after the tag fault.
#else
GTEST_SKIP() << "Requires aarch64";
#endif
}
TEST_F(CrasherTest, LD_PRELOAD) {
int intercept_result;
unique_fd output_fd;

View file

@ -96,4 +96,8 @@ const char* get_sigcode(const siginfo_t*);
// Number of bytes per MTE granule.
constexpr size_t kTagGranuleSize = 16;
// Number of rows and columns to display in an MTE tag dump.
constexpr size_t kNumTagColumns = 16;
constexpr size_t kNumTagRows = 16;
#endif // _DEBUGGERD_UTILITY_H

View file

@ -362,8 +362,10 @@ static void dump_thread(Tombstone* tombstone, unwindstack::Unwinder* unwinder,
dump.set_mapping_name(map_info->name());
}
char buf[256];
uint8_t tags[256 / kTagGranuleSize];
constexpr size_t kNumBytesAroundRegister = 256;
constexpr size_t kNumTagsAroundRegister = kNumBytesAroundRegister / kTagGranuleSize;
char buf[kNumBytesAroundRegister];
uint8_t tags[kNumTagsAroundRegister];
size_t start_offset = 0;
ssize_t bytes = dump_memory(buf, sizeof(buf), tags, sizeof(tags), &value, memory);
if (bytes == -1) {
@ -377,7 +379,19 @@ static void dump_thread(Tombstone* tombstone, unwindstack::Unwinder* unwinder,
}
dump.set_memory(buf, bytes);
dump.set_tags(tags, bytes / kTagGranuleSize);
bool has_tags = false;
#if defined(__aarch64__)
for (size_t i = 0; i < kNumTagsAroundRegister; ++i) {
if (tags[i] != 0) {
has_tags = true;
}
}
#endif // defined(__aarch64__)
if (has_tags) {
dump.mutable_arm_mte_metadata()->set_memory_tags(tags, kNumTagsAroundRegister);
}
*thread.add_memory_dump() = std::move(dump);
}
@ -531,6 +545,50 @@ static void dump_logcat(Tombstone* tombstone, pid_t pid) {
dump_log_file(tombstone, "main", pid);
}
static void dump_tags_around_fault_addr(Signal* signal, const Tombstone& tombstone,
unwindstack::Unwinder* unwinder, uintptr_t fault_addr) {
if (tombstone.arch() != Architecture::ARM64) return;
fault_addr = untag_address(fault_addr);
constexpr size_t kNumGranules = kNumTagRows * kNumTagColumns;
constexpr size_t kBytesToRead = kNumGranules * kTagGranuleSize;
// If the low part of the tag dump would underflow to the high address space, it's probably not
// a valid address for us to dump tags from.
if (fault_addr < kBytesToRead / 2) return;
unwindstack::Memory* memory = unwinder->GetProcessMemory().get();
constexpr uintptr_t kRowStartMask = ~(kNumTagColumns * kTagGranuleSize - 1);
size_t start_address = (fault_addr & kRowStartMask) - kBytesToRead / 2;
MemoryDump tag_dump;
size_t granules_to_read = kNumGranules;
// Attempt to read the first tag. If reading fails, this likely indicates the
// lowest touched page is inaccessible or not marked with PROT_MTE.
// Fast-forward over pages until one has tags, or we exhaust the search range.
while (memory->ReadTag(start_address) < 0) {
size_t page_size = sysconf(_SC_PAGE_SIZE);
size_t bytes_to_next_page = page_size - (start_address % page_size);
if (bytes_to_next_page >= granules_to_read * kTagGranuleSize) return;
start_address += bytes_to_next_page;
granules_to_read -= bytes_to_next_page / kTagGranuleSize;
}
tag_dump.set_begin_address(start_address);
std::string* mte_tags = tag_dump.mutable_arm_mte_metadata()->mutable_memory_tags();
for (size_t i = 0; i < granules_to_read; ++i) {
long tag = memory->ReadTag(start_address + i * kTagGranuleSize);
if (tag < 0) break;
mte_tags->push_back(static_cast<uint8_t>(tag));
}
if (!mte_tags->empty()) {
*signal->mutable_fault_adjacent_metadata() = tag_dump;
}
}
static std::optional<uint64_t> read_uptime_secs() {
std::string uptime;
if (!android::base::ReadFileToString("/proc/uptime", &uptime)) {
@ -594,7 +652,9 @@ void engrave_tombstone_proto(Tombstone* tombstone, unwindstack::Unwinder* unwind
if (process_info.has_fault_address) {
sig.set_has_fault_address(true);
sig.set_fault_address(process_info.maybe_tagged_fault_address);
uintptr_t fault_addr = process_info.maybe_tagged_fault_address;
sig.set_fault_address(fault_addr);
dump_tags_around_fault_addr(&sig, result, unwinder, fault_addr);
}
*result.mutable_signal_info() = sig;

View file

@ -29,6 +29,7 @@
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
#include <async_safe/log.h>
#include <bionic/macros.h>
#include "tombstone.pb.h"
@ -193,8 +194,11 @@ static void print_thread_memory_dump(CallbackType callback, const Tombstone& tom
uint64_t addr = mem.begin_address();
for (size_t offset = 0; offset < mem.memory().size(); offset += bytes_per_line) {
uint64_t tagged_addr = addr;
if (mem.tags().size() > offset / kTagGranuleSize) {
tagged_addr |= static_cast<uint64_t>(mem.tags()[offset / kTagGranuleSize]) << 56;
if (mem.has_arm_mte_metadata() &&
mem.arm_mte_metadata().memory_tags().size() > offset / kTagGranuleSize) {
tagged_addr |=
static_cast<uint64_t>(mem.arm_mte_metadata().memory_tags()[offset / kTagGranuleSize])
<< 56;
}
std::string line = StringPrintf(" %0*" PRIx64, word_size * 2, tagged_addr + offset);
@ -232,6 +236,60 @@ static void print_thread(CallbackType callback, const Tombstone& tombstone, cons
print_thread_memory_dump(callback, tombstone, thread);
}
static void print_tag_dump(CallbackType callback, const Tombstone& tombstone) {
if (!tombstone.has_signal_info()) return;
const Signal& signal = tombstone.signal_info();
if (!signal.has_fault_address() || !signal.has_fault_adjacent_metadata()) {
return;
}
const MemoryDump& memory_dump = signal.fault_adjacent_metadata();
if (!memory_dump.has_arm_mte_metadata() || memory_dump.arm_mte_metadata().memory_tags().empty()) {
return;
}
const std::string& tags = memory_dump.arm_mte_metadata().memory_tags();
CBS("");
CBS("Memory tags around the fault address (0x%" PRIx64 "), one tag per %zu bytes:",
signal.fault_address(), kTagGranuleSize);
constexpr uintptr_t kRowStartMask = ~(kNumTagColumns * kTagGranuleSize - 1);
size_t tag_index = 0;
size_t num_tags = tags.length();
uintptr_t fault_granule = untag_address(signal.fault_address()) & ~(kTagGranuleSize - 1);
for (size_t row = 0; tag_index < num_tags; ++row) {
uintptr_t row_addr =
(memory_dump.begin_address() + row * kNumTagColumns * kTagGranuleSize) & kRowStartMask;
std::string row_contents;
bool row_has_fault = false;
for (size_t column = 0; column < kNumTagColumns; ++column) {
uintptr_t granule_addr = row_addr + column * kTagGranuleSize;
if (granule_addr < memory_dump.begin_address() ||
granule_addr >= memory_dump.begin_address() + num_tags * kTagGranuleSize) {
row_contents += " . ";
} else if (granule_addr == fault_granule) {
row_contents += StringPrintf("[%1hhx]", tags[tag_index++]);
row_has_fault = true;
} else {
row_contents += StringPrintf(" %1hhx ", tags[tag_index++]);
}
}
if (row_contents.back() == ' ') row_contents.pop_back();
if (row_has_fault) {
CBS(" =>0x%" PRIxPTR ":%s", row_addr, row_contents.c_str());
} else {
CBS(" 0x%" PRIxPTR ":%s", row_addr, row_contents.c_str());
}
}
}
static void print_main_thread(CallbackType callback, const Tombstone& tombstone,
const Thread& thread) {
print_thread_header(callback, tombstone, thread, true);
@ -299,6 +357,8 @@ static void print_main_thread(CallbackType callback, const Tombstone& tombstone,
}
}
print_tag_dump(callback, tombstone);
print_thread_memory_dump(callback, tombstone, thread);
CBS("");

View file

@ -56,8 +56,11 @@ message Signal {
bool has_fault_address = 8;
uint64 fault_address = 9;
// Note, may or may not contain the dump of the actual memory contents. Currently, on arm64, we
// only include metadata, and not the contents.
MemoryDump fault_adjacent_metadata = 10;
reserved 10 to 999;
reserved 11 to 999;
}
message HeapObject {
@ -142,14 +145,22 @@ message BacktraceFrame {
reserved 9 to 999;
}
message ArmMTEMetadata {
// One memory tag per granule (e.g. every 16 bytes) of regular memory.
bytes memory_tags = 1;
reserved 2 to 999;
}
message MemoryDump {
string register_name = 1;
string mapping_name = 2;
uint64 begin_address = 3;
bytes memory = 4;
bytes tags = 5;
oneof metadata {
ArmMTEMetadata arm_mte_metadata = 6;
}
reserved 6 to 999;
reserved 5, 7 to 999;
}
message MemoryMapping {