subagent transcript
session 17f09f89
delegated task
I'm debugging a use-after-free in a FreeCAD→WebAssembly port. During a workbench switch (Gui.activateWorkbench("CAMWorkbench") or "BIMWorkbench"), a t
▸ Magik
I'm debugging a use-after-free in a FreeCAD→WebAssembly port. During a workbench switch (Gui.activateWorkbench("CAMWorkbench") or "BIMWorkbench"), a top-level QWidget that is a QWidgetWindow's wrapped widget (i.e. focus_window's widget) gets FREED **without its ~QWidget destructor running** — proven because after the free, the widget's QPointer is still non-null AND it's still present in QWidgetPrivate::allWidgets (both of which ~QWidget/~QObject would have cleared). So the memory is reclaimed/reused by a path that bypasses the normal `delete`/destructor (a raw free(), a double-free/heap corruption, or a mismatched deallocation — possibly via PySide/shiboken Python ownership of a C++ QWidget).
I need to LOCATE the code that could improperly free a top-level widget (a window, dock widget, toolbar, or MDI view) during workbench activation/deactivation. This is a FreeCAD source tree at /home/magik6k/lcad-wasm/freecad-port/FreeCAD.
Please search and report (with file:line and short code excerpts) for:
1. Workbench switch teardown: In src/Gui/, find where a workbench is deactivated/activated and what widgets it removes or deletes. Look at WorkbenchManager.cpp, Workbench.cpp, Application.cpp (activateWorkbench / setupContextMenu / retranslate), ToolBarManager.cpp, DockWindowManager.cpp, MenuManager.cpp. Report any `delete`, `deleteLater`, `free(`, `->deleteLater()`, or manual widget destruction, especially of QToolBar, QDockWidget, QWidget top-levels.
2. Any use of `free(` or `operator delete` or `qFree` or placement-new / custom allocators on QObject/QWidget-derived types anywhere in src/Gui/ and src/Main/.
3. PySide/shiboken ownership transfers: search src/Gui/ and the whole tree for `setParent`, `Shiboken`, `getTypeUserData`, `releaseOwnership`, `invalidate`, `keepReference`, wrapping of QWidget in Python (e.g. `PythonWrapper`, `qt_wrapInstance`, `wrapInstance`, `getMainWindow` returning PySide). Also look in src/Gui/PythonWrapper.cpp if it exists.
4. In the CAM (src/Mod/CAM or src/Mod/Path) and BIM (src/Mod/BIM) workbench InitGui.py and any Gui command classes: look for creation of Qt widgets (QComboBox, QLineEdit, task panels, toolbars) during workbench Activated()/Initialize(), and any widget deletion or ownership handoff.
5. Anything that deletes or reparents the 3D MDI view (Gui::View3DInventor / MDIView / QuarterWidget) or a dock widget during workbench switch.
Report the most suspicious candidates for an improper free of a top-level widget, ranked, with file:line and excerpts. Do not edit anything — this is read-only investigation.
Fable
thinking
I'm diving into the FreeCAD codebase to understand its structure and locate the relevant code sections for this investigation.
Fable
I'll investigate this use-after-free in the FreeCAD wasm port. Let me start with broad searches across the relevant areas in parallel.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && ls src/Gui/ | grep -iE \"Workbench|ToolBar|DockWindow|MenuManager|Application|PythonWrapper\" ", "description": "List relevant Gui source files"}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && grep -rnE \"delete |deleteLater|\\bfree\\(|operator delete|qFree\" src/Gui/WorkbenchManager.cpp src/Gui/Workbench.cpp 2>/dev/null | head -80", "description": "Search deletes in…
Fable
thinking
I'm examining the Workbench.cpp file around the deletion logic and checking other files to understand the broader context.
Fable
Let me look at the critical Workbench.cpp deletions and search the other managers in parallel.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Workbench.cpp", "offset": 430, "limit": 100}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && grep -rnE \"delete |deleteLater|\\bfree\\(|operator delete|qFree|->deleteLater\" src/Gui/ToolBarManager.cpp src/Gui/DockWindowManager.cpp src/Gui/MenuManager.cpp 2>/dev/null | …
Fable
thinking
I'm tracing through the workbench activation flow, starting with the `activate()` method in Workbench.cpp which orchestrates the switch by calling setup methods on ToolBarManager, DockWindowManager, and MenuManager. I need to examine where the teardown and rebuild logic happens in each of these setup methods, particularly looking at the deletion points in ToolBarManager around lines 144 and 210, and in DockWindowManager at lines 385, 416, and 671.
Fable
The `activate()` path calls `setup()` on each manager, which is where teardown happens. Let me examine those setup methods and the surrounding deletes.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/ToolBarManager.cpp", "offset": 100, "limit": 240}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 340, "limit": 120}
Fable
thinking
I need to examine the `setup` methods in ToolBarManager and DockWindowManager to understand what gets initialized during workbench activation.
Fable
Now let me look at the key `setup()` methods that run during activation, plus the ToolBarManager destruction logic.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && grep -nE \"::setup\\(|void .*setup|ToolBarManager::setup|DockWindowManager::setup|MenuManager::setup|removeToolBars|::retranslate|createToolBar|customTB\" src/Gui/ToolBarManage…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && grep -nE \"::setup\\(|void .*setup|DockWindowManager::setup|setupDockWindows|addDockWindow|removeDockWindow\" src/Gui/DockWindowManager.cpp | head -50", "description": "Find se…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/ToolBarManager.cpp", "offset": 680, "limit": 140}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 516, "limit": 160}
Fable
thinking
The DockWindowManager and toolbar setup during workbench switches preserve existing windows and toolbars rather than recreating them—they just toggle visibility as needed. The real issue is that a top-level QWidget wrapped by a QWidgetWindow is being freed prematurely, and that QWidgetWindow is the QWindow backing the top-level widget.
Given the PySide/shiboken involvement, I need to investigate how shiboken handles C++ QWidget ownership, particularly in PythonWrapper.cpp. I should also trace through the CAM/BIM InitGui.py widget creation and any reparenting that happens in MDIView or View3DInventor. The focus is on identifying which top-level widget gets freed during workbench activation a …
Fable
The manager `setup()` methods only hide/show — they don't delete top-levels during a switch. The bug smells like PySide/shiboken ownership. Let me dig into PythonWrapper.cpp and shiboken ownership, plus MenuManager::setup and retranslate.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/PythonWrapper.cpp"}
Fable
thinking
I'm looking at the PySide wrapper code, specifically the `fromQWidget` function that's creating new Python objects from Qt widgets. The key issue is that it's calling Shiboken's `newObject` with `hasOwnership` set to false, which affects how the Python wrapper manages the underlying C++ object's lifetime.
The real problem seems to be a type resolution issue — when the resolved type doesn't match the actual object type, Shiboken might use the wrong deallocator when cleaning up, potentially causing a raw free of the C++ memory without properly running the destructor. This could explain why the widget gets freed but its destructor never runs and it remains in the allWidgets list. So the real i …
Fable
This is the core wrapper. `fromQWidget` uses `newObject(..., false, ...)` (ownership false) which is correct. Let me now cast a wide net: shiboken ownership calls tree-wide, the CAM/BIM InitGui, and getMainWindow wrapping. Let me spawn parallel searches.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== newObject ownership true (whole tree) ===\"; grep -rnE \"newObject\\(\" src/ | grep -vE \", false,\" | head -40; echo; echo \"=== releaseOwnership / getOwnership / s…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== CAM/Path/BIM dirs ===\"; ls src/Mod/ | grep -iE \"CAM|Path|BIM\"; echo; echo \"=== InitGui files ===\"; find src/Mod/CAM src/Mod/Path src/Mod/BIM -name \"InitGui.py\…
Fable
thinking
The grep search confirms that all shiboken `newObject` calls use `false` for ownership, while FreeCAD's own `newObject` calls are separate. Now I need to examine the CAM and BIM InitGui.py Activated methods, then search the codebase for Coin/Quarter view reparenting and trace the `getMainWindow` path.
The ToolBarManager::setup code shows focus handling logic that checks if a combo box inside a toolbar has focus during workbench switches, but it doesn't delete anything. The real issue is likely a top-level QWidget being freed — in Qt, every top-level widget has a backing QWidgetWindow, and the "focus_window" refers to whichever QWindow has focus at the time. During workbench switches, variou …
Fable
Shiboken `newObject` calls all use ownership `false` (the ownership-true grep only matched FreeCAD's document `newObject`). Now let me look at the CAM and BIM InitGui Activated/Initialize, and the WorkbenchSelector (a combo box in a toolbar — a likely focus widget).
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== CAM InitGui Activated/Initialize ===\"; grep -nE \"def Activated|def Initialize|def Deactivated|QComboBox|QLineEdit|QWidget|QToolBar|QDockWidget|deleteLater|\\.delet…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WorkbenchSelector.cpp"}
Fable
thinking
The WorkbenchSelector is in a toolbar, not at the top level. Let me trace through the actual bug—looking at the CAM and BIM InitGui Activated and Deactivated methods to understand what's happening. The core issue seems to be about a top-level QWidget that's wrapped by QWidgetWindow, possibly the main window itself or a task dialog, which is particularly relevant in the wasm build.
Now I need to examine the CAM InitGui code around the Activated and Deactivated methods to see what's being registered and unregistered.
Fable
Let me read the CAM and BIM InitGui Activated/Deactivated bodies.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/CAM/InitGui.py", "offset": 355, "limit": 40}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Mod/BIM/InitGui.py", "offset": 625, "limit": 120}
Fable
thinking
Looking at how the BIM Views widget gets created and destroyed through `findWidget()`, and how the dock widget lifecycle works during workbench switches. The key issue is that `reloadActive()` at line 715 re-triggers the `activate()` method, which re-runs all the toolbar, dock, and menu setup — creating a re-entrancy problem.
The real mechanism I need to focus on is in the C++ side, not the Python wrappers. The bug signature points to a raw `free()` call on a QWidget that's still registered in Qt's allWidgets list, which suggests improper memory management in the wasm port or shiboken bindings. Since this is a wasm build with static Qt and shiboken6, the issue likely stems from how Python o …
Fable
The BIM `Activated()` is heavy: it calls `reloadActive()` (re-entrant `activate()`), manipulates the BimViews dock widget, and uses `draftToolBar`. Let me trace the MDI view / getActiveWindow wrapping, View3DInventor deletion, and the draftToolBar widget.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== getActiveWindow / getMainWindow python wrap ===\"; grep -rnE \"getActiveWindow|getMainWindow\" src/Gui/ApplicationPy.cpp src/Gui/MainWindowPy.cpp 2>/dev/null | head;…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== reloadActive ===\"; grep -rnE \"reloadActive|reloadActiveWorkbench\" src/Gui/*.cpp; echo; echo \"=== activateWorkbench flow in Application.cpp ===\"; grep -nE \"acti…
Fable
thinking
I need to trace through the `Application::activateWorkbench` method to see what happens when switching workbenches, and then look at how `MainWindowPy::createWrapper` handles the ownership of `getMainWindow`.
Fable
Let me read the full `Application::activateWorkbench` flow and how `getMainWindow`/`getActiveWindow` create their Python wrappers.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/Application.cpp", "offset": 1901, "limit": 130}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindowPy.cpp", "offset": 150, "limit": 120}
Fable
thinking
I'm checking how `getMainWindow()` and `getActiveWindow()` work—they both return PyCXX wrapper objects rather than shiboken-generated ones, with `getMainWindow()` wrapping the C++ pointer through `MainWindowPy::createWrapper()` and `getActiveWindow()` returning the MDI view's Python object.
The real issue is tracking what happens during workbench switching. I need to examine the deletion paths in `MainWindow.cpp` around the `removeWindow` calls and see if `activateWorkbench` is inadvertently freeing a top-level widget. Let me check those specific code sections to identify which deletion site is most likely causing the crash.
Fable
`getActiveWindow`/`getWindows` return `mdi->getPyObject()` — I need to see how MDIView's Python object and ownership work, plus the MainWindow MDI deletion sites. Let me read those.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 1445, "limit": 60}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== MainWindow::activateWorkbench ===\"; grep -nE \"MainWindow::activateWorkbench\" src/Gui/MainWindow.cpp; echo \"=== MDIView getPyObject ===\"; grep -rnE \"getPyObject…
Fable
thinking
I need to trace through the deletion flow by examining the activateWorkbench method and how MDIView handles its own destruction, then check the other deletion sites in MainWindow to understand the full lifecycle.
Fable
Let me read `MainWindow::activateWorkbench`, the MDIView destructor/deleteSelf, and the other MainWindow deletion sites.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindow.cpp", "offset": 1161, "limit": 70}
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MDIView.cpp", "offset": 90, "limit": 80}
Fable
thinking
MDIView clears its Python object properly in the destructor using PyCXX, so that's not the issue. The real problem is that the QPointer is still non-null and the widget remains in allWidgets, which means the QWidget destructor never actually ran. I'm reconsidering what's happening here—the widget is wrapped inside a QWidgetWindow that was created for a top-level QWidget, and something's preventing that destructor from being called.
In a WebAssembly Qt build, each top-level widget gets its own QWidgetWindow, and the focus_window is whichever QWindow currently has keyboard focus. The Draft toolbar and Snapper are Python-created widgets living in the task panel or as docks. But the real issue …
Fable
MDIView uses PyCXX (not shiboken) and clears its wrapper in the destructor — so that path would run `~QWidget`. The symptom (no destructor) points elsewhere. Let me examine MenuManager's setup/teardown (the classic menu-rebuild path) and the WorkbenchManipulator (BIM adds one via `modifyMenuBar`).
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MenuManager.cpp", "offset": 120, "limit": 120}
Fable
thinking
I'm diving deeper into the MenuManager::setup method to understand how menus are being created and destroyed.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MenuManager.cpp", "offset": 240, "limit": 140}
Fable
thinking
I'm finding something important in the MenuManager setup code — when `clear()` is called on the menu bar, it removes all actions, but the actual QMenu objects that were added remain as children of the menu bar rather than being deleted. Let me think through the Qt semantics here to understand what's actually happening.
So after `clear()` empties the actions list, the next iteration through menu items calls `findAction()` with an empty actions list, which means it always returns null and creates brand new QMenu objects every time. The old menus just stay orphaned as children of the menu bar until it's destroyed — that's a memory leak, not a proper cleanup.
But wait, I need to refocus on wha …
Fable
Found `menuBar->clear()` (MenuManager.cpp:224) — rebuilds menus each switch, but that orphans/leaks QMenus rather than raw-freeing them. Let me now run the item-2 search (`free`/`operator delete`/`qFree`) across src/Gui and src/Main, and inspect the toolbar-area reparenting and dock-window creation paths.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== free( / operator delete / qFree / ::free in src/Gui and src/Main ===\"; grep -rnE \"\\bfree\\s*\\(|operator delete|qFree|::free\\b|std::free|placement|malloc\\(|real…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== ToolBarAreaWidget delete/remove/reparent ===\"; grep -nE \"delete|deleteLater|setParent|removeWidget|addWidget|takeAt|removeAt\" src/Gui/ToolBarAreaWidget.cpp | head…
Fable
thinking
I'm trying to debug why my grep search for `free(` is returning noisy results that don't actually contain "free" — the matches seem to be pulling up "placement" lines instead, which shouldn't be happening with my pattern, so I'm trying to figure out what's actually matching. I'm getting tangled up in the grep results — let me step back and redo this search more carefully with word boundaries and fixed strings instead of trying to parse potentially mangled patterns. The real investigation should focus on DockWindowManager's addDockWindow and removeDockWindow methods, plus how dock widgets like the combo view and tree view are created and destroyed during workbench switches. I've already cover …
Fable
The free-search got contaminated by "placement" noise. Let me rerun it cleanly, and inspect the dock-window creation path plus the BIM Views widget lifecycle.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== raw free()/operator delete/qFree (code only) in src/Gui + src/Main ===\"; grep -rnwE \"free|qFree|realloc\" src/Gui/*.cpp src/Main/*.cpp 2>/dev/null | grep -E \"free…
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/DockWindowManager.cpp", "offset": 251, "limit": 90}
Fable
thinking
I'm not seeing any raw `free()` or `operator delete` calls on widgets in the GUI code, which rules out a direct memory deallocation issue—this points more toward a shiboken/PySide ownership problem or a double-free triggered by `deleteLater`. Looking at the DockWindowManager now, specifically the `addDockWindow` function around line 256-259, which seems to be where the issue might be occurring. During a workspace switch, the ToolBarManager, DockWindowManager, and MenuManager only hide or rebuild—they don't delete top-level widgets, except MenuManager which clears and recreates menus. Top-level dock widgets and MDI views get deleted via `delete dw` in DockWindowManager or `deleteLater` calls …
Fable
No raw `free()`/`operator delete` on widgets exists in src/Gui or src/Main (only a comment) — this rules out a literal raw-free in FreeCAD C++ Gui code and strengthens the shiboken/PySide or re-entrancy hypothesis. Let me examine the BIM Views dock lifecycle and the Draft toolbar, which are the Python-created top-level widgets touched during BIM activation.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== BimViews.py findWidget / dock creation / delete ===\"; grep -rnE \"def findWidget|findChild|addDockWidget|removeDockWindow|QDockWidget|deleteLater|\\.close\\(|setPar…
Fable
thinking
I'm seeing how FreeCAD's DraftGui creates a toolbar by instantiating a C++ ToolBar class through the UiLoader, wrapping it in PySide, then adding it to the main window and setting the parent relationship.
The critical question is whether the Python wrapper maintains ownership of the underlying C++ ToolBar after `setParent()` is called, or if ownership transfers to the C++ parent — this could explain the crash when the workbench switches and Python garbage collection runs. I need to check UiLoader.cpp to understand the ownership semantics when creating widgets this way, especially since Gui::ToolBar is a top-level-capable widget that can be floated independently.
Fable
Major lead: `DraftGui.py:230` creates a C++ `Gui::ToolBar` via `FreeCADGui.UiLoader().createWidget("Gui::ToolBar")` and holds it as a PySide wrapper (`self.tray`) — a toolbar can float and become a top-level window. Ownership of that wrapper is the crux. Let me inspect the UiLoader.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== UiLoader files ===\"; ls src/Gui/UiLoader* 2>/dev/null; echo \"=== createWidget / newObject / ownership in UiLoader.cpp ===\"; grep -nE \"createWidget|newObject|Shib…
Fable
thinking
Tracing through the widget creation flow, `createWidget` returns a Python wrapper via `fromQWidget`, which uses Shiboken to create the wrapper with ownership set to false — meaning the Python `self.tray` doesn't own the underlying C++ ToolBar object, so garbage collection won't affect it.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/UiLoader.cpp", "offset": 600, "limit": 70}
Fable
thinking
I'm looking at the `wrapFromWidgetFactory` function around lines 60-110 to understand the ownership model, particularly focusing on line 101 where it returns the result of `wrap.fromQWidget`.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/UiLoader.cpp", "offset": 36, "limit": 115}
Fable
thinking
So `wrapFromWidgetFactory` creates the C++ widget through WidgetFactory and wraps it with `fromQWidget` where the Python wrapper doesn't take ownership. This means garbage collection of the Python wrapper won't deallocate the underlying C++ object. But there's a critical subtlety when the widget is created with a parent through WidgetFactory—if it initially has no parent, something important happens...
For the Draft tray specifically, `FreeCADGui.UiLoader().createWidget("Gui::ToolBar")` passes no parent, so it creates a parentless C++ ToolBar that Python doesn't own. Normally this would leak, but then `mw.addToolBar(self.tray)` and `self.tray.setParent(mw)` give it a C++ parent in the mainw …
Fable
Ownership is `false` in all these wrap paths, so GC of a Python wrapper shouldn't free the C++ widget. But BIM creates a Python-owned `QDockWidget()` directly (BimViews.py:73). Let me check the remaining teardown surfaces: ToolBarManager's full delete/reparent picture and `saveState`, and the WorkbenchManipulator that BIM registers.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== ALL delete/deleteLater/setParent/removeToolBar in ToolBarManager.cpp ===\"; grep -nE \"delete|deleteLater|setParent|removeToolBar|onToolBar|destroyed|takeChildren|->…
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== WorkbenchManipulator.cpp deletes/reparent ===\"; grep -nE \"delete|deleteLater|setParent|removeWidget|clear\\(\\)\" src/Gui/WorkbenchManipulator.cpp | head; echo; ec…
Fable
thinking
Looking at the removeToolBar call in ToolBarManager around line 1034 to understand how custom toolbars are being torn down, and then checking the BimViews implementation where a QDockWidget is created in Python with its close event handling.
The key issue is ownership transfer: when `addDockWidget` is called in PySide, it should transfer C++ ownership so Python's garbage collector won't delete it. But if the wasm port's typesystem wasn't properly updated, `addDockWidget` might not actually transfer ownership, leaving Python holding the reference. Then during a workbench switch, if Python's GC runs and collects the dock widget, shiboken would free the C++ object and run its destructor — whic …
Fable
Let me look at the `removeToolBar` context in ToolBarManager (line 1034) and the `createActionWidget` static — the last teardown surfaces I need.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/ToolBarManager.cpp", "offset": 1000, "limit": 55}
Fable
thinking
That's not the issue I'm looking for. I need to dig deeper into the `TaskView` and `Control` mechanisms—specifically how `clearTaskWatcher` and `addTaskWatcher` interact during activation, especially since the Draft toolbar lives inside the task panel. I should also verify whether the WrapperManager is creating duplicate wrappers somewhere.
Let me check if there's a re-entrancy problem in `activate()` triggered by BIM's `reloadActive` call, and trace through what `WorkbenchPyImp::reloadActive` actually does.
Fable
Let me check `reloadActive` (BIM calls it re-entrantly during `Activated`) and the Control task-panel teardown that BIM triggers.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/WorkbenchPyImp.cpp", "offset": 160, "limit": 40}
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== WrapperManager addQObject re-wrap / getMainWindow wrapper reuse ===\"; grep -nE \"createWrapper|MainWindowPy::MainWindowPy|_mw|Py_INCREF|Py_DECREF|static .*wrapper\"…
Fable
thinking
I'm looking at how TaskView.cpp handles cleanup of task panel widgets during the teardown process, particularly around the sections where panels are deleted and focus is managed through focusNextPrevChild. The key areas seem to be in the lines around 784 and 961 where this widget destruction and focus forwarding happens.
The Draft toolbar is embedded in the task panel through task watchers, and when BIM calls clearTaskWatcher/addTaskWatcher, it tears down the old watcher widgets — which could include the draftToolBar's widget. I need to trace through the TaskView teardown code to see exactly where this deletion occurs and how it relates to the focus_window issue.
Fable
TaskView has many `delete panel`/`delete taskPanel` sites, and comments about deleting focus widgets to avoid `focusNextPrevChild()` forwarding — and BIM's `Activated()` calls `Control.clearTaskWatcher()`/`addTaskWatcher()`. Let me read those teardown sections.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/TaskView/TaskView.cpp", "offset": 770, "limit": 130}
Fable
thinking
I'm seeing that when `TaskView::addTaskWatcher()` is called, it deletes the previous TaskWatcher objects, which in turn own their associated content widgets. So when BIM switches workbenches and calls `clearTaskWatcher()` followed by `addTaskWatcher()` with new watchers, the old ones from the previous workbench like Draft get cleaned up. The question is whether deleting a TaskWatcher also properly deletes its owned widgets.
Looking at the code flow, the watcher content widgets get reparented into the action panel (Qt parent becomes the action panel), which means the action panel takes ownership. If the TaskWatcher destructor also tries to delete them, or if the Python side still holds a ref …
Fable
`TaskView::addTaskWatcher` does `delete tw` on old watchers during the switch, and their content widgets get reparented into the action panel (line 864). Let me check the TaskWatcher destructor / content ownership — a shared-ownership double-free candidate — plus the `setShownTaskInfo` panel deletes.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== TaskWatcher class + dtor + getWatcherContent ===\"; grep -rnE \"class TaskWatcher|~TaskWatcher|getWatcherContent|Cont\\b|std::vector<QWidget|delete\" src/Gui/TaskVie…
Fable
thinking
Looking at the TaskWatcher destructor, it deletes each content widget it owns, so when TaskView switches workbenches and calls addTaskWatcher with new watchers, it first deletes the old ones—triggering their destructors and cleaning up all their child widgets in the process.
The tricky part is that these content widgets were already reparented into the actionPanel via addWidget, so they have Qt as their parent. When the TaskWatcher deletes them directly, Qt's destructor properly removes them from the parent's children list, so there's no double-free there. But the real risk emerges if the same widget exists in multiple watchers' content lists, or if a widget is owned by both a TaskWatcher A …
Fable
Confirmed: `TaskWatcher::~TaskWatcher()` deletes its Content widgets (TaskWatcher.cpp:52), and those same widgets get reparented into the action panel (TaskView.cpp:864). Let me read the TaskWatcher destructor and content to confirm the ownership picture.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/TaskView/TaskWatcher.cpp", "offset": 40, "limit": 50}
Fable
thinking
So `TaskWatcher::addTaskBox` wraps a widget in a `TaskBox` and adds it to the content's layout, with the taskbox managing the widget's lifetime — when the taskbox is deleted, it takes its children with it. For Draft, these wrapped widgets are the command bar components, and I'm tracing through how they get created and destroyed.
The key re-entrancy issue is that `BIM.Activated` calls `reloadActive()` which triggers `activate()` a second time, even though `WorkbenchManager::instance()->activate()` already ran it once during the initial workbench activation. This double-activation tears down and rebuilds toolbars, docks, and menus, which is the documented re-entrancy problem.
I should also v …
Fable
Let me confirm what `Gui.getMainWindow()` actually returns to Python (BimViews calls `mw.addDockWidget`, `mw.findChild`, `mw.statusBar` — QMainWindow methods), since that determines whether a shiboken-wrapped top-level QMainWindow is in play.
Fable
⚙ Read {"file_path": "/home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Gui/MainWindowPy.cpp", "offset": 60, "limit": 92}
Fable
thinking
I see how the wrapper works now. The `createWrapper` function takes the real QMainWindow object and wraps it using Shiboken to create a proper PySide wrapper, then attaches FreeCAD's custom methods like `getWindows` and `getActiveWindow` as attributes on that wrapped object.
Each call to `Gui.getMainWindow()` returns a fresh Shiboken wrapper (unless Shiboken's BindingManager reuses an existing one), with ownership set to FALSE so the C++ main window itself won't be garbage collected by Python. When BimViews adds a dock widget via `mw.addDockWidget()`, PySide transfers ownership of that widget to the C++ main window, meaning Python's garbage collector won't touch it — the C++ side manages it …
Fable
I now have a complete picture across all five requested areas. Let me do one final confirming check on the re-entrancy trigger location in the activation flow, then report.
Fable
⚙ Bash {"command": "cd /home/magik6k/lcad-wasm/freecad-port/FreeCAD && echo \"=== BIM Control.clearTaskWatcher/addTaskWatcher/reloadActive lines ===\"; grep -nE \"clearTaskWatcher|addTaskWatcher|reloadActive\" src/Mod/BIM/InitGui.py; echo \"=== Co…
Fable
thinking
I've traced through how BIM activation and deactivation trigger a chain of control methods that clear and re-register task watchers, ultimately leading to a workbench reactivation. Now I'm ready to compile the final ranked report.
Fable
Investigation complete. I covered all five requested areas by tracing the actual `activateWorkbench` → `Workbench::activate()` teardown path in C++, the PySide/shiboken wrapping layer, and the CAM/BIM `Activated()`/`Deactivated()` Python triggers.
## Key structural finding
`Workbench::activate()` (`src/Gui/Workbench.cpp:453-480`) is the C++ core of every switch. It calls `ToolBarManager::setup`, `DockWindowManager::setup`, `MenuManager::setup`. I read all three `setup()` bodies: **during a normal switch they only hide/show and rebuild — they do not delete top-level toolbars/docks.** Also, a clean grep found **no raw `free()` / `operator delete` / `qFree` on any QObject/QWidget in `src/Gui` or `src/Main`** (only an unrelated comment in `Command.cpp:1045`). So the improper free is **not** a literal raw-free in FreeCAD Gui C++ — which, given your symptom (no `~QWidget`, QPointer still set, still in `allWidgets`), points hard at the **shiboken/PySide dealloc path** or heap corruption/double-free.
## Ranked suspects
### 1. Shiboken wrapping of C++ QWidgets — `PythonWrapper::fromQWidget` / `fromQObject` (BEST match for the symptom)
`src/Gui/PythonWrapper.cpp:838-873` (widget) and `:808-836` (object):
```cpp
auto type = getPyTypeObjectForTypeName<QWidget>();
const char* wrapperName = typeName;
if (!type) { type = getPyTypeObjectForPySideTypeName(wrapperName); }
if (!type) { wrapperName = getWrapperName(widget); type = getPyTypeObjectForPySideTypeName(wrapperName); }
if (type) {
PyObject* pyobj = Shiboken::Object::newObject(type, widget, false, false, wrapperName);
WrapperManager::instance().addQObject(widget, pyobj);
```
Ownership is passed `false` (correct), **but the type is resolved by fragile fallbacks** (typeid name → PySide type-name → `getWrapperName` walking the metaobject to a base class, `:875-900`). In the wasm shiboken6/CPython3.14 build, if this resolves to a base/mismatched `SbkObjectType`, the wrapper's registered C++ destructor is the wrong one (or null) — so when that SbkObject is deallocated shiboken frees the block via `tp_free`/mismatched dtor **without running `~QWidget`**, leaving the QPointer set and the entry in `allWidgets`. This is the only code path I found whose failure mode exactly reproduces your symptom.
### 2. `Gui.getMainWindow()` returns a fresh shiboken QMainWindow every call
`src/Gui/MainWindowPy.cpp:94-121`:
```cpp
Py::Object py = wrap.fromQWidget(mw, "QMainWindow"); // new SbkObject each call, ownership=false
```
`getActiveWindow()`/`getWindows()` similarly re-wrap MDI views (`:195-208`, via `MDIView::getPyObject`). BIM's `Activated()` calls `getMainWindow()...getActiveWindow()` ~15 times (InitGui.py:247-424). Combined with `WrapperManager::addQObject` (`PythonWrapper.cpp:411-449`), which keys its invalidator on the *pyobj pointer* and `Py_INCREF`s each **distinct** wrapper, you get multiple live SbkObjects for one C++ top-level widget with per-wrapper `destroyed→setValidCpp(false)` connections — a race/ordering hazard for invalidation during the switch.
### 3. BIM creates a Python-owned top-level dock widget
`src/Mod/BIM/bimcommands/BimViews.py:73` `vm = QtGui.QDockWidget()` (shiboken ownership=true), given a Python ref cycle via `vm.closeEvent = self.onClose` (`:75`), then `mw.addDockWidget(..., vm)` (`:166`). A floating QDockWidget is a top-level window. **If the port's PySide6 typesystem doesn't mark `QMainWindow.addDockWidget` as transferring ownership to C++**, Python keeps ownership and GC during the switch frees the C++ dock. Worth verifying the typesystem annotation. Analogous: Draft's `self.tray = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")` (`src/Mod/Draft/DraftGui.py:230`) — a C++ `Gui::ToolBar` (floatable → top-level), wrapped ownership=false at `UiLoader.cpp:101/636`.
### 4. Task-panel widget deletion during the switch — matches "focus_window's widget"
BIM `Activated()`/`Deactivated()` call `FreeCADGui.Control.clearTaskWatcher()` and `addTaskWatcher(...)` (`src/Mod/BIM/InitGui.py:657,675,745`). These reach `TaskView::addTaskWatcher` (`src/Gui/TaskView/TaskView.cpp:825-834`) which does `delete tw`, and `~TaskWatcher` (`src/Gui/TaskView/TaskWatcher.cpp:49-55`) does `delete it` on its Content taskboxes — after those widgets were reparented into the action panel (`TaskView.cpp:864`). This block **explicitly manipulates the focus widget** (`TaskView.cpp:782-798`: "make sure to set the focus on a widget that won't be hidden or deleted... otherwise Qt may forward the focus"), which lines up precisely with your "focus_window's widget." A widget deleted here that also has a live shiboken wrapper (from #1) yields a mismatched/double free. Also `Workbench::removeTaskWatcher` → `TaskView::clearTaskWatcher` (`Workbench.cpp:503-509`).
### 5. Re-entrant `activate()` amplifier
BIM `Activated()` calls `Gui.activeWorkbench().reloadActive()` (`InitGui.py:715`, also Deactivated `:770`) → `WorkbenchPyImp.cpp:175` `active->activate()`. This runs **inside** `Application::activateWorkbench`'s Activated handler (`Application.cpp:1995-2002`), *after* `WorkbenchManager::activate` already ran `Workbench::activate()` once (`Application.cpp:1962`). So the full toolbar/dock/menu teardown+rebuild runs **twice within one switch**, widening every double-free/dangling-wrapper window above. `MenuManager::setup` does `menuBar->clear()` and recreates all menus each pass (`MenuManager.cpp:224`).
### Lower priority (not normally hit on a plain switch, but reachable)
- `DockWindowManager::removeDockWindow` → `delete dw` (`DockWindowManager.cpp:385, 416`) and `onWidgetDestroyed` → `dw->deleteLater()` (`:671`).
- `MainWindow::removeWindow` → `subwindow->setParent(nullptr); subwindow->deleteLater(); parent->deleteLater()` (`MainWindow.cpp:1484-1494`), plus MDI deletes at `:663, 1381, 1772, 2087`.
## Recommendation
The symptom (memory reclaimed, `~QWidget` never ran) is only producible by suspect **#1's** shiboken dealloc path or by heap corruption — not by any FreeCAD `delete`/`deleteLater` (those all run `~QWidget`). I'd instrument `PythonWrapper::fromQWidget`/`fromQObject` (log the resolved `type`/`wrapperName` and the `hasOwnership`/dtor for the crashing widget) and check whether the wasm PySide6 typesystem transfers ownership on `addDockWidget`/`addToolBar`. Suspects #2–#5 are the FreeCAD triggers that put a top-level widget under a shiboken wrapper or delete it mid-switch, feeding #1.
No files were modified (read-only investigation).