Comment #0 by stanislav.blinov — 2018-10-19T19:52:47Z
Per @safe-ty rules, @safe functions shall not call @system functions. Unfortunately, the GC may run finalizers during collection, even when that collection is triggered from within @safe function:
import std.stdio;
class C {
~this() // @system!
{
printf("Called @system function\n");
}
}
void safeFunc() @safe {
auto a = new int[10^^6]; // 'new' may require collection
// do 'safe' things with 'a'
}
void main() {
new C; // the object is no longer referenced, will be collected, i.e. assume that this happened elsewhere in the program via last reference going out of scope
printf("Entering @safe function\n");
safeFunc(); // if this triggers collection, it effectively calls @system C.__dtor inside @safe safeFunc
printf("Exited @safe function\n");
}
Output with default GC options:
Entering @safe function
Called @system function
Exited @safe function
Thus, arbitrary non-@safe code may 'escape' into @safe context. This of course applies to struct destructors as well.
This issue is made worse by the fact that the behavior is non-deterministic: collection may or may not trigger depending on the GC state, destructor may or may not be called depending on program state.
Comment #1 by simen.kjaras — 2018-10-21T15:04:45Z
Can you demonstrate how this actually causes problems? No data is being passed from @safe to @system, so nothing actually un-@safe will happen due to this.
Comment #2 by stanislav.blinov — 2018-10-21T18:27:56Z
It is being passed: C may define static data accessible to 'safeFunc'.
The point is, this includes any arbitrary un-@safe code, e.g. catching an Error or even a Throwable. Net effect is that this code:
static C c;
void safeFunc() @safe {
destroy(c);
}
would fail to compile, and yet that same code executes via GC.
There's another unfortunate consequence of this behavior: 'pure' is also affected, as is nothrow (though the latter terminates the program anyway as it results in a FinalizationError).
Comment #3 by schveiguy — 2021-11-13T21:46:58Z
The GC's finalizer execution should not be treated as being called via the allocation. It can be thought of as the GC hijacking the thread's call stack to run the finalizers independent of the safe function (or pure function, or whatever else is restricted). You can imagine the finalizers running in a separate thread completely (which actually could be implemented), and then the "problem" in this bug is eliminated, but doesn't change semantics at all.
In fact, at that point, any pure or safe function could call an impure or unsafe finalizer because the OS has interrupted that thread with a signal.
This bug is invalid.