firebase / firebase/firebase-ios-sdk

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

Open Beginner friendly
#16,519 1 comment 0 reactions 0 assignees View on GitHub
api: crashlytics
Dominant language
C++
Stars
6.7k
Forks
1.8k
Avg merge
2d 18h
Merged PRs (30d)
75

Description

### 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:

```objc
[_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"`:

```sh
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`:

```objc
- (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:

```sh
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

```shell
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
```

Contributor guide

Open the contributing guide

Research direction

Start in Crashlytics/Crashlytics/Models/FIRCLSSymbolResolver.m, especially loadBinaryImagesFromFile: and its existing base-value filtering. Add the named fixture beside binary_images_with_null_base_entry.clsrecord and extend Crashlytics/UnitTests/FIRCLSSymbolResolverTests.m with the string-base case; done means malformed records are skipped without aborting and the regression test passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, objective-c
Domain
mobile-dev, observability-sre, testing-qa
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.