fix: incarnate QProxyStyle before installing app-wide event filters - #586
Open
FelipeDefensor wants to merge 1 commit into
Open
fix: incarnate QProxyStyle before installing app-wide event filters#586FelipeDefensor wants to merge 1 commit into
FelipeDefensor wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
LongOperationToolbarinstalls two event filters on theQApplicationwhile 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 withSIGSEGV.Native backtrace at the crash (
EXC_BAD_ACCESS,KERN_INVALID_ADDRESS at 0x0), read bottom-up:The type that triggers it is
QProxyStyle, whichQWidget.setStyleSheetinstalls viaQStyleSheetStyle. TiLiA callssetStyleSheetin several places, includingTiliaMainWindow.Fix
Import
QProxyStyleintilia/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.openis 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.pyand running: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=0Setting 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
TestLazyTypeIncarnationasserts the invariant directly — an un-incarnated type is absent from theQtWidgetsmodule 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 aQApplication.Full suite: 1828 passed. The
tests/test_app.pyfailures underpytest -n autoare pre-existing xdist flakiness — all 90 pass serially.