Skip to content

Commit bea33f5

Browse files
authored
bpo-38920: Add audit hooks for when sys.excepthook and sys.unraisable hooks are invoked (GH-17392)
Also fixes some potential segfaults in unraisable hook handling.
1 parent 02519f7 commit bea33f5

File tree

7 files changed

+153
-47
lines changed

7 files changed

+153
-47
lines changed

Doc/library/sys.rst

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,18 @@ always available.
3636
.. audit-event:: sys.addaudithook "" sys.addaudithook
3737

3838
Raise an auditing event ``sys.addaudithook`` with no arguments. If any
39-
existing hooks raise an exception derived from :class:`Exception`, the
39+
existing hooks raise an exception derived from :class:`RuntimeError`, the
4040
new hook will not be added and the exception suppressed. As a result,
4141
callers cannot assume that their hook has been added unless they control
4242
all existing hooks.
4343

4444
.. versionadded:: 3.8
4545

46+
.. versionchanged:: 3.8.1
47+
48+
Exceptions derived from :class:`Exception` but not :class:`RuntimeError`
49+
are no longer suppressed.
50+
4651
.. impl-detail::
4752

4853
When tracing is enabled (see :func:`settrace`), Python hooks are only
@@ -308,6 +313,15 @@ always available.
308313
before the program exits. The handling of such top-level exceptions can be
309314
customized by assigning another three-argument function to ``sys.excepthook``.
310315

316+
.. audit-event:: sys.excepthook hook,type,value,traceback sys.excepthook
317+
318+
Raise an auditing event ``sys.excepthook`` with arguments ``hook``,
319+
``type``, ``value``, ``traceback`` when an uncaught exception occurs.
320+
If no hook has been set, ``hook`` may be ``None``. If any hook raises
321+
an exception derived from :class:`RuntimeError` the call to the hook will
322+
be suppressed. Otherwise, the audit hook exception will be reported as
323+
unraisable and ``sys.excepthook`` will be called.
324+
311325
.. seealso::
312326

313327
The :func:`sys.unraisablehook` function handles unraisable exceptions
@@ -1540,6 +1554,13 @@ always available.
15401554

15411555
See also :func:`excepthook` which handles uncaught exceptions.
15421556

1557+
.. audit-event:: sys.unraisablehook hook,unraisable sys.unraisablehook
1558+
1559+
Raise an auditing event ``sys.unraisablehook`` with arguments
1560+
``hook``, ``unraisable`` when an exception that cannot be handled occurs.
1561+
The ``unraisable`` object is the same as what will be passed to the hook.
1562+
If no hook has been set, ``hook`` may be ``None``.
1563+
15431564
.. versionadded:: 3.8
15441565

15451566
.. data:: version

Lib/test/audit-tests.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,50 @@ def trace(frame, event, *args):
263263

264264
def test_mmap():
265265
import mmap
266+
266267
with TestHook() as hook:
267268
mmap.mmap(-1, 8)
268269
assertEqual(hook.seen[0][1][:2], (-1, 8))
269270

270271

272+
def test_excepthook():
273+
def excepthook(exc_type, exc_value, exc_tb):
274+
if exc_type is not RuntimeError:
275+
sys.__excepthook__(exc_type, exc_value, exc_tb)
276+
277+
def hook(event, args):
278+
if event == "sys.excepthook":
279+
if not isinstance(args[2], args[1]):
280+
raise TypeError(f"Expected isinstance({args[2]!r}, " f"{args[1]!r})")
281+
if args[0] != excepthook:
282+
raise ValueError(f"Expected {args[0]} == {excepthook}")
283+
print(event, repr(args[2]))
284+
285+
sys.addaudithook(hook)
286+
sys.excepthook = excepthook
287+
raise RuntimeError("fatal-error")
288+
289+
290+
def test_unraisablehook():
291+
from _testcapi import write_unraisable_exc
292+
293+
def unraisablehook(hookargs):
294+
pass
295+
296+
def hook(event, args):
297+
if event == "sys.unraisablehook":
298+
if args[0] != unraisablehook:
299+
raise ValueError(f"Expected {args[0]} == {unraisablehook}")
300+
print(event, repr(args[1].exc_value), args[1].err_msg)
301+
302+
sys.addaudithook(hook)
303+
sys.unraisablehook = unraisablehook
304+
write_unraisable_exc(RuntimeError("nonfatal-error"), "for audit hook test", None)
305+
306+
271307
if __name__ == "__main__":
272308
from test.libregrtest.setup import suppress_msvcrt_asserts
309+
273310
suppress_msvcrt_asserts(False)
274311

