Files
2026-07-10 10:27:48 +03:00

43 lines
1.6 KiB
Markdown

# godot-kotlin-jvm binding leak PoC
Repro project for [utopia-rise/godot-kotlin-jvm#941](https://github.com/utopia-rise/godot-kotlin-jvm/issues/941).
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+.
```bash
./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.