Files
AnyPS5/core/libs/tests/GuestMainStackMove.cpp
T
javierUDP dd5ee9e4c6 fix(libkernel): keep the process start block mapped when the main stack moves
On macOS under Rosetta, Aps5StartGuest moves the main thread off the host
stack and releases the old range to the guest arena. dyld keeps its own
pointers into the process start block at the top of that range and offers
no call that redirects them, where argv and environ have one:
_NSGetExecutablePath copies the path string out of the block on every
call, so the first call after the move read unmapped memory and the
process died with KERN_PROTECTION_FAILURE. A title does not have to call
it: libxpc reaches it from xpc_connection_resume, and ShaderCacheDirectory
calls it for the shader disk cache.

Release the old stack only up to the pages that hold the block, and only
while argv still points into the stack. They are the top two pages, 8 KiB
of the 8372224-byte stack here, so a title that fixes a mapping over that
range now fails with a host region overlap; the rest is free as before.

guest_main_stack_move asked for a 64 KiB guest mapping aligned down from
a local of main, which falls inside the kept pages. It now probes the
64 KiB below them, requires the kept pages to be refused, and calls
_NSGetExecutablePath after the move, which segfaults without this change.
2026-11-04 04:22:23 -03:00

124 lines
4.8 KiB
C++

#include "prx/libc/include/GuestArena.hpp"
#include <crt_externs.h>
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <pthread.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string>
extern "C" [[noreturn]] void Aps5StartGuest_nid_no_patch(void (*entry)(void*, void*), void* block);
extern "C" void Aps5RunOnMainThread_nid_no_patch(void (*function)(void*), void* context);
namespace {
constexpr std::uintptr_t MappingAlignment = 0x10000;
constexpr std::size_t MappingBytes = 0x10000;
constexpr std::size_t NeighbourBytes = std::size_t {1} << 20;
std::uintptr_t originalStack = 0;
std::string originalArgument;
std::string originalPath;
bool originalInArena = false;
mach_vm_address_t neighbour = 0;
bool RegionOf(mach_vm_address_t address, mach_vm_address_t& begin, mach_vm_address_t& end, std::uint32_t& tag) {
mach_vm_address_t base = address;
mach_vm_size_t size = 0;
natural_t depth = 0;
vm_region_submap_info_data_64_t info {};
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
if (mach_vm_region_recurse(mach_task_self(), &base, &size, &depth, reinterpret_cast<vm_region_recurse_info_t>(&info), &count) != KERN_SUCCESS || base > address) return false;
begin = base;
end = base + size;
tag = info.user_tag;
return true;
}
mach_vm_address_t MapStackNeighbour(std::uintptr_t stack) {
mach_vm_address_t low = 0, end = 0;
std::uint32_t tag = 0;
if (!RegionOf(stack, low, end, tag) || tag != VM_MEMORY_STACK) return 0;
for (;;) {
mach_vm_address_t begin = 0, previousEnd = 0;
if (!RegionOf(low - 1, begin, previousEnd, tag) || tag != VM_MEMORY_STACK || previousEnd != low) break;
low = begin;
}
mach_vm_address_t at = low - NeighbourBytes;
if (mach_vm_deallocate(mach_task_self(), at, NeighbourBytes) != KERN_SUCCESS) return 0;
if (mach_vm_allocate(mach_task_self(), &at, NeighbourBytes, VM_MAKE_TAG(VM_MEMORY_STACK)) != KERN_SUCCESS) return 0;
return at;
}
void Require(bool value, const char* what) {
if (value) return;
std::fprintf(stderr, "check failed: %s\n", what);
std::exit(1);
}
bool InArena(std::uintptr_t address) {
std::uintptr_t base = 0;
std::size_t bytes = 0;
GuestArena::GuestArenaRange_nid_postfix(&base, &bytes);
return address >= base && address < base + bytes;
}
void CheckMainThread(void*) {
int local = 0;
Require(pthread_main_np() != 0, "the check runs on the main thread");
char path[4096];
std::uint32_t pathBytes = sizeof(path);
Require(_NSGetExecutablePath(path, &pathBytes) == 0, "the executable path is readable after the move");
Require(originalPath == path, "the executable path survived the move");
const auto address = reinterpret_cast<std::uintptr_t>(&local);
if (originalInArena) {
Require(!InArena(address), "the main thread stack is outside the guest arena");
Require(address != originalStack, "the main thread stack moved");
const auto kept = originalStack & ~(MappingAlignment - 1);
const auto released = reinterpret_cast<const void*>(kept - MappingBytes);
try {
GuestArena::GuestArenaMarkUsed_nid_postfix(released, MappingBytes);
} catch (const std::runtime_error&) {
Require(false, "a fixed guest mapping fits where the main thread stack was");
}
GuestArena::GuestArenaRelease_nid_postfix(released, MappingBytes);
bool refused = false;
try {
GuestArena::GuestArenaMarkUsed_nid_postfix(reinterpret_cast<const void*>(kept), MappingBytes);
} catch (const std::runtime_error&) {
refused = true;
}
if (!refused) GuestArena::GuestArenaRelease_nid_postfix(reinterpret_cast<const void*>(kept), MappingBytes);
Require(refused, "the process start block stays mapped where dyld points");
}
if (neighbour != 0) {
mach_vm_address_t begin = 0, end = 0;
std::uint32_t tag = 0;
Require(RegionOf(neighbour, begin, end, tag) && tag == VM_MEMORY_STACK, "a stack region next to the main stack survived the move");
}
Require(originalArgument == (*_NSGetArgv())[0], "argv survived the move");
Require(std::getenv("PATH") != nullptr, "the environment survived the move");
}
void Guest(void*, void*) {
Aps5RunOnMainThread_nid_no_patch(CheckMainThread, nullptr);
std::exit(0);
}
}
int main(int, char** argv) {
int local = 0;
originalStack = reinterpret_cast<std::uintptr_t>(&local);
originalInArena = InArena(originalStack);
originalArgument = argv[0];
char path[4096];
std::uint32_t pathBytes = sizeof(path);
if (_NSGetExecutablePath(path, &pathBytes) == 0) originalPath = path;
if (originalInArena) neighbour = MapStackNeighbour(originalStack);
Aps5StartGuest_nid_no_patch(Guest, nullptr);
}