275312
test = sys.argv[1]

Lib/test/test_audit.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,23 @@ def do_test(self, *args):
2424
sys.stdout.writelines(p.stdout)
2525
sys.stderr.writelines(p.stderr)
2626
if p.returncode:
27-
self.fail(''.join(p.stderr))
27+
self.fail("".join(p.stderr))
28+
29+
def run_python(self, *args):
30+
events = []
31+
with subprocess.Popen(
32+
[sys.executable, "-X utf8", AUDIT_TESTS_PY, *args],
33+
encoding="utf-8",
34+
stdout=subprocess.PIPE,
35+
stderr=subprocess.PIPE,
36+
) as p:
37+
p.wait()
38+
sys.stderr.writelines(p.stderr)
39+
return (
40+
p.returncode,
41+
[line.strip().partition(" ") for line in p.stdout],
42+
"".join(p.stderr),
43+
)
2844

2945
def test_basic(self):
3046
self.do_test("test_basic")
@@ -36,19 +52,11 @@ def test_block_add_hook_baseexception(self):
3652
self.do_test("test_block_add_hook_baseexception")
3753

3854
def test_finalize_hooks(self):
39-
events = []
40-
with subprocess.Popen(
41-
[sys.executable, "-X utf8", AUDIT_TESTS_PY, "test_finalize_hooks"],
42-
encoding="utf-8",
43-
stdout=subprocess.PIPE,
44-
stderr=subprocess.PIPE,
45-
) as p:
46-
p.wait()
47-
for line in p.stdout:
48-
events.append(line.strip().partition(" "))
49-
sys.stderr.writelines(p.stderr)
50-
if p.returncode:
51-
self.fail(''.join(p.stderr))
55+
returncode, events, stderr = self.run_python("test_finalize_hooks")
56+
if stderr:
57+
print(stderr, file=sys.stderr)
58+
if returncode:
59+
self.fail(stderr)
5260

5361
firstId = events[0][2]
5462
self.assertSequenceEqual(
@@ -76,6 +84,26 @@ def test_cantrace(self):
7684
def test_mmap(self):
7785
self.do_test("test_mmap")
7886

87+
def test_excepthook(self):
88+
returncode, events, stderr = self.run_python("test_excepthook")
89+
if not returncode:
90+
self.fail(f"Expected fatal exception\n{stderr}")
91+
92+
self.assertSequenceEqual(
93+
[("sys.excepthook", " ", "RuntimeError('fatal-error')")], events
94+
)
95+
96+
def test_unraisablehook(self):
97+
returncode, events, stderr = self.run_python("test_unraisablehook")
98+
if returncode:
99+
self.fail(stderr)
100+
101+
self.assertEqual(events[0][0], "sys.unraisablehook")
102+
self.assertEqual(
103+
events[0][2],
104+
"RuntimeError('nonfatal-error') Exception ignored for audit hook test",
105+
)
106+
79107

80108
if __name__ == "__main__":
81109
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add audit hooks for when :func:`sys.excepthook` and
2+
:func:`sys.unraisablehook` are invoked

Python/errors.c

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,43 +1391,53 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
13911391
}
13921392
}
13931393

