From dd73f229e779e1b4a055dde67a3326a59f3ded2c Mon Sep 17 00:00:00 2001 From: Adam ? Date: Tue, 25 Aug 2026 01:24:19 -0700 Subject: [PATCH] Add liveness.cpp/hpp logic and force offset resolution to be done early We do this by forcibly including the il2cpp source directly. This required some hackiness to get around the bs-hook typedefs.h issues, but ultimately this should be an improvement, since that source is relatively stable across Unity updates so far. This should also help prevent many of spurious liveness issues that were plaguing people in the past. Atomic logic and HasParentUnsafe logic taken from: https://github.com/QuestPackageManager/Il2CppQuestTypePatching/pull/24 --- include/liveness.hpp | 9 + qpm.shared.json | 22 +-- src/liveness.cpp | 424 +++++++++++++++++++++++++++++++++++++++++++ src/register.cpp | 391 +-------------------------------------- 4 files changed, 448 insertions(+), 398 deletions(-) create mode 100644 include/liveness.hpp create mode 100644 src/liveness.cpp diff --git a/include/liveness.hpp b/include/liveness.hpp new file mode 100644 index 0000000..6a8129b --- /dev/null +++ b/include/liveness.hpp @@ -0,0 +1,9 @@ +#pragma once + +// Defines the hooks used for liveness. +#ifndef CT_USE_GCDESCRIPTOR_DEBUG +#warning "You probably should only include this with CT_USE_GCDESCRIPTOR_DEBUG" +#endif +namespace liveness { + void EnsureHooks(); +} diff --git a/qpm.shared.json b/qpm.shared.json index 5db1609..b59b9da 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -103,12 +103,12 @@ { "dependency": { "id": "paper2_scotland2", - "versionRange": "=4.7.0", + "versionRange": "=4.8.0", "additionalData": { - "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.7.0/libpaper2_scotland2.so", + "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.8.0/libpaper2_scotland2.so", "overrideSoName": "libpaper2_scotland2.so", - "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.7.0/paper2_scotland2.qmod", - "branchName": "version/v4_7_0", + "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.8.0/paper2_scotland2.qmod", + "branchName": "version/v4_8_0", "compileOptions": { "systemIncludes": [ "shared/utfcpp/source" @@ -117,7 +117,7 @@ "cmake": false } }, - "version": "4.7.0" + "version": "4.8.0" }, { "dependency": { @@ -153,13 +153,13 @@ { "dependency": { "id": "beatsaber-hook", - "versionRange": "=8.0.0", + "versionRange": "=8.1.0", "additionalData": { - "soLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v8.0.0/libbeatsaber-hook.so", - "debugSoLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v8.0.0/debug_libbeatsaber-hook.so", + "soLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v8.1.0/libbeatsaber-hook.so", + "debugSoLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v8.1.0/debug_libbeatsaber-hook.so", "overrideSoName": "libbeatsaber-hook.so", - "modLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v8.0.0/beatsaber-hook.qmod", - "branchName": "version/v8_0_0", + "modLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v8.1.0/beatsaber-hook.qmod", + "branchName": "version/v8_1_0", "compileOptions": { "cppFlags": [ "-Wno-extra-qualification" @@ -168,7 +168,7 @@ "cmake": true } }, - "version": "8.0.0" + "version": "8.1.0" }, { "dependency": { diff --git a/src/liveness.cpp b/src/liveness.cpp new file mode 100644 index 0000000..d99d9fa --- /dev/null +++ b/src/liveness.cpp @@ -0,0 +1,424 @@ +// This file serves to provide debug information for liveness related issues. +// It is tied with register.cpp, who will install the exported hooks from this. + +// This file is an il2cpp-private includer, so we should NEVER include bs-hook files here (sans logging). + +#ifdef CT_USE_GCDESCRIPTOR_DEBUG + +#ifdef LOCAL_TEST +#warning "Hey, you shouldn't use LOCAL_TEST while building with CT_USE_GCDESCRIPTOR_DEBUG!" +#endif + +#include "liveness.hpp" + +#include "logging.hpp" + +#include +#include + +// The filter class offset is of the LivenessState structure, defined in: il2cpp/libil2cpp/vm/Liveness.cpp +// The issue, however, is that this is defined cleanly within a .cpp file. We CAN include this here, however. +// TODO: In the future, we should decide whether the overhead of emitting is actually worth it over copying the struct... +// WE NEED THE FOLLOWING DEFINES, AND THEY MUST BE BEFORE ANY BS-HOOK INCLUDE! +#define RUNTIME_IL2CPP 1 +#define BASELIB_INLINE_NAMESPACE il2cpp_baselib +#pragma GCC visibility push(hidden) +#include "vm/Liveness.cpp" +#pragma GCC visibility pop + +namespace { + + bool disableLivenessChecks() { + return std::getenv("CT_DISABLE_LIVENESS_CHECKS") != nullptr || std::getenv("ANDROID_EMULATOR") != nullptr; + } + + void* ct_malloc(std::size_t sz) { + custom_types::logger.debug("Tracking an il2cpp malloc of: {}", sz); + return malloc(sz); + } + void* ct_aligned_malloc(std::size_t sz, std::size_t alignment) { + custom_types::logger.debug("Tracking an il2cpp aligned malloc: {}, {}", sz, alignment); + return memalign(sz, alignment); + } + void ct_free(void* ptr) { + custom_types::logger.debug("Tracking an il2cpp free: {}", fmt::ptr(ptr)); + free(ptr); + } + void ct_aligned_free(void* ptr) { + custom_types::logger.debug("Tracking an il2cpp aligned free: {}", fmt::ptr(ptr)); + free(ptr); + } + void* ct_calloc(std::size_t nmeb, std::size_t sz) { + custom_types::logger.debug("Tracking an il2cpp calloc: {}, {}", nmeb, sz); + return calloc(nmeb, sz); + } + void* ct_realloc(void* ptr, std::size_t sz) { + custom_types::logger.debug("Tracking an il2cpp realloc: {}, {}", fmt::ptr(ptr), sz); + return realloc(ptr, sz); + } + void* ct_aligned_realloc(void* memory, std::size_t newSize, std::size_t alignment) { + custom_types::logger.debug("Tracking an il2cpp aligned realloc: {}, {}, {}", fmt::ptr(memory), newSize, alignment); + void* newMemory = realloc(memory, newSize); + + // Fast path: realloc returned aligned memory + if ((reinterpret_cast(newMemory) & (alignment - 1)) == 0) + return newMemory; + + // Slow path: realloc returned non-aligned memory + void* alignedMemory = ct_aligned_malloc(newSize, alignment); + memcpy(alignedMemory, newMemory, newSize); + free(newMemory); + return alignedMemory; + } + +} // end anonymous namespace + +#include "capstone-helpers.hpp" +#include "logging.hpp" +#include "beatsaber-hook/shared/api.hpp" +#include "beatsaber-hook/shared/capstone.hpp" +#include "beatsaber-hook/shared/hooking.hpp" + +namespace { + + char const* namespaze(Il2CppClass* const klass) { + return klass->namespaze ? klass->namespaze : ""; + } + char const* namek(Il2CppClass* const klass) { + return klass->name ? klass->name : ""; + } + std::string generics(Il2CppClass* const klass) { + auto* genClass = klass->generic_class; + if (genClass) { + auto* genInst = genClass->context.class_inst; + if (genInst) { + std::string outp; + for (size_t i = 0; i < genInst->type_argc; i++) { + outp += std::string(" ") + i2c::type_simple_name(genInst->type_argv[i]); + } + return outp; + } + } + return "NO GENERICS"; + } + + std::atomic_size_t generic_obj_traverse_count = 0; + std::atomic_size_t obj_traverse_count = 0; + + MAKE_HOOK(LivenessState_TraverseGenericObject, (nullptr), void, Il2CppObject* obj, il2cpp::vm::LivenessState* state) { + if (disableLivenessChecks()) { + // If we are disabling liveness checks, just call the original function. + return LivenessState_TraverseGenericObject(obj, state); + } + // We are calling this with an object and a state. + // The state is the LivenessState instance + // There is a process_array that we want to look at + // Likewise we also want to look at our traverse_depth + // Not to mention the actual object and state ptrs + + // auto klass = GET_CLASS(obj); + // custom_types::logger.debug("{}: + // LivenessState::TraverseGenericObject({}, {}), with klass: {} ({}::{})", + // generic_obj_traverse_count++, fmt::ptr(obj), fmt::ptr(state), fmt::ptr(obj->klass), namespaze(klass), + // namek(klass)); process_array is at 0x18 (custom_growable_array*) + // traverse_depth is at 0x48 (int) + // auto arrPtr = + // *reinterpret_cast**>(reinterpret_cast(state) + // + 0x18); custom_types::logger.debug("traverse_depth: {}", + // *reinterpret_cast(reinterpret_cast(state) + 0x48)); + // custom_types::logger.debug("arrPtr: {}, inner: {}, size: {}", fmt::ptr(arrPtr), + // fmt::ptr(arrPtr->data()), arrPtr->size()); custom_types::logger.flush(); for + // (size_t i = 0; i < arrPtr->size(); i++) { auto inst = arrPtr->data()[i]; + // custom_types::logger.debug("arr val: {} ptr: {}, class: {} + // ({}::{})", i, fmt::ptr(inst), fmt::ptr(inst->klass), namespaze(GET_CLASS(inst)), + // namek(GET_CLASS(inst))); + // } + // if (arrPtr->size() > 0) { + // auto inst = arrPtr->data()[arrPtr->size() - 1]; + // custom_types::logger.debug("arr val: {}, ptr: {}, class: {} + // ({}::{})", arrPtr->size() - 1, fmt::ptr(inst), fmt::ptr(inst->klass), + // namespaze(GET_CLASS(inst)), namek(GET_CLASS(inst))); + // custom_types::logger.flush(); + // } + generic_obj_traverse_count.fetch_add(1, std::memory_order_relaxed); + LivenessState_TraverseGenericObject(obj, state); + // custom_types::logger.debug("Complete + // LivenessState::TraverseGenericObject"); + } + + MAKE_HOOK( + LivenessState_TraverseObjectInternal, (nullptr), bool, Il2CppObject* obj, bool isStruct, Il2CppClass* klass, il2cpp::vm::LivenessState* state + ) { + if (disableLivenessChecks()) { + // If we are disabling liveness checks, do not call the original function. + return LivenessState_TraverseObjectInternal(obj, isStruct, klass, state); + } + // Here we are going to log... AGAIN + // but this time only a few things + // custom_types::logger.debug("LivenessState::TraverseObjectInternal({}, + // {}, {}, {})", fmt::ptr(obj), isStruct, fmt::ptr(klass), fmt::ptr(state)); + // custom_types::logger.flush(); + // custom_types::logger.debug("class: ({}::{})", namespaze(klass), + // namek(klass)); custom_types::logger.flush(); + obj_traverse_count.fetch_add(1, std::memory_order_relaxed); + auto ret = LivenessState_TraverseObjectInternal(obj, isStruct, klass, state); + // custom_types::logger.debug("Complete + // LivenessState::TraverseObjectInternal"); + return ret; + } + + // MAKE_HOOK(Liveness_FromStatics, nullptr, void, void* state) { + // auto filter = state->filter); + // custom_types::logger.debug("Liveness::FromStatics({})", fmt::ptr(state)); + // custom_types::logger.debug("filter class: {}", fmt::ptr(filter)); + // custom_types::logger.flush(); + // custom_types::logger.debug("filter class: {}::{}", namespaze(filter), + // namek(filter)); custom_types::logger.flush(); + // // TODO: Log class statics info + // Liveness_FromStatics(state); + // custom_types::logger.debug("Complete Liveness::FromStatics"); + // } + + static inline bool HasParentUnsafe(Il2CppClass const* klass, Il2CppClass const* parent) { + return klass->typeHierarchyDepth >= parent->typeHierarchyDepth && klass->typeHierarchy[parent->typeHierarchyDepth - 1] == parent; + } + + MAKE_HOOK(LivenessState_TraverseGCDescriptor, (nullptr), void, Il2CppObject* obj, il2cpp::vm::LivenessState* state) { + if (disableLivenessChecks()) { + return LivenessState_TraverseGCDescriptor(obj, state); + } + + // Note: gc_desc is a bitfield that holds which fields of the type has + // references, or, if it is too large, a pointer to a table which holds a + // larger bitfield. + int i = 0; + size_t mask = (size_t) (GET_CLASS(obj)->gc_desc); + + IL2CPP_ASSERT(mask & (size_t) 1); + + for (i = 0; i < WORDSIZE - 2; i++) { + size_t offset = ((size_t) 1 << (WORDSIZE - 1 - i)); + if (mask & offset) { + Il2CppObject** valptr = (Il2CppObject**) (((char*) obj) + i * sizeof(void*)); + Il2CppObject* val = *valptr; + if (!val || IS_MARKED(val)) { + // Null instances are permitted + continue; + } + // We aren't marked at this point, val is a new pointer in our instance + // PreviewDifficultyBeatmapSet + // mask: 0b0011000...1 + // f1: BeatmapCharacteristicSO (skipped explicitly in if above) + // f2: BeatmapDifficulty[] (crashes below) + // val is: pointer to BeatmapDifficulty[] (at least, it SHOULD be) + // specifically, val is a pointer to: 39D0 before obj + // Offset of filter class + auto filterClass = state->filter; + if (!val->klass || + (GET_CLASS(val)->has_references == 0 && GET_CLASS(val)->klass != GET_CLASS(val) && GET_CLASS(val)->name == nullptr) || + // If our filter class is not null, and + // our filter class' type hierarchy depth is <= ours and + // our type hierarchy pointer is garbage + (filterClass && filterClass->typeHierarchyDepth <= GET_CLASS(val)->typeHierarchyDepth && + (reinterpret_cast(GET_CLASS(val)->typeHierarchy) <= 0x1000 || + (reinterpret_cast(GET_CLASS(val)->typeHierarchy) & 0x00FFFFFFFFFFFFFFULL) > 0xe000000000))) { + // We have a VERY BIG PROBLEM! + // This will cause a (hard to diagnose) crash! + // So, we will dump as much info as we can. + custom_types::logger.critical("WARNING! THIS WILL CRASH, DUMPING SEMANTIC INFORMATION..."); + custom_types::logger.critical( + "LivenessState::TraverseGCDescriptor({}, {}) class: {}, gc_desc: " + "{}, {}::{} {}", + fmt::ptr(obj), + fmt::ptr(state), + fmt::ptr(GET_CLASS(obj)), + fmt::ptr(GET_CLASS(obj)->gc_desc), + namespaze(GET_CLASS(obj)), + namek(GET_CLASS(obj)), + generics(GET_CLASS(obj)).c_str() + ); + // malloc_info() + // TODO: Yeah + std::filesystem::path path = "/sdcard/ModData/com.beatgames.beatsaber/Mods/CustomTypes/CustomTypesMallocInfoOnExit.xml"; + if (!std::filesystem::exists(path.parent_path())) + std::filesystem::create_directories(path.parent_path()); + auto f = fopen(path.c_str(), "w"); + if (!malloc_info(0, f)) { + custom_types::logger.critical("Failed to write to: {}!", path.c_str()); + } else { + custom_types::logger.debug("Wrote malloc info to: {}", path.c_str()); + } + fclose(f); + custom_types::logger.critical( + "LivenessState::TraverseGCDescriptor({}, {}), with val: {} (klass: " + "{}), idx: {}", + fmt::ptr(obj), + fmt::ptr(state), + fmt::ptr(val), + fmt::ptr(val->klass), + i + ); + if (GET_CLASS(val)) { + custom_types::logger.critical("has_references: {}", (bool) GET_CLASS(val)->has_references); + } + // custom_types::logger.critical("Logging filterClass: {}", + // fmt::ptr(filterClass)); custom_types::logAll(filterClass); + // custom_types::logger.critical("Logging all registered custom + // types..."); for (auto k : custom_types::Register::classes) { + // custom_types::logger.critical("KLASS PTR: {}", fmt::ptr(k)); + // custom_types::logAll(k); + // } + + custom_types::logger.debug( + "gc descriptor test field: obj: {}, " + "field: {}, value: {}, class: {}", + fmt::ptr(obj), + fmt::ptr(valptr), + fmt::ptr(val), + fmt::ptr(val ? val->klass : nullptr) + ); + custom_types::logger.critical("obj_traverse_count: {}", obj_traverse_count.load(std::memory_order_relaxed)); + custom_types::logger.critical("generic_obj_traverse_count: {}", generic_obj_traverse_count.load(std::memory_order_relaxed)); + + custom_types::logger.critical( + "Talk to Sc2ad to try and understand what the hell is going on " + "here and why." + ); + custom_types::logger.critical( + "Also, please be very kind and send him this whole log file! It " + "would be much appreciated." + ); + custom_types::logger.critical( + "With that said, the log in this file may have been truncated, so " + "consider grabbing the file log for custom types instead." + ); + custom_types::logger.critical( + "custom types will now try to log as much information it can about " + "the offending instance's class before crashing..." + ); + custom_types::logger.debug("Capturing memory snapshot..."); + // auto snapshot = il2cpp_functions::capture_memory_snapshot(); + // auto snapshot_path = string_format(LOG_PATH, + // "com.beatgames.beatsaber") + "MemoryDump.bin"; std::ofstream + // memory_snapshot(snapshot_path, std::ios::binary); + // memory_snapshot.write(reinterpret_cast(snapshot), + // sizeof(*snapshot)); memory_snapshot.close(); + // il2cpp_functions::free_captured_memory_snapshot(snapshot); + // custom_types::logger.debug("Logging memory dump to {}", + // snapshot_path.c_str()); + custom_types::logger.critical("KLASS PTR: {}", fmt::ptr(obj->klass)); + if (GET_CLASS(obj)) { + custom_types::logAll(GET_CLASS(obj)); + } + custom_types::logger.critical("KLASS PTR: {}", fmt::ptr(val->klass)); + if (filterClass) { + custom_types::logger.critical("Attempting HasParentUnsafe({}, {})...", fmt::ptr(GET_CLASS(val)), fmt::ptr(filterClass)); + auto ret = HasParentUnsafe(GET_CLASS(val), filterClass); + custom_types::logger.critical("HasParentUnsafe return: {}", ret); + } + if (GET_CLASS(val)) { + custom_types::logAll(GET_CLASS(val)); + } + // Things I have learned, just dumping here: + // static fields and classes that have a nonzero quantity of static + // fields need to be added to: Class::GetStaticFieldData() as for + // Liveness::FromRoot, it MIGHT NOT be the base, because the recursion + // layer is not bt-able due to b's instead of bl's So, what COULD happen + // is that the root liveness calc DOES NOT have a gc descriptor, and it + // is only a type later on that does. Perhaps we should hook the + // TraverseGenericObject function instead and see what we can learn as + // we walk the root? Seems to be the first field of a dictionary that + // has this issue Also the 0x60 field of a Regex::Match type, which is + // an array: [FieldOffset(Offset = "0x60")] [Token(Token = + // "0x040002A5")] internal int[] _matchcount; + + // Have we damaged an array creation method on accident? + } + } + } + // Call orig + LivenessState_TraverseGCDescriptor(obj, state); +#undef WORDSIZE +#undef GET_CLASS +#undef IS_MARKED + } + + std::optional readsafeb(uint32_t const* const addr) { + cs_insn* insns; + // Read from addr, 1 instruction, with pc at addr, into insns. + // TODO: consider using cs_disasm_iter + auto count = + cs_disasm(cs::get_handle(), reinterpret_cast(addr), sizeof(uint32_t), reinterpret_cast(addr), 1, &insns); + RET_DEF_UNLESS(custom_types::logger, count == 1); + auto inst = insns[0]; + // Thunks have a single b + RET_DEF_UNLESS(custom_types::logger, inst.id == ARM64_INS_B); + auto platinsn = inst.detail->arm64; + RET_DEF_UNLESS(custom_types::logger, platinsn.op_count == 1); + auto op = platinsn.operands[0]; + RET_DEF_UNLESS(custom_types::logger, op.type == ARM64_OP_IMM); + // Our b dest is addr + (imm << 2), except capstone does this for us. + auto dst = reinterpret_cast(op.imm); + cs_free(insns, 1); + return dst; + } + +} // end anonymous namespace + +void liveness::EnsureHooks() { + using namespace custom_types; + // Install memory callbacks + Il2CppMemoryCallbacks callbacks{ + .malloc_func = ct_malloc, + .aligned_malloc_func = ct_aligned_malloc, + .free_func = ct_free, + .aligned_free_func = ct_aligned_free, + .calloc_func = ct_calloc, + .realloc_func = ct_realloc, + .aligned_realloc_func = ct_aligned_realloc, + }; + // il2cpp_functions::set_memory_callbacks(&callbacks); +#define BREAK(var, ...) \ + do { \ + if (!var) { \ + logger.warn(__VA_ARGS__); \ + goto exit; \ + } \ + } while (0) + { + // We need to xref trace to get to LivenessState stuff + // il2cpp_unity_liveness_calculation_from_root + // only a b --> Liveness::FromRoot + // 2nd bl --> LivenessState::TraverseObjects + // 1st bl --> LivenessState::TraverseGenericObject + // 2nd b --> LivenessState::TraverseGCDescriptor + + // If we fail anywhere in this chain, simply log it and move on. + // We shouldn't care, this is just a debug hook after all. + auto opt = readsafeb((uint32_t*) i2c::functions::unity_liveness_calculation_from_root); + BREAK(opt, "Failed to find b in il2cpp_unity_liveness_calculation_from_root!"); + opt = cs::findNthBlSafe<2>(*opt); + BREAK(opt, "Failed to find 2nd bl in Liveness::FromRoot!"); + opt = cs::findNthBlSafe<1>(*opt); + BREAK(opt, "Failed to find 1st bl in Liveness::TraverseObject!"); + auto traverseGeneric = *opt; + auto traverseInternal = cs::findNthBSafe<3>(*opt); + opt = cs::findNthBSafe<2>(*opt); + BREAK(opt, "Failed to find 2nd b in LivenessState::TraverseGenericObject!"); + BREAK(traverseInternal, "Failed to find 3rd b in LivenessState::TraverseGenericObject!"); + // We found all of the chain, lets install our debug hook! + INSTALL_HOOK(logger, LivenessState_TraverseGenericObject, traverseGeneric); + INSTALL_HOOK(logger, LivenessState_TraverseGCDescriptor, *opt); + INSTALL_HOOK(logger, LivenessState_TraverseObjectInternal, *traverseInternal); + // opt = + // readsafeb((uint32_t*)i2c::il2cpp_unity_liveness_calculation_from_statics); + // BREAK(opt, "Failed to find b in + // il2cpp_unity_liveness_calculation_from_statics!"); + // INSTALL_HOOK(logger, Liveness_FromStatics, *opt); + } +#undef BREAK +exit: + return; +} + +#endif diff --git a/src/register.cpp b/src/register.cpp index c7eace2..3efa831 100644 --- a/src/register.cpp +++ b/src/register.cpp @@ -10,14 +10,10 @@ #include #ifdef CT_USE_GCDESCRIPTOR_DEBUG -#include "capstone-helpers.hpp" -#include "utils/dynamic_array.h" -#include "vm/MemoryInformation.h" +#include "liveness.hpp" #endif -bool disableLivenessChecks() { - return std::getenv("CT_DISABLE_LIVENESS_CHECKS") != nullptr || std::getenv("ANDROID_EMULATOR") != nullptr; -} +namespace { // checks whether the ty->data could be a pointer. technically could be UB if the address is low enough bool MetadataHandleSet(Il2CppType const* ty) { @@ -159,335 +155,7 @@ MAKE_HOOK(GetScriptingClass, (nullptr), Il2CppClass*, void* thisptr, char* assem return ret; } -#ifdef CT_USE_GCDESCRIPTOR_DEBUG - -#ifdef LOCAL_TEST -#warning "Hey, you shouldn't use LOCAL_TEST while building with CT_USE_GCDESCRIPTOR_DEBUG!" -#endif - -#include - -void* ct_malloc(std::size_t sz) { - custom_types::logger.debug("Tracking an il2cpp malloc of: {}", sz); - return malloc(sz); -} -void* ct_aligned_malloc(std::size_t sz, std::size_t alignment) { - custom_types::logger.debug("Tracking an il2cpp aligned malloc: {}, {}", sz, alignment); - return memalign(sz, alignment); -} -void ct_free(void* ptr) { - custom_types::logger.debug("Tracking an il2cpp free: {}", fmt::ptr(ptr)); - free(ptr); -} -void ct_aligned_free(void* ptr) { - custom_types::logger.debug("Tracking an il2cpp aligned free: {}", fmt::ptr(ptr)); - free(ptr); -} -void* ct_calloc(std::size_t nmeb, std::size_t sz) { - custom_types::logger.debug("Tracking an il2cpp calloc: {}, {}", nmeb, sz); - return calloc(nmeb, sz); -} -void* ct_realloc(void* ptr, std::size_t sz) { - custom_types::logger.debug("Tracking an il2cpp realloc: {}, {}", fmt::ptr(ptr), sz); - return realloc(ptr, sz); -} -void* ct_aligned_realloc(void* memory, std::size_t newSize, std::size_t alignment) { - custom_types::logger.debug("Tracking an il2cpp aligned realloc: {}, {}, {}", fmt::ptr(memory), newSize, alignment); - void* newMemory = realloc(memory, newSize); - - // Fast path: realloc returned aligned memory - if ((reinterpret_cast(newMemory) & (alignment - 1)) == 0) - return newMemory; - - // Slow path: realloc returned non-aligned memory - void* alignedMemory = ct_aligned_malloc(newSize, alignment); - memcpy(alignedMemory, newMemory, newSize); - free(newMemory); - return alignedMemory; -} - -char const* namespaze(Il2CppClass* const klass) { - return klass->namespaze ? klass->namespaze : ""; -} -char const* namek(Il2CppClass* const klass) { - return klass->name ? klass->name : ""; -} -std::string generics(Il2CppClass* const klass) { - auto* genClass = klass->generic_class; - if (genClass) { - auto* genInst = genClass->context.class_inst; - if (genInst) { - std::string outp; - for (size_t i = 0; i < genInst->type_argc; i++) { - outp += std::string(" ") + i2c::type_simple_name(genInst->type_argv[i]); - } - return outp; - } - } - return "NO GENERICS"; -} - -#define WORDSIZE ((int)sizeof(size_t) * 8) -#define GET_CLASS(obj) ((Il2CppClass*)(((size_t)(obj)->klass) & ~(size_t)1)) -#define IS_MARKED(obj) (((size_t)(obj)->klass) & (size_t)1) - -std::size_t generic_obj_traverse_count = 0; -std::size_t obj_traverse_count = 0; - -MAKE_HOOK(LivenessState_TraverseGenericObject, (nullptr), void, Il2CppObject* obj, void* state) { - if (disableLivenessChecks()) { - // If we are disabling liveness checks, do not call the original function. - return LivenessState_TraverseGenericObject(obj, state); - } - // We are calling this with an object and a state. - // The state is the LivenessState instance - // There is a process_array that we want to look at - // Likewise we also want to look at our traverse_depth - // Not to mention the actual object and state ptrs - - // auto klass = GET_CLASS(obj); - // custom_types::logger.debug("{}: - // LivenessState::TraverseGenericObject({}, {}), with klass: {} ({}::{})", - // generic_obj_traverse_count++, fmt::ptr(obj), fmt::ptr(state), fmt::ptr(obj->klass), namespaze(klass), - // namek(klass)); process_array is at 0x18 (custom_growable_array*) - // traverse_depth is at 0x48 (int) - // auto arrPtr = - // *reinterpret_cast**>(reinterpret_cast(state) - // + 0x18); custom_types::logger.debug("traverse_depth: {}", - // *reinterpret_cast(reinterpret_cast(state) + 0x48)); - // custom_types::logger.debug("arrPtr: {}, inner: {}, size: {}", fmt::ptr(arrPtr), - // fmt::ptr(arrPtr->data()), arrPtr->size()); custom_types::logger.flush(); for - // (size_t i = 0; i < arrPtr->size(); i++) { auto inst = arrPtr->data()[i]; - // custom_types::logger.debug("arr val: {} ptr: {}, class: {} - // ({}::{})", i, fmt::ptr(inst), fmt::ptr(inst->klass), namespaze(GET_CLASS(inst)), - // namek(GET_CLASS(inst))); - // } - // if (arrPtr->size() > 0) { - // auto inst = arrPtr->data()[arrPtr->size() - 1]; - // custom_types::logger.debug("arr val: {}, ptr: {}, class: {} - // ({}::{})", arrPtr->size() - 1, fmt::ptr(inst), fmt::ptr(inst->klass), - // namespaze(GET_CLASS(inst)), namek(GET_CLASS(inst))); - // custom_types::logger.flush(); - // } - generic_obj_traverse_count++; - LivenessState_TraverseGenericObject(obj, state); - // custom_types::logger.debug("Complete - // LivenessState::TraverseGenericObject"); -} - -MAKE_HOOK(LivenessState_TraverseObjectInternal, (nullptr), bool, Il2CppObject* obj, bool isStruct, Il2CppClass* klass, void* state) { - if (disableLivenessChecks()) { - // If we are disabling liveness checks, do not call the original function. - return LivenessState_TraverseObjectInternal(obj, isStruct, klass, state); - } - // Here we are going to log... AGAIN - // but this time only a few things - // custom_types::logger.debug("LivenessState::TraverseObjectInternal({}, - // {}, {}, {})", fmt::ptr(obj), isStruct, fmt::ptr(klass), fmt::ptr(state)); - // custom_types::logger.flush(); - // custom_types::logger.debug("class: ({}::{})", namespaze(klass), - // namek(klass)); custom_types::logger.flush(); - obj_traverse_count++; - auto ret = LivenessState_TraverseObjectInternal(obj, isStruct, klass, state); - // custom_types::logger.debug("Complete - // LivenessState::TraverseObjectInternal"); - return ret; -} - -// MAKE_HOOK(Liveness_FromStatics, nullptr, void, void* state) { -// // filter class is 0x10 -// auto filter = -// *reinterpret_cast(reinterpret_cast(state) + 0x10); -// custom_types::logger.debug("Liveness::FromStatics({})", fmt::ptr(state)); -// custom_types::logger.debug("filter class: {}", fmt::ptr(filter)); -// custom_types::logger.flush(); -// custom_types::logger.debug("filter class: {}::{}", namespaze(filter), -// namek(filter)); custom_types::logger.flush(); -// // TODO: Log class statics info -// Liveness_FromStatics(state); -// custom_types::logger.debug("Complete Liveness::FromStatics"); -// } - -static inline bool HasParentUnsafe(Il2CppClass const* klass, Il2CppClass const* parent) { - return klass->typeHierarchyDepth >= parent->typeHierarchyDepth && klass->typeHierarchy[parent->typeHierarchyDepth - 1] == parent; -} - -MAKE_HOOK(LivenessState_TraverseGCDescriptor, (nullptr), void, Il2CppObject* obj, void* state) { - if (disableLivenessChecks()) { - return LivenessState_TraverseGCDescriptor(obj, state); - } - - // Note: gc_desc is a bitfield that holds which fields of the type has - // references, or, if it is too large, a pointer to a table which holds a - // larger bitfield. - int i = 0; - size_t mask = (size_t) (GET_CLASS(obj)->gc_desc); - - IL2CPP_ASSERT(mask & (size_t) 1); - - for (i = 0; i < WORDSIZE - 2; i++) { - size_t offset = ((size_t) 1 << (WORDSIZE - 1 - i)); - if (mask & offset) { - Il2CppObject** valptr = (Il2CppObject**) (((char*) obj) + i * sizeof(void*)); - Il2CppObject* val = *valptr; - if (!val || IS_MARKED(val)) { - // Null instances are permitted - continue; - } - // We aren't marked at this point, val is a new pointer in our instance - // PreviewDifficultyBeatmapSet - // mask: 0b0011000...1 - // f1: BeatmapCharacteristicSO (skipped explicitly in if above) - // f2: BeatmapDifficulty[] (crashes below) - // val is: pointer to BeatmapDifficulty[] (at least, it SHOULD be) - // specifically, val is a pointer to: 39D0 before obj - // Offset of filter class - auto filterClass = *reinterpret_cast(reinterpret_cast(state) + 0x10); - if (!val->klass || (GET_CLASS(val)->has_references == 0 && GET_CLASS(val)->klass != GET_CLASS(val) && GET_CLASS(val)->name == nullptr) || - // If our filter class is not null, and - // our filter class' type hierarchy depth is <= ours and - // our type hierarchy pointer is garbage - (filterClass && filterClass->typeHierarchyDepth <= GET_CLASS(val)->typeHierarchyDepth && - (reinterpret_cast(GET_CLASS(val)->typeHierarchy) <= 0x1000 || - (reinterpret_cast(GET_CLASS(val)->typeHierarchy) & 0x00FFFFFFFFFFFFFFULL) > 0xe000000000))) { - // We have a VERY BIG PROBLEM! - // This will cause a (hard to diagnose) crash! - // So, we will dump as much info as we can. - custom_types::logger.critical("WARNING! THIS WILL CRASH, DUMPING SEMANTIC INFORMATION..."); - custom_types::logger.critical( - "LivenessState::TraverseGCDescriptor({}, {}) class: {}, gc_desc: " - "{}, {}::{} {}", - fmt::ptr(obj), - fmt::ptr(state), - fmt::ptr(GET_CLASS(obj)), - fmt::ptr(GET_CLASS(obj)->gc_desc), - namespaze(GET_CLASS(obj)), - namek(GET_CLASS(obj)), - generics(GET_CLASS(obj)).c_str() - ); - // malloc_info() - // TODO: Yeah - std::filesystem::path path = "/sdcard/ModData/com.beatgames.beatsaber/Mods/CustomTypes/CustomTypesMallocInfoOnExit.xml"; - if (!std::filesystem::exists(path.parent_path())) - std::filesystem::create_directories(path.parent_path()); - auto f = fopen(path.c_str(), "w"); - if (!malloc_info(0, f)) { - custom_types::logger.critical("Failed to write to: {}!", path.c_str()); - } else { - custom_types::logger.debug("Wrote malloc info to: {}", path.c_str()); - } - fclose(f); - custom_types::logger.critical( - "LivenessState::TraverseGCDescriptor({}, {}), with val: {} (klass: " - "{}), idx: {}", - fmt::ptr(obj), - fmt::ptr(state), - fmt::ptr(val), - fmt::ptr(val->klass), - i - ); - if (GET_CLASS(val)) { - custom_types::logger.critical("has_references: {}", (bool) GET_CLASS(val)->has_references); - } - // custom_types::logger.critical("Logging filterClass: {}", - // fmt::ptr(filterClass)); custom_types::logAll(filterClass); - // custom_types::logger.critical("Logging all registered custom - // types..."); for (auto k : custom_types::Register::classes) { - // custom_types::logger.critical("KLASS PTR: {}", fmt::ptr(k)); - // custom_types::logAll(k); - // } - - custom_types::logger.debug( - "gc descriptor test field: obj: {}, " - "field: {}, value: {}, class: {}", - fmt::ptr(obj), - fmt::ptr(valptr), - fmt::ptr(val), - fmt::ptr(val ? val->klass : nullptr) - ); - custom_types::logger.critical("obj_traverse_count: {}", obj_traverse_count); - custom_types::logger.critical("generic_obj_traverse_count: {}", generic_obj_traverse_count); - - custom_types::logger.critical( - "Talk to Sc2ad to try and understand what the hell is going on " - "here and why." - ); - custom_types::logger.critical( - "Also, please be very kind and send him this whole log file! It " - "would be much appreciated." - ); - custom_types::logger.critical( - "With that said, the log in this file may have been truncated, so " - "consider grabbing the file log for custom types instead." - ); - custom_types::logger.critical( - "custom types will now try to log as much information it can about " - "the offending instance's class before crashing..." - ); - custom_types::logger.debug("Capturing memory snapshot..."); - // auto snapshot = i2c::functions::capture_memory_snapshot(); - // auto snapshot_path = string_format(LOG_PATH, - // "com.beatgames.beatsaber") + "MemoryDump.bin"; std::ofstream - // memory_snapshot(snapshot_path, std::ios::binary); - // memory_snapshot.write(reinterpret_cast(snapshot), - // sizeof(*snapshot)); memory_snapshot.close(); - // i2c::functions::free_captured_memory_snapshot(snapshot); - // custom_types::logger.debug("Logging memory dump to {}", - // snapshot_path.c_str()); - custom_types::logger.critical("KLASS PTR: {}", fmt::ptr(obj->klass)); - if (GET_CLASS(obj)) { - custom_types::logAll(GET_CLASS(obj)); - } - custom_types::logger.critical("KLASS PTR: {}", fmt::ptr(val->klass)); - custom_types::logger.critical("Attempting HasParentUnsafe({}, {})...", fmt::ptr(GET_CLASS(val)), fmt::ptr(filterClass)); - auto ret = HasParentUnsafe(GET_CLASS(val), filterClass); - custom_types::logger.critical("HasParentUnsafe return: {}", ret); - if (GET_CLASS(val)) { - custom_types::logAll(GET_CLASS(val)); - } - // Things I have learned, just dumping here: - // static fields and classes that have a nonzero quantity of static - // fields need to be added to: Class::GetStaticFieldData() as for - // Liveness::FromRoot, it MIGHT NOT be the base, because the recursion - // layer is not bt-able due to b's instead of bl's So, what COULD happen - // is that the root liveness calc DOES NOT have a gc descriptor, and it - // is only a type later on that does. Perhaps we should hook the - // TraverseGenericObject function instead and see what we can learn as - // we walk the root? Seems to be the first field of a dictionary that - // has this issue Also the 0x60 field of a Regex::Match type, which is - // an array: [FieldOffset(Offset = "0x60")] [Token(Token = - // "0x040002A5")] internal int[] _matchcount; - - // Have we damaged an array creation method on accident? - } - } - } - // Call orig - LivenessState_TraverseGCDescriptor(obj, state); -#undef WORDSIZE -#undef GET_CLASS -#undef IS_MARKED -} - -std::optional readsafeb(uint32_t const* const addr) { - cs_insn* insns; - // Read from addr, 1 instruction, with pc at addr, into insns. - // TODO: consider using cs_disasm_iter - auto count = cs_disasm(cs::get_handle(), reinterpret_cast(addr), sizeof(uint32_t), reinterpret_cast(addr), 1, &insns); - RET_DEF_UNLESS(custom_types::logger, count == 1); - auto inst = insns[0]; - // Thunks have a single b - RET_DEF_UNLESS(custom_types::logger, inst.id == ARM64_INS_B); - auto platinsn = inst.detail->arm64; - RET_DEF_UNLESS(custom_types::logger, platinsn.op_count == 1); - auto op = platinsn.operands[0]; - RET_DEF_UNLESS(custom_types::logger, op.type == ARM64_OP_IMM); - // Our b dest is addr + (imm << 2), except capstone does this for us. - auto dst = reinterpret_cast(op.imm); - cs_free(insns, 1); - return dst; -} -#endif +} // end anonymous namespace // NOTE THAT THIS HOOK DOES NOT PERMIT TYPES OF IDENTICAL NAMESPACE AND NAME BUT // IN DIFFERENT IMAGES! This could be worked around if we also have image be a @@ -644,58 +312,7 @@ namespace custom_types { // } #ifdef CT_USE_GCDESCRIPTOR_DEBUG - // Install memory callbacks - Il2CppMemoryCallbacks callbacks{ - .malloc_func = ct_malloc, - .aligned_malloc_func = ct_aligned_malloc, - .free_func = ct_free, - .aligned_free_func = ct_aligned_free, - .calloc_func = ct_calloc, - .realloc_func = ct_realloc, - .aligned_realloc_func = ct_aligned_realloc, - }; - // i2c::functions::set_memory_callbacks(&callbacks); -#define BREAK(var, ...) \ - do { \ - if (!var) { \ - logger.warn(__VA_ARGS__); \ - goto exit; \ - } \ - } while (0) - - { - // We need to xref trace to get to LivenessState stuff - // il2cpp_unity_liveness_calculation_from_root - // only a b --> Liveness::FromRoot - // 2nd bl --> LivenessState::TraverseObjects - // 1st bl --> LivenessState::TraverseGenericObject - // 2nd b --> LivenessState::TraverseGCDescriptor - - // If we fail anywhere in this chain, simply log it and move on. - // We shouldn't care, this is just a debug hook after all. - auto opt = readsafeb((uint32_t*) i2c::functions::unity_liveness_calculation_from_root); - BREAK(opt, "Failed to find b in il2cpp_unity_liveness_calculation_from_root!"); - opt = cs::findNthBlSafe<2>(*opt); - BREAK(opt, "Failed to find 2nd bl in Liveness::FromRoot!"); - opt = cs::findNthBlSafe<1>(*opt); - BREAK(opt, "Failed to find 1st bl in Liveness::TraverseObject!"); - auto traverseGeneric = *opt; - auto traverseInternal = cs::findNthBSafe<3>(*opt); - opt = cs::findNthBSafe<2>(*opt); - BREAK(opt, "Failed to find 2nd b in LivenessState::TraverseGenericObject!"); - BREAK(traverseInternal, "Failed to find 3rd b in LivenessState::TraverseGenericObject!"); - // We found all of the chain, lets install our debug hook! - INSTALL_HOOK(logger, LivenessState_TraverseGenericObject, traverseGeneric); - INSTALL_HOOK(logger, LivenessState_TraverseGCDescriptor, *opt); - INSTALL_HOOK(logger, LivenessState_TraverseObjectInternal, *traverseInternal); - // opt = - // readsafeb((uint32_t*)i2c::functions::unity_liveness_calculation_from_statics); - // BREAK(opt, "Failed to find b in - // il2cpp_unity_liveness_calculation_from_statics!"); - // INSTALL_HOOK(logger, Liveness_FromStatics, *opt); - } -#undef BREAK - exit: + liveness::EnsureHooks(); #endif installed = true; }