Hi!
We’ve encountered a problem while migrating our project from 2020.3.40f1
to 2022.2.19f1
, it seems there’s a problem with AnimationCurve
being attempted to be destroyed without it ever getting constructed in the first place, which evidently results in a crash. Also worth mentioning is that the problem manifests itself on Android
platform only and no matter what configuration or settings those are assembled with, OS versions don’t affect the behavior as well.
The crash happens when we load a scene, exit that scene and then run garbage collection.
Something along these lines: main menu
→ level
→ main menu
→ run gc
.
The first thing we concentrated on is the removal of all of our explicit AnimationCurve
uses case from the codebase just to be able to isolate the possibility of us doing something wonky with it, but it didn’t prove to be the case here.
Next, we decided to try other Unity 2022
releases just to exclude the possibility of a regression that could’ve been introduced at some point. We’ve even given a go to the 2023.1 beta
release. No dice.
We have also tried disabling incremental GC, building in development mode, switching IL2CPP
flags to produce a smaller build instead of a more optimized one, and using GLES
renderer instead of a Vulkan
-based one. Still no positive outcome.
The last resort for us was to export the Android project locally and dig into IL2CPP-generated code to see what was going under the hood. We added a lot of logging around the AnimationCurve
functions to see if anything gets corrupted for some reason.
Our log output would look like this for proper AnimationCurve
construction and destruction flows (
i.e. AnimationCurve_Finalize
invocations that clearly have a matching construction call AnimationCurve__ctor
before it):
AnimationCurve_Finalize_m803AC16166EE497C4DFA996B15692D91F4D04C3C: 0=482526307184 1=482526307184 2=482526307184 this=483584882176
memzero AnimationCurve_Finalize_m803AC16166EE497C4DFA996B15692D91F4D04C3C: 0=0 1=0 2=0 this=483584882176```
In case there's a mismatch, i.e. the finalization is performed on an `AnimationCurve`, which hasn't been constructed yet, we'd see this (preceded with a `=====>` just to make it easier to spot and filter out):
```=====> AnimationCurve_Finalize_m803AC16166EE497C4DFA996B15692D91F4D04C3C: 0=12970367422304525504 1=0 2=0 this=484505394416```
Every log message provides the following information:
- 3 copies of the pointer stored within `AnimationCurve_tCBFFAAD05CEBB35EF8D8631BD99914BE1A6BB354` the (`0=…, 1=…, 2=…`) parts.
- pointer to the `AnimationCurve_tCBFFAAD05CEBB35EF8D8631BD99914BE1A6BB354` instance itself (the `this=` part)
`AnimationCurve_tCBFFAAD05CEBB35EF8D8631BD99914BE1A6BB354` would look like this now:
```struct AnimationCurve_tCBFFAAD05CEBB35EF8D8631BD99914BE1A6BB354 : public RuntimeObject
{
intptr_t ___m_Ptr;
intptr_t ___m_Ptr2;
intptr_t ___m_Ptr3;
};```
Copies were introduced to see if there's some sort of memory corruption taking place, which somehow either reuses some older instances without ever re-constructing them properly or it's simply a case of double-freeing. They are assigned along the "primary" `__m_Ptr` at the same places in the code (like `AnimationCurve__ctor_mEABC98C03805713354D61E50D9340766BD5B717E`, etc).
Also, we extended the `AnimationCurve_Finalize` function to include a logic, which would check whether all of the pointer copies are identical and if there are not, then it would log the case and skip destruction of the `AnimationCurve` instance altogether:
```csharp
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void AnimationCurve_Finalize_m803AC16166EE497C4DFA996B15692D91F4D04C3C (AnimationCurve_tCBFFAAD05CEBB35EF8D8631BD99914BE1A6BB354* __this, const RuntimeMethod* method)
{
{
}
{
auto __finallyBlock = il2cpp::utils::Finally([&]
{
FINALLY_0010:
{
Object_Finalize_mC98C96301CCABFE00F1A7EF8E15DF507CACD42B2(__this, NULL);
return;
}
});
try
{
intptr_t L_0 = __this->___m_Ptr;
if (L_0 == __this->___m_Ptr2 && L_0 == __this->___m_Ptr3)
{
AnimationCurve_Internal_Destroy_m240B298D0A13EEC1652955C4BDCDBE9B7B2EE296(L_0, NULL);
// zero-out the struct to be sure there are no leftovers in case something reuses the object.
memset(__this, 0, sizeof(AnimationCurve_tCBFFAAD05CEBB35EF8D8631BD99914BE1A6BB354));
// log normally
}
else
{
// log the offending case
}
goto IL_0018;
}
catch(Il2CppExceptionWrapper& e)
{
__finallyBlock.StoreException(e.ex);
}
}
IL_0018:
{
return;
}
}
Modifying the code this way solved our crashes, but of course, there’s no guarantee there won’t be any memory leaks associated with it or something even worse (latent segfaults that are waiting to happen, etc).
Currently, we’re sticking to the hack mentioned above, but hoping for a better solution, which wouldn’t force us to inject anything into C++ code right before compiling & linking it.
Segfault’s call stack, which might be useful:
Thread 0 Crashed:
0 libunity.so 0x79b143c650 MemoryManager::VirtualAllocator::GetBlockInfoFromPointer
1 libunity.so 0x79b14398b8 DualThreadAllocator<T>::Contains
2 libunity.so 0x79b14396a8 DualThreadAllocator<T>::TryDeallocate
3 libunity.so 0x79b143bc3c MemoryManager::smile:eallocate
4 libil2cpp.so 0x799dfe01f8 [inlined] AnimationCurve_Internal_Destroy_m240B298D0A13EEC1652955C4BDCDBE9B7B2EE296 (UnityEngine.CoreModule.cpp:9709)
5 libil2cpp.so 0x799dfe01f8 AnimationCurve_Finalize_m803AC16166EE497C4DFA996B15692D91F4D04C3C (UnityEngine.CoreModule.cpp:9749)
6 libil2cpp.so 0x799ba95f04 il2cpp::vm::Runtime::InvokeWithThrow (Runtime.cpp:604)
7 libil2cpp.so 0x799ba95e50 il2cpp::vm::Runtime::Invoke (Runtime.cpp:590)
8 libil2cpp.so 0x799ba8d7c8 il2cpp::gc::GarbageCollector::RunFinalizer (GarbageCollector.cpp:178)
9 libil2cpp.so 0x799babee10 GC_invoke_finalizers (finalize.c:1314)
10 libil2cpp.so 0x799ba8d710 il2cpp::gc::FinalizerThread (GarbageCollector.cpp:102)
11 libil2cpp.so 0x799ba935b0 il2cpp::os::Thread::RunWrapper (Thread.cpp:200)
12 libil2cpp.so 0x799bab4958 il2cpp::os::ThreadImpl::ThreadStartWrapper (ThreadImpl.cpp:123)
13 libc.so 0x7cf0e52268 __pthread_start
14 libc.so 0x7cf0de4a2c __start_thread
A bug has been reported: case number IN-41806