Skip to content

GH-81061: Fix refcount issue when returning None from a ctypes.py_object callback #13364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Lib/test/test_ctypes/test_refcounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,20 @@ def func(a, b):
f(1, 2)
self.assertEqual(sys.getrefcount(ctypes.c_int), a)

@support.refcount_test
def test_callback_py_object_none_return(self):
Comment on lines +100 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this test is rendered pointless in 3.12 due to the adoption of PEP 683 (Immortal Objects, Using a Fixed Refcount). Because None is immortal, executing Py_INCREF(Py_None) and Py_DECREF(Py_None) will no longer modify its refcount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good to know. I assume this isn't implemented yet though, because as of the current state of main (447d061), this test still hard-crashes without the fix.

# bpo-36880: test that returning None from a py_object callback
# does not decrement the refcount of None.

for FUNCTYPE in (ctypes.CFUNCTYPE, ctypes.PYFUNCTYPE):
with self.subTest(FUNCTYPE=FUNCTYPE):
@FUNCTYPE(ctypes.py_object)
def func():
return None

# Check that calling func does not affect None's refcount.
for _ in range(10000):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single call should be enough to trigger the refleak checker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not... I tested again - apparently the refleak checker (with default settings: -m test --huntrleaks=: test.test_ctypes.test_refcounts) only detects if None's refcount is too high, not if it's too low. The latter case is only detected once None's refcount gets to zero/negative (which triggers an assertion), and that requires a thousand or so iterations of the broken code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If None refcount goes too small it will crash at some point assuming the rest of the code decrements normally

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not... I tested again - apparently the refleak checker (with default settings: -m test --huntrleaks=: test.test_ctypes.test_refcounts) only detects if None's refcount is too high, not if it's too low. The latter case is only detected once None's refcount gets to zero/negative (which triggers an assertion), and that requires a thousand or so iterations of the broken code.

I see, it is because of #74959.

func()

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a reference counting issue when a :mod:`ctypes` callback with return
type :class:`~ctypes.py_object` returns ``None``, which could cause crashes.
15 changes: 7 additions & 8 deletions Modules/_ctypes/callbacks.c
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,14 @@ static void _CallPythonObject(void *mem,
"of ctypes callback function",
callable);
}
else if (keep == Py_None) {
/* Nothing to keep */
Py_DECREF(keep);
}
else if (setfunc != _ctypes_get_fielddesc("O")->setfunc) {
if (-1 == PyErr_WarnEx(PyExc_RuntimeWarning,
"memory leak in callback function.",
1))
{
if (keep == Py_None) {
/* Nothing to keep */
Py_DECREF(keep);
}
else if (PyErr_WarnEx(PyExc_RuntimeWarning,
"memory leak in callback function.",
1) == -1) {
_PyErr_WriteUnraisableMsg("on converting result "
"of ctypes callback function",
callable);
Expand Down