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:
-
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.
-
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.
-
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.
- Link
FirebasePerformance with automatic instrumentation enabled (the default).
- Link
GoogleMobileAdsMediationPangle and let AdMob mediation serve a Pangle banner.
- Pangle creates an
NSURLSession whose delegate is a PAGWeakProxy; Firebase Performance instruments it and ISA-swizzles the proxy's weak target.
- 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.
Description
Firebase Performance's
NSURLSessiondelegate instrumentation crashes withEXC_BAD_ACCESSwhen the delegate is Pangle'sPAGWeakProxy, anNSProxysubclass that forwards through a__weakivar.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:
object_getIvar→_class_lookUpIvarobjc_loadWeakRetained+[GULSwizzler ivarObjectsForObject:]-[PAGWeakProxy respondsToSelector:]-classgave a mismatched ivar list__weakreferent is read raw, then ISA-swizzled#16270 corrected which class's ivar list is scanned (
object_getClassinstead 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:159routes proxy delegates intoregisterProxy::That reaches
FPRIvarObjectsForProxyinFPRProxyObjectHelper.m, which reads every object-typed ivar withobject_getIvarand hands matches tovarFoundHandler— which ISA-swizzles them viaregisterObject:→FPRObjectSwizzler.PAGWeakProxy's forwarding target is a__weakivar. Three problems follow:object_getIvaron a__weakivar returns the raw stored pointer, bypassingobjc_loadWeakRetainedentirely. There is no side-table lock and no safe retain, so it can return an object that is concurrently mid-dealloc.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.The retrieved object gets ISA-swizzled. Replacing the
isaof an object that is the referent of a live__weakreference desynchronizes the runtime's weak bookkeeping (theweakly_referencedbit lives in the isa's non-pointer bits), and-[FPRObjectSwizzler dealloc](FPRObjectSwizzler.m:210) subsequently callsobjc_disposeClassPairon the generated class.Later, CFNetwork probes the delegate with
respondsToSelector:.PAGWeakProxyresolves its__weaktarget, so the runtime takes the side-table lock and callsrootTryRetain()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_wait2beneathobjc_loadWeakRetained,objc_destroyWeak, andobjc_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 owncom.buMediacache.downloadqueue at the moment of the crash (see log output below).Suggested fix
Skip
__weakivars during proxy ivar enumeration.ivar_getTypeEncodingdoes not carry the ownership qualifier, butclass_getWeakIvarLayout(Class)returns the weak-slot bitmap for exactly this purpose — cross-referencing it againstivar_getOffsetidentifies 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 withobject_getIvaris unsafe regardless of what is done with the result.Secondary request: extend
firebase_performance_swizzle_denylistto cover the per-instance path. It is currently only consulted inregisterClassInstrumentor:(FPRInstrument.m:61), so it cannot excluderegisterObject:/registerProxy:targets. AddingPAGWeakProxyto 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.
FirebasePerformancewith automatic instrumentation enabled (the default).GoogleMobileAdsMediationPangleand let AdMob mediation serve a Pangle banner.NSURLSessionwhose delegate is aPAGWeakProxy; Firebase Performance instruments it and ISA-swizzles the proxy's weak target.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
Firebase Performance's swizzler deallocating on Pangle's own download queue at the same moment:
The main thread, blocked behind the side-table lock held by the crashing thread:
Two further threads blocked the same way:
com.apple.CFNetwork.LoaderQunderobjc_destroyWeak,and
com.dt.iasdkcore.dtxtask.sharedDeliveryQueueunderobjc_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.