subagent transcript

session 17f09f89 · workflow wf_93de0fbc
delegated task
PROVEN (empirical, ground truth): On FreeCAD-for-WebAssembly (Qt 6.11 static, JSPI + native wasm-EH, single-thread QT_FEATURE_thread=-1, shiboken/PySi
▸ Magik
PROVEN (empirical, ground truth): On FreeCAD-for-WebAssembly (Qt 6.11 static, JSPI + native wasm-EH, single-thread QT_FEATURE_thread=-1, shiboken/PySide bindings), activating the CAM **or** BIM GUI workbench throws a wasm 'RuntimeError: memory access out of bounds'. It is NOT OOM (peak wasm memory 23-29MB, MAXIMUM_MEMORY=4GB, no emscripten abort() — onAbort never fires) and NOT a missing Python module (all imports succeed first). CAM and BIM produce the IDENTICAL, deterministic stack (top frames, truncated below by the JSPI promise boundary): RuntimeError: memory access out of bounds at QMetaObject::cast(QObject const*) const <-- OOB read occurs HERE at QWidget::isActiveWindow() const at QStyleOption::initFrom(QWidget const*) at QGroupBox::initStyleOption(QStyleOptionGroupBox*) const at QGroupBox::event(QEvent*) at QApplicationPrivate::notify_helper(QObject*, QEvent*) at QApplication::notify -> Gui::GUIApplication::notify at QCoreApplication::sendEvent(QObject*, QEvent*) at QWidgetPrivate::hideChildren(bool) Working workbenches (Part, Draft, Sketcher) do NOT crash on activation. Qt source tree: /opt/toolchains/qtsrc/qtbase (read qtbase/src/corelib/kernel/qmetaobject.cpp, qtbase/src/widgets/kernel/qwidget.cpp, qtbase/src/widgets/styles/qstyleoption.cpp, qtbase/src/widgets/widgets/qgroupbox.cpp). FreeCAD source: /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui (Application.cpp activateWorkbench/onToolBarSwitch, Workbench.cpp, MainWindow.cpp, DockWindowManager.cpp, PreferencePages/dialog). Read-only — do NOT build. Decide the layer of the bug. Is this (a) a Qt-for-wasm bug (QMetaObject::cast / isActiveWindow assuming something not true under emscripten — e.g., a QWindow platform handle that is null on the offscreen/FBO wasm platform, see the QWasm platform plugin), (b) a shiboken/PySide binding bug (metaobject of a PySide-wrapped QGroupBox subclass is malformed, or Python GC deletes a widget C++ still references), or (c) a FreeCAD widget-lifecycle bug (a preferences/task widget parented then its window destroyed). Look for null-window/isActiveWindow handling in qtbase/src/plugins/platforms or the qtbase wasm platform, and any FreeCAD __EMSCRIPTEN__ widget guards. Give the most-likely layer with evidence + the smallest robust fix (e.g., guard isActiveWindow when window handle null, or fix the FreeCAD widget parenting, or a shiboken ownership flag). Cite file:line.
Fable
thinking
I'm looking at a crash in FreeCAD-wasm where there's an out-of-bounds memory read happening in Qt's QMetaObject::cast function, which gets triggered when QWidget tries to check if it's the active window during style initialization for a QGroupBox. The stack trace shows the issue propagating through the style option initialization chain.

The core problem seems to be that QMetaObject::cast is walking the superclass chain and hitting garbage data—likely because the QObject's metaobject pointer is pointing to a deleted or dangling object. I need to examine the Qt source files to understand exactly where the pointer corruption occurs and trace back to what's causing the object to be in an invali  …
Fable
⚙ Bash {"command": "grep -n \"QMetaObject::cast\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp | head", "description": "Find QMetaObject::cast"}
tool result
398:    \fn QObject *QMetaObject::cast(QObject *obj) const
411:const QObject *QMetaObject::cast(const QObject *obj) const
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "grep -n \"isActiveWindow\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp | head", "description": "Find isActiveWindow"}
tool result
576:        \l isActiveWindow, activateWindow(), \l minimized, showMinimized(),
4581:    } else if ((!isVisible() || isActiveWindow())
6550:    proxy) if this widget or one of its parents is the \l{isActiveWindow()}{active window}. The \a reason argument will
6620:    if (f->isActiveWindow()) {
6754:    \l{isActiveWindow()}{active window}.
6953:    \property QWidget::isActiveWindow
6967:bool QWidget::isActiveWindow() const
6976:            return tlwExtra->proxyWidget->isActiveWindow();
6983:     …[truncated]
Fable
⚙ Bash {"command": "grep -n \"initFrom\\|isActiveWindow\\|State_Active\" /opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp | head", "description": "Find initFrom"}
tool result
129:    \fn void QStyleOption::initFrom(const QWidget *widget)
141:void QStyleOption::initFrom(const QWidget *widget)
153:    if (window->isActiveWindow())
154:        state |= QStyle::State_Active;
241:    \sa initFrom()
250:    \sa initFrom()
259:    \sa initFrom()
268:    \sa initFrom()
284:    \sa initFrom()
293:    \sa initFrom(), QStyle::drawPrimitive(), QStyle::drawControl(),
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm reviewing the key sections now.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp", "offset": 411, "limit": 40}
tool result
411	const QObject *QMetaObject::cast(const QObject *obj) const
412	{
413	    return (obj && obj->metaObject()->inherits(this)) ? obj : nullptr;
414	}
415	
416	#ifndef QT_NO_TRANSLATION
417	/*!
418	    \internal
419	*/
420	QString QMetaObject::tr(const char *s, const char *c, int n) const
421	{
422	    return QCoreApplication::translate(className(), s, c, n);
423	}
424	#endif // QT_NO_TRANSLATION
425	
426	/*!
427	    \since 6.2
428	    Returns the metatype corresponding to this metaobject.
429	    …[truncated]
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 6967, "limit": 30}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp", "offset": 141, "limit": 30}
tool result
6967	bool QWidget::isActiveWindow() const
6968	{
6969	    QWidget *tlw = window();
6970	    if (tlw == QApplication::activeWindow() || (isVisible() && (tlw->windowType() == Qt::Popup)))
6971	        return true;
6972	
6973	#if QT_CONFIG(graphicsview)
6974	    if (const auto &tlwExtra = tlw->d_func()->extra) {
6975	        if (isVisible() && tlwExtra->proxyWidget)
6976	            return tlwExtra->proxyWidget->isActiveWindow();
6977	    }
6978	#endif
6979	
6980	    if (style()->styleHint(QStyle::  …[truncated]
tool result
141	void QStyleOption::initFrom(const QWidget *widget)
142	{
143	    QWidget *window = widget->window();
144	    state = QStyle::State_None;
145	    if (widget->isEnabled())
146	        state |= QStyle::State_Enabled;
147	    if (widget->hasFocus())
148	        state |= QStyle::State_HasFocus;
149	    if (window->testAttribute(Qt::WA_KeyboardFocusChange))
150	        state |= QStyle::State_KeyboardFocusChange;
151	    if (widget->underMouse())
152	        state |= QStyle::State_MouseOver;
153	    …[truncated]
Fable
thinking
I'm seeing that `QMetaObject::cast` performs an inheritance check by calling `metaObject()->inherits()`, which traverses the superclass chain through `d.superdata` pointers. When the object is a dangling pointer to freed memory, `metaObject()` returns garbage data, and then `inherits` walks through invalid superdata pointers, causing the out-of-bounds read. Now I need to trace back where `QMetaObject::cast` gets invoked from `isActiveWindow` — the crash stack shows it's coming from `QApplication::activeWindow()`, but the frame is cut off.

Looking at the `isActiveWindow` code around line 6970, there's no obvious `qobject_cast` visible, though it could be inlined from within `QApplication::ac  …
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 6993, "limit": 30}
tool result
6993	
6994	    // Check for an active window container
6995	    if (QWindow *ww = QGuiApplication::focusWindow()) {
6996	        while (ww) {
6997	            QWidgetWindow *qww = qobject_cast<QWidgetWindow *>(ww);
6998	            QWindowContainer *qwc = qww ? qobject_cast<QWindowContainer *>(qww->widget()) : 0;
6999	            if (qwc && qwc->topLevelWidget() == tlw)
7000	                return true;
7001	            ww = ww->parent();
7002	        }
7003	    }
7004	
7005	    // Check if plat  …[truncated]
Fable
⚙ Bash {"command": "grep -n \"inherits\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp | head", "description": "Find inherits"}
tool result
292:    if (!mobj->inherits(&QObject::staticMetaObject)) {
380:    Returns \c true if the class described by this QMetaObject inherits
387:bool QMetaObject::inherits(const QMetaObject *metaObject) const noexcept
401:    Returns \a obj if object \a obj inherits from this
408:    Returns \a obj if object \a obj inherits from this
413:    return (obj && obj->metaObject()->inherits(this)) ? obj : nullptr;
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
Looking at the qobject_cast call at line 6997, it's attempting to cast a QWindow pointer from `QGuiApplication::focusWindow()` to QWidgetWindow by checking the metaobject inheritance chain. The issue seems to be that if this pointer is dangling or if traversing the parent chain returns something invalid, it could lead to an out-of-bounds access.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp", "offset": 387, "limit": 12}
tool result
387	bool QMetaObject::inherits(const QMetaObject *metaObject) const noexcept
388	{
389	    const QMetaObject *m = this;
390	    do {
391	        if (metaObject == m)
392	            return true;
393	    } while ((m = m->d.superdata));
394	    return false;
395	}
396	
397	/*!
398	    \fn QObject *QMetaObject::cast(QObject *obj) const
Fable
⚙ Bash {"command": "grep -rn \"focusWindow\\|m_focusWindow\" /opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp | head", "description": "focusWindow source"}
tool result
/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp:469:            focusWindow(),
/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp:1004:    // might be the same as focusWindow() if that's a popup
/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp:1045:QWindow *QGuiApplication::focusWindow()
/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp:1060:    \fn QGuiApplication::focusWindowChanged(QWindow *focusWindow)
/opt/toolchains/qtsrc/qtbase/src  …[truncated]
Fable
thinking
So the out-of-bounds read happens because `QMetaObject::inherits` loops through the superclass chain by dereferencing `m->d.superdata`, and when you call `qobject_cast` on a dangling pointer, the metaobject pointer is stale—the object's already been deleted, so you're walking through garbage memory until you hit an access violation.

The crash occurs in `isActiveWindow()` when it calls `qobject_cast<QWidgetWindow*>(focusWindow())`, meaning the window pointer returned by `focusWindow()` is dangling. But here's the puzzle: if this were a general Qt-wasm platform issue where `focus_window` isn't cleared after deletion, every workbench activation would crash, not just CAM and BIM. The fact that   …
Fable
⚙ Bash {"command": "grep -n \"::event\\|initStyleOption\\|hideChildren\\|updateCheckable\\|calculateFrame\" /opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp | head", "description": "groupbox event"}
Fable
⚙ Bash {"command": "grep -n \"hideChildren\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "description": "hideChildren location"}
tool result
8264:    hideChildren(false);
8535:void QWidgetPrivate::hideChildren(bool spontaneous)
8555:        widget->d_func()->hideChildren(spontaneous);
9308:                d->hideChildren(true);
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
tool result
37:    void calculateFrame();
63:void QGroupBox::initStyleOption(QStyleOptionGroupBox *option) const
187:    calculateFrame();
202:    d->calculateFrame();
283:    initStyleOption(&option);
288:bool QGroupBox::event(QEvent *e)
306:    initStyleOption(&box);
354:    return QWidget::event(e);
425:void QGroupBoxPrivate::calculateFrame()
429:    q->initStyleOption(&box);
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 8535, "limit": 30}
tool result
8535	void QWidgetPrivate::hideChildren(bool spontaneous)
8536	{
8537	    Q_Q(QWidget);
8538	    qCDebug(lcWidgetShowHide) << "Hiding children of" << q
8539	                              << "spontaneously" << spontaneous;
8540	
8541	    QList<QObject*> childList = children;
8542	    for (int i = 0; i < childList.size(); ++i) {
8543	        QWidget *widget = qobject_cast<QWidget*>(childList.at(i));
8544	        if (!widget)
8545	            continue;
8546	        qCDebug(lcWidgetShowHide) << "Cons  …[truncated]
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp", "offset": 288, "limit": 40}
tool result
288	bool QGroupBox::event(QEvent *e)
289	{
290	    Q_D(QGroupBox);
291	#ifndef QT_NO_SHORTCUT
292	    if (e->type() == QEvent::Shortcut) {
293	        QShortcutEvent *se = static_cast<QShortcutEvent *>(e);
294	        if (se->shortcutId() == d->shortcutId) {
295	            if (!isCheckable()) {
296	                d->_q_fixFocus(Qt::ShortcutFocusReason);
297	            } else {
298	                d->click();
299	                setFocus(Qt::ShortcutFocusReason);
300	            }
301	          …[truncated]
Fable
thinking
So the issue is that QGroupBox::event calls initStyleOption for every single event, including QHideEvent from hideChildren. This triggers a chain: initStyleOption → initFrom → window()->isActiveWindow() → focusWindow() with a qobject_cast that causes an out-of-bounds access. The problem traces back to how window() is retrieved in initFrom and then isActiveWindow() is called on it.

The real culprit appears to be focusWindow() returning a stale pointer to a deleted QWindow. When that dangling pointer's vtable is accessed through the virtual metaObject() call, it reads garbage, causing the QMetaObject inherits loop to walk out of bounds. This fits the Qt-wasm platform bug pattern where QGuiApp  …
Fable
⚙ Bash {"command": "ls /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/ 2>/dev/null; echo \"---\"; grep -rn \"isActive\\|focus_window\\|focusWindow\\|setFocusWindow\\|activeWindow\" /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/*…
tool result
CMakeLists.txt
main.cpp
qtloader.js
qwasmaccessibility.cpp
qwasmaccessibility.h
qwasmbackingstore.cpp
qwasmbackingstore.h
qwasmbase64iconstore.cpp
qwasmbase64iconstore.h
qwasmclipboard.cpp
qwasmclipboard.h
qwasmcompositor.cpp
qwasmcompositor.h
qwasmcssstyle.cpp
qwasmcssstyle.h
qwasmcursor.cpp
qwasmcursor.h
qwasmdom.cpp
qwasmdom.h
qwasmdrag.cpp
qwasmdrag.h
qwasmevent.cpp
qwasmeventdispatcher.cpp
qwasmeventdispatcher.h
qwasmevent.h
qwasmfiledialoghelper.cpp
qwasmfiledialoghelper.h
qwasmfontdatabas  …[truncated]
Fable
thinking
I'm checking how Qt handles the focus_window pointer when a window is destroyed—specifically whether QWindow's destructor or QGuiApplicationPrivate properly clears it to avoid dangling references. The standard Qt implementation should handle this through hide_helper or the destroy mechanism, but I want to verify the actual behavior rather than assume.

Actually, stepping back—the real issue is likely FreeCAD-specific. When workbench activation hides/shows dock windows and toolbars, a QGroupBox in a being-hidden subtree receives a QHideEvent. QGroupBox::event calls initStyleOption unconditionally, which calls initFrom, which calls window()->isActiveWindow(). Walking up the parent chain to fin  …
Fable
⚙ Bash {"command": "grep -n \"focus_window\" /opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp", "description": "focus_window usage"}
tool result
194:Q_CONSTINIT QWindow *QGuiApplicationPrivate::focus_window = nullptr;
1047:    return QGuiApplicationPrivate::focus_window;
2703:    QWindow *previous = QGuiApplicationPrivate::focus_window;
2726:    QGuiApplicationPrivate::focus_window = newFocus;
2742:    if (QGuiApplicationPrivate::focus_window) {
2748:        QCoreApplication::sendSpontaneousEvent(QGuiApplicationPrivate::focus_window, &focusIn);
2749:        QObject::connect(QGuiApplicationPrivate::focus_window, SIGNAL(focusObjectChanged(  …[truncated]
Fable
⚙ Bash {"command": "grep -n \"focus_window\\|~QWindow\\|QWindowPrivate::destroy\\|processWindowSystemEvent\\|clearFocusObject\" /opt/toolchains/qtsrc/qtbase/src/gui/kernel/qwindow.cpp | head", "description": "QWindow destroy focus clearing"}
tool result
175:QWindow::~QWindow()
200:    if (QGuiApplicationPrivate::focus_window == this)
201:        QGuiApplicationPrivate::focus_window = nullptr;
214:QWindowPrivate::~QWindowPrivate()
614:void QWindowPrivate::clearFocusObject()
2146:void QWindowPrivate::destroy()
2183:    if (QGuiApplicationPrivate::focus_window == q)
2184:        QGuiApplicationPrivate::focus_window = q->parent();
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
Looking at the Qt source, the destructor clears focus_window if it points to the destroyed window, and QWindowPrivate::destroy reassigns it to the parent window. So Qt does handle cleanup on destruction. But there's a potential issue: if the parent window is also being destroyed or is already dangling, reassigning focus_window to it could leave a dangling pointer. Let me think through whether that's actually the source of the problem here.

Actually, I should reconsider whether the issue is really in the focusWindow path at all. The loop only executes if there's an active focus window, so on the wasm offscreen platform, maybe there never is one. Let me step back and think about this from the  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -n \"activateWorkbench\\|hide()\\|hideChildren\\|setVisible(false)\\|->hide\" Application.cpp | head -30", "description": "activateWorkbench in FreeCAD"}
tool result
1358:            activateWorkbench(d->activeDocument->workbench().c_str());
1740:        vp->hide();
1901:bool Application::activateWorkbench(const char* name)
1963:            getMainWindow()->activateWorkbench(QString::fromLatin1(name));
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -rn \"EMSCRIPTEN\\|__wasm__\" Application.cpp MainWindow.cpp Workbench.cpp DockWindowManager.cpp | head", "description": "emscripten guards in Gui"}
tool result
Application.cpp:463:#ifdef __EMSCRIPTEN__
Application.cpp:2740:#ifdef __EMSCRIPTEN__
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 70}
tool result
1901	bool Application::activateWorkbench(const char* name)
1902	{
1903	    bool ok = false;
1904	    WaitCursor wc;
1905	    Workbench* oldWb = WorkbenchManager::instance()->active();
1906	    if (oldWb && oldWb->name() == name) {
1907	        return false;  // already active
1908	    }
1909	
1910	    Base::PyGILStateLocker lock;
1911	    // we check for the currently active workbench and call its 'Deactivated'
1912	    // method, if available
1913	    PyObject* pcOldWorkbench = nullptr;
1914	    …[truncated]
Fable
thinking
I'm examining the MainWindow::activateWorkbench method to understand how it orchestrates the GUI changes, particularly the toolbar switching and hideChildren behavior.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -n \"activateWorkbench\\|onToolBarSwitch\\|removeToolBars\\|setVisible\\|->hide\\|show()\" MainWindow.cpp | head -30", "description": "MainWindow workbench switch"}
tool result
387:                    sb->setVisible(d->hGrp->GetBool("StatusBar", sb->isVisible()));
1161:void MainWindow::activateWorkbench(const QString& name)
1437:        view->show();
1587:            this->activateWorkbench(currWb);
1658:        action->setVisible(true);
1665:        actions[index]->setVisible(false);
1669:        actions.last()->setVisible(true);
1771:            mdi->hide();
2088:        it->show();
2093:        it->show();
2193:    max ? showMaximized() : show();
2209:    statusBar(  …[truncated]
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -n \"activateWorkbench\\|onToolBarSwitch\" Application.cpp Workbench.cpp WorkbenchManager.cpp 2>/dev/null", "description": "onToolBarSwitch"}
tool result
Application.cpp:1358:            activateWorkbench(d->activeDocument->workbench().c_str());
Application.cpp:1901:bool Application::activateWorkbench(const char* name)
Application.cpp:1963:            getMainWindow()->activateWorkbench(QString::fromLatin1(name));
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 1161, "limit": 40}
tool result
1161	void MainWindow::activateWorkbench(const QString& name)
1162	{
1163	    // remember workbench by tab (if enabled)
1164	
1165	    const ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
1166	        "User parameter:BaseApp/Preferences/View"
1167	    );
1168	    const bool saveWB = hGrp->GetBool("SaveWBbyTab", false);
1169	    MDIView* view = activeWindow();
1170	    if (view && saveWB) {
1171	        view->setProperty("ownWB", name);
1172	    }
1173	
1174	    // emit  …[truncated]
Fable
thinking
The toolbar rebuild is actually happening in the Workbench activation flow rather than the switch itself, and the hideChildren calls are tied to visibility changes on toolbars and dock widgets. I'm reconsidering my approach to the FreeCAD profiling issue — I need to step back and look at the evidence more carefully instead of getting caught up in one particular theory.

Looking at the actual crash mechanism through `QMetaObject::inherits`, the vulnerability happens when either the object's metaObject pointer is corrupted or the superdata chain walks out of bounds. In a static wasm build with custom metaobjects, this could happen if a shiboken-generated dynamic metaobject has a malformed supe  …
Fable
⚙ Bash {"command": "grep -rn \"QPointer\\|widget()\\|m_widget\\|QWidget \\*widget\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidgetwindow_p.h 2>/dev/null; echo \"==DESTROY==\"; sed -n '2146,2190p' /opt/toolchains/qtsrc/qtbase/src/gui/kerne…
tool result
/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidgetwindow_p.h:40:    QWidgetWindow(QWidget *widget);
/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidgetwindow_p.h:43:    QWidget *widget() const { return m_widget; }
/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidgetwindow_p.h:50:    static void focusNextPrevChild(QWidget *widget, bool next);
/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidgetwindow_p.h:69:    void handleDragEnterEvent(QDragMoveEvent *, QWidget *widget = nullptr);  …[truncated]
Fable
thinking
I'm tracing through the QPointer behavior in QWidgetWindow — since `m_widget` is a QPointer, it automatically nullifies when the widget is deleted, so calling `widget()` safely returns null rather than a dangling pointer. This means the qobject_cast on line 6998 receives nullptr and safely returns nullptr (since QMetaObject::cast checks for null first), so that's not where the dangling reference originates. Now I'm looking at line 2184 where `focus_window` gets assigned from `q->parent()`.

The key insight is that `destroy()` recursively destroys child windows first before destroying the parent, so when a child's destroy runs and sets `focus_window = child->parent()`, that points to the pare  …
Fable
⚙ Bash {"command": "grep -n \"activeWindow\\|active_window\\|activeWindow()\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qapplication.cpp | head", "description": "activeWindow in qapplication"}
tool result
274:            activeWindow(),
331:QWidget *QApplicationPrivate::active_window = nullptr;        // toplevel with keyboard focus
713:    QApplicationPrivate::active_window = nullptr; //### this should not be necessary
1494:    \sa QWidget::setFocus(), QWidget::hasFocus(), activeWindow(), focusChanged()
1565:    activeWindow() even if there is no focusWidget(), for example if no widget
1571:QWidget *QApplication::activeWindow()
1573:    return QApplicationPrivate::active_window;
1656:    QMessag  …[truncated]
Fable
⚙ Bash {"command": "sed -n '240,300p' /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmwindow.cpp", "description": "wasm window activation handling"}
tool result
    m_beforeInputCallback = QWasmEventHandler(m_window, "beforeinput",
        [this](emscripten::val event){ handleBeforeInputEvent(event); });
    }

QWasmWindow::~QWasmWindow()
{
    QWasmOpenGLContext::destroyWebGLContext(this);

#if QT_CONFIG(accessibility)
    QWasmAccessibility::onRemoveWindow(window());
#endif
    QObject::disconnect(m_transientWindowChangedConnection);
    QObject::disconnect(m_modalityChangedConnection);

    shutdown();

    emscripten::val::module_property("specialHT  …[truncated]
Fable
thinking
I'm noticing that `QApplicationPrivate::active_window` is stored as a raw pointer rather than a QPointer, which raises the question of whether it gets properly cleared when the active top-level widget is destroyed. Looking at the code, pointer comparisons like `tlw == QApplication::activeWindow()` are safe even if the pointer dangles since they don't dereference it, so that's not where the issue lies.

But here's where it gets tricky: during a workbench switch in FreeCAD, when a top-level window (like a floating dock or task dialog) is being destroyed, its `QWasmWindow::shutdown()` calls `requestActivateWindow()` on the previously-active window to reactivate it. If that previous window is al  …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD; ls src/Mod | grep -iE \"path|cam|arch|bim\"; echo \"===\"; sed -n '700,720p' /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qapplication.cpp", "description": "CAM/BIM modules an…
tool result
BIM
CAM
===

    delete QApplicationPrivate::app_style;
    QApplicationPrivate::app_style = nullptr;

#if QT_CONFIG(draganddrop)
    if (!qt_is_tty_app)
        delete QDragManager::self();
#endif

    d->cleanupMultitouch();

    QPixmapCache::clear();

    QApplicationPrivate::active_window = nullptr; //### this should not be necessary

    if (QApplicationPrivate::widgetCount)
        qDebug("Widgets left: %i    Max widgets: %i \n", QWidgetPrivate::instanceCounter, QW  …[truncated]
Fable
thinking
I'm checking whether `active_window` gets dereferenced anywhere during normal operation, since if it points to a deleted widget that could cause issues — though comparing it with `QApplication::activeWindow()` should be safe. Let me look into how FreeCAD's CAM/BIM modules handle widget and dialog creation to understand the pattern better.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod; grep -rln \"QGroupBox\\|QDialog\\|setParent\\|new QWidget\\|QDockWidget\" CAM/Gui BIM 2>/dev/null | head; echo \"=== InitGui files ===\"; ls CAM/Gui/*.cpp 2>/dev/null | h…
tool result
CAM/Gui/TaskDlgPathCompound.cpp
CAM/Gui/DlgJobChooser.ui
CAM/Gui/DlgProcessorChooser.h
CAM/Gui/DlgProcessorChooser.ui
CAM/Gui/DlgProcessorChooser.cpp
CAM/Gui/DlgSettingsPathColor.ui
CAM/Gui/AppPathGuiPy.cpp
CAM/Gui/PreCompiled.h
CAM/Gui/TaskDlgPathCompound.h
CAM/Gui/Resources/panels/DlgJobModelSelect.ui
=== InitGui files ===
CAM/Gui/AppPathGui.cpp
CAM/Gui/AppPathGuiPy.cpp
CAM/Gui/Command.cpp
CAM/Gui/DlgProcessorChooser.cpp
CAM/Gui/DlgSettingsPathColor.cpp
CAM/Gui/TaskDlgPathCompound.cpp
CAM/Gui/  …[truncated]
Fable
thinking
I'm tracing through the CAM and BIM workbench initialization to understand when preference pages get instantiated. Both workbenches register their preference pages through the same mechanism in AppPathGui.cpp, but these pages are typically created when the Preferences dialog opens, not during workbench activation. The crash happens on activation though, so I need to reconsider whether FreeCAD might be pre-loading these pages somewhere.

Let me step back and focus on what I can definitively prove from the evidence. I've confirmed that QGroupBox::event always calls initStyleOption, which checks isActiveWindow(), and that's where the crash occurs—specifically in isActiveWindow's qobject_cast wh  …
Fable
⚙ Bash {"command": "grep -n \"requestActivateWindow\\|handleWindowActivated\\|focus_window\\|setFocusWindow\\|::focus(\" /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmwindow.cpp; echo \"=== gui setFocusWindow ===\"; grep -rn \"focus…
tool result
292:            prevW->requestActivateWindow();
349:    requestActivateWindow();
1087:void QWasmWindow::requestActivateWindow()
1091:        static_cast<QWasmWindow *>(modalWindow->handle())->requestActivateWindow();
1100:    QPlatformWindow::requestActivateWindow();
1103:void QWasmWindow::focus()
=== gui setFocusWindow ===
/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp:194:Q_CONSTINIT QWindow *QGuiApplicationPrivate::focus_window = nullptr;
/opt/toolchains/qtsrc/qtbase/src/gui/  …[truncated]
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp", "offset": 2695, "limit": 55}
tool result
2695	    currentMouseWindow = nullptr;
2696	
2697	    QEvent event(QEvent::Leave);
2698	    QCoreApplication::sendSpontaneousEvent(e->leave.data(), &event);
2699	}
2700	
2701	void QGuiApplicationPrivate::processFocusWindowEvent(QWindowSystemInterfacePrivate::FocusWindowEvent *e)
2702	{
2703	    QWindow *previous = QGuiApplicationPrivate::focus_window;
2704	    QWindow *newFocus = e->focused.data();
2705	
2706	    if (previous == newFocus)
2707	        return;
2708	
2709	    bool activatedPopup =  …[truncated]
Fable
thinking
The focus_window gets set through processFocusWindowEvent when a FocusWindowEvent is queued, extracting the focused target from a QPointer — which means newFocus becomes null if that target has already been destroyed. This leaves focus_window as a dangling raw pointer. The critical issue emerges during QWasmWindow teardown: shutdown() calls requestActivateWindow() on the previous window, and I need to trace what that actually does to understand how it triggers the crash.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmwindow.cpp", "offset": 1087, "limit": 35}
tool result
1087	void QWasmWindow::requestActivateWindow()
1088	{
1089	    QWindow *modalWindow;
1090	    if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) {
1091	        static_cast<QWasmWindow *>(modalWindow->handle())->requestActivateWindow();
1092	        return;
1093	    }
1094	
1095	    raise();
1096	    setAsActiveNode();
1097	
1098	    if (!QWasmIntegration::get()->inputContext())
1099	        focus();
1100	    QPlatformWindow::requestActivateWindow();
1101	}
1102	
110  …[truncated]
Fable
thinking
I'm trying to recall the exact behavior of `QPlatformWindow::requestActivateWindow()` — whether it posts a FocusWindowEvent or calls `handleFocusWindowChanged()` depending on the Qt version — but I have enough context to make a solid decision without diving deeper into those implementation details.

There's a potential issue at line 1090 where `isWindowBlocked` checks for modal windows: if a modal window's platform handle is null (a QWindow without a backing platform window), the static cast yields a dangling pointer and calling `requestActivateWindow()` on it could set focus to garbage. That's a separate crash vector though.

The real question is whether `QPlatformWindow::requestActivateWin  …
Fable
I have traced the full crash path through Qt source and the wasm platform plugin. Here is my analysis.
Fable
⚙ StructuredOutput {"area": "Qt-for-wasm platform plugin (layer a): stale QGuiApplicationPrivate::focus_window consumed by QWidget::isActiveWindow(); QGroupBox::event() is the amplifier, CAM/BIM's transient top-level window is the trigger", "findings": "ROOT …
tool result
Structured output provided successfully