Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7a9f61e
Diagnostic: emulate x86-TSO on ARM64 with acquire/release fences
ITSeniy Jul 11, 2026
2556c74
Diagnostic v6: honor ExCreateThread stack sizes, add stack canaries
ITSeniy Jul 11, 2026
26eb35f
Diagnostic v7: software watchpoint on the corrupted animation-node page
ITSeniy Jul 11, 2026
112a85a
Diagnostic v8: lifetime tracking for the guest small-block allocator
ITSeniy Jul 12, 2026
e81a8a0
Merge sansnope/main into diag/issue27-tso-fences
ITSeniy Jul 12, 2026
f01dbfd
Merge sansnope/main (v0.4.0) into diag/issue27-tso-fences
ITSeniy Jul 12, 2026
3158382
Diagnostic v9: quarantine + poison small-block frees
ITSeniy Jul 12, 2026
3d2d273
Diagnostic v10: stop the node-clone from propagating dangling children
ITSeniy Jul 12, 2026
839c4bd
Fix large mods hiding themselves and later mods from the manager (iss…
ITSeniy Jul 12, 2026
b694d01
Restore the original launcher icon with proper adaptive sizing
ITSeniy Jul 12, 2026
2a322c5
Install game files and mods from a user-picked ZIP or folder
ITSeniy Jul 12, 2026
3bed6d2
Adaptive touch controls: menu D-pad and cutscene SKIP button
ITSeniy Jul 12, 2026
336eef1
Hide desktop-only video options on Android, translate the app UI
ITSeniy Jul 12, 2026
3046fe1
Add Texture Quality option and a 256 shadow resolution step
ITSeniy Jul 12, 2026
8c672e3
Add a Planar Reflections toggle
ITSeniy Jul 12, 2026
051248b
Diagnostic v11: repair dead children in the pool swap-remove flusher
ITSeniy Jul 12, 2026
a5d05bf
Diagnostic v12: sink-only child repair, never the surviving sibling
ITSeniy Jul 12, 2026
6d6feec
Diagnostic v13: quarantine every allocator size class under a byte bu…
ITSeniy Jul 12, 2026
3495012
Diagnostic v14: 360-mode teardown tolerance - no poison, absorber region
ITSeniy Jul 12, 2026
e0ba971
Diagnostic v15: poison and absorber together
ITSeniy Jul 12, 2026
9e851e8
Diagnostic v16: survive guest asserts on Android
ITSeniy Jul 12, 2026
e0ad9cb
Diagnostic v17: defer the deallocating destructor - the lifetime fix
ITSeniy Jul 12, 2026
80d2540
Diagnostic v18: raise destructor deferral to one second
ITSeniy Jul 12, 2026
67e31e5
Diagnostic v19: drop drain operations on destroyed pair members
ITSeniy Jul 12, 2026
9b118a4
diag19 verification pass: drop the destructor-deferral experiment
ITSeniy Jul 12, 2026
ddc689c
Diagnostic v20: filter on the entry's node pointer, not the entry
ITSeniy Jul 12, 2026
5b680f6
Diagnostic v21: close both halves of the node lifetime defect
ITSeniy Jul 12, 2026
913036f
Diagnostic v22: notify from the embedded node, not the object base
ITSeniy Jul 12, 2026
f05acfe
Diagnostic v23: defer user-heap frees - close the o1heap corruption leg
ITSeniy Jul 12, 2026
198ce41
Diagnostic v24: remove dying nodes' agents via the game's own removal
ITSeniy Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UnleashedRecomp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ elseif (ANDROID)
set(UNLEASHED_RECOMP_OS_CXX_SOURCES
"os/android/logger_android.cpp"
"os/android/media_android.cpp"
"os/android/page_watch.cpp"
"os/android/process_android.cpp"
"os/android/storage_android.cpp"
"os/android/user_android.cpp"
Expand Down
87 changes: 80 additions & 7 deletions UnleashedRecomp/cpu/guest_thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,76 @@
#include <kernel/memory.h>
#include <kernel/heap.h>
#include <kernel/function.h>
#include <os/logger.h>
#include "ppc_context.h"

constexpr size_t PCR_SIZE = 0xAB0;
constexpr size_t TLS_SIZE = 0x100;
constexpr size_t TEB_SIZE = 0x2E0;
constexpr size_t STACK_SIZE = 0x40000;
constexpr size_t TOTAL_SIZE = PCR_SIZE + TLS_SIZE + TEB_SIZE + STACK_SIZE;
constexpr size_t DEFAULT_STACK_SIZE = 0x40000;

constexpr size_t TEB_OFFSET = PCR_SIZE + TLS_SIZE;

GuestThreadContext::GuestThreadContext(uint32_t cpuNumber)
// Written to the bottom (deepest addresses) of every guest stack; the watchdog and the
// thread's own teardown check it to catch silent stack overruns into the neighbouring
// heap allocations (issue #27 corruption pattern).
constexpr uint32_t STACK_CANARY_WORD = 0x57ACC0DE;
constexpr size_t STACK_CANARY_BYTES = 256;

struct GuestStackRecord
{
const uint8_t* canary;
uint32_t threadId;
bool reported;
};

static std::mutex g_guestStackMutex;
static std::vector<GuestStackRecord> g_guestStacks;

static void FillStackCanary(uint8_t* canary)
{
for (size_t i = 0; i < STACK_CANARY_BYTES; i += sizeof(uint32_t))
*(uint32_t*)(canary + i) = STACK_CANARY_WORD;
}

static bool IsStackCanaryIntact(const uint8_t* canary)
{
for (size_t i = 0; i < STACK_CANARY_BYTES; i += sizeof(uint32_t))
{
if (*(const uint32_t*)(canary + i) != STACK_CANARY_WORD)
return false;
}
return true;
}

void GuestThread::CheckStackCanaries()
{
std::lock_guard lock(g_guestStackMutex);
for (auto& record : g_guestStacks)
{
if (!record.reported && !IsStackCanaryIntact(record.canary))
{
record.reported = true;
LOGF_ERROR("GUEST STACK OVERFLOW: thread {:X} overran its guest stack into the user heap.",
record.threadId);
}
}
}

GuestThreadContext::GuestThreadContext(uint32_t cpuNumber, uint32_t stackSize)
{
assert(thread == nullptr);

thread = (uint8_t*)g_userHeap.Alloc(TOTAL_SIZE);
memset(thread, 0, TOTAL_SIZE);
// Honor the stack size the game requested through ExCreateThread (rounded up to
// 64 KiB); previously every thread got a fixed 256 KiB regardless of the request.
size_t guestStackSize = stackSize != 0
? (size_t(stackSize) + 0xFFFF) & ~size_t(0xFFFF)
: DEFAULT_STACK_SIZE;
guestStackSize = std::max(guestStackSize, size_t(DEFAULT_STACK_SIZE));

allocSize = PCR_SIZE + TLS_SIZE + TEB_SIZE + guestStackSize;
thread = (uint8_t*)g_userHeap.Alloc(allocSize);
memset(thread, 0, allocSize);

*(uint32_t*)thread = ByteSwap(g_memory.MapVirtual(thread + PCR_SIZE)); // tls pointer
*(uint32_t*)(thread + 0x100) = ByteSwap(g_memory.MapVirtual(thread + PCR_SIZE + TLS_SIZE)); // teb pointer
Expand All @@ -27,16 +81,35 @@ GuestThreadContext::GuestThreadContext(uint32_t cpuNumber)
*(uint32_t*)(thread + PCR_SIZE + 0x10) = 0xFFFFFFFF; // that one TLS entry that felt quirky
*(uint32_t*)(thread + PCR_SIZE + TLS_SIZE + 0x14C) = ByteSwap(GuestThread::GetCurrentThreadId()); // thread id

ppcContext.r1.u64 = g_memory.MapVirtual(thread + PCR_SIZE + TLS_SIZE + TEB_SIZE + STACK_SIZE); // stack pointer
ppcContext.r1.u64 = g_memory.MapVirtual(thread + allocSize); // stack pointer
ppcContext.r13.u64 = g_memory.MapVirtual(thread);
ppcContext.fpscr.loadFromHost();

uint8_t* canary = thread + PCR_SIZE + TLS_SIZE + TEB_SIZE;
FillStackCanary(canary);
{
std::lock_guard lock(g_guestStackMutex);
g_guestStacks.push_back({ canary, GuestThread::GetCurrentThreadId(), false });
}

assert(GetPPCContext() == nullptr);
SetPPCContext(ppcContext);
}

GuestThreadContext::~GuestThreadContext()
{
const uint8_t* canary = thread + PCR_SIZE + TLS_SIZE + TEB_SIZE;
if (!IsStackCanaryIntact(canary))
{
LOGF_ERROR("GUEST STACK OVERFLOW: thread {:X} overran its {} KiB guest stack (detected at exit).",
GuestThread::GetCurrentThreadId(), (allocSize - PCR_SIZE - TLS_SIZE - TEB_SIZE) / 1024);
}

{
std::lock_guard lock(g_guestStackMutex);
std::erase_if(g_guestStacks, [&](const GuestStackRecord& r) { return r.canary == canary; });
}

g_userHeap.Free(thread);
}

Expand Down Expand Up @@ -144,7 +217,7 @@ uint32_t GuestThread::Start(const GuestThreadParams& params)
const auto procMask = (uint8_t)(params.flags >> 24);
const auto cpuNumber = procMask == 0 ? 0 : 7 - std::countl_zero(procMask);

GuestThreadContext ctx(cpuNumber);
GuestThreadContext ctx(cpuNumber, params.stackSize);
ctx.ppcContext.r3.u64 = params.value;

g_memory.FindFunction(params.function)(ctx.ppcContext, g_memory.base);
Expand Down
12 changes: 11 additions & 1 deletion UnleashedRecomp/cpu/guest_thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ struct GuestThreadContext
{
PPCContext ppcContext{};
uint8_t* thread = nullptr;
size_t allocSize = 0;

GuestThreadContext(uint32_t cpuNumber);
GuestThreadContext(uint32_t cpuNumber, uint32_t stackSize = 0);
~GuestThreadContext();
};

Expand All @@ -27,6 +28,11 @@ struct GuestThreadParams
uint32_t function;
uint32_t value;
uint32_t flags;
// Guest stack size requested through ExCreateThread; 0 selects the default.
// Ignoring it used to force every thread onto a fixed 256 KiB stack carved
// from the shared user heap, so any deeper thread silently overran into
// neighbouring allocations (issue #27).
uint32_t stackSize = 0;
};

struct GuestThreadHandle : KernelObject
Expand Down Expand Up @@ -55,6 +61,10 @@ struct GuestThread
static uint32_t GetCurrentThreadId();
static void SetLastError(uint32_t error);

// Logs an error for every live guest stack whose bottom canary was overwritten.
// Called periodically by the Android log watchdog.
static void CheckStackCanaries();

#ifdef _WIN32
static void SetThreadName(uint32_t threadId, const char* name);
#endif
Expand Down
Loading