NSButton added/removed in a window leaks ~12 NSKeyValueDependency + block objects per button (macOS 26.5.1)

Adding and removing NSButton (any NSControl) in a window leaks AppKit-internal objects. A plain NSView does not.

Environment: macOS 26.5.1 (25F80), Apple silicon (arm64), Swift, swiftc -O, no third-party code.

Repro (single file):

import AppKit
func mb()->Int{ var i=task_vm_info_data_t(); var c=mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size/4); withUnsafeMutablePointer(to:&i){ $0.withMemoryRebound(to:integer_t.self,capacity:1){ _=task_info(mach_task_self_,task_flavor_t(TASK_VM_INFO),$0,&c)}}; return Int(i.phys_footprint/1048576)}
let app = NSApplication.shared; app.setActivationPolicy(.accessory)
let mode=CommandLine.arguments[1]
let win=NSWindow(contentRect:NSRect(x:100,y:100,width:400,height:100),styleMask:[.titled],backing:.buffered,defer:false)
let stack=NSStackView(); win.contentView=stack; win.orderFrontRegardless()
var n=0
print(mode,"start",mb()); fflush(stdout)
Timer.scheduledTimer(withTimeInterval:0.002,repeats:true){ t in
  autoreleasepool {
    for v in stack.arrangedSubviews { stack.removeArrangedSubview(v); v.removeFromSuperview() }
    for _ in 0..<10 {
      let b: NSView = mode=="view" ? NSView(frame:.zero) : NSButton(title:"", target:nil, action:nil)
      if mode=="plainadd" { win.contentView!.addSubview(b); b.removeFromSuperview() } else { stack.addArrangedSubview(b) }
    }
  }
  n+=1
  if n%500==0 { print(mode,"iter",n*10,mb()); fflush(stdout) }
  if n>=2500 { t.invalidate() }
}
app.run()
swiftc -O repro.swift -o tw
./tw button &   # then: heap $(pgrep -x tw) -s | grep -E "NSKeyValueDependency |__NSMallocBlock__|NSButton "
./tw view   &   # control case: plain NSView

Results (25,000 buttons created and removed; only 10 alive at a time):

NSMallocBlockmodefootprint (MB) at 5k / 15k / 25kNSKeyValueDependency
button (NSButton in NSStackView)26 / 43 / 60 (steady growth)292,621251,145
view (plain NSView)flat at 13172n/a

heap shows only 10 live NSButton instances, so the buttons themselves are freed. leaks reports no leaks, so AppKit still holds references to the objects. That is about 12 objects (~3 KB) per button.

Allocation stack (MallocStackLogging=1 + malloc_history -callTree NSKeyValueDependency, taken on my real app):

-[NSStackView insertArrangedSubview:atIndex:]
 -[NSView addSubview:]
  -[NSView _setSuperview:]
   -[NSView(NSViewRoot) _updateViewRoot]
    -[NSControl _viewRootDidChange]
     -[NSControl _setupGestureRecognizers]
      __cons_NSDP_NSDPTypeBool_block_invoke.273
       NSDP_getComputedPropertyValue
        -[NSKeyValueDependencyInfo addDependencyInContext:onValueForKey:]
         _objc_rootAllocWithZone

The allocation happens when an NSControl is added to a view hierarchy that has a window (_viewRootDidChange → _setupGestureRecognizers). The KVO dependency objects are never released after the control is removed and deallocated.

Real-world impact: an app rebuilt about 40 buttons every 5 s. That leaked about 40 objects per second (~16 MB/hour). After 19 days it reached 7.2 GB footprint, mostly small malloc blocks, with ~1.4M NSKeyValueDependency objects and ~1.4M blocks.

Questions:

  1. Is this a known issue? Is there a Feedback number?
  2. Is there a supported way to avoid or release these dependency objects, other than not recreating NSControls?
  3. Does it reproduce on macOS 26.x or 27 betas?

Workaround for now: reuse buttons and only rebuild when state actually changes.