mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-18 23:07:07 -05:00
#4265 gives each fiber an mmap'd host stack on Linux and leaves Windows as it was: there a fiber still runs host calls inside the context the title passed to sceFiberInitialize. Demon's Souls packs its 16 KiB job fiber contexts back to back. A job that calls sceAgcCreateInterpolantMapping reaches shader preparation on the fiber (Driver::ResolveShaderAbi, PrepareRegistered, the RDNA decoder, BuildShaderStageInputInfo), goes more than 16 KiB deep and overwrites the frames at the top of the context below it. On the loading screen after the opening cinematic the fiber that owns that context then failed the title's stack check, or returned to 0x7fffffff. On Windows each fiber now gets a host stack from VirtualAlloc, the context size plus 1 MiB, committed like the 1 MiB host margin of guest threads, with a PAGE_NOACCESS page at the bottom. While the fiber runs, the TIB stack base and limit and the deallocation stack describe that host stack, as Windows does for its own fibers, so stack probes and GetCurrentThreadStackLimits see the stack the fiber runs on. A host stack that cannot be allocated, protected or released throws instead of falling back to the context. agc_driver_gpu_map_on_fiber: GetCurrentThreadStackLimits on a fiber now returns its host stack, which has room for the import, so on Windows a GPU mapping made on a fiber imports on the fiber's stack instead of a host thread. The fiber's context is still not written. Test: on Windows, fiber checks that the TIB stack bounds inside a fiber cover its host stack, at least the context plus 256 KiB. With #4265 alone it fails at "the thread's stack bounds are the fiber's host stack", and without that check at "a host call from a 16 KiB fiber writes nothing below its context"; with this change fiber and agc_driver_gpu_map_on_fiber pass (Windows Server 2025, RTX 3060, WinLibs GCC 15.2.0, Release). Demon's Souls (PPSA01342) on that host: at the loading screen the two 16 KiB job fibers above the overwritten one now use 17,592 and 16,936 bytes of their host stacks, the title's stack check no longer fails there, and the load goes on until a separate Vulkan allocation failure (vkAllocateMemory of a 4 MiB host-visible buffer, out of device memory). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
11 KiB
C++
214 lines
11 KiB
C++
#include "SceTypes.hpp"
|
|
#include "prx/libc/include/general/VabiMacros.hpp"
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <thread>
|
|
#include <xmmintrin.h>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
using Entry = void (APS5_VABI*)(std::uint64_t, std::uint64_t);
|
|
|
|
extern "C" {
|
|
std::int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject*, const char*, Entry, std::uint64_t, void*, std::uint64_t, const void*, std::uint32_t);
|
|
std::int32_t APS5_VABI sceFiberFinalize(FiberObject*);
|
|
std::int32_t APS5_VABI sceFiberRun_nid_postfix(FiberObject*, std::uint64_t, std::uint64_t*);
|
|
std::int32_t APS5_VABI sceFiberSwitch(FiberObject*, std::uint64_t, std::uint64_t*);
|
|
std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*);
|
|
std::int32_t APS5_VABI sceFiberGetSelf(FiberObject**);
|
|
std::int32_t APS5_VABI sceFiberGetInfo(FiberObject*, FiberInfo*);
|
|
std::int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(std::uint64_t*);
|
|
std::int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(std::uint64_t*);
|
|
std::int32_t APS5_VABI sceFiberStartContextSizeCheck(std::uint32_t);
|
|
std::int32_t APS5_VABI sceFiberStopContextSizeCheck(void);
|
|
}
|
|
|
|
static void Require(bool value, const char* what) {
|
|
if (!value) { std::fprintf(stderr, "Fiber check failed: %s\n", what); std::abort(); }
|
|
}
|
|
|
|
namespace {
|
|
|
|
constexpr std::int32_t FiberErrorNull = static_cast<std::int32_t>(0x80590001);
|
|
constexpr std::int32_t FiberErrorInvalid = static_cast<std::int32_t>(0x80590004);
|
|
constexpr std::int32_t FiberErrorPermission = static_cast<std::int32_t>(0x80590005);
|
|
constexpr std::int32_t FiberErrorState = static_cast<std::int32_t>(0x80590006);
|
|
|
|
alignas(16) std::array<unsigned char, 64 * 1024> g_firstContext;
|
|
alignas(16) std::array<unsigned char, 64 * 1024> g_secondContext;
|
|
struct alignas(8) FiberStorage {
|
|
unsigned char bytes[0x100];
|
|
};
|
|
|
|
FiberStorage g_firstStorage;
|
|
FiberStorage g_checkedStorage;
|
|
alignas(16) std::array<unsigned char, 16 * 1024> g_checkedContext;
|
|
std::uint64_t g_threadFramePointer = 0;
|
|
FiberStorage g_secondStorage;
|
|
auto* const g_first = reinterpret_cast<FiberObject*>(&g_firstStorage);
|
|
auto* const g_second = reinterpret_cast<FiberObject*>(&g_secondStorage);
|
|
|
|
void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) {
|
|
Require(argOnInitialize == 11 && argOnRun == 100, "first fiber arguments");
|
|
FiberObject* self = nullptr;
|
|
Require(sceFiberGetSelf(&self) == 0 && self == g_first, "first fiber self");
|
|
std::uint64_t framePointer = 0;
|
|
Require(sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "frame pointer null");
|
|
Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "thread frame pointer");
|
|
framePointer = 0;
|
|
Require(_sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "underscore frame pointer null");
|
|
Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "underscore thread frame pointer");
|
|
volatile double carried = 1.5;
|
|
std::uint64_t received = 0;
|
|
Require(sceFiberSwitch(g_second, 200, &received) == 0, "switch to second fiber");
|
|
Require(received == 300 && carried == 1.5, "switch back to first fiber");
|
|
for (;;) {
|
|
Require(sceFiberReturnToThread(received + 1, &received) == 0, "first fiber return");
|
|
}
|
|
}
|
|
|
|
void APS5_VABI SecondEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) {
|
|
Require(argOnInitialize == 22 && argOnRun == 200, "second fiber arguments");
|
|
Require(sceFiberSwitch(g_second, 0, nullptr) == FiberErrorState, "switch to self");
|
|
_mm_setcsr(_mm_getcsr() | 0x8000u);
|
|
std::uint64_t received = 0;
|
|
Require(sceFiberReturnToThread(250, &received) == 0 && received == 260, "second fiber resumed on another thread");
|
|
Require((_mm_getcsr() & 0x8000u) != 0, "second fiber keeps its MXCSR");
|
|
Require(sceFiberSwitch(g_first, 300, nullptr) == 0, "switch to first fiber");
|
|
Require(false, "second fiber resumed after its last switch");
|
|
}
|
|
|
|
void APS5_VABI CheckedEntry(std::uint64_t, std::uint64_t) {
|
|
volatile unsigned char used[4096];
|
|
for (auto& byte : used) byte = 1;
|
|
std::uint64_t received = 0;
|
|
for (;;) sceFiberReturnToThread(0, &received);
|
|
}
|
|
|
|
constexpr std::size_t SmallContextSize = 2048;
|
|
constexpr unsigned char SmallGuardFill = 0xa5;
|
|
alignas(16) std::array<unsigned char, 64 * 1024> g_smallArea;
|
|
FiberStorage g_smallStorage;
|
|
|
|
void APS5_VABI SmallEntry(std::uint64_t, std::uint64_t) {
|
|
std::uint64_t received = 0;
|
|
for (;;) sceFiberReturnToThread(0, &received);
|
|
}
|
|
|
|
void RunSmallContextFirst() {
|
|
auto* small = reinterpret_cast<FiberObject*>(&g_smallStorage);
|
|
auto* context = g_smallArea.data() + g_smallArea.size() - SmallContextSize;
|
|
g_smallArea.fill(SmallGuardFill);
|
|
Require(sceFiberReturnToThread(0, nullptr) == FiberErrorPermission, "return to thread outside a fiber");
|
|
Require(_sceFiberInitializeImpl_nid_postfix(small, "small", SmallEntry, 0, context, SmallContextSize, nullptr, 0) == 0, "initialize small");
|
|
Require(sceFiberRun_nid_postfix(small, 0, nullptr) == 0, "first fiber run of the process on a 2 KiB context");
|
|
Require(sceFiberRun_nid_postfix(small, 0, nullptr) == 0, "resume on a 2 KiB context");
|
|
Require(sceFiberFinalize(small) == 0, "finalize small");
|
|
for (auto* byte = g_smallArea.data(); byte != context; ++byte) {
|
|
Require(*byte == SmallGuardFill, "nothing is written below a 2 KiB context");
|
|
}
|
|
}
|
|
|
|
constexpr std::size_t HostCallContextSize = 16 * 1024;
|
|
constexpr std::size_t HostCallStack = 256 * 1024;
|
|
constexpr std::size_t HostCallFrameSize = 4096;
|
|
constexpr unsigned char HostCallGuardFill = 0x5c;
|
|
alignas(16) std::array<unsigned char, HostCallStack + 64 * 1024> g_hostCallArea;
|
|
FiberStorage g_hostCallStorage;
|
|
|
|
std::size_t DescendHostStack(std::size_t remaining);
|
|
std::size_t (*volatile g_descendHostStack)(std::size_t) = DescendHostStack;
|
|
|
|
std::size_t DescendHostStack(std::size_t remaining) {
|
|
volatile unsigned char frame[HostCallFrameSize];
|
|
frame[0] = 0x11;
|
|
frame[HostCallFrameSize - 1] = 0x22;
|
|
const std::size_t frames = remaining > HostCallFrameSize ? g_descendHostStack(remaining - HostCallFrameSize) : 0;
|
|
Require(frame[0] == 0x11 && frame[HostCallFrameSize - 1] == 0x22, "host call frame intact");
|
|
return frames + 1;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
void CheckWindowsHostStack() {
|
|
const auto* tib = reinterpret_cast<const NT_TIB*>(NtCurrentTeb());
|
|
const auto low = reinterpret_cast<std::uintptr_t>(tib->StackLimit);
|
|
const auto high = reinterpret_cast<std::uintptr_t>(tib->StackBase);
|
|
const volatile unsigned char marker = 0;
|
|
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
|
|
Require(frame > low && frame < high && high - low >= HostCallContextSize + HostCallStack, "the thread's stack bounds are the fiber's host stack");
|
|
}
|
|
#endif
|
|
|
|
void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) {
|
|
#ifdef _WIN32
|
|
CheckWindowsHostStack();
|
|
#endif
|
|
Require(g_descendHostStack(HostCallStack) == HostCallStack / HostCallFrameSize, "host call depth");
|
|
std::uint64_t received = 0;
|
|
for (;;) sceFiberReturnToThread(0, &received);
|
|
}
|
|
|
|
void RunHostCallOnSmallContext() {
|
|
auto* fiber = reinterpret_cast<FiberObject*>(&g_hostCallStorage);
|
|
auto* context = g_hostCallArea.data() + g_hostCallArea.size() - HostCallContextSize;
|
|
g_hostCallArea.fill(HostCallGuardFill);
|
|
Require(_sceFiberInitializeImpl_nid_postfix(fiber, "host call", HostCallEntry, 0, context, HostCallContextSize, nullptr, 0) == 0, "initialize host call fiber");
|
|
Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run a 256 KiB host call from a 16 KiB fiber");
|
|
Require(sceFiberFinalize(fiber) == 0, "finalize host call fiber");
|
|
for (auto* byte = g_hostCallArea.data(); byte != context; ++byte) {
|
|
Require(*byte == HostCallGuardFill, "a host call from a 16 KiB fiber writes nothing below its context");
|
|
}
|
|
}
|
|
|
|
[[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) {
|
|
g_threadFramePointer = reinterpret_cast<std::uint64_t>(__builtin_frame_address(0));
|
|
const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned);
|
|
asm volatile("" ::: "memory");
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
int main() {
|
|
RunSmallContextFirst();
|
|
FiberObject* self = nullptr;
|
|
Require(sceFiberGetSelf(&self) == FiberErrorPermission, "no fiber on the thread");
|
|
Require(_sceFiberInitializeImpl_nid_postfix(g_first, "first", FirstEntry, 11, g_firstContext.data(), g_firstContext.size(), nullptr, 0) == 0, "initialize first");
|
|
Require(_sceFiberInitializeImpl_nid_postfix(g_second, "second", SecondEntry, 22, g_secondContext.data(), g_secondContext.size(), nullptr, 0) == 0, "initialize second");
|
|
const auto threadCsr = _mm_getcsr();
|
|
std::uint64_t returned = 0;
|
|
std::uint64_t framePointer = 0;
|
|
Require(sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "frame pointer outside a fiber");
|
|
Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "underscore frame pointer outside a fiber");
|
|
Require(RunFirst(&returned) == 0 && returned == 250, "run first until second returns");
|
|
Require(_mm_getcsr() == threadCsr, "thread MXCSR restored after the fibers");
|
|
std::thread([&] {
|
|
Require(sceFiberRun_nid_postfix(g_second, 260, &returned) == 0 && returned == 301, "resume second on another thread");
|
|
}).join();
|
|
Require(sceFiberFinalize(g_first) == 0, "finalize first");
|
|
Require(sceFiberFinalize(g_second) == 0, "finalize second");
|
|
|
|
auto* checked = reinterpret_cast<FiberObject*>(&g_checkedStorage);
|
|
FiberInfo info{};
|
|
info.size = sizeof(info);
|
|
Require(sceFiberStopContextSizeCheck() == FiberErrorState, "stop before start");
|
|
Require(sceFiberStartContextSizeCheck(1) == FiberErrorInvalid, "start with flags");
|
|
Require(_sceFiberInitializeImpl_nid_postfix(checked, "unchecked", CheckedEntry, 0, g_checkedContext.data(), g_checkedContext.size(), nullptr, 0) == 0, "initialize unchecked");
|
|
Require(sceFiberGetInfo(checked, &info) == 0 && info.size_context_margin == ~0ull, "no margin without size check");
|
|
Require(sceFiberFinalize(checked) == 0, "finalize unchecked");
|
|
Require(sceFiberStartContextSizeCheck(0) == 0, "start size check");
|
|
Require(sceFiberStartContextSizeCheck(0) == FiberErrorState, "start twice");
|
|
Require(_sceFiberInitializeImpl_nid_postfix(checked, "checked", CheckedEntry, 0, g_checkedContext.data(), g_checkedContext.size(), nullptr, 0) == 0, "initialize checked");
|
|
Require(sceFiberGetInfo(checked, &info) == 0 && info.size_context_margin == g_checkedContext.size(), "untouched context is all margin");
|
|
Require(sceFiberRun_nid_postfix(checked, 0, nullptr) == 0, "run checked");
|
|
Require(sceFiberGetInfo(checked, &info) == 0, "checked info");
|
|
Require(info.size_context_margin > 0 && info.size_context_margin < g_checkedContext.size() - 4096, "margin below the used stack");
|
|
Require(info.size_context_margin % 8 == 0, "margin in whole words");
|
|
Require(sceFiberStopContextSizeCheck() == 0, "stop size check");
|
|
|
|
RunHostCallOnSmallContext();
|
|
}
|