wxPython 4.1 seg fault when scrolling ScrolledWindow

I’m still early in investigation of this, so the description will be a bit sketchy…
Python 3.8.1
wxPython==4.1.0
numpy [required: Any, installed: 1.18.3]
pillow [required: Any, installed: 7.1.1]
six [required: Any, installed: 1.14.0]

This all works fine with the 4.0.n releases.

Not yet sure how much of this is relevant as I have not carved out a reproducer, but I’ve got a wx.ScrolledWindow with several wx.Panel children. Each child contains a wx.grid.Grid, which has visibility toggled on and off to “collapse” the data out of view.

The wx.grid.Grid objects have EVT_MOUSEWHEEL bound to OnScrollWheel, thus (lots of details deleted):

    def __init__(...):
        self.owner = the_scrolled_window # parent...
        frame = wx.Panel(parent=self.owner)
        grid = wx.grid.Grid(parent=self.owner)
        grid.Bind(wx.EVT_MOUSEWHEEL, self.OnScrollWheel)

    def OnScrollWheel(self, event):
        if event.WheelAxis == wx.MOUSE_WHEEL_VERTICAL:
            amount = -event.WheelRotation // event.WheelDelta * event.LinesPerAction
            self.owner.ScrollLines(amount)
        event.Skip()

When you roll the scroll wheel rapidly (whatever that means) or just scroll to the bottom of the window, it seg faults. You can cause this to happen immediately by just setting amount to 999 or something big in the event handler above.

In PyCharm the code simply seg faults before you can see anything, but I put some diagnostic prints in the event handler, and sometimes I can get the following out for the two events preceding the crash. Note that all of a sudden the “WheelDelta” value is the same as the preceding “EventType”. Hmm, sure looks like a pointer is getting moved back 4 or 8 bytes or something, doesn’t it?

event.EventType=10045 event.LinesPerAction=3 event.WheelRotation=-120 event.WheelDelta=120 event.WheelAxis=0 win.GetScrollPos(0)=24
event.EventType=59966513 event.LinesPerAction=-31726 event.WheelRotation=439 event.WheelDelta=10045 event.WheelAxis=-1487834288 win.GetScrollPos(0)=25

I’m working on getting a small test case (don’t hold your breath for it), in the meantime anyone have any ideas where to start looking for the root cause?

I had crashes when I was adding a panel to a notebook, then adding a Grid to the panel… crashes went away when I got rid of the panel and added the Grid directly to the Notebook (got rid of the panel, and made the parent of the Grid the Notebook itself). So maybe try getting rid of the panel from the equation. Although checking back to your code snippet, it doesn’t look like your Grid has the panel as it’s parent… so maybe this won’t help you.

I had some time to mess with this today, so I grabbed the PDBs from extras/, the source from git and generated the sipped code. Ran to the seg fault, popped into Visual Studio and here’s what I found, along with my naive interpretation (I’m completely ignorant of what this should look like, but some of the pieces look very suspicious).

The event object appears to be correct at the bottom of call stack (shown below), in CallEventHandler, but after it has passed through the EventThunker (where it’s invisible, “optimized away”) and python38 dll, it is mangled in GetEventObject.

Here’s the good version of the event object from CallEventHandler, note the value of m_eventType is 10045 as expected.

-	event	{m_clickCount=-1 m_wheelAxis=wxMOUSE_WHEEL_VERTICAL (0) m_wheelRotation=-120 ...}	wxEvent & {wxmsw314u_core_vc140_x64.dll!wxMouseEvent}
+		[wxMouseEvent]	{m_clickCount=-1 m_wheelAxis=wxMOUSE_WHEEL_VERTICAL (0) m_wheelRotation=-120 ...}	wxmsw314u_core_vc140_x64.dll!wxMouseEvent
+		wxObject	{m_refData=0x0000000000000000 <NULL> }	wxObject
+		m_eventObject	0x0000025242b2c9b0 {m_type=wxGridWindowNormal (0) }	wxObject * {wxmsw314u_core_vc140_x64.dll!wxGridWindow}
		m_eventType	10045	int
		m_timeStamp	701971546	long
		m_id	-31726	int
+		m_callbackUserData	0x00000252519b2180 {m_func=0x00000252480cbac0 {ob_refcnt=1 ob_type=0x00007ff91ddd95b0 {python38.dll!_typeobject PyMethod_Type} {...} } }	wxObject * {_core.cp38-win_amd64.pyd!wxPyCallback}
+		m_handlerToProcessOnlyIn	0x0000000000000000 <NULL>	wxEvtHandler *
		m_propagationLevel	0	int
+		m_propagatedFrom	0x0000000000000000 <NULL>	wxEvtHandler *
		m_skipped	false	bool
		m_isCommandEvent	false	bool
		m_wasProcessed	true	bool
		m_willBeProcessedAgain	false	bool

Now here is what I believe to be the same object after passing through EventThunker and a bunch of CPython library calls. m_eventType is wacky and the eventObject pointer is clearly out into space.

This happens when I roll the scroll wheel past the bottom of the window, speed doesn’t seem to matter and it’s reproducible, but not cleanly (you just need to start scrolling the window up and down until it happens, sometimes right away, sometimes it takes 20-30 tries).

-	sipCpp	0x0000006e0bbee540 {m_eventObject=0x0000000000000466 {m_refData=??? } m_eventType=49087590 m_timeStamp=...}	const wxEvent *
+		wxObject	{m_refData=0x00000000ff880000 {m_count=??? } }	wxObject
+		m_eventObject	0x0000000000000466 {m_refData=??? }	wxObject *
		m_eventType	49087590	int
		m_timeStamp	0	long
		m_id	0	int
