Describe the bug
On iOS, Talsec.start runs synchronously on the platform (main) thread and blocks there on a keychain write — SecKeyGeneratePair → SecItemAdd → a synchronous XPC round trip to securityd. On iOS 26 we measured this blocking the main thread for 30+ seconds in a single launch, producing four consecutive "app hang ≥ 2000 ms" reports from our crash reporter, and (on a second device, same build) two stackless WatchdogTermination kills.
The Dart-side await Talsec.instance.start(config) cannot help: SwiftFreeraspPlugin registers its FlutterMethodChannel with the default messenger and no taskQueue:, so handle(_:result:) — and therefore Talsec.start(config:) — executes on the main thread.
I verified the bridge is unchanged in the latest release: SwiftFreeraspPlugin.start(configJson:result:) in 8.2.1 still calls Talsec.start(config:) inline (ios/freerasp/Sources/SwiftFreeraspPlugin.swift:61-73), same as 8.0.0.
Captured main-thread stack (release build, real device)
FlutterMethodChannel handler (main thread)
→ SwiftFreeraspPlugin.handle
→ SwiftFreeraspPlugin.start (SwiftFreeraspPlugin.swift:69)
→ Talsec.start(config:)
→ _dispatch_once_callout
→ SecKeyGeneratePair → SecKeyCreateRandomKey → SecItemAdd
→ SecItemAuthDoQuery → securityd_send_sync_and_do
→ xpc_connection_send_message_with_reply_sync
→ dispatch_mach_send_and_wait_for_reply
→ mach_msg2_trap ← blocked here
A second sample from the same launch reaches the identical terminal frames (SecKeyGeneratePair → SecItemAdd → sync XPC) via a UIScreen KVO/notification path, so this is not limited to the one-shot start call.
To reproduce
Not reliably reproducible on demand — it is device- and state-dependent. It appeared for us on:
- iPhone 12,8 (SE 2nd gen), iOS 26.6, thermal state
serious, first launch after install — 4 hangs across ~32 s in one launch.
- iPhone 13,2, iOS 26.5 — 2
WatchdogTermination kills, ~53 min apart, same build.
Both were first launches of a freshly installed build. Our hypothesis is that the OS kills the app before SecItemAdd completes, so the key is never persisted and the next launch repeats the generation.
Expected behavior
Talsec.start should not occupy the platform thread for the duration of a keychain/XPC round trip. Concretely, either:
- register the method channel on a background task queue (
registrar.messenger().makeBackgroundTaskQueue(), passed as taskQueue: when constructing FlutterMethodChannel), so start is dispatched off the main thread and the existing Dart await becomes meaningful; or
- have
Talsec.start(config:) perform the keychain work off the calling thread internally and return once it is scheduled.
Option 1 is a small change in this repo and does not require touching TalsecRuntime.
Please complete the following information
- Device: iPhone12,8 / iPhone13,2
- OS version: iOS 26.6 / iOS 26.5
- Version of freeRASP: 8.0.0 (TalsecRuntime 6.14.4); bridge verified unchanged in 8.2.1 (TalsecRuntime 7.1.2)
- Flutter: 3.44.1 (stable), Dart 3.12.1
- Build: release, real device, no debugger
Additional context
Related, and the reason this looks like an iOS-side gap rather than a general one: the Android equivalent (#180, "ANR — Main Thread Blocking During Talsec Initialization") was closed by reworking thread handling, and [Free-RASP-iOS#38] ("Long initialization", release build, real device, "always") was closed without a fix. The 8.1.0 note about postponed iOS subchecks suggests work in this direction — but the initial start call is still synchronous on the caller's thread, which for Flutter is always the main thread.
As a stopgap we now defer our Talsec.instance.start call until after the first frame is rasterized, which moves the block out of the iOS launch-watchdog window. It does not make the call cheap — the UI still freezes for as long as the keychain work takes.
Describe the bug
On iOS,
Talsec.startruns synchronously on the platform (main) thread and blocks there on a keychain write —SecKeyGeneratePair→SecItemAdd→ a synchronous XPC round trip tosecurityd. On iOS 26 we measured this blocking the main thread for 30+ seconds in a single launch, producing four consecutive "app hang ≥ 2000 ms" reports from our crash reporter, and (on a second device, same build) two stacklessWatchdogTerminationkills.The Dart-side
await Talsec.instance.start(config)cannot help:SwiftFreeraspPluginregisters itsFlutterMethodChannelwith the default messenger and notaskQueue:, sohandle(_:result:)— and thereforeTalsec.start(config:)— executes on the main thread.I verified the bridge is unchanged in the latest release:
SwiftFreeraspPlugin.start(configJson:result:)in 8.2.1 still callsTalsec.start(config:)inline (ios/freerasp/Sources/SwiftFreeraspPlugin.swift:61-73), same as 8.0.0.Captured main-thread stack (release build, real device)
A second sample from the same launch reaches the identical terminal frames (
SecKeyGeneratePair→SecItemAdd→ sync XPC) via aUIScreenKVO/notification path, so this is not limited to the one-shotstartcall.To reproduce
Not reliably reproducible on demand — it is device- and state-dependent. It appeared for us on:
serious, first launch after install — 4 hangs across ~32 s in one launch.WatchdogTerminationkills, ~53 min apart, same build.Both were first launches of a freshly installed build. Our hypothesis is that the OS kills the app before
SecItemAddcompletes, so the key is never persisted and the next launch repeats the generation.Expected behavior
Talsec.startshould not occupy the platform thread for the duration of a keychain/XPC round trip. Concretely, either:registrar.messenger().makeBackgroundTaskQueue(), passed astaskQueue:when constructingFlutterMethodChannel), sostartis dispatched off the main thread and the existing Dartawaitbecomes meaningful; orTalsec.start(config:)perform the keychain work off the calling thread internally and return once it is scheduled.Option 1 is a small change in this repo and does not require touching TalsecRuntime.
Please complete the following information
Additional context
Related, and the reason this looks like an iOS-side gap rather than a general one: the Android equivalent (#180, "ANR — Main Thread Blocking During Talsec Initialization") was closed by reworking thread handling, and [Free-RASP-iOS#38] ("Long initialization", release build, real device, "always") was closed without a fix. The 8.1.0 note about postponed iOS subchecks suggests work in this direction — but the initial
startcall is still synchronous on the caller's thread, which for Flutter is always the main thread.As a stopgap we now defer our
Talsec.instance.startcall until after the first frame is rasterized, which moves the block out of the iOS launch-watchdog window. It does not make the call cheap — the UI still freezes for as long as the keychain work takes.