fix(kernel): support page-aligned mprotect for loaded images

This commit is contained in:
boykopovar 2026-09-23 12:45:39 +03:00
parent 0b3ffe47b0
commit 2df58cf09c
6 changed files with 125 additions and 9 deletions

View File

@ -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<std::uintptr_t>(state), alignof(std::uint32_t), __func__);
Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version");
return 0;
}

View File

@ -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__);

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include <cstring>
#include <array>
#include <algorithm>
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<std::uintptr_t>(&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<void*>(allocationAddress)); });
reject([&] { mutation.Remove(reinterpret_cast<void*>(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
}

View File

@ -17,12 +17,16 @@ struct Range {
bool writable;
std::uint64_t allocationAddress;
std::size_t allocationBytes;
bool releasable = true;
};
using Lease = std::vector<std::shared_ptr<const Range>>;
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); }

View File

@ -1,7 +1,14 @@
#include "prx/libc/include/GuestAllocations.hpp"
#include <limits>
#include <iterator>
#include <map>
#include <stdexcept>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
namespace GuestAllocations {
namespace {
@ -9,6 +16,7 @@ namespace {
struct Registry {
std::mutex mutex;
std::map<std::uint64_t, std::shared_ptr<const Range>> ranges;
bool mainImageRegistered = false;
};
Registry& registry() {
@ -30,6 +38,44 @@ void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept {
delete static_cast<std::unique_lock<std::mutex>*>(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<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");
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");
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<const Range>(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<std::uintptr_t>(pointer);
require(address != 0 && bytes <= std::numeric_limits<std::uint64_t>::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<std::uintptr_t>(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<std::uint64_t, std::shared_ptr<const Range>> 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<const Range>(Range{first, static_cast<std::size_t>(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes}));
if (first < last) replacement.emplace(first, std::make_shared<const Range>(Range{first, static_cast<std::size_t>(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;

View File

@ -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<std::uintptr_t>(addr);
constexpr auto pageMask = static_cast<std::uintptr_t>(PS5_PAGE_SIZE - 1);
const auto limit = std::numeric_limits<std::uintptr_t>::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<std::size_t>(end - first);
const auto* pointer = reinterpret_cast<const void*>(first);
const auto nativeProtection = LinuxProtFromSce(prot);
GuestAllocations::Mutation mutation;
mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, [&] {
if (mprotect(const_cast<void*>(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<void*>(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed");
});
return 0;
}