fix(agc): import shared guest memory through a read-write alias and never offer read-only pages

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michon 2026-09-30 17:03:03 +02:00
parent ac70928d86
commit 8b8b710b07
5 changed files with 78 additions and 5 deletions

View File

@ -26,6 +26,7 @@ struct HostImport {
VkBuffer buffer;
VkDeviceMemory memory;
VkDeviceAddress address;
void* alias = nullptr;
// Identity for the life of this import (see HostImportSerial); 0 until first asked for.
std::uint64_t serial = 0;
bool unwatched = false;

View File

@ -6,6 +6,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "prx/libc/include/CpuTopology.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#ifdef _WIN32
#include <windows.h>
@ -133,6 +134,9 @@ HostImports& Imports() {
void destroyImport(const Context& context, const HostImport& entry) {
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, entry.buffer, nullptr);
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, entry.memory, nullptr);
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
}
// Frees a dropped import's Vulkan objects when it is released. Never copied: a copy would destroy the
@ -163,8 +167,8 @@ void retireImport(const Context& context, HostImports& state, std::map<std::uint
}
const char* createImport(const Context& context, HostImport& entry, VkResult& failure) {
const auto base = entry.base;
const auto bytes = entry.bytes;
void* const host = entry.alias != nullptr ? entry.alias : reinterpret_cast<void*>(entry.base);
const auto failed = [&](const char* step, VkResult result) -> const char* {
if (entry.buffer) context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, entry.buffer, nullptr);
if (entry.memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, entry.memory, nullptr);
@ -181,12 +185,12 @@ const char* createImport(const Context& context, HostImport& entry, VkResult& fa
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (const auto result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result);
VkMemoryHostPointerPropertiesEXT pointer{VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT};
if (const auto result = context.Function<PFN_vkGetMemoryHostPointerPropertiesEXT>("vkGetMemoryHostPointerPropertiesEXT")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, reinterpret_cast<const void*>(base), &pointer); result != VK_SUCCESS) return failed("vkGetMemoryHostPointerPropertiesEXT", result);
if (const auto result = context.Function<PFN_vkGetMemoryHostPointerPropertiesEXT>("vkGetMemoryHostPointerPropertiesEXT")(context.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &pointer); result != VK_SUCCESS) return failed("vkGetMemoryHostPointerPropertiesEXT", result);
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements);
const auto types = requirements.memoryTypeBits & pointer.memoryTypeBits;
if (types == 0) return failed("memory type selection", VK_ERROR_FORMAT_NOT_SUPPORTED);
const VkImportMemoryHostPointerInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, reinterpret_cast<void*>(base)};
const VkImportMemoryHostPointerInfoEXT import{VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host};
const VkMemoryAllocateFlagsInfo flags{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO, &import, VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT, 0};
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, &flags};
allocation.allocationSize = bytes;
@ -272,20 +276,32 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
}
return nullptr;
}
HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
#ifdef _WIN32
// Drivers pin imported pages, so every page must be committed and accessible.
bool privateWritable = true;
bool mapped = false;
for (std::uint64_t cursor = base; cursor < base + bytes;) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<const void*>(cursor), &info, sizeof(info)) == 0 || info.State != MEM_COMMIT || (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) {
state.failed.insert(base);
return nullptr;
}
if (info.Type == MEM_MAPPED) mapped = true;
const auto protection = info.Protect & 0xffu;
if (info.Type != MEM_PRIVATE || (protection != PAGE_READWRITE && protection != PAGE_EXECUTE_READWRITE)) privateWritable = false;
cursor = reinterpret_cast<std::uint64_t>(info.BaseAddress) + info.RegionSize;
}
if (!privateWritable) {
entry.alias = mapped ? GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes)) : nullptr;
if (entry.alias == nullptr) {
state.failed.insert(base);
return nullptr;
}
}
#endif
decideImportWatch(context, state);
if (state.unwatchImports) GuestMemory::Unwatch(base, bytes);
HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0};
entry.unwatched = state.unwatchImports;
VkResult result = VK_SUCCESS;
const char* step = nullptr;
@ -294,6 +310,9 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
return step == nullptr;
});
if (step != nullptr) {
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
state.failed.insert(base);
std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result));
#ifdef _WIN32
@ -335,6 +354,9 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le
// guest addresses. Walks the imports only when the registry changed since the last walk.
void refreshImports(const Context& context, HostImports& state, const GuestAllocations::Lease& lease) {
if (state.device != context.device) {
#ifdef _WIN32
for (const auto& [address, entry] : state.imports) GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
state.imports.clear();
state.failed.clear();
state.device = context.device;

View File

@ -26,6 +26,8 @@ bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t byt
void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule);
void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes);
void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection);
void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes);
void GuestArenaUnmapAlias_nid_postfix(void* alias);
#endif
}

