EXC_BAD_ACCESS on com.mapbox.threadpool: unsynchronized device-location-provider lookup in MapboxCommon when location authorization changes (24.32.0, 24.33.0)
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
Research direction
Start with the authorization-change flow described in the issue: ConfigurationServiceWorker calls the device-location-provider lookup in MapboxCommon, which iterates the LocationService provider list. The issue provides no source files or tests to run, so first trace that call path in the SDK source if available. Done means concurrent authorization callbacks no longer race on the provider list; validate the fix with the supplied forced-interleaving procedure.
Written by the indexing model from the issue text.
Description
Environment
- Xcode version: 27.1 (27A9275)
- iOS version: iOS 27.0 Simulator (arm64), on macOS 26.6.2 and macOS 27.0.1 hosts
- Devices affected: iPhone 17 Pro Max Simulator. Not yet tried on a physical device (the app is pre-release), but nothing in the code path below is Simulator-specific.
- Maps SDK Version: 11.32.0 (MapboxCommon 24.32.0, MapboxCoreMaps 11.32.0) and 11.33.0 (MapboxCommon 24.33.0, MapboxCoreMaps 11.33.0), through Swift Package Manager
Observed behavior and steps to reproduce
The app crashes on a com.mapbox.threadpool thread right after location authorization changes, for example when the user taps "Allow While Using App" in the system prompt, while a map exists. The signal varies from run to run: EXC_BAD_ACCESS with SIGBUS or SIGSEGV, a garbage PC, __shared_weak_count::lock on freed memory, or a malloc abort ("pointer being freed was not allocated"). The crashed thread is always in the same MapboxCommon function (see below).
Setup:
- Create an
OfflineManager(). We keep one alive for style packs. - Show a
MapView(SwiftUIMap) with aPuck2Dfed throughmapView.location.dataModel. Positions come from the app's ownCLLocationManager; we use none of the SDK's other location features. - Call
requestWhenInUseAuthorization()on the app's ownCLLocationManager, and grant.
Without lldb it happens on about 2% of grants on the Simulator: 3 in ~133 grants on 24.32.0 and 1 in 20 on 24.33.0. The rate goes up when the host machine is loaded. Without an authorization change it never happens: 0 crashes in 10 launches without the prompt. It reproduces both on the first-launch prompt and on a prompt raised later from a "locate me" button.
Under lldb it reproduces every time with a forced thread interleaving, described below.
Expected behavior
No crash. Concurrent authorization callbacks should not race on the LocationService's provider list.
Notes / preliminary analysis
The crashed thread, as reported without a debugger. On 24.32.0 (MapboxCommon simulator slice UUID 65BB91DB-D516-35B8-9955-DE94EF3B52B8):
EXC_BAD_ACCESS SIGBUS KERN_PROTECTION_FAILURE queue: com.mapbox.threadpool
0 MapboxCommon 0x2b01c4
1 MapboxCommon 0x2b53c0
2 MapboxCommon 0x1859d4
3 MapboxCommon 0x1855ec
4 MapboxCommon 0x1858a0
5 MapboxCommon 0x21f284
6 MapboxCommon 0x1bacdc
7 libdispatch.dylib _dispatch_call_block_and_release
8 libdispatch.dylib _dispatch_client_callout
9 libdispatch.dylib _dispatch_continuation_pop
10 libdispatch.dylib _dispatch_async_redirect_invoke
11 libdispatch.dylib _dispatch_root_queue_drain
12 libdispatch.dylib _dispatch_worker_thread2
13 libsystem_pthread.dylib _pthread_wqthread
14 libsystem_pthread.dylib start_wqthread
Frame 0 also shows up at +0x2b0100, +0x2b0148, +0x2b01d4 and +0x2b0680 in other reports. Frames 1 to 6 are always the same.
On 24.33.0 (UUID 1E3BAE8C-9719-3BF2-A1CE-407DC1FD9862) it is the same path at new addresses: 0x2b1d68 (or 0x2b1ca4, 0x2b1cec, 0x2b1d78), then 0x2b6f64, 0x18559c, 0x1851b4, 0x185468, 0x21f32c, 0x1ba8d8.
What those functions are. No dSYM ships, so we identified them by the log strings they reference. Offsets are given as 24.32.0 / 24.33.0:
0x185468/0x185030logsonPermissionStatusChanged to {}. This is theConfigurationServiceWorkerpermission handler.0x18593c/0x185504logsFailed to obtain device location provider.0x2b5364/0x2b6f08logsLocation service is already destroyed, then calls:0x2b008c/0x2b1c30, the device-location-provider lookup. It iterates and compacts a vector of{variant, weak_ptr}elements held atthis+0x48(begin, end and capacity at+0x48,+0x50and+0x58). No lock is taken anywhere in the function.
The lookup and its callers are instruction-for-instruction identical in 24.32.0 and 24.33.0.
Who calls it concurrently. On every authorization change, onPermissionStatusChanged runs once per ConfigurationServiceWorker, each on its own com.mapbox.threadpool thread, at the same time. Each handler reaches the lookup on the same LocationService. Our app has up to three workers; lldb shows these exported names:
- one from
ConfigurationService::getOrCreate, called from MapboxCoreMaps'OfflineManager::OfflineManager(); - two from
getOrCreateIfNotCleanedUp, called from theMapView'sEventsManager.
Launching with -MGLMapboxMetricsEnabled NO still leaves three workers. Between one and three handlers run on a given grant. With only one there is nothing to race, which fits the low natural rate.
Forced interleaving. We set breakpoints on the permission handler, on the lookup's loop body and on the lookup's return into its caller. When two or more handlers are running, the first one to enter the loop is suspended, and it is resumed once the others have returned. While it is held, the vector moves to a new buffer, and the held thread then crashes. Here is a 24.33.0 run with three handlers:
permission t15 (1)
permission t36 (2)
permission t4 (3)
held t15 at element 0x11e9ee300; vector [0x11e9ee300,0x11e9ee380) cap 0x11e9ee380
t4 returned (1 of 2)
t36 returned (2 of 2)
vector now [0x124306c00,0x124306d80) cap 0x124306e00
resumed t15
* thread #15, queue = 'com.mapbox.threadpool', stop reason = EXC_BAD_ACCESS (code=1, address=0x9343fd0954001780)
* frame #0: 0x00007d0954001780
frame #1: MapboxCommon`___lldb_unnamed_symbol_2b1c30 + 328
frame #2: MapboxCommon`___lldb_unnamed_symbol_2b6f08 + 92
frame #3: MapboxCommon`___lldb_unnamed_symbol_185504 + 152
frame #4: MapboxCommon`___lldb_unnamed_symbol_185030 + 388
frame #5: MapboxCommon`___lldb_unnamed_symbol_185314 + 340
frame #6: MapboxCommon`___lldb_unnamed_symbol_21eddc + 1360
frame #7: MapboxCommon`___lldb_unnamed_symbol_1ba66c + 620
frame #8: libdispatch.dylib`_dispatch_call_block_and_release + 24
Results:
| 24.32.0 | 24.33.0 | |
|---|---|---|
| Forced interleaving: crashed launches, of those where 2 or more handlers ran | 7 of 7 | 9 of 9 |
| Control: same breakpoints, but each handler waits for the previous one to return | 0 of 2 | 0 of 4 |
The lldb script (24.33.0 simulator offsets; attach with process attach --pid, then command script import force_race33.py and continue)
"""Only the threads that ran onPermissionStatusChanged (fn 0x185030) take
part. The first of them inside the device-provider lookup loop (fn 0x2b1c30,
loop body 0x2b1cb4) is suspended; once every other permission thread has
returned to fn 0x2b6f08 (0x2b6f64), the vector's bounds are read again and
the held thread resumes. x19 is `this` and x26 the current element at the
loop body, in this build."""
import time
import lldb
STATE = {"perm": set(), "held": None, "this": None, "returned": set(),
"resumed": False}
def _say(msg):
print("RACE %.3f %s" % (time.time(), msg), flush=True)
def _bounds(process, this):
err = lldb.SBError()
return tuple(process.ReadPointerFromMemory(this + off, err)
for off in (0x48, 0x50, 0x58))
def on_permission(frame, bp_loc, extra, internal_dict):
thread = frame.GetThread()
if STATE["held"] is None:
STATE["perm"].add(thread.GetThreadID())
_say("permission t%d (%d)" % (thread.GetIndexID(), len(STATE["perm"])))
return False
def in_loop(frame, bp_loc, extra, internal_dict):
thread = frame.GetThread()
tid = thread.GetThreadID()
if STATE["held"] is None and tid in STATE["perm"] and len(STATE["perm"]) >= 2:
this = frame.FindRegister("x19").GetValueAsUnsigned()
x26 = frame.FindRegister("x26").GetValueAsUnsigned()
STATE["held"], STATE["this"] = tid, this
thread.Suspend()
_say("held t%d at element 0x%x; vector [0x%x,0x%x) cap 0x%x" % (
(thread.GetIndexID(), x26) + _bounds(thread.GetProcess(), this)))
return False
def returned(frame, bp_loc, extra, internal_dict):
thread = frame.GetThread()
tid = thread.GetThreadID()
held = STATE["held"]
if held is None or STATE["resumed"] or tid == held or tid not in STATE["perm"]:
return False
STATE["returned"].add(tid)
_say("t%d returned (%d of %d)" % (thread.GetIndexID(), len(STATE["returned"]),
len(STATE["perm"]) - 1))
if len(STATE["returned"]) >= len(STATE["perm"]) - 1:
process = thread.GetProcess()
_say("vector now [0x%x,0x%x) cap 0x%x" % _bounds(process, STATE["this"]))
for t in process:
if t.GetThreadID() == held:
t.Resume()
STATE["resumed"] = True
_say("resumed t%d" % t.GetIndexID())
return False
def __lldb_init_module(debugger, internal_dict):
for addr, fn in ((0x185030, "on_permission"), (0x2b1cb4, "in_loop"),
(0x2b6f64, "returned")):
debugger.HandleCommand(
"breakpoint set -s MapboxCommon -a 0x%x -N race33_%s" % (addr, fn))
debugger.HandleCommand(
"breakpoint command add -F force_race33.%s race33_%s" % (fn, fn))
Question. Is there a supported way to avoid this until a fix ships? Creating the OfflineManager later would only remove one of the three workers. The MapView's two would remain.
We're happy to run a fix candidate under the same forced interleaving.
Additional links and references
These look different but are also crashes on authorization changes: #2236 (closed as stale: onAccuracyAuthorizationChanged on com.mapbox.common.location.sharedRunloop) and #2260 (EXC_BAD_ACCESS on a Mapbox worker thread with "Always" permission).
- Dominant language
- Swift
- Stars
- 601
- Forks
- 198
- PR merge metrics
- No merged PRs in 30d
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from mapbox/mapbox-maps-ios
-
auto-triaged bug :beetle: p3 SwiftUI
Difficulty 4/5 3-5 days Newbie friendliness 48/100
mapbox/mapbox-maps-ios#2452 · 6 reactions ·
-
auto-triaged bug :beetle: p2
Difficulty 4/5 3-5 days Newbie friendliness 42/100
mapbox/mapbox-maps-ios#2451 ·
-
auto-triaged bug :beetle: needs info p3 performance :zap:
Difficulty 5/5 Over a week Newbie friendliness 22/100
mapbox/mapbox-maps-ios#2450 · 1 comment ·
-
auto-triaged bug :beetle: p2
Difficulty 3/5 1-2 days Newbie friendliness 55/100
mapbox/mapbox-maps-ios#2448 ·
-
auto-triaged bug :beetle: p2 performance :zap:
Difficulty 3/5 1-2 days Newbie friendliness 69/100
mapbox/mapbox-maps-ios#2447 ·
All issues in mapbox/mapbox-maps-ios
Similar issues
-
Seed a JRNYAx price before the last close so asset-detail S1.3 doesn't fail from 08:00Z to the openOpenbug tests
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
lognorman20/monaco#7231 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 5 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Provenance-Emu/SkinKit#42 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
lwouis/alt-tab-macos#6154 ·
-
bug mac アプリ: コード アプリ: チャット 優先: 中 要トリアージ
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
yksr-melt/Meltype#1346 · 2 comments ·
Maintainers usually reply within 1 day