Skip to content
Merged
29 changes: 29 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,35 @@ Object Protocol
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)

Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise.
Expand Down
12 changes: 8 additions & 4 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1084,19 +1084,23 @@ New features

(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`.)
Copy link
Member Author

Choose a reason for hiding this comment

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

Entries should be sorted.

Copy link
Member

Choose a reason for hiding this comment

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

I didn't know that. What's the sort key?

Copy link
Member Author

Choose a reason for hiding this comment

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

"Sorted alphabetically". @hugovk added comments like that in the What's New document:

.. Add C API deprecations above alphabetically, not here at the end.

We should add a similar comment for C API "New features". I think that it was @AA-Turner who started to sort items in What's New In Python 3.14.


* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
(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
:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
the stack protection base address and stack protection size of a Python
thread state.
(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
--------------
Expand Down
14 changes: 9 additions & 5 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) PyUnstable_Object_Dump(PyObject *);

// Alias for backward compatibility
#define _PyObject_Dump PyUnstable_Object_Dump

PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *);

Expand Down Expand Up @@ -387,10 +390,11 @@ 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
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.
However, before aborting, Python will also try to call
PyUnstable_Object_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.

The WITH_MSG variant allows you to supply an additional message that Python
will attempt to print to stderr, after the object dump. */
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enum
import os
import sys
import textwrap
import unittest
Expand All @@ -13,6 +14,9 @@
_testcapi = import_helper.import_module('_testcapi')
_testinternalcapi = import_helper.import_module('_testinternalcapi')

NULL = None
STDERR_FD = 2


class Constant(enum.IntEnum):
Py_CONSTANT_NONE = 0
Expand Down Expand Up @@ -247,5 +251,53 @@ def func(x):

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__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = {
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
{"pyobject_print_null", pyobject_print_null, METH_VARARGS},
Expand All @@ -511,6 +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_VARARGS},
{NULL},
};

Expand Down
4 changes: 2 additions & 2 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ _PyObject_IsFreed(PyObject *op)

/* For debugging convenience. See Misc/gdbinit for some useful gdb hooks */
void
_PyObject_Dump(PyObject* op)
PyUnstable_Object_Dump(PyObject* op)
{
if (_PyObject_IsFreed(op)) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add a special case for op == NULL?

And for type == NULL?

Copy link
Member Author

Choose a reason for hiding this comment

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

It displays freed in both cases. There is now an unit test for that.

How do you suggest to format op==NULL and/or type==NULL?

Copy link
Member

Choose a reason for hiding this comment

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

If this is already tested, then good. I was not sure that _PyObject_IsFreed() works with NULL (it could crash).

If type==NULL (very unlikely), then type->tp_name and repr() will crash. They will also crash if type was freed or if it is just a garbage. We could skip prints in that case avoid crash, although it may be too late.

Looking at the code, it seems the ideas I had (testing against the patterns used to fill the freed memory) was already implemented in _PyMem_IsPtrFreed().

Copy link
Member Author

Choose a reason for hiding this comment

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

If type==NULL (very unlikely), then type->tp_name and repr() will crash

_PyObject_IsFreed() checks _PyMem_IsPtrFreed(Py_TYPE(op)). We just display freed in this case, without displaying tp_name or trying to call repr().

/* It seems like the object memory has been freed:
Expand Down Expand Up @@ -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);
PyUnstable_Object_Dump(obj);

fprintf(stderr, "\n");
fflush(stderr);
Expand Down
3 changes: 2 additions & 1 deletion Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,8 @@ 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 PyUnstable_Object_Dump() during finalization for debugging purpose.
*/
if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
return 0;
}
Expand Down
2 changes: 1 addition & 1 deletion Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2237,7 +2237,7 @@ _PyGC_Fini(PyInterpreterState *interp)
void
_PyGC_Dump(PyGC_Head *g)
{
_PyObject_Dump(FROM_GC(g));
PyUnstable_Object_Dump(FROM_GC(g));
}


Expand Down
8 changes: 4 additions & 4 deletions Python/pythonrun.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
PyUnstable_Object_Dump(value);
fprintf(stderr, "lost sys.stderr\n");
}
Py_XDECREF(ctx.seen);
Expand All @@ -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);
PyUnstable_Object_Dump(value);
fprintf(stderr, "lost sys.stderr\n");
_PyObject_Dump(exc);
PyUnstable_Object_Dump(exc);
Py_DECREF(exc);
return;
}
if (file == NULL) {
_PyObject_Dump(value);
PyUnstable_Object_Dump(value);
fprintf(stderr, "lost sys.stderr\n");
return;
}
Expand Down
Loading