Crash tearing 2nd-to-last dock out of a floating tabbed group (QDockWidgetGroupWindow) — Windows, Qt 6.11.1
-
Crash tearing 2nd-to-last dock out of a floating tabbed group (QDockWidgetGroupWindow) — Windows, Qt 6.11.1
Posting here because the bug tracker login is looping for me; happy for a maintainer
to move this into Jira. Minimal 40-line repro + crash stack below.What happens
On Windows, dragging the second-to-last dock widget out of a floating tabbed
group (QDockWidgetGroupWindow) crashes with an access violation. The group drops
from 2 members to 1, Qt destroys the group window and reparents the surviving dock
to the main window, andQDockAreaLayoutInfo::reparentWidgets()dereferences a
danglingitem.widgetItemwhileQMainWindowLayout::applyState()replays the
drag's saved layout state.- A 3-member group torn down to 2 does not crash — only the transition that
destroys the group window (2→1). - It's not drag-specific: programmatically removing one member of a 2-member
group viaaddDockWidget()+setFloating()also crashes, just deferred ~1 s.
Removing both (2→0, empty group) crashes too — so the fault is the group-window
teardown itself.
Looks like a Windows regression / uncovered case of
QTBUG-118579 (fixed 6.5.4 / 6.6.2 /
6.7.0 for Linux/X11 + macOS). That fix — theqobject_cast<QDockWidgetGroupWindow*>
skip inreparentWidgets— is present in 6.11.1 but runs after the faulting
dereference.Environment
Qt 6.11.1, Windows 11, MSVC x64 (reproduced with the stock
msvc2022_64shared
kit and with a from-source static build).Steps to reproduce
- Build & run the program below (3 dock widgets open floating).
- Drag two of them together into one tabbed floating group.
- Drag one of those two tabs back out and drop it. → crash.
Crash stack
QDockAreaLayoutInfo::reparentWidgets(QWidget*) qdockarealayout.cpp:2210 QMainWindowLayout::applyState(QMainWindowLayoutState&, bool) qmainwindowlayout.cpp:3240 QMainWindowLayout::restore(QInternal::SaveStateRule) qmainwindowlayout.cpp:2727 QDockWidgetPrivate::endDrag(EndDragMode) qdockwidget.cpp:833 QDockWidgetPrivate::setFloating(bool) qdockwidget.cpp:1485 QDockWidget::setFloating(bool) qdockwidget.cpp:1474 QDockWidgetGroupWindow::reparentToMainWindow(QDockWidget*) qdockwidget.cpp:772 QDockWidgetPrivate::endDrag(EndDragMode) qdockwidget.cpp:860 QMainWindowTabBar::mouseReleaseEvent(QMouseEvent*) qmainwindowlayout.cpp:2111The AV reads the
item.widgetItemvalue (seen as0xFFFFFFFFFFFFFFFFand
0x0000000100000001across runs — a stale/garbage pointer initem_list):// qdockarealayout.cpp, QDockAreaLayoutInfo::reparentWidgets if (item.widgetItem) { QWidget *w = item.widgetItem->widget(); // <-- AV (item.widgetItem dangling) if (qobject_cast<QDockWidgetGroupWindow *>(w)) // QTBUG-118579 skip, runs too late continue; ...Minimal reproducer
main.cpp:#include <QApplication> #include <QMainWindow> #include <QDockWidget> #include <QPlainTextEdit> #include <QLabel> int main(int argc, char** argv) { QApplication app(argc, argv); QMainWindow w; w.resize(1000, 650); w.setDockOptions(QMainWindow::AllowTabbedDocks | QMainWindow::AllowNestedDocks | QMainWindow::GroupedDragging); w.setCentralWidget(new QLabel("central", &w)); for (int i = 1; i <= 3; ++i) { auto* d = new QDockWidget(QString("Dock %1").arg(i), &w); d->setWidget(new QPlainTextEdit(QString("content %1").arg(i))); d->setAllowedAreas(Qt::AllDockWidgetAreas); w.addDockWidget(Qt::LeftDockWidgetArea, d); d->setFloating(true); d->move(120 + i * 80, 120 + i * 70); d->resize(360, 260); } w.show(); return app.exec(); }CMakeLists.txt:cmake_minimum_required(VERSION 3.21) project(qtdockcrash LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_standard_project_setup() qt_add_executable(qtdockcrash WIN32 main.cpp) target_link_libraries(qtdockcrash PRIVATE Qt6::Widgets) - A 3-member group torn down to 2 does not crash — only the transition that
-
This is most likely fixed in Qt6.11.2 and Qt6.12
-
Root cause found, and a working fix (local qtbase patch)
Tracked this down to
QDockWidgetGroupWindow::reparentToMainWindow()in
src/widgets/widgets/qmainwindowlayout.cpp. When the group drops below 2 members and
is torn down, that function reparents the dock and callssetFloating(), which nests
intoQDockWidgetPrivate::endDrag() → QMainWindowLayout::restore():reparentToMainWindow()mutatesmwLayout->layoutState(parentInfo.add()+
layoutInfo()->remove()), which frees/replaces someQLayoutItem*.- The drag's
savedStatesnapshot shares those sameQLayoutItem*pointers with
layoutState, so it is now stale. - The nested
setFloating()→endDrag()→restore()doeslayoutState = savedState
and walks it inapplyState()→QDockAreaLayoutInfo::reparentWidgets(), where
item.widgetItem->widget()dereferences a freedwidgetItem→ access violation
(qdockarealayout.cpp ~line 2210). Theqobject_cast<QDockWidgetGroupWindow*>skip
that landed for QTBUG-118579 sits after that deref, so it doesn't help here.
It is not drag-specific: doing the 2→1 dissolution purely programmatically
(addDockWidget()+setFloating()on a member of a 2-member group) crashes too, just
deferred — confirming the fault is the group-window teardown replaying a stale snapshot.The fix
Drop the stale snapshot before the nested
setFloating(), but only when this
reparent actually dissolves the group (< 2 members left) — so a normal tear where the
group survives keeps its originalrestore()untouched:// in QDockWidgetGroupWindow::reparentToMainWindow(), right before // dockWidget->setFloating(wasFloating): if (findChildren<QDockWidget *>(Qt::FindDirectChildrenOnly).size() < 2) mwLayout->savedState.clear();QMainWindowLayoutState::clear()only resets the structure (item_list.clear()); it
does not delete the sharedQLayoutItems, solayoutState(already updated by hand
above) stays valid, and the nestedrestore()early-returns becausesavedStateis now
invalid. (An unconditionalmwLayout->savedState.clear();also fixes the crash, but it
changes the surviving-tear path too and adds a needless repaint on it — hence the guard.)Build a patched qtbase (or just QtWidgets) and link against it. Verified on
Windows 11, Qt 6.11.1, MSVC x64: the 2→1 tear no longer crashes; create / move /
3→2 are unaffected. (A brief repaint flash on tear is inherent Qt reparent behaviour,
present with or without the patch.)For maintainers
This is a local workaround; the cleaner upstream fix is probably to stop
restore()
from replaying asavedStatethatreparentToMainWindow()has just invalidated — e.g.
keepsavedStateconsistent across the dissolution, or skip the nested
restore/reparentWidgets when the group is being torn down. Happy to help test a proper
patch. This looks like a Windows regression/uncovered case of the QTBUG-118579 family
(fixed for Linux/X11 + macOS in 6.5.4 / 6.6.2 / 6.7.0). -
Thanks Christian — that lines up. I checked the
qt/qtbase6.11 branch and the fix
is right there inQDockWidgetGroupWindow::reparentToMainWindow()
(src/widgets/widgets/qmainwindowlayout.cpp): an earlymwLayout->savedState.clear();,
with a comment that nails the cause:mwLayout->widgetAnimator.abort(dockWidget); // The saved state is now invalid because it contains // a reference to the dock widget inside the group window. // - the dock widget has been reparented. // - if it was the last dock widget, the group window will be deleted // => clear saved state. mwLayout->savedState.clear();That matches the root cause exactly: the drag's
savedStatesnapshot shares
QLayoutItem*withlayoutState,reparentToMainWindow()invalidates them, and the
nestedsetFloating()→endDrag()→restore()replays the stale snapshot in
reparentWidgets()→ the AV. (Reassuring to land on the same one-liner independently.)So for anyone hitting this on 6.11.1 before 6.11.2 ships: the one-line backport above
inreparentToMainWindow()fixes it — verified on Windows 11 / Qt 6.11.1 / MSVC x64
(2→1 tear no longer crashes; create/move and the 3→2 tear are unaffected). And once on
6.11.2 / 6.12 the official fix covers it, so the local patch can just be dropped.Thanks again for the quick pointer!