godot: Crash when connecting or disconnecting a signal from multiple threads

Godot version: v3.2.beta.custom_build. 318c69351

OS/device including version: Arch Linux, 64 bit, updated around mid-December.

Issue description: When connecting to an object from multiple threads, its slot_map can get corrupted, leading to missing entries or crashes. I feel something similar might be possible with the connections list

Issue was observed by Discord user Sungray while creating multiple sprite nodes with the same texture from different threads. (Which leads to those sprites connecting to the texture’s changed signal.)

Steps to reproduce: Put this script on any node:

extends Node

signal dummy_signal()

class DummyObject extends Object:
	func dummy_method():
		pass

func _ready():
	var threads = []
	for i in range(2):
		var thread = Thread.new()
		thread.start(self, "_thread", null)
		threads.push_back(thread)

func _thread(_x):
	while true:
		var object = DummyObject.new()
		connect("dummy_signal", object, "dummy_method") # dummy method
		object.call_deferred("free") # Run the free on the main thread, locks up
		#object.free() # Run the free on the non-main thread, crashes

Run, should crash or enter an infinite loop on the main thread relatively fast. When the project freezes, it should spam a errors looking like:

ERROR: _disconnect: Disconnecting nonexistent signal 'dummy_signal', slot: 1224:dummy_method.
   At: core/object.cpp:1526.

When crashing, it can generate a wide array of backtraces. Some of them are related to malloc or free, surprisingly.

No backtrace crashes (likely malloc-related)
handle_crash: Program crashed with signal 11
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
malloc(): invalid next->prev_inuse (unsorted)
realloc(): invalid next size
double free or corruption (!prev)
free(): corrupted unsorted chunks
One or more backtrace crashes
handle_crash: Program crashed with signal 4
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
handle_crash: Program crashed with signal 11
handle_crash: Program crashed with signal 11
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
[1] /usr/lib/libc.so.6(+0x3bfb0) [0x7fa7a7b21fb0] (??:0)
[1] /usr/lib/libc.so.6(+0x3bfb0) [0x7fa7a7b21fb0] (??:0)
[1] /usr/lib/libc.so.6(+0x3bfb0) [0x7fa7a7b21fb0] (??:0)
[2] Object::Signal::Target::operator<(Object::Signal::Target const&) const (/home/bobo/Projects/godot/godot/./core/object.h:449)
[2] StringName::unref() (/home/bobo/Projects/godot/godot/./core/safe_refcount.h:118)
[2] CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::get_m(int) (/home/bobo/Projects/godot/godot/./core/cowdata.h:145)
[3] StringName::~StringName() (/home/bobo/Projects/godot/godot/core/string_name.cpp:422)
[3] VMap<Object::Signal::Target, Object::Signal::Slot>::_find_exact(Object::Signal::Target const&) const (/home/bobo/Projects/godot/godot/./core/vmap.h:104)
[3] VMap<Object::Signal::Target, Object::Signal::Slot>::operator[](Object::Signal::Target const&) (/home/bobo/Projects/godot/godot/./core/vmap.h:202)
[4] Object::Connection::~Connection() (/home/bobo/Projects/godot/godot/./core/object.h:412)
[4] VMap<Object::Signal::Target, Object::Signal::Slot>::has(Object::Signal::Target const&) const (/home/bobo/Projects/godot/godot/./core/vmap.h:131)
[4] Object::connect(StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int) (/home/bobo/Projects/godot/godot/core/object.cpp:1485)
[5] Object::Signal::Slot::~Slot() (/home/bobo/Projects/godot/godot/./core/object.h:458)
[5] MethodBind5R<Object, Error, StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int>::call(Object*, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/method_bind.gen.inc:4285)
[5] Object::_disconnect(StringName const&, Object*, StringName const&, bool) (/home/bobo/Projects/godot/godot/core/object.cpp:1526)
[6] VMap<Object::Signal::Target, Object::Signal::Slot>::Pair::~Pair() (/home/bobo/Projects/godot/godot/./core/vmap.h:40)
[6] Object::~Object() (/home/bobo/Projects/godot/godot/core/object.cpp:1974)
[6] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:921)
[7] void memdelete<Object>(Object*) (/home/bobo/Projects/godot/godot/./core/os/memory.h:117)
[7] CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::resize(int) (/home/bobo/Projects/godot/godot/./core/cowdata.h:304)
[7] Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/variant_call.cpp:1112)
[8] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:893)
[8] CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::insert(int, VMap<Object::Signal::Target, Object::Signal::Slot>::Pair const&) (/home/bobo/Projects/godot/godot/./core/cowdata.h:175)
[8] GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) (/home/bobo/Projects/godot/godot/modules/gdscript/gdscript_function.cpp:1081)
[9] GDScriptInstance::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/modules/gdscript/gdscript.cpp:1173)
[9] MessageQueue::_call_function(Object*, StringName const&, Variant const*, int, bool) (/home/bobo/Projects/godot/godot/core/message_queue.cpp:250)
[9] VMap<Object::Signal::Target, Object::Signal::Slot>::insert(Object::Signal::Target const&, Object::Signal::Slot const&) (/home/bobo/Projects/godot/godot/./core/vmap.h:125)
[10] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:900)
[10] MessageQueue::flush() (/home/bobo/Projects/godot/godot/core/message_queue.cpp:299)
[10] VMap<Object::Signal::Target, Object::Signal::Slot>::operator[](Object::Signal::Target const&) (/home/bobo/Projects/godot/godot/./core/vmap.h:199)
[11] SceneTree::iteration(float) (/home/bobo/Projects/godot/godot/scene/main/scene_tree.cpp:485)
[11] _Thread::_start_func(void*) (/home/bobo/Projects/godot/godot/core/bind/core_bind.cpp:2660)
[11] Object::connect(StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int) (/home/bobo/Projects/godot/godot/core/object.cpp:1485)
[12] Main::iteration() (/home/bobo/Projects/godot/godot/main/main.cpp:1987)
[13] OS_X11::run() (/home/bobo/Projects/godot/godot/platform/x11/os_x11.cpp:3255)
[12] ThreadPosix::thread_callback(void*) (/home/bobo/Projects/godot/godot/drivers/unix/thread_posix.cpp:76)
[14] godot3.dbg(main+0x1a3) [0x178b5f3] (/home/bobo/Projects/godot/godot/platform/x11/godot_x11.cpp:56)
[13] /usr/lib/libpthread.so.0(+0x94cf) [0x7fa7a80074cf] (??:0)
[15] /usr/lib/libc.so.6(__libc_start_main+0xf3) [0x7fa7a7b0d153] (??:0)
[14] /usr/lib/libc.so.6(clone+0x43) [0x7fa7a7be52d3] (??:0)
-- END OF BACKTRACE --
handle_crash: Program crashed with signal 11
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
[2] CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::get_m(int) (/home/bobo/Projects/godot/godot/./core/cowdata.h:145)
[1] /usr/lib/libc.so.6(+0x3bfb0) [0x7f2df29e5fb0] (??:0)
Failed method: Object:free target ID: 44863
Object was deleted while awaiting a callback # Repeated many times

