Skip to content

Crash during discovery on iOS 26 when services change #1118

Description

@sjp4

Steps to reproduce:

  • Connect + pair to a peripheral, from an iOS 26 device
  • Update firmware on the peripheral so that it now has a new characteristic in an existing service
  • Reconnect to peripheral - this will trigger the onServiceChanged flow because of the new characteristic
  • It seems that on iOS 26, discoverDescriptorsForCharacteristic will throw an error if it has already been called for the characteristic (behaviour change in this iOS version?). This crashes the app.

It seems like this essentially means the service table can't change on the peripheral without causing an iOS crash (we got some crash reports like this after testing a firmware update internally which added a characteristic, so we rolled that firmware change back)

Potential fix:

I don't have an iOS 26 device to test with right now so haven't been able to try this - Claude suggests that in Connection.kt/discoverServices, when serviceUuids is provided (service-change path), we should only process
those specific services, not all services.

diff --git a/kable-core/src/appleMain/kotlin/Connection.kt b/kable-core/src/appleMain/kotlin/Connection.kt
--- a/kable-core/src/appleMain/kotlin/Connection.kt
+++ b/kable-core/src/appleMain/kotlin/Connection.kt
@@ -96,7 +96,17 @@
         // https://developer.apple.com/documentation/corebluetooth/cbperipheral/services
         @Suppress("UNCHECKED_CAST")
-        val discoveredServices = peripheral.services as List<CBService>?
+        val allServices = peripheral.services as List<CBService>?
+
+        // When serviceUuids is provided (service-change path), only process the re-discovered
+        // services. Re-discovering characteristics/descriptors for non-invalidated services is
+        // unnecessary and causes a crash on iOS 26 when calling discoverDescriptorsForCharacteristic
+        // on characteristics that already have their descriptors discovered.
+        val servicesToProcess = if (serviceUuids == null) {
+            allServices
+        } else {
+            allServices?.filter { service -> serviceUuids.any { it.isEqual(service.UUID) } }
+        }
 
