digitalmars.D.bugs - [Issue 23253] New: asserting in a destructor causes a deadlock
- d-bugmail puremagic.com (77/77) Jul 15 2022 https://issues.dlang.org/show_bug.cgi?id=23253
https://issues.dlang.org/show_bug.cgi?id=23253 Issue ID: 23253 Summary: asserting in a destructor causes a deadlock Product: D Version: D2 Hardware: All OS: All Status: NEW Severity: major Priority: P1 Component: druntime Assignee: nobody puremagic.com Reporter: schveiguy gmail.com Creating a class that has a destructor with an assert will hang the process if the GC destroys the object. Reproducible Example: ```d class S { int x = 0; ~this() { assert(x != 0); } } void foo() { auto s = new S; s = null; } void killstack() { int[2048] smashit; } void main() { import core.memory; foo(); killstack(); GC.collect(); } ``` Why does this happen? Because inside the GC there are a few places with this pattern: ```d _inFinalizer = true; scope (failure) _inFinalizer = false; doSomething(); // calls some code that asserts, e.g. the destructor _inFinalizer = false; ``` Here is what happens on this seemingly impossible code: 1. The inFinalizer boolean is set to true. 2. The destructor is called 3. The assert triggers. This throws a *statically backed* AssertError. This error is not allocated with the GC, but rather static memory. 4. When throwing, the runtime attempts to allocate a TraceInfo object for the throw. 5. The default trace info handler sees that `inFinalizer` is true, and returns null to signify no trace information 6. The scope(failure) statement is triggered. This *catches the Error*, and then sets `inFinalizer` to false. 7. The scope(failure) statement then *re-throws the error*, still holding the GC lock. 8. The trace info handler this time sees the inFinalizer is false, and now tries to allocate the trace info -- triggering a deadlock as it tries to take the lock. The solution here is to either use a different boolean to trigger whether traceinfo should be allocated, and make sure that is set/cleared *outside* the GC lock, or to find a way to suppress the trace info from attempting an allocation in this instance. Alternatively, finding a way to allocate the trace info without using the GC would be an additional solution, as this is the only allocation that might happen between the flag being cleared and the gc being unlocked. Even if a workaround can be implemented, it's still probably best to just use a separate flag. This bug was discovered due to an assert in an invariant, which the compiler can call at any time for objects in scope, possibly even when there is no explicit call to the object, and trigger an assert. --
Jul 15 2022