fix(libkernel): reprotect bundled guest module images on Windows

On Windows, sceKernelMprotect accepted only the executable's image and threw
"Memory protection of a foreign image is not supported" for every other
image. Bundled title modules are relinked into DLLs named <module>.guest.prx,
so a title that reprotects its own module pages failed. Dying Light 2's
bundled libNRD does this at start-up (#1794).

DoMprotect now also accepts images whose file name ends in .guest.prx, and
registers their committed pages like the executable's. Other images are
still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lexroid 2026-10-08 22:27:54 +03:00 committed by Adrià Franch
parent 85d16bcc23
commit 9f1d7b0516
6 changed files with 90 additions and 8 deletions

View File

@ -25,6 +25,9 @@ using Lease = std::vector<std::shared_ptr<const Range>>;
extern "C" {
void* GuestAllocationsBegin_nid_postfix();
void GuestAllocationsRegisterMainImage_nid_postfix(void* mutation);
#ifdef _WIN32
void GuestAllocationsRegisterImage_nid_postfix(void* mutation, const void* image);
#endif
void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept;
void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable);
void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes);
@ -58,6 +61,9 @@ public:
Mutation(const Mutation&) = delete;
Mutation& operator=(const Mutation&) = delete;
void RegisterMainImage() { GuestAllocationsRegisterMainImage_nid_postfix(handle); }
#ifdef _WIN32
void RegisterImage(const void* image) { GuestAllocationsRegisterImage_nid_postfix(handle, image); }
#endif
void Add(void* pointer, std::size_t bytes, bool readable, bool writable) { GuestAllocationsAdd_nid_postfix(handle, pointer, bytes, readable, writable); }
void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); }
void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); }

View File

