Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions Lib/test/test_free_threading/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import threading
import unittest

from test import support
from test.support import threading_helper

threading_helper.requires_working_threading(module=True)
Expand Down Expand Up @@ -104,6 +105,84 @@ def writer(frame):

run_with_frame([reader, writer, reader, writer])

def test_concurrent_f_trace_while_tracing(self):
frame_var = None
ready = threading.Event()
start = threading.Barrier(2, timeout=support.SHORT_TIMEOUT)

def trace(frame, event, arg):
if frame.f_code is runner.__code__:
return trace

def runner():
nonlocal frame_var
frame_var = sys._getframe()
ready.set()
start.wait()
for _ in range(100):
pass

def executor():
try:
sys.settrace(trace)
runner()
finally:
sys.settrace(None)

def reader():
self.assertTrue(ready.wait(support.SHORT_TIMEOUT))
frame = frame_var
start.wait()
for _ in range(100):
self.assertIs(frame.f_trace, trace)

threading_helper.run_concurrently([executor, reader])

def _test_concurrent_trace_flag_while_tracing(self, name):
frame_var = None
ready = threading.Event()
start = threading.Barrier(2, timeout=support.SHORT_TIMEOUT)

def trace(frame, event, arg):
if frame.f_code is runner.__code__:
return trace

def runner():
nonlocal frame_var
frame_var = sys._getframe()
ready.set()
start.wait()
for _ in range(100):
pass

def executor():
try:
sys.settrace(trace)
runner()
finally:
sys.settrace(None)

def writer():
self.assertTrue(ready.wait(support.SHORT_TIMEOUT))
frame = frame_var
start.wait()
original = getattr(frame, name)
try:
for _ in range(100):
for value in (True, False):
setattr(frame, name, value)
self.assertIs(getattr(frame, name), value)
finally:
setattr(frame, name, original)

threading_helper.run_concurrently([executor, writer])

def test_concurrent_f_trace_lines_while_tracing(self):
self._test_concurrent_trace_flag_while_tracing('f_trace_lines')

def test_concurrent_f_trace_opcodes_while_tracing(self):
self._test_concurrent_trace_flag_while_tracing('f_trace_opcodes')

def test_concurrent_f_trace_opcodes_write(self):
def writer(frame):
frame.f_trace_opcodes = True
Expand Down
108 changes: 108 additions & 0 deletions Lib/test/test_sys_settrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3074,6 +3074,114 @@ def foo(*args):
del foo
sys.settrace(sys.gettrace())

def test_local_trace_replacement(self):
events = []

def assigned(frame, event, arg):
events.append(('assigned', event))
return assigned

def replacement(frame, event, arg):
events.append(('replacement', event))
return replacement

def local_trace(frame, event, arg):
events.append(('original', event))
frame.f_trace = assigned
return replacement

def trace(frame, event, arg):
if frame.f_code is target.__code__:
return local_trace

def target():
value = 1
return value

sys.settrace(trace)
target()
sys.settrace(None)
self.assertEqual(events, [('original', 'line'),
('replacement', 'line'),
('replacement', 'return')])

def test_local_trace_clear(self):
events = []

def local_trace(frame, event, arg):
events.append(event)
frame.f_trace = None
return None

def trace(frame, event, arg):
if frame.f_code is target.__code__:
return local_trace

def target():
value = 1
return value

sys.settrace(trace)
target()
sys.settrace(None)
self.assertEqual(events, ['line'])

def test_local_trace_error(self):
frames = []

def local_trace(frame, event, arg):
raise RuntimeError('local trace error')

def trace(frame, event, arg):
if frame.f_code is target.__code__:
frames.append(frame)
return local_trace

def target():
return 1

sys.settrace(trace)
with self.assertRaisesRegex(RuntimeError, 'local trace error'):
target()
self.assertIsNone(sys.gettrace())
self.assertEqual(len(frames), 1)
self.assertIsNone(frames[0].f_trace)

def test_local_trace_finalizer_reentrancy(self):
events = []

def final_trace(frame, event, arg):
events.append(('final', event))
return final_trace

def replacement(frame, event, arg):
events.append(('replacement', event))
return replacement

class LocalTrace:
def __call__(self, frame, event, arg):
self.frame = frame
events.append(('original', event))
return replacement

def __del__(self):
events.append(('finalize', self.frame.f_trace is replacement))
self.frame.f_trace = final_trace

def trace(frame, event, arg):
if frame.f_code is target.__code__:
return LocalTrace()

def target():
value = 1
return value

sys.settrace(trace)
target()
sys.settrace(None)
self.assertEqual(events, [('original', 'line'), ('finalize', True),
('final', 'line'), ('final', 'return')])


