wxFsEventsFileSystemWatcher destructor leaks live FSEvents streams -> use-after-free crash (macOS) (Issue #26658)

8 views
Skip to first unread message

Juraj Kavka

unread,
Jul 5, 2026, 10:40:52 AM (3 days ago) Jul 5
to wx-...@googlegroups.com, Subscribed
JurajKavka created an issue (wxWidgets/wxWidgets#26658)

Disclaimer: This issue was identified with the help of an AI model
(Claude Fable 5, via Claude Code) while I was working on my C++ markdown
reader. I observed the segfaults while testing the app; the root-cause
analysis below (destructor missing stream cleanup, confirmed against the
wxWidgets sources for 3.2.8 and master) was done by the model. I have
verified the workaround fixes the crash in my application.

Description

On macOS, destroying a wxFileSystemWatcher that has active watches created with
AddTree() leaves the underlying FSEvents streams alive and scheduled on the run
loop. Their callback context (wxFSEventWatcherContext) holds a pointer to the
destroyed watcher, so the next filesystem event in a previously watched tree calls
wxFSEventCallback -> wxFsEventsFileSystemWatcher::PostChange() on freed memory
and crashes with EXC_BAD_ACCESS (on Apple Silicon typically reported as a possible
pointer authentication failure).

This is easy to hit in practice: a frame owns a watcher, watches a directory tree
with AddTree(), the user closes the window, and any file activity in that tree
shortly afterwards crashes the app during shutdown.

Analysis

In src/osx/fswatcher_fsevents.cpp (unchanged from 3.2.8 through current master):

wxFsEventsFileSystemWatcher::~wxFsEventsFileSystemWatcher()
{
    delete m_pImpl;   // m_pImpl->m_streams holds FSEventStreamRef entries;
}                     // no FSEventStreamStop/Invalidate/Release is done

RemoveTree() and RemoveAll() do clean up correctly
(FSEventStreamStop + FSEventStreamInvalidate + FSEventStreamRelease), but the
destructor never calls them. The base class destructor cannot compensate: any
RemoveAll() call from ~wxFileSystemWatcherBase resolves to the base
implementation, not the FSEvents override, because the derived part of the object
is already gone by then.

Per Apple's documentation, after FSEventStreamInvalidate() returns the callback
is guaranteed not to be invoked again; since the streams are never invalidated,
the run loop keeps a callback registered whose context points into freed memory.

Suggested fix

Clean up the streams in the destructor, e.g.:

wxFsEventsFileSystemWatcher::~wxFsEventsFileSystemWatcher()
{
    RemoveAll(); // stops, invalidates and releases all m_streams entries
    delete m_pImpl;
}

(or inline the Stop/Invalidate/Release loop from RemoveAll()).

Steps to reproduce

  1. On macOS, create a wxFileSystemWatcher (heap or member) and call
    AddTree(someDirectory).
  2. Destroy the watcher (e.g. close the frame that owns it) without calling
    RemoveAll() first.
  3. Create/modify/delete files under someDirectory while the event loop is still
    running (e.g. during app shutdown, or in any app that outlives the watcher).
  4. Crash in wxFsEventsFileSystemWatcher::PostChange.

Crash stack (top frames)

Exception Type:    EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x036535201a3d9ebd (possible pointer authentication failure)

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0  libwx_baseu-3.2.0.4.1.dylib  wxFsEventsFileSystemWatcher::PostChange(wxFileName const&, wxFileName const&, int) + 116
1  libwx_baseu-3.2.0.4.1.dylib  wxFSEventCallback(__FSEventStream const*, void*, unsigned long, void*, unsigned int const*, unsigned long long const*) + 608
2  FSEvents                     implementation_callback_rpc + 3236
3  FSEvents                     _Xcallback_rpc + 220
4  FSEvents                     FSEventsD2F_server + 68
...

Workaround

Call RemoveAll() on the watcher while it is still alive (e.g. in the owning
frame's destructor) before the watcher itself is destroyed.

Platform and version

  • wxWidgets 3.2.8 (Homebrew), bug also present in current master
  • macOS 26.5.1, Apple Silicon (arm64)
  • osx_cocoa port


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/26658@github.com>

VZ

unread,
Jul 5, 2026, 11:31:56 AM (3 days ago) Jul 5
to wx-...@googlegroups.com, Subscribed
vadz left a comment (wxWidgets/wxWidgets#26658)

Thanks, this seems to make sense and calling RemoveAll() shouldn't do any harm, so I'll merge this.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/26658/4886569916@github.com>

VZ

unread,
Jul 5, 2026, 12:03:00 PM (3 days ago) Jul 5
to wx-...@googlegroups.com, Subscribed

Closed #26658 as completed via bc73166.


Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issue/26658/issue_event/27576312912@github.com>

Reply all
Reply to author
Forward
0 new messages