Skip to content

fix(ios): run Talsec.start off the platform thread - #239

Merged
tompsota merged 3 commits into
talsec:masterfrom
shoaib-akhtar:fix/ios-start-off-main-thread
Nov 9, 2026
Merged

tompsota merged 3 commits into
talsec:masterfrom
shoaib-akhtar:fix/ios-start-off-main-thread

Conversation

@shoaib-akhtar

@shoaib-akhtar shoaib-akhtar commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #231

Problem

SwiftFreeraspPlugin handles the start method call on the platform (main) thread and calls Talsec.start(config:) inline. Talsec.start generates a key through SecKeyGeneratePair → SecItemAdd, which is a synchronous XPC round trip to securityd. On iOS 26 that round trip has been measured blocking the main thread for 30+ seconds, which causes app-hang reports and WatchdogTermination kills on first launch (details in #231). Because the work runs inline, the Dart-side await Talsec.instance.start(config) gives no relief.

Change

  • Talsec.start now runs on a serial background queue (talsec.app/freerasp/start, .userInitiated).
  • The Flutter result is delivered on the main thread after Talsec.start returns, so await Talsec.instance.start(...) still means "the SDK has started".
  • Config decoding errors are still returned straight away, as before.
  • The other method channel calls are unchanged. blockScreenCapture/isScreenCaptureBlocked already hop to main.

This matches how the React Native bridge already calls Talsec.start: RN module methods run on a background method queue, so running the SDK start off-main has precedent in production.

I went with an explicit queue rather than a channel taskQueue: (option 1 in the issue) so that only start moves off the main thread and the change can be unit-tested directly.

Tests (written first)

The repo had no native iOS tests, so this adds a RunnerTests XCTest target to the example app (example/ios) and a small seam (talsecStarter) so tests can observe the call without running TalsecRuntime.

The commits are ordered red → green:

  1. test(ios): …: the tests, run against the unchanged bridge:
    • testStartRunsTalsecOffTheMainThread: fails (Optional(true) is not equal to Optional(false))
    • testStartReturnsBeforeTalsecFinishes: fails (.timedOut, because handle blocked until start finished)
    • testStartRepliesOnTheMainThreadAfterTalsecStarts: passes (guard)
    • testStartWithInvalidConfigReturnsErrorWithoutStartingTalsec: passes (guard)
  2. fix(ios): …: all 4 pass.

Run with:

cd example && flutter pub get && cd ios && pod install
xcodebuild test -workspace Runner.xcworkspace -scheme Runner \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -only-testing:RunnerTests

Verified on Xcode 26.1.1, iOS 26.1 simulator, Flutter 3.38.2. As an extra check, I also called the real TalsecRuntime start through the patched plugin from a test. It started off-main, replied, and did not crash.

Not covered

The issue also shows the same keychain path being reached from inside TalsecRuntime through a UIScreen KVO/notification callback. That code lives in the closed-source xcframework, so this PR can't change it.

@shoaib-akhtar
shoaib-akhtar force-pushed the fix/ios-start-off-main-thread branch from 493b271 to bdc502d Compare October 14, 2026 10:14
@shoaib-akhtar

shoaib-akhtar commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @tompsota, any chance you could take a look at this? It's the fix for the 30s main thread hang on iOS 26 from #231. It moves only start off the platform thread and still replies after the SDK has started. Let me know if you want anything changed.

shoaib-akhtar and others added 3 commits November 9, 2026 17:44
Adds an XCTest target to the example app and a seam (`talsecStarter`) so
the plugin's call to Talsec.start can be observed without running
TalsecRuntime. The two threading tests fail against the current bridge:
start runs inline on the platform thread and blocks handle(_:result:).

Refs talsec#231
Talsec.start generates a key over a synchronous keychain/XPC call. Called
from the method channel handler it ran on the main thread, which on iOS 26
hung the UI for 30+ s and led to watchdog terminations. Start now runs on
a serial background queue and the Flutter result is delivered on the main
thread once it returns, so `await Talsec.instance.start(...)` keeps its
meaning.

Fixes talsec#231
…ests

The RunnerTests target linked Foundation through a path pinned to the
iPhoneOS18.0 SDK inside the developer directory, which no longer exists
with Xcode 26. Swift links Foundation automatically, so the reference,
its build file and its group are removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@tompsota
tompsota force-pushed the fix/ios-start-off-main-thread branch from bdc502d to d2af1e5 Compare November 9, 2026 16:47
@tompsota

tompsota commented Nov 9, 2026

Copy link
Copy Markdown
Member

Thanks @shoaib-akhtar, this is a clean fix and the test-first commits made it easy to review. We're merging it.

Before merging, I pushed a few small updates to your branch:

  • Rebased onto 8.3.0 and resolved the CHANGELOG conflict. Your [Unreleased] entry now sits above [8.3.0].
  • Removed the Foundation.framework link from the RunnerTests target. It pointed to the iOS 18.0 SDK path, which doesn't exist in newer Xcode versions, and Swift links Foundation automatically.

On expectations: this change moves start off the platform thread, which removes the block you measured on that call. Not all of the SDK's startup work goes through this call, though, so it may not remove every hang reported in #231. We'll look at the rest on the native SDK side and update #231 when there's more.

@tompsota
tompsota merged commit 3a6f3d9 into talsec:master Nov 9, 2026
6 checks passed
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.

iOS: Talsec.start blocks the platform (main) thread on a synchronous keychain/XPC call — 30 s hang and watchdog termination on iOS 26

2 participants