Skip to content

[Bug]: iOS cold-start: scene-update watchdog (0x8BADF00D) due to dispatch_once race in +[TSLocationRequestService sharedInstance] #2593

@LudoLamerre

Description

@LudoLamerre

Required Reading

  • Confirmed

Plugin Version

5.1.1 (pod TSLocationManager 4.1.3)

Mobile operating-system(s)

  • iOS
  • Android

Device Manufacturer(s) and Model(s)

Apple iPhone 15 Pro (iPhone15,4)

Device operating-systems(s)

iOS 26.4.2 (build 23E261)

React Native / Expo version

React Native 0.81.5 Expo SDK 54.0.32 (bare workflow, no managed)

What happened?

Summary

iOS scene-update watchdog kills the app (SIGKILL, 0x8BADF00D) ~10 seconds after process launch in background. Reproducible at cold-start of a TestFlight build when JS code calls getCurrentPosition() very shortly after ready() resolves.

Root cause (per symbolicated crash report)

a deadlock between the native TSAppState.onEnterForeground observer (running on the main thread, triggered by UIApplicationDidBecomeActiveNotification) and a JS-initiated getCurrentPosition() call (running on the TurboModule queue). Both paths race for the lazy +sharedInstance dispatch_once of TSLocationRequestService:

  • Thread 0 (main, crashed): observer chain TSAppState.onEnterForeground TSLocationManager.ready_block_invokeTSLocationManager.doStart:TSTrackingService.start: TSTrackingService.changePace: +[TSLocationRequestService sharedInstance]_dispatch_once_wait (blocked, waiting for the dispatch_once block to complete).
  • Thread 6 (com.meta.react.turbomodulemanager.queue): RNBackgroundGeolocation getCurrentPosition:resolve:reject: (RNBackgroundGeolocation.mm:361) → TSLocationManager.getCurrentPosition: TSTrackingService.getCurrentPosition:+[TSLocationRequestService sharedInstance] _dispatch_once_callout (inside the dispatch_once block) → _dispatch_sync_f_slow (blocked, waiting on another queue while inside the once block).

Same image offset (+10395088 from the main app binary, symbolicated to +[TSLocationRequestService sharedInstance]) appears in both stacks. The main thread is waiting for the dispatch_once that the TurboModule queue is inside, and the TurboModule queue is dispatch_sync’ing to a queue that ultimately needs the main thread → classic circular wait. iOS scene-update watchdog fires at ~10s and kills the process.

Expected behavior

Initializing the TSLocationRequestService shared instance from one thread should not block on the main thread when a concurrent getCurrentPosition() call enters the same dispatch_once block from another thread, especially during a foreground transition where the SDK’s own observer also accesses the singleton.

Reproduction context

The deadlock is timing-sensitive and only triggers when the singleton’s dispatch_once has not yet executed for the process (i.e. cold-start). Once it has executed, subsequent accesses bypass the dispatch_once and no race is possible.

In our app we had a JS-side trigger that called getCurrentPosition() immediately after BackgroundGeolocation.ready() resolved (cold-start chain). This collided with the SDK's own TSAppState.onEnterForeground observer firing in the same window (since ready() is followed almost immediately by the scene becoming active). We mitigated by throttling our getCurrentPosition() calls to one per 1500 ms and removing the immediate post-ready() call. The crash disappeared, but the underlying race in the singleton init is still present in the SDK.

Thank you for your help !

Plugin Code and/or Config

const TRACKING_CONFIG: Config = {
  reset: true,
  geolocation: {
    desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
    distanceFilter: 10,
    allowIdenticalLocations: false,
    stationaryRadius: 25,
    stopTimeout: 5,
    elasticityMultiplier: 1.2,
    activityType: BackgroundGeolocation.ActivityType.Other,
    showsBackgroundLocationIndicator: true,
    disableLocationAuthorizationAlert: true,
    locationAuthorizationRequest: 'Always',
  },
  activity: {
    activityRecognitionInterval: 10000,
    minimumActivityRecognitionConfidence: 50,
    stopOnStationary: false,
  },
  app: {
    stopOnTerminate: false,
    startOnBoot: true,
    enableHeadless: true,
    heartbeatInterval: 60,
    preventSuspend: true,
    notification: { /* ... */ },
  },
  http: {
    url: 'https://api.<env>.example.fr/locations',
    headers: { 'X-App-Key': '...' },
    rootProperty: 'locations',
    autoSync: true,
    batchSync: true,
    maxBatchSize: 50,
    autoSyncThreshold: 10,
  },
  persistence: { maxDaysToPersist: 60 },
  logger: { debug: false, logLevel: BackgroundGeolocation.LogLevel.Verbose },
};

