Remove MainWindow's own event filters before its members are destroyed by whosawhatsis · Pull Request #7047 · openscad/openscad · GitHub
Skip to content

Remove MainWindow's own event filters before its members are destroyed - #7047

Open
whosawhatsis wants to merge 2 commits into
openscad:masterfrom
whosawhatsis:bugfix/mainwindow-eventfilter-uaf
Open

whosawhatsis wants to merge 2 commits into
openscad:masterfrom
whosawhatsis:bugfix/mainwindow-eventfilter-uaf

Conversation

@whosawhatsis

Copy link
Copy Markdown
Contributor

User documentation for the wiki is in the last section.

Summary

Closing a window reads freed memory.

MainWindow installs itself as an event filter on itself (MainWindow.cc, installEventFilter(this))
and on the find panel, and neither filter is removed when the window is destroyed. Destroying a member
such as rubberBandManager reparents the widget it owns; reparenting delivers events; those events
reach MainWindow::eventFilter(), which calls rubberBandManager.isVisible() — on the member that is
at that moment being destroyed.

Under AddressSanitizer this is a heap-use-after-free. In an ordinary build the freed block is usually
still intact, so the read silently returns stale data, which is why it has gone unnoticed.

ERROR: AddressSanitizer: heap-use-after-free
  READ  RubberBandManager::isVisible()             RubberBandManager.cc:25
        MainWindow::eventFilter(QObject*, QEvent*) MainWindow.cc:1619
        QCoreApplicationPrivate::sendThroughObjectEventFilters
        QObjectPrivate::setParent_helper
        QObject::~QObject
  FREED ~RubberBandManager()                       RubberBandManager.h:13
        MainWindow::~MainWindow()                  MainWindow.cc:627
  ALLOC QRubberBand ctor, RubberBandManager::RubberBandManager(MainWindow*) RubberBandManager.cc:6

The fix

~MainWindow already severs the dock connections and dock event filters for exactly this reason, with
a comment describing the hazard. That fix simply missed the two filters the window installs on itself:

removeEventFilter(this);
if (find_panel) find_panel->removeEventFilter(this);

Guarding inside eventFilter() instead would leave the re-entrancy in place and require a
"being destroyed" flag; removing the filters ends it at the source and matches the existing code.

How this was found

Running the GUI test suite under AddressSanitizer while investigating an unrelated test-infrastructure
issue. It is independent of that work: it reproduces on master as soon as a test closes a window.

Testing

  • New TestMainWindow::checkClosingWindowDoesNotUseFreedMembers(): opens a second window, closes it,
    and checks the window count returns to its previous value. It passes in an ordinary build — the
    failure it exists to catch is the sanitizer report — so it was confirmed failing first: before the
    fix ASan reported the use-after-free with both the allocation and the free traced into this test's
    own lines; after the fix, --run-all-gui-tests is ASan-clean with 0 failures.
  • ASan configuration: -DENABLE_GUI_TESTS=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo,
    -fsanitize=address -fno-omit-frame-pointer -g, run with
    ASAN_OPTIONS=detect_stack_use_after_return=1:detect_leaks=0.
  • ctest -j8 on macOS: 2758/2761, the only failures being the 3 known SVG arc tessellation tests that
    also fail on master.
  • ./scripts/beautify.sh --check clean with clang-format 18.1.8.

User documentation (for the openscad.org wiki)

Closing a window no longer reads memory belonging to parts of that window that have already been torn
down. The effect was not reliably visible — usually nothing appeared to go wrong, since the freed
memory was typically still intact — but it could produce a crash on window close. No setting or
workflow changes.

🤖 Generated with Claude Code

whosawhatsis and others added 2 commits September 15, 2026 19:29
MainWindow stays installed as an event filter while its own members are
being destroyed, so MainWindow::eventFilter() can run against a
destroyed member and read freed memory. It is silent in an ordinary
build and a heap-use-after-free under AddressSanitizer.

The test opens a second window, closes it, and checks the window count
returns to what it was. It passes in an ordinary build and fails under
AddressSanitizer until the event filter is removed before the members
are torn down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MainWindow installs itself as an event filter on itself and on the find
panel. Destroying a member such as rubberBandManager reparents the
widget it owns, and reparenting delivers events, which re-enter
MainWindow::eventFilter() while that member is being destroyed.
eventFilter() calls rubberBandManager.isVisible(), so it reads freed
memory: a heap-use-after-free under AddressSanitizer, and a silent read
of stale memory in an ordinary build.

The destructor already severs the dock connections and filters for
exactly this reason; do the same for the two filters we install on
ourselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jordanbrown0

Copy link
Copy Markdown
Contributor

@t-paul

t-paul commented Sep 16, 2026

Copy link
Copy Markdown
Member

I don't think this is the correct solution for this case. The RubberBandManager is a QObject, so I believe this should be created via new and handed to Qt which handles the life cycle.

@whosawhatsis

Copy link
Copy Markdown
Contributor Author

Not with Qt's public API, unfortunately. There is no removeEventFilters(), and no way to
enumerate either direction of the relationship: the filters installed on an object live in
QObjectPrivate::eventFilters, and an object that has installed itself elsewhere keeps no list of
where. So "remove all of mine" can only be spelled out target by target.

Qt does clean this up automatically — but in ~QObject, which for MainWindow runs after its
members are destroyed. That gap is exactly where this bug lives: ~MainWindow destroys
rubberBandManager, the reparenting that causes delivers events, and the filter is still installed
at that moment because the base-class destructor has not run yet.

MainWindow installs itself in three places — on itself, on each dock, and on the find panel. The
destructor already removed the dock filters for this same reason; this PR adds the two that were
missed, so the set is now complete.

If you would rather it not depend on that set staying complete, the small version is to route the
installs through a helper that records its targets and removes them in its own destructor, with the
helper declared last in MainWindow so it is destroyed first:

class EventFilterGuard {
public:
  explicit EventFilterGuard(QObject *filter) : filter_(filter) {}
  void install(QObject *target) { target->installEventFilter(filter_); targets_.append(target); }
  ~EventFilterGuard() { for (auto& t : targets_) if (t) t->removeEventFilter(filter_); }
private:
  QObject *filter_;
  QList<QPointer<QObject>> targets_;
};

That makes a future installEventFilter(this) unable to reintroduce this class of bug, which the
current fix does not. I left it out to keep the bugfix minimal and reviewable, but I am happy to add
it here or as a follow-up — whichever you prefer.

@jordanbrown0

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants