Description
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 usePy_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), butob_ref_local
is still zero. The nextPy_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.
Lines 373 to 398 in 8303d32
Originally reported by @albanD
Linked PRs
- gh-121794: Don't set
ob_tid
to zero in fast-path dealloc #121799 - [3.13] gh-121794: Don't set
ob_tid
to zero in fast-path dealloc (GH-121799) #121821
Footnotes
-
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. ↩