## This one is fishy -- probably one of the threads crashed, while the other continued working and spamming the message queue.

Without deferring free:

handle_crash: Program crashed with signal 11
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
[1] /usr/lib/libc.so.6(+0x3bfb0) [0x7f523442dfb0] (??:0)
[2] Object::Signal::Target::operator<(Object::Signal::Target const&) const (/home/bobo/Projects/godot/godot/./core/object.h:449)
[3] VMap<Object::Signal::Target, Object::Signal::Slot>::_find_exact(Object::Signal::Target const&) const (/home/bobo/Projects/godot/godot/./core/vmap.h:104)
[4] VMap<Object::Signal::Target, Object::Signal::Slot>::has(Object::Signal::Target const&) const (/home/bobo/Projects/godot/godot/./core/vmap.h:131)
[5] Object::_disconnect(StringName const&, Object*, StringName const&, bool) (/home/bobo/Projects/godot/godot/core/object.cpp:1526)
[6] Object::~Object() (/home/bobo/Projects/godot/godot/core/object.cpp:1974)
[7] void memdelete<Object>(Object*) (/home/bobo/Projects/godot/godot/./core/os/memory.h:117)
[8] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:893)
[9] Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/variant_call.cpp:1112)
[10] GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) (/home/bobo/Projects/godot/godot/modules/gdscript/gdscript_function.cpp:1081)
[11] GDScriptInstance::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/modules/gdscript/gdscript.cpp:1173)
[12] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:900)
[13] _Thread::_start_func(void*) (/home/bobo/Projects/godot/godot/core/bind/core_bind.cpp:2660)
[14] ThreadPosix::thread_callback(void*) (/home/bobo/Projects/godot/godot/drivers/unix/thread_posix.cpp:76)
[15] /usr/lib/libpthread.so.0(+0x94cf) [0x7f52349134cf] (??:0)
[16] /usr/lib/libc.so.6(clone+0x43) [0x7f52344f12d3] (??:0)
-- END OF BACKTRACE --
ERROR: _find: low > high, this may be a bug
   At: ./core/vmap.h:70.
