Repository navigation
Conversation
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.
|
This is separate from PR #26. That fixed where the filter class is read from, which removed false positives. This is about the dereference of
if (filterClass) { ... HasParentUnsafe(GET_CLASS(val), filterClass); }
if (GET_CLASS(val)) { custom_types::logAll(GET_CLASS(val)); }They're null checks, and the pointer isn't null — it's garbage. That's the case the diagnostic exists to report, so it fires correctly and then dies partway through the dump. From the logs of the user I was helping: The log ends there, and the tombstone shows a fault at So a correct filter offset doesn't prevent this. #26 and this one are both needed. |
TraverseGCDescriptor's diagnostic fires when a traversed value looks corrupt, and then dereferences that value's class pointer — first viaHasParentUnsafe, which readsklass->typeHierarchyDepthand indexesklass->typeHierarchy, then vialogAll. Both sites only null-check the pointer.When the class pointer really is bad — which is the case the diagnostic exists to report — that turns the report into a SIGSEGV on the
AssetGarbageCollectorthread. The log ends atAttempting HasParentUnsafe(...)with nothing after it, so the warning that says "THIS WILL CRASH" is itself the crash, and the information it was about to dump is lost.Evidence
Seen on Beat Saber 1.40.8 with custom-types 0.18.4, while helping a user debug repeated crashes after finishing or quitting a song. 18 of 40 Beat Saber crashes in their tombstones were this signature — the single most common cause, ahead of an unrelated beatsaber-hook issue at 16.
Every one faulted at
0x00fffffffffffff8. That is exactly the address produced bywhen
typeHierarchyisNULLand the depth byte reads0, i.e.*(NULL - 8).Worth stressing: the game's own code tolerates these same values. Disassembling the corresponding game path shows it reads a flag byte and skips. So this diagnostic converts a condition the game handles safely into a hard crash.
Why this applies to
master0.20.1 changed where the filter class is read from (
state->filterrather than a hardcoded offset), which removed a source of false positives. But the dereference of the suspect class is unguarded either way, so the fatal path is still present onmaster:The change
Add
LooksLikeValidClass()— a realIl2CppClasspoints itsklassfield back at itself and has a name — and check both class pointers before dereferencing them, logging which one looked wrong when skipping.Everything that was already safe to dump is unchanged: the warning, the offending instance, its class and
gc_desc, the field index, the value pointer, the traverse counters, and thelogAllofobj. The diagnostic still reports what it was written to report; it just no longer dies partway through.Before (stock 0.18.4 — log ends here, process gone):
After (same input, same repro):
Testing
The equivalent change on
version/v0_18_4(same code, inregister.cppbefore it moved toliveness.cpp) was built and tested on a Quest 2 running Beat Saber 1.40.8, using a test mod that reproduces the condition in memory — it builds aPlayerLevelStatsDatawhose_levelIDpoints at an invalid object and then runsResources.UnloadUnusedAssets():I can share that repro mod, and a PC-only version that replays the hook against the real
Il2CppClasslayout underqemu-aarch64and reproduces the same fault address with no headset needed, if either would be useful.One caveat: I could not build
masterlocally (it needs NDK 29 and beatsaber-hook 8, which I don't have set up). The change is the same shape as the 0.18.4 one that does build and pass, but CI is the real check here.Not addressed
PlayerLevelStatsData._levelIDwhile the record stays live; that cause is still unknown. This only stops the reporting path being fatal.The same fix against
version/v0_18_4is in PR #28. That's the last beatsaber-hook 6.x line and the only one that runs on Beat Saber 1.40.8, which is where this is currently hitting users.