Skip to content

fix: serialize backgroundTimeRemaining with cancellation check#667

Merged
nickolas-dimitrakas merged 5 commits intomainfrom
fix/beginBackgroundTimeCheck-loop
Mar 17, 2026
Merged

fix: serialize backgroundTimeRemaining with cancellation check#667
nickolas-dimitrakas merged 5 commits intomainfrom
fix/beginBackgroundTimeCheck-loop

Conversation

@nickolas-dimitrakas
Copy link
Contributor

@nickolas-dimitrakas nickolas-dimitrakas commented Mar 16, 2026

Background

  • A crash was occurring in beginBackgroundTimeCheckLoop on iOS 26's hardened xzone allocator (EXC_BREAKPOINT in _xzm_xzone_malloc_freelist_outlined).
  • The root cause was a TOCTOU (time-of-check-to-time-of-use) race condition: the isCancelled check and the backgroundTimeRemaining call ran on a worker thread, while the expiration handler and foreground handler cancelled the operation and ended the background task on the main thread. In the ~2ms gap between the check and the call, iOS could tear down the underlying XPC objects (RBSProcessHandle, RBSConnection), causing backgroundTimeRemaining to access freed memory.
  • PR fix: background expiration race #577 narrowed this window but did not fully close it since the check and the call were still non-atomic on different threads.

What Has Changed

  • The isCancelled check and backgroundTimeRemaining call are now performed together inside a single dispatch_sync(dispatch_get_main_queue()) block. This serializes them with the expiration handler and foreground handler (both fire on the main thread), making it impossible for the background task to be torn down between the check and the call.
  • Added testForegroundHandlerStopsBackgroundTimeRemainingCalls unit test covering the foreground handler cancellation path — the exact scenario where the race was observed in production.

Checklist

  • I have performed a self-review of my own code.
  • I have made corresponding changes to the documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have tested this locally.

Additional Notes

  • This crash occurs while the app is already in the background, so users typically don't see a crash UI — however it still terminates the process and is recorded by crash reporters.
  • iOS 26 introduced a hardened zone allocator (xzone_malloc) that catches use-after-free as EXC_BREAKPOINT rather than silently corrupting memory. The same race existed on older iOS versions but was masked by the permissive allocator.
