[macOS] Logical memory leak in Cocoa backend (wxStaticText, wxStaticBitmap, wxScrolledWindow) confirmed via malloc statistics (Issue #26208)

15 views
Skip to first unread message

szigand

unread,
Feb 19, 2026, 10:49:27 AM (yesterday) Feb 19
to wx-...@googlegroups.com, Subscribed
szigand created an issue (wxWidgets/wxWidgets#26208)

I am reporting a persistent logical memory leak (unintentional object retention) on macOS that affects several common widgets, including wxStaticText, wxStaticBitmap, and wxScrolledWindow.

Even when Destroy() is called and the application enters an idle state, the memory associated with these widgets is not reclaimed. Using the Xcode Memory Graph, the root cause appears to be that the underlying Cocoa objects are being held by the NSNotificationCenter, preventing their deallocation.

Environment:

wxWidgets Version: 3.2.9 and current master branch.

Operating System: macOS 11 through macOS 26.

Backend: wxOSX/Cocoa.

Observed Behavior:
The memory consumption increases linearly with every creation/destruction cycle. To rule out delayed destruction issues, the attached reproduction code includes a timer with "rest periods." During these pauses, the event loop is free to process pending deletions, yet the size_in_use (measured via malloc_zone_statistics) never drops.

Technical Analysis:
The issue behaves like a Lapsed Listener Problem. Each widget instance seems to register for notifications or observers that are not fully cleared upon destruction, causing the native NSView (and its peer) to be retained indefinitely.

Run the attached minimal sample.

**

wxMemoryLeak.cpp

**

Observe the "In use" memory counter displayed on the window.

Note that the memory increases when widgets are created, but does not decrease when they are destroyed and the cycle pauses.

Screenshot.2026-02-19.at.16.48.00.png (view on web)


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/issues/26208@github.com>

VZ

unread,
Feb 19, 2026, 11:35:17 AM (yesterday) Feb 19
to wx-...@googlegroups.com, Subscribed
vadz left a comment (wxWidgets/wxWidgets#26208)

Thanks for reporting this but I have no idea why does it happen, these controls don't use NSNotificationCenter explicitly, so I don't see how could they remove themselves from it.

AFAICS wxNSStaticTextView should be destroyed when CFRelease() is called on it in wxWidgetCocoaImpl dtor. It would be nice to confirm that this dtor is indeed executed and calls CFRelease() as it should.

I also think there was some way to track reference count changes for the given object, but I don't remember it was... If anybody knows it, it would be interesting to use this to check what happens to the wxNSStaticTextView instance.


Reply to this email directly, view it on GitHub, or unsubscribe.

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

szigand

unread,
Feb 19, 2026, 11:59:43 AM (yesterday) Feb 19
to wx-...@googlegroups.com, Subscribed
szigand left a comment (wxWidgets/wxWidgets#26208)

wxWidgetCocoaImpl::~wxWidgetCocoaImpl()
{
if ( GetWXPeer() && GetWXPeer()->IsFrozen() )
SetDrawingEnabled(true);

RemoveAssociations( this );

if ( !IsRootControl() )
{
    NSView *sv = [m_osxView superview];
    if ( sv != nil )
        [m_osxView removeFromSuperview];
}

[m_osxView release];
m_osxView = nil;

wxCocoaGestures::EraseForObject(this);

}

Yes, I have debugged this dtor, it calls [m_osxView removeFromSuperview] and then [m_osxView release].

As far as I can see, the Cocoa objects register themselves to NSNotificationCenter when their initWithFrame: is called as it can be seen on the attached screenhot. The NSMallocBlock is created by NSNotificationCenter in - [NSTextField initWithFrame:]. However, when the C++ dtor releases wxNSStaticTextView, NSNotificationCenter still holds the reference to this object.

Screenshot.2026-02-19.at.17.43.25.png (view on web)

I don't know Cocoa in depth, I don't know what else needs to be done for NSNotificationCenter to release these objects, which is why I'm sending this bug report.


Reply to this email directly, view it on GitHub, or unsubscribe.

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

szigand

unread,
12:34 PM (10 hours ago) 12:34 PM
to wx-...@googlegroups.com, Subscribed
szigand left a comment (wxWidgets/wxWidgets#26208)

I managed to fix it. As it turned out, the destructor I copied here was already a patched version, I just had forgotten. I had modified it a long time ago because I was having problems with the original code, where Cocoa and CoreFoundation retain/release were mixed. However, that patch increased the reference count by one, so the objects were never released.

This current code now works properly:

wxWidgetCocoaImpl::wxWidgetCocoaImpl( wxWindowMac* peer , WXWidget w, int flags ) :
wxWidgetImpl( peer, flags )
{
    Init();
    m_osxView = w;
    m_osxClipView = nil;

    // check if the user wants to create the control initially hidden
    if ( !peer->IsShown() )
        SetVisibility(false);
    
    m_osxView.clipsToBounds = YES;
}

wxWidgetCocoaImpl::~wxWidgetCocoaImpl()
{
    if ( GetWXPeer() && GetWXPeer()->IsFrozen() )
        SetDrawingEnabled(true);
    
    RemoveAssociations( this );

    if ( !IsRootControl() )
    {
        NSView *sv = [m_osxView superview];
        if ( sv != nil )
            [m_osxView removeFromSuperview];
    }

    [m_osxView release];
    m_osxView = nil;

    if(m_osxClipView != nil)
    {
        [m_osxClipView release];
        m_osxClipView = nil;
    }
    
    wxCocoaGestures::EraseForObject(this);
}


Reply to this email directly, view it on GitHub, or unsubscribe.

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

VZ

unread,
12:57 PM (10 hours ago) 12:57 PM
to wx-...@googlegroups.com, Subscribed
vadz left a comment (wxWidgets/wxWidgets#26208)

Thanks, but this is still not very clear: there seem to be 2 changes, first you seem to have removed this code

    if ( m_osxView )
        CFRetain(m_osxView);
    [m_osxView release];

but this doesn't really seem to change anything, does it?

And, second, you've added this:

    if(m_osxClipView != nil)
    {
        [m_osxClipView release];
        m_osxClipView = nil;
    }

which is probably the real fix, right? I.e. it's m_osxClipView which is getting leaked?


Reply to this email directly, view it on GitHub, or unsubscribe.

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

szigand

unread,
1:31 PM (9 hours ago) 1:31 PM
to wx-...@googlegroups.com, Subscribed
szigand left a comment (wxWidgets/wxWidgets#26208)

You are correct that these are two separate issues addressed:

  1. Switching from CoreFoundation to Objective-C messaging:
    I removed the CFRetain/CFRelease calls on m_osxView because they are not perfectly equivalent to Objective-C retain/release on modern macOS.

The issue: NSView is not officially "Toll-Free Bridged" to any Core Foundation type.

The consequence: While CFRelease decrements the reference count, it does so at a lower level, bypassing the standard Objective-C runtime hooks. On modern AppKit, [release] is the expected way to trigger the full deallocation sequence. Relying on CFRelease for NSObject types can lead to subtle issues where internal runtime finalizers are not properly executed. Transitioning to native messages makes the code more robust and compliant with current macOS standards.

  1. The missing cleanup for m_osxClipView:
    Yes, this is a separate, direct fix. The original code completely lacked the release of m_osxClipView in the destructor, which led to a straightforward memory leak whenever a clip view was involved.

Summary:
The patch addresses both a methodological risk (by using the correct runtime messages for NSView) and a concrete leak (by adding the missing m_osxClipView cleanup).

(I asked Google Gemini to help, it speaks English better than me, I hope it is not a problem)

This was the patched code which caused the problem that I described in my first post, I did not realize that it was not your original code:

wxWidgetCocoaImpl::wxWidgetCocoaImpl( wxWindowMac* peer , WXWidget w, int flags ) :
wxWidgetImpl( peer, flags )
{
    Init();
    m_osxView = [w retain];
    m_osxClipView = nil;

    // check if the user wants to create the control initially hidden
    if ( !peer->IsShown() )
        SetVisibility(false);
    
    m_osxView.clipsToBounds = YES;
}

wxWidgetCocoaImpl::~wxWidgetCocoaImpl()
{
    if ( GetWXPeer() && GetWXPeer()->IsFrozen() )
        SetDrawingEnabled(true);
    
    RemoveAssociations( this );

    if ( !IsRootControl() )
    {
        NSView *sv = [m_osxView superview];
        if ( sv != nil )
            [m_osxView removeFromSuperview];
    }

    [m_osxView release];
    m_osxView = nil;

    wxCocoaGestures::EraseForObject(this);
}

I ran into a problem years ago with the CoreFoundation mode releasing that the original code had, but I don't remember what it was exactly. This patch helped me that times, but that [w retain] caused the memory leak I talked about, that I realized only a few days ago. But it was my fault. However the change to the Objective-C mode release from the CoreFoundation one seems to be a good idea as we do not use Garbage Collection any more.


Reply to this email directly, view it on GitHub, or unsubscribe.

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

VZ

unread,
3:51 PM (7 hours ago) 3:51 PM
to wx-...@googlegroups.com, Subscribed
vadz left a comment (wxWidgets/wxWidgets#26208)

(I asked Google Gemini to help, it speaks English better than me, I hope it is not a problem)

I don't know if it's a problem or not, but I don't trust anything LLM-generated without verifying it. In this case, I have serious doubts about whether there is any real difference between CFRelease(m_osxView) and [m_osxView release].

I admit that I don't understand why do we need

    if ( m_osxView )
        CFRetain(m_osxView);
    [m_osxView release];

either, as it looks pointless, but at least this was written by @csomor who might still remember why it was done and doesn't seem to do any real harm.

In any case, could you please check if applying only this change to unmodified wx 3.2.9 sources fixes the memory leak you observed?

diff --git a/src/osx/cocoa/window.mm b/src/osx/cocoa/window.mm
index 777dfa077e..e15ac0159a 100644
--- a/src/osx/cocoa/window.mm
+++ b/src/osx/cocoa/window.mm
@@ -2645,6 +2645,9 @@ wxWidgetCocoaImpl::~wxWidgetCocoaImpl()
     if ( m_osxView )
         CFRelease(m_osxView);
 
+    if ( m_osxClipView )
+        CFRelease(m_osxClipView);
+
     wxCocoaGestures::EraseForObject(this);
 }
 

I'd like to do it before the upcoming 3.3.2 and 3.2.10 releases.

Stefan, please let me know if you see anything wrong with this.

TIA!


Reply to this email directly, view it on GitHub, or unsubscribe.

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

Stefan Csomor

unread,
4:13 PM (6 hours ago) 4:13 PM
to wx-...@googlegroups.com, Subscribed
csomor left a comment (wxWidgets/wxWidgets#26208)

@vadz I'll look into it, thanks


Reply to this email directly, view it on GitHub, or unsubscribe.

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

Stefan Csomor

unread,
4:56 PM (6 hours ago) 4:56 PM
to wx-...@googlegroups.com, Subscribed
csomor left a comment (wxWidgets/wxWidgets#26208)

parts of the code (CF calls) were introduced way back when to support gc mode when using wx as shared lib in a host app, these can be removed as gc was removed from OSX


Reply to this email directly, view it on GitHub, or unsubscribe.

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

Reply all
Reply to author
Forward
0 new messages