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.
This commit is contained in:
parent
02519f75d1
commit
bea33f5e1d
|
@ -36,13 +36,18 @@ always available.
|
||||||
.. audit-event:: sys.addaudithook "" sys.addaudithook
|
.. audit-event:: sys.addaudithook "" sys.addaudithook
|
||||||
|
|
||||||
Raise an auditing event ``sys.addaudithook`` with no arguments. If any
|
Raise an auditing event ``sys.addaudithook`` with no arguments. If any
|
||||||
existing hooks raise an exception derived from :class:`Exception`, the
|
existing hooks raise an exception derived from :class:`RuntimeError`, the
|
||||||
new hook will not be added and the exception suppressed. As a result,
|
new hook will not be added and the exception suppressed. As a result,
|
||||||
callers cannot assume that their hook has been added unless they control
|
callers cannot assume that their hook has been added unless they control
|
||||||
all existing hooks.
|
all existing hooks.
|
||||||
|
|
||||||
.. versionadded:: 3.8
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
|
.. versionchanged:: 3.8.1
|
||||||
|
|
||||||
|
Exceptions derived from :class:`Exception` but not :class:`RuntimeError`
|
||||||
|
are no longer suppressed.
|
||||||
|
|
||||||
.. impl-detail::
|
.. impl-detail::
|
||||||
|
|
||||||
When tracing is enabled (see :func:`settrace`), Python hooks are only
|
When tracing is enabled (see :func:`settrace`), Python hooks are only
|
||||||
|
@ -308,6 +313,15 @@ always available.
|
||||||
before the program exits. The handling of such top-level exceptions can be
|
before the program exits. The handling of such top-level exceptions can be
|
||||||
customized by assigning another three-argument function to ``sys.excepthook``.
|
customized by assigning another three-argument function to ``sys.excepthook``.
|
||||||
|
|
||||||
|
.. audit-event:: sys.excepthook hook,type,value,traceback sys.excepthook
|
||||||
|
|
||||||
|
Raise an auditing event ``sys.excepthook`` with arguments ``hook``,
|
||||||
|
``type``, ``value``, ``traceback`` when an uncaught exception occurs.
|
||||||
|
If no hook has been set, ``hook`` may be ``None``. If any hook raises
|
||||||
|
an exception derived from :class:`RuntimeError` the call to the hook will
|
||||||
|
be suppressed. Otherwise, the audit hook exception will be reported as
|
||||||
|
unraisable and ``sys.excepthook`` will be called.
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
The :func:`sys.unraisablehook` function handles unraisable exceptions
|
The :func:`sys.unraisablehook` function handles unraisable exceptions
|
||||||
|
@ -1540,6 +1554,13 @@ always available.
|
||||||
|
|
||||||
See also :func:`excepthook` which handles uncaught exceptions.
|
See also :func:`excepthook` which handles uncaught exceptions.
|
||||||
|
|
||||||
|
.. audit-event:: sys.unraisablehook hook,unraisable sys.unraisablehook
|
||||||
|
|
||||||
|
Raise an auditing event ``sys.unraisablehook`` with arguments
|
||||||
|
``hook``, ``unraisable`` when an exception that cannot be handled occurs.
|
||||||
|
The ``unraisable`` object is the same as what will be passed to the hook.
|
||||||
|
If no hook has been set, ``hook`` may be ``None``.
|
||||||
|
|
||||||
.. versionadded:: 3.8
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
.. data:: version
|
.. data:: version
|
||||||
|
|
|
@ -263,13 +263,50 @@ def test_cantrace():
|
||||||
|
|
||||||
def test_mmap():
|
def test_mmap():
|
||||||
import mmap
|
import mmap
|
||||||
|
|
||||||
with TestHook() as hook:
|
with TestHook() as hook:
|
||||||
mmap.mmap(-1, 8)
|
mmap.mmap(-1, 8)
|
||||||
assertEqual(hook.seen[0][1][:2], (-1, 8))
|
assertEqual(hook.seen[0][1][:2], (-1, 8))
|
||||||
|
|
||||||
|
|
||||||
|
def test_excepthook():
|
||||||
|
def excepthook(exc_type, exc_value, exc_tb):
|
||||||
|
if exc_type is not RuntimeError:
|
||||||
|
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
||||||
|
|
||||||
|
def hook(event, args):
|
||||||
|
if event == "sys.excepthook":
|
||||||
|
if not isinstance(args[2], args[1]):
|
||||||
|
raise TypeError(f"Expected isinstance({args[2]!r}, " f"{args[1]!r})")
|
||||||
|
if args[0] != excepthook:
|
||||||
|
raise ValueError(f"Expected {args[0]} == {excepthook}")
|
||||||
|
print(event, repr(args[2]))
|
||||||
|
|
||||||
|
sys.addaudithook(hook)
|
||||||
|
sys.excepthook = excepthook
|
||||||
|
raise RuntimeError("fatal-error")
|
||||||
|
|
||||||
|
|
||||||
|
def test_unraisablehook():
|
||||||
|
from _testcapi import write_unraisable_exc
|
||||||
|
|
||||||
|
def unraisablehook(hookargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def hook(event, args):
|
||||||
|
if event == "sys.unraisablehook":
|
||||||
|
if args[0] != unraisablehook:
|
||||||
|
raise ValueError(f"Expected {args[0]} == {unraisablehook}")
|
||||||
|
print(event, repr(args[1].exc_value), args[1].err_msg)
|
||||||
|
|
||||||
|
sys.addaudithook(hook)
|
||||||
|
sys.unraisablehook = unraisablehook
|
||||||
|
write_unraisable_exc(RuntimeError("nonfatal-error"), "for audit hook test", None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from test.libregrtest.setup import suppress_msvcrt_asserts
|
from test.libregrtest.setup import suppress_msvcrt_asserts
|
||||||
|
|
||||||
suppress_msvcrt_asserts(False)
|
suppress_msvcrt_asserts(False)
|
||||||
|
|
||||||
test = sys.argv[1]
|
test = sys.argv[1]
|
||||||
|
|
|
@ -24,7 +24,23 @@ class AuditTest(unittest.TestCase):
|
||||||
sys.stdout.writelines(p.stdout)
|
sys.stdout.writelines(p.stdout)
|
||||||
sys.stderr.writelines(p.stderr)
|
sys.stderr.writelines(p.stderr)
|
||||||
if p.returncode:
|
if p.returncode:
|
||||||
self.fail(''.join(p.stderr))
|
self.fail("".join(p.stderr))
|
||||||
|
|
||||||
|
def run_python(self, *args):
|
||||||
|
events = []
|
||||||
|
with subprocess.Popen(
|
||||||
|
[sys.executable, "-X utf8", AUDIT_TESTS_PY, *args],
|
||||||
|
encoding="utf-8",
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
) as p:
|
||||||
|
p.wait()
|
||||||
|
sys.stderr.writelines(p.stderr)
|
||||||
|
return (
|
||||||
|
p.returncode,
|
||||||
|
[line.strip().partition(" ") for line in p.stdout],
|
||||||
|
"".join(p.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
self.do_test("test_basic")
|
self.do_test("test_basic")
|
||||||
|
@ -36,19 +52,11 @@ class AuditTest(unittest.TestCase):
|
||||||
self.do_test("test_block_add_hook_baseexception")
|
self.do_test("test_block_add_hook_baseexception")
|
||||||
|
|
||||||
def test_finalize_hooks(self):
|
def test_finalize_hooks(self):
|
||||||
events = []
|
returncode, events, stderr = self.run_python("test_finalize_hooks")
|
||||||
with subprocess.Popen(
|
if stderr:
|
||||||
[sys.executable, "-X utf8", AUDIT_TESTS_PY, "test_finalize_hooks"],
|
print(stderr, file=sys.stderr)
|
||||||
encoding="utf-8",
|
if returncode:
|
||||||
stdout=subprocess.PIPE,
|
self.fail(stderr)
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
) as p:
|
|
||||||
p.wait()
|
|
||||||
for line in p.stdout:
|
|
||||||
events.append(line.strip().partition(" "))
|
|
||||||
sys.stderr.writelines(p.stderr)
|
|
||||||
if p.returncode:
|
|
||||||
self.fail(''.join(p.stderr))
|
|
||||||
|
|
||||||
firstId = events[0][2]
|
firstId = events[0][2]
|
||||||
self.assertSequenceEqual(
|
self.assertSequenceEqual(
|
||||||
|
@ -76,6 +84,26 @@ class AuditTest(unittest.TestCase):
|
||||||
def test_mmap(self):
|
def test_mmap(self):
|
||||||
self.do_test("test_mmap")
|
self.do_test("test_mmap")
|
||||||
|
|
||||||
|
def test_excepthook(self):
|
||||||
|
returncode, events, stderr = self.run_python("test_excepthook")
|
||||||
|
if not returncode:
|
||||||
|
self.fail(f"Expected fatal exception\n{stderr}")
|
||||||
|
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
[("sys.excepthook", " ", "RuntimeError('fatal-error')")], events
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unraisablehook(self):
|
||||||
|
returncode, events, stderr = self.run_python("test_unraisablehook")
|
||||||
|
if returncode:
|
||||||
|
self.fail(stderr)
|
||||||
|
|
||||||
|
self.assertEqual(events[0][0], "sys.unraisablehook")
|
||||||
|
self.assertEqual(
|
||||||
|
events[0][2],
|
||||||
|
"RuntimeError('nonfatal-error') Exception ignored for audit hook test",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Add audit hooks for when :func:`sys.excepthook` and
|
||||||
|
:func:`sys.unraisablehook` are invoked
|
|
@ -1391,14 +1391,33 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject *hook_args = make_unraisable_hook_args(
|
||||||
|
tstate, exc_type, exc_value, exc_tb, err_msg, obj);
|
||||||
|
if (hook_args == NULL) {
|
||||||
|
err_msg_str = ("Exception ignored on building "
|
||||||
|
"sys.unraisablehook arguments");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
_Py_IDENTIFIER(unraisablehook);
|
_Py_IDENTIFIER(unraisablehook);
|
||||||
PyObject *hook = _PySys_GetObjectId(&PyId_unraisablehook);
|
PyObject *hook = _PySys_GetObjectId(&PyId_unraisablehook);
|
||||||
if (hook != NULL && hook != Py_None) {
|
if (hook == NULL) {
|
||||||
PyObject *hook_args;
|
Py_DECREF(hook_args);
|
||||||
|
goto default_hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PySys_Audit("sys.unraisablehook", "OO", hook, hook_args) < 0) {
|
||||||
|
Py_DECREF(hook_args);
|
||||||
|
err_msg_str = "Exception ignored in audit hook";
|
||||||
|
obj = NULL;
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hook == Py_None) {
|
||||||
|
Py_DECREF(hook_args);
|
||||||
|
goto default_hook;
|
||||||
|
}
|
||||||
|
|
||||||
hook_args = make_unraisable_hook_args(tstate, exc_type, exc_value,
|
|
||||||
exc_tb, err_msg, obj);
|
|
||||||
if (hook_args != NULL) {
|
|
||||||
PyObject *res = _PyObject_CallOneArg(hook, hook_args);
|
PyObject *res = _PyObject_CallOneArg(hook, hook_args);
|
||||||
Py_DECREF(hook_args);
|
Py_DECREF(hook_args);
|
||||||
if (res != NULL) {
|
if (res != NULL) {
|
||||||
|
@ -1406,28 +1425,19 @@ _PyErr_WriteUnraisableMsg(const char *err_msg_str, PyObject *obj)
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
err_msg_str = "Exception ignored in sys.unraisablehook";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
err_msg_str = ("Exception ignored on building "
|
|
||||||
"sys.unraisablehook arguments");
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_XDECREF(err_msg);
|
|
||||||
err_msg = PyUnicode_FromString(err_msg_str);
|
|
||||||
if (err_msg == NULL) {
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sys.unraisablehook failed: log its error using default hook */
|
/* sys.unraisablehook failed: log its error using default hook */
|
||||||
|
obj = hook;
|
||||||
|
err_msg_str = NULL;
|
||||||
|
|
||||||
|
error:
|
||||||
|
/* err_msg_str and obj have been updated and we have a new exception */
|
||||||
|
Py_XSETREF(err_msg, PyUnicode_FromString(err_msg_str ?
|
||||||
|
err_msg_str : "Exception ignored in sys.unraisablehook"));
|
||||||
Py_XDECREF(exc_type);
|
Py_XDECREF(exc_type);
|
||||||
Py_XDECREF(exc_value);
|
Py_XDECREF(exc_value);
|
||||||
Py_XDECREF(exc_tb);
|
Py_XDECREF(exc_tb);
|
||||||
_PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
|
_PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
|
||||||
|
|
||||||
obj = hook;
|
|
||||||
}
|
|
||||||
|
|
||||||
default_hook:
|
default_hook:
|
||||||
/* Call the default unraisable hook (ignore failure) */
|
/* Call the default unraisable hook (ignore failure) */
|
||||||
(void)write_unraisable_exc(tstate, exc_type, exc_value, exc_tb,
|
(void)write_unraisable_exc(tstate, exc_type, exc_value, exc_tb,
|
||||||
|
|
|
@ -695,6 +695,14 @@ _PyErr_PrintEx(PyThreadState *tstate, int set_sys_last_vars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hook = _PySys_GetObjectId(&PyId_excepthook);
|
hook = _PySys_GetObjectId(&PyId_excepthook);
|
||||||
|
if (PySys_Audit("sys.excepthook", "OOOO", hook ? hook : Py_None,
|
||||||
|
exception, v, tb) < 0) {
|
||||||
|
if (PyErr_ExceptionMatches(PyExc_RuntimeError)) {
|
||||||
|
PyErr_Clear();
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
_PyErr_WriteUnraisableMsg("in audit hook", NULL);
|
||||||
|
}
|
||||||
if (hook) {
|
if (hook) {
|
||||||
PyObject* stack[3];
|
PyObject* stack[3];
|
||||||
PyObject *result;
|
PyObject *result;
|
||||||
|
|
|
@ -323,8 +323,8 @@ PySys_AddAuditHook(Py_AuditHookFunction hook, void *userData)
|
||||||
/* Cannot invoke hooks until we are initialized */
|
/* Cannot invoke hooks until we are initialized */
|
||||||
if (runtime->initialized) {
|
if (runtime->initialized) {
|
||||||
if (PySys_Audit("sys.addaudithook", NULL) < 0) {
|
if (PySys_Audit("sys.addaudithook", NULL) < 0) {
|
||||||
if (_PyErr_ExceptionMatches(tstate, PyExc_Exception)) {
|
if (_PyErr_ExceptionMatches(tstate, PyExc_RuntimeError)) {
|
||||||
/* We do not report errors derived from Exception */
|
/* We do not report errors derived from RuntimeError */
|
||||||
_PyErr_Clear(tstate);
|
_PyErr_Clear(tstate);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue