Skip to content

Commit b8e9d6c

Browse files
xdegayeserhiy-storchaka
authored andcommitted
bpo-17288: Prevent jumps from 'return' and 'exception' trace events. (GH-6107)
(cherry picked from commit e32bbaf)
1 parent 5affd5c commit b8e9d6c

File tree

3 files changed

+93
-15
lines changed

3 files changed

+93
-15
lines changed

Lib/test/test_sys_settrace.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -555,20 +555,35 @@ def g(frame, event, arg):
555555
class JumpTracer:
556556
"""Defines a trace function that jumps from one place to another."""
557557

558-
def __init__(self, function, jumpFrom, jumpTo):
559-
self.function = function
558+
def __init__(self, function, jumpFrom, jumpTo, event='line',
559+
decorated=False):
560+
self.code = function.__code__
560561
self.jumpFrom = jumpFrom
561562
self.jumpTo = jumpTo
563+
self.event = event
564+
self.firstLine = None if decorated else self.code.co_firstlineno
562565
self.done = False
563566

564567
def trace(self, frame, event, arg):
565-
if not self.done and frame.f_code == self.function.__code__:
566-
firstLine = frame.f_code.co_firstlineno
567-
if event == 'line' and frame.f_lineno == firstLine + self.jumpFrom:
568+
if self.done:
569+
return
570+
# frame.f_code.co_firstlineno is the first line of the decorator when
571+
# 'function' is decorated and the decorator may be written using
572+
# multiple physical lines when it is too long. Use the first line
573+
# trace event in 'function' to find the first line of 'function'.
574+
if (self.firstLine is None and frame.f_code == self.code and
575+
event == 'line'):
576+
self.firstLine = frame.f_lineno - 1
577+
if (event == self.event and self.firstLine and
578+
frame.f_lineno == self.firstLine + self.jumpFrom):
579+
f = frame
580+
while f is not None and f.f_code != self.code:
581+
f = f.f_back
582+
if f is not None:
568583
# Cope with non-integer self.jumpTo (because of
569584
# no_jump_to_non_integers below).
570585
try:
571-
frame.f_lineno = firstLine + self.jumpTo
586+
frame.f_lineno = self.firstLine + self.jumpTo
572587
except TypeError:
573588
frame.f_lineno = self.jumpTo
574589
self.done = True
@@ -608,8 +623,9 @@ def compare_jump_output(self, expected, received):
608623
"Expected: " + repr(expected) + "\n" +
609624
"Received: " + repr(received))
610625

611-
def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
612-
tracer = JumpTracer(func, jumpFrom, jumpTo)
626+
def run_test(self, func, jumpFrom, jumpTo, expected, error=None,
627+
event='line', decorated=False):
628+
tracer = JumpTracer(func, jumpFrom, jumpTo, event, decorated)
613629
sys.settrace(tracer.trace)
614630
output = []
615631
if error is None:
@@ -620,15 +636,15 @@ def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
620636
sys.settrace(None)
621637
self.compare_jump_output(expected, output)
622638

623-
def jump_test(jumpFrom, jumpTo, expected, error=None):
639+
def jump_test(jumpFrom, jumpTo, expected, error=None, event='line'):
624640
"""Decorator that creates a test that makes a jump
625641
from one place to another in the following code.
626642
"""
627643
def decorator(func):
628644
@wraps(func)
629645
def test(self):
630-
# +1 to compensate a decorator line
631-
self.run_test(func, jumpFrom+1, jumpTo+1, expected, error)
646+
self.run_test(func, jumpFrom, jumpTo, expected,
647+
error=error, event=event, decorated=True)
632648
return test
633649
return decorator
634650

@@ -1128,6 +1144,36 @@ class fake_function:
11281144
sys.settrace(None)
11291145
self.compare_jump_output([2, 3, 2, 3, 4], namespace["output"])
11301146

