From 307f1d7550bece38b419ee314692fc4eab81cd50 Mon Sep 17 00:00:00 2001 From: Trey Date: Fri, 18 Sep 2026 14:18:15 -0500 Subject: [PATCH] Don't crash while reporting a bad liveness value TraverseGCDescriptor's diagnostic fires when a traversed value looks corrupt, and then dereferences that value's class pointer - first via HasParentUnsafe, then via logAll. When the class pointer really is bad, that turns the report into a SIGSEGV on the AssetGarbageCollector thread, so the crash it warns about is the crash it causes. The log ends at "Attempting HasParentUnsafe(...)" with nothing after it. Seen on Beat Saber 1.40.8 (custom-types 0.18.4): 18 of 40 crashes in one user's tombstones were this, faulting at 0xfffffffffffffff8 - the address you get from klass->typeHierarchy[parent->typeHierarchyDepth - 1] when typeHierarchy is NULL and the depth byte reads 0. The game's own code tolerates the same values; only this diagnostic dies on them. Check both class pointers before dereferencing them, and log which one looked wrong when we skip. Everything that was already safe to dump - the warning, the offending instance, its class and gc_desc, the field index, the value, the traverse counters, the class dump of obj - is unchanged, so the diagnostic still reports what it was written to report. Note the filter class is read from state+0x10; on this build the LivenessState constructor stores the filter at +0x08 (+0x10 is process_array), so filterClass is usually not a class at all. This change deliberately does not touch that offset - it just stops a wrong value being fatal. 0.20.1 resolves the offset properly in liveness.cpp. --- src/register.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/register.cpp b/src/register.cpp index 5e5925e..b5fc7d5 100644 --- a/src/register.cpp +++ b/src/register.cpp @@ -325,6 +325,14 @@ MAKE_HOOK(LivenessState_TraverseObjectInternal, nullptr, bool, Il2CppObject* obj // custom_types::logger.debug("Complete Liveness::FromStatics"); // } +// The value we are reporting on is, by definition, one whose class pointer we +// already suspect. Dereferencing it (or a filter class read out of the liveness +// state) is what turns this diagnostic into a SIGSEGV instead of a log message. +// A real Il2CppClass points its klass field back at itself and has a name. +static inline bool LooksLikeValidClass(const Il2CppClass* klass) { + return klass != nullptr && klass->klass == klass && klass->name != nullptr; +} + static inline bool HasParentUnsafe(const Il2CppClass* klass, const Il2CppClass* parent) { return klass->typeHierarchyDepth >= parent->typeHierarchyDepth && klass->typeHierarchy[parent->typeHierarchyDepth - 1] == parent; } @@ -434,12 +442,19 @@ MAKE_HOOK(LivenessState_TraverseGCDescriptor, nullptr, void, Il2CppObject* 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)) { + if (LooksLikeValidClass(GET_CLASS(val))) { + if (LooksLikeValidClass(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); + } else { + custom_types::logger.critical("Filter class {} is not a valid Il2CppClass, skipping HasParentUnsafe", fmt::ptr(filterClass)); + } custom_types::logAll(GET_CLASS(val)); + } else { + custom_types::logger.critical("Class {} of the offending instance is not a valid Il2CppClass, skipping the parent check and class dump", fmt::ptr(GET_CLASS(val))); } + custom_types::logger.critical("Continuing without crashing"); // 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