| // 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 info resource, since we'll need it to dump. |
| auto info_result = zxdump::GetInfoResource(); |
| EXPECT_TRUE(info_result.is_ok()) << info_result.error_value(); |
| info_resource_ = *std::move(info_result); |
| } |
| |
| void TestProcessForKernelInfo::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) { |
| zxdump::LiveHandle info_resource_copy; |
| EXPECT_EQ(ZX_OK, info_resource().duplicate(ZX_RIGHT_SAME_RIGHTS, &info_resource_copy)); |
| auto insert_result = holder.Insert(std::move(info_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& info = holder.info_resource(); |
| EXPECT_NE(info.koid(), ZX_KOID_INVALID); |
| EXPECT_EQ(info.type(), ZX_OBJ_TYPE_RESOURCE); |
| |
| KernelData dump_data, live_data; |
| |
| { |
| // Use a fresh holder to populate the live data. It can consume the info |
| // resource handle we used in Precollect, since we've already dumped and |
| // don't need it any more. |
| zxdump::TaskHolder live_holder; |
| auto live_info = live_holder.Insert(std::move(info_resource_)); |
| ASSERT_TRUE(live_info.is_ok()) << live_info.error_value(); |
| ASSERT_NO_FATAL_FAILURE(live_data.Fill(*live_info)); |
| } |
| |
| // Fetch all the data from the dump. |
| ASSERT_NO_FATAL_FAILURE(dump_data.Fill(info)); |
| |
| // 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 |