Skip to content

[Perf] Crash in objc_loadWeakRetained via -[PAGWeakProxy respondsToSelector:]FPRProxyObjectHelper reads and ISA-swizzles a __weak delegate ivar #16469

Description

@kvukusic

Description

Firebase Performance's NSURLSession delegate instrumentation crashes with EXC_BAD_ACCESS when the delegate is Pangle's PAGWeakProxy, an NSProxy subclass that forwards through a __weak ivar.

Impact: ~250,000 affected users in the last 7 days, across all iOS versions — no OS-specific clustering. The lack of any OS correlation is itself a signal: this is a pure SDK-interaction defect, not an OS regression.

This is not a duplicate of #16254 — we are running 12.16.0, which includes the #16270 fix, and the fault has moved to a different site:

#16254 This report
Faulting frame object_getIvar_class_lookUpIvar objc_loadWeakRetained
Caller +[GULSwizzler ivarObjectsForObject:] -[PAGWeakProxy respondsToSelector:]
Cause forwarded -class gave a mismatched ivar list a __weak referent is read raw, then ISA-swizzled

#16270 corrected which class's ivar list is scanned (object_getClass instead of the forwarded -class). The enumeration now succeeds — the remaining problem is what happens to a weak referent once it has been retrieved.

Mechanism

FPRNSURLSessionInstrument.m:159 routes proxy delegates into registerProxy::

