Files
gkj-binding-leak-poc/README.md
T

1.5 KiB

godot-kotlin-jvm binding leak PoC

Minimal reproduction for a JVM-side binding leak in godot-kotlin-jvm 0.16.2 (present since the MemoryManager rework, still present on master): every non-RefCounted Object freed at runtime leaves its entry in MemoryManager.ObjectDB forever. For Objects the binding holds a strong reference to the wrapper, so the JVM heap grows without bound while native memory stays flat.

Run

Requires a godot-kotlin-jvm 0.16.2-4.6.3 editor binary and a JDK 17+.

./gradlew build
godot --headless --import .
godot --headless --import .   # run twice, first pass may bail early
godot --headless --path . res://main.tscn

Output on 0.16.2-4.6.3 (macOS, headless):

[POC] freed=20000 nativeObjects=1472 jvmObjectDB=20039
[POC] LEAK CONFIRMED: ~18567 zombie bindings in MemoryManager.ObjectDB

All 20000 freed nodes are still in the JVM ObjectDB. How the node is freed does not matter: free() from Kotlin, queueFree(), or engine-side deletion all leak.

Root cause

KotlinBindingManager::_instance_binding_free_callback queues the dead object via MemoryManager::queue_dead_object(obj), which reads obj->get_instance_id(). Godot calls instance-binding free callbacks at the very end of Object::~Object, after ObjectDB::remove_instance(this); _instance_id = ObjectID(); (core/object/object.cpp, same order in every 4.x release since 4.3). The id is always the null ObjectID by then, so the JVM is told to remove ObjectID(0) instead of the real id and the real entry stays.