Skip to content

Commit e548ee2

Browse files
authored
Swallow potential exceptions from showtraceback() (#13934)
The nbgrader project is aware of a form of cheating where students disrupt `InteractiveShell.showtraceback` in hopes of hiding exceptions to avoid losing points. They have implemented a solution to prevent this cheating from working on the client side, and have some tests to demonstrate this technique: https://github.com/jupyter/nbgrader/blob/main/nbgrader/tests/apps/files/submitted-cheat-attempt.ipynb https://github.com/jupyter/nbgrader/blob/main/nbgrader/tests/apps/files/submitted-cheat-attempt-alternative.ipynb In essence, these attacks import the interactive shell and erase the traceback handler so that their failing tests won't report failures. ```python import IPython.core.interactiveshell IPython.core.interactiveshell.InteractiveShell.showtraceback = None ``` The problem is that this causes an exception inside the kernel, leading to a stalled execution. The kernel has stopped working, but the client continues to wait for messages. So far, nbgrader's solution to this is to require a timeout value so the client can eventually decide it is done. This prevents allowing a value of `None` for `Execute.timeout` because this would cause a test case to infinitely hang. This commit addresses the problem by making `InteractiveShell._run_cell` a little more protective around it's call to `showtraceback()`. There is already a try/except block around running the cell. This commit adds a finally clause so that the method will _always_ return an `ExecutionResult`, even if a new exception is thrown within the except clause. For the record, the exception thrown is: TypeError: 'NoneType' object is not callable Accepting this change will allow nbgrader to update `nbgrader.preprocessors.Execute` to support a type of `Integer(allow_none=True)` as the parent `NotebookClient` intended. Discussion about this is ongoing in jupyter/nbgrader#1690.
2 parents 0694b08 + a011765 commit e548ee2

File tree

2 files changed

+48
-1
lines changed

2 files changed

+48
-1
lines changed

IPython/core/interactiveshell.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3013,14 +3013,15 @@ def _run_cell(
30133013
runner = _pseudo_sync_runner
30143014

30153015
try:
3016-
return runner(coro)
3016+
result = runner(coro)
30173017
except BaseException as e:
30183018
info = ExecutionInfo(
30193019
raw_cell, store_history, silent, shell_futures, cell_id
30203020
)
30213021
result = ExecutionResult(info)
30223022
result.error_in_exec = e
30233023
self.showtraceback(running_compiled_code=True)
3024+
finally:
30243025
return result
30253026

30263027
def should_run_async(

IPython/core/tests/test_interactiveshell.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,3 +1127,49 @@ def foo(*args, **kwargs):
11271127

11281128
# clean up
11291129
ip.Completer.custom_matchers.pop()
1130+
1131+
1132+
class TestShowTracebackAttack(unittest.TestCase):
1133+
"""Test that the interactive shell is resilient against the client attack of
1134+
manipulating the showtracebacks method. These attacks shouldn't result in an
1135+
unhandled exception in the kernel."""
1136+
1137+
def setUp(self):
1138+
self.orig_showtraceback = interactiveshell.InteractiveShell.showtraceback
1139+
1140+
def tearDown(self):
1141+
interactiveshell.InteractiveShell.showtraceback = self.orig_showtraceback
1142+
1143+
def test_set_show_tracebacks_none(self):
1144+
"""Test the case of the client setting showtracebacks to None"""
1145+
1146+
result = ip.run_cell(
1147+
"""
1148+
import IPython.core.interactiveshell
1149+
IPython.core.interactiveshell.InteractiveShell.showtraceback = None
1150+
1151+
assert False, "This should not raise an exception"
1152+
"""
1153+
)
1154+
print(result)
1155+
1156+
assert result.result is None
1157+
assert isinstance(result.error_in_exec, TypeError)
1158+
assert str(result.error_in_exec) == "'NoneType' object is not callable"
1159+
1160+
def test_set_show_tracebacks_noop(self):
1161+
"""Test the case of the client setting showtracebacks to a no op lambda"""
1162+
1163+
result = ip.run_cell(
1164+
"""
1165+
import IPython.core.interactiveshell
1166+
IPython.core.interactiveshell.InteractiveShell.showtraceback = lambda *args, **kwargs: None
1167+
1168+
assert False, "This should not raise an exception"
1169+
"""
1170+
)
1171+
print(result)
1172+
1173+
assert result.result is None
1174+
assert isinstance(result.error_in_exec, AssertionError)
1175+
assert str(result.error_in_exec) == "This should not raise an exception"

0 commit comments

Comments
 (0)