1147+
@jump_test(2, 3, [1], event='call', error=(ValueError, "can't jump from"
1148+
" the 'call' trace event of a new frame"))
1149+
def test_no_jump_from_call(output):
1150+
output.append(1)
1151+
def nested():
1152+
output.append(3)
1153+
nested()
1154+
output.append(5)
1155+
1156+
@jump_test(2, 1, [1], event='return', error=(ValueError,
1157+
"can only jump from a 'line' trace event"))
1158+
def test_no_jump_from_return_event(output):
1159+
output.append(1)
1160+
return
1161+
1162+
@jump_test(2, 1, [1], event='exception', error=(ValueError,
1163+
"can only jump from a 'line' trace event"))
1164+
def test_no_jump_from_exception_event(output):
1165+
output.append(1)
1166+
1 / 0
1167+
1168+
@jump_test(3, 2, [2], event='return', error=(ValueError,
1169+
"can't jump from a yield statement"))
1170+
def test_no_jump_from_yield(output):
1171+
def gen():
1172+
output.append(2)
1173+
yield 3
1174+
next(gen())
1175+
output.append(5)
1176+
11311177

11321178
if __name__ == "__main__":
11331179
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prevent jumps from 'return' and 'exception' trace events.

Objects/frameobject.c

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ get_arg(const _Py_CODEUNIT *codestr, Py_ssize_t i)
8181
* the blockstack needs to be set up before their code runs.
8282
* o 'for' and 'async for' loops can't be jumped into because the
8383
* iterator needs to be on the stack.
84+
* o Jumps cannot be made from within a trace function invoked with a
85+
* 'return' or 'exception' event since the eval loop has been exited at
86+
* that time.
8487
*/
8588
static int
8689
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
@@ -109,13 +112,32 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
109112
return -1;
110113
}
111114

115+
/* Upon the 'call' trace event of a new frame, f->f_lasti is -1 and
116+
* f->f_trace is NULL, check first on the first condition.
117+
* Forbidding jumps from the 'call' event of a new frame is a side effect
118+
* of allowing to set f_lineno only from trace functions. */
119+
if (f->f_lasti == -1) {
120+
PyErr_Format(PyExc_ValueError,
121+
"can't jump from the 'call' trace event of a new frame");
122+
return -1;
123+
}
124+
112125
/* You can only do this from within a trace function, not via
113126
* _getframe or similar hackery. */
114-
if (!f->f_trace)
115-
{
127+
if (!f->f_trace) {
116128
PyErr_Format(PyExc_ValueError,
117-
"f_lineno can only be set by a"
118-
" line trace function");
129+
"f_lineno can only be set by a trace function");
130+
return -1;
131+
}
132+
133+
/* Forbid jumps upon a 'return' trace event (except after executing a
134+
* YIELD_VALUE or YIELD_FROM opcode, f_stacktop is not NULL in that case)
135+
* and upon an 'exception' trace event.
136+
* Jumps from 'call' trace events have already been forbidden above for new
137+
* frames, so this check does not change anything for 'call' events. */
138+
if (f->f_stacktop == NULL) {
139+
PyErr_SetString(PyExc_ValueError,
140+
"can only jump from a 'line' trace event");
119141
return -1;
120142
}
121143

@@ -175,6 +197,15 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
175197
/* We're now ready to look at the bytecode. */
176198
PyBytes_AsStringAndSize(f->f_code->co_code, (char **)&code, &code_len);
177199

200+
/* The trace function is called with a 'return' trace event after the
201+
* execution of a yield statement. */
202+
assert(f->f_lasti != -1);
203+
if (code[f->f_lasti] == YIELD_VALUE || code[f->f_lasti] == YIELD_FROM) {
204+
PyErr_SetString(PyExc_ValueError,
205+
"can't jump from a yield statement");
206+
return -1;
207+
}
208+
178209
/* You can't jump onto a line with an 'except' statement on it -
179210
* they expect to have an exception on the top of the stack, which
180211
* won't be true if you jump to them. They always start with code

0 commit comments

Comments
 (0)