Merge pull request #6124 from yripper/feat/macos-guest-arena

feat(libc): reserve the guest arena on macOS
This commit is contained in:
Adrià authored and GitHub committed 2026-10-18 19:09:43 +02:00
commit 3aebaa7de6
5 files changed
+199 -11

No files matched your search

+1
View File
@@ -272,6 +272,7 @@ if(BUILD_TESTING)
endif()
if(APPLE)
add_subdirectory(tests/write_watch)
add_subdirectory(tests/guest_arena)
endif()
if(WIN32)
add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp)
+54 -11
View File
@@ -11,6 +11,10 @@
#include <string>
#include <sys/mman.h>
#endif
#ifdef __APPLE__
#include <mach/mach.h>
#include <mach/mach_vm.h>
#endif
#include <cstdio>
#include <iterator>
#include <map>
@@ -148,8 +152,8 @@ public:
private:
Arena() {
#if defined(__linux__)
SeedLinuxUsedRanges();
#if defined(__linux__) || defined(__APPLE__)
SeedHostUsedRanges();
#elif defined(_WIN32)
_writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr;
SYSTEM_INFO system{};
@@ -182,7 +186,8 @@ private:
}
#if defined(__linux__)
void SeedLinuxUsedRanges() {
static std::vector<std::pair<std::uintptr_t, std::uintptr_t>> HostMappings() {
std::vector<std::pair<std::uintptr_t, std::uintptr_t>> mappings;
std::ifstream maps("/proc/self/maps");
if (!maps.is_open()) throw std::runtime_error("guest arena: cannot open /proc/self/maps");
std::string line;
@@ -191,6 +196,48 @@ private:
std::uintptr_t begin = 0, end = 0;
char dash = 0;
if (!(fields >> std::hex >> begin >> dash >> end) || dash != '-' || end <= begin) throw std::runtime_error("guest arena: malformed /proc/self/maps line: " + line);
mappings.emplace_back(begin, end);
}
return mappings;
}
static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) {
void* const wanted = reinterpret_cast<void*>(address);
void* const placed = mmap(wanted, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0);
if (placed == wanted) return 0;
if (placed == MAP_FAILED) return errno;
munmap(placed, bytes);
return EINVAL;
}
#elif defined(__APPLE__)
static std::vector<std::pair<std::uintptr_t, std::uintptr_t>> HostMappings() {
std::vector<std::pair<std::uintptr_t, std::uintptr_t>> mappings;
mach_vm_address_t address = ArenaStart;
while (address < ApplicationAreaEnd) {
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;
const kern_return_t result = mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object);
if (result == KERN_INVALID_ADDRESS) break;
if (result != KERN_SUCCESS) throw std::runtime_error("guest arena: mach_vm_region failed");
mappings.emplace_back(static_cast<std::uintptr_t>(address), static_cast<std::uintptr_t>(address + size));
address += size;
}
return mappings;
}
static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) {
mach_vm_address_t wanted = address;
const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT);
if (result == KERN_SUCCESS) return 0;
return result == KERN_NO_SPACE ? EEXIST : ENOMEM;
}
#endif
#if defined(__linux__) || defined(__APPLE__)
void SeedHostUsedRanges() {
for (const auto& [begin, end] : HostMappings()) {
if (end <= ArenaStart || begin >= ApplicationAreaEnd) continue;
const auto first = std::max(begin, ArenaStart);
const auto last = std::min(end, ApplicationAreaEnd);
@@ -208,12 +255,8 @@ private:
constexpr std::uintptr_t kChunk = 1ULL << 30;
for (std::uintptr_t cursor = from; cursor < to; cursor += kChunk) {
const auto end = std::min(cursor + kChunk, to);
void* const wanted = reinterpret_cast<void*>(cursor);
void* const placed = mmap(wanted, end - cursor, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0);
if (placed == wanted) continue;
const int failed = placed == MAP_FAILED ? errno : EINVAL;
if (placed != MAP_FAILED) munmap(placed, end - cursor);
const int failed = ReserveNoReplace(cursor, end - cursor);
if (failed == 0) continue;
if (failed != EEXIST) {
char message[128];
std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx",
@@ -428,7 +471,7 @@ void GuestArenaForgetSection_nid_postfix(void* section) {
}
#endif
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
namespace {
int PosixProtection(std::uint32_t windowsProtection) {
switch (windowsProtection) {
@@ -449,7 +492,7 @@ void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) {
}
#endif
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) {
if (!Arena::Get().Contains(reinterpret_cast<void*>(address), bytes)) throw OutsideArena("set protection", reinterpret_cast<void*>(address), bytes);
if (mprotect(reinterpret_cast<void*>(address), bytes, PosixProtection(protection)) != 0)
+122
View File
@@ -0,0 +1,122 @@
#include "prx/libc/include/GuestArena.hpp"
#include <cstdint>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#if !defined(__APPLE__)
#error These tests cover the macOS guest arena reservation
#endif
using namespace GuestArena;
namespace {
constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull;
constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull;
constexpr std::uint32_t ProtectionNoAccess = 0x01;
constexpr std::uint32_t ProtectionReadOnly = 0x02;
constexpr std::uint32_t ProtectionReadWrite = 0x04;
struct HostRegion {
std::uintptr_t start;
std::uintptr_t end;
vm_prot_t maxProtection;
};
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
std::vector<HostRegion> RegionsInArenaRange() {
std::vector<HostRegion> regions;
mach_vm_address_t address = ArenaStart;
while (address < ApplicationAreaEnd) {
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(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) != KERN_SUCCESS) break;
if (address >= ApplicationAreaEnd) break;
regions.push_back({static_cast<std::uintptr_t>(address), static_cast<std::uintptr_t>(address + size), info.max_protection});
address += size;
}
return regions;
}
void CheckAvailable() {
Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS");
std::uintptr_t base = 0;
std::size_t bytes = 0;
GuestArenaRange_nid_postfix(&base, &bytes);
Require(base == ArenaStart, "the guest arena does not start at the PS5 application map base");
Require(base + bytes == ApplicationAreaEnd, "the guest arena does not end at the PS5 application map limit");
}
void CheckSkipsUnmappable() {
for (const auto& region : RegionsInArenaRange()) {
if (region.maxProtection != VM_PROT_NONE || region.end - region.start < 0x10000) continue;
void* placed = GuestArenaAllocateAtOrAbove_nid_postfix(region.start, 0x10000, 0x10000);
const auto address = reinterpret_cast<std::uintptr_t>(placed);
Require(address >= region.end, "an allocation landed inside a range the kernel never lets the process map");
GuestArenaRelease_nid_postfix(placed, 0x10000);
return;
}
std::puts("no unmappable range inside the arena on this host; nothing to skip");
}
void CheckHostRegionsKept() {
std::size_t hostRegions = 0;
for (const auto& region : RegionsInArenaRange()) {
if (region.maxProtection == VM_PROT_ALL) continue;
++hostRegions;
bool rejected = false;
try {
GuestArenaMarkUsed_nid_postfix(reinterpret_cast<void*>(region.start), region.end - region.start);
} catch (const std::runtime_error&) {
rejected = true;
}
Require(rejected, "a fixed guest mapping over a host region was accepted");
}
std::printf("%zu host regions inside the arena range are protected\n", hostRegions);
}
void CheckCommitAndProtect() {
constexpr std::size_t bytes = 0x10000;
auto* pointer = static_cast<std::uint8_t*>(GuestArenaAllocate_nid_postfix(bytes, bytes));
GuestArenaCommit_nid_postfix(pointer, bytes, ProtectionReadWrite, bytes);
pointer[0] = 0x5a;
pointer[bytes - 1] = 0xa5;
Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0xa5, "committed guest memory does not keep writes");
GuestArenaSetProtection_nid_postfix(reinterpret_cast<std::uintptr_t>(pointer), bytes, ProtectionReadOnly);
Require(pointer[0] == 0x5a, "read-only guest memory lost its contents");
GuestArenaReset_nid_postfix(pointer, bytes);
GuestArenaSetProtection_nid_postfix(reinterpret_cast<std::uintptr_t>(pointer), bytes, ProtectionNoAccess);
GuestArenaRelease_nid_postfix(pointer, bytes);
}
}
int main(int argc, char** argv) {
try {
Require(argc == 2, "expected available, unmappable, host or commit");
const std::string_view mode(argv[1]);
if (mode == "available") CheckAvailable();
else if (mode == "unmappable") CheckSkipsUnmappable();
else if (mode == "host") CheckHostRegionsKept();
else if (mode == "commit") CheckCommitAndProtect();
else throw std::runtime_error("unknown guest arena test");
std::printf("guest arena %s passed\n", argv[1]);
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.20)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
project(GuestArenaTests LANGUAGES CXX)
enable_testing()
set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
endif()
find_package(Threads REQUIRED)
add_executable(guest_arena_tests ../GuestArenaHost.cpp ../../prx/libc/src/GuestArena.cpp ../../prx/libc/src/GuestWriteWatch.cpp)
target_include_directories(guest_arena_tests PRIVATE ../..)
target_compile_features(guest_arena_tests PRIVATE cxx_std_20)
target_link_libraries(guest_arena_tests PRIVATE Threads::Threads)
set_target_properties(guest_arena_tests PROPERTIES
CXX_EXTENSIONS OFF
RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
foreach(mode available unmappable host commit)
add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode})
set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30)
endforeach()
+1
View File
@@ -68,6 +68,7 @@
- a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails;
- code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree;
- the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same.
- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them.
- [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync`
- [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way
- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated