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.
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.
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.
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()).
wxFileSystemWatcher (heap or member) and callAddTree(someDirectory).RemoveAll() first.someDirectory while the event loop is stillwxFsEventsFileSystemWatcher::PostChange.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
...
Call RemoveAll() on the watcher while it is still alive (e.g. in the owning
frame's destructor) before the watcher itself is destroyed.
—
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.![]()
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.![]()
—
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.![]()