--- /dev/null
+import functools
+import sys
+import threading
+import unittest
+
+from test.support import threading_helper
+
+threading_helper.requires_working_threading(module=True)
+
+
+def run_with_frame(funcs, runner=None, iters=10):
+ """Run funcs with a frame from another thread that is currently executing.
+
+ Args:
+ funcs: A function or list of functions that take a frame argument
+ runner: Optional function to run in the executor thread. If provided,
+ it will be called and should return eventually. The frame
+ passed to funcs will be the runner's frame.
+ iters: Number of iterations each func should run
+ """
+ if not isinstance(funcs, list):
+ funcs = [funcs]
+
+ frame_var = None
+ e = threading.Event()
+ b = threading.Barrier(len(funcs) + 1)
+
+ if runner is None:
+ def runner():
+ j = 0
+ for i in range(100):
+ j += i
+
+ def executor():
+ nonlocal frame_var
+ frame_var = sys._getframe()
+ e.set()
+ b.wait()
+ runner()
+
+ def func_wrapper(func):
+ e.wait()
+ frame = frame_var
+ b.wait()
+ for _ in range(iters):
+ func(frame)
+
+ test_funcs = [functools.partial(func_wrapper, f) for f in funcs]
+ threading_helper.run_concurrently([executor] + test_funcs)
+
+
+class TestFrameRaces(unittest.TestCase):
+ def test_concurrent_f_lasti(self):
+ run_with_frame(lambda frame: frame.f_lasti)
+
+ def test_concurrent_f_lineno(self):
+ run_with_frame(lambda frame: frame.f_lineno)
+
+ def test_concurrent_f_code(self):
+ run_with_frame(lambda frame: frame.f_code)
+
+ def test_concurrent_f_back(self):
+ run_with_frame(lambda frame: frame.f_back)
+
+ def test_concurrent_f_globals(self):
+ run_with_frame(lambda frame: frame.f_globals)
+
+ def test_concurrent_f_builtins(self):
+ run_with_frame(lambda frame: frame.f_builtins)
+
+ def test_concurrent_f_locals(self):
+ run_with_frame(lambda frame: frame.f_locals)
+
+ def test_concurrent_f_trace_read(self):
+ run_with_frame(lambda frame: frame.f_trace)
+
+ def test_concurrent_f_trace_opcodes_read(self):
+ run_with_frame(lambda frame: frame.f_trace_opcodes)
+
+ def test_concurrent_repr(self):
+ run_with_frame(lambda frame: repr(frame))
+
+ def test_concurrent_f_trace_write(self):
+ def trace_func(frame, event, arg):
+ return trace_func
+
+ def writer(frame):
+ frame.f_trace = trace_func
+ frame.f_trace = None
+
+ run_with_frame(writer)
+
+ def test_concurrent_f_trace_read_write(self):
+ # Test concurrent reads and writes of f_trace on a live frame.
+ def trace_func(frame, event, arg):
+ return trace_func
+
+ def reader(frame):
+ _ = frame.f_trace
+
+ def writer(frame):
+ frame.f_trace = trace_func
+ frame.f_trace = None
+
+ run_with_frame([reader, writer, reader, writer])
+
+ def test_concurrent_f_trace_opcodes_write(self):
+ def writer(frame):
+ frame.f_trace_opcodes = True
+ frame.f_trace_opcodes = False
+
+ run_with_frame(writer)
+
+ def test_concurrent_f_trace_opcodes_read_write(self):
+ # Test concurrent reads and writes of f_trace_opcodes on a live frame.
+ def reader(frame):
+ _ = frame.f_trace_opcodes
+
+ def writer(frame):
+ frame.f_trace_opcodes = True
+ frame.f_trace_opcodes = False
+
+ run_with_frame([reader, writer, reader, writer])
+
+ def test_concurrent_frame_clear(self):
+ # Test race between frame.clear() and attribute reads.
+ def create_frame():
+ x = 1
+ y = 2
+ return sys._getframe()
+
+ frame = create_frame()
+
+ def reader():
+ for _ in range(10):
+ try:
+ _ = frame.f_locals
+ _ = frame.f_code
+ _ = frame.f_lineno
+ except ValueError:
+ # Frame may be cleared
+ pass
+
+ def clearer():
+ frame.clear()
+
+ threading_helper.run_concurrently([reader, reader, clearer])
+
+
+if __name__ == "__main__":
+ unittest.main()
frame_lasti_get_impl(PyFrameObject *self)
/*[clinic end generated code: output=03275b4f0327d1a2 input=0225ed49cb1fbeeb]*/
{
- int lasti = _PyInterpreterFrame_LASTI(self->f_frame);
+ int lasti = PyUnstable_InterpreterFrame_GetLasti(self->f_frame);
if (lasti < 0) {
return PyLong_FromLong(-1);
}
- return PyLong_FromLong(lasti * sizeof(_Py_CODEUNIT));
+ return PyLong_FromLong(lasti);
}
/*[clinic input]
frame_repr(PyObject *op)
{
PyFrameObject *f = PyFrameObject_CAST(op);
+ PyObject *result;
+ Py_BEGIN_CRITICAL_SECTION(f);
int lineno = PyFrame_GetLineNumber(f);
PyCodeObject *code = _PyFrame_GetCode(f->f_frame);
- return PyUnicode_FromFormat(
+ result = PyUnicode_FromFormat(
"<frame at %p, file %R, line %d, code %S>",
f, code->co_filename, lineno, code->co_name);
+ Py_END_CRITICAL_SECTION();
+ return result;
}
static PyMethodDef frame_methods[] = {
_PyFrame_Copy(frame, new_frame);
// _PyFrame_Copy takes the reference to the executable,
// so we need to restore it.
- frame->f_executable = PyStackRef_DUP(new_frame->f_executable);
+ new_frame->f_executable = PyStackRef_DUP(new_frame->f_executable);
f->f_frame = new_frame;
new_frame->owner = FRAME_OWNED_BY_FRAME_OBJECT;
if (_PyFrame_IsIncomplete(new_frame)) {
return PyStackRef_AsPyObjectNew(frame->f_executable);
}
-int
+// NOTE: We allow racy accesses to the instruction pointer from other threads
+// for sys._current_frames() and similar APIs.
+int _Py_NO_SANITIZE_THREAD
PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame *frame)
{
return _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT);
}
-// NOTE: We allow racy accesses to the instruction pointer from other threads
-// for sys._current_frames() and similar APIs.
int _Py_NO_SANITIZE_THREAD
PyUnstable_InterpreterFrame_GetLine(_PyInterpreterFrame *frame)
{