Repository navigation
iOS: Talsec.start blocks the platform (main) thread on a synchronous keychain/XPC call — 30 s hang and watchdog termination on iOS 26 #231
Description
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
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 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 astaskQueue:when constructingFlutterMethodChannel), sostartis dispatched off the main thread and the existing Dartawaitbecomes 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.
Activity
martinzigrai commentedon Aug 25, 2026
Hello @GarryWalsh,
Thanks for the detailed report. We'll take a closer look into it and get back to you here.
Best regards,
Martin from Talsec team
martinzigrai commentedon Sep 8, 2026
Hello @GarryWalsh,
We've looked into this. Please update to the latest version, 8.2.2 — it should improve the behavior here. Let us know how it goes; we're keeping the issue open in the meantime.
Best regards,
Martin from Talsec Team
test(ios): add RunnerTests showing Talsec.start blocks the main thread
fix(ios): run Talsec.start off the platform thread