[3.7] bpo-17288: Prevent jumps from 'return' and 'exception' trace events. (GH-5928)
(cherry picked from commit e32bbaf376
)
Co-authored-by: xdegaye <xdegaye@gmail.com>
This commit is contained in:
parent
6a526f6738
commit
cf61a81f1d
|
@ -510,20 +510,35 @@ class RaisingTraceFuncTestCase(unittest.TestCase):
|
||||||
class JumpTracer:
|
class JumpTracer:
|
||||||
"""Defines a trace function that jumps from one place to another."""
|
"""Defines a trace function that jumps from one place to another."""
|
||||||
|
|
||||||
def __init__(self, function, jumpFrom, jumpTo):
|
def __init__(self, function, jumpFrom, jumpTo, event='line',
|
||||||
self.function = function
|
decorated=False):
|
||||||
|
self.code = function.__code__
|
||||||
self.jumpFrom = jumpFrom
|
self.jumpFrom = jumpFrom
|
||||||
self.jumpTo = jumpTo
|
self.jumpTo = jumpTo
|
||||||
|
self.event = event
|
||||||
|
self.firstLine = None if decorated else self.code.co_firstlineno
|
||||||
self.done = False
|
self.done = False
|
||||||
|
|
||||||
def trace(self, frame, event, arg):
|
def trace(self, frame, event, arg):
|
||||||
if not self.done and frame.f_code == self.function.__code__:
|
if self.done:
|
||||||
firstLine = frame.f_code.co_firstlineno
|
return
|
||||||
if event == 'line' and frame.f_lineno == firstLine + self.jumpFrom:
|
# 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
|
# Cope with non-integer self.jumpTo (because of
|
||||||
# no_jump_to_non_integers below).
|
# no_jump_to_non_integers below).
|
||||||
try:
|
try:
|
||||||
frame.f_lineno = firstLine + self.jumpTo
|
frame.f_lineno = self.firstLine + self.jumpTo
|
||||||
except TypeError:
|
except TypeError:
|
||||||
frame.f_lineno = self.jumpTo
|
frame.f_lineno = self.jumpTo
|
||||||
self.done = True
|
self.done = True
|
||||||
|
@ -563,8 +578,9 @@ class JumpTestCase(unittest.TestCase):
|
||||||
"Expected: " + repr(expected) + "\n" +
|
"Expected: " + repr(expected) + "\n" +
|
||||||
"Received: " + repr(received))
|
"Received: " + repr(received))
|
||||||
|
|
||||||
def run_test(self, func, jumpFrom, jumpTo, expected, error=None):
|
def run_test(self, func, jumpFrom, jumpTo, expected, error=None,
|
||||||
tracer = JumpTracer(func, jumpFrom, jumpTo)
|
event='line', decorated=False):
|
||||||
|
tracer = JumpTracer(func, jumpFrom, jumpTo, event, decorated)
|
||||||
sys.settrace(tracer.trace)
|
sys.settrace(tracer.trace)
|
||||||
output = []
|
output = []
|
||||||
if error is None:
|
if error is None:
|
||||||
|
@ -575,15 +591,15 @@ class JumpTestCase(unittest.TestCase):
|
||||||
sys.settrace(None)
|
sys.settrace(None)
|
||||||
self.compare_jump_output(expected, output)
|
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
|
"""Decorator that creates a test that makes a jump
|
||||||
from one place to another in the following code.
|
from one place to another in the following code.
|
||||||
"""
|
"""
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def test(self):
|
def test(self):
|
||||||
# +1 to compensate a decorator line
|
self.run_test(func, jumpFrom, jumpTo, expected,
|
||||||
self.run_test(func, jumpFrom+1, jumpTo+1, expected, error)
|
error=error, event=event, decorated=True)
|
||||||
return test
|
return test
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@ -1058,6 +1074,36 @@ output.append(4)
|
||||||
sys.settrace(None)
|
sys.settrace(None)
|
||||||
self.compare_jump_output([2, 3, 2, 3, 4], namespace["output"])
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Prevent jumps from 'return' and 'exception' trace events.
|
|
@ -56,6 +56,9 @@ frame_getlineno(PyFrameObject *f, void *closure)
|
||||||
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack
|
* 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
|
* needs to be set up before their code runs, and for 'for' loops the
|
||||||
* iterator needs to be on the stack.
|
* 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
|
static int
|
||||||
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
|
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
|
||||||
|
@ -91,13 +94,32 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
|
||||||
return -1;
|
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
|
/* You can only do this from within a trace function, not via
|
||||||
* _getframe or similar hackery. */
|
* _getframe or similar hackery. */
|
||||||
if (!f->f_trace)
|
if (!f->f_trace) {
|
||||||
{
|
|
||||||
PyErr_Format(PyExc_ValueError,
|
PyErr_Format(PyExc_ValueError,
|
||||||
"f_lineno can only be set by a"
|
"f_lineno can only be set by a trace function");
|
||||||
" line 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +178,16 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
|
||||||
|
|
||||||
/* We're now ready to look at the bytecode. */
|
/* We're now ready to look at the bytecode. */
|
||||||
PyBytes_AsStringAndSize(f->f_code->co_code, (char **)&code, &code_len);
|
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);
|
min_addr = Py_MIN(new_lasti, f->f_lasti);
|
||||||
max_addr = Py_MAX(new_lasti, f->f_lasti);
|
max_addr = Py_MAX(new_lasti, f->f_lasti);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue