Description
FIRCLSSymbolResolver aborts the process while Crashlytics is processing a pending
report at launch, if that report's binary_images.clsrecord contains a load record
whose base value is neither a number nor null.
loadBinaryImagesFromFile: filters out records with a missing base/size
(FIRCLSSymbolResolver.m:65-67) and records where either is NSNull
(FIRCLSSymbolResolver.m:69-72). Anything else that came out of NSJSONSerialization
is passed straight into the sort comparator:
[_binaryImages sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSNumber* base1 = [obj1 objectForKey:@"base"]; // FIRCLSSymbolResolver.m:78
NSNumber* base2 = [obj2 objectForKey:@"base"]; // :79
return [base1 compare:base2]; // :81
}];
When one base is an NSNumber and another is an NSString,
-[__NSCFNumber compare:] sends objCType to the argument and the process dies with
an uncaught NSInvalidArgumentException. It happens on Crashlytics' own operation
queue, so an app has no way to catch it.
Expected: a record with a malformed base is skipped, exactly like the NSNull
and missing-key cases already are.
Actual: SIGABRT.
This is the same defect class that testLoadingBinaryImagesWithNullBaseValue and
testLoadingBinaryImagesWithMissingBaseValue were added for — a non-NSNumber,
non-NSNull base is simply a third shape the guard does not cover. The existing
binary_images_with_null_base_entry.clsrecord fixture is itself evidence that base
really does come back non-numeric from real devices.
Impact. The report being processed has already been moved to processing/ at this
point (FIRCLSReportUploader.m:110-113), and processing/ is retried with
withProcessing:NO on the next launch, so the original report is not lost — it is just
delivered late and without on-device symbolication. The visible damage is that
Crashlytics records and reports its own abort as an application crash
(Crashed: com.google.firebase.crashlytics.ios.exception), which misattributes a crash
to the app and skews crash-free-users.
Possibly related. FIRCLSFileWriteString (FIRCLSFile.m:327-370) writes string
values with no JSON escaping — it wraps the raw bytes in quotes and nothing else.
path, bundle_id, build_version and display_version all go through it, and the
last three are read from the Info.plist of loaded bundles. A " in any of them
re-shapes the record, and a newline splits it, since FIRCLSFileReadSections treats
\n as the record separator. I can't claim this is how our file got a string base —
I have no copy of it — but it is one deterministic way to produce a record that parses
as valid JSON with the wrong value types.
I have a fix and a regression test ready and will open a PR against main if the
approach looks right.
Reproducing the issue
Minimal fixture — your own binary_images_with_null_base_entry.clsrecord with a single
substitution on the last record, "base":null → "base":"4390797312":
sed 's/"base":null/"base":"4390797312"/' \
Crashlytics/UnitTests/Data/binary_images_with_null_base_entry.clsrecord \
> Crashlytics/UnitTests/Data/binary_images_with_string_base_entry.clsrecord
and a test alongside the two existing ones in
Crashlytics/UnitTests/FIRCLSSymbolResolverTests.m:
- (void)testLoadingBinaryImagesWithStringBaseValue {
FIRCLSSymbolResolver* resolver = [[FIRCLSSymbolResolver alloc] init];
NSString* binaryImagePath =
[self pathForResource:@"binary_images_with_string_base_entry.clsrecord"];
XCTAssert([resolver loadBinaryImagesFromFile:binaryImagePath]);
}
This does not fail the assertion — it aborts the test process.
I verified against 12.14.0 and against main (FIRCLSSymbolResolver.m is byte-identical
between them) by compiling the real sources into a standalone host binary — no app, no
simulator — which reproduces in about a second:
clang -fobjc-arc -fmodules -I . -o clsrepro main.m \
Crashlytics/Crashlytics/Models/FIRCLSSymbolResolver.m \
Crashlytics/Crashlytics/Helpers/FIRCLSFile.m \
Crashlytics/Crashlytics/FIRStackFrame.m \
Crashlytics/Shared/FIRCLSByteUtility.m \
stubs.m -framework Foundation
where main.m builds the record file through your own writer API
(FIRCLSFileWriteSectionStart / FIRCLSFileWriteHashEntryString /
FIRCLSFileWriteHashEntryUint64, mirroring FIRCLSBinaryImageRecordSlice) and then
calls loadBinaryImagesFromFile:. Results:
| records |
base of one record |
outcome |
| 8 |
all uint64 |
exit 0 |
| 8 |
key omitted |
exit 0 (filtered at :65-67) |
| 8 |
string |
SIGABRT (134) |
| 1 |
string |
exit 0 — sortUsingComparator: never invokes the block |
At least two records are required, which matches the deep __CFSimpleMergeSort
recursion seen in the field.
Firebase SDK Version
12.14.0 (the comparator is unchanged on main as of today, so 12.17.0 is affected too)
Xcode Version
27.0
Installation Method
Swift Package Manager
Firebase Product(s)
Crashlytics
Targeted Platforms
iOS
Relevant Log Output
Fatal Exception: NSInvalidArgumentException
-[__NSCFString objCType]: unrecognized selector sent to instance 0x1130c06c0
0 CoreFoundation __exceptionPreprocess
1 libobjc.A.dylib objc_exception_throw
2 CoreFoundation +[NSObject(NSObject) _copyDescription]
3 CoreFoundation ___forwarding___
4 CoreFoundation _CF_forwarding_prep_0
5 CoreFoundation -[__NSCFNumber compare:]
6 MyApp __49-[FIRCLSSymbolResolver loadBinaryImagesFromFile:]_block_invoke + 81 (FIRCLSSymbolResolver.m:81)
7 CoreFoundation __CFSimpleMergeSort
... (10 more __CFSimpleMergeSort frames)
17 CoreFoundation CFSortIndexes
18 CoreFoundation -[NSMutableArray sortRange:options:usingComparator:]
19 MyApp -[FIRCLSSymbolResolver loadBinaryImagesFromFile:] + 77 (FIRCLSSymbolResolver.m:77)
20 MyApp -[FIRCLSProcessReportOperation main] + 104 (FIRCLSProcessReportOperation.m:104)
21 Foundation __NSOPERATION_IS_INVOKING_MAIN__
22 Foundation -[NSOperation start]
23 MyApp __91-[FIRCLSReportUploader prepareAndSubmitReport:dataCollectionToken:asUrgent:withProcessing:]_block_invoke + 127 (FIRCLSReportUploader.m:127)
24 MyApp FIRCLSApplicationActivity + 205 (FIRCLSApplication.m:205)
25 MyApp -[FIRCLSReportUploader prepareAndSubmitReport:dataCollectionToken:asUrgent:withProcessing:] + 164 (FIRCLSReportUploader.m:164)
26 MyApp __92-[FIRCLSExistingReportManager processExistingActiveReportPath:dataCollectionToken:asUrgent:]_block_invoke_2 + 244 (FIRCLSExistingReportManager.m:244)
27 Foundation __NSINDEXSET_IS_CALLING_OUT_TO_A_BOOL_BLOCK__
28 Foundation -[NSBlockOperation main]
29 Foundation __NSOPERATION_IS_INVOKING_MAIN__
30 Foundation -[NSOperation start]
31 Foundation __NSOPERATIONQUEUE_IS_STARTING_AN_OPERATION__
32 Foundation __NSOQSchedule_f
33 libdispatch _dispatch_block_async_invoke2
--- reproduction on 12.14.0, frame for frame ---
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[__NSCFString objCType]: unrecognized selector sent to instance 0x105441ca0'
3 CoreFoundation ___forwarding___ + 1504
4 CoreFoundation __forwarding_prep_0___ + 96
5 CoreFoundation -[__NSCFNumber compare:] + 52
6 clsrepro __49-[FIRCLSSymbolResolver loadBinaryImagesFromFile:]_block_invoke + 172
7 CoreFoundation __CFSimpleMergeSort + 100
Description
FIRCLSSymbolResolveraborts the process while Crashlytics is processing a pendingreport at launch, if that report's
binary_images.clsrecordcontains aloadrecordwhose
basevalue is neither a number nornull.loadBinaryImagesFromFile:filters out records with a missingbase/size(
FIRCLSSymbolResolver.m:65-67) and records where either isNSNull(
FIRCLSSymbolResolver.m:69-72). Anything else that came out ofNSJSONSerializationis passed straight into the sort comparator:
When one
baseis anNSNumberand another is anNSString,-[__NSCFNumber compare:]sendsobjCTypeto the argument and the process dies withan uncaught
NSInvalidArgumentException. It happens on Crashlytics' own operationqueue, so an app has no way to catch it.
Expected: a record with a malformed
baseis skipped, exactly like theNSNulland missing-key cases already are.
Actual:
SIGABRT.This is the same defect class that
testLoadingBinaryImagesWithNullBaseValueandtestLoadingBinaryImagesWithMissingBaseValuewere added for — a non-NSNumber,non-
NSNullbaseis simply a third shape the guard does not cover. The existingbinary_images_with_null_base_entry.clsrecordfixture is itself evidence thatbasereally does come back non-numeric from real devices.
Impact. The report being processed has already been moved to
processing/at thispoint (
FIRCLSReportUploader.m:110-113), andprocessing/is retried withwithProcessing:NOon the next launch, so the original report is not lost — it is justdelivered late and without on-device symbolication. The visible damage is that
Crashlytics records and reports its own abort as an application crash
(
Crashed: com.google.firebase.crashlytics.ios.exception), which misattributes a crashto the app and skews crash-free-users.
Possibly related.
FIRCLSFileWriteString(FIRCLSFile.m:327-370) writes stringvalues with no JSON escaping — it wraps the raw bytes in quotes and nothing else.
path,bundle_id,build_versionanddisplay_versionall go through it, and thelast three are read from the
Info.plistof loaded bundles. A"in any of themre-shapes the record, and a newline splits it, since
FIRCLSFileReadSectionstreats\nas the record separator. I can't claim this is how our file got a stringbase—I have no copy of it — but it is one deterministic way to produce a record that parses
as valid JSON with the wrong value types.
I have a fix and a regression test ready and will open a PR against
mainif theapproach looks right.
Reproducing the issue
Minimal fixture — your own
binary_images_with_null_base_entry.clsrecordwith a singlesubstitution on the last record,
"base":null→"base":"4390797312":and a test alongside the two existing ones in
Crashlytics/UnitTests/FIRCLSSymbolResolverTests.m:This does not fail the assertion — it aborts the test process.
I verified against 12.14.0 and against
main(FIRCLSSymbolResolver.mis byte-identicalbetween them) by compiling the real sources into a standalone host binary — no app, no
simulator — which reproduces in about a second:
clang -fobjc-arc -fmodules -I . -o clsrepro main.m \ Crashlytics/Crashlytics/Models/FIRCLSSymbolResolver.m \ Crashlytics/Crashlytics/Helpers/FIRCLSFile.m \ Crashlytics/Crashlytics/FIRStackFrame.m \ Crashlytics/Shared/FIRCLSByteUtility.m \ stubs.m -framework Foundationwhere
main.mbuilds the record file through your own writer API(
FIRCLSFileWriteSectionStart/FIRCLSFileWriteHashEntryString/FIRCLSFileWriteHashEntryUint64, mirroringFIRCLSBinaryImageRecordSlice) and thencalls
loadBinaryImagesFromFile:. Results:baseof one recorduint64sortUsingComparator:never invokes the blockAt least two records are required, which matches the deep
__CFSimpleMergeSortrecursion seen in the field.
Firebase SDK Version
12.14.0 (the comparator is unchanged on
mainas of today, so 12.17.0 is affected too)Xcode Version
27.0
Installation Method
Swift Package Manager
Firebase Product(s)
Crashlytics
Targeted Platforms
iOS
Relevant Log Output