| // 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/elfldltl/note.h> |
| #include <lib/fit/defer.h> |
| #include <lib/zxdump/elf-search.h> |
| |
| #include <array> |
| #include <cinttypes> |
| #include <cstdint> |
| #include <initializer_list> |
| #include <ostream> |
| #include <string> |
| #include <tuple> |
| #include <vector> |
| |
| #include <gmock/gmock.h> |
| |
| #include "dump-tests.h" |
| #include "test-file.h" |
| |
| namespace std { |
| |
| std::ostream& operator<<(std::ostream& os, const std::vector<std::byte>& bytes) { |
| for (std::byte byte : bytes) { |
| char buf[3]; |
| snprintf(buf, sizeof(buf), "%02x", static_cast<unsigned int>(byte)); |
| os << buf; |
| } |
| return os; |
| } |
| |
| } // namespace std |
| |
| namespace zxdump::testing { |
| |
| using ::testing::AllOf; |
| using ::testing::Contains; |
| using ::testing::Field; |
| using ::testing::IsEmpty; |
| using ::testing::Not; |
| |
| using ByteVector = std::vector<std::byte>; |
| |
| struct ElfId { |
| ByteVector build_id; |
| std::string soname; |
| uint64_t base = 0; |
| }; |
| |
| std::ostream& operator<<(std::ostream& os, const ElfId& id) { |
| return os << "{ build ID: " << id.build_id << ", SONAME: \"" << id.soname << "\", 0x" << std::hex |
| << id.base << " }"; |
| } |
| |
| auto MakeElfId(std::initializer_list<uint8_t> id, std::string_view soname) { |
| ByteVector bytes; |
| for (uint8_t byte : id) { |
| bytes.push_back(static_cast<std::byte>(byte)); |
| } |
| return ElfId{std::move(bytes), std::string(soname)}; |
| } |
| |
| // The generated .inc file has `MakeElfId({0x...,...}, "soname"),` lines. |
| const std::array kElfSearchIds = { |
| #include "test-child-elf-search.inc" |
| }; |
| |
| auto ElfIdEq(const ElfId& id, bool use_soname) { |
| return AllOf(Field("build ID", &ElfId::build_id, id.build_id), |
| use_soname ? Field("SONAME", &ElfId::soname, id.soname) |
| : Field("SONAME", &ElfId::soname, IsEmpty())); |
| } |
| |
| auto HasElfSearchIds(bool use_soname) { |
| auto has_ids = [use_soname](auto... ids) { |
| return AllOf(Contains(ElfIdEq(ids, use_soname)).Times(1)...); |
| }; |
| return std::apply(has_ids, kElfSearchIds); |
| } |
| |
| // This is called before dumping starts to make callbacks. |
| void TestProcessForElfSearch::Precollect(zxdump::TaskHolder& holder, zxdump::ProcessDump& dump) { |
| dump_ = &dump; |
| } |
| |
| // This is the callback made from DumpMemory for each segment. |
| // It needs the dumper pointer saved in Precollect. |
| fit::result<zxdump::Error, zxdump::SegmentDisposition> |
| TestProcessForElfSearch::DumpAllMemoryWithBuildIds(zxdump::SegmentDisposition segment, |
| const zx_info_maps_t& maps, |
| const zx_info_vmo_t& vmo) { |
| if (segment.filesz > 0 && zxdump::IsLikelyElfMapping(maps)) { |
| auto result = dump_->FindBuildIdNote(maps); |
| if (result.is_error()) { |
| return result.take_error(); |
| } |
| segment.note = result.value(); |
| } |
| return fit::ok(segment); |
| } |
| |
| void TestProcessForElfSearch::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({"-d", "-D"})); |
| |
| // The test-child wrote the pointers dladdr returned as module base addresses |
| // for the main and DSO symbols. Reading these immediately synchronizes with |
| // the child having started up and progressed far enough to have finished all |
| // its loading 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(2, fscanf(pipef, "%" SCNx64 "\n%" SCNx64, &main_ptr_, &dso_ptr_)); |
| } |
| |
| void TestProcessForElfSearch::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 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)); |
| } |
| |
| std::vector<ElfId> found_elf; |
| auto maps = read_process.get_info<ZX_INFO_PROCESS_MAPS>(); |
| ASSERT_TRUE(maps.is_ok()) << maps.error_value(); |
| auto first = maps->begin(); |
| do { |
| auto [next, last] = zxdump::ElfSearch(read_process, first, maps->end()); |
| if (next != last) { |
| const zx_info_maps_t& segment = *next; |
| ASSERT_EQ(segment.type, ZX_INFO_MAPS_TYPE_MAPPING); |
| auto detect = zxdump::DetectElf(read_process, segment); |
| ASSERT_TRUE(detect.is_ok()) << detect.error_value(); |
| cpp20::span phdrs = **detect; |
| EXPECT_THAT(phdrs, Not(IsEmpty())); |
| auto identity = zxdump::DetectElfIdentity(read_process, segment, phdrs); |
| ASSERT_TRUE(identity.is_ok()) << identity.error_value(); |
| EXPECT_GT(identity->build_id.size, zxdump::ElfIdentity::kBuildIdOffset); |
| auto id_bytes = read_process.read_memory<std::byte, zxdump::ByteView>( |
| identity->build_id.vaddr + zxdump::ElfIdentity::kBuildIdOffset, |
| identity->build_id.size - zxdump::ElfIdentity::kBuildIdOffset); |
| ASSERT_TRUE(id_bytes.is_ok()) << id_bytes.error_value(); |
| std::string soname; |
| if (identity->soname.size != 0) { |
| auto result = read_process.read_memory<char, std::string_view>( // |
| identity->soname.vaddr, identity->soname.size); |
| ASSERT_TRUE(result.is_ok()) << result.error_value(); |
| soname = **result; |
| } |
| found_elf.push_back({ |
| .build_id{id_bytes.value()->begin(), id_bytes.value()->end()}, |
| .soname{std::move(soname)}, |
| .base = segment.base, |
| }); |
| } |
| first = last; |
| } while (first != maps->end()); |
| |
| EXPECT_THAT(found_elf, HasElfSearchIds(true)); |
| |
| // Only the main executable should have no SONAME. |
| EXPECT_THAT(found_elf, Contains(Field("soname", &ElfId::soname, IsEmpty())).Times(1)); |
| |
| // That module's base should be what dladdr said in the child. |
| EXPECT_THAT(found_elf, Contains(AllOf(Field("soname", &ElfId::soname, IsEmpty()), |
| Field("load address", &ElfId::base, main_ptr())))); |
| |
| // Similar pair for the DSO module. |
| EXPECT_THAT(found_elf, Contains(Field(&ElfId::soname, kDsoSoname)).Times(1)); |
| EXPECT_THAT(found_elf, Contains(AllOf(Field("soname", &ElfId::soname, kDsoSoname), |
| Field("load address", &ElfId::base, dso_ptr())))); |
| } |
| |
| void TestProcessForElfSearch::CheckNotes(int fd) { |
| auto must_read = [fd](auto&& span, off_t pos) { |
| cpp20::span data(span); |
| ssize_t nread = pread(fd, data.data(), data.size_bytes(), pos); |
| ASSERT_GE(nread, 0) << strerror(errno); |
| ASSERT_EQ(data.size_bytes(), static_cast<size_t>(nread)); |
| }; |
| |
| zxdump::Elf::Ehdr ehdr; |
| ASSERT_NO_FATAL_FAILURE(must_read(cpp20::span(&ehdr, 1), 0)); |
| |
| std::vector<zxdump::Elf::Phdr> phdrs(ehdr.phnum); |
| ASSERT_NO_FATAL_FAILURE(must_read(phdrs, ehdr.phoff)); |
| |
| std::vector<ElfId> found_elf; |
| for (const zxdump::Elf::Phdr& phdr : phdrs) { |
| if (phdr.type == elfldltl::ElfPhdrType::kNote) { |
| ByteVector bytes(phdr.filesz, {}); |
| ASSERT_NO_FATAL_FAILURE(must_read(bytes, phdr.offset)); |
| elfldltl::ElfNoteSegment<elfldltl::ElfData::k2Lsb> notes(bytes); |
| for (const auto& note : notes) { |
| if (note.IsBuildId()) { |
| found_elf.push_back({ |
| .build_id{note.desc.begin(), note.desc.end()}, |
| .base = (&phdr)[-1].vaddr, |
| }); |
| } |
| } |
| } |
| } |
| |
| EXPECT_THAT(found_elf, HasElfSearchIds(false)); |
| |
| for (const auto& module : kElfSearchIds) { |
| const uint64_t base = module.soname.empty() ? main_ptr_ : dso_ptr_; |
| auto base_eq = Field("load address", &ElfId::base, base); |
| EXPECT_THAT(found_elf, Contains(AllOf(ElfIdEq(module, false), base_eq))); |
| } |
| } |
| |
| } // namespace zxdump::testing |
| |
| namespace { |
| |
| TEST(ZxdumpTests, ElfSearchLive) { |
| zxdump::testing::TestProcessForElfSearch process; |
| ASSERT_NO_FATAL_FAILURE(process.StartChild()); |
| |
| zxdump::TaskHolder holder; |
| auto insert_result = holder.Insert(process.handle()); |
| ASSERT_TRUE(insert_result.is_ok()) << insert_result.error_value(); |
| |
| ASSERT_NO_FATAL_FAILURE(process.CheckDump(holder)); |
| } |
| |
| TEST(ZxdumpTests, ElfSearchDump) { |
| zxdump::testing::TestFile file; |
| zxdump::FdWriter writer(file.RewoundFd()); |
| |
| zxdump::testing::TestProcessForElfSearch 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)); |
| |
| ASSERT_NO_FATAL_FAILURE(process.CheckNotes(file.RewoundFd().get())); |
| } |
| |
| } // namespace |