+		m_callbackUserData	0x0000000000000084 {m_refData=??? }	wxObject *
+		m_handlerToProcessOnlyIn	0x000000140000004d {m_nextHandler=??? m_previousHandler=??? m_dynamicEvents=??? ...}	wxEvtHandler *
		m_propagationLevel	0	int
+		m_propagatedFrom	0x0000025242b2c9b0 {m_type=wxGridWindowNormal (0) }	wxEvtHandler * {wxmsw314u_core_vc140_x64.dll!wxGridWindow}
		m_skipped	true (254)	bool
		m_isCommandEvent	true (255)	bool
		m_wasProcessed	true (255)	bool
		m_willBeProcessedAgain	true (255)	bool

The call stack near the crash point (with both ends trimmed off as they seemed too early or to late to help).

_core.cp38-win_amd64.pyd!meth_wxEvent_GetEventObject(_object * sipSelf, _object * sipArgs) Line 88
	at c:\projects\bb2\dist-win64-py38\build\sip\cpp\sip_corewxevent.cpp(88)
python38.dll!cfunction_call_varargs(_object * func, _object * args, _object * kwargs) Line 757
	at c:\a\27\s\objects\call.c(757)
python38.dll!_PyObject_MakeTpCall(_object * callable, _object * const * args, __int64 nargs, _object * keywords) Line 160
	at c:\a\27\s\objects\call.c(160)
python38.dll!property_descr_get(_object * self, _object * obj, _object * type) Line 1495
	at c:\a\27\s\objects\descrobject.c(1495)
python38.dll!_PyObject_GenericGetAttrWithDict(_object * obj, _object * name, _object * dict, int suppress) Line 1251
	at c:\a\27\s\objects\object.c(1251)
[Inline Frame] python38.dll!PyObject_GenericGetAttr(_object *) Line 1332
	at c:\a\27\s\objects\object.c(1332)
python38.dll!PyObject_GetAttr(_object * v, _object * name) Line 949
	at c:\a\27\s\objects\object.c(949)
python38.dll!_PyEval_EvalFrameDefault(_frame * f, int throwflag) Line 2967
	at c:\a\27\s\python\ceval.c(2967)
[Inline Frame] python38.dll!PyEval_EvalFrameEx(_frame *) Line 741
	at c:\a\27\s\python\ceval.c(741)
[Inline Frame] python38.dll!function_code_fastcall(PyCodeObject * args, _object * const *) Line 283
	at c:\a\27\s\objects\call.c(283)
python38.dll!_PyFunction_Vectorcall(_object * func, _object * const * stack, unsigned __int64 nargsf, _object * kwnames) Line 410
	at c:\a\27\s\objects\call.c(410)
[Inline Frame] python38.dll!_PyObject_Vectorcall(_object * nargsf, _object * const *) Line 127
	at c:\a\27\s\include\cpython\abstract.h(127)
python38.dll!method_vectorcall(_object * method, _object * const * args, unsigned __int64 nargsf, _object * kwnames) Line 89
	at c:\a\27\s\objects\classobject.c(89)
python38.dll!PyVectorcall_Call(_object * callable, _object * tuple, _object * kwargs) Line 199
	at c:\a\27\s\objects\call.c(199)
python38.dll!PyObject_Call(_object * callable, _object * args, _object * kwargs) Line 251
	at c:\a\27\s\objects\call.c(251)
_core.cp38-win_amd64.pyd!wxPyCallback::EventThunker(wxEvent & event) Line 72
	at c:\projects\bb2\dist-win64-py38\build\sip\cpp\sip_corewxevthandler.cpp(72)
wxbase314u_vc140_x64.dll!wxAppConsoleBase::CallEventHandler(wxEvtHandler * handler, wxEventFunctor & functor, wxEvent & event) Line 670
	at c:\projects\bb2\dist-win64-py38\build\ext\wxwidgets\src\common\appbase.cpp(670)

Today’s exploration shows that the sipParseArgs call in sip_corewxEvent.cpp line 75 is producing a value for ‘sipCpp’ that is 0x50 (80) bytes before the actual address of the wxEvent struct. This is seems highly suspicious, as the sizeof(wxEvent) turns out to be 0x50, hmm… In msdev, I can edit the pointer value after the sipParseArgs call to match the address of the ‘event’ struct back in CallEventHandler and then the call ‘sipCpp->GetEventObject()’ at line 82 works as expected.

This only happens when the originating object is a wxGridWindow (see ext/wxWidgets/include/wx/generic/private/grid.h at line 399). None of the other window types, including the other wxGridSubwindow derivatives cause the seg fault, and it looks to me like they traverse pretty much that same code path.

I am still unable to reproduce in anything smaller than the full application. I have a small version that looks pretty much the same, but never crashes, the scroll wheel events just work.

It was an arduous journey, but it turns out to be a poor interaction between GridWindow, TipText and gettext.

My grid cells display tip text upon hover. Sometimes the tip text is just raw text, and sometimes it is text translated using gettext. If the tip text is translated, and an EVT_MOUSEWHEEL occurs while the tip is displayed on the grid, the event address is borked as described in excruciating detail above, hence the seg fault. So maybe locale processing, or the translation lookup (in all case the gettext performs an identity transformation, as I’m in en_US and that’s the source definition). I don’t think it is the tip text timer, since that occurs with the raw text messages, too, and nothing fails then. Who knows what the real issue is, certainly not me, but I at least know what to avoid now…

I have tested some other ScrolledWindow-based cases, and they do not exhibit this problem. For example, our plotting control panel is scrolled and the buttons and dropdowns on it all use translated tip text, yet it scrolls without issue when the tips are on-screen.