Repository navigation
Conversation
`sys.setprofile` and `sys.settrace` callbacks are no longer called for `sys.monitoring` events that extension modules fire for their own code objects through the monitoring C API. Until now, the `sys.setprofile` or `sys.settrace` function would get run, but it would be passed the top frame on the Python stack rather than anything related to the code object passed to the monitoring C API, and this would manifest to the legacy tracing function as nonsensical duplicate events rather than anything that could be attributed to the code object passed to the monitoring API.
| ( 1, E.PY_RESUME, capi.fire_event_py_resume), | ||
| ( 1, E.PY_YIELD, capi.fire_event_py_yield, 10), | ||
| ( 1, E.PY_RETURN, capi.fire_event_py_return, 20), | ||
| ( 2, E.CALL, capi.fire_event_call, callable, 40), |
There was a problem hiding this comment.
Note: The removed 1st tuple element (never reflected in the above comment!) was the expected number of calls that would be seen. It was 1 for everything except E.CALL, where it was 2 instead. It was 2 for E.CALL because we'd see both a call to the monitoring API and a call to the Python function that triggered the call to the monitoring API. Instead of having this count of expected calls, the test now uses a Counter class that looks only for calls for a particular CodeLike and ignores all others, which makes it so that the expected count is 1 for everything instead of for everything but CALL.
| sys.monitoring.register_callback(TEST_TOOL, event, counter) | ||
| if event == E.C_RETURN or event == E.C_RAISE: | ||
| sys.monitoring.set_events(TEST_TOOL, E.CALL) | ||
| event_value = int(math.log2(E.CALL)) |
There was a problem hiding this comment.
Note for reviewer: previously this would call
self.Scope(self.codelike, math.log2(E.C_RETURN))which results in an out-of-bounds read inside PyMonitoring_EnterScope. It accesses m->tools[event], and m->tools is an array of 16 elements, and PY_MONITORING_EVENT_C_RETURN is 16 and so one past the end of that array.
This was wrong; you get the C_RETURN event by subscribing to C_CALL events. That's what this fix is.
| if (PyVectorcall_NARGS(nargsf) != 4) { | ||
| // PyMonitoring_FireCReturnEvent and PyMonitoring_FireCRaiseEvent | ||
| // don't provide the callable. | ||
| assert(PyVectorcall_NARGS(nargsf) == 3); |
There was a problem hiding this comment.
Note for reviewer: this fixes an assertion error that would be hit on debug builds whenever you call PyMonitoring_FireCReturnEvent or PyMonitoring_FireCRaiseEvent with a sys.setprofile legacy profile function installed.
| assert(!PyErr_Occurred()); | ||
| assert(kwnames == NULL); | ||
| assert(PyVectorcall_NARGS(nargsf) == 3); | ||
| assert(PyCode_Check(args[0])); |
There was a problem hiding this comment.
Note for reviewer: this would trigger an assertion error in the tests, where we pass a CodeLike instead of a code object. It only didn't fail up until now because the path wasn't being exercised.
| PyFrameObject *frame = get_top_frame_if_running_code(args[0]); | ||
| if (frame == NULL) { | ||
| Py_RETURN_NONE; | ||
| } |
There was a problem hiding this comment.
Note for reviewer: this check is moved up so that we know that the code object we received is actually a PyCodeObject before we cast it to one.
| return NULL; | ||
| } | ||
| PyObject *res = call_profile_func(self, meth); | ||
| PyObject *res = call_profile_func(self, args[0], meth); |
There was a problem hiding this comment.
Can we check the code mismatch before tp_descr_get? Reporting list.append with arg0=1 still raises TypeError before reaching this guard, preventing delivery to sys.monitoring.
| return NULL; | ||
| } | ||
| PyObject *res = call_profile_func(self, meth); | ||
| PyObject *res = call_profile_func(self, args[0], meth); |
There was a problem hiding this comment.
This still does the tp_descr_get before the frame check, so a PyMonitoring_FireCallEvent with a method descriptor and an arg0 of another type fails with a TypeError under sys.setprofile instead of being ignored (and otherwise we create a bound method just to throw it away). I think we should do the frame check at the top of sys_profile_call_or_return. Can you add a test for this case as well?
|
|
||
| /* Return the top frame (borrowed) if it's running `code`, else NULL. */ | ||
| static PyFrameObject * | ||
| get_top_frame_if_running_code(PyObject *code) |
There was a problem hiding this comment.
Why not mask the legacy tools out in PyMonitoring_EnterScope (or in capi_call_instrumentation) instead? Tool ids 6 and 7 are only used by sys.setprofile/sys.settrace, so something like
state_array[i].active = m->tools[event] & ~LEGACY_TOOLS;would keep all these handlers untouched, and extensions would not even pay for the call (and the PyEval_GetFrame()) when only legacy tracing is active. Am I missing a case where we want these handlers to run for C API events?
There was a problem hiding this comment.
Huh, that approach didn't occur to me, but I do think it's cleaner.
Am I missing a case where we want these handlers to run for C API events?
The only case I can think of is perhaps if the user has called _PyInterpreterState_SetEvalFrameFunc, and is calling the monitoring C API from their own frame evaluation function? In that case they would have a real frame on the PyEval_GetFrame() stack, and the existing behavior would be right for them.
There was a problem hiding this comment.
I've updated the implementation to that. I left the tests alone. Let me know if you'd like to pare down the tests as well, now that all of these events are being ignored in one common place, instead of in many separate places.
Instead of making the callbacks associated with the `sys.setprofile` and `sys.settrace` legacy trace functions detect when they were called with something that's not a code object or not the code object held by the top frame on the Python stack, sidestep the problem entirely by making sure that the legacy profile functions are masked out of the set of tools to fire events for when using the monitoring C API.
|
The failed test from the latest run seems unrelated to my changes... |
sys.setprofileandsys.settracecallbacks are no longer called forsys.monitoringevents that extension modules fire for their own code objects through the monitoring C API.Until now, the
sys.setprofileorsys.settracefunction would get run, but it would be passed the top frame on the Python stack rather than anything related to the code object passed to the monitoring C API, and this would manifest to the legacy tracing function as nonsensical duplicate events rather than anything that could be attributed to the code object passed to the monitoring API.PyMonitoring_FirePyStartEventresults in incorrect duplicate calls tosys.setprofileprofile functions #158711