Skip to content

Commit 7626a0e

Browse files
committed
bpo-30744: Trace hooks no longer reset closure state
Previously, trace hooks running on a nested function could incorrectly reset the state of a closure cell. This avoids that by slightly changing the semantics of the locals namespace in functions, generators, and coroutines, such that closure references are represented in the locals namespace by their actual cell objects while a trace hook implemented in Python is running. Depends on PEP 558 and bpo-17960 to cover the underlying change to frame.f_locals semantics.
1 parent 1b46131 commit 7626a0e

File tree

5 files changed

+116
-15
lines changed

5 files changed

+116
-15
lines changed

Include/descrobject.h

+4
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ PyAPI_FUNC(PyObject *) PyDescr_NewWrapper(PyTypeObject *,
9999
#endif
100100

101101
PyAPI_FUNC(PyObject *) PyDictProxy_New(PyObject *);
102+
#ifdef Py_BUILD_CORE
103+
PyAPI_FUNC(PyObject *) _PyDictProxy_GetMapping(PyObject *);
104+
#endif
105+
102106
PyAPI_FUNC(PyObject *) PyWrapper_New(PyObject *, PyObject *);
103107

104108

Include/frameobject.h

+3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ PyAPI_FUNC(void) PyFrame_LocalsToFast(PyFrameObject *, int);
7878

7979
PyAPI_FUNC(int) PyFrame_FastToLocalsWithError(PyFrameObject *f);
8080
PyAPI_FUNC(void) PyFrame_FastToLocals(PyFrameObject *);
81+
#ifdef Py_BUILD_CORE
82+
int _PyFrame_FastToLocalsInternal(PyFrameObject *f, int); /* For sys.settrace */
83+
#endif
8184

8285
PyAPI_FUNC(int) PyFrame_ClearFreeList(void);
8386

Lib/test/test_sys_settrace.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -889,13 +889,56 @@ class fake_function:
889889
self.compare_jump_output([2, 3, 2, 3, 4], namespace["output"])
890890

891891

892+
class FrameLocalsTestCase(unittest.TestCase):
893+
def setUp(self):
894+
self.addCleanup(sys.settrace, sys.gettrace())
895+
sys.settrace(None)
896+
897+
def test_closures_are_not_implicitly_reset_to_previous_state(self):
898+
# See https://bugs.python.org/issue30744 for details
899+
i_from_generator = []
900+
x_from_generator = []
901+
x_from_nested_ref = []
902+
x_from_nested_locals = []
903+
def outer():
904+
x = 0
905+
906+
def update_nested_refs():
907+
x_from_nested_ref.append(x)
908+
x_from_nested_locals.append(locals()["x"])
909+
910+
yield update_nested_refs
911+
for i in range(1, 10):
912+
i_from_generator.append(i)
913+
x += 1
914+
yield x
915+
916+
incrementing_generator = outer()
917+
update_nested_refs = next(incrementing_generator)
918+
919+
def tracefunc(frame, event, arg):
920+
x_from_generator.append(next(incrementing_generator))
921+
return tracefunc
922+
923+
sys.settrace(tracefunc)
924+
try:
925+
update_nested_refs()
926+
update_nested_refs()
927+
finally:
928+
sys.settrace(None)
929+
self.assertEqual(x_from_generator, i_from_generator)
930+
self.assertEqual(x_from_nested_ref, [2, 6])
931+
self.assertEqual(x_from_nested_locals, [3, 7])
932+
933+
892934
def test_main():
893935
support.run_unittest(
894936
TraceTestCase,
895937
SkipLineEventsTraceTestCase,
896938
TraceOpcodesTestCase,
897939
RaisingTraceFuncTestCase,
898-
JumpTestCase
940+
JumpTestCase,
941+
FrameLocalsTestCase
899942
)
900943

901944
if __name__ == "__main__":

Objects/frameobject.c

+59-13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010

1111
#define OFF(x) offsetof(PyFrameObject, x)
1212

13+
/* PEP558:
14+
*
15+
* Forward declaration of fastlocalsproxy
16+
* PyEval_GetLocals will need a new PyFrame_GetLocals() helper function
17+
* that ensures it always gets the snapshot reference and never the proxy
18+
* even when tracing is enabled.
19+
* That should probably be a public API for the benefit of third party debuggers
20+
* implemented in C.
21+
*
22+
*/
23+
1324
static PyMemberDef frame_memberlist[] = {
1425
{"f_back", T_OBJECT, OFF(f_back), READONLY},
1526
{"f_code", T_OBJECT, OFF(f_code), READONLY},
@@ -813,18 +824,20 @@ map_to_dict(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
813824
814825
map and values are input arguments. map is a tuple of strings.
815826
values is an array of PyObject*. At index i, map[i] is the name of
816-
the variable with value values[i]. The function copies the first
817-
nmap variable from map/values into dict. If values[i] is NULL,
818-
the variable is deleted from dict.
827+
the variable with value values[i]. The function gets the new value
828+
for values[i] by looking up map[i] in the dict.
829+
830+
If clear is true and map[i] is missing from the dict, then values[i] is
831+
set to NULL. If clear is false, then values[i] is left alone in that case.
819832
820833
If deref is true, then the values being copied are cell variables
821-
and the value is extracted from the cell variable before being put
822-
in dict. If clear is true, then variables in map but not in dict
823-
are set to NULL in map; if clear is false, variables missing in
824-
dict are ignored.
834+
and the value is inserted into the cell variable rather than overwriting
835+
the value directly. If the value in the dict *is* the cell itself, then
836+
the cell value is left alone, and instead that value is written back
837+
into the dict (replacing the cell reference).
825838
826-
Exceptions raised while modifying the dict are silently ignored,
827-
because there is no good way to report them.
839+
Exceptions raised while reading or updating the dict or updating a cell
840+
reference are silently ignored, because there is no good way to report them.
828841
*/
829842

830843
static void
@@ -847,7 +860,16 @@ dict_to_map(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
847860
}
848861
if (deref) {
849862
assert(PyCell_Check(values[j]));
850-
if (PyCell_GET(values[j]) != value) {
863+
PyObject *cell_value = PyCell_GET(values[j]);
864+
if (values[j] == value) {
865+
/* The dict currently contains the cell itself, so write the
866+
cell's value into the dict rather than the other way around
867+
*/
868+
if (PyObject_SetItem(dict, key, cell_value) != 0) {
869+
PyErr_Clear();
870+
}
871+
} else if (cell_value != value) {
872+
/* Write the value from the dict back into the cell */
851873
if (PyCell_Set(values[j], value) < 0)
852874
PyErr_Clear();
853875
}
@@ -860,7 +882,7 @@ dict_to_map(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
860882
}
861883

862884
int
863-
PyFrame_FastToLocalsWithError(PyFrameObject *f)
885+
_PyFrame_FastToLocalsInternal(PyFrameObject *f, int deref)
864886
{
865887
/* Merge fast locals into f->f_locals */
866888
PyObject *locals, *map;
@@ -879,6 +901,14 @@ PyFrame_FastToLocalsWithError(PyFrameObject *f)
879901
if (locals == NULL)
880902
return -1;
881903
}
904+
/* PEP558:
905+
*
906+
* If a trace function is active, ensure f_locals is a fastlocalsproxy
907+
* instance, while locals still refers to the underlying mapping.
908+
* If a trace function is *not* active, discard the proxy, if any (and
909+
* invalidate its reference back to the frame) to disallow further writes.
910+
*/
911+
882912
co = f->f_code;
883913
map = co->co_varnames;
884914
if (!PyTuple_Check(map)) {
@@ -898,8 +928,17 @@ PyFrame_FastToLocalsWithError(PyFrameObject *f)
898928
ncells = PyTuple_GET_SIZE(co->co_cellvars);
899929
nfreevars = PyTuple_GET_SIZE(co->co_freevars);
900930
if (ncells || nfreevars) {
931+
/* If deref is true, we'll replace cells with their values in the
932+
namespace. If it's false, we'll include the cells themselves, which
933+
means PyFrame_LocalsToFast will skip writing them back (unless
934+
they've actually been modified).
935+
936+
The trace hook implementation relies on this to allow debuggers to
937+
inject changes to local variables without inadvertently resetting
938+
closure variables to a previous value.
939+
*/
901940
if (map_to_dict(co->co_cellvars, ncells,
902-
locals, fast + co->co_nlocals, 1))
941+
locals, fast + co->co_nlocals, deref))
903942
return -1;
904943

905944
/* If the namespace is unoptimized, then one of the
@@ -912,13 +951,20 @@ PyFrame_FastToLocalsWithError(PyFrameObject *f)
912951
*/
913952
if (co->co_flags & CO_OPTIMIZED) {
914953
if (map_to_dict(co->co_freevars, nfreevars,
915-
locals, fast + co->co_nlocals + ncells, 1) < 0)
954+
locals, fast + co->co_nlocals + ncells, deref) < 0)
916955
return -1;
917956
}
918957
}
958+
919959
return 0;
920960
}
921961

962+
int
963+
PyFrame_FastToLocalsWithError(PyFrameObject *f)
964+
{
965+
return _PyFrame_FastToLocalsInternal(f, 1);
966+
}
967+
922968
void
923969
PyFrame_FastToLocals(PyFrameObject *f)
924970
{

Python/sysmodule.c

+6-1
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ call_trampoline(PyObject* callback,
458458
PyObject *result;
459459
PyObject *stack[3];
460460

461-
if (PyFrame_FastToLocalsWithError(frame) < 0) {
461+
/* Put any cell references into locals as the actual cells */
462+
if (_PyFrame_FastToLocalsInternal(frame, 0) < 0) {
462463
return NULL;
463464
}
464465

@@ -469,6 +470,10 @@ call_trampoline(PyObject* callback,
469470
/* call the Python-level function */
470471
result = _PyObject_FastCall(callback, stack, 3);
471472

473+
/* All local references will be written back here, but cell references
474+
will only be written back here if they were changed to refer to
475+
something other than the cell itself.
476+
*/
472477
PyFrame_LocalsToFast(frame, 1);
473478
if (result == NULL) {
474479
PyTraceBack_Here(frame);

0 commit comments

Comments
 (0)