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"}
tool result
# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This file is part of the FreeCAD CAx development system.              *
# *                                                                         *
# *   This program is free software; you can redistribute it and  …[truncated]
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"}
tool result
ArchAxis.py
ArchAxisSystem.py
ArchBuildingPart.py
ArchBuilding.py
ArchCommands.py
ArchComponent.py
ArchCoveringGui.py
ArchCovering.py
ArchCurtainWall.py
ArchCutPlane.py
arch.dox
ArchEquipment.py
ArchFence.py
ArchFloor.py
ArchFrame.py
ArchGrid.py
ArchIFC.py
ArchIFCSchema.py
ArchIFCView.py
ArchMaterial.py
ArchNesting.py
ArchPanel.py
ArchPipe.py
ArchPrecast.py
ArchProfile.py
ArchProject.py
Arch.py
ArchRebar.py
ArchReference.py
ArchReport.py
ArchRoof.py
ArchSchedule.py
ArchSectionPlane.py
ArchSite.p  …[truncated]
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"}
tool result
            PathPreferencesPathDressup.DressupPreferencesPage,
            QT_TRANSLATE_NOOP("QObject", "CAM"),
        )

        Path.GuiInit.Startup()

        # build commands list
        projcmdlist = ["CAM_Job", "CAM_Sanity"]
        postcmdlist = ["CAM_Post", "CAM_PostSelected"]
        toolcmdlist = ["CAM_Inspect", "CAM_SelectLoop", "CAM_OpActiveToggle"]

        simcmdlist = ["CAM_SimulatorGL", "CAM_Simulator"]
        prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CA  …[truncated]
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"}
tool result
# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2017 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This file is part of FreeCAD.                                         *
# *                                                               …[truncated]
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…
tool result
=== Dressup Preferences ===
# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com>               *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Softw  …[truncated]
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"}
tool result
60:        self.loadPreferences()
593:    def loadPreferences(self):
===
    def loadPreferences(self):
        """Set up preferences pages"""

        def QT_TRANSLATE_NOOP(context, text):
            return text

        t1 = QT_TRANSLATE_NOOP("QObject", "BIM")
        t2 = QT_TRANSLATE_NOOP("QObject", "Draft")
        FreeCADGui.addPreferencePage(":/ui/preferences-arch.ui", t1)
        FreeCADGui.addPreferencePage(":/ui/preferences-archdefaults.ui", t1)
        FreeCADGui.addPreferencePage(":  …[truncated]
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…
tool result
Action.cpp:47:#include "PreferencePages/DlgSettingsWorkbenchesImp.h"
Command.cpp:53:#include "PreferencePages/DlgSettingsWorkbenchesImp.h"
ApplicationPy.cpp:47:#include <Gui/PreferencePages/DlgSettingsPDF.h>
ApplicationPy.cpp:393:    {"addPreferencePage",
ApplicationPy.cpp:396:     "addPreferencePage(path, group) -> None\n"
ApplicationPy.cpp:397:     "addPreferencePage(dialog, group) -> None\n"
Application.cpp:76:#include "PreferencePages/DlgSettingsCacheDirectory.h"
MDIView.cpp:40:#includ  …[truncated]
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…
tool result
Action.cpp
Clipping.cpp
CommandStd.cpp
Command.cpp
Application.cpp
ApplicationPy.cpp
FileDialog.cpp
MainWindow.cpp
PrefWidgets.cpp
StartupProcess.cpp
=== activateWorkbench ===
1358:            activateWorkbench(d->activeDocument->workbench().c_str());
1901:bool Application::activateWorkbench(const char* name)
1945:            Py::Callable activate(handler.getAttr(std::string("Initialize")));
1962:        else if (WorkbenchManager::instance()->activate(name, type)) {
1963:            getMainWindo  …[truncated]
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 130}
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 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…
tool result
=== isActiveWindow ===
6967:bool QWidget::isActiveWindow() const
bool QWidget::isActiveWindow() const
{
    QWidget *tlw = window();
    if (tlw == QApplication::activeWindow() || (isVisible() && (tlw->windowType() == Qt::Popup)))
        return true;

#if QT_CONFIG(graphicsview)
    if (const auto &tlwExtra = tlw->d_func()->extra) {
        if (isVisible() && tlwExtra->proxyWidget)
            return tlwExtra->proxyWidget->isActiveWindow();
    }
#endif

    if (style()->styleHint(QStyle::SH_Wi  …[truncated]
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)\…
tool result
const QObject *QMetaObject::cast(const QObject *obj) const
{
    return (obj && obj->metaObject()->inherits(this)) ? obj : nullptr;
}
=== who calls QMetaObject::cast ===
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
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}
tool result
393	    {"addPreferencePage",
394	     (PyCFunction)ApplicationPy::sAddPreferencePage,
395	     METH_VARARGS,
396	     "addPreferencePage(path, group) -> None\n"
397	     "addPreferencePage(dialog, group) -> None\n"
398	     "\n"
399	     "Add a UI form to the preferences dialog in the specified group.\n"
400	     "\n"
401	     "path : str\n    UI file path.\n"
402	     "group : str\n    Group name.\n"
403	     "dialog : type\n    Preference page."},
404	    {"addCommand",
405	     (PyCFunction)  …[truncated]
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"}
tool result
     (PyCFunction)ApplicationPy::sAddPreferencePage,
     METH_VARARGS,
     "addPreferencePage(path, group) -> None\n"
     "addPreferencePage(dialog, group) -> None\n"
     "\n"
     "Add a UI form to the preferences dialog in the specified group.\n"
     "\n"
     "path : str\n    UI file path.\n"
     "group : str\n    Group name.\n"
     "dialog : type\n    Preference page."},
    {"addCommand",
     (PyCFunction)ApplicationPy::sAddCommand,
     METH_VARARGS,
     "addCommand(n  …[truncated]
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…
tool result
394:     (PyCFunction)ApplicationPy::sAddPreferencePage,
1280:PyObject* ApplicationPy::sAddPreferencePage(PyObject* /*self*/, PyObject* args)
---body---
PyObject* ApplicationPy::sAddPreferencePage(PyObject* /*self*/, PyObject* args)
{
    char* fn = nullptr;
    char* grp = nullptr;
    if (PyArg_ParseTuple(args, "ss", &fn, &grp)) {
        QFileInfo fi(QString::fromUtf8(fn));
        if (!fi.exists()) {
            PyErr_SetString(PyExc_RuntimeError, "UI file does not exist");
         …[truncated]
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…
tool result
src/corelib/kernel/qobject.h:427:inline T qobject_cast(QObject *object)
src/corelib/kernel/qobject.h:433:inline T qobject_cast(const QObject *object)
src/corelib/kernel/qobject.h:460:    template <> inline IFace *qobject_cast<IFace *>(QObject *object) \
src/corelib/kernel/qobject.h:462:    template <> inline const IFace *qobject_cast<const IFace *>(const QObject *object) \
=== inherits ===
bool QMetaObject::inherits(const QMetaObject *metaObject) const noexcept
{
    const QMetaObject *m = this;  …[truncated]
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null; sed -n '420,445p' src/corelib/kernel/qobject.h", "description": "qobject_cast impl"}
tool result
#endif // inline since 6.6
bool QObject::setProperty(const char *name, QVariant &&value)
{
    return doSetProperty(name, value, &value);
}

template <class T>
inline T qobject_cast(QObject *object)
{
    return QtPrivate::qobject_cast_helper<T>(object);
}

template <class T>
inline T qobject_cast(const QObject *object)
{
    static_assert(std::is_const_v<std::remove_pointer_t<T>>,
                  "qobject_cast cannot cast away constness (use const_cast)");
    return QtPrivate::qobject_cast_h  …[truncated]
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"}
tool result
src/corelib/kernel/qobjectdefs.h:745:    inline TgtType qobject_cast_helper(SrcType *object)
src/corelib/kernel/qobject.h:429:    return QtPrivate::qobject_cast_helper<T>(object);
src/corelib/kernel/qobject.h:437:    return QtPrivate::qobject_cast_helper<T>(object);
    return QtPrivate::qobject_cast_helper<T>(object);
}

template <class T>
inline T qobject_cast(const QObject *object)
{
    static_assert(std::is_const_v<std::remove_pointer_t<T>>,
                  "qobject_cast cannot cast away   …[truncated]
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"}
tool result
namespace QtPrivate {
    // Trait that tells if a QObject has a Q_OBJECT macro
    template <typename Object> struct HasQ_OBJECT_Macro {
        template <typename T>
        static char test(int (T::*)(QMetaObject::Call, int, void **));
        static int test(int (Object::*)(QMetaObject::Call, int, void **));
        enum { Value =  sizeof(test(&Object::qt_metacall)) == sizeof(int) };
    };

    template <class TgtType, class SrcType>
    inline TgtType qobject_cast_helper(SrcType *object)
   …[truncated]
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"}
tool result
    \fn void QStyleOption::initFrom(const QWidget *widget)
    \since 4.1

    Initializes the \l state, \l direction, \l rect, \l palette, \l fontMetrics
    and \l styleObject member variables based on the specified \a widget.

    This is a convenience function; the member variables can also be
    initialized manually.

    \sa QWidget::layoutDirection(), QWidget::rect(),
        QWidget::palette(), QWidget::fontMetrics()
*/
void QStyleOption::initFrom(const QWidget *widget)
{
    QWidget *w  …[truncated]
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 ===…
tool result
=== QGroupBox in CAM ===
=== GuiInit.Startup ===
def Startup():
    global Processed
    if not Processed:
        Path.Log.debug("Initializing PathGui")
        from Path.Base.Gui import PropertyBag
        from Path.Base.Gui import SetupSheet
        from Path.Dressup.Gui import Array
        from Path.Dressup.Gui import AxisMap
        from Path.Dressup.Gui import DogboneII
        from Path.Dressup.Gui import Dragknife
        from Path.Dressup.Gui import LeadInOut
        from Path.Dressup.  …[truncated]
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…
tool result
=== Activated/ContextMenu in InitGui ===
625:    def Activated(self):
629:        import BimStatus
656:        todo.ToDo.delay(BimStatus.setStatusIcons, True)
719:    def Deactivated(self):
722:        import BimStatus
744:        todo.ToDo.delay(BimStatus.setStatusIcons, False)
774:    def ContextMenu(self, recipient):
=== lines 200-320 ===
            "BIM_IfcExplorer",
            "Arch_IfcSpreadsheet",
            "BIM_ImagePlane",
            "BIM_Unclone",
            "BIM_Rewire",
         …[truncated]
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 ===\"…
tool result
=== MainWindow::activateWorkbench ===
void MainWindow::activateWorkbench(const QString& name)
{
    // remember workbench by tab (if enabled)

    const ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
        "User parameter:BaseApp/Preferences/View"
    );
    const bool saveWB = hGrp->GetBool("SaveWBbyTab", false);
    MDIView* view = activeWindow();
    if (view && saveWB) {
        view->setProperty("ownWB", name);
    }

    // emit this signal
    Q_EMIT workbenc  …[truncated]
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"}
tool result
937:    static QPointer<OverlayDragFrame> tbPlaceholder;
944:                    lastArea->removeWidget(tbPlaceholder);
947:                tbPlaceholder->hide();
980:            if (tbPlaceholder) {
982:                    lastArea->removeWidget(tbPlaceholder);
985:                tbPlaceholder->hide();
1007:        if (!tbPlaceholder) {
1008:            tbPlaceholder = new OverlayDragFrame(getMainWindow());
1009:            tbPlaceholder->hide();
1014:            tbPlaceholder->setSiz  …[truncated]
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}
tool result
255	    }
256	    QDockWidget* dw = qobject_cast<QDockWidget*>(widget->parentWidget());
257	    if (dw) {
258	        return dw;
259	    }
260	
261	    // creates the dock widget as container to embed this widget
262	    MainWindow* mw = getMainWindow();
263	    dw = new QDockWidget(mw);
264	
265	    if (d->overlayManager) {
266	        d->overlayManager->setupTitleBar(dw);
267	    }
268	
269	    // Note: By default all dock widgets are hidden but the user can show them manually in the view
270	  …[truncated]
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 \"===…
tool result
=== addDockWidget / DockWindow in BIM+CAM ===
BIM/bimcommands/BimTutorial.py:82:            m.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
BIM/bimcommands/BimViews.py:166:            mw.addDockWidget(self.getDockArea(area), vm)
=== BimStatus setStatusIcons ===
# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2017 Y  …[truncated]
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…
tool result
=== QGroupBox constructed in Python imported at CAM/BIM init ===
CAM/Path/Tool/assets/ui/preferences.py:56:        self.assets_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Asset Location"))
CAM/Path/Tool/assets/ui/preferences.py:88:        self.machines_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Machines"))
BIM/bimcommands/BimMaterial.py:128:            createButtonsLayoutBox = QtGui.QGroupBox(
BIM/bimcommands/BimPreflight.py:122:                        box = QtGu  …[truncated]
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…
tool result
=== Workbench::activate ===
bool Workbench::activate()
{
    ToolBarItem* tb = setupToolBars();
    setupCustomToolbars(tb, "Toolbar");
    WorkbenchManipulator::changeToolBars(tb);
    ToolBarManager::getInstance()->setup(tb);
    delete tb;

    // ToolBarItem* cb = setupCommandBars();
    // setupCustomToolbars(cb, "Toolboxbar");
    // ToolBoxManager::getInstance()->setup( cb );
    // delete cb;

    DockWindowItems* dw = setupDockWindows();
    WorkbenchManipulator::changeDoc  …[truncated]
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}
tool result
440	
441	    if (!dw->toggleViewAction()->isChecked()) {
442	        dw->toggleViewAction()->activate(QAction::Trigger);
443	    }
444	
445	    dw->raise();
446	}
447	
448	/**
449	 * Sets the window title for the dockable windows.
450	 */
451	void DockWindowManager::retranslate()
452	{
453	    for (QList<QDockWidget*>::Iterator it = d->_dockedWindows.begin(); it != d->_dockedWindows.end();
454	         ++it) {
455	        QString title = (*it)->windowTitle();
456	        if (title.isEmpty()) {
4  …[truncated]
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…
tool result
=== DockWindowManager::setup ===
void DockWindowManager::setup(DockWindowItems* items)
{
    // save state of current dock windows
    saveState();
    d->_dockWindowItems = *items;

    QList<QDockWidget*> docked = d->_dockedWindows;
    const QList<DockWindowItem>& dws = items->dockWidgets();
    for (const auto& it : dws) {
        QDockWidget* dw = findDockWidget(docked, it.name);
        QByteArray dockName = it.name.toLatin1();
        bool visible = d->_hPref->GetBool(dockName.  …[truncated]
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…
tool result
=== BIM Activated 625-720 ===
    def Activated(self):

        import WorkingPlane
        from draftutils import todo
        import BimStatus
        from nativeifc import ifc_observer
        from draftutils import grid_observer

        PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM")

        if hasattr(FreeCADGui, "draftToolBar"):
            FreeCADGui.draftToolBar.Activated()
        if hasattr(FreeCADGui, "Snapper"):
            FreeCADGui.Snapper.show()
          …[truncated]
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…
tool result
TaskView/TaskView.h:82:class GuiExport TaskBox: public QSint::ActionGroup, public TaskContent
PrefWidgets.h:492:class GuiExport PrefCheckableGroupBox: public QGroupBox, public PrefWidget
TaskDlgRelocation.h:37:class TaskBoxPosition: public Gui::TaskView::TaskBox
TaskDlgRelocation.h:54:class TaskBoxAngle: public Gui::TaskView::TaskBox
=== all QGroupBox subclasses in Gui ===
PrefWidgets.h
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
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…
tool result
Gui/QSint/actionpanel/actionpanelscheme.h:31:class QSINT_EXPORT ActionPanelScheme
Gui/QSint/actionpanel/actionbox.h:26:class QSINT_EXPORT ActionBox : public QFrame
Gui/QSint/actionpanel/actiongroup.h:19:class ActionLabel;
Gui/QSint/actionpanel/actiongroup.h:20:class ActionPanelScheme;
Gui/QSint/actionpanel/actiongroup.h:29:class QSINT_EXPORT ActionGroup : public QWidget
Gui/QSint/actionpanel/actionpanel.h:18:class ActionPanelScheme;
Gui/QSint/actionpanel/actionpanel.h:19:class ActionGroup;
Gui/Q  …[truncated]
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"}
tool result
        )
        Log("Loading CAM workbench... done\n")

    def GetClassName(self):
        return "Gui::PythonWorkbench"

    def Activated(self):
        # update the translation engine
        FreeCADGui.updateLocale()
        # Msg("CAM workbench activated\n")

    def Deactivated(self):
        # Msg("CAM workbench deactivated\n")
        pass

    def ContextMenu(self, recipient):
        selection = FreeCADGui.Selection.getSelection()
        if not selection:
            return

        …[truncated]
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…
tool result
63:void QGroupBox::initStyleOption(QStyleOptionGroupBox *option) const
283:    initStyleOption(&option);
288:bool QGroupBox::event(QEvent *e)
306:    initStyleOption(&box);
429:    q->initStyleOption(&box);
456:    initStyleOption(&option);
677:    initStyleOption(&box);
693:    initStyleOption(&box);
719:    initStyleOption(&box);
=== isActiveWindow qobject_cast line ===
6997:            QWidgetWindow *qww = qobject_cast<QWidgetWindow *>(ww);
8162:    auto window = qobject_cast<QWidgetWindow *>  …[truncated]
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…
tool result
8535:void QWidgetPrivate::hideChildren(bool spontaneous)
12:    events. event() checks for Tab and Shift+Tab and tries to move the
=== show hideChildren body ===
void QWidgetPrivate::hideChildren(bool spontaneous)
{
    Q_Q(QWidget);
    qCDebug(lcWidgetShowHide) << "Hiding children of" << q
                              << "spontaneously" << spontaneous;

    QList<QObject*> childList = children;
    for (int i = 0; i < childList.size(); ++i) {
        QWidget *widget = qobject_cast<QWidget*>(c  …[truncated]
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…
tool result
143:    QWidget *window = widget->window();
153:    if (window->isActiveWindow())
=== groupbox initStyleOption 63-90 ===
void QGroupBox::initStyleOption(QStyleOptionGroupBox *option) const
{
    if (!option)
        return;

    Q_D(const QGroupBox);
    option->initFrom(this);
    option->text = d->title;
    option->lineWidth = 1;
    option->midLineWidth = 0;
=== confirm sendSpontaneousEvent QHideEvent line ===
            widget->setAttribute(Qt::WA_Mapped, false);
        else
            w  …[truncated]
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…
tool result
Structured output provided successfully