gh-142186: Allow all PEP-669 events to be per-code object and disableable (GH-146182)

* Make the `PY_UNWIND` monitoring event available as a code-local
event to allow trapping on function exit events when an exception
bubbles up. This complements the PY_RETURN event by allowing to
catch any function exit event.

* Allow `PY_UNWIND`  to be `DISABLE`d; disabling it disables the event for the whole code object.

* Do the above for `PY_THROW`, `RAISE`, `EXCEPTION_HANDLED`, and `RERAISE` events.
This commit is contained in:
Gabriele N. Tornetta 2026-04-22 09:08:23 +01:00 committed by GitHub
parent 09233bd198
commit 858e69eab0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 436 additions and 71 deletions

View file

@ -2406,15 +2406,16 @@ void
_PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame,
_Py_CODEUNIT *instr)
{
if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_RAISE)) {
if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_RAISE)) {
return;
}
do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RAISE);
}
bool
_PyEval_NoToolsForUnwind(PyThreadState *tstate) {
return no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_UNWIND);
_PyEval_NoToolsForUnwind(PyThreadState *tstate, _PyInterpreterFrame *frame)
{
return no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_PY_UNWIND);
}

View file

@ -367,7 +367,7 @@ no_tools_for_global_event(PyThreadState *tstate, int event)
static inline bool
no_tools_for_local_event(PyThreadState *tstate, _PyInterpreterFrame *frame, int event)
{
assert(event < _PY_MONITORING_LOCAL_EVENTS);
assert(event < _PY_MONITORING_UNGROUPED_EVENTS);
_PyCoMonitoringData *data = _PyFrame_GetCode(frame)->_co_monitoring;
if (data) {
return data->active_monitors.tools[event] == 0;
@ -382,7 +382,7 @@ monitor_handled(PyThreadState *tstate,
_PyInterpreterFrame *frame,
_Py_CODEUNIT *instr, PyObject *exc)
{
if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_EXCEPTION_HANDLED)) {
if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_EXCEPTION_HANDLED)) {
return 0;
}
return _Py_call_instrumentation_arg(tstate, PY_MONITORING_EVENT_EXCEPTION_HANDLED, frame, instr, exc);
@ -393,7 +393,7 @@ monitor_throw(PyThreadState *tstate,
_PyInterpreterFrame *frame,
_Py_CODEUNIT *instr)
{
if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_THROW)) {
if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_PY_THROW)) {
return;
}
do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_PY_THROW);
@ -403,7 +403,7 @@ static void
monitor_reraise(PyThreadState *tstate, _PyInterpreterFrame *frame,
_Py_CODEUNIT *instr)
{
if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_RERAISE)) {
if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_RERAISE)) {
return;
}
do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RERAISE);
@ -431,7 +431,7 @@ monitor_unwind(PyThreadState *tstate,
_PyInterpreterFrame *frame,
_Py_CODEUNIT *instr)
{
if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_UNWIND)) {
if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_PY_UNWIND)) {
return;
}
do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_PY_UNWIND);

View file