class TestLinesAfterTraceStarted(TraceTestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix data races in frame tracing state in the free-threaded build. Protect
access to the frame trace callback and use atomic accesses for line and opcode
tracing flags.
9 changes: 5 additions & 4 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,7 @@ static PyObject *
frame_trace_opcodes_get_impl(PyFrameObject *self)
/*[clinic end generated code: output=53ff41d09cc32e87 input=4eb91dc88e04677a]*/
{
return self->f_trace_opcodes ? Py_True : Py_False;
return FT_ATOMIC_LOAD_CHAR_RELAXED(self->f_trace_opcodes) ? Py_True : Py_False;
}

/*[clinic input]
Expand All @@ -1231,13 +1231,13 @@ frame_trace_opcodes_set_impl(PyFrameObject *self, PyObject *value)
return -1;
}
if (value == Py_True) {
self->f_trace_opcodes = 1;
FT_ATOMIC_STORE_CHAR_RELAXED(self->f_trace_opcodes, 1);
if (self->f_trace) {
return _PyEval_SetOpcodeTrace(self, true);
}
}
else {
self->f_trace_opcodes = 0;
FT_ATOMIC_STORE_CHAR_RELAXED(self->f_trace_opcodes, 0);
return _PyEval_SetOpcodeTrace(self, false);
}
return 0;
Expand Down Expand Up @@ -1956,7 +1956,8 @@ frame_trace_set_impl(PyFrameObject *self, PyObject *value)
}
if (value != self->f_trace) {
Py_XSETREF(self->f_trace, Py_XNewRef(value));
if (value != NULL && self->f_trace_opcodes) {
if (value != NULL &&
FT_ATOMIC_LOAD_CHAR_RELAXED(self->f_trace_opcodes)) {
return _PyEval_SetOpcodeTrace(self, true);
}
}
Expand Down
2 changes: 1 addition & 1 deletion Python/instrumentation.c
Original file line number Diff line number Diff line change
Expand Up @@ -1367,7 +1367,7 @@ _Py_call_instrumentation_line(PyThreadState *tstate, _PyInterpreterFrame* frame,
if (frame_obj == NULL) {
return -1;
}
if (frame_obj->f_trace_lines) {
if (FT_ATOMIC_LOAD_CHAR_RELAXED(frame_obj->f_trace_lines)) {
/* Need to set tracing and what_event as if using
* the instrumentation call. */
int old_what = tstate->what_event;
Expand Down
13 changes: 8 additions & 5 deletions Python/legacy_tracing.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "pycore_ceval.h" // export _PyEval_SetProfile()
#include "pycore_frame.h" // PyFrameObject members
#include "pycore_interpframe.h" // _PyFrame_GetCode()
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_CHAR_RELAXED()

#include "opcode.h"
#include <stddef.h>
Expand Down Expand Up @@ -187,7 +188,7 @@ call_trace_func(_PyLegacyEventHandler *self, PyObject *arg)
"Missing frame when calling trace function.");
return NULL;
}
if (frame->f_trace_opcodes) {
if (FT_ATOMIC_LOAD_CHAR_RELAXED(frame->f_trace_opcodes)) {
if (_PyEval_SetOpcodeTrace(frame, true) != 0) {
return NULL;
}
Expand Down Expand Up @@ -302,7 +303,8 @@ sys_trace_instruction_func(
return NULL;
}
PyThreadState *tstate = _PyThreadState_GET();
if (!tstate->c_tracefunc || !frame->f_trace_opcodes) {
if (!tstate->c_tracefunc ||
!FT_ATOMIC_LOAD_CHAR_RELAXED(frame->f_trace_opcodes)) {
if (_PyEval_SetOpcodeTrace(frame, false) != 0) {
return NULL;
}
Expand All @@ -323,7 +325,7 @@ trace_line(
PyThreadState *tstate, _PyLegacyEventHandler *self,
PyFrameObject *frame, int line
) {
if (!frame->f_trace_lines) {
if (!FT_ATOMIC_LOAD_CHAR_RELAXED(frame->f_trace_lines)) {
Py_RETURN_NONE;
}
if (line < 0) {
Expand Down Expand Up @@ -403,7 +405,7 @@ sys_trace_jump_func(
"Missing frame when calling trace function.");
return NULL;
}
if (!frame->f_trace_lines) {
if (!FT_ATOMIC_LOAD_CHAR_RELAXED(frame->f_trace_lines)) {
Py_RETURN_NONE;
}
return trace_line(tstate, self, frame, to_line);
Expand Down Expand Up @@ -680,7 +682,8 @@ maybe_set_opcode_trace(PyThreadState *tstate)
return 0;
}
PyFrameObject *frame = iframe->frame_obj;
if (frame == NULL || !frame->f_trace_opcodes) {
if (frame == NULL ||
!FT_ATOMIC_LOAD_CHAR_RELAXED(frame->f_trace_opcodes)) {
return 0;
}
return set_opcode_trace_world_stopped(_PyFrame_GetCode(iframe), true);
Expand Down
12 changes: 10 additions & 2 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1104,25 +1104,33 @@ trace_trampoline(PyObject *self, PyFrameObject *frame,
{
PyObject *callback;
if (what == PyTrace_CALL) {
callback = self;
callback = Py_XNewRef(self);
}
else {
callback = frame->f_trace;
Py_BEGIN_CRITICAL_SECTION(frame);
callback = Py_XNewRef(frame->f_trace);
Py_END_CRITICAL_SECTION();
}
if (callback == NULL) {
return 0;
}

PyThreadState *tstate = _PyThreadState_GET();
/* The callback can change f_trace or release the thread state. */
PyObject *result = call_trampoline(tstate, callback, frame, what, arg);
Py_DECREF(callback);
if (result == NULL) {
_PyEval_SetTrace(tstate, NULL, NULL);
Py_BEGIN_CRITICAL_SECTION(frame);
Py_CLEAR(frame->f_trace);
Py_END_CRITICAL_SECTION();
return -1;
}

if (result != Py_None) {
Py_BEGIN_CRITICAL_SECTION(frame);
Py_XSETREF(frame->f_trace, result);
Py_END_CRITICAL_SECTION();
}
else {
Py_DECREF(result);
Expand Down
Loading