Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.6] bpo-17288: Prevent jumps from 'return' and 'exception' trace events. (GH-5928) #6100

Merged
merged 1 commit into from Mar 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 57 additions & 11 deletions Lib/test/test_sys_settrace.py
Expand Up @@ -510,20 +510,35 @@ def g(frame, event, arg):
class JumpTracer:
"""Defines a trace function that jumps from one place to another."""

def __init__(self, function, jumpFrom, jumpTo):
self.function = function
def __init__(self, function, jumpFrom, jumpTo, event='line',
decorated=False):
self.code = function.__code__
self.jumpFrom = jumpFrom
self.jumpTo = jumpTo
self.event = event
self.firstLine = None if decorated else self.code.co_firstlineno
self.done = False

def trace(self, frame, event, arg):
if not self.done and frame.f_code == self.function.__code__:
firstLine = frame.f_code.co_firstlineno
if event == 'line' and frame.f_lineno == firstLine + self.jumpFrom:
if self.done:
return
# frame.f_code.co_firstlineno is the first line of the decorator when
# 'function' is decorated and the decorator may be written using
# multiple physical lines when it is too long. Use the first line
# trace event in 'function' to find the first line of 'function'.
if (self.firstLine is None and frame.f_code == self.code and
event == 'line'):
self.firstLine = frame.f_lineno - 1
if (event == self.event and self.firstLine and
frame.f_lineno == self.firstLine + self.jumpFrom):
f = frame
while f is not None and f.f_code != self.code:
f = f.f_back
if f is not None:
# Cope with non-integer self.jumpTo (because of
# no_jump_to_non_integers below).
try:
frame.f_lineno = firstLine + self.jumpTo
frame.f_lineno = self.firstLine + self.jumpTo
except TypeError:
frame.f_lineno = self.jumpTo
self.done = True
Expand Down Expand Up @@ -563,8 +578,9 @@ def compare_jump_output(self, expected, received):
"Expected: " + repr(expected) + "\n" +
"Received: " + repr(received))

def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
tracer = JumpTracer(func, jumpFrom, jumpTo)
def run_test(self, func, jumpFrom, jumpTo, expected, error=None,
event='line', decorated=False):
tracer = JumpTracer(func, jumpFrom, jumpTo, event, decorated)
sys.settrace(tracer.trace)
output = []
if error is None:
Expand All @@ -575,15 +591,15 @@ def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
sys.settrace(None)
self.compare_jump_output(expected, output)

def jump_test(jumpFrom, jumpTo, expected, error=None):
def jump_test(jumpFrom, jumpTo, expected, error=None, event='line'):
"""Decorator that creates a test that makes a jump
from one place to another in the following code.
"""
def decorator(func):
@wraps(func)
def test(self):
# +1 to compensate a decorator line
self.run_test(func, jumpFrom+1, jumpTo+1, expected, error)
self.run_test(func, jumpFrom, jumpTo, expected,
error=error, event=event, decorated=True)
return test
return decorator

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

@jump_test(2, 3, [1], event='call', error=(ValueError, "can't jump from"
" the 'call' trace event of a new frame"))
def test_no_jump_from_call(output):
output.append(1)
def nested():
output.append(3)
nested()
output.append(5)

@jump_test(2, 1, [1], event='return', error=(ValueError,
"can only jump from a 'line' trace event"))
def test_no_jump_from_return_event(output):
output.append(1)
return

@jump_test(2, 1, [1], event='exception', error=(ValueError,
"can only jump from a 'line' trace event"))
def test_no_jump_from_exception_event(output):
output.append(1)
1 / 0

@jump_test(3, 2, [2], event='return', error=(ValueError,
"can't jump from a yield statement"))
def test_no_jump_from_yield(output):
def gen():
output.append(2)
yield 3
next(gen())
output.append(5)


if __name__ == "__main__":
unittest.main()
@@ -0,0 +1 @@
Prevent jumps from 'return' and 'exception' trace events.
40 changes: 36 additions & 4 deletions Objects/frameobject.c
Expand Up @@ -56,6 +56,9 @@ frame_getlineno(PyFrameObject *f, void *closure)
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack
* needs to be set up before their code runs, and for 'for' loops the
* iterator needs to be on the stack.
* o Jumps cannot be made from within a trace function invoked with a
* 'return' or 'exception' event since the eval loop has been exited at
* that time.
*/
static int
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
Expand Down Expand Up @@ -91,13 +94,32 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
return -1;
}

/* Upon the 'call' trace event of a new frame, f->f_lasti is -1 and
* f->f_trace is NULL, check first on the first condition.
* Forbidding jumps from the 'call' event of a new frame is a side effect
* of allowing to set f_lineno only from trace functions. */
if (f->f_lasti == -1) {
PyErr_Format(PyExc_ValueError,
"can't jump from the 'call' trace event of a new frame");
return -1;
}

/* You can only do this from within a trace function, not via
* _getframe or similar hackery. */
if (!f->f_trace)
{
if (!f->f_trace) {
PyErr_Format(PyExc_ValueError,
"f_lineno can only be set by a"
" line trace function");
"f_lineno can only be set by a trace function");
return -1;
}

/* Forbid jumps upon a 'return' trace event (except after executing a
* YIELD_VALUE or YIELD_FROM opcode, f_stacktop is not NULL in that case)
* and upon an 'exception' trace event.
* Jumps from 'call' trace events have already been forbidden above for new
* frames, so this check does not change anything for 'call' events. */
if (f->f_stacktop == NULL) {
PyErr_SetString(PyExc_ValueError,
"can only jump from a 'line' trace event");
return -1;
}

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

/* We're now ready to look at the bytecode. */
PyBytes_AsStringAndSize(f->f_code->co_code, (char **)&code, &code_len);

/* The trace function is called with a 'return' trace event after the
* execution of a yield statement. */
assert(f->f_lasti != -1);
if (code[f->f_lasti] == YIELD_VALUE || code[f->f_lasti] == YIELD_FROM) {
PyErr_SetString(PyExc_ValueError,
"can't jump from a yield statement");
return -1;
}

min_addr = Py_MIN(new_lasti, f->f_lasti);
max_addr = Py_MAX(new_lasti, f->f_lasti);

Expand Down