www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.bugs - [Issue 23253] New: asserting in a destructor causes a deadlock

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