fix(libSceFiber): pin fiber contexts writable under write tracking on Windows

Write tracking protects shared guest pages read-only. A push on a fiber
stack in such a page faults, and Windows cannot dispatch it because the
exception frame lands on the same protected stack, so the process was
terminated without a report. Pin fiber contexts writable for their
lifetime; pinned pages are always collected as written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SP4C3B4R-8 2026-10-07 11:25:48 +02:00
parent 219a1cf8d2
commit 57588cf2dd
4 changed files with 56 additions and 2 deletions

View File

@ -14,6 +14,7 @@
#ifdef _WIN32
#include <windows.h>
#include "prx/libc/include/GuestArena.hpp"
#endif
static constexpr int32_t SCE_OK = 0;
@ -186,6 +187,14 @@ static void SetBounds(const StackBounds& bounds) {
*reinterpret_cast<void**>(teb + 0x1478) = bounds.deallocation;
}
static void PinStack(const void* context, std::uint64_t bytes) {
GuestArena::GuestArenaPinWritable_nid_postfix(context, static_cast<std::size_t>(bytes));
}
static void UnpinStack(const void* context, std::uint64_t bytes) {
GuestArena::GuestArenaUnpinWritable_nid_postfix(context, static_cast<std::size_t>(bytes));
}
#else
extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load);
@ -244,6 +253,10 @@ static StackBounds CurrentBounds() {
static void SetBounds(const StackBounds&) {}
static void PinStack(const void*, std::uint64_t) {}
static void UnpinStack(const void*, std::uint64_t) {}
#endif
static StackBounds FiberBounds(const Fiber* fiber) {
@ -330,6 +343,7 @@ int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject* object, const
auto* words = static_cast<std::uint64_t*>(addr_context);
std::fill(words, words + size_context / sizeof(std::uint64_t), FIBER_CONTEXT_FILL);
}
PinStack(addr_context, size_context);
if (TraceFibers()) std::fprintf(stderr, "[fiber] init %s object=%p context=%p+0x%llx entry=%p\n", fiber->name, static_cast<void*>(object), addr_context, static_cast<unsigned long long>(size_context), reinterpret_cast<void*>(entry));
return SCE_OK;
}
@ -340,6 +354,7 @@ int32_t APS5_VABI sceFiberFinalize(FiberObject* object) {
const auto state = fiber->state.load(std::memory_order_acquire);
if (state == FiberState::Running || state == FiberState::Suspending) return SCE_FIBER_ERROR_STATE;
fiber->magic = 0;
UnpinStack(fiber->context, fiber->contextSize);
return SCE_OK;
}

View File

@ -22,6 +22,8 @@ bool GuestArenaWriteWatched_nid_postfix();
#ifdef _WIN32
void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection);
bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address);
void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes);
void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes);
bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection);
bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear);
bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes);

View File

@ -102,6 +102,31 @@ public:
}
}
void Pin(std::uintptr_t address, std::size_t bytes) {
std::lock_guard lock(mutex);
for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < address + bytes; ++it) {
auto& page = *it->second.page;
++page.pins;
for (const auto alias : page.aliases) {
auto& view = views.at(alias);
if (!view.armed) continue;
DWORD previous;
if (!VirtualProtect(reinterpret_cast<void*>(alias), pageBytes, view.protection, &previous)) fail("pin shared guest page writable");
view.armed = false;
}
invalidate(page);
}
}
void Unpin(std::uintptr_t address, std::size_t bytes) {
std::lock_guard lock(mutex);
for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < address + bytes; ++it) {
auto& page = *it->second.page;
if (page.pins != 0) --page.pins;
invalidate(page);
}
}
bool HandleWrite(std::uintptr_t address) {
std::lock_guard lock(mutex);
const auto base = address & ~(pageBytes - 1);
@ -207,7 +232,8 @@ public:
auto& view = found->second;
const auto stop = std::min(end, base + pageBytes);
if (view.protection == PAGE_NOACCESS) return false;
if (view.seen != view.page->generation) {
const bool pinned = view.page->pins != 0;
if (pinned || view.seen != view.page->generation) {
const auto needed = (stop - cursor + 4095) / 4096;
if (needed > capacity - *count) {
for (auto at = cursor; *count < capacity; at += 4096) pages[(*count)++] = reinterpret_cast<void*>(at);
@ -215,7 +241,7 @@ public:
}
for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast<void*>(at);
}
if (clear) {
if (clear && !pinned) {
for (const auto alias : view.page->aliases) {
auto& other = views.at(alias);
if (!writable(other.protection) || other.armed || other.hostWrites != 0) continue;
@ -249,6 +275,7 @@ private:
struct SharedPage {
std::uint64_t generation = 1;
std::vector<std::uintptr_t> aliases;
std::uint32_t pins = 0;
};
struct Section {
HANDLE handle;

View File

@ -188,6 +188,16 @@ bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) {
return WindowsMappings::Get().HandleWrite(address);
}
void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes) {
if (!Arena::Get().Contains(pointer, bytes)) return;
WindowsMappings::Get().Pin(reinterpret_cast<std::uintptr_t>(pointer), bytes);
}
void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes) {
if (!Arena::Get().Contains(pointer, bytes)) return;
WindowsMappings::Get().Unpin(reinterpret_cast<std::uintptr_t>(pointer), bytes);
}
bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection) {
return WindowsMappings::Get().Protection(address, protection);
}