handle_crash: Program crashed with signal 11
Dumping the backtrace. Please include this when reporting the bug on https://github.com/godotengine/godot/issues
[1] /usr/lib/libc.so.6(+0x3bfb0) [0x7f9a19eadfb0] (??:0)
[2] StringName::operator=(StringName const&) (/home/bobo/Projects/godot/godot/./core/safe_refcount.h:107)
[3] Object::Connection::operator=(Object::Connection const&) (/home/bobo/Projects/godot/godot/./core/object.h:412)
[4] Object::Signal::Slot::operator=(Object::Signal::Slot const&) (/home/bobo/Projects/godot/godot/./core/object.h:458)
[5] VMap<Object::Signal::Target, Object::Signal::Slot>::Pair::operator=(VMap<Object::Signal::Target, Object::Signal::Slot>::Pair const&) (/home/bobo/Projects/godot/godot/./core/vmap.h:40)
[6] CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::remove(int) (/home/bobo/Projects/godot/godot/./core/cowdata.h:164)
[7] VMap<Object::Signal::Target, Object::Signal::Slot>::erase(Object::Signal::Target const&) (/home/bobo/Projects/godot/godot/./core/vmap.h:140)
[8] Object::_disconnect(StringName const&, Object*, StringName const&, bool) (/home/bobo/Projects/godot/godot/core/object.cpp:1538)
[9] Object::~Object() (/home/bobo/Projects/godot/godot/core/object.cpp:1974)
[10] void memdelete<Object>(Object*) (/home/bobo/Projects/godot/godot/./core/os/memory.h:117)
[11] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:893)
[12] Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/variant_call.cpp:1112)
[13] GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) (/home/bobo/Projects/godot/godot/modules/gdscript/gdscript_function.cpp:1081)
[14] GDScriptInstance::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/modules/gdscript/gdscript.cpp:1173)
[15] Object::call(StringName const&, Variant const**, int, Variant::CallError&) (/home/bobo/Projects/godot/godot/core/object.cpp:900)
[16] _Thread::_start_func(void*) (/home/bobo/Projects/godot/godot/core/bind/core_bind.cpp:2660)
[17] ThreadPosix::thread_callback(void*) (/home/bobo/Projects/godot/godot/drivers/unix/thread_posix.cpp:76)
[18] /usr/lib/libpthread.so.0(+0x94cf) [0x7f9a1a3934cf] (??:0)
[19] /usr/lib/libc.so.6(clone+0x43) [0x7f9a19f712d3] (??:0)
-- END OF BACKTRACE --
(gdb) bt
#0  0x00000000046840c8 in atomic_conditional_increment<unsigned int> (pw=0x20051) at ./core/safe_refcount.h:107
#1  SafeRefCount::ref (this=0x20051) at ./core/safe_refcount.h:182
#2  StringName::StringName (this=0x7fffcc001158, p_name=...) at core/string_name.cpp:173
#3  0x0000000002d71d6b in Object::Connection::Connection (this=0x7fffcc001140) at ./core/object.h:412
#4  0x000000000462541d in Object::Signal::Slot::Slot (this=0x7fffcc001138) at ./core/object.h:458
#5  0x000000000462536f in VMap<Object::Signal::Target, Object::Signal::Slot>::Pair::Pair (this=0x7fffcc001128) at ./core/vmap.h:40
#6  0x0000000004625279 in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::_copy_on_write (this=0x7fffcc000f60)
    at ./core/cowdata.h:241
#7  0x00000000046261de in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::set (this=0x7fffcc000f60, p_index=1, 
    p_elem=...) at ./core/cowdata.h:139
#8  0x0000000004625bb5 in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::insert (this=0x7fffcc000f60, p_pos=1, 
    p_val=...) at ./core/cowdata.h:178
#9  0x000000000462585d in VMap<Object::Signal::Target, Object::Signal::Slot>::insert (this=0x7fffcc000f60, p_key=..., p_val=...)
    at ./core/vmap.h:125
#10 0x00000000046220e6 in VMap<Object::Signal::Target, Object::Signal::Slot>::operator[] (this=0x7fffcc000f60, p_key=...)
    at ./core/vmap.h:199
#11 0x000000000461a406 in Object::connect (this=0x6f9f170, p_signal=..., p_to_object=0x7fffcc000b90, p_to_method=..., p_binds=..., 
    p_flags=0) at core/object.cpp:1485
