| // 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 <lib/fit/defer.h> |
| #include <lib/stdcompat/string_view.h> |
| #include <lib/zxdump/task.h> |
| #include <sys/mman.h> |
| #include <sys/stat.h> |
| #include <zircon/assert.h> |
| #include <zircon/syscalls/object.h> |
| |
| #include <algorithm> |
| #include <charconv> |
| #include <forward_list> |
| #include <variant> |
| |
| #include <rapidjson/document.h> |
| |
| #include "core.h" |
| #include "dump-file.h" |
| #include "job-archive.h" |
| #include "rights.h" |
| |
| #ifdef __Fuchsia__ |
| #include <lib/zx/job.h> |
| #include <lib/zx/process.h> |
| #endif |
| |
| namespace zxdump { |
| |
| using namespace internal; |
| |
| namespace { |
| |
| using namespace std::literals; |
| |
| // The result of parsing an archive member header. The name view may point |
| // into the original header buffer, so this must live no longer than that. |
| struct MemberHeader { |
| std::string_view name; |
| time_t date; |
| size_t size; |
| }; |
| |
| std::string_view TrimSpaces(std::string_view string) { |
| auto pos = string.find_last_not_of(' '); |
| if (pos == std::string_view::npos) { |
| return {}; |
| } |
| return string.substr(0, pos + 1); |
| } |
| |
| template <typename T> |
| bool ParseHeaderInteger(std::string_view field, T& value) { |
| field = TrimSpaces(field); |
| if (field.empty()) { |
| // Some special members can have wholly blank integer fields and that's OK. |
| value = 0; |
| return true; |
| } |
| const char* first = field.data(); |
| const char* last = first + field.size(); |
| auto result = std::from_chars(first, last, value); |
| return result.ptr == last && result.ec != std::errc::result_out_of_range; |
| } |
| |
| // Parse the basic archive header. The name may need additional decoding. |
| fit::result<Error, MemberHeader> ParseArchiveHeader(ByteView header) { |
| if (header.size() < sizeof(ar_hdr)) { |
| return fit::error(Error{"truncated archive", ZX_ERR_OUT_OF_RANGE}); |
| } |
| static_assert(alignof(ar_hdr) == 1); |
| auto ar = reinterpret_cast<const ar_hdr*>(header.data()); |
| if (!ar->valid()) { |
| return CorruptedDump(); |
| } |
| MemberHeader member{TrimSpaces({ar->ar_name, sizeof(ar->ar_name)}), 0, 0}; |
| if (!ParseHeaderInteger({ar->ar_date, sizeof(ar->ar_date)}, member.date) || |
| !ParseHeaderInteger({ar->ar_size, sizeof(ar->ar_size)}, member.size)) { |
| return CorruptedDump(); |
| } |
| return fit::ok(member); |
| } |
| |
| // Update member.name if it's an encoded reference to the long name table. |
| bool HandleLongName(std::string_view name_table, MemberHeader& member) { |
| if (member.name.substr(0, ar_hdr::kLongNamePrefix.size()) == ar_hdr::kLongNamePrefix) { |
| size_t name_offset = std::string_view::npos; |
| if (!ParseHeaderInteger(member.name.substr(ar_hdr::kLongNamePrefix.size()), name_offset)) { |
| return false; |
| } |
| member.name = name_table.substr(name_offset); |
| size_t end = member.name.find(ar_hdr::kNameTableTerminator); |
| if (end == 0 || end == std::string_view::npos) { |
| return false; |
| } |
| member.name = member.name.substr(0, end); |
| } |
| return true; |
| } |
| |
| // If name starts with match, then parse it as a note and store it in the map. |
| // The successful return value is false if the name didn't match or true if it |
| // was a valid note that wasn't already in the map. |
| template <typename Key> |
| fit::result<Error, std::optional<Key>> JobNoteName(std::string_view match, std::string_view name) { |
| if (name.size() > match.size() && name[match.size()] == '.' && cpp20::starts_with(name, match)) { |
| name.remove_prefix(match.size() + 1); |
| if (name.empty()) { |
| return CorruptedDump(); |
| } |
| Key key = 0; |
| if (ParseHeaderInteger(name, key)) { |
| return fit::ok(key); |
| } |
| } |
| return fit::ok(std::nullopt); |
| } |
| |
| // Add a note to an info_ or properties_ map. Duplicates are not allowed. |
| template <typename Key> |
| fit::result<Error> AddNote(std::map<Key, ByteView>& map, Key key, ByteView data) { |
| auto [it, unique] = map.insert({key, data}); |
| if (!unique) { |
| return fit::error(Error{ |
| "duplicate note name in dump", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| return fit::ok(); |
| } |
| |
| // rapidjson's built-in features require NUL-terminated strings. |
| // Modeled on rapidjson::StringBuffer from <rapidjson/stringbuffer.h>. |
| class StringViewStream { |
| public: |
| using Ch = char; |
| |
| explicit StringViewStream(std::string_view data) : data_(data) {} |
| |
| Ch Peek() const { return data_.empty() ? '\0' : data_.front(); } |
| |
| Ch Take() { |
| Ch c = data_.front(); |
| data_.remove_prefix(1); |
| return c; |
| } |
| |
| size_t Tell() const { return data_.size(); } |
| |
| Ch* PutBegin() { |
| RAPIDJSON_ASSERT(false); |
| return 0; |
| } |
| void Put(Ch) { RAPIDJSON_ASSERT(false); } |
| void Flush() { RAPIDJSON_ASSERT(false); } |
| size_t PutEnd(Ch*) { |
| RAPIDJSON_ASSERT(false); |
| return 0; |
| } |
| |
| private: |
| std::string_view data_; |
| }; |
| |
| constexpr Error kTaskNotFound{"task KOID not found", ZX_ERR_NOT_FOUND}; |
| |
| #ifdef __Fuchsia__ |
| using LiveJob = zx::job; |
| using LiveProcess = zx::process; |
| #else |
| using LiveJob = LiveTask; |
| using LiveProcess = LiveTask; |
| #endif |
| |
| using InsertChild = std::variant<std::monostate, Job, Process, Thread>; |
| |
| } // namespace |
| |
| // This is the real guts of the zxdump::TaskHolder class. |
| class TaskHolder::JobTree { |
| public: |
| Job& root_job() const { return root_job_; } |
| |
| // Insert any number of dumps by reading a core file or an archive. |
| fit::result<Error> Insert(fbl::unique_fd fd, bool read_memory) { |
| if (auto result = DumpFile::Open(std::move(fd)); result.is_error()) { |
| return result.take_error(); |
| } else { |
| dumps_.push_front(std::move(result).value()); |
| } |
| auto& file = *dumps_.front(); |
| auto result = Read(file, read_memory, {0, file.size()}); |
| if (!read_memory) { |
| file.shrink_to_fit(); |
| } |
| if (file.size() == 0) { |
| dumps_.pop_front(); |
| } |
| Reroot(); |
| return result; |
| } |
| |
| // Insert a live task. |
| auto Insert(LiveTask live, InsertChild* parent) |
| -> fit::result<Error, std::reference_wrapper<Task>> { |
| zx_info_handle_basic_t info; |
| if (zx_status_t status = |
| live.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); |
| status != ZX_OK) { |
| return fit::error(Error{"invalid live task", status}); |
| } |
| |
| // Place the basic info into a new Task object now that it's known valid. |
| // Everything relies on the basic info always being available in the map. |
| auto ingest = [&](auto attach, auto task) // |
| -> fit::result<Error, std::reference_wrapper<Task>> { |
| task.date_ = time(nullptr); // Time of first data sample from this task. |
| auto buffer = GetBuffer(sizeof(info)); |
| memcpy(buffer, &info, sizeof(info)); |
| task.info_.emplace(ZX_INFO_HANDLE_BASIC, ByteView{buffer, sizeof(info)}); |
| if (parent) { |
| *parent = std::move(task); |
| return fit::ok(std::ref(std::get<decltype(task)>(*parent))); |
| } |
| return (this->*attach)(std::move(task)); |
| }; |
| |
| switch (info.type) { |
| case ZX_OBJ_TYPE_JOB: |
| return ingest(&JobTree::AttachJob, Job{*this, std::move(live)}); |
| case ZX_OBJ_TYPE_PROCESS: |
| return ingest(&JobTree::AttachProcess, Process{*this, std::move(live)}); |
| case ZX_OBJ_TYPE_THREAD: |
| if (parent) { |
| return ingest(static_cast<fit::error<Error> (JobTree::*)(Thread)>(nullptr), |
| Thread{*this, std::move(live)}); |
| } |
| [[fallthrough]]; |
| default: |
| return fit::error(Error{"not a valid job or process handle", ZX_ERR_BAD_HANDLE}); |
| } |
| } |
| |
| void AssertIsSuperroot(Task& task) { ZX_DEBUG_ASSERT(&task == &superroot_); } |
| |
| // Unlike generic get_info, the view is always fully aligned for casting. |
| fit::result<Error, ByteView> GetSuperrootInfo(zx_object_info_topic_t topic) { |
| switch (topic) { |
| case ZX_INFO_JOB_CHILDREN: |
| if (!superroot_info_children_) { |
| // No value cached. |
| zx_koid_t* p = new zx_koid_t[superroot_.children_.size()]; |
| superroot_info_children_.reset(p); |
| for (const auto& [koid, job] : superroot_.children_) { |
| *p++ = koid; |
| } |
| } |
| return fit::ok(ByteView{ |
| reinterpret_cast<const std::byte*>(superroot_info_children_.get()), |
| superroot_.children()->get().size(), |
| }); |
| |
| case ZX_INFO_JOB_PROCESSES: |
| if (!superroot_info_processes_) { |
| // No value cached. |
| zx_koid_t* p = new zx_koid_t[superroot_.processes_.size()]; |
| superroot_info_processes_.reset(p); |
| for (const auto& [koid, job] : superroot_.processes_) { |
| *p++ = koid; |
| } |
| } |
| return fit::ok(ByteView{reinterpret_cast<const std::byte*>(superroot_info_processes_.get()), |
| superroot_.processes()->get().size()}); |
| |
| default: |
| return fit::error(Error{"fake root job info", ZX_ERR_NOT_SUPPORTED}); |
| } |
| } |
| |
| // Allocate a buffer saved for the life of this holder. |
| std::byte* GetBuffer(size_t size) { |
| std::byte* buffer = new std::byte[size]; |
| buffers_.emplace_front(buffer); |
| return buffer; |
| } |
| |
| void TakeBuffer(std::unique_ptr<std::byte[]> owned_buffer) { |
| buffers_.push_front(std::move(owned_buffer)); |
| } |
| |
| template <typename T> |
| T GetSystemData(const char* key) const; |
| |
| private: |
| // This is the actual reader, implemented below. |
| fit::result<Error> Read(DumpFile& file, bool read_memory, FileRange where, time_t date = 0); |
| fit::result<Error> ReadElf(DumpFile& file, FileRange where, time_t date, ByteView header, |
| bool read_memory); |
| fit::result<Error> ReadArchive(DumpFile& file, FileRange archive, ByteView header, |
| bool read_memory); |
| |
| fit::result<Error> ReadSystemNote(ByteView data); |
| const rapidjson::Value* GetSystemJsonData(const char* key) const; |
| |
| // Snap the root job pointer to the sole job or back to the superroot. |
| // Also clear the cached get_info lists so they'll be regenerated on demand. |
| void Reroot() { |
| if (superroot_.processes_.empty() && superroot_.children_.size() == 1) { |
| auto& [koid, job] = *superroot_.children_.begin(); |
| root_job_ = std::ref(job); |
| } else { |
| root_job_ = std::ref(superroot_); |
| } |
| superroot_info_children_.reset(); |
| superroot_info_processes_.reset(); |
| } |
| |
| fit::result<Error, std::reference_wrapper<Job>> AttachJob(Job&& job) { |
| // See if any of the orphan jobs are this job's children. |
| // If a child job is found in the superroot, claim it. |
| if (!superroot_.children_.empty()) { |
| auto result = job.get_info<ZX_INFO_JOB_CHILDREN>(); |
| if (result.is_ok()) { |
| for (zx_koid_t koid : result.value()) { |
| auto it = superroot_.children_.find(koid); |
| if (it != superroot_.children_.end()) { |
| superroot_info_children_.reset(); // Clear stale cache. |
| auto [job_it, unique] = job.children_.insert(std::move(*it)); |
| superroot_.children_.erase(it); |
| if (!unique) { |
| return fit::error(Error{ |
| "duplicate job KOID", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| } |
| } |
| } |
| } |
| |
| // See if any of the orphaned processes belong to this job. |
| // If a process is found in the superroot, claim it. |
| if (!superroot_.processes_.empty()) { |
| auto result = job.get_info<ZX_INFO_JOB_PROCESSES>(); |
| if (result.is_ok()) { |
| for (zx_koid_t koid : result.value()) { |
| auto it = superroot_.processes_.find(koid); |
| if (it != superroot_.processes_.end()) { |
| superroot_info_processes_.reset(); // Clear stale cache. |
| auto [job_it, unique] = job.processes_.insert(std::move(*it)); |
| superroot_.processes_.erase(it); |
| if (!unique) { |
| return fit::error(Error{ |
| "duplicate process KOID", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| } |
| } |
| } |
| } |
| |
| // Now that it has wrangled its children, find this job's own parent. |
| zx_koid_t koid = job.koid(); |
| if (auto it = missing_.find(koid); it != missing_.end()) { |
| // There is a parent looking for this lost child! |
| auto& [parent_koid, parent] = *it; |
| auto [j, unique] = parent.children_.try_emplace(koid, std::move(job)); |
| ZX_DEBUG_ASSERT(unique); |
| missing_.erase(it); |
| return fit::ok(std::ref(j->second)); |
| } else { |
| // The superroot fosters the orphan until its parent appears (if ever). |
| auto [j, unique] = superroot_.children_.try_emplace(koid, std::move(job)); |
| if (!unique) { |
| return fit::error(Error{ |
| "duplicate job KOID", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| return fit::ok(std::ref(j->second)); |
| } |
| } |
| |
| fit::result<Error, std::reference_wrapper<Process>> AttachProcess(Process&& process) { |
| zx_koid_t koid = process.koid(); |
| if (auto it = missing_.find(koid); it != missing_.end()) { |
| // There is a job looking for this lost process! |
| auto& [job_koid, job] = *it; |
| auto [p, unique] = job.processes_.try_emplace(koid, std::move(process)); |
| ZX_DEBUG_ASSERT(unique); |
| missing_.erase(it); |
| return fit::ok(std::ref(p->second)); |
| } |
| |
| // The superroot holds the process until a job claims it (if ever). |
| auto [it, unique] = superroot_.processes_.try_emplace(koid, std::move(process)); |
| if (!unique) { |
| return fit::error(Error{ |
| "duplicate process KOID", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| |
| return fit::ok(std::ref(it->second)); |
| } |
| |
| std::forward_list<std::unique_ptr<DumpFile>> dumps_; |
| std::forward_list<std::unique_ptr<std::byte[]>> buffers_; |
| |
| rapidjson::Document system_; |
| |
| // The superroot holds all the orphaned jobs and processes that haven't been |
| // claimed by a parent job. |
| Job superroot_{*this}; |
| |
| // This records any dangling child or process KOIDs required by jobs already |
| // in the holder. When a matching task is attached, it goes to that job |
| // instead of the superroot. |
| std::map<zx_koid_t, Job&> missing_; |
| |
| // These are the buffers for the synthetic ZX_INFO_JOB_CHILDREN and |
| // ZX_INFO_JOB_PROCESSES results returned by get_info calls on the superroot. |
| // They are regenerated on demand, and cleared when new tasks are inserted. |
| std::unique_ptr<zx_koid_t[]> superroot_info_children_, superroot_info_processes_; |
| |
| // The root job is either the superroot or its only child. |
| std::reference_wrapper<Job> root_job_{superroot_}; |
| }; |
| |
| // JobTree is an incomplete type outside this translation unit. Some methods |
| // on TaskHolder et al need to access tree_, so they are defined here. |
| |
| TaskHolder::TaskHolder() { tree_ = std::make_unique<JobTree>(); } |
| |
| TaskHolder::~TaskHolder() = default; |
| |
| Job& TaskHolder::root_job() const { return tree_->root_job(); } |
| |
| fit::result<Error> TaskHolder::Insert(fbl::unique_fd fd, bool read_memory) { |
| return tree_->Insert(std::move(fd), read_memory); |
| } |
| |
| fit::result<Error, std::reference_wrapper<Task>> TaskHolder::Insert(LiveTask task) { |
| return tree_->Insert(std::move(task), nullptr); |
| } |
| |
| Task::~Task() = default; |
| |
| Job::~Job() = default; |
| |
| Process::~Process() = default; |
| |
| Thread::~Thread() = default; |
| |
| fit::result<Error, std::reference_wrapper<zxdump::Job::JobMap>> Job::children() { |
| if (children_.empty() && live()) { |
| // The first time called on a live task (or on repeated calls iff the first |
| // time there were no children), populate the whole list. |
| auto result = get_info<ZX_INFO_JOB_CHILDREN>(); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| LiveJob job{std::move(live())}; |
| auto restore = fit::defer([&]() { live() = std::move(job); }); |
| for (zx_koid_t koid : result.value()) { |
| if (koid == ZX_KOID_INVALID) { |
| continue; |
| } |
| LiveTask live_child; |
| zx_status_t status = job.get_child(koid, kChildRights, &live_child); |
| switch (status) { |
| case ZX_OK: |
| break; |
| |
| case ZX_ERR_NOT_FOUND: |
| // It's not an error if the child has simply died already so the |
| // KOID is no longer valid. |
| continue; |
| |
| default: |
| return fit::error(Error{"zx_object_get_child", status}); |
| } |
| |
| InsertChild child; |
| auto result = tree().Insert(std::move(live_child), &child); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| Job& child_job = std::get<Job>(child); |
| ZX_ASSERT(child_job.koid() == koid); |
| [[maybe_unused]] auto [it, unique] = children_.emplace(koid, std::move(child_job)); |
| ZX_DEBUG_ASSERT(unique); |
| } |
| } |
| return fit::ok(std::ref(children_)); |
| } |
| |
| fit::result<Error, std::reference_wrapper<zxdump::Job::ProcessMap>> Job::processes() { |
| if (processes_.empty() && live()) { |
| // The first time called on a live task (or on repeated calls iff the first |
| // time there were no processes), populate the whole list. |
| auto result = get_info<ZX_INFO_JOB_PROCESSES>(); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| LiveJob job{std::move(live())}; |
| auto restore = fit::defer([&]() { live() = std::move(job); }); |
| for (zx_koid_t koid : result.value()) { |
| LiveTask live_process; |
| zx_status_t status = job.get_child(koid, kChildRights, &live_process); |
| switch (status) { |
| case ZX_OK: |
| break; |
| |
| case ZX_ERR_NOT_FOUND: |
| // It's not an error if the process has simply died already so the |
| // KOID is no longer valid. |
| continue; |
| |
| default: |
| return fit::error(Error{"zx_object_get_child", status}); |
| } |
| |
| InsertChild child; |
| auto result = tree().Insert(std::move(live_process), &child); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| Process& process = std::get<Process>(child); |
| ZX_ASSERT(process.koid() == koid); |
| [[maybe_unused]] auto [it, unique] = processes_.emplace(koid, std::move(process)); |
| ZX_DEBUG_ASSERT(unique); |
| } |
| } |
| return fit::ok(std::ref(processes_)); |
| } |
| |
| fit::result<Error, std::reference_wrapper<zxdump::Process::ThreadMap>> Process::threads() { |
| if (threads_.empty() && live()) { |
| // The first time called on a live task (or on repeated calls iff the first |
| // time there were no processes), populate the whole list. |
| auto result = get_info<ZX_INFO_PROCESS_THREADS>(); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| LiveProcess process{std::move(live())}; |
| auto restore = fit::defer([&]() { live() = std::move(process); }); |
| for (zx_koid_t koid : result.value()) { |
| LiveTask live_thread; |
| zx_status_t status = process.get_child(koid, kChildRights, &live_thread); |
| switch (status) { |
| case ZX_OK: |
| break; |
| |
| case ZX_ERR_NOT_FOUND: |
| // It's not an error if the thread has simply died already so the |
| // KOID is no longer valid. |
| continue; |
| |
| default: |
| return fit::error(Error{"zx_object_get_child", status}); |
| } |
| |
| InsertChild child; |
| auto result = tree().Insert(std::move(live_thread), &child); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| |
| Thread& thread = std::get<Thread>(child); |
| ZX_ASSERT(thread.koid() == koid); |
| [[maybe_unused]] auto [it, unique] = threads_.emplace(koid, std::move(thread)); |
| ZX_DEBUG_ASSERT(unique); |
| } |
| } |
| |
| return fit::ok(std::ref(threads_)); |
| } |
| |
| fit::result<Error, std::reference_wrapper<Task>> Task::find(zx_koid_t match) { |
| if (koid() == match) { |
| return fit::ok(std::ref(*this)); |
| } |
| switch (this->type()) { |
| case ZX_OBJ_TYPE_JOB: |
| return static_cast<Job*>(this)->find(match); |
| case ZX_OBJ_TYPE_PROCESS: |
| return static_cast<Process*>(this)->find(match); |
| } |
| return fit::error{kTaskNotFound}; |
| } |
| |
| fit::result<Error, std::reference_wrapper<Task>> Job::find(zx_koid_t match) { |
| if (koid() == match) { |
| return fit::ok(std::ref(*this)); |
| } |
| |
| // First check our immediate child tasks. |
| if (auto it = children_.find(match); it != children_.end()) { |
| return fit::ok(std::ref(it->second)); |
| } |
| if (auto it = processes_.find(match); it != processes_.end()) { |
| return fit::ok(std::ref(it->second)); |
| } |
| |
| if (live()) { |
| // Those maps aren't populated eagerly for live tasks. |
| // Instead, just query the kernel for this one KOID first. |
| LiveTask live_child; |
| |
| // zx::handle doesn't permit get_child, so momentarily move the live() |
| // handle to a zx::job. On non-Fuchsia, it's all still no-ops. |
| LiveJob job{std::move(live())}; |
| zx_status_t status = job.get_child(match, kChildRights, &live_child); |
| live() = std::move(job); |
| |
| if (status == ZX_OK) { |
| // This is a child of ours, just not inserted yet. |
| InsertChild child; |
| auto result = tree().Insert(std::move(live_child), &child); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| |
| if (auto job = std::get_if<Job>(&child)) { |
| ZX_ASSERT(job->koid() == match); |
| auto [it, unique] = children_.emplace(match, std::move(*job)); |
| ZX_DEBUG_ASSERT(unique); |
| return fit::ok(std::ref(it->second)); |
| } |
| |
| auto& process = std::get<Process>(child); |
| ZX_ASSERT(process.koid() == match); |
| auto [it, unique] = processes_.emplace(match, std::move(process)); |
| ZX_DEBUG_ASSERT(unique); |
| return fit::ok(std::ref(it->second)); |
| } |
| } |
| |
| // For a live job, children() actively fills the children_ list. |
| if (auto result = children(); result.is_error()) { |
| return result.take_error(); |
| } |
| |
| // Recurse on the child jobs. |
| for (auto& [koid, job] : children_) { |
| auto result = job.find(match); |
| if (result.is_ok()) { |
| return result; |
| } |
| } |
| |
| // For a live job, processes() actively fills the processes_ list. |
| if (auto result = processes(); result.is_error()) { |
| return result.take_error(); |
| } |
| |
| // Recurse on the child processes. |
| for (auto& [koid, process] : processes_) { |
| auto result = process.find(match); |
| if (result.is_ok()) { |
| return result; |
| } |
| } |
| |
| return fit::error{kTaskNotFound}; |
| } |
| |
| fit::result<Error, std::reference_wrapper<Task>> Process::find(zx_koid_t match) { |
| if (koid() == match) { |
| return fit::ok(std::ref(*this)); |
| } |
| if (auto it = threads_.find(match); it != threads_.end()) { |
| return fit::ok(std::ref(it->second)); |
| } |
| return fit::error{kTaskNotFound}; |
| } |
| |
| std::byte* Task::GetBuffer(size_t size) { return tree().GetBuffer(size); } |
| |
| void Task::TakeBuffer(std::unique_ptr<std::byte[]> buffer) { tree().TakeBuffer(std::move(buffer)); } |
| |
| fit::result<Error, ByteView> Task::GetSuperrootInfo(zx_object_info_topic_t topic) { |
| tree_.get().AssertIsSuperroot(*this); |
| return tree_.get().GetSuperrootInfo(topic); |
| } |
| |
| fit::result<Error, ByteView> Task::get_info_aligned( // |
| zx_object_info_topic_t topic, size_t record_size, size_t align) { |
| ByteView bytes; |
| if (auto result = get_info(topic, record_size); result.is_error()) { |
| return result.take_error(); |
| } else { |
| bytes = result.value(); |
| } |
| |
| void* ptr = const_cast<void*>(static_cast<const void*>(bytes.data())); |
| size_t space = bytes.size(); |
| if (std::align(align, space, ptr, space)) { |
| // It's already aligned. |
| return fit::ok(bytes); |
| } |
| |
| // Allocate a buffer with alignment slop and make the holder hold onto it. |
| space = bytes.size() + align - 1; |
| ptr = tree_.get().GetBuffer(space); |
| |
| // Copy the data into the buffer with the right alignment. |
| void* aligned_ptr = std::align(align, bytes.size(), ptr, space); |
| memcpy(aligned_ptr, bytes.data(), bytes.size()); |
| |
| // Return the aligned data in the buffer now held in the holder and replace |
| // the cached data with the aligned copy for the next lookup to find. |
| ByteView copy{static_cast<std::byte*>(aligned_ptr), bytes.size()}; |
| info_[topic] = copy; |
| return fit::ok(copy); |
| } |
| |
| fit::result<Error> TaskHolder::JobTree::Read(DumpFile& real_file, bool read_memory, FileRange where, |
| time_t date) { |
| // If the file is compressed, this will iterate with the decompressed file. |
| for (DumpFile* file = &real_file; where.size >= kHeaderProbeSize; |
| // Read the whole uncompressed file as a stream. Its size is unknown. |
| where = FileRange::Unbounded()) { |
| ByteView header; |
| if (auto result = file->ReadEphemeral(where / kHeaderProbeSize); result.is_error()) { |
| return result.take_error(); |
| } else { |
| header = result.value(); |
| } |
| |
| if (uint32_t word; memcpy(&word, header.data(), sizeof(word)), word == Elf::Ehdr::kMagic) { |
| return ReadElf(*file, where, date, header, read_memory); |
| } |
| |
| std::string_view header_string{ |
| reinterpret_cast<const char*>(header.data()), |
| header.size(), |
| }; |
| if (cpp20::starts_with(header_string, kArchiveMagic)) { |
| return ReadArchive(*file, where, header, read_memory); |
| } |
| |
| // If it's not a compressed file, we don't grok it. |
| if (!DumpFile::IsCompressed(header)) { |
| break; |
| } |
| |
| // Start streaming decompression to deliver the uncompressed dump file. |
| // Then iterate to read that (streaming) file. |
| auto result = file->Decompress(where, header); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| file = result.value().get(); |
| dumps_.push_front(std::move(result).value()); |
| } |
| return fit::error(Error{"not an ELF or archive file", ZX_ERR_NOT_FILE}); |
| } |
| |
| fit::result<Error> TaskHolder::JobTree::ReadElf(DumpFile& file, FileRange where, time_t date, |
| ByteView header, bool read_memory) { |
| Elf::Ehdr ehdr; |
| if (header.size() < sizeof(ehdr)) { |
| return TruncatedDump(); |
| } |
| memcpy(&ehdr, header.data(), sizeof(ehdr)); |
| if (!ehdr.Valid() || ehdr.phentsize() != sizeof(Elf::Phdr) || |
| ehdr.type != elfldltl::ElfType::kCore) { |
| return fit::error(Error{"ELF file is not a Zircon core dump", ZX_ERR_IO_DATA_INTEGRITY}); |
| } |
| |
| // Get the count of program headers. Large counts use a special encoding |
| // marked by PN_XNUM. The 0th section header's sh_info is the real count. |
| size_t phnum = ehdr.phnum; |
| if (phnum == Elf::Ehdr::kPnXnum) { |
| Elf::Shdr shdr; |
| if (ehdr.shoff < sizeof(ehdr) || ehdr.shnum() == 0 || ehdr.shentsize() != sizeof(shdr)) { |
| return fit::error(Error{ |
| "invalid ELF section headers for PN_XNUM", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| auto result = file.ReadEphemeral(where / FileRange{ehdr.shoff, sizeof(shdr)}); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| if (result.value().size() < sizeof(shdr)) { |
| return TruncatedDump(); |
| } |
| memcpy(&shdr, result.value().data(), sizeof(shdr)); |
| phnum = shdr.info; |
| } |
| |
| // Read the program headers. |
| ByteView phdrs_bytes; |
| if (ehdr.phoff > where.size || where.size / sizeof(Elf::Phdr) < phnum) { |
| return TruncatedDump(); |
| } else { |
| const size_t phdrs_size_bytes = phnum * sizeof(Elf::Phdr); |
| auto result = file.ReadEphemeral(where / FileRange{ehdr.phoff, phdrs_size_bytes}); |
| if (result.is_error()) { |
| return result.take_error(); |
| } else { |
| phdrs_bytes = result.value(); |
| } |
| if (phdrs_bytes.size() < phdrs_size_bytes) { |
| // If it doesn't have all the phdrs, it won't have anything after them. |
| return TruncatedDump(); |
| } |
| } |
| |
| // Parse the program headers. Note they occupy the ephemeral buffer |
| // throughout the parsing loop, so it cannot use ReadEphemeral at all. |
| |
| // Process-wide notes will accumulate in the Process. |
| Process process(*this); |
| |
| // Per-thread notes will accumulate in the thread until a new thread's first |
| // note is seen. |
| std::optional<Thread> thread; |
| |
| auto reify_thread = [&process, &thread]() { |
| if (thread) { |
| zx_koid_t koid = thread->koid(); |
| // Ignore duplicates here since they do no real harm. |
| process.threads_.emplace_hint(process.threads_.end(), koid, std::move(*thread)); |
| } |
| }; |
| |
| // Parse a note segment. Truncated notes do not cause an error. |
| auto parse_notes = [&](FileRange notes) -> fit::result<Error> { |
| // Cap the segment size to what's available in the file. |
| notes.size = std::min(notes.size, where.size - notes.offset); |
| |
| // Read the whole segment and keep it forever. |
| ByteView bytes; |
| if (auto result = file.ReadPermanent(where / notes); result.is_error()) { |
| return result.take_error(); |
| } else { |
| bytes = result.value(); |
| } |
| |
| // TODO(mcgrathr): Use elfldltl note parser. |
| // Iterate through the notes. |
| Elf::Nhdr nhdr; |
| while (bytes.size() >= sizeof(nhdr)) { |
| memcpy(&nhdr, bytes.data(), sizeof(nhdr)); |
| bytes = bytes.subspan(sizeof(nhdr)); |
| auto name_bytes = bytes.subspan(0, nhdr.namesz); |
| if (bytes.size() < NoteAlign(nhdr.namesz)) { |
| break; |
| } |
| bytes = bytes.subspan(NoteAlign(nhdr.namesz)); |
| if (bytes.size() < NoteAlign(nhdr.namesz)) { |
| break; |
| } |
| auto desc = bytes.subspan(0, nhdr.descsz); |
| if (bytes.size() < NoteAlign(nhdr.descsz)) { |
| break; |
| } |
| bytes = bytes.subspan(NoteAlign(nhdr.descsz)); |
| |
| // All valid note names end with a NUL terminator. |
| std::string_view name{ |
| reinterpret_cast<const char*>(name_bytes.data()), |
| name_bytes.size(), |
| }; |
| if (name.empty() || name.back() != '\0') { |
| // Ignore bogus notes. Could make them an error? |
| continue; |
| } |
| name.remove_suffix(1); |
| |
| // Check for a system note. |
| if (name == kSystemNoteName) { |
| auto result = ReadSystemNote(desc); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| continue; |
| } |
| |
| // Check for a dump date note. |
| if (name == kDateNoteName) { |
| if (desc.size() < sizeof(process.date_)) { |
| return CorruptedDump(); |
| } |
| memcpy(&process.date_, desc.data(), sizeof(process.date_)); |
| continue; |
| } |
| |
| // Check for a process info note. |
| if (name == kProcessInfoNoteName) { |
| if (nhdr.type == ZX_INFO_HANDLE_BASIC) { |
| zx_info_handle_basic_t info; |
| if (desc.size() < sizeof(info)) { |
| return CorruptedDump(); |
| } |
| memcpy(&info, desc.data(), sizeof(info)); |
| |
| // Validate the type because it's used for static_cast validation. |
| if (info.type != ZX_OBJ_TYPE_PROCESS) { |
| return CorruptedDump(); |
| } |
| } |
| auto result = AddNote(process.info_, nhdr.type(), desc); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| continue; |
| } |
| |
| // Not a process info note. Check for a process property note. |
| if (name == kProcessPropertyNoteName) { |
| auto result = AddNote(process.properties_, nhdr.type(), desc); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| continue; |
| } |
| |
| // Not any kind of process note. Check for a thread info note. |
| if (name == kThreadInfoNoteName) { |
| if (nhdr.type == ZX_INFO_HANDLE_BASIC) { |
| // This marks the first note of a new thread. Reify the last one. |
| reify_thread(); |
| |
| zx_info_handle_basic_t info; |
| if (desc.size() < sizeof(info)) { |
| return CorruptedDump(); |
| } |
| memcpy(&info, desc.data(), sizeof(info)); |
| |
| // Validate the type because it's used for static_cast validation. |
| if (info.type != ZX_OBJ_TYPE_THREAD) { |
| return CorruptedDump(); |
| } |
| |
| // Start recording a new thread. This is the only place that |
| // constructs new Thread objects, so every extant Thread has the |
| // basic info. But we don't validate that the KOID is not zero or a |
| // duplicate. Such bogons don't really do harm. They will be |
| // visible in the threads() list or to get_child calls using their |
| // bogus KOIDs, even if they are never in the ZX_INFO_PROCESS_THREADS |
| // list. That behavior is inconsistent with a real live process but |
| // it's consistent with the way the dump was actually written. |
| // |
| // This can't use emplace because the default constructor is private, |
| // but the move constructor and move assignment operator are public. |
| thread = {Thread{*this}}; |
| } else if (!thread) { |
| return fit::error(Error{ |
| "first thread info note is not ZX_INFO_HANDLE_BASIC", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| |
| auto result = AddNote(thread->info_, nhdr.type(), desc); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| continue; |
| } |
| |
| // Not a thread info note. Check for a thread property note. |
| if (name == kThreadPropertyNoteName) { |
| if (!thread) { |
| return fit::error(Error{ |
| "thread property note before thread ZX_INFO_HANDLE_BASIC note", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| |
| auto result = AddNote(thread->properties_, nhdr.type(), desc); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| continue; |
| } |
| |
| // Not a thread property note. Check for a thread state note. |
| if (name == kThreadStateNoteName) { |
| if (!thread) { |
| return fit::error(Error{ |
| "thread state note before thread ZX_INFO_HANDLE_BASIC note", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| |
| auto result = AddNote(thread->state_, nhdr.type(), desc); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| continue; |
| } |
| |
| // Ignore unrecognized notes. Could make them an error? |
| } |
| |
| return fit::ok(); |
| }; |
| |
| // Validate a memory segment and add it to the memory map. |
| auto add_segment = [&process](uint64_t vaddr, Process::Segment segment) // |
| -> fit::result<Error> { |
| ZX_DEBUG_ASSERT(segment.memsz > 0); |
| if (!process.memory_.empty()) { |
| const auto& [last_vaddr, last_segment] = *process.memory_.crbegin(); |
| ZX_DEBUG_ASSERT(last_segment.memsz > 0); |
| if (vaddr <= last_vaddr) { |
| return fit::error(Error{ |
| "ELF core file PT_LOAD segments not in ascending address order", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| if (vaddr < last_vaddr + last_segment.memsz) { |
| return fit::error(Error{ |
| "ELF core file PT_LOAD segments overlap", |
| ZX_ERR_IO_DATA_INTEGRITY, |
| }); |
| } |
| } |
| process.memory_.emplace_hint(process.memory_.end(), vaddr, segment); |
| return fit::ok(); |
| }; |
| |
| while (!phdrs_bytes.empty()) { |
| Elf::Phdr phdr; |
| if (phdrs_bytes.size() < sizeof(phdr)) { |
| return TruncatedDump(); |
| } |
| memcpy(&phdr, phdrs_bytes.data(), sizeof(phdr)); |
| phdrs_bytes = phdrs_bytes.subspan(sizeof(phdr)); |
| if (phdr.type == elfldltl::ElfPhdrType::kNote && phdr.memsz() == 0 && phdr.filesz > 0) { |
| // A non-allocated note segment should hold core notes. |
| auto result = parse_notes({phdr.offset, phdr.filesz}); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| } else if (read_memory && phdr.type == elfldltl::ElfPhdrType::kLoad && phdr.memsz > 0) { |
| auto result = add_segment(phdr.vaddr, {phdr.offset, phdr.filesz, phdr.memsz}); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| } |
| } |
| |
| if (process.koid() == 0) { // There was no ZX_INFO_HANDLE_BASIC note. |
| return CorruptedDump(); |
| } |
| |
| // Looks like a valid dump. Finish out the last pending thread. |
| reify_thread(); |
| if (auto result = AttachProcess(std::move(process)); result.is_error()) { |
| return result.take_error(); |
| } |
| return fit::ok(); |
| } |
| |
| fit::result<Error> TaskHolder::JobTree::ReadArchive(DumpFile& file, FileRange archive, |
| ByteView header, bool read_memory) { |
| // The first member's header comes immediately after kArchiveMagic. |
| archive %= kArchiveMagic.size(); |
| header = header.subspan(kArchiveMagic.size()); |
| |
| if (archive.empty()) { |
| return fit::ok(); |
| } |
| |
| // This holds the current member's details. |
| MemberHeader member{}; |
| FileRange contents{}; |
| |
| // This parses the header into member and contents, and consumes them from |
| // archive. |
| auto parse = [&archive, &member, &contents](ByteView header) // |
| -> fit::result<Error, bool> { |
| if (auto result = ParseArchiveHeader(header); result.is_error()) { |
| return result.take_error(); |
| } else { |
| member = result.value(); |
| } |
| archive %= sizeof(ar_hdr); |
| if (member.size > archive.size) { |
| return TruncatedDump(); |
| } |
| contents = archive / member.size; |
| archive %= member.size + (member.size & 1); |
| return fit::ok(true); |
| }; |
| |
| // This reads and parses the next header, consuming the member from archive. |
| auto next = [&](bool probe = false) -> fit::result<Error, bool> { |
| ByteView header; |
| if (auto result = file.ReadProbe(archive / sizeof(ar_hdr)); result.is_error()) { |
| return result.take_error(); |
| } else { |
| header = result.value(); |
| } |
| if (probe && header.empty()) { |
| return fit::ok(false); |
| } |
| if (header.size() < sizeof(ar_hdr)) { |
| return TruncatedDump(); |
| } |
| return parse(header); |
| }; |
| |
| // Parse the first member header. |
| if (auto result = parse(header); result.is_error()) { |
| return result.take_error(); |
| } |
| |
| if (member.name == ar_hdr::kSymbolTableName) { |
| // An archive symbol table was created by `ar`. `gcore` won't add one. |
| // Ignore it and read the next member. |
| if (archive.empty()) { |
| return fit::ok(); |
| } |
| if (auto result = next(); result.is_error()) { |
| return result.take_error(); |
| } |
| } |
| |
| std::string_view name_table; |
| if (member.name == ar_hdr::kNameTableName) { |
| // The special first member (or second member, if there was a symbol table) |
| // is the long name table. |
| if (auto result = file.ReadPermanent(contents); result.is_error()) { |
| return result.take_error(); |
| } else { |
| name_table = { |
| reinterpret_cast<const char*>(result.value().data()), |
| result.value().size(), |
| }; |
| } |
| if (archive.empty()) { |
| return fit::ok(); |
| } |
| if (auto result = next(); result.is_error()) { |
| return result.take_error(); |
| } |
| } |
| |
| // Any note members will collect in this Job. |
| Job job{*this}; |
| |
| // Process one normal member. It might be a note or an embedded dump file. |
| auto handle_member = [&]() -> fit::result<Error> { |
| // Check for an info note. |
| if (auto info = JobNoteName<zx_object_info_topic_t>(kJobInfoName, member.name); |
| info.is_error()) { |
| return info.take_error(); |
| } else if (info.value()) { |
| const zx_object_info_topic_t topic = *info.value(); |
| ByteView bytes; |
| if (auto result = file.ReadPermanent(contents); result.is_error()) { |
| return result.take_error(); |
| } else { |
| bytes = result.value(); |
| } |
| if (topic == ZX_INFO_HANDLE_BASIC) { |
| zx_info_handle_basic_t basic_info; |
| if (bytes.size() < sizeof(basic_info)) { |
| return CorruptedDump(); |
| } |
| memcpy(&basic_info, bytes.data(), sizeof(basic_info)); |
| |
| // Validate the type because it's used for static_cast validation. |
| if (basic_info.type != ZX_OBJ_TYPE_JOB) { |
| return CorruptedDump(); |
| } |
| } |
| if (job.date_ == 0) { |
| job.date_ = member.date; |
| } |
| return AddNote(job.info_, topic, bytes); |
| } |
| |
| // Not an info note. Check for a property note. |
| if (auto property = JobNoteName<uint32_t>(kJobPropertyName, member.name); property.is_error()) { |
| return property.take_error(); |
| } else if (property.value()) { |
| auto result = file.ReadPermanent(contents); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| return AddNote(job.properties_, *property.value(), result.value()); |
| } |
| |
| // Check for a system note. |
| if (member.name == kSystemNoteName) { |
| auto result = file.ReadEphemeral(contents); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| return ReadSystemNote(result.value()); |
| } |
| |
| // This member file is not a job note. It's an embedded dump file. |
| return Read(file, read_memory, contents, member.date); |
| }; |
| |
| // Iterate through the normal members. |
| while (true) { |
| // Specially-encoded member names are actually indices into the name table. |
| if (!HandleLongName(name_table, member)) { |
| return CorruptedDump(); |
| } |
| |
| if (auto result = handle_member(); result.is_error()) { |
| return result.take_error(); |
| } |
| |
| if (archive.empty()) { |
| break; |
| } |
| |
| if (auto result = next(true); result.is_error()) { |
| return result.take_error(); |
| } else if (!result.value()) { |
| break; |
| } |
| } |
| |
| // End of the archive. Reify the job. |
| if (job.koid() != ZX_KOID_INVALID) { |
| // Looks like a valid job. |
| auto result = AttachJob(std::move(job)); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| return fit::ok(); |
| } |
| |
| if (job.info_.empty() && job.properties_.empty()) { |
| // This was just a plain archive, not actually a job archive at all. |
| return fit::ok(); |
| } |
| |
| // This job archive had some notes but no ZX_INFO_HANDLE_BASIC note. |
| return CorruptedDump(); |
| } |
| |
| fit::result<Error> TaskHolder::JobTree::ReadSystemNote(ByteView data) { |
| // If it's already been collected, then ignore new data. |
| if (system_.IsObject()) { |
| return fit::ok(); |
| } |
| |
| std::string_view sv{reinterpret_cast<const char*>(data.data()), data.size()}; |
| StringViewStream stream{sv}; |
| system_.ParseStream(stream); |
| |
| return fit::ok(); |
| } |
| |
| const rapidjson::Value* TaskHolder::JobTree::GetSystemJsonData(const char* key) const { |
| if (system_.IsObject()) { |
| auto it = system_.FindMember(key); |
| if (it != system_.MemberEnd()) { |
| return &it->value; |
| } |
| } |
| return nullptr; |
| } |
| |
| template <> |
| std::string_view TaskHolder::JobTree::GetSystemData<std::string_view>(const char* key) const { |
| const rapidjson::Value* value = GetSystemJsonData(key); |
| if (!value || !value->IsString()) { |
| return {}; |
| } |
| return {value->GetString(), value->GetStringLength()}; |
| } |
| |
| template <> |
| uint32_t TaskHolder::JobTree::GetSystemData<uint32_t>(const char* key) const { |
| const rapidjson::Value* value = GetSystemJsonData(key); |
| return !value ? 0 |
| : value->IsUint() ? value->GetUint() |
| : value->IsNumber() ? static_cast<uint32_t>(value->GetDouble()) |
| : 0; |
| } |
| |
| template <> |
| uint64_t TaskHolder::JobTree::GetSystemData<uint64_t>(const char* key) const { |
| const rapidjson::Value* value = GetSystemJsonData(key); |
| return !value ? 0 |
| : value->IsUint64() ? value->GetUint64() |
| : value->IsNumber() ? static_cast<uint64_t>(value->GetDouble()) |
| : 0; |
| } |
| |
| std::string_view TaskHolder::system_get_version_string() const { |
| return tree_->GetSystemData<std::string_view>("version_string"); |
| } |
| |
| uint32_t TaskHolder::system_get_dcache_line_size() const { |
| return tree_->GetSystemData<uint32_t>("dcache_line_size"); |
| } |
| |
| uint32_t TaskHolder::system_get_num_cpus() const { |
| return tree_->GetSystemData<uint32_t>("num_cpus"); |
| } |
| |
| uint64_t TaskHolder::system_get_page_size() const { |
| return tree_->GetSystemData<uint64_t>("page_size"); |
| } |
| |
| uint64_t TaskHolder::system_get_physmem() const { |
| return tree_->GetSystemData<uint64_t>("physmem"); |
| } |
| |
| } // namespace zxdump |