Skip to content

Commit

Permalink
pythongh-109496: Detect Py_DECREF() after dealloc in debug mode (pyth…
Browse files Browse the repository at this point in the history
…on#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.
  • Loading branch information
vstinner authored Sep 18, 2023
1 parent ef659b9 commit 0bb0d88
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 16 deletions.
10 changes: 4 additions & 6 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -660,17 +660,15 @@ static inline void Py_DECREF(PyObject *op) {
#elif defined(Py_REF_DEBUG)
static inline void Py_DECREF(const char *filename, int lineno, PyObject *op)
{
if (op->ob_refcnt <= 0) {
_Py_NegativeRefcount(filename, lineno, op);
}
if (_Py_IsImmortal(op)) {
return;
}
_Py_DECREF_STAT_INC();
_Py_DECREF_DecRefTotal();
if (--op->ob_refcnt != 0) {
if (op->ob_refcnt < 0) {
_Py_NegativeRefcount(filename, lineno, op);
}
}
else {
if (--op->ob_refcnt == 0) {
_Py_Dealloc(op);
}
}
Expand Down
36 changes: 26 additions & 10 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,24 +301,40 @@ def test_getitem_with_error(self):
def test_buildvalue_N(self):
_testcapi.test_buildvalue_N()

@unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'),
'need _testcapi.negative_refcount')
def test_negative_refcount(self):
def check_negative_refcount(self, code):
# bpo-35059: Check that Py_DECREF() reports the correct filename
# when calling _Py_NegativeRefcount() to abort Python.
code = textwrap.dedent("""
import _testcapi
from test import support
with support.SuppressCrashReport():
_testcapi.negative_refcount()
""")
code = textwrap.dedent(code)
rc, out, err = assert_python_failure('-c', code)
self.assertRegex(err,
br'_testcapimodule\.c:[0-9]+: '
br'_Py_NegativeRefcount: Assertion failed: '
br'object has negative ref count')

@unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'),
'need _testcapi.negative_refcount()')
def test_negative_refcount(self):
code = """
import _testcapi
from test import support
with support.SuppressCrashReport():
_testcapi.negative_refcount()
"""
self.check_negative_refcount(code)

@unittest.skipUnless(hasattr(_testcapi, 'decref_freed_object'),
'need _testcapi.decref_freed_object()')
def test_decref_freed_object(self):
code = """
import _testcapi
from test import support
with support.SuppressCrashReport():
_testcapi.decref_freed_object()
"""
self.check_negative_refcount(code)

def test_trashcan_subclass(self):
# bpo-35983: Check that the trashcan mechanism for "list" is NOT
# activated when its tp_dealloc is being called by a subclass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
On a Python built in debug mode, :c:func:`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()``. Patch by Victor Stinner.
21 changes: 21 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,26 @@ negative_refcount(PyObject *self, PyObject *Py_UNUSED(args))

Py_RETURN_NONE;
}

static PyObject *
decref_freed_object(PyObject *self, PyObject *Py_UNUSED(args))
{
PyObject *obj = PyUnicode_FromString("decref_freed_object");
if (obj == NULL) {
return NULL;
}
assert(Py_REFCNT(obj) == 1);

// Deallocate the memory
Py_DECREF(obj);
// obj is a now a dangling pointer

// gh-109496: If Python is built in debug mode, Py_DECREF() must call
// _Py_NegativeRefcount() and abort Python.
Py_DECREF(obj);

Py_RETURN_NONE;
}
#endif


Expand Down Expand Up @@ -3299,6 +3319,7 @@ static PyMethodDef TestMethods[] = {
{"bad_get", _PyCFunction_CAST(bad_get), METH_FASTCALL},
#ifdef Py_REF_DEBUG
{"negative_refcount", negative_refcount, METH_NOARGS},
{"decref_freed_object", decref_freed_object, METH_NOARGS},
#endif
{"meth_varargs", meth_varargs, METH_VARARGS},
{"meth_varargs_keywords", _PyCFunction_CAST(meth_varargs_keywords), METH_VARARGS|METH_KEYWORDS},
Expand Down

0 comments on commit 0bb0d88

Please sign in to comment.