From 2df58cf09c113c6cf09f2bd61ebd0d95da85ccda Mon Sep 17 00:00:00 2001 From: boykopovar Date: Wed, 23 Sep 2026 12:45:39 +0300 Subject: [PATCH] fix(kernel): support page-aligned mprotect for loaded images --- core/libs/prx/libSceAgc/DcbState/src/Init.cpp | 7 ++- core/libs/prx/libSceAgc/tests/Commands.cpp | 5 ++ .../tests/GuestAllocations.cpp | 40 ++++++++++++++ .../prx/libc/include/GuestAllocations.hpp | 7 +++ core/libs/prx/libc/src/GuestAllocations.cpp | 54 ++++++++++++++++++- .../libkernel/DirectMemory/DirectMemory.cpp | 21 ++++++-- 6 files changed, 125 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index da372b02a..737e0b62a 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -9,10 +9,9 @@ extern "C" { APS5_EXPORT("23LRUSvYu1M", sceAgcInit); -int APS5_VABI sceAgcInit(uint32_t* state, uint32_t ver) { - (void)state; - (void)ver; - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAgcInit(std::uint32_t* state, std::uint32_t version) { + Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); + Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version"); return 0; } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index fc08c9d25..421ed8a0f 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -12,6 +12,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument); extern "C" int APS5_VABI sceAgcSuspendPoint(); +extern "C" int APS5_VABI sceAgcInit(std::uint32_t* state, std::uint32_t version); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier); extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference); extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type); @@ -252,6 +253,10 @@ void testMemory() { } void testDefaults() { + std::uint32_t state = 0x12345678; + check(sceAgcInit(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state"); + expectFailure([] { sceAgcInit(nullptr, 8); }); + expectFailure([&] { sceAgcInit(&state, 14); }); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index 87e915baa..a9e0e5e99 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include #include +#include namespace { @@ -75,4 +76,43 @@ void RunGuestAllocationTests() { }); } Require(GuestAllocations::GuestAllocationsAcquire_nid_postfix().empty(), "unmapped fragments remain registered"); +#ifdef _WIN32 + static std::byte imageProbe{}; + { + GuestAllocations::Mutation mutation; + mutation.RegisterMainImage(); + mutation.RegisterMainImage(); + } + const auto imageAddress = reinterpret_cast(&imageProbe); + std::uint64_t allocationAddress = 0; + std::size_t imageRangeCount = 0; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + imageRangeCount = lease.size(); + const auto found = std::find_if(lease.begin(), lease.end(), [&](const auto& range) { return imageAddress >= range->address && imageAddress - range->address < range->bytes; }); + Require(found != lease.end() && (*found)->writable && !(*found)->releasable, "main image registration is missing or releasable"); + allocationAddress = (*found)->allocationAddress; + GuestAllocations::Mutation mutation; + bool applied = false; + reject([&] { mutation.Protect(&imageProbe, 1, true, false, [&] { applied = true; }); }); + Require(!applied, "pinned image protection changed"); + } + { + GuestAllocations::Mutation mutation; + reject([&] { mutation.Find(reinterpret_cast(allocationAddress)); }); + reject([&] { mutation.Remove(reinterpret_cast(allocationAddress)); }); + bool applied = false; + reject([&] { mutation.Unmap(&imageProbe, 1, [&](const void*, bool) { applied = true; }); }); + Require(!applied, "image memory was unmapped"); + reject([&] { mutation.Protect(&imageProbe, 1, true, false, [] { throw std::runtime_error("host protection failure"); }); }); + } + Require(GuestAllocations::GuestAllocationsAcquire_nid_postfix().size() == imageRangeCount, "failed image protection changed registry ranges"); + { + GuestAllocations::Mutation mutation; + mutation.Protect(&imageProbe, 1, true, true, [] {}); + bool applied = false; + reject([&] { mutation.Unmap(&imageProbe, 1, [&](const void*, bool) { applied = true; }); }); + Require(!applied, "split image memory became releasable"); + } +#endif } diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 1c8105778..db5f0394f 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -17,12 +17,16 @@ struct Range { bool writable; std::uint64_t allocationAddress; std::size_t allocationBytes; + bool releasable = true; }; using Lease = std::vector>; extern "C" { void* GuestAllocationsBegin_nid_postfix(); +#ifdef _WIN32 +void GuestAllocationsRegisterMainImage_nid_postfix(void* mutation); +#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); @@ -40,6 +44,9 @@ public: ~Mutation() { GuestAllocationsEnd_nid_postfix(handle); } Mutation(const Mutation&) = delete; Mutation& operator=(const Mutation&) = delete; +#ifdef _WIN32 + void RegisterMainImage() { GuestAllocationsRegisterMainImage_nid_postfix(handle); } +#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); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index a6ce06182..f89137008 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -1,7 +1,14 @@ #include "prx/libc/include/GuestAllocations.hpp" #include +#include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace GuestAllocations { namespace { @@ -9,6 +16,7 @@ namespace { struct Registry { std::mutex mutex; std::map> ranges; + bool mainImageRegistered = false; }; Registry& registry() { @@ -30,6 +38,44 @@ void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept { delete static_cast*>(mutation); } +#ifdef _WIN32 +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"); + auto replacement = state.ranges; + auto cursor = reinterpret_cast(image); + bool registered = false; + for (;;) { + MEMORY_BASIC_INFORMATION memory{}; + require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query the main 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(memory.BaseAddress) == cursor && memory.RegionSize != 0 && memory.RegionSize <= std::numeric_limits::max() - cursor, "invalid guest image range"); + if (memory.State == MEM_COMMIT) { + require((memory.Protect & PAGE_GUARD) == 0, "guarded guest image pages are not supported"); + const auto protection = memory.Protect & 0xffu; + const bool writable = protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY; + const bool readable = writable || protection == PAGE_READONLY || protection == PAGE_EXECUTE_READ; + require(readable || protection == PAGE_NOACCESS || protection == PAGE_EXECUTE, "unsupported guest image protection"); + const auto next = replacement.lower_bound(cursor); + require(next == replacement.end() || cursor + memory.RegionSize <= next->first, "guest image overlaps a registered allocation"); + if (next != replacement.begin()) { + const auto& previous = *std::prev(next)->second; + require(previous.address + previous.bytes <= cursor, "guest image overlaps a registered allocation"); + } + replacement.emplace(cursor, std::make_shared(Range{cursor, memory.RegionSize, readable, writable, cursor, memory.RegionSize, false})); + registered = true; + } + cursor += memory.RegionSize; + } + require(registered, "main guest image has no committed pages"); + state.ranges.swap(replacement); + state.mainImageRegistered = true; +} +#endif + void GuestAllocationsAdd_nid_postfix(void*, void* pointer, std::size_t bytes, bool readable, bool writable) { const auto address = reinterpret_cast(pointer); require(address != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); @@ -66,7 +112,10 @@ void GuestAllocationsRequireAvailable_nid_postfix(void*, const void* pointer, st Range GuestAllocationsFind_nid_postfix(void*, const void* pointer) { const auto address = reinterpret_cast(pointer); for (const auto& [base, range] : registry().ranges) { - if (range->allocationAddress == address) return {address, range->allocationBytes, range->readable, range->writable, address, range->allocationBytes}; + if (range->allocationAddress == address) { + require(range->releasable, "guest image memory is not a releasable allocation"); + return {address, range->allocationBytes, range->readable, range->writable, address, range->allocationBytes, range->releasable}; + } } throw std::runtime_error("guest allocation is not registered"); } @@ -94,7 +143,7 @@ std::map> replaceRange(const void* p require(base <= cursor, "guest protection or unmap range has a hole"); replacement.erase(base); const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite) { - if (first < last) replacement.emplace(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes})); + if (first < last) replacement.emplace(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable})); }; insert(base, std::max(base, address), range.readable, range.writable); if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable); @@ -120,6 +169,7 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: const auto found = registry().ranges.upper_bound(address); require(found != registry().ranges.begin(), "unmap address is not registered"); const auto& range = *std::prev(found)->second; + require(range.releasable, "guest image memory cannot be unmapped"); require(address >= range.address && address - range.allocationAddress <= range.allocationBytes && bytes <= range.allocationBytes - (address - range.allocationAddress), "unmap crosses allocation boundaries"); auto replacement = replaceRange(pointer, bytes, true, false, false); bool last = true; diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index b1640307b..321fb2828 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -201,11 +201,26 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { } int DoMprotect(const void* addr, size_t len, int prot) { - if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; + const auto address = reinterpret_cast(addr); + constexpr auto pageMask = static_cast(PS5_PAGE_SIZE - 1); + const auto limit = std::numeric_limits::max(); + if (address == 0 || len == 0 || len > limit - address || address + len > limit - pageMask) throw std::invalid_argument("Invalid guest memory protection range"); + const auto first = address & ~pageMask; + const auto end = (address + len + pageMask) & ~pageMask; + const auto bytes = static_cast(end - first); + const auto* pointer = reinterpret_cast(first); const auto nativeProtection = LinuxProtFromSce(prot); GuestAllocations::Mutation mutation; - mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, [&] { - if (mprotect(const_cast(addr), len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); +#ifdef _WIN32 + 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(); + } +#endif + mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, [&] { + if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); }); return 0; }