From 2d5542583ee910ecc0c0b421bd070598ee769016 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 5 Nov 2025 21:48:34 +0100 Subject: [PATCH 01/11] gh-141070: Add PyObject_Dump() function * Promote _PyObject_Dump() as a public function. * Keep _PyObject_Dump() alias to PyObject_Dump() for backward compatibility. * Replace _PyObject_Dump() with PyObject_Dump(). --- Doc/c-api/object.rst | 20 ++++++++++ Doc/whatsnew/3.15.rst | 4 ++ Include/cpython/object.h | 7 +++- .../pycore_global_objects_fini_generated.h | 2 +- Lib/test/test_capi/test_object.py | 38 +++++++++++++++++++ ...-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst | 2 + Modules/_testcapi/object.c | 9 +++++ Objects/object.c | 4 +- Objects/unicodeobject.c | 2 +- Python/gc.c | 2 +- Python/pythonrun.c | 8 ++-- 11 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 96353266ac7300..bc724ca451c4c2 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -85,6 +85,26 @@ Object Protocol instead of the :func:`repr`. +.. c:function:: void PyObject_Dump(PyObject* op) + + Dump an object *op* to ``stderr``. Function used for debugging. + + It can be called without an :term:`attached thread state`, even if it's not + recommended. + + Implement an heuristic to detect if the object memory has been freed. + + Example of output:: + + object address : 0x7f80124702c0 + object refcount : 2 + object type : 0x9902e0 + object type name: str + object repr : 'abcdef' + + .. versionadded:: next + + .. c:function:: int PyObject_HasAttrWithError(PyObject *o, PyObject *attr_name) Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5379ac3abba227..290b6469c1a4db 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -947,6 +947,10 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +* Add :c:func:`PyObject_Dump` to dump an object to ``stderr``. Function used + for debugging. + (Contributed by Victor Stinner in :gh:`141070`.) + * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. (Contributed by Victor Stinner in :gh:`111489`.) diff --git a/Include/cpython/object.h b/Include/cpython/object.h index d64298232e705c..f25112ee4567e1 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -295,7 +295,10 @@ PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *); PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); PyAPI_FUNC(void) _Py_BreakPoint(void); -PyAPI_FUNC(void) _PyObject_Dump(PyObject *); +PyAPI_FUNC(void) PyObject_Dump(PyObject *); + +// Alias for backward compatibility +#define _PyObject_Dump PyObject_Dump PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); @@ -387,7 +390,7 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *); process with a message on stderr if the given condition fails to hold, but compile away to nothing if NDEBUG is defined. - However, before aborting, Python will also try to call _PyObject_Dump() on + However, before aborting, Python will also try to call PyObject_Dump() on the given object. This may be of use when investigating bugs in which a particular object is corrupt (e.g. buggy a tp_visit method in an extension module breaking the garbage collector), to help locate the broken objects. diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 92ded14891a101..0c51e3ad75c5ac 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -13,7 +13,7 @@ static inline void _PyStaticObject_CheckRefcnt(PyObject *obj) { if (!_Py_IsImmortal(obj)) { fprintf(stderr, "Immortal Object has less refcnt than expected.\n"); - _PyObject_Dump(obj); + PyObject_Dump(obj); } } #endif diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index d4056727d07fbf..127bac0aaa32d3 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -1,4 +1,5 @@ import enum +import os import sys import textwrap import unittest @@ -13,6 +14,8 @@ _testcapi = import_helper.import_module('_testcapi') _testinternalcapi = import_helper.import_module('_testinternalcapi') +STDERR_FD = 2 + class Constant(enum.IntEnum): Py_CONSTANT_NONE = 0 @@ -247,5 +250,40 @@ def func(x): func(object()) + def test_pyobject_dump(self): + pyobject_dump = _testcapi.pyobject_dump + obj = 'test string' + + filename = os_helper.TESTFN + self.addCleanup(os_helper.unlink, filename) + + try: + old_stderr = os.dup(STDERR_FD) + except OSError as exc: + # os.dup(STDERR_FD) is not supported on WASI + self.skipTest(f"os.dup() failed with {exc!r}") + + try: + with open(filename, "wb") as fp: + fd = fp.fileno() + os.dup2(fd, STDERR_FD) + pyobject_dump(obj) + finally: + os.dup2(old_stderr, STDERR_FD) + os.close(old_stderr) + + with open(filename) as fp: + output = fp.read() + + hex_regex = r'0x[0-9a-fA-F]+' + self.assertRegex(output.rstrip(), + fr"object address : {hex_regex}\n" + r"object refcount : [0-9]+\n" + fr"object type : {hex_regex}\n" + r"object type name: str\n" + r"object repr : 'test string'" + ) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst b/Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst new file mode 100644 index 00000000000000..29628b69c37ae6 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyObject_Dump` to dump an object to ``stderr``. Function used +for debugging. Patch by Victor Stinner. diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 798ef97c495aeb..bae01ea817416b 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -485,6 +485,14 @@ is_uniquely_referenced(PyObject *self, PyObject *op) } +static PyObject * +pyobject_dump(PyObject *self, PyObject *op) +{ + PyObject_Dump(op); + Py_RETURN_NONE; +} + + static PyMethodDef test_methods[] = { {"call_pyobject_print", call_pyobject_print, METH_VARARGS}, {"pyobject_print_null", pyobject_print_null, METH_VARARGS}, @@ -511,6 +519,7 @@ static PyMethodDef test_methods[] = { {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, + {"pyobject_dump", pyobject_dump, METH_O}, {NULL}, }; diff --git a/Objects/object.c b/Objects/object.c index 0540112d7d2acf..6bebb8ea7e4191 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -713,7 +713,7 @@ _PyObject_IsFreed(PyObject *op) /* For debugging convenience. See Misc/gdbinit for some useful gdb hooks */ void -_PyObject_Dump(PyObject* op) +PyObject_Dump(PyObject* op) { if (_PyObject_IsFreed(op)) { /* It seems like the object memory has been freed: @@ -3150,7 +3150,7 @@ _PyObject_AssertFailed(PyObject *obj, const char *expr, const char *msg, /* This might succeed or fail, but we're about to abort, so at least try to provide any extra info we can: */ - _PyObject_Dump(obj); + PyObject_Dump(obj); fprintf(stderr, "\n"); fflush(stderr); diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 4e8c132327b7d0..d2a147b387c60f 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -547,7 +547,7 @@ unicode_check_encoding_errors(const char *encoding, const char *errors) } /* Disable checks during Python finalization. For example, it allows to - call _PyObject_Dump() during finalization for debugging purpose. */ + call PyObject_Dump() during finalization for debugging purpose. */ if (_PyInterpreterState_GetFinalizing(interp) != NULL) { return 0; } diff --git a/Python/gc.c b/Python/gc.c index 03a5d7366ea6c9..7ed0807eeecdac 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -2235,7 +2235,7 @@ _PyGC_Fini(PyInterpreterState *interp) void _PyGC_Dump(PyGC_Head *g) { - _PyObject_Dump(FROM_GC(g)); + PyObject_Dump(FROM_GC(g)); } diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 45211e1b075042..691cbdda8dba52 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1181,7 +1181,7 @@ _PyErr_Display(PyObject *file, PyObject *unused, PyObject *value, PyObject *tb) } if (print_exception_recursive(&ctx, value) < 0) { PyErr_Clear(); - _PyObject_Dump(value); + PyObject_Dump(value); fprintf(stderr, "lost sys.stderr\n"); } Py_XDECREF(ctx.seen); @@ -1199,14 +1199,14 @@ PyErr_Display(PyObject *unused, PyObject *value, PyObject *tb) PyObject *file; if (PySys_GetOptionalAttr(&_Py_ID(stderr), &file) < 0) { PyObject *exc = PyErr_GetRaisedException(); - _PyObject_Dump(value); + PyObject_Dump(value); fprintf(stderr, "lost sys.stderr\n"); - _PyObject_Dump(exc); + PyObject_Dump(exc); Py_DECREF(exc); return; } if (file == NULL) { - _PyObject_Dump(value); + PyObject_Dump(value); fprintf(stderr, "lost sys.stderr\n"); return; } From 8abec044976038ff73fad236a3801bb3fb368fb8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 5 Nov 2025 22:23:06 +0100 Subject: [PATCH 02/11] Fix doc formatting --- Doc/c-api/object.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index bc724ca451c4c2..02910f235d0f07 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -94,7 +94,9 @@ Object Protocol Implement an heuristic to detect if the object memory has been freed. - Example of output:: + Example of output: + + .. code-block:: output object address : 0x7f80124702c0 object refcount : 2 From 4338ac07eb7667aa7f4e1f44b187525804e98858 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 5 Nov 2025 23:10:10 +0100 Subject: [PATCH 03/11] Try to fix test on Windows --- Lib/test/test_capi/test_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 127bac0aaa32d3..0eeaf03d7a8dcf 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -275,7 +275,7 @@ def test_pyobject_dump(self): with open(filename) as fp: output = fp.read() - hex_regex = r'0x[0-9a-fA-F]+' + hex_regex = r'(0x)?[0-9a-fA-F]+' self.assertRegex(output.rstrip(), fr"object address : {hex_regex}\n" r"object refcount : [0-9]+\n" From 2e982a177ee640f1605047968765aaa0aecd1bef Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 00:18:08 +0100 Subject: [PATCH 04/11] Update Doc/c-api/object.rst Co-authored-by: Peter Bierma --- Doc/c-api/object.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 02910f235d0f07..575e654f032cf8 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -92,7 +92,7 @@ Object Protocol It can be called without an :term:`attached thread state`, even if it's not recommended. - Implement an heuristic to detect if the object memory has been freed. + Implement a heuristic to detect if the object memory has been freed. Example of output: From e4c7cdc15e7fa1ab530ee5aa5747fd6b50ea37bb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 00:18:34 +0100 Subject: [PATCH 05/11] Update Doc/c-api/object.rst Co-authored-by: Peter Bierma --- Doc/c-api/object.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 575e654f032cf8..73e8693f8b1a7f 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -87,7 +87,7 @@ Object Protocol .. c:function:: void PyObject_Dump(PyObject* op) - Dump an object *op* to ``stderr``. Function used for debugging. + Dump an object *op* to ``stderr``. This should only be used for debugging. It can be called without an :term:`attached thread state`, even if it's not recommended. From c65db8f1df5baf66d38f66ba44b384d21fa95796 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 00:19:22 +0100 Subject: [PATCH 06/11] Update Doc/c-api/object.rst Co-authored-by: Peter Bierma --- Doc/c-api/object.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 73e8693f8b1a7f..3b4596153a939d 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -89,8 +89,8 @@ Object Protocol Dump an object *op* to ``stderr``. This should only be used for debugging. - It can be called without an :term:`attached thread state`, even if it's not - recommended. + This function can be called without an :term:`attached thread state`, but it's not + recommended to do so. Implement a heuristic to detect if the object memory has been freed. From 94b9315408c54efd03fe959322a7b4b751fb599a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 12:07:12 +0100 Subject: [PATCH 07/11] Update Doc/c-api/object.rst Co-authored-by: Kumar Aditya --- Doc/c-api/object.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 3b4596153a939d..ebc19f9c84c9a8 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -85,7 +85,7 @@ Object Protocol instead of the :func:`repr`. -.. c:function:: void PyObject_Dump(PyObject* op) +.. c:function:: void PyObject_Dump(PyObject *op) Dump an object *op* to ``stderr``. This should only be used for debugging. From 423d25aca282a9db9f154854c9cd7560c5490065 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 12:17:33 +0100 Subject: [PATCH 08/11] Enhance the doc Co-authored-by: Petr Viktorin --- Doc/c-api/object.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index ebc19f9c84c9a8..7ee7ca9c0528ff 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -89,10 +89,16 @@ Object Protocol Dump an object *op* to ``stderr``. This should only be used for debugging. - This function can be called without an :term:`attached thread state`, but it's not - recommended to do so. - - Implement a heuristic to detect if the object memory has been freed. + The output is intended to try dumping objects even after memory corruption: + + * Information is written starting with fields that are the least likely to + crash when accessed. + * This function can be called without an :term:`attached thread state`, but + it's not recommended to do so: it can cause deadlocks. + * An object that does not belong to the current interpreter may be dumped, + but this may also cause crashes or unintended behavior. + * Implement a heuristic to detect if the object memory has been freed. Don't + display the object contents in this case, only its memory address. Example of output: From f64a8120f014a1d07d33d868bb4f3b0845447c12 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 12:16:29 +0100 Subject: [PATCH 09/11] Enhance tests: test NULL and release the GIL --- Lib/test/test_capi/test_object.py | 46 +++++++++++++++++++++---------- Modules/_testcapi/object.c | 22 +++++++++++++-- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 0eeaf03d7a8dcf..18db91f89be0a4 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -14,6 +14,7 @@ _testcapi = import_helper.import_module('_testcapi') _testinternalcapi = import_helper.import_module('_testinternalcapi') +NULL = None STDERR_FD = 2 @@ -250,12 +251,8 @@ def func(x): func(object()) - def test_pyobject_dump(self): + def pyobject_dump(self, obj, release_gil=False): pyobject_dump = _testcapi.pyobject_dump - obj = 'test string' - - filename = os_helper.TESTFN - self.addCleanup(os_helper.unlink, filename) try: old_stderr = os.dup(STDERR_FD) @@ -263,26 +260,45 @@ def test_pyobject_dump(self): # os.dup(STDERR_FD) is not supported on WASI self.skipTest(f"os.dup() failed with {exc!r}") + filename = os_helper.TESTFN try: - with open(filename, "wb") as fp: - fd = fp.fileno() - os.dup2(fd, STDERR_FD) - pyobject_dump(obj) + try: + with open(filename, "wb") as fp: + fd = fp.fileno() + os.dup2(fd, STDERR_FD) + pyobject_dump(obj, release_gil) + finally: + os.dup2(old_stderr, STDERR_FD) + os.close(old_stderr) + + with open(filename) as fp: + return fp.read().rstrip() finally: - os.dup2(old_stderr, STDERR_FD) - os.close(old_stderr) - - with open(filename) as fp: - output = fp.read() + os_helper.unlink(filename) + def test_pyobject_dump(self): + # test string object + str_obj = 'test string' + output = self.pyobject_dump(str_obj) hex_regex = r'(0x)?[0-9a-fA-F]+' - self.assertRegex(output.rstrip(), + regex = ( fr"object address : {hex_regex}\n" r"object refcount : [0-9]+\n" fr"object type : {hex_regex}\n" r"object type name: str\n" r"object repr : 'test string'" ) + self.assertRegex(output, regex) + + # release the GIL + output = self.pyobject_dump(str_obj, release_gil=True) + self.assertRegex(output, regex) + + # test NULL object + output = self.pyobject_dump(NULL) + hex_regex = r'(0x)?[0-9a-fA-F]+' + self.assertEqual(output, + '') if __name__ == "__main__": diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index bae01ea817416b..7603d512f2d815 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -486,9 +486,25 @@ is_uniquely_referenced(PyObject *self, PyObject *op) static PyObject * -pyobject_dump(PyObject *self, PyObject *op) +pyobject_dump(PyObject *self, PyObject *args) { - PyObject_Dump(op); + PyObject *op; + int release_gil = 0; + + if (!PyArg_ParseTuple(args, "O|i", &op, &release_gil)) { + return NULL; + } + NULLABLE(op); + + if (release_gil) { + Py_BEGIN_ALLOW_THREADS + PyObject_Dump(op); + Py_END_ALLOW_THREADS + + } + else { + PyObject_Dump(op); + } Py_RETURN_NONE; } @@ -519,7 +535,7 @@ static PyMethodDef test_methods[] = { {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, - {"pyobject_dump", pyobject_dump, METH_O}, + {"pyobject_dump", pyobject_dump, METH_VARARGS}, {NULL}, }; From bf279d3b7b687007a150d0baa85af99e6f7123d4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 12:26:21 +0100 Subject: [PATCH 10/11] The output format may change at any time --- Doc/c-api/object.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 7ee7ca9c0528ff..aab5c2175eade0 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -99,6 +99,7 @@ Object Protocol but this may also cause crashes or unintended behavior. * Implement a heuristic to detect if the object memory has been freed. Don't display the object contents in this case, only its memory address. + * The output format may change at any time. Example of output: From 70f543300d1524f894ae50fb6ae4708751553148 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Nov 2025 12:45:18 +0100 Subject: [PATCH 11/11] Fix NULL test on macOS and Windows --- Lib/test/test_capi/test_object.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 18db91f89be0a4..debd46ce2b4423 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -297,8 +297,8 @@ def test_pyobject_dump(self): # test NULL object output = self.pyobject_dump(NULL) hex_regex = r'(0x)?[0-9a-fA-F]+' - self.assertEqual(output, - '') + self.assertRegex(output, + r'') if __name__ == "__main__":