Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

EXC_BAD_ACCESS on com.mapbox.threadpool: unsynchronized device-location-provider lookup in MapboxCommon when location authorization changes (24.32.0, 24.33.0)

Open
#2,449 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
swift
Domain
backend, mobile

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

auto-triaged bug :beetle: p1

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:

  1. Create an OfflineManager(). We keep one alive for style packs.
  2. Show a MapView (SwiftUI Map) with a Puck2D fed through mapView.location.dataModel. Positions come from the app's own CLLocationManager; we use none of the SDK's other location features.
  3. Call requestWhenInUseAuthorization() on the app's own CLLocationManager, 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 / 0x185030 logs onPermissionStatusChanged to {}. This is the ConfigurationServiceWorker permission handler.
  • 0x18593c / 0x185504 logs Failed to obtain device location provider.
  • 0x2b5364 / 0x2b6f08 logs Location 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 at this+0x48 (begin, end and capacity at +0x48, +0x50 and +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 the MapView's EventsManager.

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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from mapbox/mapbox-maps-ios

All issues in mapbox/mapbox-maps-ios

Similar issues

More Swift issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.