#include "loaded_image.h" #include #include #include #include #include #include #include namespace raven { static constexpr size_t page_size = 4096; LoadedImage::LoadedImage(const PEImage &image, uint64_t base, bool relocated) : size(image.image_size), address(base) { const uint64_t limit = image.is_64 ? 0x800000000000ULL : 0x100000000ULL; if (!(image.characteristics & 2)) throw std::runtime_error("PE is not an executable image"); if (!base || base % 65536 || base >= limit || size > limit - base) throw std::runtime_error("invalid guest image base"); if (sysconf(_SC_PAGESIZE) != static_cast(page_size)) throw std::runtime_error("shared PE images require 4096-byte host pages"); if (size > 1024ULL * 1024 * 1024) throw std::runtime_error("PE image exceeds initial mapping size limit"); protections.resize((size + page_size - 1) / page_size, PROT_NONE); fd = memfd_create("raven-image", MFD_CLOEXEC); if (fd < 0) throw std::system_error(errno, std::generic_category(), "create PE backing file"); try { if (ftruncate(fd, static_cast(size)) != 0) throw std::system_error(errno, std::generic_category(), "size PE backing file"); // The contents go in through the descriptor: copying them through the mapping took a page fault for every page of a library // of hundreds of megabytes, in every process that loads it. const auto put = [&](std::span source, uint64_t offset) { for (size_t done = 0; done < source.size();) { const auto written = pwrite(fd, source.data() + done, source.size() - done, static_cast(offset + done)); if (written < 0 && errno == EINTR) continue; if (written <= 0) throw std::system_error(errno, std::generic_category(), "fill PE backing file"); done += size_t(written); } }; put(image.file_span(0, image.headers_size), 0); for (const auto §ion : image.sections) if (section.file_size) put(image.file_span(section.address, section.file_size), section.address); void *mapping = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mapping == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map PE backing file"); storage = static_cast(mapping); for (size_t page = 0; page * page_size < image.headers_size; ++page) protections[page] = PROT_READ; for (const auto §ion : image.sections) { int protection = PROT_NONE; if (section.characteristics & 0x40000000) protection |= PROT_READ; if (section.characteristics & 0x80000000) protection |= PROT_WRITE; if (section.characteristics & 0x20000000) protection |= PROT_EXEC; const uint64_t end = uint64_t(section.address) + std::max(section.virtual_size, section.file_size); for (size_t page = section.address / page_size; page * page_size < end; ++page) protections.at(page) |= protection; } if (base != image.image_base && relocated) relocate(image); } catch (...) { release(); throw; } } void LoadedImage::relocate(const PEImage &image) { const auto directory = image.directories[5]; if ((image.characteristics & 1) || !directory.address || !directory.size) { // A library of resources alone, with no code to run, no imports and no thread data, holds no address to fix: it works anywhere. if (!image.entry && !image.directories[1].address && !image.directories[9].address && !image.directories[13].address) return; throw std::runtime_error("PE image has no relocation table for the requested base"); } const auto table = image.file_span(directory.address, directory.size); const size_t width = image.is_64 ? 8 : 4; const auto inside = [&](const auto §ion, uint64_t location) { return location >= section.address && location < uint64_t(section.address) + std::max(section.virtual_size, section.file_size); }; const auto mapped = [&](uint64_t location) { return location < image.headers_size || std::any_of(image.sections.begin(), image.sections.end(), [&](const auto §ion) { return inside(section, location); }); }; // Every fixup is checked before any is applied. A block covers one page: when the page and the bytes a fixup at its end can reach lie // in one section, every fixup of the block is mapped, and only blocks at a section boundary are checked a byte at a time. A library // of hundreds of megabytes carries millions of fixups, relocated again in every process that loads it. // The high bit marks a 32-bit fixup in a 64-bit image. constexpr uint32_t narrow_fixup = 0x80000000U; std::vector fixups; for (size_t offset = 0; offset < table.size();) { if (table.size() - offset < 8 || offset % 4) throw std::runtime_error("truncated or misaligned PE relocation block"); uint32_t page = 0; uint32_t block_size = 0; for (size_t i = 0; i < 4; ++i) { page |= uint32_t(table[offset + i]) << (i * 8); block_size |= uint32_t(table[offset + 4 + i]) << (i * 8); } if (page % page_size || block_size < 8 || block_size % 2 || block_size > table.size() - offset) throw std::runtime_error("invalid PE relocation block"); const uint64_t reach = uint64_t(page) + page_size - 1 + width - 1; const bool whole = reach < image.headers_size || std::any_of(image.sections.begin(), image.sections.end(), [&](const auto §ion) { return inside(section, page) && inside(section, reach); }); for (size_t item = offset + 8; item < offset + block_size; item += 2) { const uint16_t entry = uint16_t(table[item]) | uint16_t(uint16_t(table[item + 1]) << 8); const uint16_t type = entry >> 12; if (!type) continue; // A 64-bit image may also hold 32-bit fixups, as one of managed code alone widened from PE32 does for its entry stub; the // fixup adds the low half of the delta, as on Windows. if (type != (image.is_64 ? 10 : 3) && !(image.is_64 && type == 3)) throw std::runtime_error("unsupported PE relocation type"); const size_t reach_width = type == 3 ? 4 : 8; const uint64_t target = uint64_t(page) + (entry & 0xfff); if (target > size || reach_width > size - target || target >= narrow_fixup) throw std::runtime_error("PE relocation target exceeds image"); for (size_t byte = 0; !whole && byte < reach_width; ++byte) if (!mapped(target + byte)) throw std::runtime_error("PE relocation target is not mapped"); fixups.push_back(static_cast(target) | (width == 8 && type == 3 ? narrow_fixup : 0)); } offset += block_size; } const uint64_t delta = address - image.image_base; for (auto fixup : fixups) { const bool narrow = fixup & narrow_fixup; fixup &= ~narrow_fixup; if (width == 8 && !narrow) { uint64_t value; std::memcpy(&value, storage + fixup, 8); value += delta; std::memcpy(storage + fixup, &value, 8); } else { uint32_t value; std::memcpy(&value, storage + fixup, 4); value += uint32_t(delta); std::memcpy(storage + fixup, &value, 4); } } relocated = fixups.size(); } LoadedImage::~LoadedImage() { release(); } LoadedImage::LoadedImage(LoadedImage &&other) noexcept : fd(other.fd), storage(other.storage), size(other.size), address(other.address), relocated(other.relocated), protections(std::move(other.protections)) { other.fd = -1; other.storage = nullptr; other.size = 0; other.address = 0; other.relocated = 0; } LoadedImage &LoadedImage::operator=(LoadedImage &&other) noexcept { if (this != &other) { release(); fd = other.fd; storage = other.storage; size = other.size; address = other.address; relocated = other.relocated; protections = std::move(other.protections); other.fd = -1; other.storage = nullptr; other.size = 0; other.address = 0; other.relocated = 0; } return *this; } void LoadedImage::release() { if (storage) munmap(storage, size); if (fd >= 0) close(fd); storage = nullptr; fd = -1; } std::span LoadedImage::data() { return {storage, size}; } std::span LoadedImage::data() const { return {storage, size}; } std::vector LoadedImage::mappings() const { std::vector result; for (size_t page = 0; page < protections.size(); ++page) { const uint64_t offset = page * page_size; const uint64_t length = std::min(page_size, size - offset); if (!result.empty() && result.back().protection == protections[page]) result.back().size += length; else result.push_back({address + offset, length, offset, protections[page]}); } return result; } int LoadedImage::descriptor() const { return fd; } uint64_t LoadedImage::base() const { return address; } size_t LoadedImage::relocation_count() const { return relocated; } }