@ -203,7 +203,7 @@ is_instrumented(int opcode)
static inline bool
monitors_equals(_Py_LocalMonitors a, _Py_LocalMonitors b)
{
for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
if (a.tools[i] != b.tools[i]) {
return false;
}
@ -216,7 +216,7 @@ static inline _Py_LocalMonitors
monitors_sub(_Py_LocalMonitors a, _Py_LocalMonitors b)
{
_Py_LocalMonitors res;
for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
res.tools[i] = a.tools[i] & ~b.tools[i];
}
return res;
@ -227,7 +227,7 @@ static inline _Py_LocalMonitors
monitors_and(_Py_LocalMonitors a, _Py_LocalMonitors b)
{
_Py_LocalMonitors res;
for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
res.tools[i] = a.tools[i] & b.tools[i];
}
return res;
@ -243,7 +243,7 @@ static inline _Py_LocalMonitors
local_union(_Py_GlobalMonitors a, _Py_LocalMonitors b)
{
_Py_LocalMonitors res;
for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
res.tools[i] = a.tools[i] | b.tools[i];
}
return res;
@ -252,7 +252,7 @@ local_union(_Py_GlobalMonitors a, _Py_LocalMonitors b)
static inline bool
monitors_are_empty(_Py_LocalMonitors m)
{
for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
if (m.tools[i]) {
return false;
}
@ -263,7 +263,7 @@ monitors_are_empty(_Py_LocalMonitors m)
static inline bool
multiple_tools(_Py_LocalMonitors *m)
{
for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
if (_Py_popcount32(m->tools[i]) > 1) {
return true;
}
@ -275,7 +275,7 @@ static inline _PyMonitoringEventSet
get_local_events(_Py_LocalMonitors *m, int tool_id)
{
_PyMonitoringEventSet result = 0;
for (int e = 0; e < _PY_MONITORING_LOCAL_EVENTS; e++) {
for (int e = 0; e < _PY_MONITORING_UNGROUPED_EVENTS; e++) {
if ((m->tools[e] >> tool_id) & 1) {
result |= (1 << e);
}
@ -453,7 +453,7 @@ static void
dump_local_monitors(const char *prefix, _Py_LocalMonitors monitors, FILE*out)
{
fprintf(out, "%s monitors:\n", prefix);
for (int event = 0; event < _PY_MONITORING_LOCAL_EVENTS; event++) {
for (int event = 0; event < _PY_MONITORING_UNGROUPED_EVENTS; event++) {
fprintf(out, " Event %d: Tools %x\n", event, monitors.tools[event]);
}
}
@ -1102,8 +1102,10 @@ get_tools_for_instruction(PyCodeObject *code, PyInterpreterState *interp, int i,
event == PY_MONITORING_EVENT_C_RETURN);
event = PY_MONITORING_EVENT_CALL;
}
assert(_PY_MONITORING_IS_UNGROUPED_EVENT(event));
CHECK(debug_check_sanity(interp, code));
if (PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
CHECK(debug_check_sanity(interp, code));
/* Instrumented events use per-instruction tool bitmaps. */
if (code->_co_monitoring->tools) {
tools = code->_co_monitoring->tools[i];
}
@ -1112,7 +1114,9 @@ get_tools_for_instruction(PyCodeObject *code, PyInterpreterState *interp, int i,
}
}
else {
tools = interp->monitors.tools[event];
/* Other (non-instrumented) events are not tied to specific instructions;
* use the code-object-level active_monitors bitmap instead. */
tools = code->_co_monitoring->active_monitors.tools[event];
}
return tools;
}
@ -1139,6 +1143,25 @@ static const char *const event_names [] = {
[PY_MONITORING_EVENT_STOP_ITERATION] = "STOP_ITERATION",
};
/* Disable an "other" (non-instrumented) event (e.g. PY_UNWIND) for a single
* tool on this code object. Must be called with the world stopped or the
* code lock held. */
static void
remove_local_tool(PyCodeObject *code, PyInterpreterState *interp,
int event, int tool)
{
ASSERT_WORLD_STOPPED_OR_LOCKED(code);
assert(_PY_MONITORING_IS_UNGROUPED_EVENT(event));
assert(!PY_MONITORING_IS_INSTRUMENTED_EVENT(event));
assert(code->_co_monitoring);
code->_co_monitoring->local_monitors.tools[event] &= ~(1 << tool);
/* Recompute active_monitors for this event as the union of global and
* (now updated) local monitors. */
code->_co_monitoring->active_monitors.tools[event] =
interp->monitors.tools[event] |
code->_co_monitoring->local_monitors.tools[event];
}
static int
call_instrumentation_vector(
_Py_CODEUNIT *instr, PyThreadState *tstate, int event,
@ -1183,7 +1206,18 @@ call_instrumentation_vector(
}
else {
/* DISABLE */
if (!PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
if (PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
_PyEval_StopTheWorld(interp);
remove_tools(code, offset, event, 1 << tool);
_PyEval_StartTheWorld(interp);
}
else if (_PY_MONITORING_IS_UNGROUPED_EVENT(event)) {
/* Other (non-instrumented) event: disable for this code object. */
_PyEval_StopTheWorld(interp);
remove_local_tool(code, interp, event, tool);
_PyEval_StartTheWorld(interp);
}
else {
PyErr_Format(PyExc_ValueError,
"Cannot disable %s events. Callback removed.",
event_names[event]);
@ -1192,12 +1226,6 @@ call_instrumentation_vector(
err = -1;
break;
}
else {
PyInterpreterState *interp = tstate->interp;
_PyEval_StopTheWorld(interp);
remove_tools(code, offset, event, 1 << tool);
_PyEval_StartTheWorld(interp);
}
}
}
Py_DECREF(arg2_obj);
@ -1681,7 +1709,7 @@ update_instrumentation_data(PyCodeObject *code, PyInterpreterState *interp)
_Py_LocalMonitors *local_monitors = &code->_co_monitoring->local_monitors;
for (int i = 0; i < PY_MONITORING_TOOL_IDS; i++) {
if (code->_co_monitoring->tool_versions[i] != interp->monitoring_tool_versions[i]) {
for (int j = 0; j < _PY_MONITORING_LOCAL_EVENTS; j++) {
for (int j = 0; j < _PY_MONITORING_UNGROUPED_EVENTS; j++) {
local_monitors->tools[j] &= ~(1 << i);
}
}
@ -1977,7 +2005,7 @@ static void
set_local_events(_Py_LocalMonitors *m, int tool_id, _PyMonitoringEventSet events)
{
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
for (int e = 0; e < _PY_MONITORING_LOCAL_EVENTS; e++) {
for (int e = 0; e < _PY_MONITORING_UNGROUPED_EVENTS; e++) {
uint8_t *tools = &m->tools[e];
int val = (events >> e) & 1;
*tools &= ~(1 << tool_id);
@ -2037,7 +2065,7 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
PyInterpreterState *interp = _PyInterpreterState_GET();
assert(events < (1 << _PY_MONITORING_LOCAL_EVENTS));
assert(events < (1 << _PY_MONITORING_UNGROUPED_EVENTS));
if (code->_co_firsttraceable >= Py_SIZE(code)) {
PyErr_Format(PyExc_SystemError, "cannot instrument shim code object '%U'", code->co_name);
return -1;
@ -2373,7 +2401,7 @@ monitoring_get_local_events_impl(PyObject *module, int tool_id,
_PyMonitoringEventSet event_set = 0;
_PyCoMonitoringData *data = ((PyCodeObject *)code)->_co_monitoring;
if (data != NULL) {
for (int e = 0; e < _PY_MONITORING_LOCAL_EVENTS; e++) {
for (int e = 0; e < _PY_MONITORING_UNGROUPED_EVENTS; e++) {
if ((data->local_monitors.tools[e] >> tool_id) & 1) {
event_set |= (1 << e);
}
@ -2416,7 +2444,7 @@ monitoring_set_local_events_impl(PyObject *module, int tool_id,
event_set &= ~(1 << PY_MONITORING_EVENT_BRANCH);
event_set |= (1 << PY_MONITORING_EVENT_BRANCH_RIGHT) | (1 << PY_MONITORING_EVENT_BRANCH_LEFT);
}
if (event_set < 0 || event_set >= (1 << _PY_MONITORING_LOCAL_EVENTS)) {
if (event_set < 0 || event_set >= (1 << _PY_MONITORING_UNGROUPED_EVENTS)) {
PyErr_Format(PyExc_ValueError, "invalid local event set 0x%x", event_set);
return NULL;
}