Skip to content

Commit 9fed892

Browse files
neobrainbylaws
authored andcommitted
CodeCache: Ensure atomicity of code page finalization
1 parent 40d5b59 commit 9fed892

4 files changed

Lines changed: 89 additions & 23 deletions

File tree

FEXCore/Source/Interface/Context/Context.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ class CodeCache : public AbstractCodeCache {
118118
* Note that FEX relocations are unrelated to ELF/PE relocations.
119119
*
120120
* @param GuestDelta Guest address offset to apply to RIP-relative data
121+
* @param RelocationOffset Offset to subtract from relocation target offsets
121122
* @param ForStorage True for serializing data (producing deterministic output); false for de-serializing it (resolving dynamic symbols)
122123
*
123124
* @return Returns true on success
124125
*/
125126
[[nodiscard]]
126-
bool ApplyCodeRelocations(uint64_t GuestDelta, std::span<std::byte> Code, std::span<const CPU::Relocation> Relocations, bool ForStorage);
127+
bool ApplyCodeRelocations(uint64_t GuestDelta, std::span<std::byte> Code, std::span<const CPU::Relocation> Relocations,
128+
uint32_t RelocationOffset, bool ForStorage);
127129
};
128130

129131
class ContextImpl final : public FEXCore::Context::Context, public CPU::CodeBufferManager {

FEXCore/Source/Interface/Core/CodeCache.cpp

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ MappedCodeCacheFile::~MappedCodeCacheFile() {
4545
if (CacheManager) {
4646
CacheManager->UnregisterMappedCodeBuffer(*this);
4747
}
48+
49+
#ifndef _WIN32
50+
if (CodeBuffer.empty()) {
51+
FEXCore::Allocator::munmap(CodeBuffer.data(), CodeBuffer.size_bytes());
52+
}
53+
#endif
4854
}
4955

5056
void AbstractCodeCache::RegisterMappedCodeBuffer(MappedCodeCacheFile& Code) {
@@ -346,7 +352,7 @@ bool CodeCache::SaveData(Core::InternalThreadState& Thread, int fd, const Execut
346352

347353
// Dump the host code (relocated for position-independent serialization)
348354
std::span CodeBufferData(reinterpret_cast<std::byte*>(CodeBuffer->Ptr), reinterpret_cast<std::byte*>(CodeBuffer->Ptr) + CTX.LatestOffset);
349-
if (!ApplyCodeRelocations(SerializedBaseAddress, CodeBufferData, Relocations, true)) {
355+
if (!ApplyCodeRelocations(SerializedBaseAddress, CodeBufferData, Relocations, 0, true)) {
350356
LOGMAN_THROW_A_FMT(false, "Failed to apply code relocations");
351357
return false;
352358
}
@@ -428,7 +434,7 @@ void CodeCache::Validate(const ExecutableFileSectionInfo& Section, fextl::set<ui
428434
NewRelocations.erase(std::remove_if(NewRelocations.begin(), NewRelocations.end(), [](const CPU::Relocation& Reloc) {
429435
return Reloc.Header.Type != CPU::RelocationTypes::RELOC_NAMED_SYMBOL_LITERAL && Reloc.Header.Type != CPU::RelocationTypes::RELOC_NAMED_THUNK_MOVE;
430436
}));
431-
(void)ApplyCodeRelocations(Section.FileStartVA, CodeBufferRangeRef, NewRelocations, false);
437+
(void)ApplyCodeRelocations(Section.FileStartVA, CodeBufferRangeRef, NewRelocations, 0, false);
432438

433439
if (ValidationCTX->LatestOffset <= CodeBufferRangeRef.size()) {
434440
// Reference compilation produced fewer bytes than our cache, so validation is going to fail.
@@ -490,11 +496,13 @@ void CodeCache::Validate(const ExecutableFileSectionInfo& Section, fextl::set<ui
490496
}
491497

492498
bool CodeCache::ApplyCodeRelocations(uint64_t GuestEntry, std::span<std::byte> Code,
493-
std::span<const FEXCore::CPU::Relocation> EntryRelocations, bool ForStorage) {
499+
std::span<const FEXCore::CPU::Relocation> EntryRelocations, uint32_t RelocationOffset, bool ForStorage) {
494500
CPU::Arm64Emitter Emitter(&CTX, Code.data(), Code.size_bytes());
495501
for (size_t j = 0; j < EntryRelocations.size(); ++j) {
496502
const FEXCore::CPU::Relocation& Reloc = EntryRelocations[j];
497-
Emitter.SetCursorOffset(Reloc.Header.Offset);
503+
LOGMAN_THROW_A_FMT(Reloc.Header.Offset >= RelocationOffset, "Invalid relocation offset");
504+
LOGMAN_THROW_A_FMT(Reloc.Header.Offset - RelocationOffset < Code.size_bytes(), "Invalid relocation offset");
505+
Emitter.SetCursorOffset(Reloc.Header.Offset - RelocationOffset);
498506

499507
switch (Reloc.Header.Type) {
500508
case FEXCore::CPU::RelocationTypes::RELOC_NAMED_SYMBOL_LITERAL: {
@@ -582,8 +590,20 @@ CodeCache::LoadCache(std::span<std::byte> CacheFile, const ExecutableFileInfo& F
582590
Cursor = reinterpret_cast<std::byte*>(AlignUp(reinterpret_cast<uintptr_t>(Cursor), Utils::FEX_PAGE_SIZE));
583591
auto CodeDataInFile = std::span {Cursor, header.CodeBufferSize};
584592

585-
// Make code data inaccessible until finalized
586-
Allocator::VirtualProtect(CodeDataInFile.data(), header.CodeBufferSize, Allocator::ProtectOptions::None);
593+
#ifndef _WIN32
594+
// Allocate target memory for post-relocation code. This is PROT_NONE until
595+
// the first execution, so that contents can be lazily populated in a
596+
// frontend-provided segfault handler.
597+
void* CodeBufferAllocation = Allocator::mmap(nullptr, header.CodeBufferSize, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
598+
if (CodeBufferAllocation == MAP_FAILED) {
599+
LogMan::Msg::EFmt("Failed to reserve target memory for code cache");
600+
return nullptr;
601+
}
602+
auto CodeBuffer = std::span {static_cast<std::byte*>(CodeBufferAllocation), header.CodeBufferSize};
603+
#else
604+
// TODO: Implement lazy mapping on Windows
605+
auto CodeBuffer = CodeDataInFile;
606+
#endif
587607

588608
// Group relocations by page
589609
size_t NumPages = header.CodeBufferSize / Utils::FEX_PAGE_SIZE;
@@ -600,7 +620,7 @@ CodeCache::LoadCache(std::span<std::byte> CacheFile, const ExecutableFileInfo& F
600620

601621
auto Storage = FEXCore::Allocator::aligned_alloc(alignof(MappedCodeCacheFile), sizeof(MappedCodeCacheFile));
602622
return fextl::unique_ptr<MappedCodeCacheFile>(
603-
new (Storage) MappedCodeCacheFile {this, CacheFile, CodeDataInFile, BlockListStart, header.NumBlocks, header.NumCodePages,
623+
new (Storage) MappedCodeCacheFile {this, CacheFile, CodeDataInFile, CodeBuffer, BlockListStart, header.NumBlocks, header.NumCodePages,
604624
std::move(PageRelocationRanges), fextl::vector<bool>(NumPages), FileStartVA});
605625
}
606626

@@ -674,7 +694,7 @@ bool CodeCache::EnableLoadedSection(Core::InternalThreadState* Thread, MappedCod
674694
}
675695

676696
// Guest code pages
677-
auto* Cursor = Code.CodeBuffer.data() + Code.CodeBuffer.size_bytes();
697+
auto* Cursor = Code.CodeBufferInFile.data() + Code.CodeBufferInFile.size_bytes();
678698
fextl::vector<uint64_t> Entrypoints;
679699
for (uint32_t i = 0; i < Code.NumCodePages; ++i) {
680700
uint64_t CodePage;
@@ -699,7 +719,12 @@ bool CodeCache::EnableLoadedSection(Core::InternalThreadState* Thread, MappedCod
699719
}
700720
}
701721

722+
#ifndef _WIN32
702723
if (!EnableLazyCodeCaching || EnableCodeCacheValidation) {
724+
#else
725+
// TODO: Implement lazy mapping on Windows
726+
if (true) {
727+
#endif
703728
auto Range = SelectCodeRangeToFinalize(Code, 0, Code.CodeBuffer.size_bytes() / Utils::FEX_PAGE_SIZE);
704729
FinalizeCodePages(Code, Range);
705730
}
@@ -792,29 +817,67 @@ void CodeCache::FinalizeCodePages(MappedCodeCacheFile& Code, std::span<std::byte
792817
const size_t StartOffset = CodeRange.data() - Code.CodeBuffer.data();
793818
const auto StartPage = StartOffset / Utils::FEX_PAGE_SIZE;
794819
const auto EndPage = StartPage + CodeRange.size_bytes() / Utils::FEX_PAGE_SIZE;
820+
const size_t Size = CodeRange.size_bytes();
795821

796822
// None of the selected pages should be loaded at all; otherwise, SelectCodeRangeToFinalize returned inconsistent ranges
797823
LOGMAN_THROW_A_FMT(std::find(Code.LoadedPages.begin() + StartPage, Code.LoadedPages.begin() + EndPage, true) == Code.LoadedPages.begin() + EndPage,
798824
"Inconsistent page load state");
799825

800826
FEXCORE_PROFILE_SCOPED("FinalizeCodePages");
801827

802-
// Map writeable to allow applying relocations
803-
// TODO: Are there any remaining race conditions here?
804-
Allocator::VirtualProtect(CodeRange.data(), CodeRange.size_bytes(), Allocator::ProtectOptions::Write);
828+
#ifndef _WIN32
829+
// Atomicity is critical when making the finalized code data visible.
830+
// We ensure this by remapping a temporary buffer onto the PROT_NONE
831+
// placeholder page in CodeBuffer. Some constraints to keep in mind are:
832+
// 1. Pages can't be write-only (readability is implicitly added), so
833+
// we can't change CodeBuffer from PROT_NONE to PROT_WRITE even for just
834+
// a short duration
835+
// 2. Naive mremap from CodeBufferInFile to CodeBuffer would leave a gap in
836+
// the former, which would make cleanup overly complicated
837+
//
838+
// Due to (1), we can't apply relocations in place (CodeBufferInFile); at
839+
// least a secondary buffer is needed for execution (CodeBuffer).
840+
// Due to (2), a third buffer is temporarily allocated here and freed on
841+
// completion. The final code data is computed here and then the memory
842+
// is remapped onto CodeBuffer.
843+
auto* Staging = reinterpret_cast<std::byte*>(Allocator::VirtualAlloc(nullptr, Size, true));
844+
if (!Staging) {
845+
ERROR_AND_DIE_FMT("Failed to allocate {} bytes of staging memory for code-cache finalization", Size);
846+
}
847+
848+
// Copy code from the cache file to the staging buffer
849+
memcpy(Staging, Code.CodeBufferInFile.data() + StartOffset, Size);
805850

806851
// Apply relocations
852+
auto StagingSpan = std::span {Staging, Size};
807853
for (size_t i = StartPage; i < EndPage; ++i) {
808854
auto PageRelocations = SpanPageRelocations(Code, i);
809-
(void)ApplyCodeRelocations(Code.GuestBase, Code.CodeBuffer, PageRelocations, false);
855+
(void)ApplyCodeRelocations(Code.GuestBase, StagingSpan, PageRelocations, static_cast<uint32_t>(StartOffset), false);
810856
Code.LoadedPages[i] = true;
811857
}
812858

813-
ARMEmitter::Emitter::ClearICache(CodeRange.data(), CodeRange.size_bytes());
814-
if (!Allocator::VirtualProtect(CodeRange.data(), CodeRange.size_bytes(),
815-
Allocator::ProtectOptions::Read | Allocator::ProtectOptions::Write | Allocator::ProtectOptions::Exec)) {
816-
ERROR_AND_DIE_FMT("VirtualProtect failed");
859+
// Atomically make the finalized code data visible by remapping the staging
860+
// buffer onto the requested CodeBuffer window. MREMAP_DONTUNMAP is used to
861+
// leave the old VA range reserved so that we can cleanly deallocate it
862+
// through Allocator.
863+
void* RemapResult = ::mremap(Staging, Size, Size, MREMAP_FIXED | MREMAP_MAYMOVE | MREMAP_DONTUNMAP, CodeRange.data());
864+
if (RemapResult == MAP_FAILED) {
865+
ERROR_AND_DIE_FMT("{}: mremap failed: {}", __FUNCTION__, errno);
817866
}
867+
Allocator::VirtualFree(Staging, Size);
868+
869+
// Release resident file pages that will no longer be needed. The VA range is left allocated to allow cleanup with a single VirtualFree.
870+
Allocator::VirtualDontNeed(Code.CodeBufferInFile.data() + StartOffset, Size);
871+
#else
872+
// TODO: Implement lazy mapping on Windows
873+
for (size_t i = StartPage; i < EndPage; ++i) {
874+
auto PageRelocations = SpanPageRelocations(Code, i);
875+
(void)ApplyCodeRelocations(Code.GuestBase, Code.CodeBuffer, PageRelocations, 0, false);
876+
Code.LoadedPages[i] = true;
877+
}
878+
#endif
879+
880+
ARMEmitter::Emitter::ClearICache(CodeRange.data(), Size);
818881
}
819882

820883
} // namespace FEXCore::Context

FEXCore/include/FEXCore/Core/CodeCache.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,12 @@ struct MappedCodeCacheFile {
194194
// If not nullptr, the MappedCodeCacheFile will be unregistered from this on destruction
195195
AbstractCodeCache* CacheManager;
196196

197-
std::span<std::byte> MappedFile; // Mapped data of the whole cache file
198-
std::span<std::byte> CodeBuffer; // Subspan of cached ARM64 data within MappedFile
199-
std::byte* BlockListInFile; // Pointer to BlockListEntry data within MappedFile
200-
uint32_t NumBlocks; // Number of BlockListEntry objects
201-
uint32_t NumCodePages; // Number of code page entrypoint mappings
197+
std::span<std::byte> MappedFile; // Mapped data of the whole cache file
198+
std::span<std::byte> CodeBufferInFile; // Subspan of cached ARM64 data within MappedFile (pre-relocation)
199+
std::span<std::byte> CodeBuffer; // Cached ARM64 data used for execution (post-relocation; owned by MappedCodeCacheFile)
200+
std::byte* BlockListInFile; // Pointer to BlockListEntry data within MappedFile
201+
uint32_t NumBlocks; // Number of BlockListEntry objects
202+
uint32_t NumCodePages; // Number of code page entrypoint mappings
202203

203204
struct PageRelocationRange {
204205
uint32_t Offset; // In bytes from start of file

Source/Tools/LinuxEmulation/LinuxSyscalls/SyscallsSMCTracking.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ LoadCodeCache(FEXCore::Core::InternalThreadState& Thread, VMATracking::VMATracki
280280
}
281281

282282
auto CacheFileSize = static_cast<std::size_t>(buf.st_size);
283-
auto MappedCache = (std::byte*)FEXCore::Allocator::mmap(nullptr, CacheFileSize, PROT_READ | PROT_WRITE, MAP_PRIVATE, CacheFD, 0);
283+
auto MappedCache = (std::byte*)FEXCore::Allocator::mmap(nullptr, CacheFileSize, PROT_READ, MAP_PRIVATE, CacheFD, 0);
284284
close(CacheFD);
285285
if (!MappedCache || MappedCache == MAP_FAILED) {
286286
LogMan::Msg::EFmt("Failed to map code cache into memory");

0 commit comments

Comments
 (0)