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.
Analyze the trigger. In qwidget.cpp read QWidgetPrivate::hideChildren(bool) and what event it sends to each child (HideToParent? or a paint/polish?), and QGroupBox::event in qgroupbox.cpp (why does hide trigger initStyleOption -> style polish/repaint?). Then in FreeCAD Gui (Application.cpp activateWorkbench, Workbench.cpp, DockWindowManager/ToolBarManager) find what gets hidden on workbench switch and whether a widget's PARENT or top-level WINDOW is destroyed/reparented while a QGroupBox child still receives events. Is this a use-after-free (parent deleted, child lingers) or a widget with a null/dangling window()? Cite file:line.
Fable
thinking
I'm looking at a crash stack trace from a Qt/FreeCAD issue. The out-of-bounds read is happening in QMetaObject::cast, which gets called through a chain starting with QWidget::isActiveWindow, flowing through style option initialization in QGroupBox, and ultimately triggered by an event handler in the application's notification system.
Fable
I'll analyze the crash by reading the relevant Qt and FreeCAD sources.
Fable
⚙ Bash {"command": "grep -n \"hideChildren\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "description": "Find hideChildren in qwidget.cpp"}
Fable
⚙ Bash {"command": "grep -n \"isActiveWindow\\|QMetaObject::cast\\|::cast(\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp | head", "description": "Find isActiveWindow"}
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/kernel/qwidget.cpp", "offset": 6967, "limit": 30}
Fable
thinking
I'm tracing through the crash in `isActiveWindow()` — it calls `window()` to get the top-level widget, then compares it against `QApplication::activeWindow()`. The stack trace shows `QMetaObject::cast` is being invoked from within `isActiveWindow()`, but the comparison itself shouldn't trigger a cast operation.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 6996, "limit": 30}
Fable
⚙ Bash {"command": "grep -n \"QWidget \\*QWidget::window\\|::activeWindow\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qapplication.cpp", "description": "Find window() and activeWind…
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 4376, "limit": 25}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qapplication.cpp", "offset": 1571, "limit": 15}
Fable
thinking
I'm tracing through the crash in `QMetaObject::cast` that gets triggered by `isActiveWindow`, which uses `qobject_cast<QWidgetWindow *>` to check the focused window. The issue seems to stem from how `style()->styleHint(QStyle::SH_Widget_ShareActiv...)` is being called, though the thought cuts off before revealing the full context.
Looking at the actual cast mechanism, `QMetaObject::cast` receives the target type's static metaobject (which is valid) and the object pointer as an argument. If that object pointer is dangling, the virtual call to `metaObject()` would read from an invalid vtable, causing the crash. I need to examine the actual implementation of `QMetaObject::cast` to understand e …
Fable
⚙ Bash {"command": "grep -n \"QMetaObject::cast\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp", "description": "Find cast in qmetaobject.cpp"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp", "offset": 411, "limit": 40}
Fable
thinking
I'm looking at a cast operation that checks if an object inherits from a certain type, but there's a critical issue: if the `obj` pointer is dangling or freed, dereferencing it to call the virtual `metaObject()` method will read garbage from the vtable, leading to out-of-bounds access. The problem traces back to wherever this cast is being called—likely in `isActiveWindow`—where a dangling pointer is being passed as the argument.
Fable
⚙ Bash {"command": "grep -n \"initFrom\\|isActiveWindow\" /opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp | head", "description": "Find initFrom in qstyleoption.cpp"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp", "offset": 141, "limit": 40}
Fable
thinking
I'm tracing through the `isActiveWindow()` call chain — the QGroupBox's window() method walks up to the top-level widget, then isActiveWindow() gets the top-level again and performs checks including a cast to QWidgetWindow from the focused window, or alternatively calls the style's styleHint method.
But I should consider whether the window pointer itself could be dangling — if the QGroupBox's parent chain leads to a deleted top-level widget. Though QObject destruction properly cleans up parent-child relationships by nullifying pointers and deleting children, so if the parent was destroyed, the QGroupBox would be deleted too. Let me look at what actually happens when hideChildren sends the Q …
Fable
⚙ Bash {"command": "grep -n \"initStyleOption\\|::event\\|QEvent::\" /opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp | head -40", "description": "Find event and initStyleOption in qgroupbox.cpp"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp", "offset": 288, "limit": 30}
Fable
thinking
So the issue traces back to QGroupBox::event being called for every event, including the QHideEvent that hideChildren sends. This triggers a chain: the event reaches initStyleOption, which calls initFrom on the style option, and that eventually checks if the window is active—leading to the out-of-bounds access through QMetaObject::cast. Now I need to figure out why this crash only affects CAM/BIM workbenches and not Part/Draft/Sketcher.
The crash itself is a dangling QObject pointer inside QMetaObject::cast—something's reading from freed memory. I'm tracing through the code path: isActiveWindow calls qobject_cast<QWidgetWindow *> on the focus window, but that's a global so it should be cons …
Fable
QGroupBox::event calls initStyleOption on EVERY event including QHideEvent. Now let me look at FreeCAD's workbench-switch path.
Fable
⚙ Bash {"command": "grep -n \"activateWorkbench\\|hide\\|setVisible\\|onToolBarSwitch\\|removeWorkbench\\|deleteLater\\|delete \" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp | head -50", "description": "Search activateWork…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 100}
Fable
thinking
I need to trace through the MainWindow and Workbench activation code to see which UI elements get hidden during the process.
Fable
Now let me look at MainWindow::activateWorkbench and Workbench.cpp activate.
Fable
⚙ Bash {"command": "grep -n \"activateWorkbench\\|setupWorkbench\\|hide\\|setVisible\\|removeDockWindow\\|delete\\|deleteLater\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Workbench.cpp | head -40", "description": "Search Workbench.cpp f…
Fable
⚙ Bash {"command": "grep -n \"activateWorkbench\\|hideChildren\\|setVisible\\|->hide\\|activateAllStandardMenus\\|setUpdatesEnabled\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp | head -40", "description": "Search MainWindo…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 1161, "limit": 60}
Fable
thinking
I'm tracing through the activateWorkbench flow to understand how WorkbenchManager triggers the Workbench activation, which then constructs menus, toolbars, and dock windows. I need to examine what happens in Workbench::activate and how ToolBarManager and DockWindowManager handle the setup, plus understand when hideChildren gets called during widget visibility changes or reparenting.
Looking at the stack trace, a container widget with a QGroupBox child is being hidden during the workbench switch, which triggers QHideEvent propagation. CAM and BIM likely add task panels or dock widgets containing QGroupBox elements that get hidden during this transition. But the actual crash points to a dangl …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 2720, "limit": 140}
Fable
thinking
I need to step back from this tangent and focus on the actual crash. The real problem is the QMetaObject::cast going out of bounds, which means there's a dangling pointer somewhere. Looking at isActiveWindow more carefully, it retrieves the active window through QApplication::activeWindow(), which returns a raw QWidget pointer from QApplicationPrivate::active_window, then compares it against the top-level widget.
The qobject_cast at line 6997 is casting `ww` from focusWindow, and we only reach that point if several earlier conditions pass: the top-level widget isn't the active window, it's not a popup, the style hint check doesn't trigger, and focusWindow actually returns something. So if f …
Fable
This is getting broad. Let me focus on what's CAM/BIM-specific. Let me check the module directories and QGroupBox usage.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod; ls -d */Gui 2>/dev/null; echo \"---CAM/Path---\"; ls -d CAM Path BIM Arch 2>/dev/null", "description": "List module Gui dirs and CAM/BIM"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -rln \"QGroupBox\" DockWindowManager.cpp ToolBarManager.cpp Workbench.cpp SelectionView.cpp 2>/dev/null; echo \"===\"; grep -rn \"focusWindow\\|active_window\\|activ…
Fable
thinking
CAM has a GUI directory written in C++, while BIM is Python-only without a GUI subdirectory. I'm examining how both workbenches initialize and noticing they produce identical crash stacks during activation—when a workbench switches, the previous one's toolbars and dock windows hide while new ones appear. The interesting part is that CAM and BIM generate the same stack traces despite CAM being C++ and BIM being Python, so I'm trying to figure out what they have in common.
Both workbenches likely register something that triggers during activation—maybe a task panel, dock widget, or status bar element with a QGroupBox. I'm wondering if it's not workbench-specific code at all, but rather shared …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -rln \"QGroupBox\\|: public QGroupBox\\|QGroupBox(\" TaskView/ 2>/dev/null; echo \"=== class defs deriving QGroupBox ===\"; grep -rn \"public QGroupBox\" . 2>/dev/nu…
Fable
thinking
I should step back and focus on the actual crash mechanism rather than getting lost in which specific QGroupBox is involved. The real question is whether this is a use-after-free situation where the parent was deleted and left a dangling child, or if it's a dangling window() pointer issue.
In the wasm build without real platform windows, `QGuiApplication::focusWindow()` returns from `QGuiApplicationPrivate::focus_window`, and there's an offscreen FBO setup for the 3D view. When switching workbenches, the MDI subwindow gets hidden, and that's likely when things go wrong. The simplest explanation might be that `obj->metaObject()` is being called on an object whose vtable pointer is already at …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Workbench.cpp", "offset": 1, "limit": 60}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -n \"::activate\\|setupToolBars\\|setupDockWindows\\|activated\\|deactivated\\|hideChildren\\|setVisible\\|removeToolBar\\|delete\" Workbench.cpp | head -40", "descr…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Workbench.cpp", "offset": 453, "limit": 60}
Fable
thinking
I need to trace through ToolBarManager::setup to understand what's happening with the toolbars — it's likely deleting or hiding toolbars that contain the QGroupBox, which could be triggering the crash. Let me examine the hide/delete/reparent logic in that method.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -n \"::setup\\|hide\\|setVisible\\|deleteLater\\|delete \\|setParent\\|removeToolBar\\|new QToolBar\\|addWidget\" ToolBarManager.cpp | head -50", "description": "Fin…
Fable
thinking
I'm tracing through the crash sequence: when switching workbenches, the ToolBarManager hides unnecessary toolbars, which triggers hideChildren events that propagate to any QGroupBox widgets nested in toolbar actions, and that's where the crash occurs. Now I need to identify which QGroupBox is involved.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/ToolBarManager.cpp", "offset": 784, "limit": 30}
Fable
thinking
The `hide()` call on toolbars triggers `hideChildren`, which sends a `QHideEvent` to child widgets like the `QGroupBox`. I need to figure out which CAM/BIM toolbars actually contain a `QGroupBox`, though the crash could also stem from a dock widget being hidden instead. Let me trace back through `hide_helper` to see where this is really coming from.
Both CAM and BIM produce identical stacks, so I'm looking for a shared widget with a `QGroupBox` that both hide. The core issue is that `QMetaObject::cast` is dereferencing a dangling pointer — either a use-after-free or a dangling window reference. I need to determine which: is `ww` (the focusWindow) dangling, or is `qww->widget()` the problem? …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui; grep -n \"::setup\\|hide\\|setVisible\\|deleteLater\\|delete \\|setParent\\|removeDockWindow\\|new QDockWidget\\|addDockWindow\" DockWindowManager.cpp | head -40", "descr…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 516, "limit": 160}
Fable
thinking
I'm noticing a clever lifetime detection mechanism in the code — it's checking whether the metaObject pointer has been downgraded to the base QDockWidget class, which happens during destruction as derived class destructors unwind. This is essentially a way to detect when a dock widget is being deleted. The pattern suggests there's some delicate lifetime management happening around dock widgets, and now I'm trying to understand what triggers the crash when switching work.
When the work bench switches, `setVisible(false)` gets called on dock widgets, which triggers hide events down to child QGroupBox elements. But I keep circling back to the core issue: the out-of-bounds access happens in `QM …
Fable
Let me check what CAM/BIM specifically register that differs, and the removeDockWindow deletion path.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 365, "limit": 55}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod; echo \"=== CAM setupDockWindows / QGroupBox ===\"; grep -rn \"setupDockWindows\\|QGroupBox\\|addDockWindow\\|StatusBar\\|GroupBox\" CAM/Gui/Workbench.cpp 2>/dev/null | he…
Fable
thinking
I'm looking at how BIM adds a status bar widget called BIMStatusWidget, which is a QToolBar, and trying to trace where the crash is happening around the QGroupBox in BimStatus.py near line 90.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM/BimStatus.py", "offset": 80, "limit": 120}
Fable
thinking
The BIM status widget is actually a toolbar in the status bar rather than a QGroupBox directly, so it's not the crash source itself. I've spent considerable time on this, and without actually building and running the code, I can't pinpoint the exact CAM/BIM QGroupBox instance causing the issue. Let me refocus on what I can confirm about the trigger mechanism and make a reasoned assessment about whether this is specific to CAM/BIM.
The crash on `obj->metaObject()` when `obj` is the result of `focusWindow()` or `qww->widget()` points to a dangling top-level QWindow or QWidget—the QGroupBox still references a window or focus object that's been freed. This is a use-after-free where the QGroupBo …
Fable
I have enough to give a well-grounded analysis. Let me verify the one discriminating detail in isActiveWindow's early-return.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp", "offset": 63, "limit": 30}
Fable
thinking
I've confirmed that initStyleOption at line 69 calls option->initFrom(this) without any conditions, so that piece checks out. Now I'm ready to put together the structured output with all the verified details.
Fable
I now have a complete picture. Let me compile the analysis.
Fable
⚙ StructuredOutput {"area": "Qt widget hide/show + FreeCAD workbench-switch widget lifetime (use-after-free on a dangling top-level/focus QObject, surfaced through QGroupBox's per-event style init)", "findings": "TRIGGER MECHANISM (confirmed from Qt source):\…