View File

@ -69,6 +69,9 @@ public:
std::lock_guard lock(mutex);
auto cursor = reinterpret_cast<std::uintptr_t>(address);
reset(cursor, bytes);
HANDLE duplicate = nullptr;
if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section");
const auto owned = std::make_shared<Section>(duplicate);
for (std::size_t done = 0; done < bytes; done += pageBytes) {
split(cursor + done, pageBytes);
void* page = reinterpret_cast<void*>(cursor + done);
@ -83,7 +86,7 @@ public:
}
const auto base = cursor + done;
shared->aliases.push_back(base);
views.emplace(base, View{shared, protection, 0, false});
views.emplace(base, View{shared, protection, 0, false, owned, offset + done});
invalidate(*shared);
}
}
@ -110,6 +113,27 @@ public:
return true;
}
void* MapAlias(std::uintptr_t address, std::size_t bytes) {
std::lock_guard lock(mutex);
if (address % pageBytes != 0 || bytes % pageBytes != 0 || bytes == 0) return nullptr;
auto view = views.find(address);
if (view == views.end()) return nullptr;
const auto section = view->second.section;
const auto offset = view->second.offset;
SYSTEM_INFO system{};
GetSystemInfo(&system);
if (offset % system.dwAllocationGranularity != 0) return nullptr;
for (std::size_t done = 0; done < bytes; done += pageBytes, ++view) {
if (view == views.end() || view->first != address + done || view->second.offset != offset + done) return nullptr;
if (view->second.section != section && !sameSection(view->second.section->handle, section->handle)) return nullptr;
}
return map(section->handle, GetCurrentProcess(), nullptr, offset, bytes, 0, PAGE_READWRITE, nullptr, 0);
}
void UnmapAlias(void* alias) {
if (alias != nullptr && !unmap(GetCurrentProcess(), alias, 0)) fail("unmap shared guest alias");
}
bool Protection(std::uintptr_t address, std::uint32_t* protection) {
std::lock_guard lock(mutex);
const auto found = views.find(address & ~(pageBytes - 1));
@ -181,11 +205,20 @@ private:
std::uint64_t generation = 1;
std::vector<std::uintptr_t> aliases;
};
struct Section {
HANDLE handle;
explicit Section(HANDLE handle) : handle(handle) {}
Section(const Section&) = delete;
Section& operator=(const Section&) = delete;
~Section() { CloseHandle(handle); }
};
struct View {
std::shared_ptr<SharedPage> page;
DWORD protection;
std::uint64_t seen;
bool armed;
std::shared_ptr<Section> section;
std::uint64_t offset;
};
void forgetClean(std::uintptr_t start, std::uintptr_t end) {
auto it = cleanRanges.lower_bound(start);
@ -215,12 +248,17 @@ private:
for (const auto alias : page.aliases) forgetClean(alias, alias + pageBytes);
}
bool sameSection(HANDLE first, HANDLE second) const {
return compare != nullptr && compare(first, second);
}
static bool writable(DWORD protection) {
return protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE;
}
using AllocateFunction = PVOID (WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG);
using MapFunction = PVOID (WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG);
using UnmapFunction = BOOL (WINAPI*)(HANDLE, PVOID, ULONG);
using CompareFunction = BOOL (WINAPI*)(HANDLE, HANDLE);
WindowsMappings() {
const auto module = GetModuleHandleW(L"KernelBase.dll");
@ -229,6 +267,7 @@ private:
map = reinterpret_cast<MapFunction>(GetProcAddress(module, "MapViewOfFile3"));
unmap = reinterpret_cast<UnmapFunction>(GetProcAddress(module, "UnmapViewOfFile2"));
if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required");
compare = reinterpret_cast<CompareFunction>(GetProcAddress(module, "CompareObjectHandles"));
}
[[noreturn]] static void fail(const char* operation) {
@ -300,6 +339,7 @@ private:
AllocateFunction allocate = nullptr;
MapFunction map = nullptr;
UnmapFunction unmap = nullptr;
CompareFunction compare = nullptr;
};
}

View File

@ -177,6 +177,14 @@ void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section,
if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("shared mapping outside the guest arena");
WindowsMappings::Get().Map(pointer, bytes, section, offset, protection);
}
void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) {
return WindowsMappings::Get().MapAlias(address, bytes);
}
void GuestArenaUnmapAlias_nid_postfix(void* alias) {
WindowsMappings::Get().UnmapAlias(alias);
}
#endif
bool GuestArenaWriteWatched_nid_postfix() {