Skip to content

fix: incarnate QProxyStyle before installing app-wide event filters - #586

Open
FelipeDefensor wants to merge 1 commit into
devfrom
fix/score-component-test-segfault
Open

fix: incarnate QProxyStyle before installing app-wide event filters#586
FelipeDefensor wants to merge 1 commit into
devfrom
fix/score-component-test-segfault

Conversation

@FelipeDefensor

Copy link
Copy Markdown
Collaborator

Problem

LongOperationToolbar installs two event filters on the QApplication while a long operation runs, so they are handed every object that receives an event.

PySide6 builds the Python wrapper for a Qt type the first time Python needs one. Doing that from inside an event filter re-enters shiboken's lazy type creation — incarnateType -> PyModule_lazyGetAttro -> incarnateType — where the inner call erases from the same type map the outer call is still iterating. That is a null dereference, and the process dies with SIGSEGV.

Native backtrace at the crash (EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at 0x0), read bottom-up:

QCoreApplication::sendEvent
QApplication::notify
QApplicationPrivate::sendThroughApplicationEventFilters
QObjectWrapper::eventFilter                     <- our Python filter
PySide::getWrapperForQObject
Shiboken::Object::newObjectWithHeuristicsHelper
Shiboken::Module::get
Shiboken::Module::PyModule_lazyGetAttro
Shiboken::Module::incarnateType                 <- nested
init_QProxyStyle
Shiboken::Module::incarnateType                 <- outer, iterating
std::__hash_table<..., TypeCreationStruct>::erase   <- crash

The type that triggers it is QProxyStyle, which QWidget.setStyleSheet installs via QStyleSheetStyle. TiLiA calls setStyleSheet in several places, including TiliaMainWindow.

Fix

Import QProxyStyle in tilia/ui/long_operation.py. That incarnates the type at import time, outside any filter, so the lazy path can never run from within one. One import plus a comment explaining why it is not dead code.

How it shows up

The crash lands in an unrelated test that opens a file — file.open is a @long_operation, so the filters are installed while timeline scenes are being built. Whether a given ordering crashes depends on whether some earlier test already incarnated the type, which initially made it look like the tests were at fault rather than the lazy-init path.

Adding a score-component test to tests/timelines/test_timeline_component_manager.py and running:

pytest tests/timelines/test_timeline_component_manager.py \
       tests/ui/timelines/score/test_score_timeline_ui.py::test_create_note \
       tests/ui/timelines/score/test_score_timeline_ui.py::test_create_staff \
       tests/file/

segfaults 5/5 before this change and passes 5/5 after. It does not reproduce under pytest -n auto, because xdist distributes those files to different workers — which is why the suite is currently green despite the latent crash.

Why not PYSIDE6_OPTION_LAZY=0

Setting that env var also avoids the crash, and confirms the mechanism. I deliberately did not add it to pytest-env: the suite should run in the same configuration as the shipped app, otherwise a crash of this kind gets masked in CI instead of caught. It also measured no faster or slower either way, so there is no performance argument for it.

Test

TestLazyTypeIncarnation asserts the invariant directly — an un-incarnated type is absent from the QtWidgets module dict — in a subprocess, since any earlier test in the same process may have incarnated it already. Fails without the fix (AssertionError: not incarnated on import), passes with it, in under a second and without needing a QApplication.

Full suite: 1828 passed. The tests/test_app.py failures under pytest -n auto are pre-existing xdist flakiness — all 90 pass serially.

LongOperationToolbar installs two event filters on the QApplication while a
long operation runs, so they are handed every object that receives an event.
PySide6 builds the Python wrapper for a Qt type the first time Python needs
one, and doing that from inside an event filter re-enters shiboken's lazy type
creation: incarnateType -> PyModule_lazyGetAttro -> incarnateType, where the
inner call erases from the same type map the outer call is still iterating.
That is a null dereference and the process dies with SIGSEGV.

The type that triggers it is QProxyStyle, which QWidget.setStyleSheet installs
via QStyleSheetStyle -- TiLiA calls setStyleSheet in several places, including
TiliaMainWindow. Importing QProxyStyle in this module incarnates the type at
import time, outside any filter, so the lazy path can never run from within
one.

Observed in the test suite, where the crash lands in an unrelated test that
opens a file (file.open is a @long_operation, so the filters are installed
while timeline scenes are being built). Whether a given ordering crashes
depends on whether some earlier test already incarnated the type, which made
it look like the tests at fault rather than the lazy-init path:

    pytest tests/timelines/test_timeline_component_manager.py \
           tests/ui/timelines/score/test_score_timeline_ui.py::test_create_note \
           tests/ui/timelines/score/test_score_timeline_ui.py::test_create_staff \
           tests/file/

with a score-component test added to the first module segfaults 5/5 before this
change and passes 5/5 after. Setting PYSIDE6_OPTION_LAZY=0 also avoids it,
which confirms the mechanism, but that is not set in tests: the suite should
run in the same configuration as the shipped app, or a crash of this kind
would be masked in CI instead of caught.

The regression test asserts the invariant directly -- an un-incarnated type is
absent from the QtWidgets module dict -- in a subprocess, since any earlier
test in the same process may have incarnated it already.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant