blob: 25ebb8436c1944e0c9e2c14d09206c833d68cdeb [file] [log] [blame]
// 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.
// Dumps PartitionAlloc's heap into a file.
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include "base/allocator/partition_allocator/partition_alloc_buildflags.h"
#include "base/allocator/partition_allocator/partition_alloc_config.h"
#include "base/allocator/partition_allocator/partition_ref_count.h"
#include "base/allocator/partition_allocator/partition_root.h"
#include "base/allocator/partition_allocator/thread_cache.h"
#include "base/bits.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/page_size.h"
#include "base/strings/stringprintf.h"
#include "base/thread_annotations.h"
#include "base/values.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/snappy/src/snappy.h"
#include "tools/memory/partition_allocator/inspect_utils.h"
namespace partition_alloc::tools {
using partition_alloc::internal::kInvalidBucketSize;
using partition_alloc::internal::kSuperPageSize;
using partition_alloc::internal::PartitionPage;
using partition_alloc::internal::PartitionPageSize;
#if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
using partition_alloc::internal::PartitionRefCountPointer;
#endif // BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
using partition_alloc::internal::PartitionSuperPageExtentEntry;
using partition_alloc::internal::SystemPageSize;
using partition_alloc::internal::ThreadSafe;
// See https://www.kernel.org/doc/Documentation/vm/pagemap.txt.
struct PageMapEntry {
uint64_t pfn_or_swap : 55;
uint64_t soft_dirty : 1;
uint64_t exclusively_mapped : 1;
uint64_t unused : 4;
uint64_t file_mapped_or_shared_anon : 1;
uint64_t swapped : 1;
uint64_t present : 1;
};
static_assert(sizeof(PageMapEntry) == sizeof(uint64_t), "Wrong bitfield size");
absl::optional<PageMapEntry> EntryAtAddress(int pagemap_fd, uintptr_t address) {
constexpr size_t kPageShift = 12;
off_t offset = (address >> kPageShift) * sizeof(PageMapEntry);
if (lseek(pagemap_fd, offset, SEEK_SET) != offset)
return absl::nullopt;
PageMapEntry entry;
if (read(pagemap_fd, &entry, sizeof(PageMapEntry)) != sizeof(PageMapEntry))
return absl::nullopt;
return {entry};
}
class HeapDumper {
public:
HeapDumper(pid_t pid, int pagemap_fd)
: pagemap_fd_(pagemap_fd), reader_(pid) {}
~HeapDumper() {
for (const auto& p : super_pages_) {
munmap(p.second, kSuperPageSize);
}
if (local_root_copy_mapping_base_) {
munmap(local_root_copy_mapping_base_, local_root_copy_mapping_size_);
}
}
bool FindRoot() {
root_address_ = FindRootAddress(reader_);
CHECK(root_address_);
auto root = RawBuffer<PartitionRoot<ThreadSafe>>::ReadFromProcessMemory(
reader_, root_address_);
CHECK(root);
root_ = *root;
// Since the heap if full of pointers, copying the data to the local address
// space doesn't allow to follow the pointers, or to call most member
// functions on the local objects.
//
// To make it easier to work with, we copy some objects in the local address
// space at the *same* address used in the remote process. This is not
// guaranteed to work though, since the addresses can already be mapped in
// the local process. However, since we are targeting 64 bit Linux, with
// ASLR executing again should solve the problem in most cases.
//
// Copy at the same address as in the remote process. Since the root is not
// page-aligned in the remote process, need to pad the mapping a bit.
size_t size_to_map = ::base::bits::AlignUp(
sizeof(PartitionRoot<ThreadSafe>) + SystemPageSize(), SystemPageSize());
uintptr_t address_to_map =
::base::bits::AlignDown(root_address_, SystemPageSize());
char* local_memory = CreateMappingAtAddress(address_to_map, size_to_map);
if (!local_memory) {
LOG(WARNING) << base::StringPrintf(
"Cannot map memory at %lx",
reinterpret_cast<uintptr_t>(address_to_map));
return false;
}
local_root_copy_ = local_memory;
memcpy(reinterpret_cast<void*>(root_address_), root_.get(),
sizeof(PartitionRoot<ThreadSafe>));
local_root_copy_mapping_base_ = reinterpret_cast<void*>(address_to_map);
local_root_copy_mapping_size_ = size_to_map;
return true;
}
bool DumpSuperPages() {
std::vector<uintptr_t> super_pages;
// There is no list of super page, only a list of extents. Walk the extent
// list to get all superpages.
uintptr_t extent_address =
reinterpret_cast<uintptr_t>(root_.get()->first_extent);
while (extent_address) {
auto extent = RawBuffer<PartitionSuperPageExtentEntry<ThreadSafe>>::
ReadFromProcessMemory(reader_, extent_address);
uintptr_t first_super_page_address = SuperPagesBeginFromExtent(
reinterpret_cast<PartitionSuperPageExtentEntry<ThreadSafe>*>(
extent_address));
for (uintptr_t super_page = first_super_page_address;
super_page < first_super_page_address +
extent->get()->number_of_consecutive_super_pages *
kSuperPageSize;
super_page += kSuperPageSize) {
super_pages.push_back(super_page);
}
extent_address = reinterpret_cast<uintptr_t>(extent->get()->next);
}
LOG(WARNING) << "Found " << super_pages.size() << std::hex
<< " super pages.";
for (uintptr_t super_page : super_pages) {
char* local_super_page =
reader_.ReadAtSameAddressInLocalMemory(super_page, kSuperPageSize);
if (!local_super_page) {
LOG(WARNING) << base::StringPrintf("Cannot read from super page 0x%lx",
super_page);
continue;
}
super_pages_.emplace(super_page, local_super_page);
}
LOG(WARNING) << "Read all super pages";
return true;
}
base::Value::List Dump() const {
auto partition_page_to_value = [](uintptr_t offset, const char* data) {
base::Value::Dict ret;
std::string value;
if (offset == 0) {
value = "metadata";
} else if (offset == kSuperPageSize - PartitionPageSize()) {
value = "guard";
} else {
value = "payload";
}
ret.Set("type", value);
if (value != "metadata" && value != "guard") {
const auto* partition_page = PartitionPage<ThreadSafe>::FromAddr(
reinterpret_cast<uintptr_t>(data + offset));
ret.Set("page_index_in_span",
partition_page->slot_span_metadata_offset);
if (partition_page->slot_span_metadata_offset == 0 &&
partition_page->slot_span_metadata.bucket) {
const auto& slot_span_metadata = partition_page->slot_span_metadata;
ret.Set("slot_size",
static_cast<int>(slot_span_metadata.bucket->slot_size));
ret.Set("is_active", slot_span_metadata.is_active());
ret.Set("is_full", slot_span_metadata.is_full());
ret.Set("is_empty", slot_span_metadata.is_empty());
ret.Set("is_decommitted", slot_span_metadata.is_decommitted());
ret.Set("slots_per_span",
static_cast<int>(
slot_span_metadata.bucket->get_slots_per_span()));
ret.Set(
"num_system_pages_per_slot_span",
static_cast<int>(
slot_span_metadata.bucket->num_system_pages_per_slot_span));
ret.Set("num_allocated_slots",
slot_span_metadata.num_allocated_slots);
ret.Set("num_unprovisioned_slots",
slot_span_metadata.num_unprovisioned_slots);
}
}
bool all_zeros = true;
for (size_t i = 0; i < PartitionPageSize(); i++) {
if (data[offset + i]) {
all_zeros = false;
break;
}
}
ret.Set("all_zeros", all_zeros);
return ret;
};
auto super_page_to_value = [&](uintptr_t address, const char* data) {
base::Value::Dict ret;
ret.Set("address", base::StringPrintf("0x%lx", address));
base::Value::List partition_pages;
for (uintptr_t offset = 0; offset < kSuperPageSize;
offset += PartitionPageSize()) {
partition_pages.Append(partition_page_to_value(offset, data));
}
ret.Set("partition_pages", std::move(partition_pages));
base::Value::List page_sizes;
// Looking at how well the heap would compress.
const size_t page_size = base::GetPageSize();
for (uintptr_t page_address = address;
page_address < address + partition_alloc::internal::kSuperPageSize;
page_address += page_size) {
auto maybe_pagemap_entry = EntryAtAddress(pagemap_fd_, page_address);
size_t uncompressed_size = 0, compressed_size = 0;
bool all_zeros = true;
for (size_t i = 0; i < page_size; i++) {
if (reinterpret_cast<unsigned char*>(page_address)[i]) {
all_zeros = false;
break;
}
}
bool should_report;
if (!maybe_pagemap_entry) {
// We cannot tell whether a page has been decommitted, but all-zero
// likely indicates that. Only report data for pages that the other
// pages.
should_report = !all_zeros;
} else {
// If it's not in memory and not in swap, only the PTE exists.
should_report =
maybe_pagemap_entry->present || maybe_pagemap_entry->swapped;
}
if (should_report) {
std::string compressed;
uncompressed_size = page_size;
// Use snappy to approximate what a fast compression algorithm
// operating with a page granularity would do. This is not the
// algorithm used in either Linux or macOS, but should give some
// indication.
compressed_size =
snappy::Compress(reinterpret_cast<const char*>(page_address),
page_size, &compressed);
}
base::Value::Dict page_size_dict;
page_size_dict.Set("uncompressed", static_cast<int>(uncompressed_size));
page_size_dict.Set("compressed", static_cast<int>(compressed_size));
page_sizes.Append(std::move(page_size_dict));
}
ret.Set("page_sizes", std::move(page_sizes));
return ret;
};
base::Value::List super_pages_value;
for (const auto& address_data : super_pages_) {
super_pages_value.Append(
super_page_to_value(address_data.first, address_data.second));
}
return super_pages_value;
}
#if PA_CONFIG(REF_COUNT_STORE_REQUESTED_SIZE)
base::Value::List DumpAllocatedSizes() {
// Note: Here and below, it is safe to follow pointers into the super page,
// or to the root or buckets, since they share the same address in the this
// process as in the Chromium process.
// Since there is no tracking of full slot spans, the way to enumerate all
// allocated memory is to walk the heap itself.
base::Value::List ret;
for (const auto& address_data : super_pages_) {
const char* data = address_data.second;
// Exclude the first and last partition pagers: metadata and guard,
// respectively.
size_t partition_page_index = 1;
while (partition_page_index < kSuperPageSize / PartitionPageSize() - 1) {
uintptr_t slot_span_start = reinterpret_cast<uintptr_t>(
data + partition_page_index * PartitionPageSize());
const auto* partition_page =
PartitionPage<ThreadSafe>::FromAddr(slot_span_start);
// No bucket for PartitionPages that were never provisioned.
if (!partition_page->slot_span_metadata.bucket) {
partition_page_index++;
continue;
}
const auto& metadata = partition_page->slot_span_metadata;
if (metadata.is_decommitted() || metadata.is_empty()) {
// Skip this entire slot span, since it doesn't hold live allocations.
partition_page_index += metadata.bucket->get_pages_per_slot_span();
continue;
}
base::Value::Dict slot_span_value;
slot_span_value.Set("start_address",
base::StringPrintf("0x%lx", slot_span_start));
slot_span_value.Set("slot_size",
static_cast<int>(metadata.bucket->slot_size));
// There is no tracking of allocated slots, need to reconstruct
// these as everything which is not in the freelist.
std::vector<bool> free_slots(metadata.bucket->get_slots_per_span());
auto* head = metadata.get_freelist_head();
while (head) {
size_t offset_in_slot_span =
reinterpret_cast<uintptr_t>(head) - slot_span_start;
size_t slot_number =
metadata.bucket->GetSlotNumber(offset_in_slot_span);
free_slots[slot_number] = true;
head = head->GetNext(0);
}
base::Value::List allocated_sizes_value;
for (size_t slot_index = 0; slot_index < free_slots.size();
slot_index++) {
// Skip unprovisioned slots, which are always at the end of the slot
// span.
if (free_slots[slot_index] ||
slot_index >= (metadata.bucket->get_slots_per_span() -
metadata.num_unprovisioned_slots)) {
continue;
}
uintptr_t slot_address =
slot_span_start + slot_index * metadata.bucket->slot_size;
auto* ref_count = PartitionRefCountPointer(slot_address);
uint32_t requested_size = ref_count->requested_size();
// Address space dumping is not synchronized with allocation, meaning
// that we can observe the heap in an inconsistent state. Skip
// obviously-wrong entries.
if (requested_size > metadata.bucket->slot_size || !requested_size)
continue;
allocated_sizes_value.Append(static_cast<int>(requested_size));
}
slot_span_value.Set("allocated_sizes",
std::move(allocated_sizes_value));
ret.Append(std::move(slot_span_value));
partition_page_index += metadata.bucket->get_pages_per_slot_span();
}
}
return ret;
}
#endif // PA_CONFIG(REF_COUNT_STORE_REQUESTED_SIZE)
base::Value::List DumpBuckets() {
base::Value::List ret;
for (const auto& bucket : root_.get()->buckets) {
if (bucket.slot_size == kInvalidBucketSize)
continue;
base::Value::Dict bucket_value;
bucket_value.Set("slot_size", static_cast<int>(bucket.slot_size));
ret.Append(std::move(bucket_value));
}
return ret;
}
private:
static uintptr_t FindRootAddress(RemoteProcessMemoryReader& reader)
NO_THREAD_SAFETY_ANALYSIS {
uintptr_t tcache_registry_address = IndexThreadCacheNeedleArray(reader, 1);
auto registry = RawBuffer<ThreadCacheRegistry>::ReadFromProcessMemory(
reader, tcache_registry_address);
if (!registry)
return 0;
auto tcache_address =
reinterpret_cast<uintptr_t>(registry->get()->list_head_);
if (!tcache_address)
return 0;
auto tcache =
RawBuffer<ThreadCache>::ReadFromProcessMemory(reader, tcache_address);
if (!tcache)
return 0;
auto root_address = reinterpret_cast<uintptr_t>(tcache->get()->root_);
return root_address;
}
const int pagemap_fd_;
uintptr_t root_address_ = 0;
RemoteProcessMemoryReader reader_;
RawBuffer<PartitionRoot<ThreadSafe>> root_ = {};
std::map<uintptr_t, char*> super_pages_ = {};
char* local_root_copy_ = nullptr;
void* local_root_copy_mapping_base_ = nullptr;
size_t local_root_copy_mapping_size_ = 0;
};
} // namespace partition_alloc::tools
int main(int argc, char** argv) {
base::CommandLine::Init(argc, argv);
auto* command_line = base::CommandLine::ForCurrentProcess();
if (!command_line->HasSwitch("pid") || !command_line->HasSwitch("json")) {
LOG(ERROR) << "Usage:" << argv[0] << " --pid=<PID> --json=<FILENAME>";
return 1;
}
int pid = atoi(command_line->GetSwitchValueASCII("pid").c_str());
LOG(WARNING) << "PID = " << pid;
auto pagemap_fd = partition_alloc::tools::OpenPagemap(pid);
partition_alloc::tools::HeapDumper dumper{pid, pagemap_fd.get()};
{
partition_alloc::tools::ScopedSigStopper stopper{pid};
if (!dumper.FindRoot()) {
LOG(WARNING) << "Cannot find (or copy) the root";
return 1;
}
if (!dumper.DumpSuperPages()) {
LOG(WARNING) << "Cannot dump (or copy) super pages.";
}
}
base::Value::Dict overall_dump;
overall_dump.Set("superpages", dumper.Dump());
#if PA_CONFIG(REF_COUNT_STORE_REQUESTED_SIZE)
overall_dump.Set("allocated_sizes", dumper.DumpAllocatedSizes());
#endif // PA_CONFIG(REF_COUNT_STORE_REQUESTED_SIZE)
overall_dump.Set("buckets", dumper.DumpBuckets());
std::string json_string;
bool ok = base::JSONWriter::WriteWithOptions(
overall_dump, base::JSONWriter::Options::OPTIONS_PRETTY_PRINT,
&json_string);
if (ok) {
base::FilePath json_filename = command_line->GetSwitchValuePath("json");
auto f = base::File(json_filename, base::File::Flags::FLAG_CREATE_ALWAYS |
base::File::Flags::FLAG_WRITE);
if (f.IsValid()) {
f.WriteAtCurrentPos(json_string.c_str(), json_string.size());
LOG(WARNING) << "\n\nDumped JSON to " << json_filename;
return 0;
}
}
return 1;
}