@ -8,6 +8,7 @@
#include <limits>
#include <iterator>
#include <map>
#include <set>
#include <stdexcept>
#ifdef _WIN32
#ifndef NOMINMAX
@ -34,6 +35,7 @@ struct Registry {
std::mutex mutex;
std::map<std::uint64_t, std::shared_ptr<const Range>> ranges;
bool mainImageRegistered = false;
std::set<std::uintptr_t> registeredImages;
};
Registry& registry() {
@ -134,17 +136,16 @@ void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)(std::uintptr_t, s
}
#ifdef _WIN32
void GuestAllocationsRegisterMainImage_nid_postfix(void*) {
namespace {
void registerImage(const void* image) {
auto& state = registry();
if (state.mainImageRegistered) return;
const auto image = GetModuleHandleW(nullptr);
require(image != nullptr, "cannot locate the main guest image");
if (state.registeredImages.contains(reinterpret_cast<std::uintptr_t>(image))) return;
auto replacement = state.ranges;
auto cursor = reinterpret_cast<std::uintptr_t>(image);
bool registered = false;
for (;;) {
MEMORY_BASIC_INFORMATION memory{};
require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query the main guest image");
require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query a guest image");
if (memory.AllocationBase != image) break;
require(memory.Type == MEM_IMAGE && (memory.State == MEM_COMMIT || memory.State == MEM_RESERVE), "unsupported guest image mapping");
require(reinterpret_cast<std::uintptr_t>(memory.BaseAddress) == cursor && memory.RegionSize != 0 && memory.RegionSize <= std::numeric_limits<std::uintptr_t>::max() - cursor, "invalid guest image range");
@ -165,10 +166,25 @@ void GuestAllocationsRegisterMainImage_nid_postfix(void*) {
}
cursor += memory.RegionSize;
}
require(registered, "main guest image has no committed pages");
require(registered, "guest image has no committed pages");
state.ranges.swap(replacement);
state.registeredImages.insert(reinterpret_cast<std::uintptr_t>(image));
}
}
void GuestAllocationsRegisterMainImage_nid_postfix(void*) {
auto& state = registry();
if (state.mainImageRegistered) return;
const auto image = GetModuleHandleW(nullptr);
require(image != nullptr, "cannot locate the main guest image");
registerImage(image);
state.mainImageRegistered = true;
}
void GuestAllocationsRegisterImage_nid_postfix(void*, const void* image) {
require(image != nullptr, "cannot register a guest image without a base");
registerImage(image);
}
#else
void GuestAllocationsRegisterMainImage_nid_postfix(void*) {
auto& state = registry();

View File

@ -691,6 +691,10 @@ if(BUILD_TESTING)
configure_windows_unwind(guest_memory_tests)
add_test(NAME guest_memory COMMAND guest_memory_tests)
if(WIN32)
add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp)
set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx")
add_dependencies(guest_memory_tests guest_memory_module)
target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$<TARGET_FILE:guest_memory_module>")
set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()

View File

@ -11,9 +11,12 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cwctype>
#include <memory>
#include <limits>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>
@ -777,6 +780,24 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) {
return 0;
}
#ifdef _WIN32
static bool IsGuestModuleImage(const void* base) {
std::wstring path(MAX_PATH, L'\0');
for (;;) {
const auto length = GetModuleFileNameW(static_cast<HMODULE>(const_cast<void*>(base)), path.data(), static_cast<DWORD>(path.size()));
if (length == 0) return false;
if (length < path.size()) {
path.resize(length);
break;
}
path.resize(path.size() * 2);
}
constexpr std::wstring_view suffix = L".guest.prx";
if (path.size() < suffix.size()) return false;
return std::equal(suffix.begin(), suffix.end(), path.end() - static_cast<std::ptrdiff_t>(suffix.size()), [](wchar_t expected, wchar_t actual) { return std::towlower(actual) == expected; });
}
#endif
int DoMprotect(const void* addr, size_t len, int prot) {
Trace("protect %p+0x%zx prot=0x%x", addr, len, prot);
const auto address = reinterpret_cast<std::uintptr_t>(addr);
@ -793,8 +814,9 @@ int DoMprotect(const void* addr, size_t len, int prot) {
MEMORY_BASIC_INFORMATION memory{};
if (VirtualQuery(pointer, &memory, sizeof(memory)) != sizeof(memory)) throw std::runtime_error("Cannot query guest memory protection range");
if (memory.Type == MEM_IMAGE) {
if (memory.AllocationBase != GetModuleHandleW(nullptr)) throw std::invalid_argument("Memory protection of a foreign image is not supported");
mutation.RegisterMainImage();
if (memory.AllocationBase == GetModuleHandleW(nullptr)) mutation.RegisterMainImage();
else if (IsGuestModuleImage(memory.AllocationBase)) mutation.RegisterImage(memory.AllocationBase);
else throw std::invalid_argument("Memory protection of a foreign image is not supported");
}
#else
mutation.RegisterMainImage();

View File

@ -19,6 +19,7 @@
#include <cstdlib>
#include <limits>
#include <source_location>
#include <stdexcept>
#include <thread>
#include <utility>
#include <vector>
@ -534,6 +535,37 @@ static void CheckNoOverwriteRejectsHostOccupiedMapping() {
Require(sceKernelReleaseDirectMemory(phys, page) == 0);
}
static DWORD ImageProtection(const void* pointer) {
MEMORY_BASIC_INFORMATION memory{};
Require(VirtualQuery(pointer, &memory, sizeof(memory)) == sizeof(memory));
return memory.Protect & 0xffu;
}
static void CheckGuestModuleImageProtection() {
constexpr std::size_t page = 0x4000;
const auto module = LoadLibraryA(GUEST_MEMORY_MODULE);
Require(module != nullptr);
auto* data = reinterpret_cast<unsigned char*>(GetProcAddress(module, "guestMemoryModuleData"));
Require(data != nullptr);
auto* target = reinterpret_cast<unsigned char*>((reinterpret_cast<std::uintptr_t>(data) + page - 1) & ~(page - 1));
Require(target + page <= data + 0x10000);
Require(sceKernelMprotect(target, page, 1) == 0);
Require(ImageProtection(target) == PAGE_READONLY);
Require(sceKernelMprotect(target, page, 3) == 0);
Require(ImageProtection(target) == PAGE_READWRITE || ImageProtection(target) == PAGE_WRITECOPY);
target[1] = 7;
Require(target[1] == 7);
Require(sceKernelMprotect(target, page, 1) == 0);
Require(ImageProtection(target) == PAGE_READONLY);
bool rejected = false;
try {
sceKernelMprotect(GetModuleHandleA("kernel32.dll"), page, 1);
} catch (const std::invalid_argument&) {
rejected = true;
}
Require(rejected);
}
static void CheckFixedMappingsReachTheApplicationAreaEnd() {
constexpr std::size_t page = 0x4000;
constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull;
@ -1267,6 +1299,7 @@ int main() {
#ifdef _WIN32
CheckNoOverwriteRejectsHostOccupiedMapping();
CheckFixedMappingsReachTheApplicationAreaEnd();
CheckGuestModuleImageProtection();
#endif
CheckSharedWriteTracking();
CheckReadsIntoSharedWriteTracking();

View File

@ -0,0 +1 @@
extern "C" unsigned char guestMemoryModuleData[0x10000] = {1};