Skip to content

Commit 8ea43e9

Browse files
committed
gh-109496: Detect Py_DECREF() after dealloc in debug mode
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.
1 parent 4dd47c6 commit 8ea43e9

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
@@ -660,17 +660,15 @@ static inline void Py_DECREF(PyObject *op) {
660660
#elif defined(Py_REF_DEBUG)
661661
static inline void Py_DECREF(const char *filename, int lineno, PyObject *op)
662662
{
663+
if (op->ob_refcnt <= 0) {
664+
_Py_NegativeRefcount(filename, lineno, op);
665+
}
663666
if (_Py_IsImmortal(op)) {
664667
return;
665668
}
666669
_Py_DECREF_STAT_INC();
667670
_Py_DECREF_DecRefTotal();
668-
if (--op->ob_refcnt != 0) {
669-
if (op->ob_refcnt < 0) {
670-
_Py_NegativeRefcount(filename, lineno, op);
671-
}
672-
}
673-
else {
671+
if (--op->ob_refcnt == 0) {
674672
_Py_Dealloc(op);
675673
}
676674
}

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
@@ -2034,6 +2034,26 @@ negative_refcount(PyObject *self, PyObject *Py_UNUSED(args))
20342034

20352035
Py_RETURN_NONE;
20362036
}
2037+
2038+
static PyObject *
2039+
decref_freed_object(PyObject *self, PyObject *Py_UNUSED(args))
2040+
{
2041+
PyObject *obj = PyUnicode_FromString("decref_freed_object");
2042+
if (obj == NULL) {
2043+
return NULL;
2044+
}
2045+
assert(Py_REFCNT(obj) == 1);
2046+
2047+
// Deallocate the memory
2048+
Py_DECREF(obj);
2049+
// obj is a now a dangling pointer
2050+
2051+
// gh-109496: If Python is built in debug mode, Py_DECREF() must call
2052+
// _Py_NegativeRefcount() and abort Python.
2053+
Py_DECREF(obj);
2054+
2055+
Py_RETURN_NONE;
2056+
}
20372057
#endif
20382058

20392059

@@ -3299,6 +3319,7 @@ static PyMethodDef TestMethods[] = {
32993319
{"bad_get", _PyCFunction_CAST(bad_get), METH_FASTCALL},
33003320
#ifdef Py_REF_DEBUG
33013321
{"negative_refcount", negative_refcount, METH_NOARGS},
3322+
{"decref_freed_object", decref_freed_object, METH_NOARGS},
33023323
#endif
33033324
{"meth_varargs", meth_varargs, METH_VARARGS},
33043325
{"meth_varargs_keywords", _PyCFunction_CAST(meth_varargs_keywords), METH_VARARGS|METH_KEYWORDS},

0 commit comments

Comments
 (0)