Skip to content

fix(connectivity_plus): guard eventSink after engine teardown (iOS)#3797

Open
friebetill wants to merge 1 commit intofluttercommunity:mainfrom
friebetill:fix/connectivity-ios-eventsink-weak-self
Open

fix(connectivity_plus): guard eventSink after engine teardown (iOS)#3797
friebetill wants to merge 1 commit intofluttercommunity:mainfrom
friebetill:fix/connectivity-ios-eventsink-weak-self

Conversation

@friebetill
Copy link
Copy Markdown

@friebetill friebetill commented Apr 15, 2026

Summary

On iOS, connectivity_plus can crash the host app with SIGABRT when NWPathMonitor fires a connectivity update while the app is in the background and the FlutterEngine's shell has already been torn down. The pending DispatchQueue.main.async block still invokes the stale FlutterEventSink, which trips an NSAssert in -[FlutterEngine sendOnChannel:message:binaryReply:] and aborts the process.

This patch guards the main-queue emission with a weak self, a local eventSink copy, and an UIApplication.applicationState != .background check, so pending updates are dropped instead of being forwarded into a dead engine.

Observed in production (Sentry) on iOS 18.7.7 across three separate crashes in one week, all with the same stack and Role: Non UI in the crash report.

Root cause

ConnectivityPlusPlugin.connectivityUpdateHandler hops to the main queue before emitting the path update:

private func connectivityUpdateHandler(connectivityTypes: [ConnectivityType]) {
  DispatchQueue.main.async {
    self.eventSink?(self.statusFrom(connectivityTypes: connectivityTypes))
  }
}

When the app is backgrounded, the Flutter engine's _shell may already have been destroyed by the time the block runs. self.eventSink is still non-nil (the engine hasn't finished detaching), so the call reaches -[FlutterEngine sendOnChannel:…], which asserts at FlutterEngine.mm:1315 and aborts.

detachFromEngine nil-ing eventSink is not a reliable fence here: it is not guaranteed to run before the pending main-queue block executes, and there is a race window between engine shutdown and the detach callback.

Fix

  1. Capture self weakly in the DispatchQueue.main.async block, so the plugin can be released cleanly during teardown.
  2. Copy eventSink into a local constant inside the block and bail out if it is nil.
  3. Bail out if UIApplication.shared.applicationState == .background. The engine's shell can be torn down while backgrounded, and forwarding events there is unsafe. The next onListen / check call after foregrounding will resync state, so no information is lost.

Only the iOS plugin source is touched. The macOS plugin lives in a separate file and is unaffected.

 import Flutter
+import UIKit

   private func connectivityUpdateHandler(connectivityTypes: [ConnectivityType]) {
-    DispatchQueue.main.async {
-      self.eventSink?(self.statusFrom(connectivityTypes: connectivityTypes))
+    DispatchQueue.main.async { [weak self] in
+      guard let self = self, let eventSink = self.eventSink else { return }
+      guard UIApplication.shared.applicationState != .background else { return }
+      eventSink(self.statusFrom(connectivityTypes: connectivityTypes))
     }
   }

Stack trace

Exception Type:  EXC_CRASH (SIGABRT)
Termination Reason: SIGNAL 6 Abort trap: 6
Role: Non UI

0  CoreFoundation                __exceptionPreprocess
1  libobjc.A.dylib               objc_exception_throw
2  Foundation                    -[NSAssertionHandler handleFailureInMethod:…]
3  Flutter                       -[FlutterEngine sendOnChannel:message:binaryReply:] + 348 (FlutterEngine.mm:1315)
4  Flutter                       -[FlutterBinaryMessengerRelay sendOnChannel:message:] + 136
5  Flutter                       invocation function for block in SetStreamHandlerMessageHandlerOnChannel(…)
6  connectivity_plus             thunk for @escaping @callee_unowned @convention(block) (@unowned Swift.AnyObject?) -> ()
7  connectivity_plus             closure #1 in ConnectivityPlusPlugin.connectivityUpdateHandler(connectivityTypes:)
8  libdispatch.dylib             _dispatch_call_block_and_release

Related issues / PRs

Testing

No automated regression test. The crash is a race during engine teardown and I don't have a reliable way to reproduce it in the plugin's unit tests.

Manually validated:

  1. Run an app that subscribes to Connectivity().onConnectivityChanged on an iOS device.
  2. Send the app to the background (home gesture).
  3. Toggle airplane mode a few times while backgrounded.
  4. Bring the app back to the foreground.

Before the fix: reproducible SIGABRT with the stack above within a few toggles. After the fix: no crash; on foreground, the subscription delivers the current connectivity state via the existing checkConnectivity / onListen flow.

…n on iOS

NWPathMonitor can fire a connectivity update while the app is in the
background, after the FlutterEngine's shell has been torn down. The
DispatchQueue.main.async block then invokes the stale FlutterEventSink,
which calls -[FlutterEngine sendOnChannel:] and hits an NSAssertion at
FlutterEngine.mm:1315, aborting the process with SIGABRT.

Guard the main-queue emission with [weak self], a local eventSink copy,
and a UIApplication.applicationState != .background check so that
pending updates are dropped instead of propagated into a dead engine.
The next onListen / check call after foregrounding will resync state.
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.

1 participant