// Cold-start call site that triggered the race (now removed in our codebase):
useEffect(() => {
  if (!trackingEnabled || !isInitDone || hasRunColdStart.current) return;
  hasRunColdStart.current = true;
  // This call was being made within ~1ms of BackgroundGeolocation.ready() resolving.
  await BackgroundGeolocation.getCurrentPosition({
    extras: { event: 'foreground' },
    persist: true,
    timeout: 15,
    samples: 1,
    maximumAge: 5000,
  });
  await BackgroundGeolocation.sync();
}, [trackingEnabled, isInitDone]);

Relevant log output

Exception Type:  EXC_CRASH (SIGKILL)
Termination Reason: FRONTBOARD 2343432205
  domain:10 code:0x8BADF00D
  explanation: scene-update watchdog transgression: ...is stuck (deadlock)
ProcessVisibility: Background
ProcessState: Running
WatchdogEvent: scene-update
WatchdogCPUStatistics:
  Elapsed total CPU time (seconds): 7.470 (user 4.300, system 3.170), 23% CPU
  Elapsed application CPU time (seconds): 0.008, 0% CPU
Process: Movely [16722]
Launch Time:  2026-05-21 17:55:07.1536
Crash Time:   2026-05-21 17:55:16.8006
(~9.6 seconds after launch)
Thread 0 name:   Dispatch queue: com.apple.main-thread  (TRIGGERED)
Thread 0 Crashed:
0   libsystem_kernel.dylib       __ulock_wait + 8
1   libdispatch.dylib            _dlock_wait + 56
2   libdispatch.dylib            _dispatch_once_wait.cold.1 + 148
3   libdispatch.dylib            _dispatch_once_wait + 60
4   Movely                       +[TSLocationRequestService sharedInstance] + 132
5   Movely                       -[TSTrackingService changePace:] + 520
6   Movely                       -[TSTrackingService start:] + 1444
7   Movely                       -[TSLocationManager doStart:] + 48
8   Movely                       __26-[TSLocationManager ready]_block_invoke + 728
9   Movely                       -[TSAppState onEnterForeground] + 532
10  CoreFoundation               __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 148
11  CoreFoundation               ___CFXRegistrationPost_block_invoke + 92
12  CoreFoundation               _CFXRegistrationPost + 440
13  CoreFoundation               _CFXNotificationPost + 736
14  Foundation                   -[NSNotificationCenter postNotificationName:object:userInfo:] + 92
15  UIKitCore                    -[UIApplication _stopDeactivatingForReason:] + 1436
... (UIKit scene update chain) ...
Thread 6 name:   Dispatch queue: com.meta.react.turbomodulemanager.queue
Thread 6:
0   libsystem_kernel.dylib       __ulock_wait + 8
1   libdispatch.dylib            _dispatch_thread_main_event_wait_slow + 76
2   libdispatch.dylib            __DISPATCH_WAIT_FOR_QUEUE__ + 464
3   libdispatch.dylib            _dispatch_sync_f_slow + 140
4   Movely                       -[TSLocationRequestService initWithLocationManager:] + 284
5   Movely                       -[TSLocationRequestService init] + 172
6   Movely                       __42+[TSLocationRequestService sharedInstance]_block_invoke + 76
7   libdispatch.dylib            _dispatch_client_callout + 16
8   libdispatch.dylib            _dispatch_once_callout + 32
9   Movely                       +[TSLocationRequestService sharedInstance] + 132
10  Movely                       -[TSTrackingService getCurrentPosition:] + 576
11  Movely                       -[TSLocationManager getCurrentPosition:] + 60
12  Movely                       -[RNBackgroundGeolocation getCurrentPosition:resolve:reject:] (RNBackgroundGeolocation.mm:361)
13  CoreFoundation               -[NSInvocation invoke] + 424
14  React                        facebook::react::ObjCTurboModule::performMethodInvocation(...)
... (TurboModule callout chain) ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions