Search Unity

Bug WeakReference<T>.SetTarget(T) unexpectedly & randomly causes assertion and editorcrash

Discussion in 'Scripting' started by Nefahl, Aug 15, 2022.

  1. Nefahl

    Nefahl

    Joined:
    Feb 20, 2017
    Posts:
    71
    Context: UnityVersion: 2021.1.28f1 Api Compatibility Level .Net 4.x

    The short version:
    WeakReference<T>.SetTarget(T obj);
    randomly causes the assertion:
    "* Assertion at ..\mono\metadata\boehm-gc.c:2229, condition `type < HANDLE_TYPE_MAX", which also crashes the editor.
    The long version:
    Hello, we keep encountering a strange assertion with WeakReference<T> crashing the editor(in playmode) and the application, that we can't get a hold on. After looking inside the Mono-Codebase we now know, that, for some reason, the GCHandle behind the WeakReference<T>'s target seems to be of an invalid handle-type, however we can't control any of that. Therefore we must assume, that there's a bug in the mono-framework used by unity:

    The following example scripts are stripped to the basic concept resulting in the issue, any additional code not affecting the WeakReference<T>.SetTarget call is removed, including sanity check for null etc:

    We have multiple objects (type is irrelevant in this case, for the sake of the example here is a dummy-class:
    Code (CSharp):
    1. // just a dummy any class can be runnable it's just some form of target-object.
    2. public class Runable
    3. {
    4.     //irrelevant implementation
    5. }
    Now we have a Container class, that owns an object of some type (in this case the Runable) and has a hard-reference on that.
    The Container also has a hard reference on a runner object, which shall 'play' the runable on demand:
    Code (CSharp):
    1. public class ContainerMonoBehaviourType : MonoBehaviour
    2. {
    3.     private Runable _runable;
    4.  
    5.     // referenced in inspector, never changed, never is null
    6.     [SerializedField]
    7.     private Runner _runner;
    8.  
    9.     private void Start()
    10.     {
    11.         _runable = new Runable();
    12.     }
    13.  
    14.     // only called after start has been called
    15.     public void Play()
    16.     {
    17.         _runner.Run(_runable);
    18.     }
    19.  
    20.     // only called after start has been called
    21.     public void Stop()
    22.     {
    23.         _runner.Stop(_runable);
    24.     }
    25. }
    At last the runner has a WeakReference<object> on it's target (normally it can run more than one, just to make the example smaller, also the type of the target is irrelevant, hence the object-wrapping to allow any target-class.)
    While the WeakReference has a valid target it will be played in update, otherwise not. But now and then SetTarget function of WeakReference<> will lead to an assertion in the c++ mono-framework code:
    Code (CSharp):
    1. public class Runner : MonoBehaviour
    2. {
    3.     WeakReference<object> _runable;
    4.  
    5.     public void Awake()
    6.     {
    7.         _runable = new WeakReference<object>(null);
    8.     }
    9.  
    10.     // is only called when not running and never null sanity-checks removed from example:
    11.     public void Run(object runable)
    12.     {
    13.         // extensive loging here prints the same for working case and
    14.         // for the fail-case the runable is the same object, of the same type and not null.
    15.         // "Assemblyname.Namespaces.Runable"
    16.         // "is null: false"
    17.         _runable.SetTarget(runable);
    18.         // chance to crash the editor/application with:
    19.         // -> * Assertion at ..\mono\metadata\boehm-gc.c:2229, condition `type < HANDLE_TYPE_MAX' not met
    20.     }
    21.  
    22.     // is only called when running sanity-checks removed from example:
    23.     public void Stop(object runable)
    24.     {
    25.         if(runable == _runable.Target)
    26.         {
    27.             _runable.SetTarget(null);
    28.         }
    29.     }
    30.  
    31.     private void Update()
    32.     {
    33.         // do sth on _runable if Target != null
    34.     }
    35. }
    The code where the mentioned assertion
    'Assertion at ..\mono\metadata\boehm-gc.c:2229, condition `type < HANDLE_TYPE_MAX' not met' is triggered, can be found here (note the line-diff, might not be the exact same version, line determined by assertion-message and function name)
    https://github.com/mono/mono/blob/main/mono/metadata/boehm-gc.c on line 1552

    The handle type values, their limits and the macros used to extract the Handle-Type index from the GCHandle found here:
    https://github.com/mono/mono/blob/main/mono/sgen/gc-internal-agnostic.h on line 49+

    Since we can't just set a breakpoint and look into what goes wrong and since it's not deterministic(from our POV) when the error occurs we don't know how to continue with this. We Logged the Target excessively right before trying to set the target. But the logs are always identically in both cases when there's no error as well as when an error with unity crash occurs and the printed log always seems like the expected target before trying to set it as WeakReferenceTarget.

    Does someone here know of a bug causing this behaviour with Mono's WeakReference version and how to avoid that?!
     
    Last edited: Aug 16, 2022
  2. The-Wild-Fox

    The-Wild-Fox

    Joined:
    Jul 29, 2017
    Posts:
    7
    We are also encountering this issue when dealing with Singletons. I have been testing commenting parts of our code for 5 days straight, and finally found one line which causes the issue. We have a normal C# class and calling a method in it, in the dispose method of out Singleton causes the same assertion error. It does not matter what I put inside the method, the Garbage Collection trips up about 50% when closing the Editor. We had an unsubcription from a static event originally, so I changed it, so we only change a boolean inside, but that also trips up. Funnily, if I put a static method inside which doesn't reference the object like Debug.Log() or leave the method empty then it is fine.

    Another issue happens when I set one reference to a C# to null in the Dispose method. This makes no sense either.

    To explain. The Dispose method is run when the Application.wantsToQuit is triggered.

    Strangely, it does not lead to an immediate crash, but crashes only when closing the Editor or rarely when recompiling scripts. This might not seem that important, but it reports crash our CI Jenkins runtime test job which fails the test.

    The crash chance seems to be around 50%, so it is painful to track down. There is no difference in anything in terms of logs, errors, warnings, etc... The only difference is that we get a crash with the line: * Assertion at ..\mono\metadata\boehm-gc.c:2229, condition `type < HANDLE_TYPE_MAX"