-        discoveredServices?.forEach { service ->
+        servicesToProcess?.forEach { service ->
             execute<DidDiscoverCharacteristicsForService> {
                 peripheral.discoverCharacteristics(null, service)

Crash report:

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Reason: -[CBService handle]: unrecognized selector sent to instance 0x113643140
Termination Reason: SIGNAL 6 Abort trap: 6
Terminating Process: Pebble [27589]

Triggered by Thread:  1

Last Exception Backtrace:
0   CoreFoundation                	0x195664964 __exceptionPreprocess + 164 (NSException.m:249)
1   libobjc.A.dylib               	0x192579814 objc_exception_throw + 88 (objc-exception.mm:356)
2   CoreFoundation                	0x1956ff2c0 -[NSObject(NSObject) doesNotRecognizeSelector:] + 384 (NSObject.m:168)
3   CoreFoundation                	0x1955e435c ___forwarding___ + 1472 (NSForwarding.m:3616)
4   CoreFoundation                	0x1955ec200 _CF_forwarding_prep_0 + 96
5   CoreBluetooth                 	0x1d3667e68 -[CBPeripheral discoverDescriptorsForCharacteristic:] + 212 (CBPeripheral.m:588)
6   ComposeApp                    	0x10482c65c kfun:com.juul.kable.Connection.Connection$discoverServices$$inlined$forEach$2.invoke#internal + 284 (_Collections.kt:151)
7   ComposeApp                    	0x10482c65c kfun:com.juul.kable.Connection.Connection$discoverServices$$inlined$forEach$2.$<bridge-DNN>invoke(platform.CoreBluetooth.CBPeripheral){}#internal + 328
8   ComposeApp                    	0x10482c808 kfun:kotlin.Function1#invoke(1:0){}1:1-trampoline + 76 (Mutex.kt:114)
9   ComposeApp                    	0x10482c808 kfun:com.juul.kable.Connection.Connection$execute$$inlined$withLock$1.invoke#internal + 184
10  ComposeApp                    	0x10462680c kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 40 (IntrinsicsNative.kt:254)
11  ComposeApp                    	0x10462680c kfun:kotlin.coroutines.intrinsics#startCoroutineUninterceptedOrReturn__at__kotlin.coroutines.SuspendFunction1<0:0,0:1>(0:0;kotlin.coroutines.Continuation<0:1>){0§<kotlin.Any?>;1§<kotlin.Any?>}kotli... + 204
12  ComposeApp                    	0x10462680c <inlined-lambda> + 208
13  ComposeApp                    	0x10462680c kfun:kotlin.coroutines.intrinsics.createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$2.invokeSuspend#internal + 380
14  ComposeApp                    	0x104624adc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 28 (ContinuationImpl.kt:26)
15  ComposeApp                    	0x104624adc <inlined-lambda> + 44
16  ComposeApp                    	0x104624adc kfun:kotlin#with(0:0;kotlin.Function1<0:0,0:1>){0§<kotlin.Any?>;1§<kotlin.Any?>}0:1 + 44
17  ComposeApp                    	0x104624adc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 176
18  ComposeApp                    	0x104779780 kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 72 (DispatchedTask.kt:82)
19  ComposeApp                    	0x104779780 kfun:kotlin.coroutines#resume__at__kotlin.coroutines.Continuation<0:0>(0:0){0§<kotlin.Any?>} + 72
20  ComposeApp                    	0x104779780 <inlined-lambda> + 708
21  ComposeApp                    	0x104779780 kfun:kotlinx.coroutines#withContinuationContext(kotlin.coroutines.Continuation<*>;kotlin.Any?;kotlin.Function0<0:0>){0§<kotlin.Any?>}0:0 + 708
22  ComposeApp                    	0x104779780 kfun:kotlinx.coroutines.DispatchedTask#run(){} + 908
23  ComposeApp                    	0x104834370 kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 68 (QueueDispatcher.kt:17)
24  ComposeApp                    	0x104834370 kfun:com.juul.kable.QueueDispatcher.QueueDispatcher$dispatch$1.invoke#internal + 72
25  ComposeApp                    	0x104834370 kfun:com.juul.kable.QueueDispatcher.QueueDispatcher$dispatch$1.$<bridge-DN>invoke(){}#internal + 100
26  ComposeApp                    	0x1065503d8 ___636f6d2e6a75756c2e6b61626c653a6b61626c652d636f72652f55736572732f72756e6e65722f776f726b2f6b61626c652f6b61626c652f6b61626c652d636f72652f7372632f6170706c654d61696e2f6b6f746c696e2f517565756544697370... + 296
27  libdispatch.dylib             	0x1ce0c7adc _dispatch_call_block_and_release + 32 (init.c:1575)
28  libdispatch.dylib             	0x1ce0e17fc _dispatch_client_callout + 16 (client_callout.mm:85)
29  libdispatch.dylib             	0x1ce0d0468 _dispatch_lane_serial_drain + 740 (queue.c:3991)
30  libdispatch.dylib             	0x1ce0d0f44 _dispatch_lane_invoke + 388 (queue.c:4082)
31  libdispatch.dylib             	0x1ce0db3ec _dispatch_root_queue_drain_deferred_wlh + 292 (queue.c:7265)
32  libdispatch.dylib             	0x1ce0dace4 _dispatch_workloop_worker_thread + 692 (queue.c:6859)
33  libsystem_pthread.dylib       	0x1f1e883b8 _pthread_wqthread + 292 (pthread.c:2696)
34  libsystem_pthread.dylib       	0x1f1e878c0 start_wqthread + 8

Thread 0 name:
Thread 0:
0   libsystem_kernel.dylib        	0x000000024035ecd4 mach_msg2_trap + 8
1   libsystem_kernel.dylib        	0x00000002403622f8 mach_msg2_internal + 76 (mach_msg.c:201)
2   libsystem_kernel.dylib        	0x0000000240362214 mach_msg_overwrite + 428 (mach_msg.c:0)
3   libsystem_kernel.dylib        	0x000000024036205c mach_msg + 24 (mach_msg.c:323)
4   CoreFoundation                	0x00000001955e5868 __CFRunLoopServiceMachPort + 160 (CFRunLoop.c:2650)
5   CoreFoundation                	0x00000001955bc848 __CFRunLoopRun + 1188 (CFRunLoop.c:3035)
6   CoreFoundation                	0x00000001955bba6c _CFRunLoopRunSpecificWithOptions + 532 (CFRunLoop.c:3462)
7   GraphicsServices              	0x00000002371f0498 GSEventRunModal + 120 (GSEvent.c:2049)
8   UIKitCore                     	0x000000019af96df8 -[UIApplication _run] + 792 (UIApplication.m:3904)
9   UIKitCore                     	0x000000019af3fe54 UIApplicationMain + 336 (UIApplication.m:5579)
10  SwiftUI                       	0x000000019e252d20 closure #1 in KitRendererCommon(_:) + 168 (UIKitApp.swift:65)
11  SwiftUI                       	0x000000019e24f8d4 runApp<A>(_:) + 112 (UIKitApp.swift:16)
12  SwiftUI                       	0x000000019e24f3c0 static App.main() + 172 (App.swift:137)
13  Pebble                        	0x0000000104202054 static iOSApp.$main() + 28 (iOSApp.swift:0)
14  Pebble                        	0x0000000104202054 main + 40 (:-1)
15  dyld                          	0x00000001925cee28 start + 7116 (dyldMain.cpp:1477)

Thread 1 name:
Thread 1 Crashed:
0   libsystem_kernel.dylib        	0x00000002403690cc __pthread_kill + 8
1   libsystem_pthread.dylib       	0x00000001f1e8e810 pthread_kill + 268 (pthread.c:1721)
2   libsystem_c.dylib             	0x00000001a0e5fff4 __abort + 132 (abort.c:163)
3   libsystem_c.dylib             	0x00000001a0e5ff70 abort + 136 (abort.c:130)
4   libc++abi.dylib               	0x000000019267b808 __abort_message + 132 (abort_message.cpp:66)
5   libc++abi.dylib               	0x000000019266a484 demangling_terminate_handler() + 304 (cxa_default_handlers.cpp:76)
6   libobjc.A.dylib               	0x000000019257bf78 _objc_terminate() + 156 (objc-exception.mm:496)
7   FirebaseCrashlytics           	0x0000000109de23e8 FIRCLSTerminateHandler() + 332 (FIRCLSException.mm:466)
8   ComposeApp                    	0x00000001065a9bb4 (anonymous namespace)::TerminateHandler::queuedHandler()::'lambda'()::operator()() const + 24
9   ComposeApp                    	0x00000001065a9ae4 void (anonymous namespace)::$_0::operator()<(anonymous namespace)::TerminateHandler::queuedHandler()::'lambda'()>((anonymous namespace)::TerminateHandler::queuedHandler()::'lambda'()) + 48
10  ComposeApp                    	0x00000001065a9ab4 (anonymous namespace)::TerminateHandler::queuedHandler() + 12
11  ComposeApp                    	0x00000001065a9800 (anonymous namespace)::TerminateHandler::kotlinHandler() + 432
12  libc++abi.dylib               	0x000000019267abdc std::__terminate(void (*)()) + 16 (cxa_handlers.cpp:59)
13  libc++abi.dylib               	0x000000019267ab80 std::terminate() + 108 (cxa_handlers.cpp:88)
14  ComposeApp                    	0x000000010482c808 kfun:kotlin.Function1#invoke(1:0){}1:1-trampoline + 76 (Mutex.kt:114)
15  ComposeApp                    	0x000000010482c808 kfun:com.juul.kable.Connection.Connection$execute$$inlined$withLock$1.invoke#internal + 184
16  ComposeApp                    	0x000000010462680c kfun:kotlin.Function2#invoke(1:0;1:1){}1:2-trampoline + 40 (IntrinsicsNative.kt:254)
17  ComposeApp                    	0x000000010462680c kfun:kotlin.coroutines.intrinsics#startCoroutineUninterceptedOrReturn__at__kotlin.coroutines.SuspendFunction1<0:0,0:1>(0:0;kotlin.coroutines.Continuation<0:1>){0§<kotlin.Any?>;1§<kotlin.Any?>}kotli... + 204
18  ComposeApp                    	0x000000010462680c <inlined-lambda> + 208
19  ComposeApp                    	0x000000010462680c kfun:kotlin.coroutines.intrinsics.createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$2.invokeSuspend#internal + 380
20  ComposeApp                    	0x0000000104624adc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any?-trampoline + 28 (ContinuationImpl.kt:26)
21  ComposeApp                    	0x0000000104624adc <inlined-lambda> + 44
22  ComposeApp                    	0x0000000104624adc kfun:kotlin#with(0:0;kotlin.Function1<0:0,0:1>){0§<kotlin.Any?>;1§<kotlin.Any?>}0:1 + 44
23  ComposeApp                    	0x0000000104624adc kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 176
24  ComposeApp                    	0x0000000104779780 kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){}-trampoline + 72 (DispatchedTask.kt:82)
25  ComposeApp                    	0x0000000104779780 kfun:kotlin.coroutines#resume__at__kotlin.coroutines.Continuation<0:0>(0:0){0§<kotlin.Any?>} + 72
26  ComposeApp                    	0x0000000104779780 <inlined-lambda> + 708
27  ComposeApp                    	0x0000000104779780 kfun:kotlinx.coroutines#withContinuationContext(kotlin.coroutines.Continuation<*>;kotlin.Any?;kotlin.Function0<0:0>){0§<kotlin.Any?>}0:0 + 708
28  ComposeApp                    	0x0000000104779780 kfun:kotlinx.coroutines.DispatchedTask#run(){} + 908
29  ComposeApp                    	0x0000000104834370 kfun:kotlinx.coroutines.Runnable#run(){}-trampoline + 68 (QueueDispatcher.kt:17)
30  ComposeApp                    	0x0000000104834370 kfun:com.juul.kable.QueueDispatcher.QueueDispatcher$dispatch$1.invoke#internal + 72
31  ComposeApp                    	0x0000000104834370 kfun:com.juul.kable.QueueDispatcher.QueueDispatcher$dispatch$1.$<bridge-DN>invoke(){}#internal + 100
32  ComposeApp                    	0x00000001065503d8 ___636f6d2e6a75756c2e6b61626c653a6b61626c652d636f72652f55736572732f72756e6e65722f776f726b2f6b61626c652f6b61626c652f6b61626c652d636f72652f7372632f6170706c654d61696e2f6b6f746c696e2f517565756544697370... + 296
33  libdispatch.dylib             	0x00000001ce0c7adc _dispatch_call_block_and_release + 32 (init.c:1575)
34  libdispatch.dylib             	0x00000001ce0e17fc _dispatch_client_callout + 16 (client_callout.mm:85)
35  libdispatch.dylib             	0x00000001ce0d0468 _dispatch_lane_serial_drain + 740 (queue.c:3991)
36  libdispatch.dylib             	0x00000001ce0d0f44 _dispatch_lane_invoke + 388 (queue.c:4082)
37  libdispatch.dylib             	0x00000001ce0db3ec _dispatch_root_queue_drain_deferred_wlh + 292 (queue.c:7265)
38  libdispatch.dylib             	0x00000001ce0dace4 _dispatch_workloop_worker_thread + 692 (queue.c:6859)
39  libsystem_pthread.dylib       	0x00000001f1e883b8 _pthread_wqthread + 292 (pthread.c:2696)
40  libsystem_pthread.dylib       	0x00000001f1e878c0 start_wqthread + 8

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions