Skip to content

Commit 74a7f5d

Browse files
[3.12] gh-109496: Detect Py_DECREF() after dealloc in debug mode (GH-109539) (#109545)
gh-109496: Detect Py_DECREF() after dealloc in debug mode (GH-109539) On a Python built in debug mode, Py_DECREF() now calls _Py_NegativeRefcount() if the object is a dangling pointer to deallocated memory: memory filled with 0xDD "dead byte" by the debug hook on memory allocators. The fix is to check the reference count *before* checking for _Py_IsImmortal(). Add test_decref_freed_object() to test_capi.test_misc. (cherry picked from commit 0bb0d88) Co-authored-by: Victor Stinner <[email protected]>
1 parent 0620bc7 commit 74a7f5d

File tree

4 files changed

+56
-16
lines changed

4 files changed

+56
-16
lines changed

Include/object.h

+4-6
Original file line numberDiff line numberDiff line change
@@ -679,17 +679,15 @@ static inline void Py_DECREF(PyObject *op) {
679679
#elif defined(Py_REF_DEBUG)
680680
static inline void Py_DECREF(const char *filename, int lineno, PyObject *op)
681681
{
682+
if (op->ob_refcnt <= 0) {
683+
_Py_NegativeRefcount(filename, lineno, op);
684+
}
682685
if (_Py_IsImmortal(op)) {
683686
return;
684687
}
685688
_Py_DECREF_STAT_INC();
686689
_Py_DECREF_DecRefTotal();
687-
if (--op->ob_refcnt != 0) {
688-
if (op->ob_refcnt < 0) {
689-
_Py_NegativeRefcount(filename, lineno, op);
690-
}
691-
}
692-
else {
690+
if (--op->ob_refcnt == 0) {
693691
_Py_Dealloc(op);
694692
}
695693
}

Lib/test/test_capi/test_misc.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -301,24 +301,40 @@ def test_getitem_with_error(self):
301301
def test_buildvalue_N(self):
302302
_testcapi.test_buildvalue_N()
303303

304-
@unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'),
305-
'need _testcapi.negative_refcount')
306-
def test_negative_refcount(self):
304+
def check_negative_refcount(self, code):
307305
# bpo-35059: Check that Py_DECREF() reports the correct filename
308306
# when calling _Py_NegativeRefcount() to abort Python.
309-
code = textwrap.dedent("""
310-
import _testcapi
311-
from test import support
312-
313-
with support.SuppressCrashReport():
314-
_testcapi.negative_refcount()
315-
""")
307+
code = textwrap.dedent(code)
316308
rc, out, err = assert_python_failure('-c', code)
317309
self.assertRegex(err,
318310
br'_testcapimodule\.c:[0-9]+: '
319311
br'_Py_NegativeRefcount: Assertion failed: '
320312
br'object has negative ref count')
321313

314+
@unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'),
315+
'need _testcapi.negative_refcount()')
316+
def test_negative_refcount(self):
317+
code = """
318+
import _testcapi
319+
from test import support
320+
321+
with support.SuppressCrashReport():
322+
_testcapi.negative_refcount()
323+
"""
324+
self.check_negative_refcount(code)
325+
326+
@unittest.skipUnless(hasattr(_testcapi, 'decref_freed_object'),
327+
'need _testcapi.decref_freed_object()')
328+
def test_decref_freed_object(self):
329+
code = """
330+
import _testcapi
331+
from test import support
332+
333+
with support.SuppressCrashReport():
334+
_testcapi.decref_freed_object()
335+
"""
336+
self.check_negative_refcount(code)
337+
322338
def test_trashcan_subclass(self):
323339
# bpo-35983: Check that the trashcan mechanism for "list" is NOT
324340
# activated when its tp_dealloc is being called by a subclass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
On a Python built in debug mode, :c:func:`Py_DECREF()` now calls
2+
``_Py_NegativeRefcount()`` if the object is a dangling pointer to
3+
deallocated memory: memory filled with ``0xDD`` "dead byte" by the debug
4+
hook on memory allocators. The fix is to check the reference count *before*
5+
checking for ``_Py_IsImmortal()``. Patch by Victor Stinner.

Modules/_testcapimodule.c

+21
Original file line numberDiff line numberDiff line change
@@ -2157,6 +2157,26 @@ negative_refcount(PyObject *self, PyObject *Py_UNUSED(args))
21572157

21582158
Py_RETURN_NONE;
21592159
}
2160+
2161+
static PyObject *
2162+
decref_freed_object(PyObject *self, PyObject *Py_UNUSED(args))
2163+
{
2164+
PyObject *obj = PyUnicode_FromString("decref_freed_object");
2165+
if (obj == NULL) {
2166+
return NULL;
2167+
}
2168+
assert(Py_REFCNT(obj) == 1);
2169+
2170+
// Deallocate the memory
2171+
Py_DECREF(obj);
2172+
// obj is a now a dangling pointer
2173+
2174+
// gh-109496: If Python is built in debug mode, Py_DECREF() must call
2175+
// _Py_NegativeRefcount() and abort Python.
2176+
Py_DECREF(obj);
2177+
2178+
Py_RETURN_NONE;
2179+
}
21602180
#endif
21612181

21622182

@@ -3306,6 +3326,7 @@ static PyMethodDef TestMethods[] = {
33063326
{"bad_get", _PyCFunction_CAST(bad_get), METH_FASTCALL},
33073327
#ifdef Py_REF_DEBUG
33083328
{"negative_refcount", negative_refcount, METH_NOARGS},
3329+
{"decref_freed_object", decref_freed_object, METH_NOARGS},
33093330
#endif
33103331
{"meth_varargs", meth_varargs, METH_VARARGS},
33113332
{"meth_varargs_keywords", _PyCFunction_CAST(meth_varargs_keywords), METH_VARARGS|METH_KEYWORDS},

0 commit comments

Comments
 (0)