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

Skip to content

Commit e32bbaf

Browse files
xdegayeserhiy-storchaka
authored andcommitted
[3.7] bpo-17288: Prevent jumps from 'return' and 'exception' trace events. (pythonGH-5928)
1 parent b4c8871 commit e32bbaf

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
@@ -556,20 +556,35 @@ def g(frame, event, arg):
556556
class JumpTracer:
557557
"""Defines a trace function that jumps from one place to another."""
558558

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

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

612-
def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
613-
tracer = JumpTracer(func, jumpFrom, jumpTo)
627+
def run_test(self, func, jumpFrom, jumpTo, expected, error=None,
628+
event='line', decorated=False):
629+
tracer = JumpTracer(func, jumpFrom, jumpTo, event, decorated)
614630
sys.settrace(tracer.trace)
615631
output = []
616632
if error is None:
@@ -621,15 +637,15 @@ def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
621637
sys.settrace(None)
622638
self.compare_jump_output(expected, output)
623639

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

@@ -1104,6 +1120,36 @@ class fake_function:
11041120
sys.settrace(None)
11051121
self.compare_jump_output([2, 3, 2, 3, 4], namespace["output"])
11061122

1123+
@jump_test(2, 3, [1], event='call', error=(ValueError, "can't jump from"
1124+
" the 'call' trace event of a new frame"))
1125+
def test_no_jump_from_call(output):
1126+
output.append(1)
1127+
def nested():
1128+
output.append(3)
1129+
nested()
1130+
output.append(5)
1131+
1132+
@jump_test(2, 1, [1], event='return', error=(ValueError,
1133+
"can only jump from a 'line' trace event"))
1134+
def test_no_jump_from_return_event(output):
1135+
output.append(1)
1136+
return
1137+
1138+
@jump_test(2, 1, [1], event='exception', error=(ValueError,
1139+
"can only jump from a 'line' trace event"))
1140+
def test_no_jump_from_exception_event(output):
1141+
output.append(1)
1142+
1 / 0
1143+
1144+
@jump_test(3, 2, [2], event='return', error=(ValueError,
1145+
"can't jump from a yield statement"))
1146+
def test_no_jump_from_yield(output):
1147+
def gen():
1148+
output.append(2)
1149+
yield 3
1150+
next(gen())
1151+
output.append(5)
1152+
11071153

11081154
if __name__ == "__main__":
11091155
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
@@ -59,6 +59,9 @@ frame_getlineno(PyFrameObject *f, void *closure)
5959
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack
6060
* needs to be set up before their code runs, and for 'for' loops the
6161
* iterator needs to be on the stack.
62+
* o Jumps cannot be made from within a trace function invoked with a
63+
* 'return' or 'exception' event since the eval loop has been exited at
64+
* that time.
6265
*/
6366
static int
6467
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
@@ -94,13 +97,32 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
9497
return -1;
9598
}
9699

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

@@ -159,6 +181,16 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
159181

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

0 commit comments

Comments
 (0)