Heap corruption leading to SIGABRT during spAnimationState listener dispatch
Description
We maintain a fork of defold/extension-spine that adds a custom "animation instancing" optimization: multiple SpineModelComponents that play the same animation (same scene, animation id, playback and blend mode) are batched onto one shared master spSkeleton/spAnimationState, which is then evaluated once per frame and its pose copied out to every member component. This is not present in upstream extension-spine — we're filing this because the root cause is a general reentrancy hazard in how defold-spine wires Spine's AnimationStateListener to synchronous Lua callbacks, and we think it's worth your awareness even though our specific trigger (a shared master state) is custom.
We see a production SIGABRT with this signature:
abort
free ← heap corruption detected here
spSlot_dispose (Slot.c:44)
spSkeleton_dispose (Skeleton.c:155)
DestroyAnimationGroup (comp_spine_model.cpp:297) [our fork's grouping/instancing addition]
EraseGroupIfEmpty (comp_spine_model.cpp:451) [our fork's grouping/instancing addition]
dmSpine::CompSpineModelUpdate (comp_spine_model.cpp:1708)
dmGameObject::UpdateComponentFunction (gameobject.cpp:2749)
dmGameSystem::CompCollectionProxyUpdate (comp_collection_proxy.cpp:448)
dmGameObject::Update (gameobject.cpp:2875 / 2893)
... (repeats for nested collection proxies) ...
dmGameObject::StepFrame (gameobject.cpp:2151)
dmEngine::Step (engine.cpp:2351)
dmEngineUpdate (engine.cpp:2728)
dmEngine::RunLoop (engine_loop.cpp:86)
EngineMain (engine_main.cpp:57)
EngineMainThread (engine_main.cpp:74)
dmThread::ThreadStartProxy (thread_posix.cpp:76)
The abort is apparently a symptom, not the cause — it's heap corruption surfacing later inside an unrelated free(), not a crash at the call site itself.
Hypothesis: a missing upstream fix (#294 / PR #297) may also, or instead, explain this
While investigating, we found defold/extension-spine#294 ("Replaying the same spine animation from the animation callback leads to crashes under GC pressure") — a sibling reentrancy bug, that was fixed by PR #297 "Deferred callback destruction":
- Merge commit
bf34b6bc302b7668ac13c9723cbab32c261342c1 (tagged 4.7.4), content commit 0b6ad848041cc26f40e734621fca0ab3556778c9, merged 2026-08-08.
- The bug, per the linked repro's
main/repro.script: a spine.play_anim() callback calls spine.play_anim() again on every invocation (unconditionally, not gated on message_id), then forces collectgarbage("collect") plus allocates a few thousand vmath.vector4s. That reentrant call can free the in-flight LuaCallbackInfo while it's still executing on the call stack (RunTrackCallback → dmScript::PCall → Lua → back into spine.play_anim → ClearCompletionCallback → dmScript::DestroyCallback). The reporter noted the crash disappears if the GC-pressure call is removed — we read that as GC activity being what turns the underlying use-after-free into a visible crash, not as a separate cause.
- The fix adds
m_CallbackInvocationDepth and m_DeferredCallbacks to SpineModelComponent, deferring callback destruction until no invocation is in flight, instead of freeing immediately.
We confirmed our fork's master does not have this fix.
We can't yet say whether this missing fix, our custom AnimationGroup sharing, or both together are responsible for the production crash — heap corruption from either mechanism would surface identically (an unrelated, later free() aborting). Noting this as an open hypothesis pending further isolation on our end; we're backporting #297 regardless, to ask for your opinion on whether our problem can be something that you've already fixed in PR #297 "Deferred callback destruction", or something you can recognize.
Expected behavior
No engine crash.
Versions
- Defold 1.13.1 (
63124d7fd7570a51224f34d3d6459a2f7d11bfe7)
- Spine extension: tagged as 4.7.2, forked
- Spine (Esoteric Software library) runtime: 4.2
Platforms
- Platform: Android
- OS: Android 12 (crash telemetry also shows occurrences on other Android versions/releases; this is a heap-logic bug, not device-specific)
- Device: e.g. vivo V2029, arm64-v8a — low/mid-tier devices over-represented, likely just exposure/frequency rather than a device-specific cause
Logs
dmSpine--DestroyAnimationGroup (comp_spine_model.cpp-297)-redacted.json
Heap corruption leading to SIGABRT during spAnimationState listener dispatch
Description
We maintain a fork of defold/extension-spine that adds a custom "animation instancing" optimization: multiple
SpineModelComponents that play the same animation (same scene, animation id, playback and blend mode) are batched onto one shared masterspSkeleton/spAnimationState, which is then evaluated once per frame and its pose copied out to every member component. This is not present in upstreamextension-spine— we're filing this because the root cause is a general reentrancy hazard in howdefold-spinewires Spine'sAnimationStateListenerto synchronous Lua callbacks, and we think it's worth your awareness even though our specific trigger (a shared master state) is custom.We see a production SIGABRT with this signature:
The abort is apparently a symptom, not the cause — it's heap corruption surfacing later inside an unrelated
free(), not a crash at the call site itself.Hypothesis: a missing upstream fix (
#294/ PR #297) may also, or instead, explain thisWhile investigating, we found
defold/extension-spine#294("Replaying the same spine animation from the animation callback leads to crashes under GC pressure") — a sibling reentrancy bug, that was fixed by PR #297 "Deferred callback destruction":bf34b6bc302b7668ac13c9723cbab32c261342c1(tagged4.7.4), content commit0b6ad848041cc26f40e734621fca0ab3556778c9, merged 2026-08-08.main/repro.script: aspine.play_anim()callback callsspine.play_anim()again on every invocation (unconditionally, not gated onmessage_id), then forcescollectgarbage("collect")plus allocates a few thousandvmath.vector4s. That reentrant call can free the in-flightLuaCallbackInfowhile it's still executing on the call stack (RunTrackCallback→dmScript::PCall→ Lua → back intospine.play_anim→ClearCompletionCallback→dmScript::DestroyCallback). The reporter noted the crash disappears if the GC-pressure call is removed — we read that as GC activity being what turns the underlying use-after-free into a visible crash, not as a separate cause.m_CallbackInvocationDepthandm_DeferredCallbackstoSpineModelComponent, deferring callback destruction until no invocation is in flight, instead of freeing immediately.We confirmed our fork's
masterdoes not have this fix.We can't yet say whether this missing fix, our custom
AnimationGroupsharing, or both together are responsible for the production crash — heap corruption from either mechanism would surface identically (an unrelated, laterfree()aborting). Noting this as an open hypothesis pending further isolation on our end; we're backporting #297 regardless, to ask for your opinion on whether our problem can be something that you've already fixed in PR #297 "Deferred callback destruction", or something you can recognize.Expected behavior
No engine crash.
Versions
63124d7fd7570a51224f34d3d6459a2f7d11bfe7)Platforms
Logs
dmSpine--DestroyAnimationGroup (comp_spine_model.cpp-297)-redacted.json