Skip to content

Commit a88c6be

Browse files
committed
Add (slow) stack ref debug mode to locate stackref leaks and double frees.
1 parent 48c70b8 commit a88c6be

File tree

8 files changed

+318
-14
lines changed

8 files changed

+318
-14
lines changed

Include/internal/pycore_interp.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extern "C" {
3434
#include "pycore_optimizer.h" // _PyOptimizerObject
3535
#include "pycore_obmalloc.h" // struct _obmalloc_state
3636
#include "pycore_qsbr.h" // struct _qsbr_state
37+
#include "pycore_stackref.h" // Py_STACKREF_DEBUG
3738
#include "pycore_tstate.h" // _PyThreadStateImpl
3839
#include "pycore_tuple.h" // struct _Py_tuple_state
3940
#include "pycore_uniqueid.h" // struct _Py_unique_id_pool
@@ -285,6 +286,11 @@ struct _is {
285286
_PyThreadStateImpl _initial_thread;
286287
// _initial_thread should be the last field of PyInterpreterState.
287288
// See https://github.com/python/cpython/issues/127117.
289+
290+
#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG)
291+
uint64_t next_stackref;
292+
_Py_hashtable_t *stackref_debug_table;
293+
#endif
288294
};
289295

290296

Include/internal/pycore_stackref.h

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
extern "C" {
55
#endif
66

7+
// Define this to get precise tracking of stackrefs.
8+
#define Py_STACKREF_DEBUG 1
9+
710
#ifndef Py_BUILD_CORE
811
# error "this header requires Py_BUILD_CORE define"
912
#endif
@@ -49,6 +52,105 @@ extern "C" {
4952
CPython refcounting operations on it!
5053
*/
5154

55+
56+
#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG)
57+
58+
typedef union _PyStackRef {
59+
uint64_t index;
60+
} _PyStackRef;
61+
62+
#define Py_TAG_BITS 0
63+
64+
PyAPI_FUNC(PyObject *) _Py_stackref_get_object(_PyStackRef ref);
65+
PyAPI_FUNC(PyObject *) _Py_stackref_close(_PyStackRef ref);
66+
PyAPI_FUNC(_PyStackRef) _Py_stackref_create(PyObject *obj, const char *filename, int linenumber);
67+
extern void _Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef ref);
68+
69+
static const _PyStackRef PyStackRef_NULL = { .index = 0 };
70+
71+
#define PyStackRef_None ((_PyStackRef){ .index = 1 } )
72+
#define PyStackRef_False ((_PyStackRef){ .index = 2 })
73+
#define PyStackRef_True ((_PyStackRef){ .index = 3 })
74+
75+
static inline int
76+
PyStackRef_IsNull(_PyStackRef ref)
77+
{
78+
return ref.index == 0;
79+
}
80+
81+
static inline int
82+
PyStackRef_IsTrue(_PyStackRef ref)
83+
{
84+
return _Py_stackref_get_object(ref) == Py_True;
85+
}
86+
87+
static inline int
88+
PyStackRef_IsFalse(_PyStackRef ref)
89+
{
90+
return _Py_stackref_get_object(ref) == Py_False;
91+
}
92+
93+
static inline int
94+
PyStackRef_IsNone(_PyStackRef ref)
95+
{
96+
return _Py_stackref_get_object(ref) == Py_None;
97+
}
98+
99+
static inline PyObject *
100+
PyStackRef_AsPyObjectBorrow(_PyStackRef ref)
101+
{
102+
return _Py_stackref_get_object(ref);
103+
}
104+
105+
static inline PyObject *
106+
PyStackRef_AsPyObjectSteal(_PyStackRef ref)
107+
{
108+
return _Py_stackref_close(ref);
109+
}
110+
111+
static inline _PyStackRef
112+
_PyStackRef_FromPyObjectNew(PyObject *obj, const char *filename, int linenumber)
113+
{
114+
Py_INCREF(obj);
115+
return _Py_stackref_create(obj, filename, linenumber);
116+
}
117+
#define PyStackRef_FromPyObjectNew(obj) _PyStackRef_FromPyObjectNew(_PyObject_CAST(obj), __FILE__, __LINE__)
118+
119+
static inline _PyStackRef
120+
_PyStackRef_FromPyObjectSteal(PyObject *obj, const char *filename, int linenumber)
121+
{
122+
return _Py_stackref_create(obj, filename, linenumber);
123+
}
124+
#define PyStackRef_FromPyObjectSteal(obj) _PyStackRef_FromPyObjectSteal(_PyObject_CAST(obj), __FILE__, __LINE__)
125+
126+
static inline _PyStackRef
127+
_PyStackRef_FromPyObjectImmortal(PyObject *obj, const char *filename, int linenumber)
128+
{
129+
assert(_Py_IsImmortal(obj));
130+
return _Py_stackref_create(obj, filename, linenumber);
131+
}
132+
#define PyStackRef_FromPyObjectImmortal(obj) _PyStackRef_FromPyObjectImmortal(_PyObject_CAST(obj), __FILE__, __LINE__)
133+
134+
static inline void
135+
PyStackRef_CLOSE(_PyStackRef ref)
136+
{
137+
PyObject *obj = _Py_stackref_close(ref);
138+
Py_DECREF(obj);
139+
}
140+
141+
static inline _PyStackRef
142+
_PyStackRef_DUP(_PyStackRef ref, const char *filename, int linenumber)
143+
{
144+
PyObject *obj = _Py_stackref_get_object(ref);
145+
Py_INCREF(obj);
146+
return _Py_stackref_create(obj, filename, linenumber);
147+
}
148+
#define PyStackRef_DUP(REF) _PyStackRef_DUP(REF, __FILE__, __LINE__)
149+
150+
#define PyStackRef_CLOSE_SPECIALIZED(stackref, dealloc) PyStackRef_CLOSE(stackref)
151+
152+
#else
153+
52154
typedef union _PyStackRef {
53155
uintptr_t bits;
54156
} _PyStackRef;
@@ -200,12 +302,15 @@ static const _PyStackRef PyStackRef_NULL = { .bits = 0 };
200302
#define PyStackRef_IsTrue(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_True)
201303
#define PyStackRef_IsFalse(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_False)
202304

305+
#endif
306+
203307
// Converts a PyStackRef back to a PyObject *, converting the
204308
// stackref to a new reference.
205309
#define PyStackRef_AsPyObjectNew(stackref) Py_NewRef(PyStackRef_AsPyObjectBorrow(stackref))
206310

207311
#define PyStackRef_TYPE(stackref) Py_TYPE(PyStackRef_AsPyObjectBorrow(stackref))
208312

313+
209314
#define PyStackRef_CLEAR(op) \
210315
do { \
211316
_PyStackRef *_tmp_op_ptr = &(op); \

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ PYTHON_OBJS= \
488488
Python/qsbr.o \
489489
Python/bootstrap_hash.o \
490490
Python/specialize.o \
491+
Python/stackrefs.o \
491492
Python/structmember.o \
492493
Python/symtable.o \
493494
Python/sysmodule.o \

Objects/frameobject.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ framelocalsproxy_setitem(PyObject *self, PyObject *key, PyObject *value)
179179
if (kind == CO_FAST_FREE) {
180180
// The cell was set when the frame was created from
181181
// the function's closure.
182-
assert(oldvalue.bits != 0 && PyCell_Check(PyStackRef_AsPyObjectBorrow(oldvalue)));
182+
assert(!PyStackRef_IsNull(oldvalue) && PyCell_Check(PyStackRef_AsPyObjectBorrow(oldvalue)));
183183
cell = PyStackRef_AsPyObjectBorrow(oldvalue);
184-
} else if (kind & CO_FAST_CELL && oldvalue.bits != 0) {
184+
} else if (kind & CO_FAST_CELL && !PyStackRef_IsNull(oldvalue)) {
185185
PyObject *as_obj = PyStackRef_AsPyObjectBorrow(oldvalue);
186186
if (PyCell_Check(as_obj)) {
187187
cell = as_obj;

Python/ceval.c

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ dump_stack(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer)
164164
PyErr_Clear();
165165
}
166166
// Don't call __repr__(), it might recurse into the interpreter.
167-
printf("<%s at %p>", Py_TYPE(obj)->tp_name, (void *)(ptr->bits));
167+
printf("<%s at %p>", Py_TYPE(obj)->tp_name, PyStackRef_AsPyObjectBorrow(*ptr));
168168
}
169169
printf("]\n");
170170
fflush(stdout);
@@ -805,7 +805,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
805805

806806

807807

808-
#ifdef Py_DEBUG
808+
#if defined(Py_DEBUG) && !defined(Py_STACKREF_DEBUG)
809809
/* Set these to invalid but identifiable values for debugging. */
810810
entry_frame.f_funcobj = (_PyStackRef){.bits = 0xaaa0};
811811
entry_frame.f_locals = (PyObject*)0xaaa1;
@@ -1817,12 +1817,27 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func,
18171817
PyStackRef_CLOSE(func);
18181818
goto error;
18191819
}
1820+
size_t total_args = nargs + PyDict_GET_SIZE(kwargs);
1821+
for (size_t i = 0; i < total_args; i++) {
1822+
((_PyStackRef *)newargs)[i] = PyStackRef_FromPyObjectSteal(newargs[i]);
1823+
}
18201824
}
18211825
else {
1822-
newargs = &PyTuple_GET_ITEM(callargs, 0);
1823-
/* We need to incref all our args since the new frame steals the references. */
1824-
for (Py_ssize_t i = 0; i < nargs; ++i) {
1825-
Py_INCREF(PyTuple_GET_ITEM(callargs, i));
1826+
if (nargs <= 8) {
1827+
PyObject *stack_array[8];
1828+
newargs = stack_array;
1829+
}
1830+
else {
1831+
newargs = PyMem_Malloc(sizeof(PyObject *) *nargs);
1832+
if (newargs == NULL) {
1833+
PyErr_NoMemory();
1834+
PyStackRef_CLOSE(func);
1835+
goto error;
1836+
}
1837+
}
1838+
/* We need to create a new reference for all our args since the new frame steals them. */
1839+
for (Py_ssize_t i = 0; i < nargs; i++) {
1840+
((_PyStackRef *)newargs)[i] = PyStackRef_FromPyObjectNew(PyTuple_GET_ITEM(callargs, i));
18261841
}
18271842
}
18281843
_PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit(
@@ -1832,6 +1847,9 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func,
18321847
if (has_dict) {
18331848
_PyStack_UnpackDict_FreeNoDecRef(newargs, kwnames);
18341849
}
1850+
else if (nargs > 8) {
1851+
PyMem_Free((void *)newargs);
1852+
}
18351853
/* No need to decref func here because the reference has been stolen by
18361854
_PyEvalFramePushAndInit.
18371855
*/
@@ -1850,21 +1868,39 @@ _PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
18501868
PyObject* const* args, size_t argcount,
18511869
PyObject *kwnames)
18521870
{
1871+
size_t total_args = argcount;
1872+
if (kwnames) {
1873+
total_args += PyTuple_GET_SIZE(kwnames);
1874+
}
1875+
_PyStackRef stack_array[8];
1876+
_PyStackRef *arguments;
1877+
if (total_args <= 8) {
1878+
arguments = stack_array;
1879+
}
1880+
else {
1881+
arguments = PyMem_Malloc(sizeof(_PyStackRef) * total_args);
1882+
if (arguments == NULL) {
1883+
return PyErr_NoMemory();
1884+
}
1885+
}
18531886
/* _PyEvalFramePushAndInit consumes the references
18541887
* to func, locals and all its arguments */
18551888
Py_XINCREF(locals);
18561889
for (size_t i = 0; i < argcount; i++) {
1857-
Py_INCREF(args[i]);
1890+
arguments[i] = PyStackRef_FromPyObjectNew(args[i]);
18581891
}
18591892
if (kwnames) {
18601893
Py_ssize_t kwcount = PyTuple_GET_SIZE(kwnames);
18611894
for (Py_ssize_t i = 0; i < kwcount; i++) {
1862-
Py_INCREF(args[i+argcount]);
1895+
arguments[i+argcount] = PyStackRef_FromPyObjectNew(args[i+argcount]);
18631896
}
18641897
}
18651898
_PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
18661899
tstate, PyStackRef_FromPyObjectNew(func), locals,
1867-
(_PyStackRef const *)args, argcount, kwnames, NULL);
1900+
arguments, argcount, kwnames, NULL);
1901+
if (total_args > 8) {
1902+
PyMem_Free(arguments);
1903+
}
18681904
if (frame == NULL) {
18691905
return NULL;
18701906
}

Python/ceval_macros.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ do { \
450450
/* How much scratch space to give stackref to PyObject* conversion. */
451451
#define MAX_STACKREF_SCRATCH 10
452452

453-
#ifdef Py_GIL_DISABLED
453+
#if defined(Py_GIL_DISABLED) || defined(Py_STACKREF_DEBUG)
454454
#define STACKREFS_TO_PYOBJECTS(ARGS, ARG_COUNT, NAME) \
455455
/* +1 because vectorcall might use -1 to write self */ \
456456
PyObject *NAME##_temp[MAX_STACKREF_SCRATCH+1]; \
@@ -461,7 +461,7 @@ do { \
461461
assert(NAME != NULL);
462462
#endif
463463

464-
#ifdef Py_GIL_DISABLED
464+
#if defined(Py_GIL_DISABLED) || defined(Py_STACKREF_DEBUG)
465465
#define STACKREFS_TO_PYOBJECTS_CLEANUP(NAME) \
466466
/* +1 because we +1 previously */ \
467467
_PyObjectArray_Free(NAME - 1, NAME##_temp);
@@ -470,7 +470,7 @@ do { \
470470
(void)(NAME);
471471
#endif
472472

473-
#ifdef Py_GIL_DISABLED
473+
#if defined(Py_GIL_DISABLED) || defined(Py_STACKREF_DEBUG)
474474
#define CONVERSION_FAILED(NAME) ((NAME) == NULL)
475475
#else
476476
#define CONVERSION_FAILED(NAME) (0)

Python/pystate.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "pycore_pymem.h" // _PyMem_SetDefaultAllocator()
2020
#include "pycore_pystate.h"
2121
#include "pycore_runtime_init.h" // _PyRuntimeState_INIT
22+
#include "pycore_stackref.h" // Py_STACKREF_DEBUG
2223
#include "pycore_obmalloc.h" // _PyMem_obmalloc_state_on_heap()
2324
#include "pycore_uniqueid.h" // _PyObject_FinalizePerThreadRefcounts()
2425

@@ -663,6 +664,23 @@ init_interpreter(PyInterpreterState *interp,
663664
/* Fix the self-referential, statically initialized fields. */
664665
interp->dtoa = (struct _dtoa_state)_dtoa_state_INIT(interp);
665666
}
667+
#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG)
668+
interp->next_stackref = 1;
669+
_Py_hashtable_allocator_t alloc = {
670+
.malloc = malloc,
671+
.free = free,
672+
};
673+
interp->stackref_debug_table = _Py_hashtable_new_full(
674+
_Py_hashtable_hash_ptr,
675+
_Py_hashtable_compare_direct,
676+
NULL,
677+
NULL,
678+
&alloc
679+
);
680+
_Py_stackref_associate(interp, Py_None, PyStackRef_None);
681+
_Py_stackref_associate(interp, Py_False, PyStackRef_False);
682+
_Py_stackref_associate(interp, Py_True, PyStackRef_True);
683+
#endif
666684

667685
interp->_initialized = 1;
668686
return _PyStatus_OK();
@@ -768,6 +786,11 @@ PyInterpreterState_New(void)
768786
return interp;
769787
}
770788

789+
#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG)
790+
extern void
791+
_Py_stackref_report_leaks(PyInterpreterState *interp);
792+
#endif
793+
771794
static void
772795
interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
773796
{
@@ -877,6 +900,12 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
877900
Py_CLEAR(interp->sysdict);
878901
Py_CLEAR(interp->builtins);
879902

903+
#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG)
904+
_Py_stackref_report_leaks(interp);
905+
_Py_hashtable_destroy(interp->stackref_debug_table);
906+
interp->stackref_debug_table = NULL;
907+
#endif
908+
880909
if (tstate->interp == interp) {
881910
/* We are now safe to fix tstate->_status.cleared. */
882911
// XXX Do this (much) earlier?

0 commit comments

Comments
 (0)