Skip to content

fix(macos): drain autoreleased CoreWLAN objects in get_wifi_transmit_rate#165

Merged
shellrow merged 1 commit into
shellrow:mainfrom
agent-habilis:fix/macos-corewlan-autorelease-leak
Jun 3, 2026
Merged

fix(macos): drain autoreleased CoreWLAN objects in get_wifi_transmit_rate#165
shellrow merged 1 commit into
shellrow:mainfrom
agent-habilis:fix/macos-corewlan-autorelease-leak

Conversation

@caiogondim

Copy link
Copy Markdown
Contributor

Problem

On macOS, get_wifi_transmit_rate (src/os/macos/wifi.rs, called from
interface() to populate transmit_speed) queries CoreWLAN —
CWWiFiClient::sharedWiFiClient()interfaceWithNametransmitRate
without an enclosing autorelease pool. Those calls produce autoreleased
Obj-C / XPC objects.

When netdev is driven from a long-lived thread that has no run loop and no
draining autorelease pool, those objects are added to a top-level pool that
never drains, so they accumulate without bound. This happens in practice via
netwatch (and thus iroh), whose
interface monitor calls get_interfaces() on every network change.

Evidence

heap <pid> on a ~7h process using netdev through netwatch — CoreWLAN /
CoreWiFi objects dominate the heap (~73 MB):

count bytes class
298,213 33.4 MB CWFRequestParameters (CoreWiFi)
223,658 17.9 MB __NSXPCInterfaceProxy_CWFXPCRequestProtocolCoreWLAN
74,555 3.6 MB CWFInterface (CoreWiFi)

RSS climbed ~18 MB/h, linear, no plateau. Linux hosts running the same software
were flat (their path is sysfs, no CoreWLAN), confirming the source.

Fix

Wrap the body in objc2::rc::autoreleasepool so each call drains its
autoreleased objects immediately (adds objc2 to the macOS deps for the pool
API). Behavior-preserving — the returned rate is unchanged; only the
autoreleased temporaries are freed per call.

Validation

Same software, only the patch differs:

  • Before: CWFRequestParameters → 298,213 over 7h (~18 MB/h RSS climb).
  • After: CWFRequestParameters pinned at 3, RSS flat (held 30+ min on a
    multi-host fleet, plus a separate long local run).

Reproducing

A unit test doesn't reproduce it — a synchronous in-process loop frees the
objects promptly (measured 0 growth over 100k calls); the leak is specific to
the async, threaded, over-time CoreWLAN XPC traffic on a pool-less long-lived
thread. To see it by hand on macOS: run a long-lived process that drives
netdev::get_interfaces() from a background monitor (e.g. via netwatch /
iroh) and watch heap <pid> | grep CWFRequestParameters climb; with the patch
it stays at a small constant.

get_wifi_transmit_rate queries CoreWLAN (CWWiFiClient/interfaceWithName/
transmitRate), which produces autoreleased Obj-C and XPC objects. netdev is
called from long-lived threads with no run loop or draining autorelease pool —
e.g. netwatch's interface monitor re-enumerates on every network event — so
those objects are added to a top-level pool that never drains and accumulate
without bound. On a live macOS process this leaked ~300K CoreWLAN objects in 7h
(~18 MB/h RSS climb); Linux was unaffected since its path uses sysfs. Wrapping
the query in objc2::rc::autoreleasepool drains the temporaries per call.
Keeps the object count flat; adds objc2 to the macOS deps for the pool API.
@shellrow

shellrow commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Thanks for the great fix and detailed investigation.

The change looks good. The only CI failure is a rustfmt issue, which I'll take care of on my side before the next release.

@shellrow shellrow merged commit 6761f95 into shellrow:main Jun 3, 2026
9 of 10 checks passed
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.

2 participants