1394+
PyObject *hook_args = make_unraisable_hook_args(
1395+
tstate, exc_type, exc_value, exc_tb, err_msg, obj);
1396+
if (hook_args == NULL) {
1397+
err_msg_str = ("Exception ignored on building "
1398+
"sys.unraisablehook arguments");
1399+
goto error;
1400+
}
1401+
13941402
_Py_IDENTIFIER(unraisablehook);
13951403
PyObject *hook = _PySys_GetObjectId(&PyId_unraisablehook);
1396-
if (hook != NULL && hook != Py_None) {
1397-
PyObject *hook_args;
1398-
1399-
hook_args = make_unraisable_hook_args(tstate, exc_type, exc_value,
1400-
exc_tb, err_msg, obj);
1401-
if (hook_args != NULL) {
1402-
PyObject *res = _PyObject_CallOneArg(hook, hook_args);
1403-
Py_DECREF(hook_args);
1404-
if (res != NULL) {
1405-
Py_DECREF(res);
1406-
goto done;
1407-
}
1408-
1409-
err_msg_str = "Exception ignored in sys.unraisablehook";
1410-
}
1411-
else {
1412-
err_msg_str = ("Exception ignored on building "
1413-
"sys.unraisablehook arguments");
1414-
}
1404+
if (hook == NULL) {
1405+
Py_DECREF(hook_args);
1406+
goto default_hook;
1407+
}
14151408

1416-
Py_XDECREF(err_msg);
1417-
err_msg = PyUnicode_FromString(err_msg_str);
1418-
if (err_msg == NULL) {
1419-
PyErr_Clear();
1420-
}
1409+
if (PySys_Audit("sys.unraisablehook", "OO", hook, hook_args) < 0) {
1410+
Py_DECREF(hook_args);
1411+
err_msg_str = "Exception ignored in audit hook";
1412+
obj = NULL;
1413+
goto error;
1414+
}
14211415

1422-
/* sys.unraisablehook failed: log its error using default hook */
1423-
Py_XDECREF(exc_type);
1424-
Py_XDECREF(exc_value);
1425-
Py_XDECREF(exc_tb);
1426-
_PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
1416+
if (hook == Py_None) {
1417+
Py_DECREF(hook_args);
1418+
goto default_hook;
1419+
}
14271420

1428-
obj = hook;
1421+
PyObject *res = _PyObject_CallOneArg(hook, hook_args);
1422+
Py_DECREF(hook_args);
1423+
if (res != NULL) {
1424+
Py_DECREF(res);
1425+
goto done;
14291426
}
14301427

1428+
/* sys.unraisablehook failed: log its error using default hook */
1429+
obj = hook;
1430+
err_msg_str = NULL;
1431+
1432+
error:
1433+
/* err_msg_str and obj have been updated and we have a new exception */
1434+
Py_XSETREF(err_msg, PyUnicode_FromString(err_msg_str ?
1435+
err_msg_str : "Exception ignored in sys.unraisablehook"));
1436+
Py_XDECREF(exc_type);
1437+
Py_XDECREF(exc_value);
1438+
Py_XDECREF(exc_tb);
1439+
_PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
1440+
14311441
default_hook:
14321442
/* Call the default unraisable hook (ignore failure) */
14331443
(void)write_unraisable_exc(tstate, exc_type, exc_value, exc_tb,

Python/pythonrun.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,14 @@ _PyErr_PrintEx(PyThreadState *tstate, int set_sys_last_vars)
695695
}
696696
}
697697
hook = _PySys_GetObjectId(&PyId_excepthook);
698+
if (PySys_Audit("sys.excepthook", "OOOO", hook ? hook : Py_None,
699+
exception, v, tb) < 0) {
700+
if (PyErr_ExceptionMatches(PyExc_RuntimeError)) {
701+
PyErr_Clear();
702+
goto done;
703+
}
704+
_PyErr_WriteUnraisableMsg("in audit hook", NULL);
705+
}
698706
if (hook) {
699707
PyObject* stack[3];
700708
PyObject *result;

Python/sysmodule.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,8 @@ PySys_AddAuditHook(Py_AuditHookFunction hook, void *userData)
323323
/* Cannot invoke hooks until we are initialized */
324324
if (runtime->initialized) {
325325
if (PySys_Audit("sys.addaudithook", NULL) < 0) {
326-
if (_PyErr_ExceptionMatches(tstate, PyExc_Exception)) {
327-
/* We do not report errors derived from Exception */
326+
if (_PyErr_ExceptionMatches(tstate, PyExc_RuntimeError)) {
327+
/* We do not report errors derived from RuntimeError */
328328
_PyErr_Clear(tstate);
329329
return 0;
330330
}

0 commit comments

Comments
 (0)