mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-28 00:35:47 +00:00
feat(libkernel): guest memory protection and VirtualQuery on macOS hosts
- mprotect: the guest rounds a range to its 16 KiB pages. A Mach-O image
is loaded at a multiple of the host page size only (4 KiB under
Rosetta), so the pages are counted from where the ELF address 0 of the
image holding the range was loaded, and a rounded edge smaller than a
guest page that lies in the gap between two segments, where no host page
exists, is left out. The write watch is told the new protection.
- sceKernelVirtualQuery reads the region with mach_vm_region.
- sceKernelMunlockall unlocks the ranges libkernel locked, one by one:
macOS has no munlockall, which fails there with ENOSYS and would
overwrite errno.
- The stack protect test reads page protections with mach_vm_region.
This is mugurc's f66a4df741 from his macOS series, without the mlock
check that #10889 adds.
Co-authored-by: mugurc <[email protected]>
This commit is contained in:
1 parent
81267b2204
commit
190ed6c8fb
6 files changed
+99
-6
No files matched your search
@@ -62,6 +62,8 @@ 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);
|
||||
void GuestAllocationsTrimToRegistered_nid_postfix(void* mutation, const void** pointer, std::size_t* bytes);
|
||||
void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t bytes, std::size_t page, std::uintptr_t* first, std::uintptr_t* end);
|
||||
bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes);
|
||||
bool GuestAllocationsForRegistered_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t)>& visit);
|
||||
Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer);
|
||||
@@ -101,6 +103,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); }
|
||||
void TrimToRegistered(const void*& pointer, std::size_t& bytes) const { GuestAllocationsTrimToRegistered_nid_postfix(handle, &pointer, &bytes); }
|
||||
bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); }
|
||||
bool ForRegistered(const void* pointer, std::size_t bytes, const std::function<void(const void*, std::size_t)>& visit) const { return GuestAllocationsForRegistered_nid_postfix(handle, pointer, bytes, visit); }
|
||||
Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); }
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include "prx/libc/include/specifics/linux/ElfTypes.hpp"
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach-o/loader.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
@@ -524,6 +529,54 @@ bool GuestAllocationsForRegistered_nid_postfix(void*, const void* pointer, std::
|
||||
return any;
|
||||
}
|
||||
|
||||
void GuestAllocationsGuestPages_nid_postfix(std::uintptr_t address, std::size_t bytes, std::size_t page, std::uintptr_t* first, std::uintptr_t* end) {
|
||||
std::uintptr_t bias = 0;
|
||||
#ifdef __APPLE__
|
||||
struct Search {
|
||||
std::uintptr_t address;
|
||||
std::uintptr_t bias;
|
||||
} search{address, 0};
|
||||
dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) {
|
||||
auto& found = *static_cast<Search*>(data);
|
||||
for (unsigned index = 0; index < image->dlpi_phnum; ++index) {
|
||||
const auto& header = image->dlpi_phdr[index];
|
||||
const auto start = static_cast<std::uintptr_t>(image->dlpi_addr + header.p_vaddr);
|
||||
if (header.p_type == PT_LOAD && found.address >= start && found.address - start < header.p_memsz) {
|
||||
found.bias = static_cast<std::uintptr_t>(image->dlpi_addr);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}, &search);
|
||||
bias = search.bias;
|
||||
#endif
|
||||
const auto mask = static_cast<std::uintptr_t>(page - 1);
|
||||
*first = bias + ((address - bias) & ~mask);
|
||||
*end = bias + ((address + bytes - bias + mask) & ~mask);
|
||||
}
|
||||
|
||||
void GuestAllocationsTrimToRegistered_nid_postfix(void*, const void** pointer, std::size_t* bytes) {
|
||||
#ifdef __APPLE__
|
||||
constexpr std::uint64_t guestPage = 0x4000;
|
||||
auto start = reinterpret_cast<std::uintptr_t>(*pointer);
|
||||
auto end = start + *bytes;
|
||||
const auto& ranges = registry().ranges;
|
||||
const auto following = ranges.upper_bound(start);
|
||||
const bool startCovered = following != ranges.begin() && std::prev(following)->first + std::prev(following)->second->bytes > start;
|
||||
if (!startCovered && following != ranges.end() && following->first < end && following->first - start < guestPage) start = following->first;
|
||||
const auto beyond = ranges.lower_bound(end);
|
||||
if (beyond != ranges.begin()) {
|
||||
const auto finish = std::prev(beyond)->first + std::prev(beyond)->second->bytes;
|
||||
if (finish < end && finish > start && end - finish < guestPage) end = finish;
|
||||
}
|
||||
*pointer = reinterpret_cast<const void*>(start);
|
||||
*bytes = end - start;
|
||||
#else
|
||||
static_cast<void>(pointer);
|
||||
static_cast<void>(bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -1325,10 +1325,13 @@ int DoMprotect(const void* addr, size_t len, int prot) {
|
||||
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);
|
||||
auto first = address & ~pageMask;
|
||||
auto end = (address + len + pageMask) & ~pageMask;
|
||||
#ifdef __APPLE__
|
||||
GuestAllocations::GuestAllocationsGuestPages_nid_postfix(address, len, PS5_PAGE_SIZE, &first, &end);
|
||||
#endif
|
||||
auto bytes = static_cast<std::size_t>(end - first);
|
||||
const void* pointer = reinterpret_cast<const void*>(first);
|
||||
const auto nativeProtection = LinuxProtFromSce(prot);
|
||||
if (GuestThreadStackHostRangeOverlaps(address, address + len))
|
||||
throw std::system_error(EACCES, std::generic_category(), "mprotect overlaps host-only guest thread stack pages");
|
||||
@@ -1395,14 +1398,18 @@ int DoMprotect(const void* addr, size_t len, int prot) {
|
||||
if (!image.guestModule) throw std::invalid_argument("Memory protection of a foreign image is not supported");
|
||||
mutation.RegisterImage(reinterpret_cast<const void*>(image.base));
|
||||
}
|
||||
mutation.TrimToRegistered(pointer, bytes);
|
||||
#endif
|
||||
std::vector<std::pair<const void*, std::size_t>> spans;
|
||||
mutation.ForRegistered(pointer, bytes, [&](const void* span, std::size_t spanBytes) { spans.emplace_back(span, spanBytes); });
|
||||
if (spans.empty() || reinterpret_cast<std::uintptr_t>(spans.front().first) != first) return SCE_KERNEL_ERROR_EINVAL;
|
||||
if (spans.empty() || spans.front().first != pointer) return SCE_KERNEL_ERROR_EINVAL;
|
||||
for (const auto& [span, spanBytes] : spans) mutation.RequireUnpinned(span, spanBytes);
|
||||
for (const auto& [span, spanBytes] : spans) {
|
||||
mutation.Protect(span, spanBytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] {
|
||||
if (mprotect(const_cast<void*>(span), spanBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed");
|
||||
#ifdef __APPLE__
|
||||
GuestWriteWatch::GuestWriteWatchProtectionChanged_nid_postfix(span, spanBytes, nativeProtection);
|
||||
#endif
|
||||
});
|
||||
RecordProtection(span, spanBytes, prot);
|
||||
}
|
||||
|
||||
@@ -389,6 +389,17 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf
|
||||
info->end = info->start + host.RegionSize;
|
||||
const bool writable = (host.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_WRITECOPY)) != 0;
|
||||
const bool executable = (host.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) != 0;
|
||||
#elif defined(__APPLE__)
|
||||
mach_vm_address_t region = address;
|
||||
mach_vm_size_t regionSize = 0;
|
||||
vm_region_basic_info_data_64_t host{};
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object = MACH_PORT_NULL;
|
||||
if (mach_vm_region(mach_task_self(), ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&host), &count, &object) != KERN_SUCCESS || region > address || (host.protection & VM_PROT_READ) == 0) return SCE_KERNEL_ERROR_EACCES;
|
||||
info->start = region;
|
||||
info->end = region + regionSize;
|
||||
const bool writable = (host.protection & VM_PROT_WRITE) != 0;
|
||||
const bool executable = (host.protection & VM_PROT_EXECUTE) != 0;
|
||||
#else
|
||||
std::ifstream maps("/proc/self/maps");
|
||||
std::string line;
|
||||
@@ -765,7 +776,7 @@ int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length)
|
||||
}
|
||||
|
||||
int APS5_VABI sceKernelMunlockall_nid_postfix() {
|
||||
#ifndef _WIN32
|
||||
#if !defined(_WIN32) && !defined(__APPLE__)
|
||||
::munlockall();
|
||||
#endif
|
||||
std::map<uintptr_t, size_t> ranges;
|
||||
@@ -791,6 +802,10 @@ int APS5_VABI sceKernelMunlockall_nid_postfix() {
|
||||
cursor = next;
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
const int saved = errno;
|
||||
for (const auto& [start, len] : ranges) ::munlock(reinterpret_cast<void*>(start), len);
|
||||
errno = saved;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#include <sys/mman.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#include <sys/resource.h>
|
||||
@@ -39,6 +43,16 @@ static PageState QueryPage(std::uintptr_t address) {
|
||||
Require(VirtualQuery(reinterpret_cast<const void*>(address), &memory, sizeof(memory)) == sizeof(memory));
|
||||
const bool mapped = memory.State == MEM_COMMIT;
|
||||
return {mapped, mapped && (memory.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE)) != 0, (static_cast<std::uint64_t>(memory.State) << 32) | memory.Protect};
|
||||
#elif defined(__APPLE__)
|
||||
mach_vm_address_t region = address;
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_basic_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object = MACH_PORT_NULL;
|
||||
if (mach_vm_region(mach_task_self(), ®ion, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) != KERN_SUCCESS || region > address) return {false, false, 0};
|
||||
const bool readable = (info.protection & VM_PROT_READ) != 0;
|
||||
const bool writable = (info.protection & VM_PROT_WRITE) != 0;
|
||||
return {true, readable && writable, 1ull | (readable ? 2ull : 0) | (writable ? 4ull : 0) | ((info.protection & VM_PROT_EXECUTE) != 0 ? 8ull : 0)};
|
||||
#else
|
||||
std::FILE* maps = std::fopen("/proc/self/maps", "r");
|
||||
Require(maps != nullptr);
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
- [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:<port>` stays `127.0.0.1:<port>`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had
|
||||
- [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), and on a connected datagram socket it disconnects first, as on a PS5 (firmware 13.60) and in FreeBSD 11.4's `soconnect()`. A connected stream socket keeps its host `EISCONN` answer. Known gap: `sendto` and `sendmsg` to port 0 on an `AF_INET` datagram socket fail with 49 (measured for an unbound socket) unless the socket is bound to a specific address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, which is the path where `udp_output()` skips `in_pcbconnect_setup()` and FreeBSD sends to port 0. There the call returns the length without the host, as Linux's `udp_sendmsg` refuses port 0 with `EINVAL`; an `AF_INET6` socket still goes to the host. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed
|
||||
- [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range fails with `EBUSY`, as it did in a payload process on a PS5 (firmware 13.60) for anonymous memory, locked or not, also with a hole unmapped inside it; a title process, reservations, direct memory and flexible memory were not measured
|
||||
- Guest memory protection on macOS ([mprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp), [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp)): the guest rounds a range to its 16 KiB pages, counted from where the ELF address 0 of the image holding it was loaded, which is a multiple of the host page size only (4 KiB under Rosetta). A rounded edge smaller than a guest page that lies in the gap between two segments of an image, where no host page exists, is left out of the request. `sceKernelVirtualQuery` reports the region `mach_vm_region` returns, so its extent is the host's. After a protection change the write watch is told the protection, which the host reports for a watched page as its own
|
||||
- [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified
|
||||
- [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it
|
||||
- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). XPSemu (PPSA97358) imports `mlockall` and `munlockall`; their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation.
|
||||
|
||||
Reference in new issue
Block a user