Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit baca85f

Browse files
xdegayeserhiy-storchaka
authored andcommitted
[2.7] bpo-17288: Prevent jumps from 'return' and 'exception' trace events. (pythonGH-6111)
(cherry picked from commit e32bbaf)
1 parent 3854f58 commit baca85f

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
@@ -482,20 +482,35 @@ def g(frame, why, extra):
482482
class JumpTracer:
483483
"""Defines a trace function that jumps from one place to another."""
484484

485-
def __init__(self, function, jumpFrom, jumpTo):
486-
self.function = function
485+
def __init__(self, function, jumpFrom, jumpTo, event='line',
486+
decorated=False):
487+
self.code = function.func_code
487488
self.jumpFrom = jumpFrom
488489
self.jumpTo = jumpTo
490+
self.event = event
491+
self.firstLine = None if decorated else self.code.co_firstlineno
489492
self.done = False
490493

491494
def trace(self, frame, event, arg):
492-
if not self.done and frame.f_code == self.function.func_code:
493-
firstLine = frame.f_code.co_firstlineno
494-
if event == 'line' and frame.f_lineno == firstLine + self.jumpFrom:
495+
if self.done:
496+
return
497+
# frame.f_code.co_firstlineno is the first line of the decorator when
498+
# 'function' is decorated and the decorator may be written using
499+
# multiple physical lines when it is too long. Use the first line
500+
# trace event in 'function' to find the first line of 'function'.
501+
if (self.firstLine is None and frame.f_code == self.code and
502+
event == 'line'):
503+
self.firstLine = frame.f_lineno - 1
504+
if (event == self.event and self.firstLine and
505+
frame.f_lineno == self.firstLine + self.jumpFrom):
506+
f = frame
507+
while f is not None and f.f_code != self.code:
508+
f = f.f_back
509+
if f is not None:
495510
# Cope with non-integer self.jumpTo (because of
496511
# no_jump_to_non_integers below).
497512
try:
498-
frame.f_lineno = firstLine + self.jumpTo
513+
frame.f_lineno = self.firstLine + self.jumpTo
499514
except TypeError:
500515
frame.f_lineno = self.jumpTo
501516
self.done = True
@@ -535,8 +550,9 @@ def compare_jump_output(self, expected, received):
535550
"Expected: " + repr(expected) + "\n" +
536551
"Received: " + repr(received))
537552

538-
def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
539-
tracer = JumpTracer(func, jumpFrom, jumpTo)
553+
def run_test(self, func, jumpFrom, jumpTo, expected, error=None,
554+
event='line', decorated=False):
555+
tracer = JumpTracer(func, jumpFrom, jumpTo, event, decorated)
540556
sys.settrace(tracer.trace)
541557
output = []
542558
if error is None:
@@ -547,15 +563,15 @@ def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
547563
sys.settrace(None)
548564
self.compare_jump_output(expected, output)
549565

550-
def jump_test(jumpFrom, jumpTo, expected, error=None):
566+
def jump_test(jumpFrom, jumpTo, expected, error=None, event='line'):
551567
"""Decorator that creates a test that makes a jump
552568
from one place to another in the following code.
553569
"""
554570
def decorator(func):
555571
@wraps(func)
556572
def test(self):
557-
# +1 to compensate a decorator line
558-
self.run_test(func, jumpFrom+1, jumpTo+1, expected, error)
573+
self.run_test(func, jumpFrom, jumpTo, expected,
574+
error=error, event=event, decorated=True)
559575
return test
560576
return decorator
561577

@@ -1018,6 +1034,36 @@ class fake_function:
10181034
sys.settrace(None)
10191035
self.compare_jump_output([2, 3, 2, 3, 4], namespace["output"])
10201036

1037+
@jump_test(2, 3, [1], event='call', error=(ValueError, "can't jump from"
1038+
" the 'call' trace event of a new frame"))
1039+
def test_no_jump_from_call(output):
1040+
output.append(1)
1041+
def nested():
1042+
output.append(3)
1043+
nested()
1044+
output.append(5)
1045+
1046+
@jump_test(2, 1, [1], event='return', error=(ValueError,
1047+
"can only jump from a 'line' trace event"))
1048+
def test_no_jump_from_return_event(output):
1049+
output.append(1)
1050+
return
1051+
1052+
@jump_test(2, 1, [1], event='exception', error=(ValueError,
1053+
"can only jump from a 'line' trace event"))
1054+
def test_no_jump_from_exception_event(output):
1055+
output.append(1)
1056+
1 / 0
1057+
1058+
@jump_test(3, 2, [2], event='return', error=(ValueError,
1059+
"can't jump from a yield statement"))
1060+
def test_no_jump_from_yield(output):
1061+
def gen():
1062+
output.append(2)
1063+
yield 3
1064+
next(gen())
1065+
output.append(5)
1066+
10211067

10221068
def test_main():
10231069
test_support.run_unittest(
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
@@ -89,6 +89,9 @@ frame_getlineno(PyFrameObject *f, void *closure)
8989
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack
9090
* needs to be set up before their code runs, and for 'for' loops the
9191
* iterator needs to be on the stack.
92+
* o Jumps cannot be made from within a trace function invoked with a
93+
* 'return' or 'exception' event since the eval loop has been exited at
94+
* that time.
9295
*/
9396
static int
9497
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
@@ -122,13 +125,32 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
122125
return -1;
123126
}
124127

128+
/* Upon the 'call' trace event of a new frame, f->f_lasti is -1 and
129+
* f->f_trace is NULL, check first on the first condition.
130+
* Forbidding jumps from the 'call' event of a new frame is a side effect
131+
* of allowing to set f_lineno only from trace functions. */
132+
if (f->f_lasti == -1) {
133+
PyErr_Format(PyExc_ValueError,
134+
"can't jump from the 'call' trace event of a new frame");
135+
return -1;
136+
}
137+
125138
/* You can only do this from within a trace function, not via
126139
* _getframe or similar hackery. */
127-
if (!f->f_trace)
128-
{
140+
if (!f->f_trace) {
129141
PyErr_Format(PyExc_ValueError,
130-
"f_lineno can only be set by a"
131-
" line trace function");
142+
"f_lineno can only be set by a trace function");
143+
return -1;
144+
}
145+
146+
/* Forbid jumps upon a 'return' trace event (except after executing a
147+
* YIELD_VALUE opcode, f_stacktop is not NULL in that case) and upon an
148+
* 'exception' trace event.
149+
* Jumps from 'call' trace events have already been forbidden above for new
150+
* frames, so this check does not change anything for 'call' events. */
151+
if (f->f_stacktop == NULL) {
152+
PyErr_SetString(PyExc_ValueError,
153+
"can only jump from a 'line' trace event");
132154
return -1;
133155
}
134156

@@ -178,6 +200,15 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
178200
min_addr = MIN(new_lasti, f->f_lasti);
179201
max_addr = MAX(new_lasti, f->f_lasti);
180202

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

0 commit comments

Comments
 (0)