#12 0x0000000004630e8f in MethodBind5R<Object, Error, StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int>::call (this=0x6409750, p_object=0x6f9f170, p_args=0x7fffd7eb8ef0, p_arg_count=3, r_error=...) at core/method_bind.gen.inc:4285
#13 0x00000000046157f9 in Object::call (this=0x6f9f170, p_method=..., p_args=0x7fffd7eb8ef0, p_argcount=3, r_error=...)
    at core/object.cpp:921
#14 0x00000000046d28f5 in Variant::call_ptr (this=0x7fffd7ebaa60, p_method=..., p_args=0x7fffd7eb8ef0, p_argcount=3, r_ret=0x0, 
    r_error=...) at core/variant_call.cpp:1112
#15 0x00000000018ea067 in GDScriptFunction::call (this=0x67f0d70, p_instance=0x7260fa0, p_args=0x7fffd7ebad60, p_argcount=1, 
    r_err=..., p_state=0x0) at modules/gdscript/gdscript_function.cpp:1081
#16 0x0000000001883b04 in GDScriptInstance::call (this=0x7260fa0, p_method=..., p_args=0x7fffd7ebad60, p_argcount=1, r_error=...)
    at modules/gdscript/gdscript.cpp:1173
#17 0x000000000461563f in Object::call (this=0x6f9f170, p_method=..., p_args=0x7fffd7ebad60, p_argcount=1, r_error=...)
    at core/object.cpp:900
#18 0x000000000492bc25 in _Thread::_start_func (ud=0x70e3f00) at core/bind/core_bind.cpp:2660
#19 0x000000000242b95c in ThreadPosix::thread_callback (userdata=0x725f4f0) at drivers/unix/thread_posix.cpp:74
#20 0x00007ffff7a164cf in start_thread () from /usr/lib/libpthread.so.0
#21 0x00007ffff75f42d3 in clone () from /usr/lib/libc.so.6
(gdb) bt
#0  0x000000000462513a in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::get_m (this=0x7fffcc000f60, p_index=1)
    at ./core/cowdata.h:145
#1  0x0000000004622129 in VMap<Object::Signal::Target, Object::Signal::Slot>::operator[] (this=0x7fffcc000f60, p_key=...)
    at ./core/vmap.h:202
#2  0x000000000461a406 in Object::connect (this=0x6f9e950, p_signal=..., p_to_object=0x7fffcc000b90, p_to_method=..., p_binds=..., 
    p_flags=0) at core/object.cpp:1485
#3  0x0000000004630e8f in MethodBind5R<Object, Error, StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int>::call (this=0x6409750, p_object=0x6f9e950, p_args=0x7fffd7eb8ef0, p_arg_count=3, r_error=...) at core/method_bind.gen.inc:4285
#4  0x00000000046157f9 in Object::call (this=0x6f9e950, p_method=..., p_args=0x7fffd7eb8ef0, p_argcount=3, r_error=...)
    at core/object.cpp:921
#5  0x00000000046d28f5 in Variant::call_ptr (this=0x7fffd7ebaa60, p_method=..., p_args=0x7fffd7eb8ef0, p_argcount=3, r_ret=0x0, 
    r_error=...) at core/variant_call.cpp:1112
#6  0x00000000018ea067 in GDScriptFunction::call (this=0x67f0560, p_instance=0x7260480, p_args=0x7fffd7ebad60, p_argcount=1, 
    r_err=..., p_state=0x0) at modules/gdscript/gdscript_function.cpp:1081
#7  0x0000000001883b04 in GDScriptInstance::call (this=0x7260480, p_method=..., p_args=0x7fffd7ebad60, p_argcount=1, r_error=...)
    at modules/gdscript/gdscript.cpp:1173
#8  0x000000000461563f in Object::call (this=0x6f9e950, p_method=..., p_args=0x7fffd7ebad60, p_argcount=1, r_error=...)
    at core/object.cpp:900
#9  0x000000000492bc25 in _Thread::_start_func (ud=0x70e3610) at core/bind/core_bind.cpp:2660
#10 0x000000000242b95c in ThreadPosix::thread_callback (userdata=0x725e9d0) at drivers/unix/thread_posix.cpp:74
#11 0x00007ffff7a164cf in start_thread () from /usr/lib/libpthread.so.0
#12 0x00007ffff75f42d3 in clone () from /usr/lib/libc.so.6

Minimal reproduction project:

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 9
  • Comments: 21 (13 by maintainers)

