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.
Trace the EXACT OOB. In qtbase/src/corelib/kernel/qmetaobject.cpp read QMetaObject::cast(const QObject*) and its helper (inherits/ the superdata walk). In qwidget.cpp read QWidget::isActiveWindow() and QWidget::window()/QWidgetPrivate. Determine the precise dereference that reads out of bounds: is it obj->metaObject() on a dangling QObject (garbage vtable), or the target-metaobject superdata chain, or a QWindow* from window() whose handle is null/dangling? State exactly which pointer is read OOB and what invalid value (freed ptr / null+offset / uninitialized) produces a wasm 'out of bounds'. Cite file:line.
Fable
thinking
I need to dig into the Qt source code to understand the exact nature of this out-of-bounds issue.
Fable
⚙ Bash {"command": "grep -n \"QMetaObject::cast\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp | head", "description": "Find QMetaObject::cast definition"}
Fable
⚙ Bash {"command": "grep -n \"isActiveWindow\\|::window()\\b\\|QWidget::window\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp | head", "description": "Find isActiveWindow and window in qwidget.cpp"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp", "offset": 398, "limit": 40}
Fable
⚙ Bash {"command": "grep -n \"isActiveWindow\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "description": "Find isActiveWindow implementation line"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 6967, "limit": 60}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 4376, "limit": 30}
Fable
thinking
Looking at the crash stack, I see the out-of-bounds read happens in QMetaObject::cast, which gets called from isActiveWindow through a qobject_cast to QWidgetWindow. The issue is that this cast only occurs when QGuiApplication::focusWindow() returns a non-null pointer, so I need to trace why that's leading to an invalid memory access.
Fable
⚙ Bash {"command": "grep -n \"initFrom\" /opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp", "description": "Find initFrom in qstyleoption.cpp"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qmetaobject.cpp", "offset": 280, "limit": 120}
Fable
thinking
Looking at the initFrom function now.
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/styles/qstyleoption.cpp", "offset": 141, "limit": 50}
Fable
thinking
I'm tracing through the call stack to understand where `QMetaObject::cast` gets invoked within `isActiveWindow`. The stack shows it's being called directly from `isActiveWindow`, but I need to figure out which operation inside that function triggers it—it's not coming from `QApplication::activeWindow()` itself, so there must be another path I'm missing.
Looking at the crash more carefully: `QMetaObject::cast` is reading `obj->metaObject()`, which means it's dereferencing the object's vtable pointer. If the object is dangling or freed, that vtable read becomes garbage, and calling through it would fail. In WebAssembly, an out-of-bounds memory access happens when the address exceeds the linea …
Fable
⚙ Bash {"command": "grep -n \"QApplication::activeWindow\\|activeWindow()\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qapplication.cpp | head", "description": "Find activeWindow"}
Fable
⚙ Bash {"command": "grep -rn \"qobject_cast\\|reinterpret_cast<T>\\|::cast(\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qobject.h | head; echo ---; grep -n \"static.*qobject_cast\\|T qobject_cast\" /opt/toolchains/qtsrc/qtbase/src/corelib/ke…
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qobject.h", "offset": 405, "limit": 40}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qapplication.cpp", "offset": 1571, "limit": 10}
Fable
⚙ Bash {"command": "grep -n \"qobject_cast_helper\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qobject.h", "description": "Find qobject_cast_helper"}
Fable
⚙ Bash {"command": "grep -rn \"qobject_cast_helper\" /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qobjectdefs.h /opt/toolchains/qtsrc/qtbase/src/corelib/kernel/*.h 2>/dev/null | head", "description": "Find qobject_cast_helper definition"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/corelib/kernel/qobjectdefs.h", "offset": 740, "limit": 30}
Fable
thinking
So the crash happens when `qobject_cast_helper` calls `QMetaObject::cast`, which checks if the object is null before dereferencing it. If object is null, there's no crash—but if it's a dangling pointer, then accessing `obj->metaObject()` reads from garbage memory and causes an out-of-bounds access. This means the crash requires a non-null but invalid pointer. Looking at `isActiveWindow`, the `qobject_cast` calls are trying to cast to `QWidgetWindow *`
and `QWindowContainer *`, both going through the same cast mechanism. The key question is whether the pointer being cast—likely `window` or `tlw` from the widget—is actually a dangling pointer rather than a valid object. Let me reconsider whet …
Fable
⚙ Bash {"command": "grep -n \"hideChildren\\|::hide()\\|void QWidgetPrivate::hide_helper\\|hideChildren(bool\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp | head", "description": "Find hideChildren"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 8535, "limit": 45}
Fable
thinking
When hideChildren sends a QHideEvent to visible child widgets, the QGroupBox processes it through its event handler, which calls initStyleOption to determine styling. This eventually traces up the parent chain via window() to find the top-level widget, then checks if that window is currently active using isActiveWindow().
The issue might be that during a workbench switch, a widget gets deleted but something still holds a reference to it, causing QMetaObject::cast to read out of bounds when it tries to validate the object.
Fable
⚙ Bash {"command": "grep -n \"QGroupBox::event\\|initStyleOption\\|::initFrom\\|QGroupBox::\" /opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp | head -30", "description": "Find QGroupBox event/initStyleOption"}
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/widgets/qgroupbox.cpp", "offset": 288, "limit": 40}
Fable
thinking
So QGroupBox::event is calling initStyleOption for every single event, including QEvent::Hide, which triggers a chain through initFrom that eventually calls window()->isActiveWindow(). This isActiveWindow call is where the crash happens in QMetaObject::cast—there's an out-of-bounds access happening during the cast operation.
The real question is whether the OOB is in the casts themselves (qobject_cast<QWidgetWindow*> or qobject_cast<QWindowContainer*>), or if the window pointer itself is already dangling when we call isActiveWindow on it. If window is a stale heap pointer, then calling its non-virtual methods would read garbage from the parent chain, eventually leading to a bad cast. The di …
Fable
QGroupBox::event unconditionally calls initStyleOption on every event. Let me confirm the FreeCAD workbench-switch path and the CAM/BIM specificity.
Fable
⚙ Bash {"command": "grep -n \"activateWorkbench\\|onToolBarSwitch\\|hide()\\|setVisible\\|deleteLater\\|delete \" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp | head -40", "description": "Scan Application.cpp for workbench …
Fable
⚙ Read {"file_path": "/opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidget.cpp", "offset": 8238, "limit": 30}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 70}
Fable
thinking
I need to examine the MainWindow::activateWorkbench function in Gui/MainWindow.cpp to understand how it handles the UI switching, particularly the toolbars and dock windows, and then look at how onToolBarSwitch fits into that flow.
Fable
⚙ Bash {"command": "grep -n \"activateWorkbench\\|hide\\|setVisible\\|delete\\|removeDockWindow\\|retranslate\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp | head -40", "description": "Scan MainWindow.cpp"}
Fable
⚙ Bash {"command": "grep -rn \"activateWorkbench\\|onToolBarSwitch\\|setupWorkbench\\|hideChildren\\|activate(\" /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Workbench.cpp | head", "description": "Scan Workbench.cpp"}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 1161, "limit": 80}
Fable
thinking
The workbench activation triggers toolbar and dock window reconstruction through Qt's signal-slot mechanism, while hidden widgets can leave dangling window references. I'm stepping back from the FreeCAD specifics to focus on the core Qt-level out-of-bounds issue, then briefly revisiting the CAM/BIM context with a more targeted approach.
CAM and BIM likely instantiate a widget containing a QGroupBox during their Initialize phase—possibly a task panel, preference dialog, or dock widget—whose parent window has already been destroyed or reparented. When the workbench switches and hideChildren traverses the widget tree, the QGroupBox's window() pointer becomes invalid, triggering the crash.
Mor …
Fable
The only `QMetaObject::cast` inside `isActiveWindow` is the `qobject_cast` in the focus-window loop. Let me confirm `focusWindow()` is a stored raw pointer that can dangle, and check the FreeCAD teardown that would leave it dangling.
Fable
⚙ Bash {"command": "grep -n \"QWindow \\*QGuiApplication::focusWindow\\|focus_window\" /opt/toolchains/qtsrc/qtbase/src/gui/kernel/qguiapplication.cpp | head", "description": "Find focusWindow storage"}
Fable
⚙ Bash {"command": "grep -rn \"QWidgetWindow *:\\|class QWidgetWindow\\|QWidget \\*QWidgetWindow::widget\\|widget()\" /opt/toolchains/qtsrc/qtbase/src/widgets/kernel/qwidgetwindow_p.h | head", "description": "Find QWidgetWindow::widget accessor"}
Fable
thinking
Looking at how `focus_window` works as a stored QWindow pointer that gets cleared when windows are destroyed, but in this single-window offscreen WASM setup, the main QWidgetWindow persists across workbench switches, so the pointer remains valid and the cast should work correctly.
But wait — I need to reconsider which `isActiveWindow()` call is actually crashing. The stack shows the cast happens within `isActiveWindow`, but if the focus loop operates on valid objects (the main window and its parent, which is null), then only one iteration occurs and the cast should succeed. Unless the crash is happening on a different `isActiveWindow()` call entirely, not from the focus loop. The stack attr …
Fable
`focus_window` is a raw stale-able pointer (qguiapplication.cpp:194). Let me quickly check what CAM/BIM specifically create that Part/Draft don't, focusing on dock widgets / QGroupBox panels created at Initialize.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod; for m in CAM Arch BIM Path Part Draft; do if [ -d \"$m\" ]; then echo \"== $m ==\"; grep -rln \"addDockWidget\\|QGroupBox\\|registerDockWidget\\|DockWindow\" \"$m/Gui\" 2…
Fable
thinking
I'm noticing that CAM and BIM both crash identically, and they're both Python workbenches, but Draft is also Python and works fine—so the language isn't the issue. I need to figure out what CAM and BIM have in common that's different from the working workbenches like Part, Draft, and Sketcher.
My best guess is that CAM and BIM both register a preference page or create a status bar widget containing QGroupBox during initialization, or they import a heavy module that instantiates a hidden widget with QGroupBox. But I can't reliably pin down the exact FreeCAD-specific object without actually building and running it.
For the Qt-level crash itself, I'm confident the issue is an invalid object p …
Fable
I have traced the exact path. Let me compile the findings.
The chain is fully confirmed at the Qt level:
1. **qgroupbox.cpp:288-306** — `QGroupBox::event()` calls `initStyleOption(&box)` unconditionally for *every* event, including the `QHideEvent`.
2. **qwidget.cpp:8535-8560** — `QWidgetPrivate::hideChildren()` sends that `QHideEvent` (line 8560 `QCoreApplication::sendEvent(widget, &e)`) to each visible child, so a QGroupBox child gets `event()` → `initStyleOption`.
3. **qstyleoption.cpp:141-153** — `QStyleOption::initFrom()` does `window = widget->window()` (143) then `window->isActiveWindow()` (153).
4. **qwidget.cpp:6967-7003** — the *only* `QMetaObject::cast` reachable inside `isActiveWindow()` is the `qobject_cast` in the focus-window container loop, lines 6997-6998. (Line 6970 `activeWindow()` is a plain pointer compare; the `windowHandle()->handle()` path at 7010-7012 is null-guarded.)
5. **qobjectdefs.h:758** — `qobject_cast_helper` → `staticMetaObject.cast(object)`.
6. **qmetaobject.cpp:411-414** — `cast()` null-checks `obj`, then evaluates `obj->metaObject()`.
Fable
⚙ StructuredOutput {"area": "Qt use-after-free: QMetaObject::cast dereferencing a dangling QObject's corrupted vtable pointer during QGroupBox hide on CAM/BIM workbench activation", "findings": "EXACT OOB: The out-of-bounds read is the virtual-dispatch load i…