// Copyright 2016 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 #include #include #include #include #include #include #include #include #include #include #include #include namespace { // This is a reasonable upper limit on the number of program headers that are // expected. 7 or 8 is more typical. constexpr size_t kMaxProgramHeaders = 16; // kWindowSize is a tuning parameter. It specifies how much memory should be // read in by ProcessMemReader when a new read is needed. The goal is to // optimize the trade-off between making too many system calls and reading in // too much memory. The larger kWindowSize is the fewer system calls are made // but the more bytes are copied over that don't need to be. The smaller it is // the more system calls need to be made but the fewer superfluous bytes are // copied. // TODO(jakehehrlich): Tune kWindowSize rather than just guessing. constexpr size_t kWindowSize = 0x400; // This is an upper bound on the number of bytes that can be used in a build ID. // md5 and sha1 are the most common hashes used for build ids and they use 20 // and 16 bytes respectively. This makes 32 a generous upper bound. constexpr size_t kMaxBuildIDSize = 32; bool IsPossibleLoadedEhdr(const Elf64_Ehdr& ehdr) { // Do some basic sanity checks including checking the ELF identifier return ehdr.e_ident[EI_MAG0] == ELFMAG0 && ehdr.e_ident[EI_MAG1] == ELFMAG1 && ehdr.e_ident[EI_MAG2] == ELFMAG2 && ehdr.e_ident[EI_MAG3] == ELFMAG3 && ehdr.e_ident[EI_CLASS] == ELFCLASS64 && ehdr.e_ident[EI_DATA] == ELFDATA2LSB && ehdr.e_ident[EI_VERSION] == EV_CURRENT && ehdr.e_type == ET_DYN && ehdr.e_machine == kNativeElfMachine && ehdr.e_version == EV_CURRENT && ehdr.e_ehsize == sizeof(Elf64_Ehdr) && ehdr.e_phentsize == sizeof(Elf64_Phdr) && ehdr.e_phnum > 0 && (ehdr.e_phoff % alignof(Elf64_Phdr) == 0); } // TODO(jakehehrlich): Switch uses of uint8_t to std::byte where appropriate. class ProcessMemReader { public: ProcessMemReader(const zx::process& proc) : process_(proc) {} // TODO (jakehehrlich): Make this interface zero-copy (by returning a // a pointer rather than copying for instance). It's important that // the lifetime of the underlying storage is correctly managed. template [[nodiscard]] zx_status_t Read(uintptr_t vaddr, T* x) { return ReadBytes(vaddr, reinterpret_cast(x), sizeof(T)); } template [[nodiscard]] zx_status_t ReadArray(uintptr_t vaddr, fbl::Array* arr) { return ReadBytes(vaddr, arr->get(), arr->size()); } template [[nodiscard]] zx_status_t ReadArray(uintptr_t vaddr, T* arr, size_t sz) { return ReadBytes(vaddr, reinterpret_cast(arr), sz * sizeof(T)); } private: const zx::process& process_; uint8_t window_[kWindowSize]; uintptr_t window_start_ = 0; size_t window_size_ = 0; zx_status_t ReadBytes(uintptr_t vaddr, uint8_t* mem, size_t size) { if (vaddr >= window_start_ && vaddr - window_start_ < window_size_) { size_t from_window_size = fbl::min(size, window_size_ - (vaddr - window_start_)); memcpy(mem, window_ + (vaddr - window_start_), from_window_size); vaddr += from_window_size; mem += from_window_size; size -= from_window_size; } while (size > 0) { // TODO(jakehehrlich): Only read into window on the last iteration of this loop. size_t actual; zx_status_t status = process_.read_memory(vaddr, window_, kWindowSize, &actual); if (status != ZX_OK) { return status; } window_start_ = vaddr; window_size_ = actual; size_t bytes_read = fbl::min(actual, size); memcpy(mem, window_, bytes_read); vaddr += bytes_read; mem += bytes_read; size -= bytes_read; } return ZX_OK; } }; [[nodiscard]] zx_status_t GetBuildID(ProcessMemReader* reader, uintptr_t base, const Elf64_Phdr& notes, uint8_t* buildID, size_t* buildIDSize) { auto NoteAlign = [](uint32_t x) { return (x + 3) & -4; }; // TODO(jakehehrlich): Sanity check here that notes.p_vaddr falls in the [p_vaddr,p_vaddr+p_filesz) range of some RO PT_LOAD. // TODO(jakehehrlich): Check that base is actually the bias and do something to alert the user to base not being the bias. uintptr_t vaddr = base + notes.p_vaddr; uintptr_t end = vaddr + notes.p_filesz; // If the virtual address we found is not aligned or the ending overflowed return early. if ((vaddr & 3) || end < vaddr) { return ZX_ERR_NOT_FOUND; } while (end - vaddr > sizeof(Elf64_Nhdr)) { Elf64_Nhdr nhdr; zx_status_t status = reader->Read(vaddr, &nhdr); if (status != ZX_OK) { return status; } vaddr += sizeof(Elf64_Nhdr); if (end - vaddr < NoteAlign(nhdr.n_namesz)) { break; } uintptr_t nameAddr = vaddr; vaddr += NoteAlign(nhdr.n_namesz); if (end - vaddr < NoteAlign(nhdr.n_descsz)) { break; } uintptr_t descAddr = vaddr; vaddr += NoteAlign(nhdr.n_descsz); // TODO(jakehehrlich): If descsz is larger than kMaxBuildIDSize but // the type and name indicate that this note entry is a build ID we // should log a warning to the user. if (nhdr.n_type == NT_GNU_BUILD_ID && nhdr.n_namesz == sizeof(ELF_NOTE_GNU) && nhdr.n_descsz <= kMaxBuildIDSize) { char name[sizeof(ELF_NOTE_GNU)]; status = reader->ReadArray(nameAddr, name, nhdr.n_namesz); if (status != ZX_OK) { return status; } if (memcmp(name, ELF_NOTE_GNU, nhdr.n_namesz) == 0) { status = reader->ReadArray(descAddr, buildID, nhdr.n_descsz); if (status != ZX_OK) { return status; } *buildIDSize = nhdr.n_descsz; return ZX_OK; } } } return ZX_ERR_NOT_FOUND; } } // anonymous namespace zx_status_t ForEachModule(const zx::process& process, ModuleAction action) { ProcessMemReader reader(process); // Read in the process maps. size_t actual, avail; zx_status_t status = process.get_info(ZX_INFO_PROCESS_MAPS, nullptr, 0, &actual, &avail); if (status != ZX_OK) { return status; } fbl::AllocChecker ac; fbl::Array maps(new (&ac) zx_info_maps[avail], avail); if (!ac.check()) { return ZX_ERR_NO_MEMORY; } status = process.get_info(ZX_INFO_PROCESS_MAPS, maps.get(), avail * sizeof(zx_info_maps), &actual, &avail); if (status != ZX_OK) { return status; } // TODO(jakehehrlich): Check permissions of program headers to make sure they agree with mappings. // 'maps' should be sorted in ascending order of base address so we should be able to use that to // quickly find the mapping associated with any given PT_LOAD. for (const auto& map : maps) { // Skip any writable maps since the RODATA segment containing the // headers will not be writable. if (map.type != ZX_INFO_MAPS_TYPE_MAPPING) { continue; } if ((map.u.mapping.mmu_flags & ZX_VM_PERM_WRITE) != 0) { continue; } // Skip any mapping that doesn't start at the beginning of a VMO. // We assume that the VMO represents the ELF file. ELF headers // always start at the beginning of the file so if our assumption // holds then we can't be looking at the start of an ELF header if // the offset into the VMO isn't also zero. if (map.u.mapping.vmo_offset != 0) { continue; } // Read in what might be an ELF header. Elf64_Ehdr ehdr; status = reader.Read(map.base, &ehdr); if (status != ZX_OK) { continue; } // Do some basic checks to see if this could ever be an ELF file. if (!IsPossibleLoadedEhdr(ehdr)) { continue; } // We only support ELF files with <= 16 program headers. // TODO(jakehehrlich): Log this because with the exception of core dumps // almost nothing should get here *and* have such a large number of phdrs. // This might indicate a larger issue. if (ehdr.e_phnum > kMaxProgramHeaders) { continue; } Elf64_Phdr phdrs_buf[kMaxProgramHeaders]; auto phdrs = MakeArrayRef(phdrs_buf, ehdr.e_phnum); status = reader.ReadArray(map.base + ehdr.e_phoff, phdrs_buf, ehdr.e_phnum); if (status != ZX_OK) { continue; } // Loop though program headers looking for a build ID. uint8_t build_id_buf[kMaxBuildIDSize]; ArrayRef build_id; for (const auto& phdr : phdrs) { if (phdr.p_type == PT_NOTE) { size_t size; status = GetBuildID(&reader, map.base, phdr, build_id_buf, &size); if (status == ZX_OK && size != 0) { build_id = MakeArrayRef(build_id_buf, size); break; } } } // We're not considering otherwise valid files with no build id here. // TODO(jakehehrlich): Consider reporting loaded modules with no build ID. if (build_id.empty()) { continue; } // TODO(jakehehrlich): Synthesize a better name via PT_DYNAMIC->DT_SONAME. char name[ZX_MAX_NAME_LEN]; if (map.name[0] == '\0') { snprintf(name, sizeof(name), "", map.u.mapping.vmo_koid); } else { snprintf(name, sizeof(name), "", map.u.mapping.vmo_koid, map.name); } // All checks have passed so we can give the user a module. action(ModuleInfo{ .name = fbl::StringPiece(name), .vaddr = map.base, .build_id = build_id, .ehdr = ehdr, .phdrs = phdrs, }); } return ZX_OK; }