Reported crash 1
Crashed: com.apple.uikit.backgroundTaskAssertionQueue
0  libsystem_malloc.dylib         0x30a0 _xzm_xzone_malloc_freelist_outlined + 864
1  CoreFoundation                 0x2bccc _CFXPCCreateXPCObjectFromCFObject + 296
2  RunningBoardServices           0x2118 _RBSXPCEncodeObjectForKey + 1288
3  RunningBoardServices           0x1a890 -[RBSXPCCoder encodeObject:forKey:] + 92
4  RunningBoardServices           0x3572c __44+[RBSXPCMessage messageForMethod:arguments:]_block_invoke + 212
5  RunningBoardServices           0x35534 +[RBSXPCMessage messageWithEncoder:] + 76
6  RunningBoardServices           0x35604 +[RBSXPCMessage messageForMethod:arguments:] + 172
7  RunningBoardServices           0x35800 +[RBSXPCMessage messageForMethod:varguments:] + 152
8  RunningBoardServices           0x22f1c -[RBSConnection limitationsForInstance:error:] + 156
9  RunningBoardServices           0xc538 -[RBSProcessHandle activeLimitations] + 92
10 AssertionServices              0x1714 BKSProcessAssertionBackgroundTimeRemaining + 44
11 UIKitCore                      0xf80bc ___UIApplicationBackgroundTimeRemaining_block_invoke + 24
12 libdispatch.dylib              0x1b7fc _dispatch_client_callout + 16
13 libdispatch.dylib              0x118c0 _dispatch_lane_barrier_sync_invoke_and_complete + 56
14 UIKitCore                      0xf8040 _UIApplicationBackgroundTimeRemaining + 148
15 mParticle_Apple_SDK            0x7e4e4 __59-[MPBackendController_PRIVATE beginBackgroundTimeCheckLoop]_block_invoke + 180
16 Foundation                     0x18354 <deduplicated_symbol> + 24
17 Foundation                     0x17f44 -[NSBlockOperation main] + 96
18 Foundation                     0x18074 __NSOPERATION_IS_INVOKING_MAIN__ + 16
19 Foundation                     0x17b14 -[NSOperation start] + 640
20 Foundation                     0x17edc __NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__ + 16
21 Foundation                     0x17798 __NSOQSchedule_f + 164
22 libdispatch.dylib              0x11450 _dispatch_block_async_invoke2 + 148
23 libdispatch.dylib              0x1b7fc _dispatch_client_callout + 16
24 libdispatch.dylib              0x6664 _dispatch_continuation_pop + 596
25 libdispatch.dylib              0x5cd8 _dispatch_async_redirect_invoke + 580
26 libdispatch.dylib              0x13f48 _dispatch_root_queue_drain + 364
27 libdispatch.dylib              0x146fc _dispatch_worker_thread2 + 180
28 libsystem_pthread.dylib        0x137c _pthread_wqthread + 232
29 libsystem_pthread.dylib        0x8c0 start_wqthread + 8
Reported crash 2
Crashed: com.apple.main-thread
0  libsystem_malloc.dylib         0x30a0 _xzm_xzone_malloc_freelist_outlined + 864
1  CoreFoundation                 0x274f8 dataCopyProperty + 140
2  CoreFoundation                 0x275f8 CFWriteStreamCopyProperty + 120
3  CoreFoundation                 0x2776c CFPropertyListCreateData + 240
4  Foundation                     0x988988 +[NSPropertyListSerialization dataWithPropertyList:format:options:error:] + 52
5  libperfcheck.dylib             0x2a60 pc_session_create_snapshot_buf + 600
6  SignpostMetrics                0xb80 -[SignpostMetricsSnapshotter encodeWithOSLogCoder:options:maxLength:] + 72
7  libsystem_trace.dylib          0xf844 _os_log_fmt_flatten_coder + 264
8  libsystem_trace.dylib          0x2090 _os_log_impl_flatten_and_send + 2344
9  libsystem_trace.dylib          0x5458 __os_signpost_emit_impl + 212
10 libsystem_trace.dylib          0x56b4 _os_signpost_emit_with_name_impl + 40
11 UIKitCore                      0x3db25c _logApplicationLifecycleMemoryMetricApplicationWillBeSuspended + 132
12 mParticle_Apple_SDK            0x7d84c __48-[MPBackendController_PRIVATE endBackgroundTask]_block_invoke + 108
13 libdispatch.dylib              0x1adc _dispatch_call_block_and_release + 32
14 libdispatch.dylib              0x1b7fc _dispatch_client_callout + 16
15 libdispatch.dylib              0x38b10 _dispatch_main_queue_drain.cold.5 + 812
16 libdispatch.dylib              0x10ec8 _dispatch_main_queue_drain + 180
17 libdispatch.dylib              0x10e04 _dispatch_main_queue_callback_4CF + 44
18 CoreFoundation                 0x6a2b4 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
19 CoreFoundation                 0x1db3c __CFRunLoopRun + 1944
20 CoreFoundation                 0x1ca6c _CFRunLoopRunSpecificWithOptions + 532
21 GraphicsServices               0x1498 GSEventRunModal + 120
22 UIKitCore                      0x9ddf8 -[UIApplication _run] + 792
23 UIKitCore                      0x46e54 UIApplicationMain + 336
24 ???                      0x4821b8 (Missing UUID 4c4c44ec55553144a1d504140580fce9)
25 ???                      0x80c0 main + 8 (main.swift:8)
26 ???                            0x188e42e28 (Missing)

Reference Issue (For employees only. Ignore if you are an outside contributor)

@nickolas-dimitrakas nickolas-dimitrakas requested a review from a team as a code owner March 16, 2026 19:02
@nickolas-dimitrakas nickolas-dimitrakas changed the title fix: serialize backgroundTimeRemaining with cancellation check to prevent use-after-free fix: serialize backgroundTimeRemaining with cancellation check Mar 16, 2026
@github-actions
Copy link

github-actions bot commented Mar 16, 2026

📦 SDK Size Impact Report

Measures how much the SDK adds to an app's size (with-SDK minus without-SDK).

Metric Target Branch This PR Change
App Bundle Impact 1.82 MB 1.82 MB +N/A
Executable Impact 896 bytes 896 bytes +N/A
XCFramework Size 9.49 MB 9.49 MB -4 KB

➡️ SDK size impact change is minimal.

Raw measurements

Target branch (main):

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":1944,"with_sdk_executable_size_bytes":76360,"sdk_impact_kb":1860,"sdk_executable_impact_bytes":896,"xcframework_size_kb":9720}

This PR:

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":1944,"with_sdk_executable_size_bytes":76360,"sdk_impact_kb":1860,"sdk_executable_impact_bytes":896,"xcframework_size_kb":9716}

Add regression test that asserts all backgroundTimeRemaining calls
happen on the main thread via dispatch_sync, preventing XPC race
conditions during app suspension that caused heap corruption crashes.

Made-with: Cursor
Eliminates ARC TOCTOU race between nil check and .isCancelled access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nickolas-dimitrakas nickolas-dimitrakas merged commit beccd65 into main Mar 17, 2026
15 checks passed
@nickolas-dimitrakas nickolas-dimitrakas deleted the fix/beginBackgroundTimeCheck-loop branch March 17, 2026 13:51
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.

3 participants