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.
Identify the actual QGroupBox and its ownership. CAM Initialize (src/Mod/CAM/InitGui.py + Path/**/Gui, PreferencesJob, Path.Dressup.Gui.Preferences) and BIM Initialize create preference-page / task-panel / dialog widgets that contain QGroupBox (many FreeCAD .ui files + PySide-created widgets). Determine which QGroupBox is alive during activation, who owns it (C++ parent vs Python/shiboken ownership), and whether shiboken/PySide widget ownership (a Python-created QWidget whose C++ parent is set, or whose Python ref is dropped -> C++ deleted while still parented) leaves a dangling widget in a tree that hideChildren walks. Contrast with Part/Draft which do NOT create such widgets at activation. Cite file:line.
Fable
thinking
I'm looking at a WebAssembly crash in FreeCAD that happens when activating the CAM or BIM workbenches. The issue stems from QMetaObject::cast reading out-of-bounds memory, which gets triggered when QWidgetPrivate::hideChildren traverses a widget tree containing a QGroupBox that has an invalid vtable pointer. I need to track down which QGroupBox is being created during the workbench activation that's causing this corruption.
Fable
⚙ Bash {"command": "cat /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/CAM/InitGui.py 2>/dev/null | head -120", "description": "Read CAM InitGui.py"}
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM/ 2>/dev/null; echo \"---\"; ls /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/ | head -60", "description": "List BIM and Mod dirs"}
Fable
thinking
The crash occurs when QWidgetPrivate::hideChildren walks the widget tree during workbench activation, sending events to child widgets like QGroupBox. When QGroupBox processes the event, it initializes style options which calls isActiveWindow(), triggering a QMetaObject cast that performs an out-of-bounds read on the widget.
This suggests the QGroupBox or something in its window/parent chain is a dangling object—either deleted but still referenced in the children list, or a Python-created widget whose ownership wasn't properly managed by shiboken. The most likely culprit is a preference page or task panel widget created during initialization where the Python object gets garbage collected whi …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/CAM 2>/dev/null; sed -n '120,320p' InitGui.py", "description": "Rest of CAM InitGui"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM 2>/dev/null; sed -n '1,200p' InitGui.py", "description": "BIM InitGui"}
Fable
thinking
I'm tracing through how both CAM and BIM handle preference pages — they both call addPreferencePage and loadPreferences, though other workbenches like Draft, Sketcher, and Part do the same thing. The crash is happening at the hide operation.
Now I need to understand when Python preference pages actually get instantiated — whether addPreferencePage in the C++ GUI code loads the .ui file and creates a live widget tree with QGroupBox at that moment, or if it happens later. Let me check how the C++ addPreferencePage stores Python pages and look at DressupPreferencesPage specifically.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/CAM 2>/dev/null; echo \"=== Dressup Preferences ===\"; cat Path/Dressup/Gui/Preferences.py 2>/dev/null; echo \"=== PreferencesJob ===\"; sed -n '1,80p' Path/Main/Gui/Prefe…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM 2>/dev/null; grep -n \"loadPreferences\" InitGui.py; echo \"===\"; sed -n '/def loadPreferences/,/def [a-z]/p' InitGui.py", "description": "BIM loadPreferences"}
Fable
thinking
BIM preference pages use .ui file path strings registered in C++, while CAM uses Python class objects that are only instantiated when the Preferences dialog opens. So preference pages aren't creating live widgets at activation, which means the QGroupBox isn't coming from them directly. But the crash trace mentions "PreferenceP"—
let me check if addPreferencePage in C++ instantiates the page immediately to read its window title or group name during registration, or if I need to look at how DlgPreferencesImp handles this.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; grep -rn \"addPreferencePage\\|registerPreferencePage\\|new PreferencePage\\|PreferencePages\" *.cpp *.h | head -40", "description": "Find addPreferencePage i…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; grep -rln \"hideChildren\\|QGroupBox\\|activateWorkbench\\|onToolBarSwitch\" *.cpp | head; echo \"=== activateWorkbench ===\"; grep -n \"activateWorkbench\\|h…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 130}
Fable
thinking
I'm tracing through the activation flow where `getMainWindow()->activateWorkbench()` triggers `WorkbenchManager::instance()->activate`, which sets up the toolbars through `setupWorkbench`. This then causes `hideChildren` to fire, which gets invoked by Qt's internal mechanism when a widget's visibility changes or gets reparented.
The crash seems to stem from a dangling QGroupBox somewhere in the widget tree — when `QGroupBox::initStyleOption` calls `option->initFrom(this)`, which then calls `isActiveWindow()`, the QMetaObject cast is hitting an out-of-bounds memory access, suggesting the object or its metaobject pointer has been freed. I need to figure out which parent widget containing a QG …
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; echo \"=== isActiveWindow ===\"; grep -n \"bool QWidget::isActiveWindow\" src/widgets/kernel/qwidget.cpp; sed -n '/bool QWidget::isActiveWindow/,/^}/p' src/widgets/kernel/qwidget.cpp…
Fable
thinking
Looking at the crash in `isActiveWindow`, the issue traces back to `tlw = window()` which walks up the parent chain—if any parent is a dangling pointer, that would crash immediately. But the actual crash is happening inside `QMetaObject::cast` when `qobject_cast<QWidgetWindow *>(ww)` is called, where `ww` comes from `QGuiApplication::focusWindow()`. I need to trace through how qobject_cast actually works and where QMetaObject::cast gets invoked in the call chain.
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; sed -n '/const QObject \\*QMetaObject::cast(const QObject/,/^}/p' src/corelib/kernel/qmetaobject.cpp; echo \"=== who calls QMetaObject::cast ===\"; grep -rn \"\\.cast(\\| cast(this)\…
Fable
thinking
The issue is that when `obj->metaObject()` is called on a freed or dangling QObject, the vtable is corrupted, causing metaObject() to return a garbage pointer. Then when `inherits()` tries to walk the superclass chain through that corrupted metadata, it reads out-of-bounds memory.
The root cause appears to be a Python-created QGroupBox whose reference was dropped while it still had a C++ parent, leaving a dangling pointer in the parent's children list. When hideChildren iterates through that list and tries to send events, it hits this freed object. The QGroupBox itself is still valid (otherwise the vtable call would crash immediately), but something it references—likely an ancestor in the p …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/ApplicationPy.cpp", "offset": 393, "limit": 70}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; sed -n '/sAddPreferencePage/,/^}/p' ApplicationPy.cpp | head -80", "description": "Read sAddPreferencePage body"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; grep -n \"ApplicationPy::sAddPreferencePage\" ApplicationPy.cpp; echo \"---body---\"; awk '/PyObject\\* ApplicationPy::sAddPreferencePage/,/^}/' ApplicationPy…
Fable
thinking
So `addPreferencePage` just registers a producer without instantiating the page itself—preference pages are definitely lazy-loaded. That means the live QGroupBox I'm seeing during activation must be coming from somewhere else entirely. I need to trace back what `hideChildren` is actually walking through and figure out which QGroupBox is active during the workbench switch, since that's when the ToolBarManager and DockWindowManager are toggling visibility.
The crash stack shows `hideChildren` recursively hiding children and sending `QEvent::HideToParent` to each one, so something with a QGroupBox descendant is being hidden—most likely a QDockWidget's contents or a task panel. When switching w …
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; grep -rn \"QMetaObject::cast\\|staticMetaObject.cast\\|->cast(\\|inline .*qobject_cast\" src/corelib/kernel/qobject.h | head; echo \"=== inherits ===\"; sed -n '/bool QMetaObject::in…
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; sed -n '420,445p' src/corelib/kernel/qobject.h", "description": "qobject_cast impl"}
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; grep -rn \"qobject_cast_helper\" src/corelib/kernel/*.h; sed -n '/qobject_cast_helper/,/^ }/p' src/corelib/kernel/qobject.h | head -30", "description": "qobject_cast_helper"}
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; sed -n '735,770p' src/corelib/kernel/qobjectdefs.h", "description": "qobject_cast_helper body"}
Fable
thinking
The `QMetaObject::cast` function performs a virtual call to `object->metaObject()`, then checks if it inherits from the target type by walking the superdata chain. If `object` is a dangling pointer to freed memory, this dereference reads garbage values and causes an out-of-bounds read when traversing the inheritance chain. The vulnerability occurs when a dangling QObject is passed as the argument to this cast operation.
Looking at the call stack, `initFrom` calls `isActiveWindow`, which contains a `qobject_cast<QWidgetWindow*>(ww)` where `ww` comes from `QGuiApplication::focusWindow()` or its parent. The dangling object is this window pointer, not a QGroupBox—which suggests this is a global …
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; sed -n '/void QStyleOption::initFrom(const QWidget \\*widget)/,/^}/p' src/widgets/styles/qstyleoption.cpp", "description": "initFrom body"}
Fable
thinking
I'm tracing through how Qt determines if a widget is the active window—starting with getting the top-level window from a QGroupBox by walking up the parent chain, then checking if that window is active using `isActiveWindow()`, which internally retrieves the focus window and performs a cast to validate it.
The critical issue emerges when that top-level window or focus window is a dangling pointer to freed memory. The parent chain walk stops at the first widget marked as a window, which could be corrupted data from a freed object. Then calling methods on this dangling pointer—like `windowType()` or the metacast operation—reads garbage memory.
This points to a classic PySide ownership proble …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod 2>/dev/null; echo \"=== QGroupBox in CAM ===\"; grep -rln \"QGroupBox\\|groupBox\" CAM/Path/GuiInit.py CAM/Path/Main/Gui 2>/dev/null | head; echo \"=== GuiInit.Startup ===…
Fable
thinking
I'm reconsidering the root cause here—maybe the QGroupBox isn't coming from the CAM or BIM widget creation at all. The real issue could be that both workbenches initialize a DockWidget or task panel through the GUI, and the crash occurs when the ToolBarManager tries to hide toolbars during that setup process.
But toolbars themselves don't contain QGroupBoxes, so that doesn't quite fit either. Let me think about what actually happens during workbench activation—the DockWindowManager hides and shows dock widgets based on the active workbench, and FreeCAD maintains per-workbench visibility states for these panels. The stack trace shows `QWidgetPrivate::hideChildren` at the deepest level, which …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM 2>/dev/null; echo \"=== Activated/ContextMenu in InitGui ===\"; grep -n \"def Activated\\|def Deactivated\\|def ContextMenu\\|Draft.setStatusWidgets\\|StatusWidget\\|s…
Fable
thinking
Both CAM and BIM crash identically during activation despite having different architectures, which points to something in the shared C++ activation machinery rather than workbench-specific code. I need to figure out what operation both workbenches perform at activation that Part, Draft, and Sketcher don't.
Both CAM and BIM call `FreeCADGui.addPreferencePage()` during initialization, but Draft does too without crashing, so that's not the culprit. The identical stack trace pointing to `hideChildren` on a QGroupBox suggests the crash happens in a shared dialog or panel—possibly triggered by the sheer number of toolbars both CAM and BIM create compared to the other workbenches, which could forc …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; echo \"=== MainWindow::activateWorkbench ===\"; awk '/void MainWindow::activateWorkbench/,/^}/' MainWindow.cpp; echo \"=== hide( calls in ToolBarManager ===\"…
Fable
thinking
I'm reconsidering how the toolbar and widget hiding works in DockWindowManager — the hide() calls at lines 272 and 491 are affecting the dock windows themselves, not just the toolbars. Let me trace through the workbench activation flow more carefully, looking at how setupToolBars, setupDockWindows, and setupMenuBar interact, and what happens when Workbench::activate() is called to understand the full initialization sequence.
Actually, I think the real issue might be that when a floating QDockWidget gets hidden during the layout changes triggered by all those toolbars and commands, it could contain a Python-owned QGroupBox whose C++ parent is set but whose Python reference was dropped — leav …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; grep -n \"tbPlaceholder\\|QGroupBox\\|Placeholder\" ToolBarManager.cpp | head -30", "description": "tbPlaceholder type"}
Fable
thinking
I'm reconsidering the architecture here—maybe this QGroupBox isn't part of the preferences dialog at all, but rather belongs to a task panel or overlay system that FreeCAD uses for workbench-specific UI elements. The pattern I'm seeing with the identical stack traces in both CAM and BIM suggests this might be tied to how Draft integrates its UI into the shared hideChildren path.
Let me trace through what actually happens during workbench deactivation—when you switch to CAM, the previous workbench (likely Part or Start) deactivates, and that's where the hideChildren call originates. I should look more closely at DockWindowManager to see what gets hidden when switching workbenches, particular …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 255, "limit": 40}
Fable
thinking
When a dock window is registered with a Python widget, the system creates a QDockWidget, immediately hides it (which triggers hideChildren on its children before the widget is even attached), then sets the widget as the dock's child and assigns it to the dock. The issue is that this hide call at line 272 happens before the widget is properly added, which could cause problems if CAM/BIM modules register dock windows containing QGroupBox elements.
I'm trying to narrow down which Python code actually calls addDockWidget or registerDockWindow during CAM/BIM activation, and whether the real culprit is how PySideUic.loadUi handles widget ownership when loading .ui files with QGroupBoxes — particu …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod 2>/dev/null; echo \"=== addDockWidget / DockWindow in BIM+CAM ===\"; grep -rn \"addDockWidget\\|registerDockWindow\\|addDockWindow\" BIM CAM 2>/dev/null | head; echo \"===…
Fable
thinking
The crash pattern is identical between CAM and BIM despite BIM having extra on-demand widgets, which rules out those being the culprit. Since both workbenches share the same stack trace terminating at hideChildren and both have a QGroupBox in a shared widget, the issue points to something triggered during workbench activation itself rather than anything specific to BIM. This shared mechanism is what differentiates them from Part, Draft, and Sketcher workbenches. Another angle: maybe the dangling object is `QGuiApplication::focusWindow()` itself. In the wasm offscreen single-window port, focus handling is custom, so if that QWindow was destroyed but the internal pointer wasn't cleared, any `i …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod 2>/dev/null; echo \"=== QGroupBox constructed in Python imported at CAM/BIM init ===\"; grep -rn \"QGroupBox(\" CAM/Path/Tool/assets CAM/Path/Main/Gui BIM/bimcommands 2>/d…
Fable
thinking
The QGroupBox instances are created lazily within the __init__ methods of preference pages like AssetPreferencesPage, so they don't appear at activation time. I've confirmed that CAM and BIM don't instantiate live QGroupBoxes at activation through the standard paths, and given the matching stack traces and shared C++ hideChildren implementation, along with the Qt-wasm JSPI environment with custom focus handling, the most likely explanation is that the crash isn't from the QGroupBox itself—which is a valid, live widget with proper vtable dispatch—but rather from dereferencing a dangling object inside window()->isActiveWindow(), specifically when casting a stale QGuiApplication::focusWindow() …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; echo \"=== Workbench::activate ===\"; awk '/bool Workbench::activate/,/^}/' Workbench.cpp; echo \"=== setup calls ===\"; grep -n \"setupToolBars\\|setupDockWi…
Fable
thinking
I'm tracing through the toolbar and dock window setup process. ToolBarManager::setup hides old toolbars by calling hide() on them, which cascades to their children like QToolButtons and QToolBarExtension, but there's no QGroupBox involved. Similarly, DockWindowManager::setup hides widgets directly at the widget level.
The real question is whether a dock widget's content could be a Python-provided widget containing a QGroupBox. When switching workbenches, dock windows not in the new workbench's setupDockWindows list get hidden. Standard dock widgets like Selection view, Report view, Python console, Model tree, Tasks, DAG view, and Measurement would all be affected by this hide operation. But …
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 440, "limit": 70}
Fable
thinking
I'm tracing the crash in registerDockWindow—when it calls `widget->hide()` on a Python-created QWidget containing QGroupBox children, it triggers a cascade through the Qt event system that eventually calls `window()->isActiveWindow()` during style initialization, which crashes. This happens during the setupDockWindows() initialization phase.
The key issue is that at registration time, the widget has no parent yet—it's still a top-level window—so when `widget->hide()` propagates to child QGroupBoxes, they try to determine the active window by calling `widget->isActiveWindow()`, but the widget itself isn't the active window (the main window is), so it falls through to checking the focus windo …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; echo \"=== DockWindowManager::setup ===\"; awk '/void DockWindowManager::setup\\(DockWindowItems/,/^}/' DockWindowManager.cpp | head -80", "description": "Doc…
Fable
thinking
During CAM/BIM activation, the setup process adds dock windows and manages their visibility—when a dock is set invisible, it hides the dock itself along with any QGroupBox children in its content widget through a cascading hideChildren call.
The key difference is that CAM and BIM register Python-created dock widgets containing QGroupBox elements, whereas Part/Draft/Sketcher either don't register such widgets or their registered widgets are C++-based without QGroupBox. Since CAM and BIM don't override setupDockWindows in their InitGui, these docks must be registered elsewhere—likely through Gui.addDockWidget calls triggered during activation or imports. I found BimViews.py registering a "BIM …
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod 2>/dev/null; echo \"=== BIM Activated 625-720 ===\"; sed -n '625,720p' BIM/InitGui.py; echo \"=== CAM Activated? ===\"; grep -n \"def Activated\\|def Deactivated\\|def Get…
Fable
thinking
The crash stack gets cut off at the JSPI promise boundary after hideChildren, but the key insight is that `FreeCADGui.Control.clearTaskWatcher()` and `addTaskWatcher` manage the TaskView panel — specifically, they create and hide task panel widgets that are QGroupBox instances, and FreeCAD's TaskBox widgets inherit from QGroupBox.
Now I'm verifying whether TaskBox actually derives from QGroupBox and how that inheritance manifests in the crash. The identical stacks appearing during both CAM and BIM activation point to this shared TaskView/TaskBox mechanism being triggered when switching workbenches, which rebuilds or hides the task panel.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui 2>/dev/null; grep -rn \"class TaskBox\\|class GuiExport TaskBox\\|: public QGroupBox\\|public QGroupBox\" TaskView/*.h Widgets.h *.h 2>/dev/null | head; echo \"=== all QGr…
Fable
thinking
TaskBox is actually a QSint::ActionGroup, not a QGroupBox, so I need to look at that base class. PrefCheckableGroupBox is a preference-page widget that inherits from QGroupBox and gets used in UI files, though preferences load lazily. Let me examine what QSint::ActionGroup provides.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src 2>/dev/null; grep -rn \"class .*ActionGroup\\|class .*ActionBox\\|class .*ActionLabel\\|class .*ActionPanel\" Gui/QSint/ 3rdParty/ 2>/dev/null | head; echo \"=== find ActionGr…
Fable
thinking
TaskBox inherits from QSint::ActionGroup rather than QGroupBox, so that's not the source. Looking at the codebase, the only QGroupBox subclass in the GUI is PrefCheckableGroupBox used in preference pages, which load lazily. That means any active QGroupBox at runtime is either a plain one from a .ui file or a Python widget in a dock or task dialog.
The task watchers registered via `FreeCADGui.Control.addTaskWatcher()` trigger when selection changes, not necessarily at activation. The Draft toolbar itself is a persistent widget with a QGroupBox-like structure, yet Draft workbench doesn't crash. BimViews creates dock widgets with `show()` calls, but CAM has an identical setup without issues.
…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/CAM 2>/dev/null; sed -n '355,400p' InitGui.py", "description": "CAM Activated/Deactivated"}
Fable
thinking
Both CAM and BIM are crashing during their initialization phase rather than in their Activated methods—CAM just calls updateLocale() which is trivial, while BIM's crash likely stems from createTools or loadPreferences. The identical stack traces point to a shared failure in the C++ activation machinery, specifically in Workbench::activate() when it invokes ToolBarManager::setup or DockWidget initialization.
The real clue is timing: CAM and BIM only run their Initialize methods on first activation, and during that process they import large Python GUI trees that create and destroy QWidgets as side effects. One of these QGroupBoxes is likely leaking into a persistent structure like the TaskVie …
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; grep -n \"void QGroupBox::initStyleOption\\|bool QGroupBox::event\\|initStyleOption(&\" src/widgets/widgets/qgroupbox.cpp | head; echo \"=== isActiveWindow qobject_cast line ===\"; g…
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; grep -n \"void QWidgetPrivate::hideChildren\" src/widgets/kernel/qwidget.cpp; awk 'NR>=8940 && NR<=9010' src/widgets/kernel/qwidget.cpp | grep -n \"hideChildren\\|HideToParent\\|send…
Fable
thinking
I found the root cause—`QGroupBox::event` unconditionally calls `initStyleOption` for every event that arrives, including `QHideEvent` during the hideChildren phase, which then triggers `initFrom` and ultimately calls `window()->isActiveWindow()`, causing the crash.
The crash chain is: hideChildren iterates visible children and sends QHideEvent to each one, the QGroupBox receives it and processes the event, which invokes initStyleOption, which calls initFrom, which tries to cast the window object via QMetaObject—but that window object is dangling, leading to an out-of-bounds read when walking the superdata chain.
The most likely shiboken scenario is that a Python-created QGroupBox was adde …
Fable
I have gathered enough to characterize the crash precisely on the Qt side and form the best-supported FreeCAD-side hypothesis. Let me verify the two remaining Qt citations (initFrom exact call and QMetaObject::cast) are as I stated, then answer.
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; grep -n \"QWidget \\*window = widget->window\\|window->isActiveWindow\" src/widgets/styles/qstyleoption.cpp; echo \"=== groupbox initStyleOption 63-90 ===\"; sed -n '63,72p' src/widg…
Fable
thinking
I've confirmed the details from the source code, and now I'm ready to write out the final answer.
Fable
I have fully traced the Qt-side chain and confirmed every frame. Here is my analysis.
Fable
⚙ StructuredOutput {"area": "FreeCAD-wasm CAM/BIM workbench activation crash — dangling QObject dereferenced by QGroupBox styling during QWidgetPrivate::hideChildren", "findings": "ROOT MECHANISM (verified end-to-end in Qt source): The QGroupBox is NOT itself…