fix(libkernel): refuse fixed no-overwrite mappings over live memory with ENOMEM

MAP_FIXED | MAP_NO_OVERWRITE over a range that already holds an
allocation fails with ENOMEM and leaves the mapping in place, as in
fpPS4's TVirtualManager.check_fixed. UFC 6 (PPSA23566) relies on it to
probe for a free address and aborted on the throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chad 2026-10-06 16:43:02 -05:00
parent edf90b169f
commit e9c8664d43
4 changed files with 58 additions and 3 deletions

View File

@ -30,6 +30,7 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t
void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes);
void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes);
bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes);
bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes);
Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer);
void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer);
void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, const std::function<void()>& apply);
@ -60,6 +61,7 @@ public:
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); }
bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); }
bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); }
Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); }
void Remove(const void* pointer) { GuestAllocationsRemove_nid_postfix(handle, pointer); }
void Unmap(const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t, const void*, bool)>& apply) { GuestAllocationsUnmap_nid_postfix(handle, pointer, bytes, apply); }

View File

@ -257,6 +257,16 @@ void GuestAllocationsRequireAvailable_nid_postfix(void*, const void* pointer, st
}
}
bool GuestAllocationsOverlaps_nid_postfix(void*, const void* pointer, std::size_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
require(address != 0 && bytes != 0 && bytes <= std::numeric_limits<std::uint64_t>::max() - address, "invalid guest allocation range");
for (const auto& [base, range] : registry().ranges) {
if (base >= address + bytes) break;
if (base + range->bytes > address) return true;
}
return false;
}
bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
require(address != 0 && bytes != 0 && bytes <= std::numeric_limits<std::uint64_t>::max() - address, "invalid guest allocation range");

View File

@ -565,6 +565,26 @@ void ValidateOutput(void** addr) {
}
bool Reserved(const void* addr, size_t len) {
const auto start = reinterpret_cast<std::uintptr_t>(addr);
const auto end = start + len;
std::lock_guard lock(g_reservationLock);
auto it = g_reservations.upper_bound(start);
if (it == g_reservations.begin()) return false;
auto cursor = start;
for (--it; it != g_reservations.end() && it->first <= cursor; ++it) {
cursor = std::max(cursor, it->second);
if (cursor >= end) return true;
}
return false;
}
bool FixedNoOverwriteConflict(const GuestAllocations::Mutation& mutation, void* addr, size_t len, int flags) {
constexpr int GuestMapNoOverwrite = 0x80;
if (addr == nullptr || (flags & GuestMapFixedFlag) == 0 || (flags & GuestMapNoOverwrite) == 0) return false;
return mutation.Overlaps(addr, len) && !Reserved(addr, len);
}
int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment) {
ValidateOutput(addr);
if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL;
@ -572,6 +592,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart,
return SCE_KERNEL_ERROR_EINVAL;
}
GuestAllocations::Mutation mutation;
if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM;
if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) {
EraseReservations(*addr, len);
RecordProtection(*addr, len, prot);
@ -600,6 +621,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) {
ValidateOutput(addr);
if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL;
GuestAllocations::Mutation mutation;
if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM;
if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) {
EraseReservations(*addr, len);
RecordProtection(*addr, len, prot);

View File

@ -111,10 +111,9 @@ static void CheckNamedAndHintedMappings() {
Require(sceKernelMapFlexibleMemory(&hinted, length, 3, 0) == 0);
Require(hinted > first && (reinterpret_cast<std::uintptr_t>(hinted) & 0x3fff) == 0);
static_cast<volatile unsigned char*>(hinted)[length - 1] = 1;
bool rejected = false;
void* overwrite = first;
try { sceKernelMapFlexibleMemory(&overwrite, 0x4000, 3, 0x90); } catch (const std::exception&) { rejected = true; }
Require(rejected && overwrite == first);
Require(sceKernelMapFlexibleMemory(&overwrite, 0x4000, 3, 0x90) == static_cast<int>(0x8002000cu));
Require(overwrite == first);
Require(sceKernelMunmap(hinted, length) == 0);
void* exclusive = hinted;
Require(sceKernelMapFlexibleMemory(&exclusive, length, 3, 0x90) == 0);
@ -366,6 +365,27 @@ static void CheckReservedRangeIsNotCommitted() {
Require(sceKernelReleaseDirectMemory(phys, page) == 0);
}
static void CheckNoOverwriteRefusesLiveMapping() {
constexpr std::size_t page = 0x4000;
constexpr int outOfMemory = static_cast<int>(0x8002000cu);
std::int64_t phys = 0;
Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0);
void* mapped = nullptr;
Require(sceKernelMapDirectMemory(&mapped, page, 3, 0, phys, 0) == 0);
static_cast<unsigned char*>(mapped)[0] = 13;
void* again = mapped;
Require(sceKernelMapDirectMemory(&again, page, 3, 0x90, phys + page, 0) == outOfMemory);
Require(again == mapped);
void* flexible = mapped;
Require(sceKernelMapFlexibleMemory(&flexible, page, 3, 0x90) == outOfMemory);
Require(static_cast<unsigned char*>(mapped)[0] == 13);
VirtualQueryInfo info{};
Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0);
Require(info.is_direct && info.offset == static_cast<std::uint64_t>(phys));
Require(sceKernelMunmap(mapped, page) == 0);
Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0);
}
#ifdef _WIN32
static void CheckNoOverwriteRejectsHostOccupiedMapping() {
constexpr std::size_t page = 0x4000;
@ -857,6 +877,7 @@ int main() {
CheckReleaseDirectMemoryClearsMappings();
CheckFixedVirtualReservation();
CheckReservedRangeIsNotCommitted();
CheckNoOverwriteRefusesLiveMapping();
CheckMlock();
CheckSharedDirectMemoryLifecycle();
CheckGetDirectMemoryType();