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"}
Fable
⚙ Bash {"command": "grep -n \"isActiveWindow\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp | head", "description": "Find isActiveWindow"}
Fable
⚙ Bash {"command": "grep -n \"initFrom\\|isActiveWindow\\|State_Active\" /opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp | head", "description": "Find initFrom"}
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}
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}
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}
Fable
⚙ Bash {"command": "grep -n \"inherits\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp | head", "description": "Find inherits"}
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}
Fable
⚙ Bash {"command": "grep -rn \"focusWindow\\|m_focusWindow\" /opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp | head", "description": "focusWindow source"}
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"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 8535, "limit": 30}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp", "offset": 288, "limit": 40}
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/*…
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"}
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"}
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"}
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"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 70}
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"}
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"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 1161, "limit": 40}
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…
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"}
Fable
⚙ Bash {"command": "sed -n '240,300p' /opt/toolchains/qtsrc/qtbase/src/plugins/platforms/wasm/qwasmwindow.cpp", "description": "wasm window activation handling"}
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…
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…
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…
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp", "offset": 2695, "limit": 55}
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}
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 …