segfault from QApplication::setFont() after discarding a temporary widget
-
I have a QTreeView that displays thousands of items, each with variable height due to word-wrapped text. To accomplish this efficiently (i.e. avoid putting thousands of widgets in the event loop) the delegate creates temporary widgets to calculate item size in sizeHint() and draw in paint(). This works nicely.
For performance when responding to rapid-fire events like mouse movement and window resizing, the most recently used temporary widgets are saved in an LRU cache. Old widgets are evicted from the cache and discarded when new ones are needed.
The problem:
When a user changes their system font, Qt crashes the process with a segmentation fault. The same thing happens if the app changes its own font with QApplication.setFont(). The crash is intermittent, but happens frequently on my KDE Plasma system.
If I disable the cache, so temporary widgets are discarded immediately instead of when new ones are created, the segfault goes away. (Of course, that also hurts event processing performance.) QListView seems immune to the crash, but I need QTreeView since the items are hierarchical.
The top of the stack when the segfault happens:
#0 0x00007fc8e92adf44 in QCoreApplication::notifyInternal2(QObject*, QEvent*) () from /lib/x86_64-linux-gnu/libQt5Core.so.5 #1 0x00007fc8e3d65a2e in QApplication::setFont(QFont const&, char const*) () from /lib/x86_64-linux-gnu/libQt5Widgets.so.5I don't have debug symbols, but gdb shows a 0 in the RDI register, which (if I understand amd64 calling conventions correctly) suggests that Qt called
notifyInternal2()with a null pointer as itsQObject *argument. If so, that seems like a bug in Qt.I wouldn't mind some more experienced eyes on this before I file a report. Perhaps I'm missing something. Anyone know what's going on here?
I have reproduced the crash with Qt 5 in KDE Plasma, Xfce, and Windows 10, using this program:
#!/usr/bin/env python3 """ Reproduce a segfault by changing font size. Usage: Press (or hold) the Space bar until it crashes. The Qt -style command line option might influence the likelihood of a crash. On Linux, it seems more common with -style Fusion/Breeze/Windows. On Win10, it seems more common with -style fusion/windowsvista. """ import functools import sys from PySide2.QtCore import QAbstractListModel from PySide2.QtCore import QModelIndex from PySide2.QtCore import QPoint from PySide2.QtCore import Qt from PySide2.QtCore import Slot from PySide2.QtGui import QFont from PySide2.QtWidgets import QApplication from PySide2.QtWidgets import QLabel from PySide2.QtWidgets import QStyledItemDelegate from PySide2.QtWidgets import QTreeView from PySide2.QtWidgets import QVBoxLayout from PySide2.QtWidgets import QWidget WIDGET_CACHE_SIZE = 2 ITEM_COUNT = WIDGET_CACHE_SIZE + 1 #xxx ensure a cache eviction class SimpleListModel(QAbstractListModel): "A model for demonstrating variable-size items" def rowCount(self, _parentidx): return ITEM_COUNT def data(self, _index, role=Qt.DisplayRole): if role == Qt.DisplayRole: return "An item with wrapping text. Press Space." class VariableSizeItemDelegate(QStyledItemDelegate): "Draw variable-size items with help from a widget" @functools.lru_cache(maxsize=WIDGET_CACHE_SIZE) def get_widget(self, index): """Get a widget for drawing a model item. Cache these, for performance during window resize & mouse operations. """ widget = QLabel(index.data()) widget.setWordWrap(True) return widget @staticmethod def update_widget_size(widget, option): "Size a widget according to a QStyleOptionViewItem" view = option.styleObject width = view.columnWidth(0) - view.indentation() widget.setFixedWidth(width) widget.adjustSize() @Slot() def invalidate_sizes(self): "Tell the view that new sizeHint() calls are needed" self.sizeHintChanged.emit(QModelIndex()) def sizeHint(self, option, index): widget = self.get_widget(index) #xxx cache eviction sets up a segfault self.update_widget_size(widget, option) return widget.size() def paint(self, painter, option, index): widget = self.get_widget(index) self.update_widget_size(widget, option) painter.save() painter.translate(option.rect.topLeft()) widget.render(painter, QPoint()) painter.restore() class SimpleTreeView(QTreeView): "Simple tree view to help reproduce a segfault" def __init__(self): super().__init__() delegate = VariableSizeItemDelegate() self.setItemDelegate(delegate) self.header().sectionResized.connect(delegate.invalidate_sizes) self.model = SimpleListModel() self.setModel(self.model) self.setWindowTitle("Press Space to Crash") def keyPressEvent(self, event): if event.key() == Qt.Key_Space: font = QFont(QApplication.font()) size = font.pointSize() + 1 if font.pointSize() < 20 else 6 font.setPointSize(size) print('\r setFont() call...', end='') QApplication.setFont(font) #xxx trigger segfault print('\r setFont() return.', end='') return super().keyPressEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) window = SimpleTreeView() window.show() sys.exit(app.exec_()) -
Reported here:
https://bugreports.qt.io/browse/PYSIDE-2022