#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/System/include/UserContext.hpp" #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #endif using ContextEntry = void (APS5_VABI*)(); extern "C" { [[gnu::returns_twice]] int APS5_VABI getcontext_nid_postfix(UserUcontext* context); int APS5_VABI setcontext_nid_postfix(const UserUcontext* context); void APS5_VABI makecontext_nid_postfix(UserUcontext* context, ContextEntry entry, int count, ...); [[gnu::returns_twice]] int APS5_VABI setjmp_nid_postfix(std::uint64_t* buffer); [[noreturn]] void APS5_VABI longjmp_nid_postfix(std::uint64_t* buffer, int value); [[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); [[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); int* APS5_VABI __error_nid_postfix(); int APS5_VABI CaptureKnownRegisters(UserUcontext* context); void CaptureReturnAddress(); void RecordRegisters(); void APS5_VABI EntryThunk(); std::uint64_t CapturedStackPointer = 0; std::uint64_t EntryStackPointer = 0; std::uint64_t RecordedRegisters[18] = {}; std::uint32_t RecordedMxcsr = 0; std::uint16_t RecordedControlWord = 0; alignas(16) std::uint8_t RecordedXmm[16 * 16] = {}; UserUcontext ReturnContext = {}; } asm(".text\n" APS5_ASM_FUNCTION("CaptureKnownRegisters") " push %rbx\n" " push %rbp\n" " push %r12\n" " push %r13\n" " push %r14\n" " push %r15\n" " sub $8, %rsp\n" " movabsq $0x1111111111111111, %rbx\n" " movabsq $0x2222222222222222, %rbp\n" " movabsq $0x3333333333333333, %r12\n" " movabsq $0x4444444444444444, %r13\n" " movabsq $0x5555555555555555, %r14\n" " movabsq $0x6666666666666666, %r15\n" " movabsq $0x7777777777777777, %rcx\n" " movabsq $0x8888888888888888, %rsi\n" " movabsq $0x9999999999999999, %r8\n" " movabsq $0xaaaaaaaaaaaaaaaa, %r9\n" " movabsq $0xbbbbbbbbbbbbbbbb, %r10\n" " movabsq $0xcccccccccccccccc, %r11\n" " movabsq $0xdddddddddddddddd, %rdx\n" " movabsq $0xeeeeeeeeeeeeeeee, %rax\n" " mov %rsp, CapturedStackPointer(%rip)\n" " stc\n" " call getcontext_nid_postfix\n" APS5_ASM_FUNCTION("CaptureReturnAddress") " add $8, %rsp\n" " pop %r15\n" " pop %r14\n" " pop %r13\n" " pop %r12\n" " pop %rbp\n" " pop %rbx\n" " ret\n" APS5_ASM_FUNCTION("RecordRegisters") " pushfq\n" " mov %rax, RecordedRegisters+0x00(%rip)\n" " pop %rax\n" " mov %rax, RecordedRegisters+0x88(%rip)\n" " mov %rbx, RecordedRegisters+0x08(%rip)\n" " mov %rcx, RecordedRegisters+0x10(%rip)\n" " mov %rdx, RecordedRegisters+0x18(%rip)\n" " mov %rsi, RecordedRegisters+0x20(%rip)\n" " mov %rdi, RecordedRegisters+0x28(%rip)\n" " mov %rbp, RecordedRegisters+0x30(%rip)\n" " mov %rsp, RecordedRegisters+0x38(%rip)\n" " mov %r8, RecordedRegisters+0x40(%rip)\n" " mov %r9, RecordedRegisters+0x48(%rip)\n" " mov %r10, RecordedRegisters+0x50(%rip)\n" " mov %r11, RecordedRegisters+0x58(%rip)\n" " mov %r12, RecordedRegisters+0x60(%rip)\n" " mov %r13, RecordedRegisters+0x68(%rip)\n" " mov %r14, RecordedRegisters+0x70(%rip)\n" " mov %r15, RecordedRegisters+0x78(%rip)\n" " stmxcsr RecordedMxcsr(%rip)\n" " fnstcw RecordedControlWord(%rip)\n" " movdqu %xmm0, RecordedXmm+0x00(%rip)\n" " movdqu %xmm1, RecordedXmm+0x10(%rip)\n" " movdqu %xmm2, RecordedXmm+0x20(%rip)\n" " movdqu %xmm3, RecordedXmm+0x30(%rip)\n" " movdqu %xmm4, RecordedXmm+0x40(%rip)\n" " movdqu %xmm5, RecordedXmm+0x50(%rip)\n" " movdqu %xmm6, RecordedXmm+0x60(%rip)\n" " movdqu %xmm7, RecordedXmm+0x70(%rip)\n" " movdqu %xmm8, RecordedXmm+0x80(%rip)\n" " movdqu %xmm9, RecordedXmm+0x90(%rip)\n" " movdqu %xmm10, RecordedXmm+0xa0(%rip)\n" " movdqu %xmm11, RecordedXmm+0xb0(%rip)\n" " movdqu %xmm12, RecordedXmm+0xc0(%rip)\n" " movdqu %xmm13, RecordedXmm+0xd0(%rip)\n" " movdqu %xmm14, RecordedXmm+0xe0(%rip)\n" " movdqu %xmm15, RecordedXmm+0xf0(%rip)\n" " cld\n" " lea ReturnContext(%rip), %rdi\n" " and $-16, %rsp\n" " call setcontext_nid_postfix\n" " ud2\n" APS5_ASM_FUNCTION("EntryThunk") " mov %rsp, EntryStackPointer(%rip)\n" " jmp ContextBody\n"); namespace { constexpr int GuestEinval = 22; constexpr int GuestSigBlock = 1; constexpr int GuestSigSetmask = 3; constexpr std::uint64_t UserChangeableFlags = 0x254dd5; constexpr std::uint64_t InterruptFlag = 0x200; constexpr std::size_t FxsaveMxcsr = 24; constexpr std::size_t FxsaveXmm = 160; constexpr std::size_t StackBytes = 256 * 1024; struct alignas(16) Stack { unsigned char bytes[StackBytes]; }; struct Fibre { UserUcontext context; std::uint64_t jump[12]; int started; }; Stack g_restoreStack; Stack g_contextStack; Stack g_jobStack; Stack g_exitStack; std::uint64_t g_arguments[6] = {}; int g_entries = 0; Fibre g_dispatcher = {}; Fibre g_job = {}; int g_jobRuns = 0; bool g_signalJumps = false; void Require(bool value, const char* what) { if (!value) { std::fprintf(stderr, "User context check failed: %s\n", what); std::abort(); } } std::uint16_t ControlWord() { std::uint16_t control = 0; asm volatile("fnstcw %0" : "=m"(control)); return control; } std::uint64_t Pattern(unsigned index) { return 0x0101010101010101ull * (index + 1) + 0x0f00000000000000ull; } void CheckStackBounds(const Stack* stack, const char* what) { #ifdef _WIN32 ULONG_PTR low = 0; ULONG_PTR high = 0; GetCurrentThreadStackLimits(&low, &high); Require(low == reinterpret_cast(stack->bytes) && high == reinterpret_cast(stack->bytes + StackBytes), what); #else (void)stack; (void)what; #endif } void ThrowAndCatch() { bool caught = false; try { throw std::runtime_error("host exception on a context stack"); } catch (const std::runtime_error&) { caught = true; } Require(caught, "a host exception is caught on a context stack"); } [[gnu::noinline]] void GrowStack(int depth) { volatile unsigned char page[16 * 1024]; page[0] = static_cast(depth); page[sizeof(page) - 1] = static_cast(depth); if (depth > 0) GrowStack(depth - 1); Require(page[0] == static_cast(depth), "a deep host frame keeps its contents"); } void CheckErrors() { *__error_nid_postfix() = 0; Require(getcontext_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "getcontext rejects a null context"); *__error_nid_postfix() = 0; Require(setcontext_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects a null context"); UserUcontext context{}; Require(getcontext_nid_postfix(&context) == 0, "capture a context to corrupt"); context.mcontext.len = 0; *__error_nid_postfix() = 0; Require(setcontext_nid_postfix(&context) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects a wrong mc_len"); context.mcontext.len = sizeof(UserMcontext); context.mcontext.flags = 0x8; *__error_nid_postfix() = 0; Require(setcontext_nid_postfix(&context) == -1 && *__error_nid_postfix() == GuestEinval, "setcontext rejects unknown mc_flags"); context.mcontext.flags = 0; UserUcontext invalid = context; invalid.stackPointer = g_contextStack.bytes; invalid.stackSize = StackBytes; makecontext_nid_postfix(&invalid, &EntryThunk, 7); Require(invalid.mcontext.len == 0, "makecontext invalidates a context with more than six arguments"); invalid = context; invalid.stackPointer = g_contextStack.bytes; invalid.stackSize = 2047; makecontext_nid_postfix(&invalid, &EntryThunk, 0); Require(invalid.mcontext.len == 0, "makecontext invalidates a context with a stack below MINSIGSTKSZ"); invalid = context; invalid.stackPointer = nullptr; invalid.stackSize = StackBytes; makecontext_nid_postfix(&invalid, &EntryThunk, 0); Require(invalid.mcontext.len == 0, "makecontext invalidates a context without a stack"); invalid = context; invalid.stackPointer = g_contextStack.bytes; invalid.stackSize = StackBytes; invalid.mcontext.len = 0; makecontext_nid_postfix(&invalid, &EntryThunk, 0); Require(invalid.mcontext.len == 0 && invalid.mcontext.rip == context.mcontext.rip, "makecontext leaves a context without a valid mc_len untouched"); makecontext_nid_postfix(nullptr, &EntryThunk, 0); } void CheckCapture() { UserUcontext context; std::memset(&context, 0xa5, sizeof(context)); UserUcontext untouched; std::memset(&untouched, 0xa5, sizeof(untouched)); std::uint32_t previous[4] = {}; const std::uint32_t blocked[4] = {0x24, 0, 0, 0}; Require(sigprocmask_nid_postfix(GuestSigSetmask, blocked, previous) == 0, "block signals before the capture"); Require(CaptureKnownRegisters(&context) == 0, "getcontext returns 0"); Require(sigprocmask_nid_postfix(GuestSigSetmask, previous, nullptr) == 0, "restore the signal mask"); const auto& machine = context.mcontext; Require(machine.rbx == 0x1111111111111111ull && machine.rbp == 0x2222222222222222ull, "getcontext saves rbx and rbp"); Require(machine.r12 == 0x3333333333333333ull && machine.r13 == 0x4444444444444444ull, "getcontext saves r12 and r13"); Require(machine.r14 == 0x5555555555555555ull && machine.r15 == 0x6666666666666666ull, "getcontext saves r14 and r15"); Require(machine.rcx == 0x7777777777777777ull && machine.rsi == 0x8888888888888888ull, "getcontext saves rcx and rsi"); Require(machine.r8 == 0x9999999999999999ull && machine.r9 == 0xaaaaaaaaaaaaaaaaull, "getcontext saves r8 and r9"); Require(machine.r10 == 0xbbbbbbbbbbbbbbbbull && machine.r11 == 0xccccccccccccccccull, "getcontext saves r10 and r11"); Require(machine.rdi == reinterpret_cast(&context), "getcontext saves rdi"); Require(machine.rax == 0 && machine.rdx == 0, "getcontext stores rax and rdx as 0"); Require((machine.rflags & 1) == 0, "getcontext clears the carry flag"); Require(machine.rip == reinterpret_cast(&CaptureReturnAddress), "getcontext saves its return address"); Require(machine.rsp == CapturedStackPointer, "getcontext saves the caller's stack pointer"); Require(machine.len == sizeof(UserMcontext) && machine.fpformat == 0x10002 && machine.ownedfp == 0x20001, "getcontext sets mc_len, mc_fpformat and mc_ownedfp"); Require(machine.onstack == 0 && machine.trapno == 0 && machine.addr == 0 && machine.flags == 0 && machine.err == 0, "getcontext zeroes the trap fields"); Require(machine.fsbase == 0 && machine.gsbase == 0 && machine.xfpustate == 0 && machine.xfpustateLength == 0, "getcontext zeroes the bases and the extended FPU state"); for (const auto word : machine.spare) Require(word == 0, "getcontext zeroes mc_spare"); Require(context.sigmask[0] == 0x24 && context.sigmask[1] == 0 && context.sigmask[2] == 0 && context.sigmask[3] == 0, "getcontext saves the signal mask"); std::uint32_t mxcsr = 0; std::memcpy(&mxcsr, reinterpret_cast(machine.fpstate) + FxsaveMxcsr, sizeof(mxcsr)); std::uint16_t control = 0; std::memcpy(&control, machine.fpstate, sizeof(control)); Require(mxcsr == _mm_getcsr() && control == ControlWord(), "getcontext saves MXCSR and the x87 control word"); Require(std::memcmp(&context.link, &untouched.link, sizeof(UserUcontext) - offsetof(UserUcontext, link)) == 0, "getcontext keeps uc_link, uc_stack and uc_flags"); } void CheckRestore() { const auto mxcsrBefore = _mm_getcsr(); const auto controlBefore = ControlWord(); UserUcontext target{}; volatile int phase = 0; Require(getcontext_nid_postfix(&ReturnContext) == 0, "capture the return context"); if (phase == 0) { phase = 1; Require(getcontext_nid_postfix(&target) == 0, "capture the target template"); auto& machine = target.mcontext; machine.rax = Pattern(0); machine.rbx = Pattern(1); machine.rcx = Pattern(2); machine.rdx = Pattern(3); machine.rsi = Pattern(4); machine.rdi = Pattern(5); machine.rbp = Pattern(6); machine.r8 = Pattern(8); machine.r9 = Pattern(9); machine.r10 = Pattern(10); machine.r11 = Pattern(11); machine.r12 = Pattern(12); machine.r13 = Pattern(13); machine.r14 = Pattern(14); machine.r15 = Pattern(15); machine.rsp = reinterpret_cast(g_restoreStack.bytes + StackBytes - 256); machine.rip = reinterpret_cast(&RecordRegisters); machine.rflags = (machine.rflags & ~UserChangeableFlags) | 0x8c1; auto* image = reinterpret_cast(machine.fpstate); const std::uint16_t control = 0x027f; std::memcpy(image, &control, sizeof(control)); const std::uint32_t mxcsr = 0x80007f80u; std::memcpy(image + FxsaveMxcsr, &mxcsr, sizeof(mxcsr)); for (unsigned index = 0; index < 32; ++index) { const auto value = Pattern(index + 16); std::memcpy(image + FxsaveXmm + index * 8, &value, sizeof(value)); } setcontext_nid_postfix(&target); Require(false, "setcontext returned from a valid context"); } const auto& machine = target.mcontext; Require(RecordedRegisters[0] == Pattern(0) && RecordedRegisters[1] == Pattern(1) && RecordedRegisters[2] == Pattern(2) && RecordedRegisters[3] == Pattern(3), "setcontext restores rax, rbx, rcx and rdx"); Require(RecordedRegisters[4] == Pattern(4) && RecordedRegisters[5] == Pattern(5) && RecordedRegisters[6] == Pattern(6), "setcontext restores rsi, rdi and rbp"); for (unsigned index = 8; index < 16; ++index) Require(RecordedRegisters[index] == Pattern(index), "setcontext restores r8 to r15"); Require(RecordedRegisters[7] == machine.rsp, "setcontext restores rsp without touching the target stack"); Require((RecordedRegisters[17] & UserChangeableFlags) == 0x8c1 && (RecordedRegisters[17] & InterruptFlag) != 0, "setcontext restores the user-changeable flags only"); Require(RecordedMxcsr == 0x7f80u, "setcontext restores MXCSR without its reserved bits"); Require(RecordedControlWord == 0x027f, "setcontext restores the x87 control word"); for (unsigned index = 0; index < 32; ++index) { std::uint64_t value = 0; std::memcpy(&value, RecordedXmm + index * 8, sizeof(value)); Require(value == Pattern(index + 16), "setcontext restores XMM0 to XMM15"); } Require(_mm_getcsr() == mxcsrBefore && ControlWord() == controlBefore, "the return context restores MXCSR and the x87 control word"); } } extern "C" void APS5_VABI ContextBody(std::uint64_t first, std::uint64_t second, std::uint64_t third, std::uint64_t fourth, std::uint64_t fifth, std::uint64_t sixth) { g_arguments[0] = first; g_arguments[1] = second; g_arguments[2] = third; g_arguments[3] = fourth; g_arguments[4] = fifth; g_arguments[5] = sixth; CheckStackBounds(&g_contextStack, "the stack bounds follow setcontext onto the context stack"); ThrowAndCatch(); ++g_entries; } extern "C" void APS5_VABI ReturnWithoutLink() {} namespace { [[gnu::noinline]] void Switch(Fibre* from, Fibre* to) { from->started = 1; if (g_signalJumps) { if (sigsetjmp_nid_postfix(from->jump, 1) == 0) { if (to->started) siglongjmp_nid_postfix(to->jump, 1); setcontext_nid_postfix(&to->context); Require(false, "setcontext into the job returned"); } return; } if (setjmp_nid_postfix(from->jump) == 0) { if (to->started) longjmp_nid_postfix(to->jump, 1); setcontext_nid_postfix(&to->context); Require(false, "setcontext into the job returned"); } } } extern "C" void APS5_VABI JobEntry() { for (;;) { ++g_jobRuns; CheckStackBounds(&g_jobStack, g_signalJumps ? "the stack bounds follow siglongjmp onto the job stack" : "the stack bounds follow longjmp onto the job stack"); ThrowAndCatch(); Switch(&g_job, &g_dispatcher); } } namespace { void CheckMakecontext() { #ifdef _WIN32 ULONG_PTR threadLow = 0; ULONG_PTR threadHigh = 0; GetCurrentThreadStackLimits(&threadLow, &threadHigh); #endif UserUcontext caller{}; UserUcontext context{}; volatile int phase = 0; Require(getcontext_nid_postfix(&caller) == 0, "capture the caller"); if (phase == 0) { phase = 1; Require(getcontext_nid_postfix(&context) == 0, "capture the context template"); context.stackPointer = g_contextStack.bytes; context.stackSize = StackBytes; context.link = &caller; makecontext_nid_postfix(&context, &EntryThunk, 6, 0x8000000000000001ull, 0x8000000000000002ull, 0x8000000000000003ull, 0x8000000000000004ull, 0x8000000000000005ull, 0x8000000000000006ull); Require(context.mcontext.len == sizeof(UserMcontext), "makecontext keeps the context valid"); setcontext_nid_postfix(&context); Require(false, "setcontext into the made context returned"); } Require(g_entries == 1, "the made context runs once and resumes uc_link"); for (unsigned index = 0; index < 6; ++index) Require(g_arguments[index] == 0x8000000000000001ull + index, "makecontext passes six 64-bit arguments"); const auto stackLow = reinterpret_cast(g_contextStack.bytes); Require(EntryStackPointer > stackLow && EntryStackPointer < stackLow + StackBytes, "the made context runs on its own stack"); Require((EntryStackPointer + 8) % 16 == 0, "the made context's function starts with an aligned stack"); #ifdef _WIN32 ULONG_PTR low = 0; ULONG_PTR high = 0; GetCurrentThreadStackLimits(&low, &high); Require(low == threadLow && high == threadHigh, "the stack bounds follow setcontext back to the thread stack"); #endif } void CheckJumpBuffers(bool signalJumps) { g_signalJumps = signalJumps; g_jobRuns = 0; g_dispatcher = {}; g_job = {}; #ifdef _WIN32 ULONG_PTR threadLow = 0; ULONG_PTR threadHigh = 0; GetCurrentThreadStackLimits(&threadLow, &threadHigh); #endif Require(getcontext_nid_postfix(&g_job.context) == 0, "capture the job template"); g_job.context.stackPointer = g_jobStack.bytes; g_job.context.stackSize = StackBytes; g_job.context.link = nullptr; makecontext_nid_postfix(&g_job.context, &JobEntry, 0); for (int round = 1; round <= 4; ++round) { Switch(&g_dispatcher, &g_job); Require(g_jobRuns == round, "the job runs once per switch"); #ifdef _WIN32 ULONG_PTR low = 0; ULONG_PTR high = 0; GetCurrentThreadStackLimits(&low, &high); Require(low == threadLow && high == threadHigh, signalJumps ? "the stack bounds follow siglongjmp back to the thread stack" : "the stack bounds follow longjmp back to the thread stack"); #endif } } void CheckSignalMask() { std::uint32_t original[4] = {}; Require(sigprocmask_nid_postfix(GuestSigBlock, nullptr, original) == 0, "read the signal mask"); const std::uint32_t first[4] = {0x10, 0, 0, 0}; const std::uint32_t second[4] = {0x20, 0, 0, 0}; UserUcontext context{}; volatile int phase = 0; Require(sigprocmask_nid_postfix(GuestSigSetmask, first, nullptr) == 0, "set the first mask"); Require(getcontext_nid_postfix(&context) == 0, "capture the masked context"); if (phase == 0) { phase = 1; Require(sigprocmask_nid_postfix(GuestSigSetmask, second, nullptr) == 0, "set the second mask"); setcontext_nid_postfix(&context); Require(false, "setcontext returned from a valid context"); } std::uint32_t current[4] = {}; Require(sigprocmask_nid_postfix(GuestSigBlock, nullptr, current) == 0 && current[0] == 0x10, "setcontext restores the context's signal mask"); Require(sigprocmask_nid_postfix(GuestSigSetmask, original, nullptr) == 0, "restore the signal mask"); } int ExitFromContext() { UserUcontext context{}; Require(getcontext_nid_postfix(&context) == 0, "capture the exiting template"); context.stackPointer = g_exitStack.bytes; context.stackSize = StackBytes; context.link = nullptr; makecontext_nid_postfix(&context, &ReturnWithoutLink, 0); setcontext_nid_postfix(&context); return 1; } } int main(int argc, char** argv) { if (argc > 1 && std::strcmp(argv[1], "exit-on-return") == 0) return ExitFromContext(); CheckErrors(); CheckCapture(); CheckRestore(); CheckSignalMask(); std::thread([] { CheckMakecontext(); CheckJumpBuffers(false); CheckJumpBuffers(true); GrowStack(32); }).join(); return 0; }