From a88c6be187e442f1ec45731c5c633f6075625928 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Thu, 19 Dec 2024 13:55:07 +0000 Subject: [PATCH 01/10] Add (slow) stack ref debug mode to locate stackref leaks and double frees. --- Include/internal/pycore_interp.h | 6 ++ Include/internal/pycore_stackref.h | 105 ++++++++++++++++++++++++ Makefile.pre.in | 1 + Objects/frameobject.c | 4 +- Python/ceval.c | 54 ++++++++++-- Python/ceval_macros.h | 6 +- Python/pystate.c | 29 +++++++ Python/stackrefs.c | 127 +++++++++++++++++++++++++++++ 8 files changed, 318 insertions(+), 14 deletions(-) create mode 100644 Python/stackrefs.c diff --git a/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h index 87cdcb5b119d15..a3c14dceffd7a0 100644 --- a/Include/internal/pycore_interp.h +++ b/Include/internal/pycore_interp.h @@ -34,6 +34,7 @@ extern "C" { #include "pycore_optimizer.h" // _PyOptimizerObject #include "pycore_obmalloc.h" // struct _obmalloc_state #include "pycore_qsbr.h" // struct _qsbr_state +#include "pycore_stackref.h" // Py_STACKREF_DEBUG #include "pycore_tstate.h" // _PyThreadStateImpl #include "pycore_tuple.h" // struct _Py_tuple_state #include "pycore_uniqueid.h" // struct _Py_unique_id_pool @@ -285,6 +286,11 @@ struct _is { _PyThreadStateImpl _initial_thread; // _initial_thread should be the last field of PyInterpreterState. // See https://github.com/python/cpython/issues/127117. + +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + uint64_t next_stackref; + _Py_hashtable_t *stackref_debug_table; +#endif }; diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index 90a3118352f7ae..8a278076cf1cf4 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -4,6 +4,9 @@ extern "C" { #endif +// Define this to get precise tracking of stackrefs. +#define Py_STACKREF_DEBUG 1 + #ifndef Py_BUILD_CORE # error "this header requires Py_BUILD_CORE define" #endif @@ -49,6 +52,105 @@ extern "C" { CPython refcounting operations on it! */ + +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + +typedef union _PyStackRef { + uint64_t index; +} _PyStackRef; + +#define Py_TAG_BITS 0 + +PyAPI_FUNC(PyObject *) _Py_stackref_get_object(_PyStackRef ref); +PyAPI_FUNC(PyObject *) _Py_stackref_close(_PyStackRef ref); +PyAPI_FUNC(_PyStackRef) _Py_stackref_create(PyObject *obj, const char *filename, int linenumber); +extern void _Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef ref); + +static const _PyStackRef PyStackRef_NULL = { .index = 0 }; + +#define PyStackRef_None ((_PyStackRef){ .index = 1 } ) +#define PyStackRef_False ((_PyStackRef){ .index = 2 }) +#define PyStackRef_True ((_PyStackRef){ .index = 3 }) + +static inline int +PyStackRef_IsNull(_PyStackRef ref) +{ + return ref.index == 0; +} + +static inline int +PyStackRef_IsTrue(_PyStackRef ref) +{ + return _Py_stackref_get_object(ref) == Py_True; +} + +static inline int +PyStackRef_IsFalse(_PyStackRef ref) +{ + return _Py_stackref_get_object(ref) == Py_False; +} + +static inline int +PyStackRef_IsNone(_PyStackRef ref) +{ + return _Py_stackref_get_object(ref) == Py_None; +} + +static inline PyObject * +PyStackRef_AsPyObjectBorrow(_PyStackRef ref) +{ + return _Py_stackref_get_object(ref); +} + +static inline PyObject * +PyStackRef_AsPyObjectSteal(_PyStackRef ref) +{ + return _Py_stackref_close(ref); +} + +static inline _PyStackRef +_PyStackRef_FromPyObjectNew(PyObject *obj, const char *filename, int linenumber) +{ + Py_INCREF(obj); + return _Py_stackref_create(obj, filename, linenumber); +} +#define PyStackRef_FromPyObjectNew(obj) _PyStackRef_FromPyObjectNew(_PyObject_CAST(obj), __FILE__, __LINE__) + +static inline _PyStackRef +_PyStackRef_FromPyObjectSteal(PyObject *obj, const char *filename, int linenumber) +{ + return _Py_stackref_create(obj, filename, linenumber); +} +#define PyStackRef_FromPyObjectSteal(obj) _PyStackRef_FromPyObjectSteal(_PyObject_CAST(obj), __FILE__, __LINE__) + +static inline _PyStackRef +_PyStackRef_FromPyObjectImmortal(PyObject *obj, const char *filename, int linenumber) +{ + assert(_Py_IsImmortal(obj)); + return _Py_stackref_create(obj, filename, linenumber); +} +#define PyStackRef_FromPyObjectImmortal(obj) _PyStackRef_FromPyObjectImmortal(_PyObject_CAST(obj), __FILE__, __LINE__) + +static inline void +PyStackRef_CLOSE(_PyStackRef ref) +{ + PyObject *obj = _Py_stackref_close(ref); + Py_DECREF(obj); +} + +static inline _PyStackRef +_PyStackRef_DUP(_PyStackRef ref, const char *filename, int linenumber) +{ + PyObject *obj = _Py_stackref_get_object(ref); + Py_INCREF(obj); + return _Py_stackref_create(obj, filename, linenumber); +} +#define PyStackRef_DUP(REF) _PyStackRef_DUP(REF, __FILE__, __LINE__) + +#define PyStackRef_CLOSE_SPECIALIZED(stackref, dealloc) PyStackRef_CLOSE(stackref) + +#else + typedef union _PyStackRef { uintptr_t bits; } _PyStackRef; @@ -200,12 +302,15 @@ static const _PyStackRef PyStackRef_NULL = { .bits = 0 }; #define PyStackRef_IsTrue(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_True) #define PyStackRef_IsFalse(ref) (PyStackRef_AsPyObjectBorrow(ref) == Py_False) +#endif + // Converts a PyStackRef back to a PyObject *, converting the // stackref to a new reference. #define PyStackRef_AsPyObjectNew(stackref) Py_NewRef(PyStackRef_AsPyObjectBorrow(stackref)) #define PyStackRef_TYPE(stackref) Py_TYPE(PyStackRef_AsPyObjectBorrow(stackref)) + #define PyStackRef_CLEAR(op) \ do { \ _PyStackRef *_tmp_op_ptr = &(op); \ diff --git a/Makefile.pre.in b/Makefile.pre.in index 3e880f7800fccf..67acf0fc520087 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -488,6 +488,7 @@ PYTHON_OBJS= \ Python/qsbr.o \ Python/bootstrap_hash.o \ Python/specialize.o \ + Python/stackrefs.o \ Python/structmember.o \ Python/symtable.o \ Python/sysmodule.o \ diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 03ed2b9480f8c9..10fd3a982c36f4 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -179,9 +179,9 @@ framelocalsproxy_setitem(PyObject *self, PyObject *key, PyObject *value) if (kind == CO_FAST_FREE) { // The cell was set when the frame was created from // the function's closure. - assert(oldvalue.bits != 0 && PyCell_Check(PyStackRef_AsPyObjectBorrow(oldvalue))); + assert(!PyStackRef_IsNull(oldvalue) && PyCell_Check(PyStackRef_AsPyObjectBorrow(oldvalue))); cell = PyStackRef_AsPyObjectBorrow(oldvalue); - } else if (kind & CO_FAST_CELL && oldvalue.bits != 0) { + } else if (kind & CO_FAST_CELL && !PyStackRef_IsNull(oldvalue)) { PyObject *as_obj = PyStackRef_AsPyObjectBorrow(oldvalue); if (PyCell_Check(as_obj)) { cell = as_obj; diff --git a/Python/ceval.c b/Python/ceval.c index fd891d7839151e..ec4fa560ea1b6f 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -164,7 +164,7 @@ dump_stack(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer) PyErr_Clear(); } // Don't call __repr__(), it might recurse into the interpreter. - printf("<%s at %p>", Py_TYPE(obj)->tp_name, (void *)(ptr->bits)); + printf("<%s at %p>", Py_TYPE(obj)->tp_name, PyStackRef_AsPyObjectBorrow(*ptr)); } printf("]\n"); fflush(stdout); @@ -805,7 +805,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int -#ifdef Py_DEBUG +#if defined(Py_DEBUG) && !defined(Py_STACKREF_DEBUG) /* Set these to invalid but identifiable values for debugging. */ entry_frame.f_funcobj = (_PyStackRef){.bits = 0xaaa0}; entry_frame.f_locals = (PyObject*)0xaaa1; @@ -1817,12 +1817,27 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, PyStackRef_CLOSE(func); goto error; } + size_t total_args = nargs + PyDict_GET_SIZE(kwargs); + for (size_t i = 0; i < total_args; i++) { + ((_PyStackRef *)newargs)[i] = PyStackRef_FromPyObjectSteal(newargs[i]); + } } else { - newargs = &PyTuple_GET_ITEM(callargs, 0); - /* We need to incref all our args since the new frame steals the references. */ - for (Py_ssize_t i = 0; i < nargs; ++i) { - Py_INCREF(PyTuple_GET_ITEM(callargs, i)); + if (nargs <= 8) { + PyObject *stack_array[8]; + newargs = stack_array; + } + else { + newargs = PyMem_Malloc(sizeof(PyObject *) *nargs); + if (newargs == NULL) { + PyErr_NoMemory(); + PyStackRef_CLOSE(func); + goto error; + } + } + /* We need to create a new reference for all our args since the new frame steals them. */ + for (Py_ssize_t i = 0; i < nargs; i++) { + ((_PyStackRef *)newargs)[i] = PyStackRef_FromPyObjectNew(PyTuple_GET_ITEM(callargs, i)); } } _PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit( @@ -1832,6 +1847,9 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, if (has_dict) { _PyStack_UnpackDict_FreeNoDecRef(newargs, kwnames); } + else if (nargs > 8) { + PyMem_Free((void *)newargs); + } /* No need to decref func here because the reference has been stolen by _PyEvalFramePushAndInit. */ @@ -1850,21 +1868,39 @@ _PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func, PyObject* const* args, size_t argcount, PyObject *kwnames) { + size_t total_args = argcount; + if (kwnames) { + total_args += PyTuple_GET_SIZE(kwnames); + } + _PyStackRef stack_array[8]; + _PyStackRef *arguments; + if (total_args <= 8) { + arguments = stack_array; + } + else { + arguments = PyMem_Malloc(sizeof(_PyStackRef) * total_args); + if (arguments == NULL) { + return PyErr_NoMemory(); + } + } /* _PyEvalFramePushAndInit consumes the references * to func, locals and all its arguments */ Py_XINCREF(locals); for (size_t i = 0; i < argcount; i++) { - Py_INCREF(args[i]); + arguments[i] = PyStackRef_FromPyObjectNew(args[i]); } if (kwnames) { Py_ssize_t kwcount = PyTuple_GET_SIZE(kwnames); for (Py_ssize_t i = 0; i < kwcount; i++) { - Py_INCREF(args[i+argcount]); + arguments[i+argcount] = PyStackRef_FromPyObjectNew(args[i+argcount]); } } _PyInterpreterFrame *frame = _PyEvalFramePushAndInit( tstate, PyStackRef_FromPyObjectNew(func), locals, - (_PyStackRef const *)args, argcount, kwnames, NULL); + arguments, argcount, kwnames, NULL); + if (total_args > 8) { + PyMem_Free(arguments); + } if (frame == NULL) { return NULL; } diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h index 398816d5f36a1d..f15633fa467376 100644 --- a/Python/ceval_macros.h +++ b/Python/ceval_macros.h @@ -450,7 +450,7 @@ do { \ /* How much scratch space to give stackref to PyObject* conversion. */ #define MAX_STACKREF_SCRATCH 10 -#ifdef Py_GIL_DISABLED +#if defined(Py_GIL_DISABLED) || defined(Py_STACKREF_DEBUG) #define STACKREFS_TO_PYOBJECTS(ARGS, ARG_COUNT, NAME) \ /* +1 because vectorcall might use -1 to write self */ \ PyObject *NAME##_temp[MAX_STACKREF_SCRATCH+1]; \ @@ -461,7 +461,7 @@ do { \ assert(NAME != NULL); #endif -#ifdef Py_GIL_DISABLED +#if defined(Py_GIL_DISABLED) || defined(Py_STACKREF_DEBUG) #define STACKREFS_TO_PYOBJECTS_CLEANUP(NAME) \ /* +1 because we +1 previously */ \ _PyObjectArray_Free(NAME - 1, NAME##_temp); @@ -470,7 +470,7 @@ do { \ (void)(NAME); #endif -#ifdef Py_GIL_DISABLED +#if defined(Py_GIL_DISABLED) || defined(Py_STACKREF_DEBUG) #define CONVERSION_FAILED(NAME) ((NAME) == NULL) #else #define CONVERSION_FAILED(NAME) (0) diff --git a/Python/pystate.c b/Python/pystate.c index 839413a65a42fb..c546b7c3a9f10e 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -19,6 +19,7 @@ #include "pycore_pymem.h" // _PyMem_SetDefaultAllocator() #include "pycore_pystate.h" #include "pycore_runtime_init.h" // _PyRuntimeState_INIT +#include "pycore_stackref.h" // Py_STACKREF_DEBUG #include "pycore_obmalloc.h" // _PyMem_obmalloc_state_on_heap() #include "pycore_uniqueid.h" // _PyObject_FinalizePerThreadRefcounts() @@ -663,6 +664,23 @@ init_interpreter(PyInterpreterState *interp, /* Fix the self-referential, statically initialized fields. */ interp->dtoa = (struct _dtoa_state)_dtoa_state_INIT(interp); } +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + interp->next_stackref = 1; + _Py_hashtable_allocator_t alloc = { + .malloc = malloc, + .free = free, + }; + interp->stackref_debug_table = _Py_hashtable_new_full( + _Py_hashtable_hash_ptr, + _Py_hashtable_compare_direct, + NULL, + NULL, + &alloc + ); + _Py_stackref_associate(interp, Py_None, PyStackRef_None); + _Py_stackref_associate(interp, Py_False, PyStackRef_False); + _Py_stackref_associate(interp, Py_True, PyStackRef_True); +#endif interp->_initialized = 1; return _PyStatus_OK(); @@ -768,6 +786,11 @@ PyInterpreterState_New(void) return interp; } +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) +extern void +_Py_stackref_report_leaks(PyInterpreterState *interp); +#endif + static void interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) { @@ -877,6 +900,12 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) Py_CLEAR(interp->sysdict); Py_CLEAR(interp->builtins); +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + _Py_stackref_report_leaks(interp); + _Py_hashtable_destroy(interp->stackref_debug_table); + interp->stackref_debug_table = NULL; +#endif + if (tstate->interp == interp) { /* We are now safe to fix tstate->_status.cleared. */ // XXX Do this (much) earlier? diff --git a/Python/stackrefs.c b/Python/stackrefs.c new file mode 100644 index 00000000000000..de13c0531ebbe2 --- /dev/null +++ b/Python/stackrefs.c @@ -0,0 +1,127 @@ + +#include "Python.h" + +#include "pycore_stackref.h" + +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + +#if SIZEOF_VOID_P < 8 +#error "Py_STACKREF_DEBUG requires 64 bit machine" +#endif + +#include "pycore_interp.h" +#include "pycore_hashtable.h" + +typedef struct _table_entry { + PyObject *obj; + const char *classname; + const char *filename; + int linenumber; +} TableEntry; + +TableEntry * +make_table_entry(PyObject *obj, const char *filename, int linenumber) +{ + TableEntry *result = malloc(sizeof(TableEntry)); + if (result == NULL) { + return NULL; + } + result->obj = obj; + result->classname = Py_TYPE(obj)->tp_name; + result->filename = filename; + result->linenumber = linenumber; + return result; +} + +PyObject * +_Py_stackref_get_object(_PyStackRef ref) +{ + if (ref.index == 0) { + return NULL; + } + PyInterpreterState *interp = PyInterpreterState_Get(); + assert(interp != NULL); + if (ref.index >= interp->next_stackref) { + _Py_FatalErrorFormat(__func__, "Garbled stack ref with ID %" PRIu64 "\n", ref.index); + } + TableEntry *entry = _Py_hashtable_get(interp->stackref_debug_table, (void *)ref.index); + if (entry == NULL) { + _Py_FatalErrorFormat(__func__, "Accessing closed stack ref with ID %" PRIu64 "\n", ref.index); + } + return entry->obj; +} + +PyObject *_Py_stackref_close(_PyStackRef ref) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + if (ref.index >= interp->next_stackref) { + _Py_FatalErrorFormat(__func__, "Garbled stack ref with ID %" PRIu64 "\n", ref.index); + } + PyObject *obj; + if (ref.index <= 3) { + // Pre-allocated reference to None, False or True -- Do not clear + TableEntry *entry = _Py_hashtable_get(interp->stackref_debug_table, (void *)ref.index); + obj = entry->obj; + } + else { + TableEntry *entry = _Py_hashtable_steal(interp->stackref_debug_table, (void *)ref.index); + if (entry == NULL) { + _Py_FatalErrorFormat(__func__, "Invalid StackRef with ID %" PRIu64 "\n", (void *)ref.index); + } + obj = entry->obj; + free(entry); + } + return obj; +} + +_PyStackRef +_Py_stackref_create(PyObject *obj, const char *filename, int linenumber) +{ + if (obj == NULL) { + Py_FatalError("Cannot create a stackref for NULL"); + } + PyInterpreterState *interp = PyInterpreterState_Get(); + uint64_t new_id = interp->next_stackref++; + TableEntry *entry = make_table_entry(obj, filename, linenumber); + if (entry == NULL) { + Py_FatalError("No memory left for stackref debug table"); + } + if (_Py_hashtable_set(interp->stackref_debug_table, (void *)new_id, entry) < 0) { + Py_FatalError("No memory left for stackref debug table"); + } + return (_PyStackRef){ .index = new_id }; +} + +void +_Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef ref) +{ + assert(interp->next_stackref >= ref.index); + interp->next_stackref = ref.index+1; + TableEntry *entry = make_table_entry(obj, "builtin-object", 0); + if (entry == NULL) { + Py_FatalError("No memory left for stackref debug table"); + } + if (_Py_hashtable_set(interp->stackref_debug_table, (void *)ref.index, (void *)entry) < 0) { + Py_FatalError("No memory left for stackref debug table"); + } +} + + +static int +report_leak(_Py_hashtable_t *ht, const void *key, const void *value, void *user_data) +{ + TableEntry *entry = (TableEntry *)value; + if (!_Py_IsStaticImmortal(entry->obj)) { + printf("Stackref leak. Refers to instance of %s at %p. Created at %s:%d\n", + entry->classname, entry->obj, entry->filename, entry->linenumber); + } + return 0; +} + +void +_Py_stackref_report_leaks(PyInterpreterState *interp) +{ + _Py_hashtable_foreach(interp->stackref_debug_table, report_leak, NULL); +} + +#endif From 5c621a64f4ade205549e46ad127dfa368ba7e756 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 09:10:06 +0000 Subject: [PATCH 02/10] Add PyStackRef_Transfer --- Include/internal/pycore_stackref.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index 8a278076cf1cf4..227e29ab72881a 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -55,6 +55,8 @@ extern "C" { #if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + + typedef union _PyStackRef { uint64_t index; } _PyStackRef; @@ -147,10 +149,20 @@ _PyStackRef_DUP(_PyStackRef ref, const char *filename, int linenumber) } #define PyStackRef_DUP(REF) _PyStackRef_DUP(REF, __FILE__, __LINE__) +static inline _PyStackRef +_PyStackRef_Transfer(_PyStackRef ref, const char *filename, int linenumber) +{ + PyObject *obj = _Py_stackref_close(ref); + return _Py_stackref_create(obj, filename, linenumber); +} +#define PyStackRef_Transfer(REF) _PyStackRef_Transfer(REF, __FILE__, __LINE__) + #define PyStackRef_CLOSE_SPECIALIZED(stackref, dealloc) PyStackRef_CLOSE(stackref) #else +#define PyStackRef_Transfer(REF) (REF) + typedef union _PyStackRef { uintptr_t bits; } _PyStackRef; From 3d67f2c05ba1c30d1c5c6a5660bc29a0f5b5d004 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 11:55:21 +0000 Subject: [PATCH 03/10] Fix stackref leaks --- Include/internal/pycore_stackref.h | 6 +++++- Python/bytecodes.c | 17 ++++++++++----- Python/executor_cases.c.h | 2 +- Python/generated_cases.c.h | 23 +++++++++++++------- Python/stackrefs.c | 34 +++++++++++++++++++++++++++--- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index 227e29ab72881a..7ab34be19970a3 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -66,6 +66,7 @@ typedef union _PyStackRef { PyAPI_FUNC(PyObject *) _Py_stackref_get_object(_PyStackRef ref); PyAPI_FUNC(PyObject *) _Py_stackref_close(_PyStackRef ref); PyAPI_FUNC(_PyStackRef) _Py_stackref_create(PyObject *obj, const char *filename, int linenumber); +PyAPI_FUNC(void) _Py_stackref_record_borrow(_PyStackRef ref, const char *filename, int linenumber); extern void _Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef ref); static const _PyStackRef PyStackRef_NULL = { .index = 0 }; @@ -99,11 +100,14 @@ PyStackRef_IsNone(_PyStackRef ref) } static inline PyObject * -PyStackRef_AsPyObjectBorrow(_PyStackRef ref) +_PyStackRef_AsPyObjectBorrow(_PyStackRef ref, const char *filename, int linenumber) { + _Py_stackref_record_borrow(ref, filename, linenumber); return _Py_stackref_get_object(ref); } +#define PyStackRef_AsPyObjectBorrow(REF) _PyStackRef_AsPyObjectBorrow((REF), __FILE__, __LINE__) + static inline PyObject * PyStackRef_AsPyObjectSteal(_PyStackRef ref) { diff --git a/Python/bytecodes.c b/Python/bytecodes.c index b67264f0440869..faec111eb3e33f 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -679,7 +679,7 @@ dummy_func( assert(Py_REFCNT(left_o) >= 2); PyStackRef_CLOSE(left); DEAD(left); - PyObject *temp = PyStackRef_AsPyObjectBorrow(*target_local); + PyObject *temp = PyStackRef_AsPyObjectSteal(*target_local); PyUnicode_Append(&temp, right_o); *target_local = PyStackRef_FromPyObjectSteal(temp); PyStackRef_CLOSE_SPECIALIZED(right, _PyUnicode_ExactDealloc); @@ -4476,17 +4476,17 @@ dummy_func( op(_DO_CALL_FUNCTION_EX, (func_st, unused, callargs_st, kwargs_st if (oparg & 1) -- result)) { PyObject *func = PyStackRef_AsPyObjectBorrow(func_st); - PyObject *callargs = PyStackRef_AsPyObjectBorrow(callargs_st); - PyObject *kwargs = PyStackRef_AsPyObjectBorrow(kwargs_st); // DICT_MERGE is called before this opcode if there are kwargs. // It converts all dict subtypes in kwargs into regular dicts. - assert(kwargs == NULL || PyDict_CheckExact(kwargs)); - assert(PyTuple_CheckExact(callargs)); EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_FUNCTION_EX, func); PyObject *result_o; assert(!_PyErr_Occurred(tstate)); if (opcode == INSTRUMENTED_CALL_FUNCTION_EX) { + PyObject *callargs = PyStackRef_AsPyObjectBorrow(callargs_st); + PyObject *kwargs = PyStackRef_AsPyObjectBorrow(kwargs_st); + assert(kwargs == NULL || PyDict_CheckExact(kwargs)); + assert(PyTuple_CheckExact(callargs)); PyObject *arg = PyTuple_GET_SIZE(callargs) > 0 ? PyTuple_GET_ITEM(callargs, 0) : &_PyInstrumentation_MISSING; int err = _Py_call_instrumentation_2args( @@ -4517,7 +4517,10 @@ dummy_func( if (Py_TYPE(func) == &PyFunction_Type && tstate->interp->eval_frame == NULL && ((PyFunctionObject *)func)->vectorcall == _PyFunction_Vectorcall) { + PyObject *callargs = PyStackRef_AsPyObjectSteal(callargs_st); assert(PyTuple_CheckExact(callargs)); + PyObject *kwargs = PyStackRef_IsNull(kwargs_st) ? NULL : PyStackRef_AsPyObjectSteal(kwargs_st); + assert(kwargs == NULL || PyDict_CheckExact(kwargs)); Py_ssize_t nargs = PyTuple_GET_SIZE(callargs); int code_flags = ((PyCodeObject *)PyFunction_GET_CODE(func))->co_flags; PyObject *locals = code_flags & CO_OPTIMIZED ? NULL : Py_NewRef(PyFunction_GET_GLOBALS(func)); @@ -4535,6 +4538,10 @@ dummy_func( frame->return_offset = 1; DISPATCH_INLINED(new_frame); } + PyObject *callargs = PyStackRef_AsPyObjectBorrow(callargs_st); + assert(PyTuple_CheckExact(callargs)); + PyObject *kwargs = PyStackRef_AsPyObjectBorrow(kwargs_st); + assert(kwargs == NULL || PyDict_CheckExact(kwargs)); result_o = PyObject_Call(func, callargs, kwargs); } PyStackRef_XCLOSE(kwargs_st); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index de61a64a6e3374..cc7e97bfa6c80e 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -850,7 +850,7 @@ */ assert(Py_REFCNT(left_o) >= 2); PyStackRef_CLOSE(left); - PyObject *temp = PyStackRef_AsPyObjectBorrow(*target_local); + PyObject *temp = PyStackRef_AsPyObjectSteal(*target_local); PyUnicode_Append(&temp, right_o); *target_local = PyStackRef_FromPyObjectSteal(temp); PyStackRef_CLOSE_SPECIALIZED(right, _PyUnicode_ExactDealloc); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 8a89ba890fd9c9..47bc72f17a4e71 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -208,7 +208,7 @@ */ assert(Py_REFCNT(left_o) >= 2); PyStackRef_CLOSE(left); - PyObject *temp = PyStackRef_AsPyObjectBorrow(*target_local); + PyObject *temp = PyStackRef_AsPyObjectSteal(*target_local); PyUnicode_Append(&temp, right_o); *target_local = PyStackRef_FromPyObjectSteal(temp); PyStackRef_CLOSE_SPECIALIZED(right, _PyUnicode_ExactDealloc); @@ -1675,16 +1675,16 @@ callargs_st = tuple; func_st = func; PyObject *func = PyStackRef_AsPyObjectBorrow(func_st); - PyObject *callargs = PyStackRef_AsPyObjectBorrow(callargs_st); - PyObject *kwargs = PyStackRef_AsPyObjectBorrow(kwargs_st); // DICT_MERGE is called before this opcode if there are kwargs. // It converts all dict subtypes in kwargs into regular dicts. - assert(kwargs == NULL || PyDict_CheckExact(kwargs)); - assert(PyTuple_CheckExact(callargs)); EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_FUNCTION_EX, func); PyObject *result_o; assert(!_PyErr_Occurred(tstate)); if (opcode == INSTRUMENTED_CALL_FUNCTION_EX) { + PyObject *callargs = PyStackRef_AsPyObjectBorrow(callargs_st); + PyObject *kwargs = PyStackRef_AsPyObjectBorrow(kwargs_st); + assert(kwargs == NULL || PyDict_CheckExact(kwargs)); + assert(PyTuple_CheckExact(callargs)); PyObject *arg = PyTuple_GET_SIZE(callargs) > 0 ? PyTuple_GET_ITEM(callargs, 0) : &_PyInstrumentation_MISSING; stack_pointer[-1 - (oparg & 1)] = callargs_st; @@ -1724,19 +1724,22 @@ if (Py_TYPE(func) == &PyFunction_Type && tstate->interp->eval_frame == NULL && ((PyFunctionObject *)func)->vectorcall == _PyFunction_Vectorcall) { + PyObject *callargs = PyStackRef_AsPyObjectSteal(callargs_st); assert(PyTuple_CheckExact(callargs)); + PyObject *kwargs = PyStackRef_IsNull(kwargs_st) ? NULL : PyStackRef_AsPyObjectSteal(kwargs_st); + assert(kwargs == NULL || PyDict_CheckExact(kwargs)); Py_ssize_t nargs = PyTuple_GET_SIZE(callargs); int code_flags = ((PyCodeObject *)PyFunction_GET_CODE(func))->co_flags; PyObject *locals = code_flags & CO_OPTIMIZED ? NULL : Py_NewRef(PyFunction_GET_GLOBALS(func)); - stack_pointer[-1 - (oparg & 1)] = callargs_st; - if (oparg & 1) stack_pointer[-(oparg & 1)] = kwargs_st; + stack_pointer += -2 - (oparg & 1); + assert(WITHIN_STACK_BOUNDS()); _PyFrame_SetStackPointer(frame, stack_pointer); _PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit_Ex( tstate, func_st, locals, nargs, callargs, kwargs, frame); stack_pointer = _PyFrame_GetStackPointer(frame); // Need to sync the stack since we exit with DISPATCH_INLINED. - stack_pointer += -3 - (oparg & 1); + stack_pointer += -1; assert(WITHIN_STACK_BOUNDS()); if (new_frame == NULL) { goto error; @@ -1745,6 +1748,10 @@ frame->return_offset = 1; DISPATCH_INLINED(new_frame); } + PyObject *callargs = PyStackRef_AsPyObjectBorrow(callargs_st); + assert(PyTuple_CheckExact(callargs)); + PyObject *kwargs = PyStackRef_AsPyObjectBorrow(kwargs_st); + assert(kwargs == NULL || PyDict_CheckExact(kwargs)); stack_pointer[-1 - (oparg & 1)] = callargs_st; if (oparg & 1) stack_pointer[-(oparg & 1)] = kwargs_st; _PyFrame_SetStackPointer(frame, stack_pointer); diff --git a/Python/stackrefs.c b/Python/stackrefs.c index de13c0531ebbe2..5e42d99ef9b06e 100644 --- a/Python/stackrefs.c +++ b/Python/stackrefs.c @@ -17,6 +17,8 @@ typedef struct _table_entry { const char *classname; const char *filename; int linenumber; + const char *filename_borrow; + int linenumber_borrow; } TableEntry; TableEntry * @@ -30,6 +32,7 @@ make_table_entry(PyObject *obj, const char *filename, int linenumber) result->classname = Py_TYPE(obj)->tp_name; result->filename = filename; result->linenumber = linenumber; + result->filename_borrow = NULL; return result; } @@ -92,6 +95,22 @@ _Py_stackref_create(PyObject *obj, const char *filename, int linenumber) return (_PyStackRef){ .index = new_id }; } +void +_Py_stackref_record_borrow(_PyStackRef ref, const char *filename, int linenumber) +{ + if (ref.index <= 3) { + return; + } + PyInterpreterState *interp = PyInterpreterState_Get(); + TableEntry *entry = _Py_hashtable_get(interp->stackref_debug_table, (void *)ref.index); + if (entry == NULL) { + _Py_FatalErrorFormat(__func__, "Invalid StackRef with ID %" PRIu64 "\n", (void *)ref.index); + } + entry->filename_borrow = filename; + entry->linenumber_borrow = linenumber; +} + + void _Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef ref) { @@ -108,12 +127,17 @@ _Py_stackref_associate(PyInterpreterState *interp, PyObject *obj, _PyStackRef re static int -report_leak(_Py_hashtable_t *ht, const void *key, const void *value, void *user_data) +report_leak(_Py_hashtable_t *ht, const void *key, const void *value, void *leak) { TableEntry *entry = (TableEntry *)value; if (!_Py_IsStaticImmortal(entry->obj)) { - printf("Stackref leak. Refers to instance of %s at %p. Created at %s:%d\n", + *(int *)leak = 1; + printf("Stackref leak. Refers to instance of %s at %p. Created at %s:%d", entry->classname, entry->obj, entry->filename, entry->linenumber); + if (entry->filename_borrow != NULL) { + printf(". Last borrow at %s:%d",entry->filename_borrow, entry->linenumber_borrow); + } + printf("\n"); } return 0; } @@ -121,7 +145,11 @@ report_leak(_Py_hashtable_t *ht, const void *key, const void *value, void *user_ void _Py_stackref_report_leaks(PyInterpreterState *interp) { - _Py_hashtable_foreach(interp->stackref_debug_table, report_leak, NULL); + int leak = 0; + _Py_hashtable_foreach(interp->stackref_debug_table, report_leak, &leak); + if (leak) { + Py_FatalError("Stackrefs leaked."); + } } #endif From 8c2a5740b6e34a6be67bb891d522e5e7606ab3a7 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 11:56:54 +0000 Subject: [PATCH 04/10] Remove unused PyStackRef_Transfer function --- Include/internal/pycore_stackref.h | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index 7ab34be19970a3..fadd32f536485c 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -153,20 +153,10 @@ _PyStackRef_DUP(_PyStackRef ref, const char *filename, int linenumber) } #define PyStackRef_DUP(REF) _PyStackRef_DUP(REF, __FILE__, __LINE__) -static inline _PyStackRef -_PyStackRef_Transfer(_PyStackRef ref, const char *filename, int linenumber) -{ - PyObject *obj = _Py_stackref_close(ref); - return _Py_stackref_create(obj, filename, linenumber); -} -#define PyStackRef_Transfer(REF) _PyStackRef_Transfer(REF, __FILE__, __LINE__) - #define PyStackRef_CLOSE_SPECIALIZED(stackref, dealloc) PyStackRef_CLOSE(stackref) #else -#define PyStackRef_Transfer(REF) (REF) - typedef union _PyStackRef { uintptr_t bits; } _PyStackRef; From e51d051b1dd0f53007da7c8e9e8c0836c861368a Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 12:00:39 +0000 Subject: [PATCH 05/10] Turn off Py_STACKREF_DEBUG by default --- Include/internal/pycore_stackref.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index fadd32f536485c..fcf764555bbcfc 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -5,7 +5,7 @@ extern "C" { #endif // Define this to get precise tracking of stackrefs. -#define Py_STACKREF_DEBUG 1 +// #define Py_STACKREF_DEBUG 1 #ifndef Py_BUILD_CORE # error "this header requires Py_BUILD_CORE define" From 7998bc9529926b54bd7d37ac63378e46943d7313 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 12:23:04 +0000 Subject: [PATCH 06/10] Make sure scope of variable covers all uses --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index ec4fa560ea1b6f..f8178e92494f2e 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1811,6 +1811,7 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, bool has_dict = (kwargs != NULL && PyDict_GET_SIZE(kwargs) > 0); PyObject *kwnames = NULL; PyObject *const *newargs; + PyObject *stack_array[8]; if (has_dict) { newargs = _PyStack_UnpackDict(tstate, _PyTuple_ITEMS(callargs), nargs, kwargs, &kwnames); if (newargs == NULL) { @@ -1824,7 +1825,6 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, } else { if (nargs <= 8) { - PyObject *stack_array[8]; newargs = stack_array; } else { From 98706ff6814f21c27f618bde0971ff9ae19634cb Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 12:25:39 +0000 Subject: [PATCH 07/10] Add news --- .../2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst new file mode 100644 index 00000000000000..3ac379cf8694c2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst @@ -0,0 +1,2 @@ +Adds stackref debugging when ``Py_STACKREF_DEBUG`` is set. Finds all +double-closes and leaks, logging the origin and last borrow. From d85c5056f5054975884f197e1b132c892e3a2eae Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 12:44:32 +0000 Subject: [PATCH 08/10] Address review comments --- Include/internal/pycore_stackref.h | 2 ++ .../2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst | 2 ++ Python/stackrefs.c | 7 ++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Include/internal/pycore_stackref.h b/Include/internal/pycore_stackref.h index fcf764555bbcfc..1ae62cc69bb364 100644 --- a/Include/internal/pycore_stackref.h +++ b/Include/internal/pycore_stackref.h @@ -75,6 +75,8 @@ static const _PyStackRef PyStackRef_NULL = { .index = 0 }; #define PyStackRef_False ((_PyStackRef){ .index = 2 }) #define PyStackRef_True ((_PyStackRef){ .index = 3 }) +#define LAST_PREDEFINED_STACKREF_INDEX 3 + static inline int PyStackRef_IsNull(_PyStackRef ref) { diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst index 3ac379cf8694c2..fde12b78ce0444 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst @@ -1,2 +1,4 @@ Adds stackref debugging when ``Py_STACKREF_DEBUG`` is set. Finds all double-closes and leaks, logging the origin and last borrow. + +Inspired by HPy's debug mode. https://docs.hpyproject.org/en/latest/debug-mode.html diff --git a/Python/stackrefs.c b/Python/stackrefs.c index 5e42d99ef9b06e..9bb46897685570 100644 --- a/Python/stackrefs.c +++ b/Python/stackrefs.c @@ -54,14 +54,15 @@ _Py_stackref_get_object(_PyStackRef ref) return entry->obj; } -PyObject *_Py_stackref_close(_PyStackRef ref) +PyObject * +_Py_stackref_close(_PyStackRef ref) { PyInterpreterState *interp = PyInterpreterState_Get(); if (ref.index >= interp->next_stackref) { _Py_FatalErrorFormat(__func__, "Garbled stack ref with ID %" PRIu64 "\n", ref.index); } PyObject *obj; - if (ref.index <= 3) { + if (ref.index <= LAST_PREDEFINED_STACKREF_INDEX) { // Pre-allocated reference to None, False or True -- Do not clear TableEntry *entry = _Py_hashtable_get(interp->stackref_debug_table, (void *)ref.index); obj = entry->obj; @@ -98,7 +99,7 @@ _Py_stackref_create(PyObject *obj, const char *filename, int linenumber) void _Py_stackref_record_borrow(_PyStackRef ref, const char *filename, int linenumber) { - if (ref.index <= 3) { + if (ref.index <= LAST_PREDEFINED_STACKREF_INDEX) { return; } PyInterpreterState *interp = PyInterpreterState_Get(); From 5eaba5a5b9b6f92c0fb7c3cce6d9395dc8057591 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 15:12:54 +0000 Subject: [PATCH 09/10] Use less type punning --- Python/ceval.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Python/ceval.c b/Python/ceval.c index f8178e92494f2e..3300deee97c9c6 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1810,17 +1810,20 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, { bool has_dict = (kwargs != NULL && PyDict_GET_SIZE(kwargs) > 0); PyObject *kwnames = NULL; - PyObject *const *newargs; - PyObject *stack_array[8]; + _PyStackRef *newargs; + PyObject *const *object_array; + _PyStackRef stack_array[8]; if (has_dict) { - newargs = _PyStack_UnpackDict(tstate, _PyTuple_ITEMS(callargs), nargs, kwargs, &kwnames); - if (newargs == NULL) { + object_array = _PyStack_UnpackDict(tstate, _PyTuple_ITEMS(callargs), nargs, kwargs, &kwnames); + if (object_array == NULL) { PyStackRef_CLOSE(func); goto error; } size_t total_args = nargs + PyDict_GET_SIZE(kwargs); + assert(sizeof(PyObject *) == sizeof(_PyStackRef)); + newargs = (_PyStackRef *)object_array; for (size_t i = 0; i < total_args; i++) { - ((_PyStackRef *)newargs)[i] = PyStackRef_FromPyObjectSteal(newargs[i]); + newargs[i] = PyStackRef_FromPyObjectSteal(object_array[i]); } } else { @@ -1828,7 +1831,7 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, newargs = stack_array; } else { - newargs = PyMem_Malloc(sizeof(PyObject *) *nargs); + newargs = PyMem_Malloc(sizeof(_PyStackRef) *nargs); if (newargs == NULL) { PyErr_NoMemory(); PyStackRef_CLOSE(func); @@ -1837,15 +1840,15 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, } /* We need to create a new reference for all our args since the new frame steals them. */ for (Py_ssize_t i = 0; i < nargs; i++) { - ((_PyStackRef *)newargs)[i] = PyStackRef_FromPyObjectNew(PyTuple_GET_ITEM(callargs, i)); + newargs[i] = PyStackRef_FromPyObjectNew(PyTuple_GET_ITEM(callargs, i)); } } _PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit( tstate, func, locals, - (_PyStackRef const *)newargs, nargs, kwnames, previous + newargs, nargs, kwnames, previous ); if (has_dict) { - _PyStack_UnpackDict_FreeNoDecRef(newargs, kwnames); + _PyStack_UnpackDict_FreeNoDecRef(object_array, kwnames); } else if (nargs > 8) { PyMem_Free((void *)newargs); From efc6cf50bb5ec3d44c67703534913c5150956bcc Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 20 Dec 2024 15:38:15 +0000 Subject: [PATCH 10/10] Keep C compilers with poor flow analysis happy --- Python/ceval.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ceval.c b/Python/ceval.c index 3300deee97c9c6..ac07935f830897 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1811,7 +1811,7 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, _PyStackRef func, bool has_dict = (kwargs != NULL && PyDict_GET_SIZE(kwargs) > 0); PyObject *kwnames = NULL; _PyStackRef *newargs; - PyObject *const *object_array; + PyObject *const *object_array = NULL; _PyStackRef stack_array[8]; if (has_dict) { object_array = _PyStack_UnpackDict(tstate, _PyTuple_ITEMS(callargs), nargs, kwargs, &kwnames); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy