Skip to content

Crashlytics aborts in FIRCLSSymbolResolver when a binary image record has a non-numeric base #16519

Description

@kondratyevdev

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

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