https://github.com/python/cpython/commit/128cc47fbd44e3e09c50d9674fe4a4bba3be450c
commit: 128cc47fbd44e3e09c50d9674fe4a4bba3be450c
branch: main
author: Mark Shannon <[email protected]>
committer: markshannon <[email protected]>
date: 2024-12-20T16:52:20Z
summary:

GH-127705: Add debug mode for `_PyStackRef`s inspired by HPy debug mode 
(GH-128121)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst
A Python/stackrefs.c
M Include/internal/pycore_interp.h
M Include/internal/pycore_stackref.h
M Makefile.pre.in
M Objects/frameobject.c
M Python/bytecodes.c
M Python/ceval.c
M Python/ceval_macros.h
M Python/executor_cases.c.h
M Python/generated_cases.c.h
M Python/pystate.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..1ae62cc69bb364 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,113 @@ 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);
+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 };
+
+#define PyStackRef_None ((_PyStackRef){ .index = 1 } )
+#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)
+{
+    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, 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)
+{
+    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 +310,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/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..fde12b78ce0444
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-20-12-25-16.gh-issue-127705.WmCz1z.rst
@@ -0,0 +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/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/bytecodes.c b/Python/bytecodes.c
index 30c12dd4dc9205..63cf1978e8abe5 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -681,7 +681,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);
@@ -4509,17 +4509,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(
@@ -4550,7 +4550,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));
@@ -4568,6 +4571,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/ceval.c b/Python/ceval.c
index bfdf5687c287db..3cf11b663c571b 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;
@@ -1810,27 +1810,48 @@ _PyEvalFramePushAndInit_Ex(PyThreadState *tstate, 
_PyStackRef func,
 {
     bool has_dict = (kwargs != NULL && PyDict_GET_SIZE(kwargs) > 0);
     PyObject *kwnames = NULL;
-    PyObject *const *newargs;
+    _PyStackRef *newargs;
+    PyObject *const *object_array = NULL;
+    _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++) {
+            newargs[i] = PyStackRef_FromPyObjectSteal(object_array[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) {
+            newargs = stack_array;
+        }
+        else {
+            newargs = PyMem_Malloc(sizeof(_PyStackRef) *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++) {
+            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);
     }
     /* No need to decref func here because the reference has been stolen by
        _PyEvalFramePushAndInit.
@@ -1850,21 +1871,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/executor_cases.c.h b/Python/executor_cases.c.h
index 6e752c57cd70f3..22335021faaa6d 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 ee5c55a832d460..bed16b60b76a2f 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/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..9bb46897685570
--- /dev/null
+++ b/Python/stackrefs.c
@@ -0,0 +1,156 @@
+
+#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;
+    const char *filename_borrow;
+    int linenumber_borrow;
+} 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;
+    result->filename_borrow = NULL;
+    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 <= 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;
+    }
+    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_record_borrow(_PyStackRef ref, const char *filename, int 
linenumber)
+{
+    if (ref.index <= LAST_PREDEFINED_STACKREF_INDEX) {
+        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)
+{
+    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 
*leak)
+{
+    TableEntry *entry = (TableEntry *)value;
+    if (!_Py_IsStaticImmortal(entry->obj)) {
+        *(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;
+}
+
+void
+_Py_stackref_report_leaks(PyInterpreterState *interp)
+{
+    int leak = 0;
+    _Py_hashtable_foreach(interp->stackref_debug_table, report_leak, &leak);
+    if (leak) {
+        Py_FatalError("Stackrefs leaked.");
+    }
+}
+
+#endif

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to