blob: b6e759bade1a305f9ea33ae6bbf16b94e26dd240 [file] [log] [blame]
// Copyright 2022 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 "dump-tests.h"
#include <lib/fit/defer.h>
#include <lib/stdcompat/string_view.h>
#include <lib/zxdump/dump.h>
#include <lib/zxdump/fd-writer.h>
#include <lib/zxdump/task.h>
#include <lib/zxdump/zstd-writer.h>
#include <unistd.h>
#include <zircon/status.h>
#include <array>
#include <cinttypes>
#include <cstdio>
#include <type_traits>
#include <fbl/unique_fd.h>
#include <gmock/gmock.h>
#include "rights.h"
#include "test-data-holder.h"
#include "test-file.h"
#include "test-tool-process.h"
// The dump format is complex enough that direct testing of output data would
// be tantamount to reimplementing the reader, and golden binary files aren't
// easy to match up with fresh data from a live system where all the KOID and
// statistics values will be different every time. So the main method used to
// test the dumper is via end-to-end tests that dump into a file via the dumper
// API, read the dump back using the reader API, and then compare the data from
// the dump to the data from the original live tasks.
namespace zxdump::testing {
using namespace std::literals;
using ::testing::Contains;
using ::testing::FieldsAre;
using ::testing::UnorderedElementsAreArray;
void TestProcessForPropertiesAndInfo::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild());
}
template <typename Writer>
void TestProcessForPropertiesAndInfo::Dump(Writer& writer, PrecollectFunction precollect,
SegmentCallback prune) {
const bool dump_memory = prune != nullptr;
if (!prune) {
prune = PruneAllMemory;
}
zxdump::TaskHolder holder;
auto insert_result = holder.Insert(handle());
ASSERT_TRUE(insert_result.is_ok()) << insert_result.error_value();
ASSERT_EQ(insert_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::ProcessDump dump(static_cast<zxdump::Process&>(insert_result->get()));
ASSERT_NO_FATAL_FAILURE(precollect(holder, dump));
auto collect_result = dump.CollectProcess(std::move(prune));
ASSERT_TRUE(collect_result.is_ok()) << collect_result.error_value();
auto dump_result = dump.DumpHeaders(writer.AccumulateFragmentsCallback());
ASSERT_TRUE(dump_result.is_ok()) << dump_result.error_value();
auto write_result = writer.WriteFragments();
ASSERT_TRUE(write_result.is_ok()) << write_result.error_value();
const size_t bytes_written = write_result.value();
auto memory_result = dump.DumpMemory(writer.WriteCallback());
ASSERT_TRUE(memory_result.is_ok()) << memory_result.error_value();
const size_t total_with_memory = memory_result.value();
if (dump_memory) {
// Dumping the memory should have added a bunch to the dump.
EXPECT_LT(bytes_written, total_with_memory);
} else {
// We pruned all memory, so DumpMemory should not have added any output.
EXPECT_EQ(bytes_written, total_with_memory);
}
}
template void TestProcessForPropertiesAndInfo::Dump(FdWriter&, PrecollectFunction, SegmentCallback);
template void TestProcessForPropertiesAndInfo::Dump(ZstdWriter&, PrecollectFunction,
SegmentCallback);
void TestProcessForPropertiesAndInfo::CheckDump(zxdump::TaskHolder& holder, bool threads_dumped) {
auto find_result = holder.root_job().find(koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
{
auto name_result = read_process.get_property<ZX_PROP_NAME>();
ASSERT_TRUE(name_result.is_ok()) << name_result.error_value();
std::string_view name(name_result->data(), name_result->size());
name = name.substr(0, name.find_first_of('\0'));
EXPECT_EQ(name, std::string_view(kChildName));
}
{
auto threads_result = read_process.get_info<ZX_INFO_PROCESS_THREADS>();
ASSERT_TRUE(threads_result.is_ok()) << threads_result.error_value();
EXPECT_EQ(threads_result->size(), size_t{1});
}
// Even though ZX_INFO_PROCESS_THREADS is present, threads() only
// returns anything if the threads were actually dumped.
{
auto threads_result = read_process.threads();
ASSERT_TRUE(threads_result.is_ok()) << threads_result.error_value();
if (threads_dumped) {
EXPECT_EQ(threads_result->get().size(), size_t{1});
} else {
EXPECT_EQ(threads_result->get().size(), size_t{0});
}
}
{
auto info_result = read_process.get_info<ZX_INFO_HANDLE_BASIC>();
ASSERT_TRUE(info_result.is_ok()) << info_result.error_value();
EXPECT_EQ(info_result->type, ZX_OBJ_TYPE_PROCESS);
EXPECT_EQ(info_result->koid, koid());
}
}
void TestProcessForSystemInfo::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild());
auto result = live_holder_.InsertSystem();
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
void TestProcessForSystemInfo::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {
auto result = dump.CollectSystem(live_holder_);
ASSERT_TRUE(result.is_ok()) << result.error_value();
}
void TestProcessForSystemInfo::CheckDump(zxdump::TaskHolder& holder) {
EXPECT_EQ(holder.system_get_dcache_line_size(), zx_system_get_dcache_line_size());
EXPECT_EQ(holder.system_get_num_cpus(), zx_system_get_num_cpus());
EXPECT_EQ(holder.system_get_page_size(), zx_system_get_page_size());
EXPECT_EQ(holder.system_get_physmem(), zx_system_get_physmem());
std::string_view version = zx_system_get_version_string();
EXPECT_EQ(holder.system_get_version_string(), version);
}
void TestProcessForKernelInfo::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild());
// Fetch the root resource, since we'll need it to dump.
auto root_result = zxdump::GetRootResource();
EXPECT_TRUE(root_result.is_ok()) << root_result.error_value();
root_resource_ = *std::move(root_result);
}
void TestProcessForKernelInfo::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {
zxdump::LiveHandle root_resource_copy;
EXPECT_EQ(ZX_OK, root_resource().duplicate(ZX_RIGHT_SAME_RIGHTS, &root_resource_copy));
auto insert_result = holder.Insert(std::move(root_resource_copy));
EXPECT_TRUE(insert_result.is_ok()) << insert_result.error_value();
auto result = dump.CollectKernel();
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
void TestProcessForKernelInfo::CheckDump(zxdump::TaskHolder& holder) {
using KernelData = TestDataHolder< //
InfoTraits<ZX_INFO_CPU_STATS>, //
InfoTraits<ZX_INFO_KMEM_STATS>, //
InfoTraits<ZX_INFO_GUEST_STATS>>;
zxdump::Resource& root = holder.root_resource();
EXPECT_NE(root.koid(), ZX_KOID_INVALID);
EXPECT_EQ(root.type(), ZX_OBJ_TYPE_RESOURCE);
KernelData dump_data, live_data;
{
// Use a fresh holder to populate the live data. It can consume the root
// resource handle we used in Precollect, since we've already dumped and
// don't need it any more.
zxdump::TaskHolder live_holder;
auto live_root = live_holder.Insert(std::move(root_resource_));
ASSERT_TRUE(live_root.is_ok()) << live_root.error_value();
ASSERT_NO_FATAL_FAILURE(live_data.Fill(*live_root));
}
// Fetch all the data from the dump.
ASSERT_NO_FATAL_FAILURE(dump_data.Fill(root));
// Check that the dump data makes sense as data collected before the live
// data just collected (after the dump was made).
dump_data.Check(live_data);
}
void TestProcessForRemarks::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild());
}
void TestProcessForRemarks::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {
{
auto result = dump.Remarks(kDefaultRemarksName, kTextRemarksData);
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
{
auto result = dump.Remarks(kTextRemarksName, kTextRemarksData);
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
{
auto result = dump.Remarks(kBinaryRemarksName, kBinaryRemarksData);
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
{
auto result = dump.Remarks(kDefaultJsonRemarksName, kNormalizedJsonRemarksData);
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
{
auto result = dump.Remarks(kJsonRemarksName, kNormalizedJsonRemarksData);
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
}
void TestProcessForRemarks::CheckDump(zxdump::TaskHolder& holder) {
auto find_result = holder.root_job().find(koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
const auto& remarks = read_process.remarks();
EXPECT_EQ(remarks.size(), 5u);
size_t n = 0;
for (const auto& [name, remark] : remarks) {
switch (n++) {
case 0:
EXPECT_EQ(name, kDefaultRemarksName);
EXPECT_EQ(AsString(remark), kTextRemarksData);
break;
case 1:
EXPECT_EQ(name, kTextRemarksName);
EXPECT_EQ(AsString(remark), kTextRemarksData);
break;
case 2:
EXPECT_EQ(name, kBinaryRemarksName);
EXPECT_THAT(remark, ::testing::ElementsAreArray(kBinaryRemarksData));
break;
case 3:
EXPECT_EQ(name, kDefaultJsonRemarksName);
EXPECT_EQ(AsString(remark), kNormalizedJsonRemarksData);
break;
case 4:
EXPECT_EQ(name, kJsonRemarksName);
EXPECT_EQ(AsString(remark), kNormalizedJsonRemarksData);
break;
default:
FAIL() << "too many remarks";
break;
}
}
}
std::string IntsString(cpp20::span<const int> ints) {
std::string str;
for (int i : ints) {
if (!str.empty()) {
str += ',';
}
str += std::to_string(i);
}
return str;
}
void TestProcessForMemory::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
fbl::unique_fd read_pipe;
{
int pipe_fd[2];
ASSERT_EQ(0, pipe(pipe_fd)) << strerror(errno);
read_pipe.reset(pipe_fd[STDIN_FILENO]);
SpawnAction({
.action = FDIO_SPAWN_ACTION_TRANSFER_FD,
.fd = {.local_fd = pipe_fd[STDOUT_FILENO], .target_fd = STDOUT_FILENO},
});
}
const std::array<int, 2> memory_sizes = {
2 * static_cast<int>(zx_system_get_page_size()), /* allocated pages */
static_cast<int>(zx_system_get_page_size()) /* reserved pages */,
};
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild({
"-m",
kMemoryText.data(),
"-M",
IntsString(cpp20::span(kMemoryInts)).c_str(),
"-w",
kMemoryText.data(),
"-p",
IntsString(cpp20::span(memory_sizes)).c_str(),
}));
// The test-child wrote the pointers where the -m text and -M int array
// appear in its memory. Reading these immediately synchronizes with the
// child having started up and progressed far enough to have this memory in
// place before the process gets dumped.
FILE* pipef = fdopen(read_pipe.get(), "r");
ASSERT_TRUE(pipef) << "fdopen: " << read_pipe.get() << strerror(errno);
auto close_pipef = fit::defer([pipef]() { fclose(pipef); });
std::ignore = read_pipe.release();
ASSERT_EQ(4, fscanf(pipef, "%" SCNx64 "\n%" SCNx64 "\n%" SCNx64 "\n%" SCNx64, &text_ptr_,
&ints_ptr_, &wtext_ptr_, &pages_ptr_));
}
void TestProcessForMemory::CheckDump(zxdump::TaskHolder& holder, bool memory_elided) {
auto find_result = holder.root_job().find(koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
{
auto name_result = read_process.get_property<ZX_PROP_NAME>();
ASSERT_TRUE(name_result.is_ok()) << name_result.error_value();
std::string_view name(name_result->data(), name_result->size());
name = name.substr(0, name.find_first_of('\0'));
EXPECT_EQ(name, std::string_view(kChildName));
}
// Basic test.
{
auto memory_result = read_process.read_memory<char>(text_ptr_, kMemoryText.size());
ASSERT_TRUE(memory_result.is_ok())
<< memory_result.error_value() << " reading 0x" << std::hex << text_ptr_;
if (memory_elided) {
EXPECT_TRUE(memory_result->empty()) << " read " << memory_result->size_bytes();
} else {
std::string_view text{(memory_result->data()), memory_result->size()};
ASSERT_EQ(text.size(), kMemoryText.size())
<< " reading 0x" << std::hex << text_ptr_ << " copied at "
<< static_cast<const void*>(text.data());
EXPECT_EQ(text, kMemoryText) //
<< " reading 0x" << std::hex << text_ptr_ << " copied at "
<< static_cast<const void*>(text.data());
}
}
// Test with a non-byte-sized type.
{
auto memory_result = read_process.read_memory<int>(ints_ptr_, kMemoryInts.size());
ASSERT_TRUE(memory_result.is_ok())
<< memory_result.error_value() << " reading 0x" << std::hex << ints_ptr_;
cpp20::span ints = **memory_result;
if (memory_elided) {
EXPECT_TRUE(ints.empty()) << " read " << ints.size_bytes();
} else {
static_assert(std::is_same_v<const int, decltype(ints)::element_type>);
ASSERT_EQ(ints.size(), kMemoryInts.size());
for (size_t i = 0; i < kMemoryInts.size(); ++i) {
EXPECT_EQ(ints[i], kMemoryInts[i]);
}
}
}
// Readahead test.
{
// Only ask to read half the string's actual size, so there will definitely
// be more than that available in the dump.
auto memory_result =
read_process.read_memory<char>(text_ptr_, kMemoryText.size() / 2, ReadMemorySize::kMore);
ASSERT_TRUE(memory_result.is_ok()) << memory_result.error_value();
if (memory_elided) {
EXPECT_TRUE(memory_result->empty()) << " read " << memory_result->size_bytes();
} else {
std::string_view text{(memory_result->data()), memory_result->size()};
// Even if the whole string ended on a page boundary, that much (which we
// know is more than the minimum requested) will be available.
ASSERT_GE(text.size(), kMemoryText.size());
EXPECT_TRUE(cpp20::starts_with(text, kMemoryText));
}
}
// Test a read crossing a page boundary.
auto test_memory_pages = [memory_elided](uint64_t ptr, size_t sample_size,
cpp20::span<const uint8_t> contents) {
if (memory_elided) {
EXPECT_TRUE(contents.empty()) << " read " << contents.size_bytes();
} else {
ASSERT_GE(contents.size(), sample_size);
for (size_t i = 0; i < sample_size; ++i) {
const unsigned int actual = contents[i];
const unsigned int expected = static_cast<uint8_t>(ptr + i);
EXPECT_EQ(actual, expected) << i << " of " << sample_size << " at " << std::hex << ptr + i;
}
}
};
{
constexpr size_t kSampleSize = 20;
ASSERT_TRUE(pages_ptr_ % zx_system_get_page_size() == 0) << std::hex << pages_ptr_;
const uint64_t ptr = pages_ptr_ + zx_system_get_page_size() - (kSampleSize / 2);
auto memory_result = read_process.read_memory<uint8_t>(ptr, kSampleSize);
ASSERT_TRUE(memory_result.is_ok()) << memory_result.error_value();
ASSERT_NO_FATAL_FAILURE(test_memory_pages(ptr, kSampleSize, **memory_result));
if (!memory_elided) {
EXPECT_EQ(memory_result->size(), kSampleSize);
}
}
// Test that reading the non-allocated page returns either an error or zero bytes.
{
constexpr size_t kSampleSize = 20;
ASSERT_TRUE(pages_ptr_ % zx_system_get_page_size() == 0) << std::hex << pages_ptr_;
const uint64_t ptr = pages_ptr_ + 2 * zx_system_get_page_size();
auto memory_result = read_process.read_memory<uint8_t>(ptr, kSampleSize);
if (read_process.is_live()) {
EXPECT_TRUE(memory_result.is_error()) << "Read " << memory_result->size_bytes() << " bytes";
} else {
ASSERT_TRUE(memory_result.is_ok()) << memory_result.error_value();
EXPECT_EQ(memory_result->size(), 0u);
}
}
// Test a read that can return less than requested.
{
constexpr size_t kSampleSize = 20;
const uint64_t ptr = pages_ptr_ + zx_system_get_page_size() - (kSampleSize / 2);
auto memory_result = read_process.read_memory<uint8_t>(ptr, kSampleSize, ReadMemorySize::kLess);
ASSERT_TRUE(memory_result.is_ok()) << memory_result.error_value();
const size_t sample_size = std::min(kSampleSize, memory_result->size());
ASSERT_NO_FATAL_FAILURE(test_memory_pages(ptr, sample_size, **memory_result));
if (!memory_elided) {
if (read_process.is_live()) {
// A live read should have been truncated to keep it in the one page.
EXPECT_EQ(sample_size, kSampleSize / 2);
} else {
// Reading a dump always has all the data if it wasn't elided: if it's
// an mmap'd file, it's all on hand; if it's another kind of dump, the
// data is being copied anyway so there's no benefit to returning less.
EXPECT_EQ(sample_size, kSampleSize);
}
}
}
}
void TestProcessForThreads::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
fbl::unique_fd read_pipe;
{
int pipe_fd[2];
ASSERT_EQ(0, pipe(pipe_fd)) << strerror(errno);
read_pipe.reset(pipe_fd[STDIN_FILENO]);
SpawnAction({
.action = FDIO_SPAWN_ACTION_TRANSFER_FD,
.fd = {.local_fd = pipe_fd[STDOUT_FILENO], .target_fd = STDOUT_FILENO},
});
}
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild({
"-t",
std::to_string(kThreadCount - 1).c_str(),
}));
// The test-child wrote the KOID for each thread. Reading these immediately
// synchronizes with the child having started up and progressed far enough
// to have all the threads launched up place before the process gets dumped.
FILE* pipef = fdopen(read_pipe.get(), "r");
ASSERT_TRUE(pipef) << "fdopen: " << read_pipe.get() << strerror(errno);
auto close_pipef = fit::defer([pipef]() { fclose(pipef); });
std::ignore = read_pipe.release();
for (zx_koid_t& koid : thread_koids_) {
// scanf needs readahead and the child will hang after writing so don't
// match the trailing \n explicitly; once it terminates each line it will
// be implicitly skipped before the next as the leading space matches all
// whitespace. But the final \n will be just seen in the readahead and not
// cause scanf to try to read any more from the pipe, which won't have any.
ASSERT_EQ(1, fscanf(pipef, " %" SCNu64, &koid));
}
}
void TestProcessForThreads::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {
auto result = dump.SuspendAndCollectThreads();
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
void TestProcessForThreads::CheckDump(zxdump::TaskHolder& holder) {
auto find_result = holder.root_job().find(koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
auto list_result = read_process.get_info<ZX_INFO_PROCESS_THREADS>();
ASSERT_TRUE(list_result.is_ok()) << list_result.error_value();
EXPECT_THAT(*list_result, UnorderedElementsAreArray(thread_koids()));
// Test get_child.
std::map<zx_koid_t, zxdump::Object*> objects;
std::map<zx_koid_t, zxdump::Thread*> threads;
for (zx_koid_t koid : thread_koids()) {
auto child_result = read_process.get_child(koid);
ASSERT_TRUE(child_result.is_ok())
<< read_process.koid() << ".get_child(" << koid << ") -> " << child_result.error_value();
zxdump::Object& child = *child_result;
objects.emplace(koid, &child);
ASSERT_EQ(child.type(), ZX_OBJ_TYPE_THREAD);
zxdump::Thread& thread = static_cast<zxdump::Thread&>(child);
threads.emplace(koid, &thread);
EXPECT_EQ(thread.koid(), koid);
}
ASSERT_EQ(objects.size(), kThreadCount);
ASSERT_EQ(threads.size(), kThreadCount);
// Test find.
for (auto [koid, object] : objects) {
auto find_result = read_process.find(koid);
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
zxdump::Object& child = *find_result;
EXPECT_EQ(object, &child);
EXPECT_EQ(child.type(), ZX_OBJ_TYPE_THREAD);
EXPECT_EQ(child.koid(), koid);
}
// Test threads().
auto threads_result = read_process.threads();
ASSERT_TRUE(threads_result.is_ok()) << threads_result.error_value();
EXPECT_EQ(threads_result->get().size(), threads.size());
for (auto& [koid, thread] : threads_result->get()) {
EXPECT_THAT(threads, Contains(FieldsAre(koid, &thread)));
}
}
void TestProcessForThreadState::StartChild() {
SpawnAction({
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name = {kChildName},
});
fbl::unique_fd read_pipe;
{
int pipe_fd[2];
ASSERT_EQ(0, pipe(pipe_fd)) << strerror(errno);
read_pipe.reset(pipe_fd[STDIN_FILENO]);
SpawnAction({
.action = FDIO_SPAWN_ACTION_TRANSFER_FD,
.fd = {.local_fd = pipe_fd[STDOUT_FILENO], .target_fd = STDOUT_FILENO},
});
}
ASSERT_NO_FATAL_FAILURE(TestProcess::StartChild({
"-t",
std::to_string(kThreadCount - 1).c_str(),
"-C",
std::to_string(kRegisterValue).c_str(),
}));
// The test-child wrote the KOID for each thread. Reading these immediately
// synchronizes with the child having started up and progressed far enough to
// have all the threads launched and crashed before the process gets dumped.
FILE* pipef = fdopen(read_pipe.get(), "r");
ASSERT_TRUE(pipef) << "fdopen: " << read_pipe.get() << strerror(errno);
auto close_pipef = fit::defer([pipef]() { fclose(pipef); });
std::ignore = read_pipe.release();
for (zx_koid_t& koid : thread_koids_) {
// scanf needs readahead and the child will hang after writing so don't
// match the trailing \n explicitly; once it terminates each line it will
// be implicitly skipped before the next as the leading space matches all
// whitespace. But the final \n will be just seen in the readahead and not
// cause scanf to try to read any more from the pipe, which won't have any.
ASSERT_EQ(1, fscanf(pipef, " %" SCNu64, &koid));
}
}
void TestProcessForThreadState::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {
auto result = dump.SuspendAndCollectThreads();
EXPECT_TRUE(result.is_ok()) << result.error_value();
}
void TestProcessForThreadState::CheckDump(zxdump::TaskHolder& holder) {
auto find_result = holder.root_job().find(koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
auto list_result = read_process.get_info<ZX_INFO_PROCESS_THREADS>();
ASSERT_TRUE(list_result.is_ok()) << list_result.error_value();
EXPECT_THAT(*list_result, UnorderedElementsAreArray(thread_koids()));
// This has an overload for each machine's general-registers type.
// Checking the dump reads the right one for the dump's machine.
struct GetCrashRegister {
constexpr uint64_t operator()(const zx_arm64_thread_state_general_regs_t& regs) const {
return regs.r[0];
}
constexpr uint64_t operator()(const zx_riscv64_thread_state_general_regs_t& regs) const {
return regs.a0;
}
constexpr uint64_t operator()(const zx_x86_64_thread_state_general_regs_t& regs) const {
return regs.rax;
}
};
// This takes the result of zxdump::Thread::read_state<RegsType>.
auto check_crash_register = [](auto result) {
ASSERT_TRUE(result.is_ok()) << result.error_value();
EXPECT_EQ(GetCrashRegister{}(*result), kRegisterValue);
};
auto threads_result = read_process.threads();
ASSERT_TRUE(threads_result.is_ok()) << threads_result.error_value();
for (auto& [koid, thread] : threads_result->get()) {
// The first KOID printed is the main thread, which doesn't crash.
// So skip that one.
if (koid != thread_koids().front()) {
switch (read_process.dump_machine()) {
case elfldltl::ElfMachine::kAarch64:
check_crash_register(thread.read_state<zx_arm64_thread_state_general_regs_t>());
break;
case elfldltl::ElfMachine::kRiscv:
check_crash_register(thread.read_state<zx_riscv64_thread_state_general_regs_t>());
break;
case elfldltl::ElfMachine::kX86_64:
check_crash_register(thread.read_state<zx_x86_64_thread_state_general_regs_t>());
break;
default:
FAIL() << "unsupported machine " << static_cast<uint32_t>(read_process.dump_machine());
}
}
}
}
namespace {
TEST(ZxdumpTests, ProcessDumpBasic) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcess process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
zxdump::TaskHolder dump_holder;
zx::process process_dup;
zx_status_t status = process.process().duplicate(ZX_RIGHT_SAME_RIGHTS, &process_dup);
ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status);
auto insert_result = dump_holder.Insert(std::move(process_dup));
ASSERT_TRUE(insert_result.is_ok()) << insert_result.error_value();
zxdump::Object& inserted_object = *insert_result;
EXPECT_EQ(inserted_object.type(), ZX_OBJ_TYPE_PROCESS);
zxdump::ProcessDump dump(static_cast<zxdump::Process&>(inserted_object));
auto collect_result = dump.CollectProcess(TestProcess::PruneAllMemory);
ASSERT_TRUE(collect_result.is_ok()) << collect_result.error_value();
auto dump_result = dump.DumpHeaders(writer.AccumulateFragmentsCallback());
ASSERT_TRUE(dump_result.is_ok()) << dump_result.error_value();
auto write_result = writer.WriteFragments();
ASSERT_TRUE(write_result.is_ok()) << write_result.error_value();
const size_t bytes_written = write_result.value();
auto memory_result = dump.DumpMemory(writer.WriteCallback());
ASSERT_TRUE(memory_result.is_ok()) << memory_result.error_value();
const size_t total_with_memory = memory_result.value();
// We pruned all memory, so DumpMemory should not have added any output.
EXPECT_EQ(bytes_written, total_with_memory);
// Now read the file back in.
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
// The dump has no jobs, so there should be a placeholder "super-root".
EXPECT_EQ(ZX_KOID_INVALID, holder.root_job().koid());
auto processes = holder.root_job().processes();
ASSERT_TRUE(processes.is_ok()) << processes.error_value();
// The fake job should have exactly one process.
EXPECT_EQ(processes->get().size(), 1u);
for (auto& [read_koid, read_process] : processes->get()) {
EXPECT_NE(read_koid, ZX_KOID_INVALID);
// Get the basic info from the real live process handle.
zx_info_handle_basic_t basic;
ASSERT_EQ(ZX_OK, process.borrow()->get_info(ZX_INFO_HANDLE_BASIC, &basic, sizeof(basic),
nullptr, nullptr));
EXPECT_EQ(read_koid, basic.koid);
EXPECT_EQ(ZX_OBJ_TYPE_PROCESS, basic.type);
// Get the same info from the dump and verify they match up. Note that the
// zx_info_handle_basic_t::rights in the dump is not usually particularly
// meaningful about the dumped process, because it's just whatever rights
// the dumper's own process handle had. But in this case it does exactly
// match the handle we just checked, since that's what we used to dump.
auto read_basic = read_process.get_info<ZX_INFO_HANDLE_BASIC>();
ASSERT_TRUE(read_basic.is_ok()) << read_basic.error_value();
EXPECT_EQ(basic.koid, read_basic->koid);
EXPECT_EQ(basic.rights, read_basic->rights);
EXPECT_EQ(basic.type, read_basic->type);
EXPECT_EQ(basic.related_koid, read_basic->related_koid);
}
}
TEST(ZxdumpTests, ProcessDumpPropertiesAndInfo) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForPropertiesAndInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder, false));
}
TEST(ZxdumpTests, ProcessDumpToZstdFile) {
constexpr std::string_view kName = "zstd-process-dump-test";
// We'll verify the data written to the file by decompressing it with the
// zstd tool and reading in the resulting uncompressed file.
zxdump::testing::TestToolProcess zstd;
ASSERT_NO_FATAL_FAILURE(zstd.Init());
// Set up the writer to send the compressed data to a temporary file.
zxdump::testing::TestToolProcess::File& zstd_file =
zstd.MakeFile(kName, zxdump::testing::TestToolProcess::File::kZstdSuffix);
zxdump::ZstdWriter writer(zstd_file.CreateInput());
TestProcessForPropertiesAndInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
// Complete the compressed stream.
auto finish = writer.Finish();
ASSERT_TRUE(finish.is_ok()) << finish.error_value();
// Decompress the file using the tool.
zxdump::testing::TestToolProcess::File& plain_file = zstd.MakeFile(kName);
std::vector<std::string> args({
"-d"s,
"-q"s,
zstd_file.name(),
"-o"s,
plain_file.name(),
});
ASSERT_NO_FATAL_FAILURE(zstd.Start("zstd"s, args));
ASSERT_NO_FATAL_FAILURE(zstd.CollectStdout());
ASSERT_NO_FATAL_FAILURE(zstd.CollectStderr());
int exit_status;
ASSERT_NO_FATAL_FAILURE(zstd.Finish(exit_status));
EXPECT_EQ(exit_status, EXIT_SUCCESS);
// The zstd tool would complain about a malformed file.
EXPECT_EQ(zstd.collected_stderr(), "");
EXPECT_EQ(zstd.collected_stdout(), "");
// Now read in the uncompressed file and check its contents.
zxdump::TaskHolder holder;
auto read_result = holder.Insert(plain_file.OpenOutput());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder, false));
}
TEST(ZxdumpTests, ProcessDumpToZstdPipe) {
// We'll verify the data by piping it directly to the zstd tool to decompress
// as a filter with pipes on both ends, reading from that pipe.
zxdump::testing::TestToolProcess zstd;
ASSERT_NO_FATAL_FAILURE(zstd.Init());
std::vector<std::string> args({"-d"s});
ASSERT_NO_FATAL_FAILURE(zstd.Start("zstd"s, args));
ASSERT_NO_FATAL_FAILURE(zstd.CollectStderr());
TestProcessForPropertiesAndInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
{
// Set up the writer to send the compressed data to the tool.
zxdump::ZstdWriter writer(std::move(zstd.tool_stdin()));
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
// Complete the compressed stream.
auto finish = writer.Finish();
ASSERT_TRUE(finish.is_ok()) << finish.error_value();
// The write side of the pipe is closed when the writer goes out of scope,
// so the decompressor can finish.
}
// Now read in the uncompressed dump stream and check its contents.
zxdump::TaskHolder holder;
auto read_result = holder.Insert(std::move(zstd.tool_stdout()), false);
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder, false));
// The reader should have consumed the all of the tool's stdout by now,
// so it will have been unblocked to finish after its stdin hit EOF when
// the writer's destruction closed the pipe.
int exit_status;
ASSERT_NO_FATAL_FAILURE(zstd.Finish(exit_status));
EXPECT_EQ(exit_status, EXIT_SUCCESS);
// The zstd tool would complain about a malformed stream.
EXPECT_EQ(zstd.collected_stderr(), "");
}
TEST(ZxdumpTests, ProcessDumpSystemInfo) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForSystemInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder));
}
// TODO(mcgrathr): test job archives with system info, nested repeats
TEST(ZxdumpTests, ProcessDumpKernelInfo) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForKernelInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder));
}
// TODO(mcgrathr): test job archives with kernel info, nested repeats
TEST(ZxdumpTests, ProcessDumpNoDate) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForPropertiesAndInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
auto find_result = holder.root_job().find(process.koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
// By default no date was recorded.
EXPECT_EQ(read_process.date(), kNoDate);
}
TEST(ZxdumpTests, ProcessDumpDate) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForPropertiesAndInfo process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
constexpr auto precollect = [](zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) {
dump.set_date(kTestDate);
};
ASSERT_NO_FATAL_FAILURE(process.Dump(writer, precollect));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
auto find_result = holder.root_job().find(process.koid());
ASSERT_TRUE(find_result.is_ok()) << find_result.error_value();
ASSERT_EQ(find_result->get().type(), ZX_OBJ_TYPE_PROCESS);
zxdump::Process& read_process = static_cast<zxdump::Process&>(find_result->get());
EXPECT_EQ(read_process.date(), kTestDate);
}
// TODO(mcgrathr): test job archives w/&w/o dates
TEST(ZxdumpTests, ProcessDumpRemarks) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForRemarks process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder));
}
// TODO(mcgrathr): test job archives with remarks, nested repeats
TEST(ZxdumpTests, ProcessDumpMemory) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForMemory process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder));
}
TEST(ZxdumpTests, ProcessDumpThreads) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForThreads process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder));
}
TEST(ZxdumpTests, ProcessDumpThreadState) {
TestFile file;
zxdump::FdWriter writer(file.RewoundFd());
TestProcessForThreadState process;
ASSERT_NO_FATAL_FAILURE(process.StartChild());
ASSERT_NO_FATAL_FAILURE(process.Dump(writer));
zxdump::TaskHolder holder;
auto read_result = holder.Insert(file.RewoundFd());
ASSERT_TRUE(read_result.is_ok()) << read_result.error_value();
ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder));
}
} // namespace
} // namespace zxdump::testing