Most upvoted comments

This issue is rather problematic. This basically means that I cannot use a Thread to instance nodes that will eventually have to get freed again as there’s always the chance of this signal crash when I free nodes that were instantiated from a thread. And instancing all my nodes from the main thread is not an option as it causes stuttering in my project.

I will continue to try and find workarounds, are there any already known workarounds for this crash? A way to free nodes that were instantiated from a separate thread instance without it potentially crashing the game?

The issue i was having was not created by queue_free in a thread. the signal map becomes corrupted when you call .instance() from the thread because the thread manipulates the signal map without locking it. It shows up later when you free the instance but the actual corruption happens when you create the nodes from a separate thread (in my case)

Some issues with this situation that aren’t easily manageable by explicit thread safe methods, or at all:

  • Resources being retrieved from cache, this isn’t a case of it being impossible, but rather one that is hard to keep track of #60098
  • Several types (for example AudioStreamPlayer) connect signals to servers unconditionally, on creation #77518
  • It also seems to occur in the editor #69614

Simple actions such as attaching a resource to a node, while doing the correct thing of preparing it on a separate thread, not realizing the resource was cached and is not unique, and simply setting the mesh of a MeshInstance can cause this (see #60098), and I don’t necessarily think it’s reasonable to entirely forgo using cached resources for this, nor really to make convoluted code to replace the resource in the main thread

Essentially, there are a bunch of cases where signals are connected, unbeknownst to the user, and trying to navigate the thread safety of that is hard even for experienced users, and the behaviour in this case isn’t something covered by the stability or compatibility of the engine, so a function can suddenly start connecting a signal that it didn’t in the last patch release, throwing everything off

To me this level of unpredictability really warrants making signal connections thread safe, while I do get that explicitly connecting signals should be something the user needs to be aware of isn’t guaranteed to be thread safe, this isn’t necessarily limited to that

A possible (at least temporary) fix would be to provide a function to make the connection in a deferred way, possibly making this thread-aware and connecting directly if on the main thread (I’m realising with this that a general option to do deferred calls only if not on the main thread as a special function might be an interesting idea)

This looks promising: https://www.stroustrup.com/lock-free-vector.pdf Or this: https://github.com/cameron314/concurrentqueue https://www.1024cores.net/home/lock-free-algorithms/queues

Cas operations are slower than no thread safety… but how much slower when contention is rare?

Alternative ideas:

  1. to ensure that instantiation of every type of scene node is very fast and cheap. If there were no costly instance() calls (like when you create a viewport node) then the thread safety of the signal connections would not be a problem in this case.
  2. defer signal connection when scenes are being instanced and ensure it’s called in the main thread. Signal connection should be fast even if the nodes being instantiated are slow. (Might be fine if you definitely add the new instanced tree fragment to the scene tree in the main thread and the signals have definitely been connected by then? Maybe a lock free queue of signal add/delete operations? If the queue is non empty it can be merged into the main connection list if the main thread is reading/writing. If the non main thread reads or writes then the contents would be considered like a journal?.. getting complicated) A journal could probably be very fast with a single consumer queue: https://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue

https://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue

These are all very complicated and usually focused more on hpc and high degrees of parallelism… maybe something could be done with a fast path/slow path where the slow path syncs access to a journal with a mutex and the fast path (when the journal is empty) only needs a memory barrier… the journal can be replayed on thread access and committed on the main thread access…

Not a lot of things in Godot are thread-safe (https://docs.godotengine.org/en/stable/tutorials/threads/thread_safe_apis.html). Object is not designed to be. among them, for performance reasons, since that would mean Object’s code would be full of mutexes when the most relevant use case is single threaded access.

In order to manipulate most types of Objects from multiple threads, a project needs to use its own locking mechanism (based on Mutex, Sempahore, etc.). The case of connecting signals from arbitrary threads is a tricky one, since the engine will check the connection map at some point without respecting any sync mechanism the project is using.

So, for anything shared with the engine in such a way that it may access it at hard to predict moments (like signal connections, nodes already in the scene tree, etc.), the right way to operate on it in a multiple threads context (if you really need to) is to create some sort of communicaiton channel between your arbitrary thread and the main one, and to let the latter do what is necessary.

This is a way to approach the example from the OP safely:

extends Node

signal dummy_signal()

# This is for avoiding thrashing the message queue if the threads are faster
# creating objects than the main thread deleting them (in an extreme example
# like this).
# (This could be a singleton with a proper _process() function.)
class DeferredDeleter:
	var objects = []
	var mutex = Mutex.new()

	func add(object):
		mutex.lock()
		objects.append(object)
		mutex.unlock()

	func process():
		mutex.lock()
		for o in objects:
			o.free()
		objects.clear()
		mutex.unlock()

class DummyObject extends Object:
	var sem = Semaphore.new()

	func dummy_method():
		pass

	func do_the_connection(main):
		main.connect("dummy_signal", self, "dummy_method") # dummy method
		print("Connected!")
		sem.post()

var dd = DeferredDeleter.new()

func _ready():
	var threads = []
	for i in range(2):
		var thread = Thread.new()
		thread.start(self, "_thread", null)
		threads.push_back(thread)

func _process(delta):
	print("Working")
	dd.process()

func _thread(_x):
	while true:
		var object = DummyObject.new()
		object.call_deferred("do_the_connection", self)
		object.sem.wait()
		dd.add(object)

UPDATE: Now I’m realizing that the deferred call to do_the_connection() can potentially thrash the engine’s message queue, too. However, I don’t have more time to spend on this example and I guess it already illustrates my point. In any case, if threads are not as aggressive as in this example, that wouldn’t be necessary; and the deferred deleter probably either (call_deferred('free') would be just fine).

That’s pretty much @RandomShaper’s de facto area of expertise now 😃

@Flarkk Unfortunately not! I had this solution where I was constantly instancing scenes from a thread and queue freeing those scenes from the main thread again. Hundreds of times per second. In this case, this issue would occur but rarely. Now I am no longer freeing scenes and I am instead reusing them so I no longer run into this potential crash anymore.

Short messages like malloc(): invalid next->prev_inuse (unsorted) or realloc(): invalid next size means that memory is too damaged to continue the program and the program can crash in random moments(each time may be different backtrace) I found that using Address Sanitizer scons p=x11 -j6 use_asan=yes show a lot of more detailed backtrace of first use invalid memory, double free etc. instead show backtrace of crash, which can happens a lot of time later. This is output from Asan(This time is very similar to crash backtraces)

==4043==ERROR: AddressSanitizer: attempting double-free on 0x61900012fc80 in thread T6 (_thread):
    #0 0x7fc65c28bf1e in __interceptor_realloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10df1e)
    #1 0xea60b75 in Memory::realloc_static(void*, unsigned long, bool) core/os/memory.cpp:137
    #2 0xe581a4e in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::resize(int) core/cowdata.h:283
    #3 0xe5812cb in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::insert(int, VMap<Object::Signal::Target, Object::Signal::Slot>::Pair const&) core/cowdata.h:175
    #4 0xe57233a in VMap<Object::Signal::Target, Object::Signal::Slot>::insert(Object::Signal::Target const&, Object::Signal::Slot const&) core/vmap.h:125
    #5 0xe5688d5 in VMap<Object::Signal::Target, Object::Signal::Slot>::operator[](Object::Signal::Target const&) core/vmap.h:199
    #6 0xe549b1a in Object::connect(StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int) core/object.cpp:1485
    #7 0xe595430 in MethodBind5R<Error, StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int>::call(Object*, Variant const**, int, Variant::CallError&) core/method_bind.gen.inc:4177
    #8 0xe5373aa in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:921
    #9 0xe7a9e6e in Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) core/variant_call.cpp:1112
    #10 0x1a1836a in GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) modules/gdscript/gdscript_function.cpp:1081
    #11 0x18549c3 in GDScriptInstance::call(StringName const&, Variant const**, int, Variant::CallError&) modules/gdscript/gdscript.cpp:1173
    #12 0xe536f17 in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:900
    #13 0xf057b55 in _Thread::_start_func(void*) core/bind/core_bind.cpp:2664
    #14 0x4f836b5 in ThreadPosix::thread_callback(void*) drivers/unix/thread_posix.cpp:74
    #15 0x7fc65bc18668 in start_thread /build/glibc-4WA41p/glibc-2.30/nptl/pthread_create.c:479
    #16 0x7fc65ae79322 in clone (/lib/x86_64-linux-gnu/libc.so.6+0x122322)

0x61900012fc80 is located 0 bytes inside of 1040-byte region [0x61900012fc80,0x619000130090)
freed by thread T7 (_thread) here:
    #0 0x7fc65c28bf1e in __interceptor_realloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10df1e)
    #1 0xea60b75 in Memory::realloc_static(void*, unsigned long, bool) core/os/memory.cpp:137
    #2 0xe581a4e in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::resize(int) core/cowdata.h:283
    #3 0xe5812cb in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::insert(int, VMap<Object::Signal::Target, Object::Signal::Slot>::Pair const&) core/cowdata.h:175
    #4 0xe57233a in VMap<Object::Signal::Target, Object::Signal::Slot>::insert(Object::Signal::Target const&, Object::Signal::Slot const&) core/vmap.h:125
    #5 0xe5688d5 in VMap<Object::Signal::Target, Object::Signal::Slot>::operator[](Object::Signal::Target const&) core/vmap.h:199
    #6 0xe549b1a in Object::connect(StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int) core/object.cpp:1485
    #7 0xe595430 in MethodBind5R<Error, StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int>::call(Object*, Variant const**, int, Variant::CallError&) core/method_bind.gen.inc:4177
    #8 0xe5373aa in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:921
    #9 0xe7a9e6e in Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) core/variant_call.cpp:1112
    #10 0x1a1836a in GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) modules/gdscript/gdscript_function.cpp:1081
    #11 0x18549c3 in GDScriptInstance::call(StringName const&, Variant const**, int, Variant::CallError&) modules/gdscript/gdscript.cpp:1173
    #12 0xe536f17 in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:900
    #13 0xf057b55 in _Thread::_start_func(void*) core/bind/core_bind.cpp:2664
    #14 0x4f836b5 in ThreadPosix::thread_callback(void*) drivers/unix/thread_posix.cpp:74
    #15 0x7fc65bc18668 in start_thread /build/glibc-4WA41p/glibc-2.30/nptl/pthread_create.c:479

previously allocated by thread T7 (_thread) here:
    #0 0x7fc65c28bf1e in __interceptor_realloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10df1e)
    #1 0xea60b75 in Memory::realloc_static(void*, unsigned long, bool) core/os/memory.cpp:137
    #2 0xe581a4e in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::resize(int) core/cowdata.h:283
    #3 0xe5812cb in CowData<VMap<Object::Signal::Target, Object::Signal::Slot>::Pair>::insert(int, VMap<Object::Signal::Target, Object::Signal::Slot>::Pair const&) core/cowdata.h:175
    #4 0xe57233a in VMap<Object::Signal::Target, Object::Signal::Slot>::insert(Object::Signal::Target const&, Object::Signal::Slot const&) core/vmap.h:125
    #5 0xe5688d5 in VMap<Object::Signal::Target, Object::Signal::Slot>::operator[](Object::Signal::Target const&) core/vmap.h:199
    #6 0xe549b1a in Object::connect(StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int) core/object.cpp:1485
    #7 0xe595430 in MethodBind5R<Error, StringName const&, Object*, StringName const&, Vector<Variant> const&, unsigned int>::call(Object*, Variant const**, int, Variant::CallError&) core/method_bind.gen.inc:4177
    #8 0xe5373aa in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:921
    #9 0xe7a9e6e in Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) core/variant_call.cpp:1112
    #10 0x1a1836a in GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) modules/gdscript/gdscript_function.cpp:1081
    #11 0x18549c3 in GDScriptInstance::call(StringName const&, Variant const**, int, Variant::CallError&) modules/gdscript/gdscript.cpp:1173
    #12 0xe536f17 in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:900
    #13 0xf057b55 in _Thread::_start_func(void*) core/bind/core_bind.cpp:2664
    #14 0x4f836b5 in ThreadPosix::thread_callback(void*) drivers/unix/thread_posix.cpp:74
    #15 0x7fc65bc18668 in start_thread /build/glibc-4WA41p/glibc-2.30/nptl/pthread_create.c:479

Thread T6 (_thread) created by T0 here:
    #0 0x7fc65c1b8805 in pthread_create (/lib/x86_64-linux-gnu/libasan.so.5+0x3a805)
    #1 0x4f83bb0 in ThreadPosix::create_func_posix(void (*)(void*), void*, Thread::Settings const&) drivers/unix/thread_posix.cpp:90
    #2 0xea7448f in Thread::create(void (*)(void*), void*, Thread::Settings const&) core/os/thread.cpp:51
    #3 0xf0595c1 in _Thread::start(Object*, StringName const&, Variant const&, _Thread::Priority) core/bind/core_bind.cpp:2710
    #4 0xf1305d1 in MethodBind4R<Error, Object*, StringName const&, Variant const&, _Thread::Priority>::call(Object*, Variant const**, int, Variant::CallError&) core/method_bind.gen.inc:3325
    #5 0xe5373aa in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:921
    #6 0xe7a9e6e in Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) core/variant_call.cpp:1112
    #7 0x1a1836a in GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) modules/gdscript/gdscript_function.cpp:1081
    #8 0x1855714 in GDScriptInstance::_ml_call_reversed(GDScript*, StringName const&, Variant const**, int) modules/gdscript/gdscript.cpp:1204
    #9 0x18559c3 in GDScriptInstance::call_multilevel_reversed(StringName const&, Variant const**, int) modules/gdscript/gdscript.cpp:1211
    #10 0x92ef1cc in Node::_notification(int) scene/main/node.cpp:149
    #11 0x171077b in Node::_notificationv(int, bool) scene/main/node.h:46
    #12 0x1712c25 in CanvasItem::_notificationv(int, bool) scene/2d/canvas_item.h:166
    #13 0xacff0d7 in Node2D::_notificationv(int, bool) scene/2d/node_2d.h:38
    #14 0xe53783c in Object::notification(int, bool) core/object.cpp:931
    #15 0x92f1169 in Node::_propagate_ready() scene/main/node.cpp:196
    #16 0x92f09c0 in Node::_propagate_ready() scene/main/node.cpp:188
    #17 0x933df06 in Node::_set_tree(SceneTree*) scene/main/node.cpp:2562
    #18 0x94027ab in SceneTree::init() scene/main/scene_tree.cpp:465
    #19 0x1482c66 in OS_X11::run() platform/x11/os_x11.cpp:3242
    #20 0x13fe299 in main platform/x11/godot_x11.cpp:56
    #21 0x7fc65ad7e1e2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x271e2)

Thread T7 (_thread) created by T0 here:
    #0 0x7fc65c1b8805 in pthread_create (/lib/x86_64-linux-gnu/libasan.so.5+0x3a805)
    #1 0x4f83bb0 in ThreadPosix::create_func_posix(void (*)(void*), void*, Thread::Settings const&) drivers/unix/thread_posix.cpp:90
    #2 0xea7448f in Thread::create(void (*)(void*), void*, Thread::Settings const&) core/os/thread.cpp:51
    #3 0xf0595c1 in _Thread::start(Object*, StringName const&, Variant const&, _Thread::Priority) core/bind/core_bind.cpp:2710
    #4 0xf1305d1 in MethodBind4R<Error, Object*, StringName const&, Variant const&, _Thread::Priority>::call(Object*, Variant const**, int, Variant::CallError&) core/method_bind.gen.inc:3325
    #5 0xe5373aa in Object::call(StringName const&, Variant const**, int, Variant::CallError&) core/object.cpp:921
    #6 0xe7a9e6e in Variant::call_ptr(StringName const&, Variant const**, int, Variant*, Variant::CallError&) core/variant_call.cpp:1112
    #7 0x1a1836a in GDScriptFunction::call(GDScriptInstance*, Variant const**, int, Variant::CallError&, GDScriptFunction::CallState*) modules/gdscript/gdscript_function.cpp:1081
    #8 0x1855714 in GDScriptInstance::_ml_call_reversed(GDScript*, StringName const&, Variant const**, int) modules/gdscript/gdscript.cpp:1204
    #9 0x18559c3 in GDScriptInstance::call_multilevel_reversed(StringName const&, Variant const**, int) modules/gdscript/gdscript.cpp:1211
    #10 0x92ef1cc in Node::_notification(int) scene/main/node.cpp:149
    #11 0x171077b in Node::_notificationv(int, bool) scene/main/node.h:46
    #12 0x1712c25 in CanvasItem::_notificationv(int, bool) scene/2d/canvas_item.h:166
    #13 0xacff0d7 in Node2D::_notificationv(int, bool) scene/2d/node_2d.h:38
    #14 0xe53783c in Object::notification(int, bool) core/object.cpp:931
    #15 0x92f1169 in Node::_propagate_ready() scene/main/node.cpp:196
    #16 0x92f09c0 in Node::_propagate_ready() scene/main/node.cpp:188
    #17 0x933df06 in Node::_set_tree(SceneTree*) scene/main/node.cpp:2562
    #18 0x94027ab in SceneTree::init() scene/main/scene_tree.cpp:465
    #19 0x1482c66 in OS_X11::run() platform/x11/os_x11.cpp:3242
    #20 0x13fe299 in main platform/x11/godot_x11.cpp:56
    #21 0x7fc65ad7e1e2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x271e2)

SUMMARY: AddressSanitizer: double-free (/lib/x86_64-linux-gnu/libasan.so.5+0x10df1e) in __interceptor_realloc