diff --git a/Source/Common/FileMappingBaseAddress.h b/Source/Common/FileMappingBaseAddress.h new file mode 100644 index 000000000..bec3a19cc --- /dev/null +++ b/Source/Common/FileMappingBaseAddress.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#include + +#include +#include +#include + +#include + +namespace FEXCore { + +/** + * Infers the base virtual address from a file mapping (as described by parameters to a single + * call to mmap()). + * + * The file offset of any given mapping need not match its virtual address offset from the base + * mapping (file offset = 0). Instead, this function searches the corresponding ELF program headers + * for an entry that generated the given file mapping. + */ +inline std::optional +InferMappingBaseAddress(std::span ProgramHeaders, uint64_t Addr, uint64_t Size, uint64_t FileOffset, int AccessFlags) { + for (auto& phdr : ProgramHeaders) { + if (phdr.p_type != PT_LOAD) { + // Skip headers that don't trigger memory mappings + continue; + } + + if ((phdr.p_flags & (PF_X | PF_W | PF_R)) != (AccessFlags & (PF_X | PF_W | PF_R))) { + continue; + } + + // The mapped file offset must be included at the start of the section header + auto SegmentStartOffset = phdr.p_offset - (phdr.p_vaddr & 0xfff); + if (FileOffset >= SegmentStartOffset && FileOffset < SegmentStartOffset + phdr.p_filesz && + (FileOffset & Utils::FEX_PAGE_MASK) == (phdr.p_offset & Utils::FEX_PAGE_MASK)) { + // Compute VA offset relative to the base mapping + return Addr - (phdr.p_vaddr - (phdr.p_offset & 0xfff)) + (ProgramHeaders[0].p_vaddr - (ProgramHeaders[0].p_offset & 0xfff)) - + (FileOffset - SegmentStartOffset); + } + } + + return std::nullopt; +} +} // namespace FEXCore diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsSMCTracking.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsSMCTracking.cpp index 3e792acf4..ea4fbb53a 100644 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsSMCTracking.cpp +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsSMCTracking.cpp @@ -8,6 +8,7 @@ $end_info$ */ #include "Common/FDUtils.h" +#include "Common/FileMappingBaseAddress.h" #include #include @@ -23,6 +24,7 @@ $end_info$ #include #include #include +#include namespace FEX::HLE { // SMC interactions @@ -172,15 +174,13 @@ std::optional SyscallHandler::LookupExecutableFileSection(FEXCore::Core::InternalThreadState& Thread, uint64_t GuestAddr) { auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, &Thread); - // Get the first mapping after GuestAddr, or end - // GuestAddr is inclusive - // If the write spans two pages, they will be flushed one at a time (generating two faults) - auto Entry = VMATracking.FindVMAEntry(GuestAddr); - if (Entry == VMATracking.VMAs.end() || !Entry->second.Resource) { + auto EntryIt = VMATracking.FindVMAEntry(GuestAddr); + if (EntryIt == VMATracking.VMAs.end() || !EntryIt->second.Resource) { return std::nullopt; } - return FEXCore::ExecutableFileSectionInfo {*Entry->second.Resource->MappedFile, Entry->second.Base - Entry->second.Offset}; + auto& [MappingBaseAddr, Entry] = *EntryIt; + return FEXCore::ExecutableFileSectionInfo {*Entry.Resource->MappedFile, Entry.Resource->FirstVMA->Base}; } FEXCore::HLE::ExecutableRangeInfo SyscallHandler::QueryGuestExecutableRange(FEXCore::Core::InternalThreadState* Thread, uint64_t Address) { @@ -195,6 +195,22 @@ FEXCore::HLE::ExecutableRangeInfo SyscallHandler::QueryGuestExecutableRange(FEXC return {Entry->first, Entry->second.Length, Entry->second.Prot.Writable}; } +static fextl::vector ReadELFHeaders(int FD, std::span HeaderData = {}) { + std::string_view ELFMagic = ELFMAG; + if (HeaderData.data()) { + if (HeaderData.size_bytes() < ELFMagic.size() || std::memcmp(ELFMagic.data(), HeaderData.data(), ELFMagic.size()) != 0) { + // Not an ELF file + return {}; + } + } else { + // Read from FD in case the caller didn't have a mapped header available + } + + ELFParser Parser; + Parser.ReadElf(dup(FD)); + return std::move(Parser.phdrs); +} + void* SyscallHandler::GuestMmap(bool Is64Bit, FEXCore::Core::InternalThreadState* Thread, void* addr, size_t length, int prot, int flags, int fd, off_t offset) { LOGMAN_THROW_A_FMT(Is64Bit || (length >> 32) == 0, "values must fit to 32 bits"); @@ -383,19 +399,56 @@ std::optional SyscallHandler: if (!(flags & MAP_ANONYMOUS)) { struct stat64 buf; fstat64(fd, &buf); - VMATracking::MRID mrid {buf.st_dev, buf.st_ino}; + + const VMATracking::MRID mrid {buf.st_dev, buf.st_ino}; char Tmp[PATH_MAX]; auto PathLength = FEX::get_fdpath(fd, Tmp); - if (PathLength != -1) { - auto [Iter, Inserted] = VMATracking.InsertMappedResource(mrid, {nullptr, nullptr, 0}); - Resource = &Iter->second; + auto [ResourceIt, ResourceEnd] = VMATracking.FindResources(mrid); + bool Inserted = false; + const bool MappedELFHeaderAgain = ResourceIt != ResourceEnd && offset == 0 && !ResourceIt->second.ProgramHeaders.empty(); + if (ResourceIt == ResourceEnd || MappedELFHeaderAgain) { + // Create a new MappedResource for previously unseen file and for re-mappings of an ELF header + ResourceIt = VMATracking.InsertMappedResource(mrid, {nullptr, nullptr, 0}); + ResourceIt->second.Iterator = ResourceIt; + Inserted = true; + } + Resource = &ResourceIt->second; + // Only handle FDs that are backed by regular files that are executable + if (PathLength != -1 && S_ISREG(buf.st_mode) && (buf.st_mode & S_IXUSR)) { + // ELF files that are mapped multiple times get a separate MappedResource for each base virtual address if (Inserted) { Resource->MappedFile = fextl::make_unique(); Resource->MappedFile->Filename = fextl::string(Tmp, PathLength); - Resource->Iterator = Iter; + + // Read ELF headers if applicable. + // For performance, skip ELF checks if we're not mapping the file header + bool CheckForElfFile = (offset == 0); +#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED + CheckForElfFile = true; +#endif + if (CheckForElfFile) { + Resource->ProgramHeaders = ReadELFHeaders(fd, std::span {reinterpret_cast(addr), length}); + LOGMAN_THROW_A_FMT(Resource->ProgramHeaders.empty() || offset == 0, "Expected file offset 0 for the first mapping of an ELF " + "file"); + } + } else if (ResourceIt->second.ProgramHeaders.empty()) { + // Not an ELF file, so we don't need to distinguish between different base addresses + } else { + // Mapped a non-header section of an ELF file. + // Look up the corresponding MappedResource using the expected base address. + + ResourceIt = std::find_if(ResourceIt, ResourceEnd, [&](const VMATracking::MappedResource::ContainerType::value_type& ResourcePair) { + auto& Resource = ResourcePair.second; + auto ExpectedBase = FEXCore::InferMappingBaseAddress( + Resource.ProgramHeaders, addr, Size, offset, + (ProtMapping.Executable ? PF_X : 0) | (ProtMapping.Writable ? PF_W : 0) | (ProtMapping.Readable ? PF_R : 0)); + return ExpectedBase == Resource.FirstVMA->Base; + }); + LOGMAN_THROW_A_FMT(ResourceIt != ResourceEnd, "ERROR: Could not find base for file mapping at {:#x} (offset {:#x})", addr, offset); + Resource = &ResourceIt->second; } const fextl::string Filename = FHU::Filesystem::GetFilename(Resource->MappedFile->Filename); @@ -416,12 +469,12 @@ std::optional SyscallHandler: } else if (flags & MAP_SHARED) { VMATracking::MRID mrid {VMATracking::SpecialDev::Anon, AnonSharedId++}; - auto [Iter, Inserted] = VMATracking.InsertMappedResource(mrid, {nullptr, nullptr, 0}); - LOGMAN_THROW_A_FMT(Inserted == true, "VMA tracking error"); + auto [Iter, IterEnd] = VMATracking.FindResources(mrid); + LOGMAN_THROW_A_FMT(Iter == IterEnd, "VMA tracking error"); + + Iter = VMATracking.InsertMappedResource(mrid, {nullptr, nullptr, 0}); Resource = &Iter->second; Resource->Iterator = Iter; - } else { - Resource = nullptr; } VMATracking.TrackVMARange(CTX, Resource, addr, offset, Size, VMATracking::VMAFlags::fromFlags(flags), ProtMapping); @@ -477,11 +530,12 @@ void SyscallHandler::TrackMremap(FEXCore::Core::InternalThreadState* Thread, uin void SyscallHandler::TrackShmat(FEXCore::Core::InternalThreadState* Thread, int shmid, uint64_t shmaddr, int shmflg, uint64_t Length) { VMATracking::MRID mrid {VMATracking::SpecialDev::SHM, static_cast(shmid)}; - auto [Iter, Inserted] = VMATracking.InsertMappedResource(mrid, {nullptr, nullptr, Length}); - auto Resource = &Iter->second; - if (Inserted) { - Resource->Iterator = Iter; + auto [Iter, IterEnd] = VMATracking.FindResources(mrid); + if (Iter == IterEnd) { + Iter = VMATracking.InsertMappedResource(mrid, {nullptr, nullptr, Length}); + Iter->second.Iterator = Iter; } + auto Resource = &Iter->second; VMATracking.TrackVMARange(CTX, Resource, shmaddr, 0, Length, VMATracking::VMAFlags::fromFlags(MAP_SHARED), VMATracking::VMAProt::fromSHM(shmflg)); } diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsVMATracking.h b/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsVMATracking.h index 87932c821..0c8ebd665 100644 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsVMATracking.h +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsVMATracking.h @@ -8,6 +8,8 @@ #include #include +#include + namespace FEX::HLE::VMATracking { ///// VMA (Virtual Memory Area) tracking ///// @@ -30,15 +32,24 @@ struct MRID { struct VMAEntry; -// Used to all MAP_SHARED VMAs of a system resource. +/** + * Meta data associated to one system resource. + * + * Typically there is one instance of this type per ELF/PE file or special device. + * However if an ELF/PE file is mapped multiple times at different base addresses, + * there will be one separate MappedResource for each base address. The MRID + * is the same in this case. + */ struct MappedResource { - using ContainerType = fextl::map; + using ContainerType = fextl::multimap; fextl::unique_ptr MappedFile; // Pointer to lowest memory range this file is mapped to VMAEntry* FirstVMA; uint64_t Length; // 0 if not fixed size ContainerType::iterator Iterator; + + fextl::vector ProgramHeaders; }; union VMAProt { @@ -116,6 +127,14 @@ struct VMATracking { return MappedResources.emplace(mrid, std::move(Resource)); } + // Returns an iterator pair spanning the range of all MappedResources matching the given MRID. + // Typically there is only one associated resource, however sometimes the same file gets mapped + // multiple times at different base addresses. In that case, each MappedResource will cover an + // exclusive set of VMAEntries that refer to a consistent base mapping address. + inline auto FindResources(const MRID& mrid) { + return MappedResources.equal_range(mrid); + } + private: MappedResource::ContainerType MappedResources; }; diff --git a/unittests/APITests/CMakeLists.txt b/unittests/APITests/CMakeLists.txt index 260f1be47..1ae70cbf7 100644 --- a/unittests/APITests/CMakeLists.txt +++ b/unittests/APITests/CMakeLists.txt @@ -1,11 +1,12 @@ set (TESTS Allocator ArgumentParser - InterruptableConditionVariable - Filesystem - StringUtils - fextl_function ExtendedVolatileMetadata + fextl_function + FileMappingBaseAddress + Filesystem + InterruptableConditionVariable + StringUtils ) list(APPEND LIBS Common FEXCore JemallocLibs) diff --git a/unittests/APITests/FileMappingBaseAddress.cpp b/unittests/APITests/FileMappingBaseAddress.cpp new file mode 100644 index 000000000..65ef78b65 --- /dev/null +++ b/unittests/APITests/FileMappingBaseAddress.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +#include +#include + +#include + +namespace { + +struct Mapping { + uint64_t Addr; + uint64_t Size; + uint64_t FileOffset; + int Flags; // PF_* +}; + +} // anonymous namespace + +TEST_CASE("libm") { + uint64_t BaseAddr = 0x123400000; + + fextl::vector Headers = { + {.p_type = PT_LOAD, .p_offset = 0x00000, .p_vaddr = 0x00000, .p_paddr = 0x00000, .p_filesz = 0x7bdd5, .p_memsz = 0x7bdd5}, + {.p_type = PT_LOAD, .p_offset = 0x7c000, .p_vaddr = 0x7c000, .p_paddr = 0x7c000, .p_filesz = 0x6f3a8, .p_memsz = 0x6f3a8}, + {.p_type = PT_LOAD, .p_offset = 0xebbd0, .p_vaddr = 0xecbd0, .p_paddr = 0xecbd0, .p_filesz = 0x434, .p_memsz = 0x440}, + }; + + fextl::vector Mappings = { + {.Addr = BaseAddr, .Size = 0x7c000, .FileOffset = 0x00000}, + {.Addr = BaseAddr + 0x7c000, .Size = 0x70000, .FileOffset = 0x7c000}, + {.Addr = BaseAddr + 0xec000, .Size = 0x2000, .FileOffset = 0xeb000}, + }; + + for (auto& Mapping : Mappings) { + INFO("Mapping to 0x" << std::hex << Mapping.Addr << "-0x" << Mapping.Addr + Mapping.Size << " from file offset 0x" << Mapping.FileOffset); + auto DeducedBase = FEXCore::InferMappingBaseAddress(Headers, Mapping.Addr, Mapping.Size, Mapping.FileOffset, Mapping.Flags); + CHECK(DeducedBase.value_or(0) == BaseAddr); + } +} + +// E.g. libX11-xcb +TEST_CASE("Access flags are checked") { + uint64_t BaseAddr = 0x123400000; + + fextl::vector Headers = { + {.p_type = PT_LOAD, .p_flags = PF_R | PF_X, .p_offset = 0x0000, .p_vaddr = 0x0000, .p_paddr = 0x0000, .p_filesz = 0x00040d, .p_memsz = 0x00040d}, + {.p_type = PT_LOAD, .p_flags = PF_R, .p_offset = 0x1000, .p_vaddr = 0x1000, .p_paddr = 0x1000, .p_filesz = 0x00036c, .p_memsz = 0x00036c}, + {.p_type = PT_LOAD, .p_flags = PF_W, .p_offset = 0x1dc8, .p_vaddr = 0x2dc8, .p_paddr = 0x2dc8, .p_filesz = 0x000238, .p_memsz = 0x000240}, + }; + + fextl::vector Mappings = { + {.Addr = BaseAddr + 0x1000, .Size = 0x1000, .FileOffset = 0x1000, .Flags = PF_R}, + {.Addr = BaseAddr + 0x2000, .Size = 0x1000, .FileOffset = 0x1000, .Flags = PF_W}, + }; + + for (auto& Mapping : Mappings) { + INFO("Mapping to 0x" << std::hex << Mapping.Addr << "-0x" << Mapping.Addr + Mapping.Size << " from file offset 0x" << Mapping.FileOffset); + auto DeducedBase = FEXCore::InferMappingBaseAddress(Headers, Mapping.Addr, Mapping.Size, Mapping.FileOffset, Mapping.Flags); + CHECK(DeducedBase.value_or(0) == BaseAddr); + } +} + +// Program headers that don't generate memory mappings can't be used to infer base addresses +TEST_CASE("Non-mapping program headers are ignored") { + uint64_t BaseAddr = 0x123400000; + + fextl::vector Headers = { + {.p_type = PT_LOAD, .p_offset = 0x00000, .p_vaddr = 0x0000, .p_paddr = 0x00000, .p_filesz = 0x1000, .p_memsz = 0x1000}, + {.p_type = PT_INTERP, .p_offset = 0x10000, .p_vaddr = 0xa000, .p_paddr = 0xa0000, .p_filesz = 0x1000, .p_memsz = 0x1000}, + {.p_type = PT_LOAD, .p_offset = 0x10000, .p_vaddr = 0x1000, .p_paddr = 0x10000, .p_filesz = 0x1000, .p_memsz = 0x1000}, + }; + + fextl::vector Mappings = { + {.Addr = BaseAddr + 0x1000, .Size = 0x1000, .FileOffset = 0x10000}, + }; + + for (auto& Mapping : Mappings) { + INFO("Mapping to 0x" << std::hex << Mapping.Addr << "-0x" << Mapping.Addr + Mapping.Size << " from file offset 0x" << Mapping.FileOffset); + auto DeducedBase = FEXCore::InferMappingBaseAddress(Headers, Mapping.Addr, Mapping.Size, Mapping.FileOffset, Mapping.Flags); + CHECK(DeducedBase.value_or(0) == BaseAddr); + } +}