From 489c9dbcbdc8ec31e2010c271211357e11e6f02d Mon Sep 17 00:00:00 2001 From: Trey Date: Fri, 18 Sep 2026 14:19:41 -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 - via HasParentUnsafe, which reads klass->typeHierarchyDepth and indexes klass->typeHierarchy, and via logAll. Both sites only null-check, so when the class pointer is genuinely bad the report becomes a SIGSEGV on the AssetGarbageCollector thread and the log ends at "Attempting HasParentUnsafe(...)" with nothing after it. 0.20.1 fixed where the filter class is read from, which removed a source of false positives, but the dereference of the suspect class is unguarded either way. Observed on 0.18.4 / Beat Saber 1.40.8, where it accounted for 18 of 40 crashes in one user's tombstones, faulting at 0xfffffffffffffff8 - the address produced by 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, and log which one looked wrong when skipping. Everything already safe to dump is unchanged. --- src/liveness.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/liveness.cpp b/src/liveness.cpp index d99d9fa..6ded927 100644 --- a/src/liveness.cpp +++ b/src/liveness.cpp @@ -179,6 +179,14 @@ namespace { // 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 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(Il2CppClass const* klass) { + return klass != nullptr && klass->klass == klass && klass->name != nullptr; + } + static inline bool HasParentUnsafe(Il2CppClass const* klass, Il2CppClass const* parent) { return klass->typeHierarchyDepth >= parent->typeHierarchyDepth && klass->typeHierarchy[parent->typeHierarchyDepth - 1] == parent; } @@ -311,14 +319,19 @@ namespace { 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)) { + 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