From f28ac03df67be5528b1e7785f5424a3f0c053fbc Mon Sep 17 00:00:00 2001 From: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:51:00 -0400 Subject: [PATCH] Fix the extern early return in HXTAllocation HXTAllocation skips allocations made inside a CFFI call, but before returning it unlocked alloc_mutex, which it has not locked yet (the lock is taken a few lines further down). Unlocking a mutex the thread does not own is undefined behaviour. On Windows it leaves the mutex held, and the next thread that tracks an allocation or stashes blocks forever. The same check also read getCurrentStackFrame(), which is mStackFrames.back() and undefined while no frame is on the stack. Drop the stray unlock and check the stack depth first. The new telemetry test allocates inside an extern frame on one thread, then stashes on another; without the fix it deadlocks on Windows. --- src/hx/Telemetry.cpp | 4 +-- test/telemetry/TestAllocations.hx | 46 +++++++++++++++++++++++++++++++ test/telemetry/TestMain.hx | 2 +- 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 test/telemetry/TestAllocations.hx diff --git a/src/hx/Telemetry.cpp b/src/hx/Telemetry.cpp index 3dd017322..002ba54e7 100644 --- a/src/hx/Telemetry.cpp +++ b/src/hx/Telemetry.cpp @@ -472,8 +472,8 @@ void hx::Telemetry::HXTAllocation(void* obj, size_t inSize, const char* type) // for every external call, hashes for every SDL event (Lime's // ExternalInterface.external_handler()), etc #ifndef HXCPP_PROFILE_EXTERNS - if (stack->getCurrentStackFrame()->position->className==hx::EXTERN_CLASS_NAME) { - alloc_mutex.unlock(); + // alloc_mutex is not locked yet, and an empty stack has no current frame. + if (stack->getDepth() > 0 && stack->getCurrentStackFrame()->position->className==hx::EXTERN_CLASS_NAME) { return; } #endif diff --git a/test/telemetry/TestAllocations.hx b/test/telemetry/TestAllocations.hx new file mode 100644 index 000000000..604385daa --- /dev/null +++ b/test/telemetry/TestAllocations.hx @@ -0,0 +1,46 @@ +import sys.thread.Lock; +import sys.thread.Thread; +import utest.Assert; +import utest.Test; + +class TestAllocations extends Test +{ + // Keeps what the worker thread allocates reachable, so it is never + // reclaimed after that thread's telemetry has been deleted + static var tracked:Array = []; + + // HXTAllocation skips allocations made inside a CFFI call. Skipping one + // must leave alloc_mutex usable, or the next thread to lock it blocks. + function testExternAllocationLeavesAllocationLockUsable() + { + var allocated = new Lock(); + Thread.create(function() { + untyped __global__.__hxcpp_hxt_start_telemetry(true, true); + tracked.push(allocateInExternFrame()); + tracked.push([4, 5, 6]); + allocated.release(); + }); + Assert.isTrue(allocated.wait(10.0)); + + // Stashing locks alloc_mutex, even without allocation tracking + var stashed = new Lock(); + Thread.create(function() { + untyped __global__.__hxcpp_hxt_stash_telemetry(); + stashed.release(); + }); + Assert.isTrue(stashed.wait(10.0)); + } + + // Primitives loaded with cpp.Lib.load run inside a frame like this one + // (see ExternalPrimitive in src/hx/Lib.cpp), so what they allocate takes + // the extern path in HXTAllocation + static function allocateInExternFrame():Array + { + var result:Array = null; + untyped __cpp__('{ + HX_STACK_FRAME(::hx::EXTERN_CLASS_NAME, "cffi", 0, "extern::cffi", __FILE__, __LINE__, 0) + {0} = ::Array_obj< int >::__new(3, 3); + }', result); + return result; + } +} diff --git a/test/telemetry/TestMain.hx b/test/telemetry/TestMain.hx index f13d9e28a..07aecc941 100644 --- a/test/telemetry/TestMain.hx +++ b/test/telemetry/TestMain.hx @@ -3,6 +3,6 @@ package; class TestMain { static function main(){ - utest.UTest.run([ new TestBasic() ]); + utest.UTest.run([ new TestBasic(), new TestAllocations() ]); } }