Merge pull request #4980 from neobrain/fix_infer_mapping_base

LinuxSyscalls: Fix base address inference for ELF binaries
This commit is contained in:
Ryan Houdek authored and GitHub committed 2025-10-20 10:26:20 -07:00
commit 197facb845
5 files changed
+226 -25

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <FEXCore/Utils/TypeDefines.h>
#include <cstdint>
#include <optional>
#include <span>
#include <elf.h>
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<uint64_t>
InferMappingBaseAddress(std::span<const Elf64_Phdr> 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
@@ -8,6 +8,7 @@ $end_info$
*/
#include "Common/FDUtils.h"
#include "Common/FileMappingBaseAddress.h"
#include <filesystem>
#include <sys/shm.h>
@@ -23,6 +24,7 @@ $end_info$
#include <FEXCore/Utils/SignalScopeGuards.h>
#include <FEXCore/Utils/TypeDefines.h>
#include <FEXHeaderUtils/Filesystem.h>
#include <Linux/Utils/ELFParser.h>
namespace FEX::HLE {
// SMC interactions
@@ -172,15 +174,13 @@ std::optional<FEXCore::ExecutableFileSectionInfo>
SyscallHandler::LookupExecutableFileSection(FEXCore::Core::InternalThreadState& Thread, uint64_t GuestAddr) {
auto lk = FEXCore::GuardSignalDeferringSection<std::shared_lock>(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<Elf64_Phdr> ReadELFHeaders(int FD, std::span<std::byte> 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::LateApplyExtendedVolatileMetadata> 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<FEXCore::ExecutableFileInfo>();
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<std::byte*>(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::LateApplyExtendedVolatileMetadata> 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<uint64_t>(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));
}
@@ -8,6 +8,8 @@
#include <FEXCore/fextl/memory.h>
#include <FEXCore/Utils/SignalScopeGuards.h>
#include <elf.h>
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<MRID, MappedResource>;
using ContainerType = fextl::multimap<MRID, MappedResource>;
fextl::unique_ptr<FEXCore::ExecutableFileInfo> 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<Elf64_Phdr> 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;
};
+5 -4
View File
@@ -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)
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
#include <catch2/catch_all.hpp>
#include <Common/FileMappingBaseAddress.h>
#include <FEXCore/fextl/vector.h>
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<Elf64_Phdr> 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<Mapping> 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<Elf64_Phdr> 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<Mapping> 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<Elf64_Phdr> 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<Mapping> 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);
}
}