Skip to content

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

@GarryWalsh

Describe the bug

On iOS, Talsec.start runs synchronously on the platform (main) thread and blocks there on a keychain write — SecKeyGeneratePairSecItemAdd → 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 (SecKeyGeneratePairSecItemAdd → 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:

  1. 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
  2. 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.

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