gh-141070: Add PyUnstable_Object_Dump() function (#141072)

* Promote _PyObject_Dump() as a public function.
* Keep _PyObject_Dump() alias to PyUnstable_Object_Dump()
  for backward compatibility.
* Replace _PyObject_Dump() with PyUnstable_Object_Dump().

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
Victor Stinner 2025-11-18 17:13:13 +01:00 committed by GitHub
parent 4695ec109d
commit 600f3feb23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 135 additions and 18 deletions

View file

@ -85,6 +85,35 @@ Object Protocol
instead of the :func:`repr`. instead of the :func:`repr`.
.. c:function:: void PyUnstable_Object_Dump(PyObject *op)
Dump an object *op* to ``stderr``. This should only be used for debugging.
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.
* The output format may change at any time.
Example of output:
.. code-block:: 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) .. c:function:: int PyObject_HasAttrWithError(PyObject *o, PyObject *attr_name)
Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise. Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise.

View file

@ -1084,19 +1084,23 @@ New features
(Contributed by Victor Stinner in :gh:`129813`.) (Contributed by Victor Stinner in :gh:`129813`.)
* Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
a module from a *spec* and *initfunc*.
(Contributed by Itamar Oren in :gh:`116146`.)
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
(Contributed by Victor Stinner in :gh:`111489`.) (Contributed by Victor Stinner in :gh:`111489`.)
* Add :c:func:`PyUnstable_Object_Dump` to dump an object to ``stderr``.
It should only be used for debugging.
(Contributed by Victor Stinner in :gh:`141070`.)
* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and * Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set :c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
the stack protection base address and stack protection size of a Python the stack protection base address and stack protection size of a Python
thread state. thread state.
(Contributed by Victor Stinner in :gh:`139653`.) (Contributed by Victor Stinner in :gh:`139653`.)
* Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
a module from a *spec* and *initfunc*.
(Contributed by Itamar Oren in :gh:`116146`.)
Changed C APIs Changed C APIs
-------------- --------------

View file

@ -295,7 +295,10 @@ PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *);
PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int);
PyAPI_FUNC(void) _Py_BreakPoint(void); PyAPI_FUNC(void) _Py_BreakPoint(void);
PyAPI_FUNC(void) _PyObject_Dump(PyObject *); PyAPI_FUNC(void) PyUnstable_Object_Dump(PyObject *);
// Alias for backward compatibility
#define _PyObject_Dump PyUnstable_Object_Dump
PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *);
@ -387,10 +390,11 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
process with a message on stderr if the given condition fails to hold, process with a message on stderr if the given condition fails to hold,
but compile away to nothing if NDEBUG is defined. 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
the given object. This may be of use when investigating bugs in which a PyUnstable_Object_Dump() on the given object. This may be of use when
particular object is corrupt (e.g. buggy a tp_visit method in an extension investigating bugs in which a particular object is corrupt (e.g. buggy a
module breaking the garbage collector), to help locate the broken objects. tp_visit method in an extension module breaking the garbage collector), to
help locate the broken objects.
The WITH_MSG variant allows you to supply an additional message that Python The WITH_MSG variant allows you to supply an additional message that Python
will attempt to print to stderr, after the object dump. */ will attempt to print to stderr, after the object dump. */

View file

@ -13,7 +13,7 @@ static inline void
_PyStaticObject_CheckRefcnt(PyObject *obj) { _PyStaticObject_CheckRefcnt(PyObject *obj) {
if (!_Py_IsImmortal(obj)) { if (!_Py_IsImmortal(obj)) {
fprintf(stderr, "Immortal Object has less refcnt than expected.\n"); fprintf(stderr, "Immortal Object has less refcnt than expected.\n");
_PyObject_Dump(obj); PyUnstable_Object_Dump(obj);
} }
} }
#endif #endif

View file

@ -1,4 +1,5 @@
import enum import enum
import os
import sys import sys
import textwrap import textwrap
import unittest import unittest
@ -13,6 +14,9 @@
_testcapi = import_helper.import_module('_testcapi') _testcapi = import_helper.import_module('_testcapi')
_testinternalcapi = import_helper.import_module('_testinternalcapi') _testinternalcapi = import_helper.import_module('_testinternalcapi')
NULL = None
STDERR_FD = 2
class Constant(enum.IntEnum): class Constant(enum.IntEnum):
Py_CONSTANT_NONE = 0 Py_CONSTANT_NONE = 0
@ -247,5 +251,53 @@ def func(x):
func(object()) func(object())
def pyobject_dump(self, obj, release_gil=False):
pyobject_dump = _testcapi.pyobject_dump
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}")
filename = os_helper.TESTFN
try:
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_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]+'
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)
self.assertRegex(output, r'<object at .* is freed>')
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -0,0 +1,2 @@
Add :c:func:`PyUnstable_Object_Dump` to dump an object to ``stderr``. It should
only be used for debugging. Patch by Victor Stinner.