if (delegate) {
  if ([delegate isProxy] && [delegateInstrument respondsToSelector:@selector(registerProxy:)]) {
    [delegateInstrument registerProxy:delegate];   // PAGWeakProxy takes this branch

That reaches FPRIvarObjectsForProxy in FPRProxyObjectHelper.m, which reads every object-typed ivar with object_getIvar and hands matches to varFoundHandler — which ISA-swizzles them via registerObject:FPRObjectSwizzler.

PAGWeakProxy's forwarding target is a __weak ivar. Three problems follow:

  1. object_getIvar on a __weak ivar returns the raw stored pointer, bypassing objc_loadWeakRetained entirely. There is no side-table lock and no safe retain, so it can return an object that is concurrently mid-dealloc.

  2. The retrieved object is added to an NSMutableArray, which retains it — extending or resurrecting the lifetime of an object that may already be deallocating, and later releasing it from a different thread.

  3. The retrieved object gets ISA-swizzled. Replacing the isa of an object that is the referent of a live __weak reference desynchronizes the runtime's weak bookkeeping (the weakly_referenced bit lives in the isa's non-pointer bits), and -[FPRObjectSwizzler dealloc] (FPRObjectSwizzler.m:210) subsequently calls objc_disposeClassPair on the generated class.

Later, CFNetwork probes the delegate with respondsToSelector:. PAGWeakProxy resolves its __weak target, so the runtime takes the side-table lock and calls rootTryRetain() on a referent whose class and weak state are no longer coherent → EXC_BAD_ACCESS.

Because the fault happens while holding the striped side-table lock, the rest of the process wedges behind it. In our report the main thread and four other threads are blocked in __ulock_wait2 beneath objc_loadWeakRetained, objc_destroyWeak, and objc_destructInstance. This surfaces in Crashlytics as a hang as often as a crash, which may be masking the true frequency for other reporters.

Corroborating evidence that the two SDKs are interacting on the same objects: -[FPRObjectSwizzler dealloc] is executing on Pangle's own com.buMediacache.download queue at the moment of the crash (see log output below).

Suggested fix

Skip __weak ivars during proxy ivar enumeration. ivar_getTypeEncoding does not carry the ownership qualifier, but class_getWeakIvarLayout(Class) returns the weak-slot bitmap for exactly this purpose — cross-referencing it against ivar_getOffset identifies weak ivars using public runtime API only (both available since iOS 2.0). A weakly-referenced object is never a safe candidate for ISA swizzling, and reading it with object_getIvar is unsafe regardless of what is done with the result.

Secondary request: extend firebase_performance_swizzle_denylist to cover the per-instance path. It is currently only consulted in registerClassInstrumentor: (FPRInstrument.m:61), so it cannot exclude registerObject: / registerProxy: targets. Adding PAGWeakProxy to the denylist is a no-op today, which means the only available mitigation is disabling automatic instrumentation app-wide (firebase_performance_instrumentation_enabled = false) and losing all network and screen traces. A denylist that reached the per-instance path would let apps exclude one third-party proxy class and keep everything else.

Reproducing the issue

Occurs in production at high volume, but not reliably reproducible on demand — it requires a Pangle banner to be served through AdMob mediation, so it depends on live ad fill.

  1. Link FirebasePerformance with automatic instrumentation enabled (the default).
  2. Link GoogleMobileAdsMediationPangle and let AdMob mediation serve a Pangle banner.
  3. Pangle creates an NSURLSession whose delegate is a PAGWeakProxy; Firebase Performance instruments it and ISA-swizzles the proxy's weak target.
  4. Crash occurs when CFNetwork next probes the delegate with respondsToSelector:.

Firebase SDK Version

12.16.0

Xcode Version

26.4.1 (17E202)

Installation Method

Swift Package Manager

Firebase Product(s)

Performance

Targeted Platforms

iOS — all OS versions affected, no version clustering.

Relevant Log Output

Crashed: com.apple.NSURLSession-work
0  libobjc.A.dylib   0x5994    objc_loadWeakRetained + 204
1  SofascoreApp      0x2929450 -[PAGWeakProxy respondsToSelector:] + 36 (PAGWeakProxy.m:36)
2  CFNetwork         0x1045a8  <redacted> + 244
3  CFNetwork         0x92c24   <redacted> + 172
4  CFNetwork         0x75ae4   <redacted> + 40
5  libdispatch.dylib 0x1ccc    <redacted> + 32
6  libdispatch.dylib 0x1bfd4   <redacted> + 16
7  libdispatch.dylib 0xa87c    <redacted> + 644
8  libdispatch.dylib 0xb470    <redacted> + 312
9  libdispatch.dylib 0x15c98   <redacted> + 248
10 libdispatch.dylib 0x156bc   <redacted> + 640
11 libsystem_pthread.dylib 0x1238 _pthread_wqthread + 292
12 libsystem_pthread.dylib 0x910  start_wqthread + 8

Firebase Performance's swizzler deallocating on Pangle's own download queue at the same moment:

com.buMediacache.download (QOS: UNSPECIFIED)
0  libsystem_kernel.dylib   0xae8    __ulock_wait2 + 8
1  libsystem_platform.dylib 0x2058   <redacted> + 192
2  libobjc.A.dylib          0x95d4   objc_destroyWeak + 92
3  libobjc.A.dylib          0x8340   <redacted> + 116
4  libobjc.A.dylib          0x3bcc   <redacted> + 76
5  libobjc.A.dylib          0x5bb4   _objc_rootDealloc + 72
6  SofascoreApp             0x1bd6c64 -[FPRObjectSwizzler dealloc] + 210 (FPRObjectSwizzler.m:210)
7  libobjc.A.dylib          0x39f8   <redacted> + 756
8  libobjc.A.dylib          0x3bdc   <redacted> + 92
9  libobjc.A.dylib          0x5bb4   _objc_rootDealloc + 72
10 libobjc.A.dylib          0x3418   <redacted> + 204
11 libobjc.A.dylib          0x30a4   objc_autoreleasePoolPop + 244
12 Foundation               0x7a4d0  <redacted> + 1120

The main thread, blocked behind the side-table lock held by the crashing thread:

com.apple.main-thread
0  libsystem_kernel.dylib   0xae8   __ulock_wait2 + 8
1  libsystem_platform.dylib 0x2058  <redacted> + 192
2  libobjc.A.dylib          0x593c  objc_loadWeakRetained + 116
3  libobjc.A.dylib          0x75f4  objc_loadWeak + 24
4  WebKit                   0xc6e20 <redacted> + 28
5  WebKit                   0x1b84d0 <redacted> + 84
...

Two further threads blocked the same way: com.apple.CFNetwork.LoaderQ under objc_destroyWeak,
and com.dt.iasdkcore.dtxtask.sharedDeliveryQueue under objc_destructInstance.

If using Swift Package Manager, the project's Package.resolved

Relevant pins:

{
  "identity" : "firebase-ios-sdk",
  "location" : "https://github.com/firebase/firebase-ios-sdk.git",
  "state" : { "revision" : "33a468adfdb75b53f05a37e7c886ca7c962b5c17", "version" : "12.17.0" }
},
{
  "identity" : "adsglobalpackage",
  "location" : "https://github.com/bytedance/AdsGlobalPackage.git",
  "state" : { "revision" : "e2d9f0850c7f9ce6c3d1256208ca0c00418dd259", "version" : "8.1.1-release.1" }
},
{
  "identity" : "swift-package-manager-google-mobile-ads",
  "location" : "https://github.com/googleads/swift-package-manager-google-mobile-ads.git",
  "state" : { "revision" : "868149fc50d1452078e83540e636cf519bac3e6a", "version" : "13.7.0" }
}

Pangle adapter: PangleAdapter-8.1.1.1.0 (binary target).

Note that #16254 reporters were on Pangle 7.9.x and 9.1.1.x while we are on 8.1.1 — the proxy
pattern appears stable across Pangle versions, so this is unlikely to be fixed on their side.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions