Skip to content

Commit 19c1462

Browse files
authored
gh-99377: Add audit events for thread creation and clear (GH-99378)
1 parent 01fa907 commit 19c1462

File tree

7 files changed

+117
-7
lines changed

7 files changed

+117
-7
lines changed

Doc/c-api/init.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,12 +1239,25 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
12391239
The global interpreter lock need not be held, but may be held if it is
12401240
necessary to serialize calls to this function.
12411241
1242+
.. audit-event:: cpython.PyThreadState_New id c.PyThreadState_New
1243+
1244+
Raise an auditing event ``cpython.PyThreadState_New`` with Python's thread
1245+
id as the argument. The event will be raised from the thread creating the new
1246+
``PyThreadState``, which may not be the new thread.
1247+
12421248
12431249
.. c:function:: void PyThreadState_Clear(PyThreadState *tstate)
12441250
12451251
Reset all information in a thread state object. The global interpreter lock
12461252
must be held.
12471253
1254+
.. audit-event:: cpython.PyThreadState_Clear id c.PyThreadState_Clear
1255+
1256+
Raise an auditing event ``cpython.PyThreadState_Clear`` with Python's
1257+
thread id as the argument. The event may be raised from a different thread
1258+
than the one being cleared. Exceptions raised from a hook will be treated
1259+
as unraisable and will not abort the operation.
1260+
12481261
.. versionchanged:: 3.9
12491262
This function now calls the :c:member:`PyThreadState.on_delete` callback.
12501263
Previously, that happened in :c:func:`PyThreadState_Delete`.

Doc/library/_thread.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ This module defines the following constants and functions:
5757
When the function raises a :exc:`SystemExit` exception, it is silently
5858
ignored.
5959

60+
.. audit-event:: _thread.start_new_thread function,args,kwargs start_new_thread
61+
6062
.. versionchanged:: 3.8
6163
:func:`sys.unraisablehook` is now used to handle unhandled exceptions.
6264

Lib/test/audit-tests.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,48 @@ def hook(event, args):
419419
sys._getframe()
420420

421421

422+
def test_threading():
423+
import _thread
424+
425+
def hook(event, args):
426+
if event.startswith(("_thread.", "cpython.PyThreadState", "test.")):
427+
print(event, args)
428+
429+
sys.addaudithook(hook)
430+
431+
lock = _thread.allocate_lock()
432+
lock.acquire()
433+
434+
class test_func:
435+
def __repr__(self): return "<test_func>"
436+
def __call__(self):
437+
sys.audit("test.test_func")
438+
lock.release()
439+
440+
i = _thread.start_new_thread(test_func(), ())
441+
lock.acquire()
442+
443+
444+
def test_threading_abort():
445+
# Ensures that aborting PyThreadState_New raises the correct exception
446+
import _thread
447+
448+
class ThreadNewAbortError(Exception):
449+
pass
450+
451+
def hook(event, args):
452+
if event == "cpython.PyThreadState_New":
453+
raise ThreadNewAbortError()
454+
455+
sys.addaudithook(hook)
456+
457+
try:
458+
_thread.start_new_thread(lambda: None, ())
459+
except ThreadNewAbortError:
460+
# Other exceptions are raised and the test will fail
461+
pass
462+
463+
422464
def test_wmi_exec_query():
423465
import _wmi
424466

Lib/test/test_audit.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,31 @@ def test_sys_getframe(self):
186186

187187
self.assertEqual(actual, expected)
188188

189+
190+
def test_threading(self):
191+
returncode, events, stderr = self.run_python("test_threading")
192+
if returncode:
193+
self.fail(stderr)
194+
195+
if support.verbose:
196+
print(*events, sep='\n')
197+
actual = [(ev[0], ev[2]) for ev in events]
198+
expected = [
199+
("_thread.start_new_thread", "(<test_func>, (), None)"),
200+
("cpython.PyThreadState_New", "(2,)"),
201+
("test.test_func", "()"),
202+
("cpython.PyThreadState_Clear", "(2,)"),
203+
]
204+
205+
self.assertEqual(actual, expected)
206+
207+
def test_threading_abort(self):
208+
# Ensures that aborting PyThreadState_New raises the correct exception
209+
returncode, events, stderr = self.run_python("test_threading_abort")
210+
if returncode:
211+
self.fail(stderr)
212+
213+
189214
def test_wmi_exec_query(self):
190215
import_helper.import_module("_wmi")
191216
returncode, events, stderr = self.run_python("test_wmi_exec_query")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add audit events for thread creation and clear operations.

Modules/_threadmodule.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,11 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
11451145
return NULL;
11461146
}
11471147

1148+
if (PySys_Audit("_thread.start_new_thread", "OOO",
1149+
func, args, kwargs ? kwargs : Py_None) < 0) {
1150+
return NULL;
1151+
}
1152+
11481153
PyInterpreterState *interp = _PyInterpreterState_GET();
11491154
if (!_PyInterpreterState_HasFeature(interp, Py_RTFLAGS_THREADS)) {
11501155
PyErr_SetString(PyExc_RuntimeError,
@@ -1160,7 +1165,10 @@ thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
11601165
boot->tstate = _PyThreadState_Prealloc(boot->interp);
11611166
if (boot->tstate == NULL) {
11621167
PyMem_Free(boot);
1163-
return PyErr_NoMemory();
1168+
if (!PyErr_Occurred()) {
1169+
return PyErr_NoMemory();
1170+
}
1171+
return NULL;
11641172
}
11651173
boot->runtime = runtime;
11661174
boot->func = Py_NewRef(func);

Python/pystate.c

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -873,14 +873,29 @@ PyThreadState *
873873
PyThreadState_New(PyInterpreterState *interp)
874874
{
875875
PyThreadState *tstate = new_threadstate(interp);
876-
_PyThreadState_SetCurrent(tstate);
876+
if (tstate) {
877+
_PyThreadState_SetCurrent(tstate);
878+
if (PySys_Audit("cpython.PyThreadState_New", "K", tstate->id) < 0) {
879+
PyThreadState_Clear(tstate);
880+
_PyThreadState_DeleteCurrent(tstate);
881+
return NULL;
882+
}
883+
}
877884
return tstate;
878885
}
879886

880887
PyThreadState *
881888
_PyThreadState_Prealloc(PyInterpreterState *interp)
882889
{
883-
return new_threadstate(interp);
890+
PyThreadState *tstate = new_threadstate(interp);
891+
if (tstate) {
892+
if (PySys_Audit("cpython.PyThreadState_New", "K", tstate->id) < 0) {
893+
PyThreadState_Clear(tstate);
894+
_PyThreadState_Delete(tstate, 0);
895+
return NULL;
896+
}
897+
}
898+
return tstate;
884899
}
885900

886901
// We keep this around for (accidental) stable ABI compatibility.
@@ -1028,6 +1043,10 @@ _PyInterpreterState_ClearModules(PyInterpreterState *interp)
10281043
void
10291044
PyThreadState_Clear(PyThreadState *tstate)
10301045
{
1046+
if (PySys_Audit("cpython.PyThreadState_Clear", "K", tstate->id) < 0) {
1047+
PyErr_WriteUnraisable(NULL);
1048+
}
1049+
10311050
int verbose = _PyInterpreterState_GetConfig(tstate->interp)->verbose;
10321051

10331052
if (verbose && tstate->cframe->current_frame != NULL) {
@@ -1545,16 +1564,16 @@ _PyGILState_Init(_PyRuntimeState *runtime)
15451564
PyStatus
15461565
_PyGILState_SetTstate(PyThreadState *tstate)
15471566
{
1567+
/* must init with valid states */
1568+
assert(tstate != NULL);
1569+
assert(tstate->interp != NULL);
1570+
15481571
if (!_Py_IsMainInterpreter(tstate->interp)) {
15491572
/* Currently, PyGILState is shared by all interpreters. The main
15501573
* interpreter is responsible to initialize it. */
15511574
return _PyStatus_OK();
15521575
}
15531576

1554-
/* must init with valid states */
1555-
assert(tstate != NULL);
1556-
assert(tstate->interp != NULL);
1557-
15581577
struct _gilstate_runtime_state *gilstate = &tstate->interp->runtime->gilstate;
15591578

15601579
gilstate->autoInterpreterState = tstate->interp;

0 commit comments

Comments
 (0)