You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Atomic GCHandle ownership and finalizer-thread shutdown guards
Two related sets of races that the GIL hid but free-threaded Python
exposes reliably.
ClassDerivedObject.tp_dealloc and ManagedType.TryFreeGCHandle both
read the GCHandle slot, then mutated it. Under FT, subtype_clear (on
the main thread) and the .NET finalizer thread can race for the same
slot; a non-atomic read-then-zero lets both threads observe the same
handle and double-free it. Both paths now use Interlocked.Exchange
to atomically claim ownership of the slot - only the thread that
observes a non-zero handle frees it. ClassDerived's strong-to-weak
swap follows the same pattern.
CreateDerivedType emits IL via Reflection.Emit, whose ModuleBuilder
and TypeBuilder operations are documented as not thread-safe.
Concurrent dynamic subclass creation under FT corrupts the IL stream
and segfaults. Serialise the entire emit-and-bake sequence on the
existing _buildersLock (the lock that already guarded the assembly /
module builder cache).
The .NET finalizer thread can dispatch Py_DecRef calls concurrently
with Py_Finalize, and a stale read of ob_ref_local after teardown
crashes the process. Three guards:
- Finalizer.ThrottledCollect and PyObject's finalizer short-circuit
when Runtime._Py_IsFinalizing(); PyObject drops the raw pointer
instead of enqueueing a decref so process exit reclaims the memory.
- Finalizer.AddFinalizedObject's Refcount > 0 Debug.Assert is kept on
GIL builds and skipped on FT; a stale ob_ref_local read from the
finalizer thread can crash the process even when the assertion
would succeed under the GIL.
- Runtime.XDecref's matching Refcount > 0 Debug.Assert gets the same
FT-only skip for the same reason.
0 commit comments