New GitHub issue #121794 from colesbury:<br>

<hr>

<pre>
# Bug report

The `_Py_MergeZeroLocalRefcount` function is called when the local refcount field reaches zero. We generally maintain the invariant [^1] that `ob_tid == 0` implies that the refcount fields are merged (i.e., `ob_ref_shared` flags are `_Py_REF_MERGED`) and vice versa.

The current implementation breaks the invariant by setting `ob_tid` to zero when the refcount fields aren't merged. Typically, this isn't a problem because:

* Most commonly, the object is deallocated so the values do not matter
* If the object is resurrected in `subtype_dealloc` (e.g., for a finalizer), we use `Py_SET_REFCNT`, which will mark the fields as merged, restoring the invariant

However, if resurrection is done slightly differently, such as by `Py_INCREF()`, then things can break in very strange ways:
* The GC may restore `ob_tid` from the allocator (because it's not merged), but `ob_ref_local` is still zero. The next `Py_DECREF` then leads to a "negative refcount" error.

### Summary

We should maintain the invariant that `ob_tid == 0` <=> `_Py_REF_IS_MERGED()` and check it with assertions when possible.

https://github.com/python/cpython/blob/8303d32ff55945c5b38eeeaf1b1811dbcf8aa9be/Objects/object.c#L373-L398

Originally reported by @albanD 

[^1]: There are a few places where we deliberately re-use `ob_tid` for other purposes, such as the trashcan mechanism and during GC, but these objects are not visible to other parts of the program.
</pre>

<hr>

<a href="https://github.com/python/cpython/issues/121794">View on GitHub</a>
<p>Labels: type-bug, 3.13, topic-free-threading, 3.14</p>
<p>Assignee: </p>