// Standalone, minimal armeabi-v7a artifact for the 2026-09-16 // ARM64_TRANSLATION_LAYER.md "isolated std::ostringstream repro" test - see // that doc's entry of the same date for the full methodology and the // GuestEngine-side driver (mpcore/src/main/cpp/emu/ostream_repro_test.cpp) // that loads and calls this. // // Why this exists: the real game's own libapp.so, deep inside its shader- // variant builder (sub_4702D8, per ARM64_TRANSLATION_LAYER.md's 2026-09-16 // entries), writes several string literals to a real, compiled // std::ostringstream-shaped object via operator<< and then extracts the // accumulated text via what's effectively basic_stringbuf::str() - // and the extraction always comes back empty under GuestEngine, even though // register/memory probes confirmed real content was genuinely written // first. Rather than keep chasing that one binary's own hardcoded // addresses (explicitly the wrong approach per the user's own direction - // "ты сейчас пытаешься подогнать эмуляцию к одному единственному бинарнику, // это не правильный подход" - that only tells us about THIS game, not // whether GuestEngine itself has a real, general ostringstream-extraction // bug), this reproduces the EXACT SAME write-then-extract shape in total // isolation from every other line of game code, compiled fresh by the same // NDK toolchain and loaded as its own tiny ELF image // (GuestEngine::LoadSecondaryImage) alongside (not instead of) libapp.so. // // Deliberately built the SAME way libapp.so itself is (default NDK // ANDROID_STL=c++_shared, not a static-libstdc++/c++_static build) rather // than statically linking libc++ into this .so: rtti_shims.cpp's own // RegisterRttiImportShims already shows GuestEngine hand-reimplements a // SPECIFIC set of libc++-internal symbols (locale/ios_base/ctype/ // num_put/__shared_weak_count/std::mutex - all undefined imports in // libapp.so's own .dynsym, meaning libapp.so links these dynamically // against libc++_shared.so too, while template-heavy code like // basic_stringbuf's own methods gets compiled directly into // libapp.so's .text, same as here). A c++_static build of this artifact // would sidestep ALL of those hand-shims entirely and test a completely // different code path - less isolated from "does the real game's runtime // dependency surface work," not more. Matching libapp.so's own linkage // means this test exercises the EXACT SAME shim functions the real crash // investigation already spent most of 2026-09-16 on (Shim_locale_ctor, // Shim_use_facet, Shim_ctype_char_do_widen, Shim_ios_base_init) - if the // bug lives in one of those, this test should reproduce it; if it doesn't, // that's real evidence the bug is specific to something in libapp.so's own // state/control flow instead. // // GuestEngine::LoadSecondaryImage resolves every undefined symbol through // the SAME ResolveOrCreateImportStub/RegisterImportShim table libapp.so's // own imports already use (see that function's own comment, // guest_engine.cpp) - so this artifact needs no special-casing on the // engine side beyond what already exists, with one confirmed exception: // operator new/delete (_Znwj/_ZdlPv et al.) had NO shim registered anywhere // in this codebase before this test - grepped the whole emu/ tree and came // up empty. Not a pre-existing bug: libapp.so's own build apparently // defines ITS OWN operator new/delete locally (a common AAA-engine pattern, // pooled/custom allocators overriding the global operator) - a LOCALLY // DEFINED symbol never touches ResolveOrCreateImportStub at all, so this // engine never had to shim it before. A plain artifact like this one that // does NOT override global operator new/delete needs the real ones, added // to rtti_shims.cpp (Shim_operator_new/Shim_operator_delete) specifically // to make this test possible - see that file's own comment. #include #include #include extern "C" __attribute__((visibility("default"))) int TestOstreamAssembly(char* outBuf, int outBufSize) { // Matches the real game's own pattern (sub_4702D8): several SEPARATE // writes via operator<< before one .str() extraction, not a single // combined write - see this session's investigation notes on why that // distinction might matter (a single write could mask a bug that only // shows up across multiple overflow()/sputn() calls into the same // streambuf). Content itself doesn't matter - it's deliberately // boilerplate-shaped (comment lines + a function skeleton) to loosely // mirror the real shader source text without claiming to BE a shader. std::ostringstream oss; oss << "//FRAGMENT SHADER\n"; oss << "//===========\n\n"; oss << "void main()\n{\n"; oss << "}\n"; std::string result = oss.str(); int32_t len = (int32_t)result.size(); if (outBuf && outBufSize >= 4) { memcpy(outBuf, &len, sizeof(len)); int avail = outBufSize - 4; int copyLen = (int)result.size(); if (copyLen > avail) copyLen = avail; if (copyLen > 0) memcpy(outBuf + 4, result.data(), (size_t)copyLen); if (copyLen < avail) outBuf[4 + copyLen] = 0; // NUL-terminate for easy logging, if room } return len; } // 2026-09-17, ARM64_TRANSLATION_LAYER.md - the 0x3d3d3d3d heap-overflow // chase. TestOstreamAssembly's own pattern (above) uses 4 short, similarly- // sized writes with no intervening function call - none of them alone force // an IMMEDIATE SSO->heap transition, and there's no real CALL boundary // between writes into the SAME stream. The real crash's own sequence // (sub_46FD58) is different in both respects: its FIRST write is // "//VERTEX SHADER\n//=============\n\n" (33 bytes - by itself already past // libc++'s ~22-byte SSO capacity, forcing an immediate heap transition on // the very first write), immediately followed by a call into a SEPARATE // function (sub_4711C8) that writes MORE content ("//Attributes\n // //==========\n", 26 bytes) into the SAME shared stream object, passed by // pointer across that real call boundary. Reproducing that exact shape here // - noinline to guarantee a genuine function-call boundary, not something // the compiler could inline away - to test whether THIS specific pattern // (not just "several small writes in one function") is what's needed to // expose the bug under GuestEngine. extern "C" __attribute__((noinline)) void WriteAttributesSectionNested(std::ostringstream& oss) { oss << "//Attributes\n//==========\n"; } extern "C" __attribute__((visibility("default"))) int TestOstreamAssemblyNested(char* outBuf, int outBufSize) { std::ostringstream oss; oss << "//VERTEX SHADER\n//=============\n\n"; WriteAttributesSectionNested(oss); std::string result = oss.str(); int32_t len = (int32_t)result.size(); if (outBuf && outBufSize >= 4) { memcpy(outBuf, &len, sizeof(len)); int avail = outBufSize - 4; int copyLen = (int)result.size(); if (copyLen > avail) copyLen = avail; if (copyLen > 0) memcpy(outBuf + 4, result.data(), (size_t)copyLen); if (copyLen < avail) outBuf[4 + copyLen] = 0; } return len; }