Files
FEX-Emu--FEX/Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsSMCTracking.cpp
T
Ryan Houdek e3f402a38a DiskCache: Support FileID hash with gnu build-id
This doesn't fully fix #5912 but gets a step closer. Instead of just
hashing the filename, hash in the build-id as well when it exists.

This isn't all encompassing because the build-id may not exist in all
cases. Crypt Of the Necrodancer for example doesn't ship with the
build-id on their 64-bit build. Although their legacy 32-bit build had
it.

The build-id is always a hash, depending on tool it is either 64-bit or
160-bit in all the executables I found. Although it can be anything so
make sure to be flexible enough to support everything.
2026-10-02 17:38:09 -07:00

856 lines
36 KiB
C++

// SPDX-License-Identifier: MIT
/*
$info$
category: LinuxSyscalls ~ Linux syscall emulation, marshaling and passthrough
tags: LinuxSyscalls|common
desc: SMC/MMan Tracking
$end_info$
*/
#include <Common/Config.h>
#include "Common/FDUtils.h"
#include "Common/FEXServerClient.h"
#include "Common/FileMappingBaseAddress.h"
#include <filesystem>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/personality.h>
#include <sys/shm.h>
#include <xxhash.h>
#include "LinuxSyscalls/Syscalls.h"
#include "LinuxSyscalls/SignalDelegator.h"
#include <FEXCore/Debug/InternalThreadState.h>
#include <FEXCore/Utils/LogManager.h>
#include <FEXCore/Utils/MathUtils.h>
#include <FEXCore/Utils/SignalScopeGuards.h>
#include <FEXCore/Utils/TypeDefines.h>
#include <FEXHeaderUtils/Filesystem.h>
#include <Linux/Utils/ELFParser.h>
namespace FEX::HLE {
static void HandleSegfaultForCodeCacheFinalization(FEXCore::Core::InternalThreadState& Thread, FEXCore::MappedCodeCacheFile& Code,
uintptr_t FaultAddress) {
FEXCORE_PROFILE_SCOPED("Load code cache page");
size_t PageIdx = (reinterpret_cast<std::byte*>(FaultAddress) - Code.CodeBuffer.data()) / FEXCore::Utils::FEX_PAGE_SIZE;
auto RangeToFinalize = Thread.CTX->GetCodeCache().SelectCodeRangeToFinalize(Code, PageIdx, PageIdx + 1);
if (!RangeToFinalize.empty()) {
Thread.CTX->GetCodeCache().FinalizeCodePages(Code, RangeToFinalize);
}
}
// Handles segfaults from:
// - call-ret shadow stack overflow
// - guest-side self-modifying code (SMC)
// - lazy loading of mapped code cache pages
bool SyscallHandler::HandleSegfault(FEXCore::Core::InternalThreadState* Thread, int Signal, void* info, void* ucontext) {
const auto FaultAddress = (uintptr_t)((siginfo_t*)info)->si_addr;
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromFEXCoreThread(Thread);
auto CallRetStackInfo = ThreadObject->GetCallRetStackInfo();
if (FaultAddress >= CallRetStackInfo.AllocationBase && FaultAddress < CallRetStackInfo.AllocationEnd) {
// Reset REG_CALLRET_SP to the default location to allow for underflows/overflows
ArchHelpers::Context::SetArmReg(ucontext, 25, CallRetStackInfo.DefaultLocation);
return true;
}
{
// Can't use the deferred signal lock in the SIGSEGV handler.
auto lk = FEXCore::MaskSignalsAndLockMutex<std::shared_lock>(_SyscallHandler->VMATracking.Mutex);
auto& VMATracking = _SyscallHandler->VMATracking;
// If the write spans two pages, they will be flushed one at a time (generating two faults)
auto Entry = VMATracking.FindVMAEntry(FaultAddress);
if (Entry == VMATracking.VMAs.end()) {
// Not a guest page; check mapped code cache pages
auto* Code = VMATracking.FindMappedCodeCacheByHostAddress(FaultAddress);
if (!Code) {
// Untracked address; not handled here
return false;
}
std::lock_guard lk(_SyscallHandler->CodeCachePatchingMutex);
HandleSegfaultForCodeCacheFinalization(*Thread, *Code, FaultAddress);
return true;
}
// If the mapping wasn't writable, it can't be handled here
if (!Entry->second.Prot.Writable) {
return false;
}
auto FaultBase = FEXCore::AlignDown(FaultAddress, FEXCore::Utils::FEX_PAGE_SIZE);
auto UnprotectRegionCallback = [](uintptr_t Start, uintptr_t Length) {
auto rv = mprotect((void*)Start, Length, PROT_READ | PROT_WRITE);
LogMan::Throw::AFmt(rv == 0, "mprotect({}, {}) failed", Start, Length);
};
if (Entry->second.Flags.Shared) {
LOGMAN_THROW_A_FMT(Entry->second.Resource, "VMA tracking error");
auto Offset = FaultBase - Entry->first + Entry->second.Offset;
auto VMA = Entry->second.Resource->FirstVMA;
LOGMAN_THROW_A_FMT(VMA, "VMA tracking error");
// Flush all mirrors, remap the page writable as needed
do {
if (VMA->Offset <= Offset && (VMA->Offset + VMA->Length) > Offset) {
auto FaultBaseMirrored = Offset - VMA->Offset + VMA->Base;
if (VMA->Prot.Writable) {
_SyscallHandler->TM.InvalidateGuestCodeRange(Thread, FaultBaseMirrored, FEXCore::Utils::FEX_PAGE_SIZE, UnprotectRegionCallback);
} else {
_SyscallHandler->TM.InvalidateGuestCodeRange(Thread, FaultBaseMirrored, FEXCore::Utils::FEX_PAGE_SIZE);
}
}
} while ((VMA = VMA->ResourceNextVMA));
} else {
_SyscallHandler->TM.InvalidateGuestCodeRange(Thread, FaultBase, FEXCore::Utils::FEX_PAGE_SIZE, UnprotectRegionCallback);
}
FEXCORE_PROFILE_INSTANT_INCREMENT(Thread, AccumulatedSMCCount, 1);
auto CTX = Thread->CTX;
if (CTX->IsAddressInCodeBuffer(Thread, ArchHelpers::Context::GetPc(ucontext)) && !CTX->IsCurrentBlockSingleInst(Thread) &&
CTX->IsAddressInCurrentBlock(Thread, FaultAddress & FEXCore::Utils::FEX_PAGE_MASK, FEXCore::Utils::FEX_PAGE_SIZE)) {
// If we are not in a single-instruction block, and the SMC write address could intersect with the current block,
// reconstruct the context and repeat the faulting instruction as a single-instruction block so any SMC it performs
// is immediately picked up.
ThreadObject->SignalInfo.Delegator->SpillSRA(Thread, ucontext, Thread->CurrentFrame->InSyscallInfo & 0xFFFF);
// Adjust context to return to the dispatcher, reloading SRA from thread state
const auto& Config = ThreadObject->SignalInfo.Delegator->GetConfig();
ArchHelpers::Context::SetPc(ucontext, Config.AbsoluteLoopTopAddressFillSRA);
ArchHelpers::Context::SetArmReg(ucontext, 1, 1); // Set ENTRY_FILL_SRA_SINGLE_INST_REG to force a single step
}
return true;
}
}
void SyscallHandler::MarkGuestExecutableRange(FEXCore::Core::InternalThreadState* Thread, uint64_t Start, uint64_t Length) {
const auto Base = Start & FEXCore::Utils::FEX_PAGE_MASK;
const auto Top = FEXCore::AlignUp(Start + Length, FEXCore::Utils::FEX_PAGE_SIZE);
{
if (SMCChecks != FEXCore::Config::CONFIG_SMC_MTRACK) {
return;
}
auto lk = FEXCore::GuardSignalDeferringSection<std::shared_lock>(VMATracking.Mutex, Thread);
// Find the first mapping at or after the range ends, or ::end().
// Top points to the address after the end of the range
auto Mapping = VMATracking.VMAs.lower_bound(Top);
while (Mapping != VMATracking.VMAs.begin()) {
Mapping--;
const auto MapBase = Mapping->first;
const auto MapTop = MapBase + Mapping->second.Length;
if (MapTop <= Base) {
// Mapping ends before the Range start, exit
break;
} else {
const auto ProtectBase = std::max(MapBase, Base);
const auto ProtectSize = std::min(MapTop, Top) - ProtectBase;
if (Mapping->second.Flags.Shared) {
LOGMAN_THROW_A_FMT(Mapping->second.Resource, "VMA tracking error");
const auto OffsetBase = ProtectBase - Mapping->first + Mapping->second.Offset;
const auto OffsetTop = OffsetBase + ProtectSize;
auto VMA = Mapping->second.Resource->FirstVMA;
LOGMAN_THROW_A_FMT(VMA, "VMA tracking error");
do {
auto VMAOffsetBase = VMA->Offset;
auto VMAOffsetTop = VMA->Offset + VMA->Length;
auto VMABase = VMA->Base;
if (VMA->Prot.Writable && VMAOffsetBase < OffsetTop && VMAOffsetTop > OffsetBase) {
const auto MirroredBase = std::max(VMAOffsetBase, OffsetBase);
const auto MirroredSize = std::min(OffsetTop, VMAOffsetTop) - MirroredBase;
auto rv = mprotect((void*)(MirroredBase - VMAOffsetBase + VMABase), MirroredSize, PROT_READ);
LogMan::Throw::AFmt(rv == 0, "mprotect({}, {}) failed", MirroredBase, MirroredSize);
}
} while ((VMA = VMA->ResourceNextVMA));
} else if (Mapping->second.Prot.Writable) {
int rv = mprotect((void*)ProtectBase, ProtectSize, PROT_READ);
LogMan::Throw::AFmt(rv == 0, "mprotect({}, {}) failed", ProtectBase, ProtectSize);
}
}
}
}
}
void SyscallHandler::InvalidateGuestCodeRange(FEXCore::Core::InternalThreadState* Thread, uint64_t Start, uint64_t Length) {
InvalidateCodeRangeIfNecessary(Thread, Start, Length, false);
}
static FEXCore::ExecutableFileSectionInfo BuildSectionInfo(const VMATracking::MappedResource& Resource, uint64_t Base, uint64_t Size) {
return FEXCore::ExecutableFileSectionInfo {*Resource.MappedFile, Resource.FirstVMA->Base, Base, Base + Size};
}
std::optional<FEXCore::ExecutableFileSectionInfo>
SyscallHandler::LookupExecutableFileSection(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestAddr) {
auto lk = FEXCore::GuardSignalDeferringSection<std::shared_lock>(VMATracking.Mutex, Thread);
auto EntryIt = VMATracking.FindVMAEntry(GuestAddr);
if (EntryIt == VMATracking.VMAs.end() || !EntryIt->second.Resource || !EntryIt->second.Resource->MappedFile) {
return std::nullopt;
}
auto& [MappingBaseAddr, Entry] = *EntryIt;
return BuildSectionInfo(*Entry.Resource, MappingBaseAddr, Entry.Length);
}
FEXCore::HLE::ExecutableRangeInfo SyscallHandler::QueryGuestExecutableRange(FEXCore::Core::InternalThreadState* Thread, uint64_t Address) {
auto lk = FEXCore::GuardSignalDeferringSection<std::shared_lock>(VMATracking.Mutex, Thread);
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromFEXCoreThread(Thread);
auto Entry = VMATracking.FindVMAEntry(Address);
if (Entry == VMATracking.VMAs.end() ||
(!Entry->second.Prot.Executable && (!(ThreadObject->persona & READ_IMPLIES_EXEC) || !Entry->second.Prot.Readable))) {
return {0, 0, false};
}
return {Entry->first, Entry->second.Length, Entry->second.Prot.Writable};
}
struct ReadELFHeadersResult {
fextl::vector<Elf64_Phdr> ProgramHeaders;
fextl::robin_map<uint32_t, FEXCore::GuestRelocationType> Relocations;
fextl::vector<uint8_t> BuildID;
bool HasCodeRelocations;
};
static ReadELFHeadersResult 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
}
// Re-open the file with a fresh file descriptor (and let ELFParser close it on return).
// NOTE: FDs returned by dup() share the same cursor state, so reading from them would have observable side effects.
auto NewFD = open(fextl::fmt::format("/proc/self/fd/{}", FD).c_str(), O_RDONLY);
ELFParser Parser;
if (!Parser.ReadElf(NewFD)) {
return {};
}
auto Relocations = Parser.PopulateRelocations();
auto HasCodeRelocations = Parser.HasCodeRelocations();
auto buildid = Parser.GetBuildID();
return ReadELFHeadersResult {std::move(Parser.phdrs), std::move(Relocations), buildid, HasCodeRelocations};
}
static fextl::unique_ptr<FEXCore::MappedCodeCacheFile>
LoadCodeCache(FEXCore::Core::InternalThreadState& Thread, VMATracking::VMATracking& VMATracking,
const FEXCore::ExecutableFileInfo& FileInfo, uint64_t CodeCacheConfigId, uint64_t FileStartVA) {
auto& CodeCache = Thread.CTX->GetCodeCache();
auto CacheFilename = fextl::fmt::format("{}cache/{}-{:016x}", FEX::Config::GetCacheDirectory(),
FEXCore::CodeMap::GetBaseFilename(FileInfo, false), CodeCacheConfigId);
int CacheFD = open(CacheFilename.c_str(), O_RDONLY);
if (CacheFD == -1) {
LogMan::Msg::IFmt("Cache file does not exist: {}", CacheFilename);
return nullptr;
}
struct stat buf;
if (fstat(CacheFD, &buf) != 0) {
LogMan::Msg::EFmt("Invalid cache file: {}", CacheFilename);
close(CacheFD);
return nullptr;
}
auto CacheFileSize = static_cast<std::size_t>(buf.st_size);
auto MappedCache = (std::byte*)FEXCore::Allocator::mmap(nullptr, CacheFileSize, PROT_READ, MAP_PRIVATE, CacheFD, 0);
close(CacheFD);
if (!MappedCache || MappedCache == MAP_FAILED) {
LogMan::Msg::EFmt("Failed to map code cache into memory");
return nullptr;
}
auto Result = CodeCache.LoadCache(std::span {MappedCache, CacheFileSize}, FileInfo, FileStartVA);
if (!Result) {
FEXCore::Allocator::munmap(MappedCache, CacheFileSize);
return nullptr;
}
// NOTE: This is synchronized by acquiring VMATracking.Mutex at call site
CodeCache.RegisterMappedCodeBuffer(*Result);
return Result;
}
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");
uint64_t Result;
size_t Size = FEXCore::AlignUp(length, FEXCore::Utils::FEX_PAGE_SIZE);
std::optional<LateApplyExtendedVolatileMetadata> LateMetadata = std::nullopt;
std::optional<FEXCore::ExecutableFileSectionInfo> CachedSection;
bool PendingResourceDeletion;
{
// NOTE: Frontend calls this with a nullptr Thread during initialization, but
// providing this code with a valid Thread object earlier would allow
// us to be more optimal by using GuardSignalDeferringSection instead
auto lk = FEXCore::GuardSignalDeferringSectionWithFallback(VMATracking.Mutex, Thread);
bool Map32Bit = !Is64Bit || (flags & FEX::HLE::X86_64_MAP_32BIT);
if (Map32Bit) {
Result = (uint64_t)Get32BitAllocator()->Mmap((void*)addr, length, prot, flags, fd, offset);
if (FEX::HLE::HasSyscallError(Result)) {
return reinterpret_cast<void*>(Result);
}
LOGMAN_THROW_A_FMT(Is64Bit || (Result >> 32) == 0 || (Result >> 32) == 0xFFFFFFFF, "values must fit to 32 bits");
} else {
Result = reinterpret_cast<uint64_t>(::mmap(reinterpret_cast<void*>(addr), length, prot, flags, fd, offset));
if (Result == ~0ULL) {
return reinterpret_cast<void*>(-errno);
}
}
LateMetadata = TrackMmap(Thread, Result, length, prot, flags, fd, offset, CachedSection);
PendingResourceDeletion = VMATracking.HasPendingResourceDeletions();
}
InvalidateCodeRangeIfNecessary(Thread, Result, Size, PendingResourceDeletion);
if (LateMetadata) {
auto CodeInvalidationlk = FEXCore::GuardSignalDeferringSectionWithFallback(CTX->GetCodeInvalidationMutex(), Thread);
CTX->AddForceTSOInformation(LateMetadata->VolatileValidRanges, std::move(LateMetadata->VolatileInstructions));
}
if (EnableCodeCaching && CachedSection) {
Thread->CTX->GetCodeCache().EnableLoadedSection(
Thread, *static_cast<const VMATracking::ExecutableFileState&>(CachedSection->FileInfo).MappedCache, *CachedSection);
}
return reinterpret_cast<void*>(Result);
}
uint64_t SyscallHandler::GuestMunmap(bool Is64Bit, FEXCore::Core::InternalThreadState* Thread, void* addr, uint64_t length) {
LOGMAN_THROW_A_FMT(Is64Bit || (reinterpret_cast<uintptr_t>(addr) >> 32) == 0, "values must fit to 32 bits: {}", fmt::ptr(addr));
LOGMAN_THROW_A_FMT(Is64Bit || (length >> 32) == 0, "values must fit to 32 bits");
uint64_t Result;
uint64_t Size = FEXCore::AlignUp(length, FEXCore::Utils::FEX_PAGE_SIZE);
bool PendingResourceDeletion;
{
// Frontend calls this with nullptr Thread during initialization.
// This is why `GuardSignalDeferringSectionWithFallback` is used here.
// To be more optimal the frontend should provide this code with a valid Thread object earlier.
auto lk = FEXCore::GuardSignalDeferringSectionWithFallback(VMATracking.Mutex, Thread);
if (reinterpret_cast<uintptr_t>(addr) < 0x1'0000'0000ULL) {
Result = Get32BitAllocator()->Munmap(addr, length);
if (FEX::HLE::HasSyscallError(Result)) {
return Result;
}
} else {
Result = ::munmap(addr, length);
if (Result == -1) {
return -errno;
}
}
TrackMunmap(Thread, addr, length);
PendingResourceDeletion = VMATracking.HasPendingResourceDeletions();
}
InvalidateCodeRangeIfNecessary(Thread, reinterpret_cast<uint64_t>(addr), Size, PendingResourceDeletion);
if (length) {
auto CodeInvalidationlk = FEXCore::GuardSignalDeferringSectionWithFallback(CTX->GetCodeInvalidationMutex(), Thread);
CTX->RemoveForceTSOInformation(reinterpret_cast<uint64_t>(addr), length);
}
return Result;
}
uint64_t SyscallHandler::GuestMremap(bool Is64Bit, FEXCore::Core::InternalThreadState* Thread, void* old_address, size_t old_size,
size_t new_size, int flags, void* new_address) {
uint64_t Result {};
{
auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, Thread);
if (Is64Bit) {
Result = reinterpret_cast<uint64_t>(::mremap(old_address, old_size, new_size, flags, new_address));
if (Result == -1) {
return -errno;
}
} else {
Result = reinterpret_cast<uint64_t>(Get32BitAllocator()->Mremap(old_address, old_size, new_size, flags, new_address));
if (FEX::HLE::HasSyscallError(Result)) {
return Result;
}
}
TrackMremap(Thread, reinterpret_cast<uint64_t>(old_address), old_size, new_size, flags, Result);
}
InvalidateCodeRangeIfNecessaryOnRemap(Thread, reinterpret_cast<uint64_t>(old_address), Result, old_size, new_size);
return Result;
}
void SyscallHandler::TriggerGuestLibWrapperCodeCacheLoad(FEXCore::Core::InternalThreadState& Thread, uint64_t AnyAddr) {
if (!EnableCodeCaching) {
return;
}
// TODO: Instead of deferring the entire cache load, only delay applicance of sha256 relocations!
auto lk = FEXCore::GuardSignalDeferringSection<std::shared_lock>(VMATracking.Mutex, &Thread);
auto VMAEntry = VMATracking.FindVMAEntry(reinterpret_cast<uint64_t>(AnyAddr));
for (auto* VMA = VMAEntry->second.Resource->FirstVMA; VMA; VMA = VMA->ResourceNextVMA) {
if (!VMA->Prot.Executable) {
continue;
}
auto SectionInfo = BuildSectionInfo(*VMAEntry->second.Resource, VMA->Base, VMA->Length);
LoadCodeCache(Thread, VMATracking, SectionInfo.FileInfo, CodeCacheConfigId, SectionInfo.FileStartVA);
}
}
int SyscallHandler::OpenCodeMapFile() {
// Query from FEXServer whether this is the first instance of this executable; if it is, also enable code dumping!
FEX_CONFIG_OPT(RootFSPath, ROOTFS);
FEX_CONFIG_OPT(Multiblock, MULTIBLOCK);
auto ProgramName = FEXCore::Config::Get(FEXCore::Config::CONFIG_APP_FILENAME);
LOGMAN_THROW_A_FMT(ProgramName && ProgramName.value()->c_str()[0] == '/', "");
// Check RootFS first, then the plain path
auto ProgramFD = open((RootFSPath() + ProgramName.value()->c_str()).c_str(), O_RDONLY);
if (ProgramFD == -1) {
ProgramFD = open(ProgramName.value()->c_str(), O_RDONLY);
}
if (ProgramFD == -1) {
return -1;
}
int CodeMapFD = FEXServerClient::RequestCodeMapFD(FEXServerClient::GetServerFD(), ProgramFD, Multiblock);
close(ProgramFD);
if (CodeMapFD == -1) {
return -1;
}
// Acquire exclusive lock to prevent FEXServer from processing this file eagerly
[[maybe_unused]] auto ret = flock(CodeMapFD, LOCK_EX);
LOGMAN_THROW_A_FMT(ret == 0, "Could not lock code map");
FM.SetProtectedCodeMapFD(CodeMapFD);
// Ensure the file descriptor is closed on exec
auto flags = fcntl(CodeMapFD, F_GETFD);
fcntl(CodeMapFD, F_SETFD, flags | FD_CLOEXEC);
return CodeMapFD;
}
uint64_t SyscallHandler::GuestMprotect(FEXCore::Core::InternalThreadState* Thread, void* addr, size_t len, int prot) {
uint64_t Result {};
{
auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, Thread);
Result = ::mprotect(addr, len, prot);
if (Result == -1) {
return -errno;
}
TrackMprotect(Thread, addr, len, prot);
}
InvalidateCodeRangeIfNecessary(Thread, reinterpret_cast<uint64_t>(addr), len, false);
// Prepare for delayed code cache load after ld/Wine is done applying relocations.
// Hooking into mprotect is a reliable heuristic that matches behavior of ld (for ELF) and Wine (for PE).
// False-positives are avoided by setting RequiresDelayedCacheLoad in TrackMmap only for
// binaries that we know will go through this path.
fextl::vector<FEXCore::ExecutableFileSectionInfo> CachedSections;
if (EnableCodeCaching && (prot & PROT_EXEC) && (prot & PROT_WRITE) == 0) {
auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, Thread);
auto VMAEntry = VMATracking.FindVMAEntry(reinterpret_cast<uint64_t>(addr));
auto Resource = VMAEntry != VMATracking.VMAs.end() ? VMAEntry->second.Resource : nullptr;
if (Resource && Resource->MappedFile && Resource->RequiresDelayedCacheLoad) {
Resource->RequiresDelayedCacheLoad = false;
LogMan::Msg::IFmt("Triggering delayed cache load for {} after mprotect of {:#x}-{:#x}", Resource->MappedFile->Filename,
VMAEntry->first, VMAEntry->first + VMAEntry->second.Length);
for (auto VMA = Resource->FirstVMA; VMA; VMA = VMA->ResourceNextVMA) {
CachedSections.push_back(BuildSectionInfo(*Resource, VMA->Base, VMA->Length));
}
}
}
// Trigger delayed cache load. This must be done separately since
// EnableLoadedSection will call interfaces that acquire the VMATracking mutex.
for (auto& CachedSection : CachedSections) {
auto Cache = static_cast<const VMATracking::ExecutableFileState&>(CachedSection.FileInfo).MappedCache.get();
if (Cache) {
Thread->CTX->GetCodeCache().EnableLoadedSection(Thread, *Cache, CachedSection);
}
}
return Result;
}
uint64_t SyscallHandler::GuestShmat(bool Is64Bit, FEXCore::Core::InternalThreadState* Thread, int shmid, const void* shmaddr, int shmflg) {
uint64_t Result;
uint64_t Length;
bool PendingResourceDeletion;
{
auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, Thread);
if (Is64Bit) {
Result = reinterpret_cast<uint64_t>(::shmat(shmid, shmaddr, shmflg));
if (Result == -1) {
return -errno;
}
} else {
uint32_t Addr;
Result = Get32BitAllocator()->Shmat(shmid, shmaddr, shmflg, &Addr);
if (FEX::HLE::HasSyscallError(Result)) {
return Result;
}
Result = Addr;
}
shmid_ds stat;
auto res = shmctl(shmid, IPC_STAT, &stat);
LOGMAN_THROW_A_FMT(res != -1, "shmctl IPC_STAT failed");
Length = stat.shm_segsz;
TrackShmat(Thread, shmid, Result, shmflg, Length);
PendingResourceDeletion = VMATracking.HasPendingResourceDeletions();
}
InvalidateCodeRangeIfNecessary(Thread, Result, Length, PendingResourceDeletion);
return Result;
}
uint64_t SyscallHandler::GuestShmdt(bool Is64Bit, FEXCore::Core::InternalThreadState* Thread, const void* shmaddr) {
uint64_t Result;
uint64_t Length;
bool PendingResourceDeletion;
{
auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, Thread);
if (Is64Bit) {
Result = ::shmdt(shmaddr);
if (Result == -1) {
return -errno;
}
} else {
Result = Get32BitAllocator()->Shmdt(shmaddr);
if (FEX::HLE::HasSyscallError(Result)) {
return Result;
}
}
Length = TrackShmdt(Thread, reinterpret_cast<uintptr_t>(shmaddr));
PendingResourceDeletion = VMATracking.HasPendingResourceDeletions();
}
InvalidateCodeRangeIfNecessary(Thread, reinterpret_cast<uintptr_t>(shmaddr), Length, PendingResourceDeletion);
return Result;
}
/**
* Computes a unique identifier for the referenced binary file to be used for
* generating the code map.
* This identifier is independent of FEX build/runtime configuration and
* stable across FEX updates.
*/
static uint64_t ComputeCodeMapId(std::string_view Filename, std::optional<fextl::vector<uint8_t>*> BuildID = std::nullopt) {
// Use a combination of filename and buildid if it exists.
// Not all executables have buildid so this isn't an all encompassing solution.
// `Crypt of the Necrodancer` as an example doesn't have a BuildID on Linux.
uint64_t Hash = ~0ULL;
if (!Filename.empty()) {
Hash = XXH3_64bits(Filename.data(), Filename.size());
}
if (BuildID) {
auto Data = (*BuildID)->data();
auto Size = (*BuildID)->size();
if (Size == 8) {
auto ID64Bit = reinterpret_cast<uint64_t*>(Data);
Hash ^= *ID64Bit;
} else {
Hash ^= XXH3_64bits(Data, Size);
}
}
return Hash;
}
// MMan Tracking
std::optional<SyscallHandler::LateApplyExtendedVolatileMetadata>
SyscallHandler::TrackMmap(FEXCore::Core::InternalThreadState* Thread, uint64_t addr, size_t length, int prot, int flags, int fd,
off_t offset, std::optional<FEXCore::ExecutableFileSectionInfo>& CachedSection) {
size_t Size = FEXCore::AlignUp(length, FEXCore::Utils::FEX_PAGE_SIZE);
const auto ProtMapping = VMATracking::VMAProt::fromProt(prot);
VMATracking::MappedResource* Resource = nullptr;
std::optional<SyscallHandler::LateApplyExtendedVolatileMetadata> VolatileMetadata = std::nullopt;
if (!(flags & MAP_ANONYMOUS)) {
struct stat64 buf;
fstat64(fd, &buf);
const VMATracking::MRID mrid {buf.st_dev, buf.st_ino};
char Tmp[PATH_MAX];
auto PathLength = FEX::get_fdpath(fd, Tmp);
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, VMATracking::MappedResource {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 ((prot & PROT_READ) && Inserted) {
Resource->MappedFile = fextl::make_unique<VMATracking::ExecutableFileState>();
Resource->MappedFile->Filename = fextl::string(Tmp, PathLength);
// Read ELF headers if applicable and needed for code caching.
// For performance, skip ELF checks if we're not mapping the file header
bool CheckForElfFile = (offset == 0) && EnableCodeCaching;
bool CheckForElfFileDiskCache = (offset == 0) && EnableDiskCache;
#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED
CheckForElfFile = true;
#endif
if (CheckForElfFile) {
auto ELFResult = ReadELFHeaders(fd, std::span {reinterpret_cast<std::byte*>(addr), length});
Resource->ProgramHeaders = std::move(ELFResult.ProgramHeaders);
Resource->MappedFile->Relocations = std::move(ELFResult.Relocations);
Resource->RequiresDelayedCacheLoad = ELFResult.HasCodeRelocations;
Resource->MappedFile->FileId = ComputeCodeMapId(Resource->MappedFile->Filename, &ELFResult.BuildID);
// GuestRelocationType::Skip indicates to FEXOfflineCompiler that
// any blocks covered by the relocation may not be cached.
// At runtime, we can safely drop these relocations.
for (auto it = Resource->MappedFile->Relocations.begin(); it != Resource->MappedFile->Relocations.end();) {
if (it->second == FEXCore::GuestRelocationType::Skip) {
it = Resource->MappedFile->Relocations.erase(it);
} else {
++it;
}
}
uint64_t MinAddress = 0;
uint64_t MaxAddress = 0;
for (auto& ProgramHeader : Resource->ProgramHeaders) {
if (ProgramHeader.p_type == PT_LOAD) {
if (!MinAddress && !MaxAddress) {
MinAddress = ProgramHeader.p_vaddr;
MaxAddress = ProgramHeader.p_vaddr + ProgramHeader.p_memsz;
}
MinAddress = std::min(MinAddress, ProgramHeader.p_vaddr);
MaxAddress = std::max(MaxAddress, ProgramHeader.p_vaddr + ProgramHeader.p_memsz);
}
}
if (MaxAddress > MinAddress) {
Resource->MappedFile->MappedSize = MaxAddress - MinAddress;
}
LOGMAN_THROW_A_FMT(Resource->ProgramHeaders.empty() || offset == 0, "Expected file offset 0 for the first mapping of an ELF "
"file");
} else if (CheckForElfFileDiskCache) {
auto ELFResult = ReadELFHeaders(fd, std::span {reinterpret_cast<std::byte*>(addr), length});
Resource->MappedFile->FileId = ComputeCodeMapId(Resource->MappedFile->Filename, &ELFResult.BuildID);
} else {
Resource->MappedFile->FileId = ComputeCodeMapId(Resource->MappedFile->Filename);
}
} 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 ExpectedBases = FEXCore::InferMappingBaseAddress(
Resource.ProgramHeaders, addr, Size, offset,
(ProtMapping.Executable ? PF_X : 0) | (ProtMapping.Writable ? PF_W : 0) | (ProtMapping.Readable ? PF_R : 0));
return std::ranges::find(ExpectedBases, Resource.FirstVMA->Base) != ExpectedBases.end();
});
if (ResourceIt == ResourceEnd) {
// This isn't necessarily a fatal exception. It just means the ELF section isn't a part of the ELF Program headers.
// Node.js hits this as it maps a section of itself that isn't a part of the program headers.
LogMan::Msg::IFmt("Warning: Could not find base for file mapping at {:#x} (offset {:#x}): {}", addr, offset,
std::string_view(Tmp, PathLength));
} else {
Resource = &ResourceIt->second;
}
}
if (Resource->MappedFile) {
const fextl::string Filename = FHU::Filesystem::GetFilename(Resource->MappedFile->Filename);
// We now have the filename and the offset in the filename getting mapped.
// Check for extended volatile metadata.
auto it = ExtendedMetaData.find(Filename);
if (it != ExtendedMetaData.end()) {
SyscallHandler::LateApplyExtendedVolatileMetadata LateMetadata;
FEX::VolatileMetadata::ApplyFEXExtendedVolatileMetadata(
it->second, LateMetadata.VolatileInstructions, LateMetadata.VolatileValidRanges, addr, addr + length, offset, offset + length);
if (!LateMetadata.VolatileInstructions.empty() || !LateMetadata.VolatileValidRanges.Empty()) {
VolatileMetadata.emplace(std::move(LateMetadata));
}
}
}
}
} else if (flags & MAP_SHARED) {
VMATracking::MRID mrid {VMATracking::SpecialDev::Anon, AnonSharedId++};
auto [Iter, IterEnd] = VMATracking.FindResources(mrid);
LOGMAN_THROW_A_FMT(Iter == IterEnd, "VMA tracking error");
Iter = VMATracking.InsertMappedResource(mrid, VMATracking::MappedResource {nullptr, nullptr, 0, {}, {}});
Resource = &Iter->second;
Resource->Iterator = Iter;
}
VMATracking.TrackVMARange(CTX, Resource, addr, offset, Size, VMATracking::VMAFlags::fromFlags(flags), VMATracking::VMAProt::fromProt(prot));
// Load code cache if present.
// FEXServer was requested to generate library caches on program launch.
if (EnableCodeCaching && Resource && Resource->MappedFile && VMATracking::VMAProt::fromProt(prot).Executable) {
if (!Resource->MappedFile->AttemptedCacheLoad) {
Resource->MappedFile->MappedCache = LoadCodeCache(*Thread, VMATracking, *Resource->MappedFile, CodeCacheConfigId, Resource->FirstVMA->Base);
Resource->MappedFile->AttemptedCacheLoad = true;
}
if (!Resource->MappedFile->MappedCache) {
// No cache present
} else if (Resource->MappedFile->Filename.ends_with("-guest.so")) {
// For guest library wrappers, cache loading must be delayed until LoadLib is called.
// Before that, we can't patch up the SHA256 function identifiers.
LogMan::Msg::IFmt("Delaying code cache load for {}", Resource->MappedFile->Filename);
} else if (Thread) {
if (!Resource->RequiresDelayedCacheLoad) {
CachedSection.emplace(BuildSectionInfo(*Resource, addr, Size));
} else {
LogMan::Msg::IFmt("Delaying code cache load for {} until mprotect {:#x}-{:#x}", Resource->MappedFile->Filename, addr, addr + Size);
}
} else {
// Cache can't be loaded with a thread; skip this for now
LogMan::Msg::DFmt("Oops, tried caching without a thread: {}", Resource->MappedFile->Filename);
}
}
return VolatileMetadata;
}
void SyscallHandler::AddVirtualPage(FEXCore::Core::InternalThreadState* Thread, uint64_t addr, size_t length, int prot) {
auto lk = FEXCore::GuardSignalDeferringSectionWithFallback(VMATracking.Mutex, Thread);
VMATracking.TrackVMARange(CTX, nullptr, addr, 0, length, VMATracking::VMAFlags::fromFlags(MAP_ANONYMOUS | MAP_PRIVATE),
VMATracking::VMAProt::fromProt(prot));
}
void SyscallHandler::TrackMunmap(FEXCore::Core::InternalThreadState* Thread, void* addr, size_t length) {
uint64_t Size = FEXCore::AlignUp(length, FEXCore::Utils::FEX_PAGE_SIZE);
VMATracking.DeleteVMARange(CTX, reinterpret_cast<uintptr_t>(addr), Size);
}
void SyscallHandler::TrackMprotect(FEXCore::Core::InternalThreadState* Thread, void* addr, size_t len, int prot) {
uint64_t Size = FEXCore::AlignUp(len, FEXCore::Utils::FEX_PAGE_SIZE);
VMATracking.ChangeProtectionFlags(reinterpret_cast<uintptr_t>(addr), Size, VMATracking::VMAProt::fromProt(prot));
}
void SyscallHandler::TrackMremap(FEXCore::Core::InternalThreadState* Thread, uint64_t OldAddress, size_t OldSize, size_t NewSize, int flags,
uint64_t NewAddress) {
OldSize = FEXCore::AlignUp(OldSize, FEXCore::Utils::FEX_PAGE_SIZE);
NewSize = FEXCore::AlignUp(NewSize, FEXCore::Utils::FEX_PAGE_SIZE);
const auto OldVMA = VMATracking.FindVMAEntry(OldAddress);
const auto OldResource = OldVMA->second.Resource;
const auto OldOffset = OldVMA->second.Offset + OldAddress - OldVMA->first;
const auto OldFlags = OldVMA->second.Flags;
const auto OldProt = OldVMA->second.Prot;
LOGMAN_THROW_A_FMT(OldVMA != VMATracking.VMAs.end(), "VMA Tracking corruption");
if (OldSize == 0) {
// Mirror existing mapping
// must be a shared mapping
LOGMAN_THROW_A_FMT(OldResource != nullptr, "VMA Tracking error");
LOGMAN_THROW_A_FMT(OldFlags.Shared, "VMA Tracking error");
VMATracking.TrackVMARange(CTX, OldResource, NewAddress, OldOffset, NewSize, OldFlags, OldProt);
} else {
#ifndef MREMAP_DONTUNMAP
// MREMAP_DONTUNMAP is kernel 5.7+ and might not exist
#define MREMAP_DONTUNMAP 4
#endif
if (!(flags & MREMAP_DONTUNMAP)) {
VMATracking.DeleteVMARange(CTX, OldAddress, OldSize, OldResource);
}
// Make anonymous mapping
VMATracking.TrackVMARange(CTX, OldResource, NewAddress, OldOffset, NewSize, OldFlags, OldProt);
}
}
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, IterEnd] = VMATracking.FindResources(mrid);
if (Iter == IterEnd) {
Iter = VMATracking.InsertMappedResource(mrid, VMATracking::MappedResource {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));
}
uint64_t SyscallHandler::TrackShmdt(FEXCore::Core::InternalThreadState* Thread, uint64_t shmaddr) {
return VMATracking.DeleteSHMRegion(CTX, reinterpret_cast<uintptr_t>(shmaddr));
}
void SyscallHandler::TrackMadvise(FEXCore::Core::InternalThreadState* Thread, uintptr_t Base, uintptr_t Size, int advice) {
Size = FEXCore::AlignUp(Size, FEXCore::Utils::FEX_PAGE_SIZE);
{
auto lk = FEXCore::GuardSignalDeferringSection(VMATracking.Mutex, Thread);
// TODO
}
}
} // namespace FEX::HLE