View file

@ -485,6 +485,30 @@ is_uniquely_referenced(PyObject *self, PyObject *op)
} }
static PyObject *
pyobject_dump(PyObject *self, PyObject *args)
{
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
PyUnstable_Object_Dump(op);
Py_END_ALLOW_THREADS
}
else {
PyUnstable_Object_Dump(op);
}
Py_RETURN_NONE;
}
static PyMethodDef test_methods[] = { static PyMethodDef test_methods[] = {
{"call_pyobject_print", call_pyobject_print, METH_VARARGS}, {"call_pyobject_print", call_pyobject_print, METH_VARARGS},
{"pyobject_print_null", pyobject_print_null, METH_VARARGS}, {"pyobject_print_null", pyobject_print_null, METH_VARARGS},
@ -511,6 +535,7 @@ static PyMethodDef test_methods[] = {
{"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
{"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"clear_managed_dict", clear_managed_dict, METH_O, NULL},
{"is_uniquely_referenced", is_uniquely_referenced, METH_O}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O},
{"pyobject_dump", pyobject_dump, METH_VARARGS},
{NULL}, {NULL},
}; };

View file

@ -713,7 +713,7 @@ _PyObject_IsFreed(PyObject *op)
/* For debugging convenience. See Misc/gdbinit for some useful gdb hooks */ /* For debugging convenience. See Misc/gdbinit for some useful gdb hooks */
void void
_PyObject_Dump(PyObject* op) PyUnstable_Object_Dump(PyObject* op)
{ {
if (_PyObject_IsFreed(op)) { if (_PyObject_IsFreed(op)) {
/* It seems like the object memory has been freed: /* 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 /* This might succeed or fail, but we're about to abort, so at least
try to provide any extra info we can: */ try to provide any extra info we can: */
_PyObject_Dump(obj); PyUnstable_Object_Dump(obj);
fprintf(stderr, "\n"); fprintf(stderr, "\n");
fflush(stderr); fflush(stderr);

View file

@ -547,7 +547,8 @@ unicode_check_encoding_errors(const char *encoding, const char *errors)
} }
/* Disable checks during Python finalization. For example, it allows to /* Disable checks during Python finalization. For example, it allows to
call _PyObject_Dump() during finalization for debugging purpose. */ * call PyUnstable_Object_Dump() during finalization for debugging purpose.
*/
if (_PyInterpreterState_GetFinalizing(interp) != NULL) { if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
return 0; return 0;
} }

View file

@ -2237,7 +2237,7 @@ _PyGC_Fini(PyInterpreterState *interp)
void void
_PyGC_Dump(PyGC_Head *g) _PyGC_Dump(PyGC_Head *g)
{ {
_PyObject_Dump(FROM_GC(g)); PyUnstable_Object_Dump(FROM_GC(g));
} }

View file

@ -1181,7 +1181,7 @@ _PyErr_Display(PyObject *file, PyObject *unused, PyObject *value, PyObject *tb)
} }
if (print_exception_recursive(&ctx, value) < 0) { if (print_exception_recursive(&ctx, value) < 0) {
PyErr_Clear(); PyErr_Clear();
_PyObject_Dump(value); PyUnstable_Object_Dump(value);
fprintf(stderr, "lost sys.stderr\n"); fprintf(stderr, "lost sys.stderr\n");
} }
Py_XDECREF(ctx.seen); Py_XDECREF(ctx.seen);
@ -1199,14 +1199,14 @@ PyErr_Display(PyObject *unused, PyObject *value, PyObject *tb)
PyObject *file; PyObject *file;
if (PySys_GetOptionalAttr(&_Py_ID(stderr), &file) < 0) { if (PySys_GetOptionalAttr(&_Py_ID(stderr), &file) < 0) {
PyObject *exc = PyErr_GetRaisedException(); PyObject *exc = PyErr_GetRaisedException();
_PyObject_Dump(value); PyUnstable_Object_Dump(value);
fprintf(stderr, "lost sys.stderr\n"); fprintf(stderr, "lost sys.stderr\n");
_PyObject_Dump(exc); PyUnstable_Object_Dump(exc);
Py_DECREF(exc); Py_DECREF(exc);
return; return;
} }
if (file == NULL) { if (file == NULL) {
_PyObject_Dump(value); PyUnstable_Object_Dump(value);
fprintf(stderr, "lost sys.stderr\n"); fprintf(stderr, "lost sys.stderr\n");
return; return;
} }