blob: 22a42096022f86abb84672c3b12d13b88991a899 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/debug_agent/zircon_process_handle.h"
#include <algorithm>
#include <iterator>
#include "src/developer/debug/debug_agent/debugged_thread.h"
#include "src/developer/debug/debug_agent/elf_utils.h"
#include "src/developer/debug/debug_agent/process_handle_observer.h"
#include "src/developer/debug/debug_agent/zircon_exception_handle.h"
#include "src/developer/debug/debug_agent/zircon_thread_handle.h"
#include "src/developer/debug/debug_agent/zircon_utils.h"
#include "src/developer/debug/ipc/records.h"
#include "src/developer/debug/shared/message_loop_fuchsia.h"
#include "third_party/crashpad/src/minidump/minidump_file_writer.h"
#include "third_party/crashpad/src/snapshot/fuchsia/process_snapshot_fuchsia.h"
#include "third_party/crashpad/src/util/file/string_file.h"
#include "third_party/crashpad/src/util/fuchsia/scoped_task_suspend.h"
namespace debug_agent {
namespace {
void FillVmoInfo(const zx_info_vmo_t& source, debug_ipc::InfoHandleVmo& dest) {
static_assert(sizeof(dest.name) == sizeof(source.name));
memcpy(dest.name, source.name, sizeof(source.name));
dest.size_bytes = source.size_bytes;
dest.parent_koid = source.parent_koid;
dest.num_children = source.num_children;
dest.num_mappings = source.num_mappings;
dest.share_count = source.share_count;
dest.flags = source.flags;
dest.committed_bytes = source.committed_bytes;
dest.cache_policy = source.cache_policy;
dest.metadata_bytes = source.metadata_bytes;
dest.committed_change_events = source.committed_change_events;
}
debug_ipc::AddressRegion MapToAddressRegion(const zx_info_maps& map) {
debug_ipc::AddressRegion region;
region.name = map.name;
region.base = map.base;
region.size = map.size;
region.depth = map.depth;
region.vmo_koid = map.u.mapping.vmo_koid;
region.vmo_offset = map.u.mapping.vmo_offset;
region.committed_pages = map.u.mapping.committed_pages;
region.read = map.u.mapping.mmu_flags & ZX_VM_PERM_READ;
region.write = map.u.mapping.mmu_flags & ZX_VM_PERM_WRITE;
region.execute = map.u.mapping.mmu_flags & ZX_VM_PERM_EXECUTE;
region.shared = false; // Unused on Fuchsia.
return region;
}
} // namespace
ZirconProcessHandle::ZirconProcessHandle(zx::process p)
: process_koid_(zircon::KoidForObject(p)), process_(std::move(p)) {}
std::string ZirconProcessHandle::GetName() const { return zircon::NameForObject(process_); }
std::vector<std::unique_ptr<ThreadHandle>> ZirconProcessHandle::GetChildThreads() const {
std::vector<std::unique_ptr<ThreadHandle>> result;
for (auto& child : zircon::GetChildThreads(process_))
result.push_back(std::make_unique<ZirconThreadHandle>(std::move(child)));
return result;
}
zx_koid_t ZirconProcessHandle::GetJobKoid() const {
if (job_koid_ == ZX_KOID_INVALID) {
zx_info_handle_basic_t info;
// The related_koid of a process is the koid of its parent job and is immutable.
if (process_.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr) == ZX_OK)
job_koid_ = info.related_koid;
}
return job_koid_;
}
debug::Status ZirconProcessHandle::Kill() { return debug::ZxStatus(process_.kill()); }
int64_t ZirconProcessHandle::GetReturnCode() const {
zx_info_process_t info = {};
if (process_.get_info(ZX_INFO_PROCESS, &info, sizeof(info), nullptr, nullptr) == ZX_OK)
return info.return_code;
return 0;
}
debug::Status ZirconProcessHandle::Attach(ProcessHandleObserver* observer) {
FX_DCHECK(observer);
observer_ = observer;
if (!process_watch_handle_.watching()) {
// Start watching.
debug::MessageLoopFuchsia* loop = debug::MessageLoopFuchsia::Current();
FX_DCHECK(loop); // Loop must be created on this thread first.
// Register for debug exceptions.
debug::MessageLoopFuchsia::WatchProcessConfig config;
config.process_name = GetName();
config.process_handle = process_.get();
config.process_koid = GetKoid();
config.watcher = this;
if (auto status = debug::ZxStatus(
loop->WatchProcessExceptions(std::move(config), &process_watch_handle_));
status.has_error()) {
return status;
}
// Check and set ZX_PROP_PROCESS_BREAK_ON_LOAD.
uintptr_t break_on_load;
zx_status_t status =
process_.get_property(ZX_PROP_PROCESS_BREAK_ON_LOAD, &break_on_load, sizeof(break_on_load));
FX_CHECK(status == ZX_OK);
// This check should never fail because the debug exception channel obtained above is exclusive.
FX_CHECK(break_on_load == 0);
break_on_load = 1;
status =
process_.set_property(ZX_PROP_PROCESS_BREAK_ON_LOAD, &break_on_load, sizeof(break_on_load));
FX_CHECK(status == ZX_OK);
}
return debug::Status();
}
void ZirconProcessHandle::Detach() {
observer_ = nullptr;
// Unset ZX_PROP_PROCESS_BREAK_ON_LOAD.
uintptr_t break_on_load = 0;
FX_CHECK(process_.set_property(ZX_PROP_PROCESS_BREAK_ON_LOAD, &break_on_load,
sizeof(break_on_load)) == ZX_OK);
// Unbind from the exception port.
process_watch_handle_.StopWatching();
}
uint64_t ZirconProcessHandle::GetLoaderBreakpointAddress() {
uintptr_t break_on_load;
FX_CHECK(process_.get_property(ZX_PROP_PROCESS_BREAK_ON_LOAD, &break_on_load,
sizeof(break_on_load)) == ZX_OK);
return break_on_load;
}
std::vector<debug_ipc::AddressRegion> ZirconProcessHandle::GetAddressSpace(uint64_t address) const {
std::vector<debug_ipc::AddressRegion> regions;
std::vector<zx_info_maps_t> map = GetMaps();
if (address) {
// Get a specific region.
for (const auto& entry : map) {
if (address < entry.base)
continue;
if (address <= (entry.base + entry.size))
regions.push_back(MapToAddressRegion(entry));
}
} else {
// Get all regions.
size_t ix = 0;
regions.resize(map.size());
for (const auto& entry : map) {
regions[ix] = MapToAddressRegion(entry);
++ix;
}
}
return regions;
}
std::vector<debug_ipc::Module> ZirconProcessHandle::GetModules() const {
uintptr_t dl_debug_addr;
FX_CHECK(process_.get_property(ZX_PROP_PROCESS_DEBUG_ADDR, &dl_debug_addr,
sizeof(dl_debug_addr)) == ZX_OK);
return GetElfModulesForProcess(*this, dl_debug_addr);
}
fit::result<debug::Status, std::vector<debug_ipc::InfoHandle>> ZirconProcessHandle::GetHandles()
const {
// Query the handle table size.
size_t handles_actual = 0;
size_t handles_avail = 0;
if (zx_status_t status =
process_.get_info(ZX_INFO_HANDLE_TABLE, nullptr, 0, &handles_actual, &handles_avail);
status != ZX_OK)
return fit::error(debug::ZxStatus(status));
// We're technically racing with the program, so add some extra buffer in case the process has
// opened more handles since the above query.
handles_avail += 64;
// Read the extended handle table.
std::vector<zx_info_handle_extended_t> handles(handles_avail);
if (zx_status_t status = process_.get_info(ZX_INFO_HANDLE_TABLE, handles.data(),
handles_avail * sizeof(zx_info_handle_extended_t),
&handles_actual, &handles_avail);
status != ZX_OK)
return fit::error(debug::ZxStatus(status));
handles.resize(handles_actual);
// Query the VMO table size.
size_t vmo_actual = 0;
size_t vmo_avail = 0;
if (zx_status_t status =
process_.get_info(ZX_INFO_PROCESS_VMOS, nullptr, 0, &vmo_actual, &vmo_avail);
status != ZX_OK)
return fit::error(debug::ZxStatus(status));
vmo_avail += 64; // Try to prevent races as above.
// Read the VMO table.
std::vector<zx_info_vmo_t> vmos(vmo_avail);
if (zx_status_t status =
process_.get_info(ZX_INFO_PROCESS_VMOS, vmos.data(), vmo_avail * sizeof(zx_info_vmo_t),
&vmo_actual, &vmo_avail);
status != ZX_OK)
return fit::error(debug::ZxStatus(status));
vmos.resize(vmo_actual);
// Index VMOs by koid to allow merging below.
std::map<zx_koid_t, zx_info_vmo_t> vmo_index;
for (const auto& vmo : vmos)
vmo_index[vmo.koid] = vmo;
std::vector<debug_ipc::InfoHandle> result(handles.size());
for (size_t i = 0; i < handles.size(); ++i) {
result[i].type = handles[i].type;
result[i].handle_value = handles[i].handle_value;
result[i].rights = handles[i].rights;
result[i].koid = handles[i].koid;
result[i].related_koid = handles[i].related_koid;
result[i].peer_owner_koid = handles[i].peer_owner_koid;
// VMO-specific extended information.
if (handles[i].type == ZX_OBJ_TYPE_VMO) {
if (auto found_vmo = vmo_index.find(handles[i].koid); found_vmo != vmo_index.end()) {
FillVmoInfo(found_vmo->second, result[i].ext.vmo);
// Remove VMO info as we find it so we know what wasn't added below.
vmo_index.erase(found_vmo);
}
}
}
// Some VMOs won't have open handles. Add these to the table also with 0 handle values. All
// previously-matched items will have already been removed from the table, so everything left
// needs to be added.
for (const auto& [koid, vmo] : vmo_index) {
auto& dest = result.emplace_back();
dest.type = ZX_OBJ_TYPE_VMO;
dest.rights = vmo.handle_rights;
dest.koid = koid;
FillVmoInfo(vmo, dest.ext.vmo);
}
return fit::success(std::move(result));
}
debug::Status ZirconProcessHandle::ReadMemory(uintptr_t address, void* buffer, size_t len,
size_t* actual) const {
return debug::ZxStatus(process_.read_memory(address, buffer, len, actual));
}
debug::Status ZirconProcessHandle::WriteMemory(uintptr_t address, const void* buffer, size_t len,
size_t* actual) {
return debug::ZxStatus(process_.write_memory(address, buffer, len, actual));
}
std::vector<debug_ipc::MemoryBlock> ZirconProcessHandle::ReadMemoryBlocks(uint64_t address,
uint32_t size) const {
// Optimistically assume the read will work which will be faster in the common case.
if (debug_ipc::MemoryBlock block = ReadOneMemoryBlock(address, size); block.valid)
return {std::move(block)};
// Failure reading, this memory is either not mapped or it may cross mapping boundaries. To solve
// the multiple boundary problem, get the memory mapping and compute all mapping boundaries in the
// requested region. Then try to read each of the resulting blocks (which may be valid or
// invalid).
//
// This computed boundaries array will contain all boundaries (including the end address and some
// duplicates) except the begin address (this will be implicit in the later computation).
std::vector<uint64_t> boundaries;
for (const zx_info_maps_t& map : GetMaps()) {
// The returned maps should be sorted so any mapping region starting past our region means all
// relevant boundaries have been found.
if (map.base > address + size)
break;
if (map.base > address)
boundaries.push_back(map.base);
uint64_t end = map.base + map.size;
if (end > address && end < address + size)
boundaries.push_back(end);
}
boundaries.push_back(address + size);
std::sort(boundaries.begin(), boundaries.end());
std::vector<debug_ipc::MemoryBlock> blocks;
uint64_t begin = address;
for (uint64_t end : boundaries) {
// There will be some duplicates in the boundaries array so skip anything that's empty. These
// duplicates are caused by a range which a child inside it that is coincident with one of the
// parent boundaries, or two regions that abut each other.
if (end == begin)
continue;
blocks.push_back(ReadOneMemoryBlock(begin, static_cast<uint32_t>(end - begin)));
begin = end;
}
return blocks;
}
debug::Status ZirconProcessHandle::SaveMinidump(const std::vector<DebuggedThread*>& threads,
std::vector<uint8_t>* core_data) {
debug::Status status = {};
// Suspend the process while we capture the snapshot.
crashpad::ScopedTaskSuspend suspend(process_);
crashpad::ProcessSnapshotFuchsia process_snapshot;
if (!process_snapshot.Initialize(process_)) {
status = debug::Status("Failed to initialize minidump from process " +
std::to_string(process_koid_) + ".");
return status;
}
// Add any exceptions to the snapshot, if present. This is particularly useful for saving the
// complete state of a process that was caught in limbo.
for (const auto& thread : threads) {
if (thread->in_exception()) {
zx_exception_report_t exception_report;
zx_status_t status = thread->thread_handle().GetNativeHandle().get_info(
ZX_INFO_THREAD_EXCEPTION_REPORT, &exception_report, sizeof(exception_report), nullptr,
nullptr);
if (status != ZX_OK) {
DEBUG_LOG(Process) << "Failed to get ZX_INFO_THREAD_EXCEPTION_REPORT for thread "
<< thread->koid();
} else if (!process_snapshot.InitializeException(thread->koid(), exception_report)) {
DEBUG_LOG(Process) << "Failed to add thread exception report to process snapshot.";
}
}
}
crashpad::MinidumpFileWriter writer;
writer.InitializeFromSnapshot(&process_snapshot);
crashpad::StringFile file;
if (!writer.WriteEverything(&file)) {
status = debug::Status("Failed to write core.");
return status;
}
// Copy data out of in memory file to the IPC format.
const std::string& s = file.string();
core_data->reserve(s.size());
std::copy(s.begin(), s.end(), std::back_inserter(*core_data));
return status;
}
debug_ipc::MemoryBlock ZirconProcessHandle::ReadOneMemoryBlock(uint64_t address,
uint32_t size) const {
debug_ipc::MemoryBlock block;
block.address = address;
block.size = size;
block.data.resize(size);
size_t bytes_read = 0;
if (process_.read_memory(address, block.data.data(), block.size, &bytes_read) == ZX_OK &&
bytes_read == size) {
block.valid = true;
} else {
block.valid = false;
block.data.resize(0);
}
return block;
}
std::vector<zx_info_maps_t> ZirconProcessHandle::GetMaps() const {
const size_t kRegionsCountGuess = 64u;
const size_t kNewRegionsCountGuess = 4u;
size_t count_guess = kRegionsCountGuess;
std::vector<zx_info_maps_t> map;
size_t actual;
size_t avail;
while (true) {
map.resize(count_guess);
zx_status_t status = process_.get_info(ZX_INFO_PROCESS_MAPS, map.data(),
sizeof(zx_info_maps) * map.size(), &actual, &avail);
if (status != ZX_OK) {
actual = 0;
break;
} else if (actual == avail) {
break;
}
count_guess = avail + kNewRegionsCountGuess;
}
map.resize(actual);
return map;
}
void ZirconProcessHandle::OnProcessTerminated(zx_koid_t process_koid) {
FX_DCHECK(observer_);
FX_DCHECK(process_koid == GetKoid());
observer_->OnProcessTerminated();
}
void ZirconProcessHandle::OnThreadStarting(zx::exception exception, zx_exception_info_t info) {
FX_DCHECK(observer_);
observer_->OnThreadStarting(std::make_unique<ZirconExceptionHandle>(std::move(exception), info));
}
void ZirconProcessHandle::OnThreadExiting(zx::exception exception, zx_exception_info_t info) {
FX_DCHECK(observer_);
observer_->OnThreadExiting(std::make_unique<ZirconExceptionHandle>(std::move(exception), info));
}
void ZirconProcessHandle::OnException(zx::exception exception, zx_exception_info_t info) {
FX_DCHECK(observer_);
observer_->OnException(std::make_unique<ZirconExceptionHandle>(std::move(exception), info));
}
} // namespace debug_agent