mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
bpo-9263: Dump Python object on GC assertion failure (GH-10062)
Changes: * Add _PyObject_AssertFailed() function. * Add _PyObject_ASSERT() and _PyObject_ASSERT_WITH_MSG() macros. * gc_decref(): replace assert() with _PyObject_ASSERT_WITH_MSG() to dump the faulty object if the assertion fails. _PyObject_AssertFailed() calls: * _PyMem_DumpTraceback(): try to log the traceback where the object memory has been allocated if tracemalloc is enabled. * _PyObject_Dump(): log repr(obj). * Py_FatalError(): log the current Python traceback. _PyObject_AssertFailed() uses _PyObject_IsFreed() heuristic to check if the object memory has been freed by a debug hook on Python memory allocators. Initial patch written by David Malcolm. Co-Authored-By: David Malcolm <dmalcolm@redhat.com>
This commit is contained in:
parent
18618e652c
commit
626bff8568
6 changed files with 180 additions and 14 deletions
|
@ -1105,6 +1105,53 @@ PyAPI_FUNC(void)
|
||||||
_PyObject_DebugTypeStats(FILE *out);
|
_PyObject_DebugTypeStats(FILE *out);
|
||||||
#endif /* ifndef Py_LIMITED_API */
|
#endif /* ifndef Py_LIMITED_API */
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef Py_LIMITED_API
|
||||||
|
/* Define a pair of assertion macros:
|
||||||
|
_PyObject_ASSERT_WITH_MSG() and _PyObject_ASSERT().
|
||||||
|
|
||||||
|
These work like the regular C assert(), in that they will abort the
|
||||||
|
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.
|
||||||
|
|
||||||
|
The WITH_MSG variant allows you to supply an additional message that Python
|
||||||
|
will attempt to print to stderr, after the object dump. */
|
||||||
|
#ifdef NDEBUG
|
||||||
|
/* No debugging: compile away the assertions: */
|
||||||
|
# define _PyObject_ASSERT_WITH_MSG(obj, expr, msg) ((void)0)
|
||||||
|
#else
|
||||||
|
/* With debugging: generate checks: */
|
||||||
|
# define _PyObject_ASSERT_WITH_MSG(obj, expr, msg) \
|
||||||
|
((expr) \
|
||||||
|
? (void)(0) \
|
||||||
|
: _PyObject_AssertFailed((obj), \
|
||||||
|
(msg), \
|
||||||
|
Py_STRINGIFY(expr), \
|
||||||
|
__FILE__, \
|
||||||
|
__LINE__, \
|
||||||
|
__func__))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define _PyObject_ASSERT(obj, expr) _PyObject_ASSERT_WITH_MSG(obj, expr, NULL)
|
||||||
|
|
||||||
|
/* Declare and define _PyObject_AssertFailed() even when NDEBUG is defined,
|
||||||
|
to avoid causing compiler/linker errors when building extensions without
|
||||||
|
NDEBUG against a Python built with NDEBUG defined. */
|
||||||
|
PyAPI_FUNC(void) _PyObject_AssertFailed(
|
||||||
|
PyObject *obj,
|
||||||
|
const char *msg,
|
||||||
|
const char *expr,
|
||||||
|
const char *file,
|
||||||
|
int line,
|
||||||
|
const char *function);
|
||||||
|
#endif /* ifndef Py_LIMITED_API */
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -196,7 +196,7 @@ PyAPI_FUNC(void) PyMem_SetAllocator(PyMemAllocatorDomain domain,
|
||||||
|
|
||||||
The function does nothing if Python is not compiled is debug mode. */
|
The function does nothing if Python is not compiled is debug mode. */
|
||||||
PyAPI_FUNC(void) PyMem_SetupDebugHooks(void);
|
PyAPI_FUNC(void) PyMem_SetupDebugHooks(void);
|
||||||
#endif
|
#endif /* Py_LIMITED_API */
|
||||||
|
|
||||||
#ifdef Py_BUILD_CORE
|
#ifdef Py_BUILD_CORE
|
||||||
/* Set the memory allocator of the specified domain to the default.
|
/* Set the memory allocator of the specified domain to the default.
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import unittest
|
import unittest
|
||||||
from test.support import (verbose, refcount_test, run_unittest,
|
from test.support import (verbose, refcount_test, run_unittest,
|
||||||
strip_python_stderr, cpython_only, start_threads,
|
strip_python_stderr, cpython_only, start_threads,
|
||||||
temp_dir, requires_type_collecting, TESTFN, unlink)
|
temp_dir, requires_type_collecting, TESTFN, unlink,
|
||||||
|
import_module)
|
||||||
from test.support.script_helper import assert_python_ok, make_script
|
from test.support.script_helper import assert_python_ok, make_script
|
||||||
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import gc
|
import gc
|
||||||
import weakref
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
import weakref
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from _testcapi import with_tp_del
|
from _testcapi import with_tp_del
|
||||||
|
@ -62,6 +65,14 @@ class Uncollectable(object):
|
||||||
def __tp_del__(self):
|
def __tp_del__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if sysconfig.get_config_vars().get('PY_CFLAGS', ''):
|
||||||
|
BUILD_WITH_NDEBUG = ('-DNDEBUG' in sysconfig.get_config_vars()['PY_CFLAGS'])
|
||||||
|
else:
|
||||||
|
# Usually, sys.gettotalrefcount() is only present if Python has been
|
||||||
|
# compiled in debug mode. If it's missing, expect that Python has
|
||||||
|
# been released in release mode: with NDEBUG defined.
|
||||||
|
BUILD_WITH_NDEBUG = (not hasattr(sys, 'gettotalrefcount'))
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -878,6 +889,58 @@ class GCCallbackTests(unittest.TestCase):
|
||||||
self.assertEqual(len(gc.garbage), 0)
|
self.assertEqual(len(gc.garbage), 0)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipIf(BUILD_WITH_NDEBUG,
|
||||||
|
'built with -NDEBUG')
|
||||||
|
def test_refcount_errors(self):
|
||||||
|
self.preclean()
|
||||||
|
# Verify the "handling" of objects with broken refcounts
|
||||||
|
|
||||||
|
# Skip the test if ctypes is not available
|
||||||
|
import_module("ctypes")
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
code = textwrap.dedent('''
|
||||||
|
from test.support import gc_collect, SuppressCrashReport
|
||||||
|
|
||||||
|
a = [1, 2, 3]
|
||||||
|
b = [a]
|
||||||
|
|
||||||
|
# Avoid coredump when Py_FatalError() calls abort()
|
||||||
|
SuppressCrashReport().__enter__()
|
||||||
|
|
||||||
|
# Simulate the refcount of "a" being too low (compared to the
|
||||||
|
# references held on it by live data), but keeping it above zero
|
||||||
|
# (to avoid deallocating it):
|
||||||
|
import ctypes
|
||||||
|
ctypes.pythonapi.Py_DecRef(ctypes.py_object(a))
|
||||||
|
|
||||||
|
# The garbage collector should now have a fatal error
|
||||||
|
# when it reaches the broken object
|
||||||
|
gc_collect()
|
||||||
|
''')
|
||||||
|
p = subprocess.Popen([sys.executable, "-c", code],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
p.stdout.close()
|
||||||
|
p.stderr.close()
|
||||||
|
# Verify that stderr has a useful error message:
|
||||||
|
self.assertRegex(stderr,
|
||||||
|
br'gcmodule\.c:[0-9]+: gc_decref: Assertion "gc_get_refs\(g\) > 0" failed.')
|
||||||
|
self.assertRegex(stderr,
|
||||||
|
br'refcount is too small')
|
||||||
|
self.assertRegex(stderr,
|
||||||
|
br'object : \[1, 2, 3\]')
|
||||||
|
self.assertRegex(stderr,
|
||||||
|
br'type : list')
|
||||||
|
self.assertRegex(stderr,
|
||||||
|
br'refcount: 1')
|
||||||
|
# "address : 0x7fb5062efc18"
|
||||||
|
# "address : 7FB5062EFC18"
|
||||||
|
self.assertRegex(stderr,
|
||||||
|
br'address : [0-9a-fA-Fx]+')
|
||||||
|
|
||||||
|
|
||||||
class GCTogglingTests(unittest.TestCase):
|
class GCTogglingTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
gc.enable()
|
gc.enable()
|
||||||
|
|
|
@ -1436,10 +1436,12 @@ _tracemalloc__get_object_traceback(PyObject *module, PyObject *obj)
|
||||||
traceback_t *traceback;
|
traceback_t *traceback;
|
||||||
|
|
||||||
type = Py_TYPE(obj);
|
type = Py_TYPE(obj);
|
||||||
if (PyType_IS_GC(type))
|
if (PyType_IS_GC(type)) {
|
||||||
ptr = (void *)((char *)obj - sizeof(PyGC_Head));
|
ptr = (void *)((char *)obj - sizeof(PyGC_Head));
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
ptr = (void *)obj;
|
ptr = (void *)obj;
|
||||||
|
}
|
||||||
|
|
||||||
traceback = tracemalloc_get_traceback(DEFAULT_DOMAIN, (uintptr_t)ptr);
|
traceback = tracemalloc_get_traceback(DEFAULT_DOMAIN, (uintptr_t)ptr);
|
||||||
if (traceback == NULL)
|
if (traceback == NULL)
|
||||||
|
|
|
@ -62,6 +62,12 @@ module gc
|
||||||
// most gc_list_* functions for it.
|
// most gc_list_* functions for it.
|
||||||
#define NEXT_MASK_UNREACHABLE (1)
|
#define NEXT_MASK_UNREACHABLE (1)
|
||||||
|
|
||||||
|
/* Get an object's GC head */
|
||||||
|
#define AS_GC(o) ((PyGC_Head *)(o)-1)
|
||||||
|
|
||||||
|
/* Get the object given the GC head */
|
||||||
|
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))
|
||||||
|
|
||||||
static inline int
|
static inline int
|
||||||
gc_is_collecting(PyGC_Head *g)
|
gc_is_collecting(PyGC_Head *g)
|
||||||
{
|
{
|
||||||
|
@ -98,16 +104,12 @@ gc_reset_refs(PyGC_Head *g, Py_ssize_t refs)
|
||||||
static inline void
|
static inline void
|
||||||
gc_decref(PyGC_Head *g)
|
gc_decref(PyGC_Head *g)
|
||||||
{
|
{
|
||||||
assert(gc_get_refs(g) > 0);
|
_PyObject_ASSERT_WITH_MSG(FROM_GC(g),
|
||||||
|
gc_get_refs(g) > 0,
|
||||||
|
"refcount is too small");
|
||||||
g->_gc_prev -= 1 << _PyGC_PREV_SHIFT;
|
g->_gc_prev -= 1 << _PyGC_PREV_SHIFT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get an object's GC head */
|
|
||||||
#define AS_GC(o) ((PyGC_Head *)(o)-1)
|
|
||||||
|
|
||||||
/* Get the object given the GC head */
|
|
||||||
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))
|
|
||||||
|
|
||||||
/* Python string to use if unhandled exception occurs */
|
/* Python string to use if unhandled exception occurs */
|
||||||
static PyObject *gc_str = NULL;
|
static PyObject *gc_str = NULL;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Defined in tracemalloc.c */
|
||||||
|
extern void _PyMem_DumpTraceback(int fd, const void *ptr);
|
||||||
|
|
||||||
_Py_IDENTIFIER(Py_Repr);
|
_Py_IDENTIFIER(Py_Repr);
|
||||||
_Py_IDENTIFIER(__bytes__);
|
_Py_IDENTIFIER(__bytes__);
|
||||||
_Py_IDENTIFIER(__dir__);
|
_Py_IDENTIFIER(__dir__);
|
||||||
|
@ -2212,6 +2215,55 @@ _PyTrash_thread_destroy_chain(void)
|
||||||
--tstate->trash_delete_nesting;
|
--tstate->trash_delete_nesting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
_PyObject_AssertFailed(PyObject *obj, const char *msg, const char *expr,
|
||||||
|
const char *file, int line, const char *function)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"%s:%d: %s: Assertion \"%s\" failed",
|
||||||
|
file, line, function, expr);
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
|
fprintf(stderr, "; %s.\n", msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fprintf(stderr, ".\n");
|
||||||
|
}
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
if (obj == NULL) {
|
||||||
|
fprintf(stderr, "<NULL object>\n");
|
||||||
|
}
|
||||||
|
else if (_PyObject_IsFreed(obj)) {
|
||||||
|
/* It seems like the object memory has been freed:
|
||||||
|
don't access it to prevent a segmentation fault. */
|
||||||
|
fprintf(stderr, "<Freed object>\n");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* Diplay the traceback where the object has been allocated.
|
||||||
|
Do it before dumping repr(obj), since repr() is more likely
|
||||||
|
to crash than dumping the traceback. */
|
||||||
|
void *ptr;
|
||||||
|
PyTypeObject *type = Py_TYPE(obj);
|
||||||
|
if (PyType_IS_GC(type)) {
|
||||||
|
ptr = (void *)((char *)obj - sizeof(PyGC_Head));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ptr = (void *)obj;
|
||||||
|
}
|
||||||
|
_PyMem_DumpTraceback(fileno(stderr), ptr);
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
Py_FatalError("_PyObject_AssertFailed");
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef Py_TRACE_REFS
|
#ifndef Py_TRACE_REFS
|
||||||
/* For Py_LIMITED_API, we need an out-of-line version of _Py_Dealloc.
|
/* For Py_LIMITED_API, we need an out-of-line version of _Py_Dealloc.
|
||||||
Define this here, so we can undefine the macro. */
|
Define this here, so we can undefine the macro. */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue