[Python-checkins] gh-99377: Add audit events for thread creation and clear (GH-99378)

zooba webhook-mailer at python.org
Wed Nov 16 12:15:59 EST 2022


https://github.com/python/cpython/commit/19c1462e8dca3319c8290e2edcce482bd18cb018
commit: 19c1462e8dca3319c8290e2edcce482bd18cb018
branch: main
author: Steve Dower <steve.dower at python.org>
committer: zooba <steve.dower at microsoft.com>
date: 2022-11-16T17:15:52Z
summary:

gh-99377: Add audit events for thread creation and clear (GH-99378)

files:
A Misc/NEWS.d/next/Core and Builtins/2022-11-11-14-04-01.gh-issue-99377.-CJvWn.rst
M Doc/c-api/init.rst
M Doc/library/_thread.rst
M Lib/test/audit-tests.py
M Lib/test/test_audit.py
M Modules/_threadmodule.c
M Python/pystate.c

diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst
index afb17719a77a..273838c1db28 100644
--- a/Doc/c-api/init.rst
+++ b/Doc/c-api/init.rst
@@ -1239,12 +1239,25 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
    The global interpreter lock need not be held, but may be held if it is
    necessary to serialize calls to this function.
 
+   .. audit-event:: cpython.PyThreadState_New id c.PyThreadState_New
+
+      Raise an auditing event ``cpython.PyThreadState_New`` with Python's thread
+      id as the argument. The event will be raised from the thread creating the new
+      ``PyThreadState``, which may not be the new thread.
+
 
 .. c:function:: void PyThreadState_Clear(PyThreadState *tstate)
 
    Reset all information in a thread state object.  The global interpreter lock
    must be held.
 
+   .. audit-event:: cpython.PyThreadState_Clear id c.PyThreadState_Clear
+
+      Raise an auditing event ``cpython.PyThreadState_Clear`` with Python's
+      thread id as the argument. The event may be raised from a different thread
+      than the one being cleared. Exceptions raised from a hook will be treated
+      as unraisable and will not abort the operation.
+
    .. versionchanged:: 3.9
       This function now calls the :c:member:`PyThreadState.on_delete` callback.
       Previously, that happened in :c:func:`PyThreadState_Delete`.
diff --git a/Doc/library/_thread.rst b/Doc/library/_thread.rst
index 9df9e7914e09..122692a42859 100644
--- a/Doc/library/_thread.rst
+++ b/Doc/library/_thread.rst
@@ -57,6 +57,8 @@ This module defines the following constants and functions:
    When the function raises a :exc:`SystemExit` exception, it is silently
    ignored.
 
+   .. audit-event:: _thread.start_new_thread function,args,kwargs start_new_thread
+
    .. versionchanged:: 3.8
       :func:`sys.unraisablehook` is now used to handle unhandled exceptions.
 
diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py
index a411072ba7d0..bf56cea541d1 100644
--- a/Lib/test/audit-tests.py
+++ b/Lib/test/audit-tests.py
@@ -419,6 +419,48 @@ def hook(event, args):
     sys._getframe()
 
 
+def test_threading():
+    import _thread
+
+    def hook(event, args):
+        if event.startswith(("_thread.", "cpython.PyThreadState", "test.")):
+            print(event, args)
+
+    sys.addaudithook(hook)
+
+    lock = _thread.allocate_lock()
+    lock.acquire()
+
+    class test_func:
+        def __repr__(self): return "<test_func>"
+        def __call__(self):
+            sys.audit("test.test_func")
+            lock.release()
+
+    i = _thread.start_new_thread(test_func(), ())
+    lock.acquire()
+
+
+def test_threading_abort():
+    # Ensures that aborting PyThreadState_New raises the correct exception
+    import _thread
+
+    class ThreadNewAbortError(Exception):
+        pass
+
+    def hook(event, args):
+        if event == "cpython.PyThreadState_New":
+            raise ThreadNewAbortError()
+
+    sys.addaudithook(hook)
+
+    try:
+        _thread.start_new_thread(lambda: None, ())
+    except ThreadNewAbortError:
+        # Other exceptions are raised and the test will fail
+        pass
+
+
 def test_wmi_exec_query():
     import _wmi
 
diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py
index e8d560ad5ce0..5a2997ac9e6f 100644
--- a/Lib/test/test_audit.py
+++ b/Lib/test/test_audit.py
@@ -186,6 +186,31 @@ def test_sys_getframe(self):
 
         self.assertEqual(actual, expected)
 
+
+    def test_threading(self):
+        returncode, events, stderr = self.run_python("test_threading")
+        if returncode:
+            self.fail(stderr)
+
+        if support.verbose:
+            print(*events, sep='\n')
+        actual = [(ev[0], ev[2]) for ev in events]
+        expected = [
+            ("_thread.start_new_thread", "(<test_func>, (), None)"),
+            ("cpython.PyThreadState_New", "(2,)"),
+            ("test.test_func", "()"),
+            ("cpython.PyThreadState_Clear", "(2,)"),
+        ]
+
+        self.assertEqual(actual, expected)
+
+    def test_threading_abort(self):
+        # Ensures that aborting PyThreadState_New raises the correct exception
+        returncode, events, stderr = self.run_python("test_threading_abort")
+        if returncode:
+            self.fail(stderr)
+
+
     def test_wmi_exec_query(self):
         import_helper.import_module("_wmi")
         returncode, events, stderr = self.run_python("test_wmi_exec_query")
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-11-11-14-04-01.gh-issue-99377.-CJvWn.rst b/Misc/NEWS.d/next/Core and Builtins/2022-11-11-14-04-01.gh-issue-99377.-CJvWn.rst
new file mode 100644
index 000000000000..631b9ca23044
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-11-11-14-04-01.gh-issue-99377.-CJvWn.rst	
@@ -0,0 +1 @@
+Add audit events for thread creation and clear operations.
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
index 5968d4e2e0ee..ec8b6d881124 100644
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -1145,6 +1145,11 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
         return NULL;
     }
 
+    if (PySys_Audit("_thread.start_new_thread", "OOO",
+                    func, args, kwargs ? kwargs : Py_None) < 0) {
+        return NULL;
+    }
+
     PyInterpreterState *interp = _PyInterpreterState_GET();
     if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) {
         PyErr_SetString(PyExc_RuntimeError,
@@ -1160,7 +1165,10 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
     boot->tstate = _PyThreadState_Prealloc(boot->interp);
     if (boot->tstate == NULL) {
         PyMem_Free(boot);
-        return PyErr_NoMemory();
+        if (!PyErr_Occurred()) {
+            return PyErr_NoMemory();
+        }
+        return NULL;
     }
     boot->runtime = runtime;
     boot->func = Py_NewRef(func);
diff --git a/Python/pystate.c b/Python/pystate.c
index b94fbf6ca0ba..d6f2645b3658 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -873,14 +873,29 @@ PyThreadState *
 PyThreadState_New(PyInterpreterState *interp)
 {
     PyThreadState *tstate = new_threadstate(interp);
-    _PyThreadState_SetCurrent(tstate);
+    if (tstate) {
+        _PyThreadState_SetCurrent(tstate);
+        if (PySys_Audit("cpython.PyThreadState_New", "K", tstate->id) < 0) {
+            PyThreadState_Clear(tstate);
+            _PyThreadState_DeleteCurrent(tstate);
+            return NULL;
+        }
+    }
     return tstate;
 }
 
 PyThreadState *
 _PyThreadState_Prealloc(PyInterpreterState *interp)
 {
-    return new_threadstate(interp);
+    PyThreadState *tstate = new_threadstate(interp);
+    if (tstate) {
+        if (PySys_Audit("cpython.PyThreadState_New", "K", tstate->id) < 0) {
+            PyThreadState_Clear(tstate);
+            _PyThreadState_Delete(tstate, 0);
+            return NULL;
+        }
+    }
+    return tstate;
 }
 
 // We keep this around for (accidental) stable ABI compatibility.
@@ -1028,6 +1043,10 @@ _PyInterpreterState_ClearModules(PyInterpreterState *interp)
 void
 PyThreadState_Clear(PyThreadState *tstate)
 {
+    if (PySys_Audit("cpython.PyThreadState_Clear", "K", tstate->id) < 0) {
+        PyErr_WriteUnraisable(NULL);
+    }
+
     int verbose = _PyInterpreterState_GetConfig(tstate->interp)->verbose;
 
     if (verbose && tstate->cframe->current_frame != NULL) {
@@ -1545,16 +1564,16 @@ _PyGILState_Init(_PyRuntimeState *runtime)
 PyStatus
 _PyGILState_SetTstate(PyThreadState *tstate)
 {
+    /* must init with valid states */
+    assert(tstate != NULL);
+    assert(tstate->interp != NULL);
+
     if (!_Py_IsMainInterpreter(tstate->interp)) {
         /* Currently, PyGILState is shared by all interpreters. The main
          * interpreter is responsible to initialize it. */
         return _PyStatus_OK();
     }
 
-    /* must init with valid states */
-    assert(tstate != NULL);
-    assert(tstate->interp != NULL);
-
     struct _gilstate_runtime_state *gilstate = &tstate->interp->runtime->gilstate;
 
     gilstate->autoInterpreterState = tstate->interp;



More information about the Python-checkins mailing list