Skip to content

Commit cf61a81

Browse files
[3.7] bpo-17288: Prevent jumps from 'return' and 'exception' trace events. (GH-5928)
(cherry picked from commit e32bbaf) Co-authored-by: xdegaye <[email protected]>
1 parent 6a526f6 commit cf61a81

File tree

3 files changed

+94
-15
lines changed

3 files changed

+94
-15
lines changed

Lib/test/test_sys_settrace.py

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

513-
def __init__(self, function, jumpFrom, jumpTo):
514-
self.function = function
513+
def __init__(self, function, jumpFrom, jumpTo, event='line',
514+
decorated=False):
515+
self.code = function.__code__
515516
self.jumpFrom = jumpFrom
516517
self.jumpTo = jumpTo
518+
self.event = event
519+
self.firstLine = None if decorated else self.code.co_firstlineno
517520
self.done = False
518521

519522
def trace(self, frame, event, arg):
520-
if not self.done and frame.f_code == self.function.__code__:
521-
firstLine = frame.f_code.co_firstlineno
522-
if event == 'line' and frame.f_lineno == firstLine + self.jumpFrom:
523+
if self.done:
524+
return
525+
# frame.f_code.co_firstlineno is the first line of the decorator when
526+
# 'function' is decorated and the decorator may be written using
527+
# multiple physical lines when it is too long. Use the first line
528+
# trace event in 'function' to find the first line of 'function'.
529+
if (self.firstLine is None and frame.f_code == self.code and
530+
event == 'line'):
531+
self.firstLine = frame.f_lineno - 1
532+
if (event == self.event and self.firstLine and
533+
frame.f_lineno == self.firstLine + self.jumpFrom):
534+
f = frame
535+
while f is not None and f.f_code != self.code:
536+
f = f.f_back
537+
if f is not None:
523538
# Cope with non-integer self.jumpTo (because of
524539
# no_jump_to_non_integers below).
525540
try:
526-
frame.f_lineno = firstLine + self.jumpTo
541+
frame.f_lineno = self.firstLine + self.jumpTo
527542
except TypeError:
528543
frame.f_lineno = self.jumpTo
529544
self.done = True
@@ -563,8 +578,9 @@ def compare_jump_output(self, expected, received):
563578
"Expected: " + repr(expected) + "\n" +
564579
"Received: " + repr(received))
565580

566-
def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
567-
tracer = JumpTracer(func, jumpFrom, jumpTo)
581+
def run_test(self, func, jumpFrom, jumpTo, expected, error=None,
582+
event='line', decorated=False):
583+
tracer = JumpTracer(func, jumpFrom, jumpTo, event, decorated)
568584
sys.settrace(tracer.trace)
569585
output = []
570586
if error is None:
@@ -575,15 +591,15 @@ def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
575591
sys.settrace(None)
576592
self.compare_jump_output(expected, output)
577593

578-
def jump_test(jumpFrom, jumpTo, expected, error=None):
594+
def jump_test(jumpFrom, jumpTo, expected, error=None, event='line'):
579595
"""Decorator that creates a test that makes a jump
580596
from one place to another in the following code.
581597
"""
582598
def decorator(func):
583599
@wraps(func)
584600
def test(self):
585-
# +1 to compensate a decorator line
586-
self.run_test(func, jumpFrom+1, jumpTo+1, expected, error)
601+
self.run_test(func, jumpFrom, jumpTo, expected,
602+
error=error, event=event, decorated=True)
587603
return test
588604
return decorator
589605

@@ -1058,6 +1074,36 @@ class fake_function:
10581074
sys.settrace(None)
10591075
self.compare_jump_output([2, 3, 2, 3, 4], namespace["output"])
10601076

1077+
@jump_test(2, 3, [1], event='call', error=(ValueError, "can't jump from"
1078+
" the 'call' trace event of a new frame"))
1079+
def test_no_jump_from_call(output):
1080+
output.append(1)
1081+
def nested():
1082+
output.append(3)
1083+
nested()
1084+
output.append(5)
1085+
1086+
@jump_test(2, 1, [1], event='return', error=(ValueError,
1087+
"can only jump from a 'line' trace event"))
1088+
def test_no_jump_from_return_event(output):
1089+
output.append(1)
1090+
return
1091+
1092+
@jump_test(2, 1, [1], event='exception', error=(ValueError,
1093+
"can only jump from a 'line' trace event"))
1094+
def test_no_jump_from_exception_event(output):
1095+
output.append(1)
1096+
1 / 0
1097+
1098+
@jump_test(3, 2, [2], event='return', error=(ValueError,
1099+
"can't jump from a yield statement"))
1100+
def test_no_jump_from_yield(output):
1101+
def gen():
1102+
output.append(2)
1103+
yield 3
1104+
next(gen())
1105+
output.append(5)
1106+
10611107

10621108
if __name__ == "__main__":
10631109
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: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ frame_getlineno(PyFrameObject *f, void *closure)
5656
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack
5757
* needs to be set up before their code runs, and for 'for' loops the
5858
* iterator needs to be on the stack.
59+
* o Jumps cannot be made from within a trace function invoked with a
60+
* 'return' or 'exception' event since the eval loop has been exited at
61+
* that time.
5962
*/
6063
static int
6164
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
@@ -91,13 +94,32 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
9194
return -1;
9295
}
9396

97+
/* Upon the 'call' trace event of a new frame, f->f_lasti is -1 and
98+
* f->f_trace is NULL, check first on the first condition.
99+
* Forbidding jumps from the 'call' event of a new frame is a side effect
100+
* of allowing to set f_lineno only from trace functions. */
101+
if (f->f_lasti == -1) {
102+
PyErr_Format(PyExc_ValueError,
103+
"can't jump from the 'call' trace event of a new frame");
104+
return -1;
105+
}
106+
94107
/* You can only do this from within a trace function, not via
95108
* _getframe or similar hackery. */
96-
if (!f->f_trace)
97-
{
109+
if (!f->f_trace) {
98110
PyErr_Format(PyExc_ValueError,
99-
"f_lineno can only be set by a"
100-
" line trace function");
111+
"f_lineno can only be set by a trace function");
112+
return -1;
113+
}
114+
115+
/* Forbid jumps upon a 'return' trace event (except after executing a
116+
* YIELD_VALUE or YIELD_FROM opcode, f_stacktop is not NULL in that case)
117+
* and upon an 'exception' trace event.
118+
* Jumps from 'call' trace events have already been forbidden above for new
119+
* frames, so this check does not change anything for 'call' events. */
120+
if (f->f_stacktop == NULL) {
121+
PyErr_SetString(PyExc_ValueError,
122+
"can only jump from a 'line' trace event");
101123
return -1;
102124
}
103125

@@ -156,6 +178,16 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
156178

157179
/* We're now ready to look at the bytecode. */
158180
PyBytes_AsStringAndSize(f->f_code->co_code, (char **)&code, &code_len);
181+
182+
/* The trace function is called with a 'return' trace event after the
183+
* execution of a yield statement. */
184+
assert(f->f_lasti != -1);
185+
if (code[f->f_lasti] == YIELD_VALUE || code[f->f_lasti] == YIELD_FROM) {
186+
PyErr_SetString(PyExc_ValueError,
187+
"can't jump from a yield statement");
188+
return -1;
189+
}
190+
159191
min_addr = Py_MIN(new_lasti, f->f_lasti);
160192
max_addr = Py_MAX(new_lasti, f->f_lasti);
161193

0 commit comments

Comments
 (0)